assistant-ui Thread & Message 原语
Thread 消息容器原语(自动滚动、turn anchor、消息迭代)与 Message 消息渲染原语(content parts、工具调用、hover 状态)。
#type / snippet
#status / evergreen
#tech / dev / frontend
#tech / ai
[!info] related notes
- 概览: assistant-ui 概览
- Composer: Composer 原语
- ActionBar: ActionBar & BranchPicker 原语
assistant-ui Thread & Message 原语
Thread 是可滚动消息容器,Message 是单条消息渲染器。两者组合构成聊天界面的核心骨架。
ThreadPrimitive 子组件
| 子组件 | 默认元素 | 职责 |
|---|---|---|
Root | <div> | 顶层容器,管理整体布局 |
Viewport | <div> | 可滚动区域,自动滚动管理 |
ViewportFooter | — | 粘性底部,通常放置 Composer |
Messages | — | 遍历所有消息,render function 接收 { message } |
ScrollToBottom | <button> | 滚动到底部,到达时自动禁用 |
Suggestions | — | 遍历建议提示 |
Empty | — | ⚠️ 已废弃,用 AuiIf 替代 |
Thread 基础结构
// components/ChatThread.tsx
import { ThreadPrimitive, AuiIf } from "@assistant-ui/react";
function ChatThread() {
return (
<ThreadPrimitive.Root className="flex h-full flex-col">
<ThreadPrimitive.Viewport className="flex-1 overflow-y-auto p-4">
{/* 空状态 */}
<AuiIf condition={(s) => s.thread.isEmpty}>
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">开始对话吧</p>
</div>
</AuiIf>
{/* 消息列表 */}
<ThreadPrimitive.Messages>
{({ message }) => {
if (message.role === "user") return <UserMessage />;
return <AssistantMessage />;
}}
</ThreadPrimitive.Messages>
</ThreadPrimitive.Viewport>
{/* 底部输入区 */}
<ThreadPrimitive.ViewportFooter>
<Composer />
</ThreadPrimitive.ViewportFooter>
</ThreadPrimitive.Root>
);
}
Viewport 关键配置
<ThreadPrimitive.Viewport
autoScroll={true} // 新消息自动滚动
turnAnchor="bottom" // 消息锚定位置
scrollToBottomOnRunStart={true} // 开始生成时滚动到底部
scrollToBottomOnThreadSwitch={true} // 切换线程时滚动到底部
topAnchorMessageClamp={{ // top 模式下长消息截断
tallerThan: "10em",
visibleHeight: "6em",
}}
>
Turn Anchor 模式
| 模式 | 效果 | 适用场景 |
|---|---|---|
turnAnchor="bottom" (默认) | 新消息在底部追加 | 传统聊天 |
turnAnchor="top" | 用户问题钉在顶部,回复向下展开 | AI 对话(类 ChatGPT) |
// top anchor 布局:问题在上,回复在下
<ThreadPrimitive.Viewport turnAnchor="top">
<ThreadPrimitive.Messages>
{({ message }) => <MyMessage />}
</ThreadPrimitive.Messages>
</ThreadPrimitive.Viewport>
<ThreadPrimitive.ViewportFooter>
<Composer />
</ThreadPrimitive.ViewportFooter>
MessagePrimitive 子组件
| 子组件 | 职责 |
|---|---|
Root | 消息容器,自动追踪 hover 状态 |
Parts | 遍历消息内容 part,render function 接收 { part } |
GroupedParts | 将相邻 part 按类型分组渲染 |
Attachments | 渲染用户消息的附件 |
Error | 仅在消息有错误时渲染 |
Quote | 渲染引用文本 |
Content Part 类型
| Part 类型 | 默认行为 |
|---|---|
text | <p> 渲染,支持流式指示器 |
image | 渲染图片 |
tool-call | 不渲染(需注册 Tool UI) |
reasoning | 不渲染 |
source | 不渲染 |
file | 不渲染 |
data | 通过 dataRendererUI 渲染 |
消息组件实现
// components/UserMessage.tsx
import { MessagePrimitive } from "@assistant-ui/react";
function UserMessage() {
return (
<MessagePrimitive.Root
className="ml-auto max-w-[80%] rounded-2xl bg-primary p-3"
data-role="user" // 手动添加 data-role 用于 CSS
>
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") {
return <MessagePartPrimitive.Text className="text-primary-foreground" />;
}
return null;
}}
</MessagePrimitive.Parts>
</MessagePrimitive.Root>
);
}
// components/AssistantMessage.tsx
function AssistantMessage() {
return (
<MessagePrimitive.Root
className="max-w-[80%] rounded-2xl bg-muted p-3"
data-role="assistant"
>
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") {
return <MessagePartPrimitive.Text className="prose dark:prose-invert" />;
}
if (part.type === "tool-call") {
return <ToolCallRenderer part={part} />;
}
return null;
}}
</MessagePrimitive.Parts>
<MessagePrimitive.Error>
<div className="text-red-500 text-sm mt-2">生成出错</div>
</MessagePrimitive.Error>
</MessagePrimitive.Root>
);
}
工具调用渲染优先级
当消息包含 tool-call part 时,UI 解析顺序:
- 全局注册的 Tool UI — 通过
Tools({ toolkit })注册 part.toolUI— children render function 中直接访问- 返回
null— 让已注册的 UI 自动渲染 - 返回
<></>— 完全隐藏该 tool-call
<ThreadPrimitive.Messages>
{({ message }) => (
<MessagePrimitive.Root data-role={message.role}>
<MessagePrimitive.Parts>
{({ part }) => {
// 优先使用 part 上挂载的 toolUI
if (part.type === "tool-call" && part.toolUI) {
return <part.toolUI />;
}
if (part.type === "text") {
return <MessagePartPrimitive.Text />;
}
return null;
}}
</MessagePrimitive.Parts>
</MessagePrimitive.Root>
)}
</ThreadPrimitive.Messages>
优雅之处:
AuiIf替代了所有If条件渲染组件,API 更统一turnAnchor="top"实现了 ChatGPT 式阅读体验,无需手动管理滚动data-role属性让 CSS 选择器可以按角色区分样式,无需 JS 条件判断