☰
在 ZCode 中构建 AI 聊天界面:ai-elements Conversation 组件完整指南
2026/9/30 1:49:53 网站建设 项目流程
  • 人工智能
  • 大模型
  • 代码智能体
  • AI Agent
  • 桌面应用
  • 后端
  • 前端
  • CLI

【免费下载链接】ZCode

ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。

项目地址:https://gitcode.com/zai-org/ZCode
点击查看免费下载

Conversation是 ZCode 仓库内置的 ai-elements 技能(skill)所提供的一套对话容器组件,用于包裹消息列表并实现「自动滚动到底部 + 悬浮滚动按钮 + 导出 Markdown」等聊天界面核心能力。本指南基于 .agents/skills/ai-elements/references/conversation.md 整理,并结合仓库内的示例脚本与配套文档,帮助你快速在 AI SDK 项目中落地一个专业、可访问、可定制的对话 UI。

背景说明:ai-elements 是构建在 shadcn/ui 之上的 AI 原生组件库,本仓库中的相关技能与参考文档派生于 Vercel 的 ai-elements(Apache-2.0 许可,详见仓库根目录的 THIRD-PARTY-NOTICES.md),由 ZCode 完成了本地化集成、格式化与适配。因此下文描述的能力、命令与代码示例均以当前仓库实际集成的版本为准。

一、Conversation 组件能做什么

Conversation组件包裹聊天消息并自动滚动到底部,同时包含一个「未在底部时自动浮现」的滚动按钮。它解决了 AI 聊天界面中最常见的三类体验问题:

  1. 滚动跟随:新消息到达时自动滚到最新内容,用户主动上翻查看历史时不被强行拉回底部;
  2. 状态提示:当用户不在底部时,显示可点击的悬浮按钮,一键回到最新消息;
  3. 导出分享:内置「下载对话为 Markdown 文件」的能力,便于保存与复盘。

从源码结构看,Conversation的自动滚动能力基于StickToBottomContext/StickToBottomInstance这类「粘性到底部」上下文机制实现(对应 Props 中的contextRef与instance参数),这允许外部代码通过 ref 精确控制滚动容器的行为,而不仅是简单的scrollIntoView。

二、安装组件

在已满足前提条件的项目中,使用 ai-elements CLI 安装:

npx ai-elements@latest add conversation

按 SKILL.md 中的说明,前提条件包括:

  • Node.js 18 或更高版本;
  • 一个已安装AI SDK的Next.js项目;
  • 已安装shadcn/ui(未安装时,运行上述命令会自动安装)。

提示:应根据项目的packageManager选择对应的包管理器命令,例如pnpm dlx ai-elements@latest add conversation或bunx --bun ai-elements@latest add conversation。默认情况下组件代码会被写入@/components/ai-elements/目录(即 shadcn components 配置指定的目录)。由于组件代码直接落入你的项目源码(而非隐藏的库),你可以直接打开组件文件查看实现细节或做二次修改。

三、与 AI SDK 组合的完整用法

原文档给出了一个开箱即用的完整示例:前端用Conversation渲染消息、用PromptInput收集输入,后端用 AI SDK 的streamText提供流式接口。这一组合可以在 .agents/skills/ai-elements/references/conversation.md 中查看原文,也可参考仓库内的演示脚本 .agents/skills/ai-elements/scripts/conversation.tsx。

3.1 前端组件(app/page.tsx)

