- 人工智能
- 大模型
- 代码智能体
- AI Agent
- 桌面应用
- 后端
- 前端
- CLI
【免费下载链接】ZCode
ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。
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 聊天界面中最常见的三类体验问题:
- 滚动跟随:新消息到达时自动滚到最新内容,用户主动上翻查看历史时不被强行拉回底部;
- 状态提示:当用户不在底部时,显示可点击的悬浮按钮,一键回到最新消息;
- 导出分享:内置「下载对话为 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 />
| Prop | Type | Default | Description |
|---|---|---|---|
contextRef | React.Ref<StickToBottomContext> | - | 可选 ref,用于访问 StickToBottom 上下文对象。 |
instance | StickToBottomInstance | - | 可选的实例,用于控制 StickToBottom 组件。 |
children | ((context: StickToBottomContext) => ReactNode) \| ReactNode | - | Render prop 或 ReactNode,支持利用上下文进行自定义渲染。 |
...props | Omit<React.HTMLAttributes<HTMLDivElement>, ...> | - | 其余属性透传到根 div。 |
5.2<ConversationContent />
| Prop | Type | Default | Description |
|---|---|---|---|
children | ((context: StickToBottomContext) => ReactNode) \| ReactNode | - | Render prop 或 ReactNode,支持利用上下文进行自定义渲染。 |
...props | Omit<React.HTMLAttributes<HTMLDivElement>, ...> | - | 其余属性透传到根 div。 |
5.3<ConversationEmptyState />
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | - | 要展示的标题文本。 |
description | string | - | 要展示的描述文本。 |
icon | React.ReactNode | - | 可选,展示在文本上方的图标。 |
children | React.ReactNode | - | 可选,渲染在文本下方的额外内容。 |
...props | ComponentProps<...> | - | 其余属性透传到根 div。 |
5.4<ConversationScrollButton />
| Prop | Type | Default | Description |
|---|---|---|---|
...props | ComponentProps<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>| Prop | Type | Default | Description |
|---|---|---|---|
messages | UIMessage[] | 必填 | 要包含在下载内容中的消息数组。 |
filename | string | - | 下载文件的文件名。 |
formatMessage | (message: UIMessage, index: number) => string | - | 自定义每条消息在输出中的格式化函数。 |
...props | Omit<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 与运行时源码。
相关推荐
ZCode 集成指南:使用 AI Elements Conversation 组件构建自动吸底的 AI 聊天界面
ZCode 集成指南:使用 AI Elements Conversation 组件构建自动吸底的 AI 聊天界面 导读 本指南以 ZCode 仓库内 .agen
ZCode 中的 PromptInput 组件:基于 ai-elements 构建 AI 聊天输入框的完整指南
ZCode 中的 PromptInput 组件:基于 ai elements 构建 AI 聊天输入框的完整指南 PromptInput 是 ai element
ZCode 中构建 AI 聊天界面:AI Elements 组件库安装、组合与深度定制实战指南
ZCode 中构建 AI 聊天界面:AI Elements 组件库安装、组合与深度定制实战指南 本文以 ZCode 仓库内嵌的 ai elements 技能文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考