在上一章中我们打通了 AI 对话的核心链路实现了基础的流式聊天和 Markdown 渲染。但这距离一个可以交付给真实用户的“企业级产品”还有很长的路要走。在企业级应用中体验即生产力。用户不会容忍一个每次刷新页面就丢失聊天记录、无法停止生成、代码无法一键复制的 AI 助手。本章我们不增加新的 AI 能力而是专注于“打磨体验”将零散的功能点整合成一套生产级的聊天系统。1. 为什么需要企业级聊天体验AI 聊天界面的状态管理远比传统表单复杂。我们需要处理以下核心痛点状态持久化浏览器刷新后历史对话不能丢失。交互控制当 AI 回答过长或方向错误时用户需要随时“踩刹车”停止生成或“重头再来”重新生成。视觉反馈AI 正在思考、正在调用工具、正在输出时UI 需要有明确的视觉状态。阅读体验长代码需要一键复制长对话需要自动滚动到底部深色模式需要无缝切换。2. 全局状态管理与聊天体验架构为了支撑这些体验我们需要对现有的状态管理进行升级。设计思考:Vercel AI SDK 的useChat已经内置了stop()和reload()方法我们不需要自己手写 AbortController 或重新发送请求。但为了实现聊天历史持久化我们需要结合 Zustand 或 React Context 将messages与数据库同步。3. 核心体验功能实现3.1 停止生成与重新生成这是 AI 聊天中最基础的控制能力。// src/components/chat/ChatControls.tsximport{Button}fromheroui/react;import{useChat}fromai/react;exportfunctionChatControls({stop,reload,isLoading}:PickReturnTypetypeofuseChat,stop|reload|isLoading){return(div classNameflex gap-2 mb-2{/* 停止生成仅在流式输出时显示 */}{isLoading(Button sizesmcolordangervariantflatonPress{stop}停止生成/Button)}{/* 重新生成仅在流式输出结束后显示 */}{!isLoading(Button sizesmcolorprimaryvariantflatonPress{reload}重新生成/Button)}/div);}3.2 代码块一键复制AI 生成的代码往往很长手动选中复制体验极差。我们需要在 Markdown 渲染组件中加入复制按钮。// src/components/chat/CodeBlock.tsximport{useState}fromreact;import{Button}fromheroui/react;exportfunctionCodeBlock({children,className}:{children:string;className?:string}){const[copied,setCopied]useState(false);consthandleCopyasync(){awaitnavigator.clipboard.writeText(children);setCopied(true);setTimeout(()setCopied(false),2000);};return(div classNamerelative groupButton sizesmclassNameabsolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacityonPress{handleCopy}{copied? 已复制: 复制}/Button{/* 这里接入上一章的 SyntaxHighlighter */}pre className{className}code{children}/code/pre/div);}3.3 自动滚动到底部当 AI 持续输出时页面必须自动跟随滚动否则用户需要手动拖拽滚动条。// src/hooks/useAutoScroll.tsimport{useEffect,useRef}fromreact;exportfunctionuseAutoScroll(dependency:any){constbottomRefuseRefHTMLDivElement(null);useEffect((){bottomRef.current?.scrollIntoView({behavior:smooth});},[dependency]);returnbottomRef;}// 在 ChatWindow 中使用// const bottomRef useAutoScroll(messages[messages.length - 1]?.content);// div ref{bottomRef} /3.4 消息状态与 Token 统计在生产环境中我们需要让用户知道 AI 当前的状态以及本次对话消耗了多少 Token用于成本控制。// src/components/chat/MessageStatus.tsxexportfunctionMessageStatus({isLoading,tokenCount}:{isLoading:boolean;tokenCount?:number}){if(isLoading){returndiv classNametext-xs text-gray-400 animate-pulseAI正在思考并生成回复.../div;}if(tokenCount){returndiv classNametext-xs text-gray-400本次消耗 Token:{tokenCount}/div;}returnnull;}3.5 主题切换与响应式布局企业级应用必须支持亮/暗色模式并且在移动端有完美的适配。HeroUI 原生支持 next-themes。// src/app/layout.tsximport{ThemeProvider}fromnext-themes;exportdefaultfunctionRootLayout({children}){return(html langzh-CNsuppressHydrationWarningbodyThemeProvider attributeclassdefaultThemesystemenableSystemHeroUIProvider{children}/HeroUIProvider/ThemeProvider/body/html);}4. 聊天历史持久化数据层体验再好刷新丢数据也是不可接受的。我们需要在useChat的onFinish回调中将完整的对话存入 PostgreSQL。// src/app/api/chat/route.tsexportasyncfunctionPOST(req:Request){const{messages,conversationId}awaitreq.json();constresultstreamText({model:openai(gpt-4o),messages,});// 流式响应结束后将 AI 回复存入数据库result.onFinish(async({finishReason,usage}){awaitprisma.message.create({data:{conversationId,role:assistant,content:result.text,// 获取完整文本tokenCount:usage.totalTokens,}});});returnresult.toDataStreamResponse();}5. 测试验证验证清单发送长文本点击“停止生成”AI 立即停止输出。点击“重新生成”AI 重新回答上一个问题。发送包含代码的问题鼠标悬停代码块点击“复制”按钮粘贴内容正确。切换系统主题页面无缝切换亮/暗色。刷新页面历史聊天记录依然存在。6. 常见问题与踩坑分析问题 1自动滚动在用户手动向上翻阅时被打断原因只要messages更新scrollIntoView就会触发导致用户无法查看历史消息。解决在useAutoScroll中加入判断如果用户当前不在底部scrollTop clientHeight scrollHeight - 50则不触发自动滚动。问题 2onFinish 中获取不到完整的 AI 回复原因流式响应是异步的直接在 streamText 返回后读取 result.text 可能为空。解决必须使用 result.onFinish 回调或者在流结束后通过 await result.text 获取。本章总结我们实现了停止生成、重新生成等核心交互控制。加入了代码一键复制、自动滚动、主题切换等生产级 UI 体验。通过 onFinish 回调实现了聊天历史的数据库持久化。引入了 Token 统计和消息状态展示增强了系统的透明度。至此你的聊天界面已经达到了商业产品的体验标准。但目前的 AI 依然是一个“只会说不会做”的模型。从下一章开始我们将进入 AI Agent 的核心领域——Tool Calling工具调用让 AI 真正拥有执行任务的能力。