mcpField
P
primitives

primitives

github.com/assistant-ui开发296.3k 次安装1 天前更新
代码仓库

关于 primitives

primitives 是 assistant-ui 的无样式、可组合 React 聊天界面组件,面向需要自定义聊天 UI 的开发者。它提供线程、消息、输入框等行为部件,并允许通过 asChild 接入自有元素。

主要功能

  1. 01组合式线程与消息部件
  2. 02输入、发送、取消及附件处理
  3. 03自动滚动与运行状态管理
  4. 04分支切换、编辑及消息操作
  5. 05键盘快捷键与焦点管理

适用场景

  1. 01构建自定义聊天线程界面
  2. 02为消息添加编辑、分支和操作栏
  3. 03实现支持附件与引用的输入框
  4. 04按运行状态控制界面显示

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/assistant-ui/skills --skill primitives

安装后在 Claude Code 中输入/primitives确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
action-bar.md
composer-input.md
composer.md
mentions.md
message.md
messages.md
part-grouping.md
suggestions.md
thread.md

详细介绍

assistant-ui Primitives

请务必查阅 assistant-ui.com/llms.txt,以获取最新 API。

Primitives 是可组合、无样式的组件,遵循 Radix 风格的部件组合方式:.Root 提供上下文,.PartName 子组件读取上下文,并且每个部件都支持 asChild,可将其行为合并到你自己的元素上,而不是渲染包装元素。它们不附带任何样式,也不对布局做任何预设,只提供行为:键盘快捷键、自动滚动、流式传输状态、焦点管理和禁用逻辑。使用它们之前,请先用 AssistantRuntimeProvider(或嵌套的 AuiProvider)包装组件树。

参考资料

导入

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

评论

支持 Markdown