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 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 解析顺序:

  1. 全局注册的 Tool UI — 通过 Tools({ toolkit }) 注册
  2. part.toolUI — children render function 中直接访问
  3. 返回 null — 让已注册的 UI 自动渲染
  4. 返回 <></> — 完全隐藏该 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 条件判断

创建于 2026/6/25 更新于 2026/6/25