"use client"; import { Conversation, ConversationContent, ConversationDownload, ConversationEmptyState, ConversationScrollButton, } from "@/components/ai-elements/conversation"; import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message"; import { PromptInput, type PromptInputMessage, PromptInputTextarea, PromptInputSubmit, } from "@/components/ai-elements/prompt-input"; import { MessageSquare } from "lucide-react"; import { useState } from "react"; import { useChat } from "@ai-sdk/react"; const ConversationDemo = () => { const [input, setInput] = useState(""); const { messages, sendMessage, status } = useChat(); const handleSubmit = (message: PromptInputMessage) => { if (message.text.trim()) { sendMessage({ text: message.text }); setInput(""); } }; return ( <div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"> <div className="flex flex-col h-full"> <Conversation> <ConversationContent> {messages.length === 0 ? ( <ConversationEmptyState icon={<MessageSquare className="size-12" />} title="Start a conversation" description="Type a message below to begin chatting" /> ) : ( messages.map((message) => ( <Message from={message.role} key={message.id}> <MessageContent> {message.parts.map((part, i) => { switch (part.type) { case "text": // we don't use any reasoning or tool calls in this example return ( <MessageResponse key={`${message.id}-${i}`}> {part.text} </MessageResponse> ); default: return null; } })} </MessageContent> </Message> )) )} </ConversationContent> <ConversationDownload messages={messages} /> <ConversationScrollButton /> </Conversation> <PromptInput onSubmit={handleSubmit} className="mt-4 w-full max-w-2xl mx-auto relative"> <PromptInputTextarea value={input} placeholder="Say something..." onChange={(e) => setInput(e.currentTarget.value)} className="pr-12" /> <PromptInputSubmit status={status === "streaming" ? "streaming" : "ready"} disabled={!input.trim()} className="absolute bottom-1 right-1" /> </PromptInput> </div> </div> ); }; export default ConversationDemo;

要点拆解:

  • Conversation是外层容器,ConversationContent负责可滚动的内容区域;
  • 消息列表为空时,用ConversationEmptyState展示引导性空状态(图标 + 标题 + 描述);
  • 非空时,遍历messages并借由Message/MessageContent/MessageResponse(来自 message.md)渲染每条消息,示例中只处理text类型的分片,reasoning、tool等分片直接跳过;
  • ConversationDownload与ConversationScrollButton作为Conversation的直接子组件放在内容区之后,即可获得导出与滚动悬浮按钮能力。

3.2 后端流式接口(api/chat/route.ts)

import { streamText, UIMessage, convertToModelMessages } from "ai"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: "openai/gpt-4o", messages: await convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); }

后端将前端传来的UIMessage[]经convertToModelMessages转换为模型消息格式,通过streamText流式生成回复,并以toUIMessageStreamResponse()返回,从而驱动前端的status === "streaming"状态与PromptInputSubmit的流式按钮显示。

四、核心功能特性

原文档列出的Conversation功能清单如下,这些也正是 AI 聊天产品的基础体验要求:

  • 新消息加入时自动滚动到底部;
  • 支持可配置动画的平滑滚动行为;
  • 不在底部时显示滚动按钮;
  • 将对话下载为 Markdown;
  • 响应式设计,支持自定义 padding 与间距;
  • 灵活的内容布局,保持一致的消息间距;
  • 为屏幕阅读器提供正确的 ARIA 角色,可访问性好;
  • 通过className属性自定义样式;
  • 支持任意数量的子消息组件。

仓库演示脚本 conversation.tsx 用定时器逐条追加消息来模拟流式对话,直观演示了「自动滚动 + 空状态 → 消息列表」的切换过程;在实际应用中,这段逻辑由useChat的messages数组增量更新天然承担。

五、Props 详解

以下 Props 表格完整继承自原文档,是使用与二次封装时的第一手依据。

5.1<Conversation />

PropTypeDefaultDescription
contextRefReact.Ref<StickToBottomContext>-可选 ref,用于访问 StickToBottom 上下文对象。
instanceStickToBottomInstance-可选的实例,用于控制 StickToBottom 组件。
children((context: StickToBottomContext) => ReactNode) \| ReactNode-Render prop 或 ReactNode,支持利用上下文进行自定义渲染。
...propsOmit<React.HTMLAttributes<HTMLDivElement>, ...>-其余属性透传到根 div。

5.2<ConversationContent />

PropTypeDefaultDescription
children((context: StickToBottomContext) => ReactNode) \| ReactNode-Render prop 或 ReactNode,支持利用上下文进行自定义渲染。
...propsOmit<React.HTMLAttributes<HTMLDivElement>, ...>-其余属性透传到根 div。

5.3<ConversationEmptyState />

PropTypeDefaultDescription
titlestring-要展示的标题文本。
descriptionstring-要展示的描述文本。
iconReact.ReactNode-可选,展示在文本上方的图标。
childrenReact.ReactNode-可选,渲染在文本下方的额外内容。
...propsComponentProps<...>-其余属性透传到根 div。

5.4<ConversationScrollButton />

PropTypeDefaultDescription
...propsComponentProps<typeof Button>-其余属性透传到底层 shadcn/ui 的 Button 组件。

注意:它继承的是 shadcn/uiButton的全部能力,因此你可以通过onClick、className、variant等按钮属性来调整其行为与外观。

5.5<ConversationDownload />

一个将对话下载为Markdown 文件的按钮,最小用法:

import { ConversationDownload } from "@/components/ai-elements/conversation"; <Conversation> <ConversationContent> {messages.map(...)} </ConversationContent> <ConversationDownload messages={messages} /> <ConversationScrollButton /> </Conversation>
PropTypeDefaultDescription
messagesUIMessage[]必填要包含在下载内容中的消息数组。
filenamestring-下载文件的文件名。
formatMessage(message: UIMessage, index: number) => string-自定义每条消息在输出中的格式化函数。
...propsOmit<ComponentProps<typeof Button>, ...>-其余属性透传到底层 shadcn/ui 的 Button 组件。

messages为必填项,用于确定导出的内容范围;filename控制导出文件名;formatMessage允许你完全掌控每条消息的落盘格式,例如只导出文本分片、附加角色标签或时间戳。

六、messagesToMarkdown:自定义导出利器

除了现成的下载按钮,组件还导出一个独立的工具函数messagesToMarkdown,用于将UIMessage[]转换为 Markdown 字符串。它适合需要自定义导出实现(如「另存为其他格式」「批量导出」「发送到剪贴板」)的场景:

import { messagesToMarkdown } from "@/components/ai-elements/conversation"; const markdown = messagesToMarkdown(messages); // With custom formatter const customMarkdown = messagesToMarkdown( messages, (msg, i) => `[${msg.role}]: ${msg.parts .filter((p) => p.type === "text") .map((p) => p.text) .join("")}`, );

ConversationDownload内部正是这类工具函数的便捷封装:默认实现直接使用默认格式化器,传入自定义formatMessage时则走你的格式化逻辑。从源码结构看,格式化器接收消息对象与序号index,返回值会被拼接进最终 Markdown,因此你可以在其中自由插入分隔线、元数据或仅保留文本分片。

七、组合使用:与 Message、PromptInput 协同

Conversation是「容器层」,消息渲染交给 Message 组件族,输入层交给 PromptInput 组件族。三者构成一个完整的 AI 聊天闭环:

  • Conversation:滚动容器 + 空状态 + 滚动按钮 + 导出;
  • Message / MessageContent / MessageResponse:按角色区分样式、渲染 Markdown(GFM、数学公式、代码高亮)、支持分支切换与操作按钮;
  • PromptInput:多行文本输入、附件上传、模型选择与提交按钮(含流式状态)。

在 prompt-input.md 的完整示例中,Conversation/ConversationContent/ConversationScrollButton被原样复用,而消息渲染与输入侧分别替换为更丰富的MessageResponse(Markdown 渲染)与带附件/模型选择器的PromptInput组合——这印证了 Conversation 的设计原则:只负责「容器 + 滚动 + 导出」,与具体消息形态解耦,因此可以稳定复用于从简到繁的各种对话界面。

八、样式定制与常见问题

8.1 定制方式

由于组件代码直接落在项目源码中,定制非常直接:

  • 通过className属性向根元素追加 Tailwind 类,控制圆角、内边距、背景、间距等;
  • 直接编辑components/ai-elements/conversation.tsx,例如调整滚动动画时长、悬浮按钮位置或空状态布局;
  • 用contextRef/instance从外部命令式地控制滚动(例如「一键回到底部」「暂停自动滚动」)。

8.2 常见问题排查(来自 SKILL.md)

  • 组件没有样式:确认项目已按 shadcn/ui(Tailwind 4)正确配置globals.css(导入 Tailwind 并包含 shadcn 基础样式)。
  • 执行 CLI 后没有任何文件被添加:确认当前目录是项目根目录(package.json所在处)、components.json配置正确,并使用最新版 CLI(npx ai-elements@latest)。
  • 主题切换不生效(一直停留在浅色模式):确认应用使用 shadcn/ui 与 ai-elements 期望的同一套data-theme体系(默认在<html>元素上切换data-theme属性),并确保tailwind.config.js使用 class 或 data 选择器。
  • 导入报 "module not found":确认组件文件存在,且tsconfig.json配置了@/路径别名("baseUrl": "."与"paths": { "@/*": ["./*"] })。

九、总结

Conversation组件以极小的组合成本解决了 AI 聊天界面的滚动跟随、状态提示与对话导出三大基础问题,并通过contextRef/instance、Render prop、className透传与messagesToMarkdown工具函数保留了充分的定制空间。在 ZCode 仓库中,你可以在 .agents/skills/ai-elements/references/conversation.md 查看组件文档全文,在 .agents/skills/ai-elements/scripts/conversation.tsx 查看可运行的演示脚本,并结合 message.md 与 prompt-input.md 拼装出生产级聊天界面。

  • 人工智能
  • 大模型
  • 代码智能体
  • AI Agent
  • 桌面应用
  • 后端
  • 前端
  • CLI

【免费下载链接】ZCode

ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。

项目地址:https://gitcode.com/zai-org/ZCode
点击查看免费下载
上一篇:终极Java代码质量工具链部署指南:Checkstyle、FindBugs、PMD和SonarQube完整配置教程
下一篇:WaybackProxy与复古浏览器完美搭配:如何在Windows 95/98系统上使用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询