关于 primitives
primitives 是 assistant-ui 的无样式、可组合 React 聊天界面组件,面向需要自定义聊天 UI 的开发者。它提供线程、消息、输入框等行为部件,并允许通过 asChild 接入自有元素。
主要功能
- 01组合式线程与消息部件
- 02输入、发送、取消及附件处理
- 03自动滚动与运行状态管理
- 04分支切换、编辑及消息操作
- 05键盘快捷键与焦点管理
适用场景
- 01构建自定义聊天线程界面
- 02为消息添加编辑、分支和操作栏
- 03实现支持附件与引用的输入框
- 04按运行状态控制界面显示
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ assistant-ui/ skills --skill primitives安装后在 Claude Code 中输入/primitives确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/action-bar.mdcomposer-input.mdcomposer.mdmentions.mdmessage.mdmessages.mdpart-grouping.mdsuggestions.mdthread.md详细介绍
assistant-ui Primitives
请务必查阅 assistant-ui.com/llms.txt,以获取最新 API。
Primitives 是可组合、无样式的组件,遵循 Radix 风格的部件组合方式:.Root 提供上下文,.PartName 子组件读取上下文,并且每个部件都支持 asChild,可将其行为合并到你自己的元素上,而不是渲染包装元素。它们不附带任何样式,也不对布局做任何预设,只提供行为:键盘快捷键、自动滚动、流式传输状态、焦点管理和禁用逻辑。使用它们之前,请先用 AssistantRuntimeProvider(或嵌套的 AuiProvider)包装组件树。
参考资料
- ./references/thread.md -- ThreadPrimitive:视口、自动滚动、轮次锚点、消息迭代器
- ./references/composer.md -- ComposerPrimitive:根组件、输入、发送、取消、提交行为
- ./references/message.md -- MessagePrimitive:部件处理管线、工具解析、附件、引用、错误
- ./references/action-bar.md -- ActionBarPrimitive 和 ActionBarMorePrimitive 溢出菜单
- ./references/part-grouping.md -- GroupedParts、groupPartByType、思维链 UI
- ./references/mentions.md --
@提及、/斜杠命令、自定义触发器匹配器 - ./references/composer-input.md -- 无头输入、输入历史、听写、引用、附件
- ./references/messages.md -- 编辑、分支、消息计时、虚拟化、滚动条、图像部件
- ./references/suggestions.md --
Suggestions([...])、建议适配器、ThreadPrimitive.Suggestions
导入
import {
AuiIf,
ThreadPrimitive,
ComposerPrimitive,
MessagePrimitive,
MessagePartPrimitive,
ActionBarPrimitive,
ActionBarMorePrimitive,
BranchPickerPrimitive,
AttachmentPrimitive,
ErrorPrimitive,
AssistantModalPrimitive,
ChainOfThoughtPrimitive,
SelectionToolbarPrimitive,
SuggestionPrimitive,
QueueItemPrimitive,
ThreadListPrimitive,
ThreadListItemPrimitive,
ThreadListItemMorePrimitive,
} from "@assistant-ui/react";
基础组件
| 基础组件 | 组成部分 |
|---|---|
ThreadPrimitive | .Root, .Viewport, .ViewportProvider, .ViewportFooter, .Messages, .MessageByIndex, .Unstable_MessageById, .ScrollToBottom, .Suggestions, .SuggestionByIndex, .Suggestion, .Empty (已弃用), .If (已弃用) |
ComposerPrimitive | .Root, .Input, .Send, .Cancel, .AddAttachment, .Attachments, .AttachmentByIndex, .AttachmentDropzone, .Dictate, .StopDictation, .DictationTranscript, .Quote, .QuoteText, .QuoteDismiss, .Queue, .Unstable_TriggerPopoverRoot, .Unstable_TriggerPopover (带有 .Directive / .Action), .Unstable_TriggerPopoverCategories, .Unstable_TriggerPopoverCategoryItem, .Unstable_TriggerPopoverItems, .Unstable_TriggerPopoverItem, .Unstable_TriggerPopoverBack, .If (已弃用) |
MessagePrimitive | .Root, .Parts(.Content 是已弃用的别名), .PartByIndex, .GroupedParts, .Unstable_PartsGrouped, .Unstable_PartsGroupedByParentId, .Attachments, .AttachmentByIndex, .Quote, .Error, .GenerativeUI, .If (已弃用) |
MessagePartPrimitive | .Text, .Image, .InProgress, .Messages |
ActionBarPrimitive | .Root, .Copy, .Reload, .Edit, .Speak, .StopSpeaking, .FeedbackPositive, .FeedbackNegative, .ExportMarkdown |
ActionBarMorePrimitive | .Root, .Trigger, .Content, .Item, .Separator |
BranchPickerPrimitive | .Root, .Previous, .Next, .Number, .Count |
AttachmentPrimitive | .Root, .Name, .Remove, .unstable_Thumb |
ErrorPrimitive | .Root, .Message |
AssistantModalPrimitive | .Root, .Trigger, .Content, .Anchor |
ChainOfThoughtPrimitive | .Root, .AccordionTrigger, .Parts(旧版,建议使用 MessagePrimitive.GroupedParts) |
SelectionToolbarPrimitive | .Root, .Quote |
SuggestionPrimitive | .Title, .Description, .Trigger |
QueueItemPrimitive | .Text, .Steer, .Remove |
ThreadListPrimitive | .Root, .New, .Items, .ItemByIndex, .LoadMore |
ThreadListItemPrimitive | .Root, .Trigger, .Title, .Archive, .Unarchive, .Delete |
ThreadListItemMorePrimitive | .Root, .Trigger, .Content, .Item, .Separator |
ThreadListPrimitive、ThreadListItemPrimitive 和 ThreadListItemMorePrimitive 用于驱动多会话侧边栏。本指南仅在上表中介绍这些组件;有关完整的自定义 UI、CRUD 操作和远程适配器演练,请参阅 thread-list。
使用 AuiIf 进行条件渲染
新增条件时,请使用 AuiIf。它接受一个针对完整助手状态(thread、message、composer、part、attachment)的选择器,而不是固定的一组布尔属性;它取代了仍随 ThreadPrimitive、MessagePrimitive 和 ComposerPrimitive 一同提供的已弃用 .If。
<AuiIf condition={(s) => s.thread.isEmpty}>
<WelcomeScreen />
</AuiIf>
<AuiIf condition={(s) => s.thread.isRunning}>
<ComposerPrimitive.Cancel>Stop</ComposerPrimitive.Cancel>
</AuiIf>
<AuiIf
condition={(s) => s.message.role === "assistant" && s.message.status?.type === "complete"}
>
<FollowUpCard />
</AuiIf>
<AuiIf condition={(s) => s.composer.dictation != null}>
<ComposerPrimitive.StopDictation>Stop</ComposerPrimitive.StopDictation>
</AuiIf>
AuiIf.Condition 已导出,可用于在 JSX 外为条件函数指定类型。s.message 和 s.part 仅在消息或部分上下文中有值;s.thread 和 s.composer 始终可用。
自定义会话示例
一个由基础组件构建的完整会话:包含欢迎建议、按角色显示的消息及内联编辑输入框、操作栏、分支选择器,以及在运行过程中会将 Send 替换为 Cancel 的输入区页脚。
function CustomThread() {
return (
<ThreadPrimitive.Root className="flex h-full flex-col">
<ThreadPrimitive.Viewport className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
<AuiIf condition={(s) => s.thread.isEmpty}>
<div className="flex flex-1 flex-col items-center justify-center gap-3">
<p>Ask me anything.</p>
<ThreadPrimitive.Suggestions>
{() => (
<SuggestionPrimitive.Trigger send className="rounded-lg border px-3 py-2">
<SuggestionPrimitive.Title />
</SuggestionPrimitive.Trigger>
)}
</ThreadPrimitive.Suggestions>
</div>
</AuiIf>
<ThreadPrimitive.Messages>
{({ message }) => {
if (message.role === "user") {
return message.composer.isEditing ? <EditComposer /> : <UserMessage />;
}
return <AssistantMessage />;
}}
</ThreadPrimitive.Messages>
<ThreadPrimitive.ViewportFooter className="sticky bottom-0 pt-2">
<ComposerPrimitive.Root className="flex items-end gap-2 rounded-2xl border bg-background p-2">
<ComposerPrimitive.Input
placeholder="Send a message..."
rows={1}
className="flex-1 resize-none bg-transparent px-2 py-1.5 focus:outline-none"
/>
<AuiIf condition={(s) => !s.thread.isRunning}>
<ComposerPrimitive.Send className="rounded-full bg-primary px-3 py-1.5 text-primary-foreground" />
</AuiIf>
<AuiIf condition={(s) => s.thread.isRunning}>
<ComposerPrimitive.Cancel className="rounded-full border px-3 py-1.5" />
</AuiIf>
</ComposerPrimitive.Root>
</ThreadPrimitive.ViewportFooter>
</ThreadPrimitive.Viewport>
</ThreadPrimitive.Root>
);
}
function UserMessage() {
return (
<MessagePrimitive.Root className="flex flex-col items-end gap-1">
<MessagePrimitive.Quote>
{({ text }) => <blockquote className="border-l pl-2 text-sm italic">{text}</blockquote>}
</MessagePrimitive.Quote>
<div className="max-w-[80%] rounded-2xl bg-primary px-4 py-2 text-primary-foreground">
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <MessagePartPrimitive.Text />;
if (part.type === "image") return <MessagePartPrimitive.Image className="max-w-full rounded-lg" />;
return null;
}}
</MessagePrimitive.Parts>
</div>
<ActionBarPrimitive.Root hideWhenRunning autohide="not-last">
<ActionBarPrimitive.Edit>Edit</ActionBarPrimitive.Edit>
</ActionBarPrimitive.Root>
</MessagePrimitive.Root>
);
}
function EditComposer() {
return (
<MessagePrimitive.Root className="flex justify-end">
<ComposerPrimitive.Root className="w-[80%] rounded-2xl border p-2">
<ComposerPrimitive.Input className="w-full resize-none bg-transparent focus:outline-none" />
<div className="flex justify-end gap-2 pt-1">
<ComposerPrimitive.Cancel className="rounded-md px-2 py-1 text-sm">Cancel</ComposerPrimitive.Cancel>
<ComposerPrimitive.Send className="rounded-md bg-primary px-2 py-1 text-sm text-primary-foreground">
Save
</ComposerPrimitive.Send>
</div>
</ComposerPrimitive.Root>
</MessagePrimitive.Root>
);
}
function AssistantMessage() {
return (
<MessagePrimitive.Root className="flex flex-col items-start gap-1">
<div className="max-w-[80%] rounded-2xl bg-muted px-4 py-2">
<MessagePrimitive.Parts>
{({ part }) => {
switch (part.type) {
case "text":
return <p className="whitespace-pre-wrap"><MessagePartPrimitive.Text /></p>;
case "image":
return <MessagePartPrimitive.Image className="max-w-full rounded-lg" />;
case "file":
return (
<a href={part.data ?? part.url} download={part.filename} className="text-sm underline">
{part.filename ?? part.mimeType}
</a>
);
case "reasoning":
return (
<details className="text-sm text-muted-foreground">
<summary>Thinking</summary>
{part.text}
</details>
);
case "source":
return part.sourceType === "url" ? (
<a href={part.url} className="text-sm underline">{part.title ?? part.url}</a>
) : (
<span className="text-sm">{part.title}</span>
);
case "tool-call":
return part.toolUI ?? <div className="rounded-md border p-2 text-sm">{part.toolName}</div>;
case "data":
return part.dataRendererUI ?? null;
case "generative-ui":
return <MessagePrimitive.GenerativeUI />;
default:
return null; // registered tool and data UIs still render when you return null
}
}}
</MessagePrimitive.Parts>
</div>
<MessagePrimitive.Error>
<ErrorPrimitive.Root className="text-sm text-destructive">
<ErrorPrimitive.Message />
</ErrorPrimitive.Root>
</MessagePrimitive.Error>
<div className="flex items-center gap-2">
<BranchPickerPrimitive.Root hideWhenSingleBranch className="flex items-center gap-1 text-xs">
<BranchPickerPrimitive.Previous>←</BranchPickerPrimitive.Previous>
<span><BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /></span>
<BranchPickerPrimitive.Next>→</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>
<ActionBarPrimitive.Root hideWhenRunning autohide="not-last">
<ActionBarPrimitive.Copy>Copy</ActionBarPrimitive.Copy>
<ActionBarPrimitive.Reload>Regenerate</ActionBarPrimitive.Reload>
</ActionBarPrimitive.Root>
</div>
</MessagePrimitive.Root>
);
}
上面的助手消息 .Parts 回调会处理流水线可能交给你的全部八种部分类型:三种模态部分(text、image、file)、四种提供方通道部分(reasoning、source、tool-call、generative-ui),以及开放式的 data 部分。从 default 情况返回 null,仍会自动交由按名称注册的工具 UI 或按 name 注册的数据渲染器处理;如果想完全抑制该部分,请改为返回 <></>。
分支选择器
BranchPickerPrimitive 会从最近的 MessagePrimitive.Root 读取分支状态,因此必须在其中渲染。到达边界时(以及运行正在进行时,除非运行时支持 switchBranchDuringRun),Previous 和 Next 会自动禁用;hideWhenSingleBranch 会在消息尚无其他分支时隐藏整个选择器,避免常见的单分支情况下操作栏上下跳动。
<BranchPickerPrimitive.Root hideWhenSingleBranch className="inline-flex items-center gap-1">
<BranchPickerPrimitive.Previous>←</BranchPickerPrimitive.Previous>
<span><BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /></span>
<BranchPickerPrimitive.Next>→</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>
用户编辑并重新发送消息,或通过 ActionBarPrimitive.Reload(或 aui.message.reload())重新生成助手消息时,会出现新分支。有关以编程方式切换分支,请参阅 messages.md 中的 aui.message.switchToBranch。
常见问题
原语不渲染任何内容,或在 Provider 外调用 Hook 时抛出错误
- 每个原语都会从最近的运行时上下文读取数据。请使用运行时 Hook(例如
useChatRuntime或useLocalRuntime)将树包裹在AssistantRuntimeProvider runtime={...}中,或嵌套一个AuiProvider extends={aui} config={AuiConfig({...})}以创建独立作用域。 ActionBarPrimitive、BranchPickerPrimitive和ErrorPrimitive还需要一个MessagePrimitive.Root祖先;SelectionToolbarPrimitive需要一个ThreadPrimitive.Root祖先,但必须置于ThreadPrimitive.Viewport外部。
没有任何样式
- 原语会渲染没有类名的原生元素(
<div>、<button>、<textarea>等)。请自行添加className,或传入asChild,将行为合并到已有样式的组件上。
ThreadPrimitive.ViewportSlack 已不存在
- 它已从公开 API 中移除。当
Viewport设置了turnAnchor="top"时,现在会在MessagePrimitive.Root内自动注册顶部锚点目标;请将任何fillClampThreshold/fillClampOffset自定义项改为设置在Viewport上的topAnchorMessageClamp。
MessagePrimitive.Content、.Empty 和所有 .If 仍可使用,但已过时
.Content是已弃用的.Parts别名。ThreadPrimitive.Empty和所有原语的.If都已由AuiIf取代;仅在尚未迁移的代码中保留它们。
groupPartByType({ "mcp-app": [...] }) 不再匹配,但不会提示
"mcp-app"键已在 0.15 中移除。请使用"standalone-tool-call";它是更广泛的匹配项,也会匹配注册 UI 选择了display: "standalone"的任何工具调用。请参阅 part-grouping.md。
带 Unstable_ 前缀的属性、Hook 和原语
- Composer 触发器弹出框系列、
unstable_useComposerInput、unstable_useComposerInputHistory和unstable_useThreadMessageIds都明确标记为不稳定,可能会在不升级主版本的情况下发生变化。可以放心使用,但升级前请锁定所依赖的确切行为。
相关技能
- elements -- 基于这些原语构建、可设置样式并可复制到项目中的组件
- setup --
create、init、add及其他 CLI 脚手架命令 - runtime --
useAui、useAuiState、AuiConfig以及每个原语读取的状态 - tools -- toolkit 中解析为
MessagePrimitive.Parts内part.toolUI的render条目 - thread-list -- 深入了解
ThreadListPrimitive和ThreadListItemPrimitive