插槽

聊天区有 17 个固定位置可以放业务组件,通过 slots 按位置名登记。

slots.tsx
import { Chatkit, type ChatkitSlots, useChatkitActions, useChatRuntime } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import '@volcengine/chatkit/styles.css';

const adapter = createMockAdapter();

// 一块带名字的色块,标出这个位置在哪
const mark = (name: string) => <span className="demo-slot">{name}</span>;

// panel 放了内容面板就会显示,所以这里按需打开:点顶栏的按钮才出现
function OpenPanel() {
  const { openPanel } = useChatkitActions();
  return (
    <button type="button" className="demo-chip" onClick={() => openPanel('mark')}>
      打开 panel
    </button>
  );
}

// 聊天区的 17 个位置。session.empty 只在空会话时出现,message.selection 在选中回答里的文字时出现。
const slots: ChatkitSlots = {
  header: (
    <div className="demo-line">
      {mark('header')}
      <OpenPanel />
    </div>
  ),
  'session.top': mark('session.top'),
  'session.bottom': mark('session.bottom'),
  'session.empty': ({ defaultNode }) => (
    <>
      {mark('session.empty')}
      {defaultNode}
    </>
  ),
  'message.header': mark('message.header'),
  'message.footer': mark('message.footer'),
  'message.actions': mark('message.actions'),
  'message.selection': mark('message.selection'),
  'composer.top': mark('composer.top'),
  'composer.bottom': mark('composer.bottom'),
  'composer.bodyTop': mark('composer.bodyTop'),
  'composer.bodyBottom': mark('composer.bodyBottom'),
  'composer.overlay': ({ plainText }) => (plainText ? null : mark('composer.overlay')),
  'composer.toolbarStart': mark('composer.toolbarStart'),
  'composer.toolbarEnd': mark('composer.toolbarEnd'),
  // 只放一项的位置:替换默认的发送按钮,这里把默认按钮留着、旁边加标记
  'composer.sendButton': ({ defaultNode }) => (
    <>
      {mark('composer.sendButton')}
      {defaultNode}
    </>
  ),
  panel: ({ id, close }) =>
    id === 'mark' ? (
      <div className="demo-panel">
        <header>
          {mark('panel')}
          <button type="button" className="demo-chip" onClick={close}>
            关闭
          </button>
        </header>
      </div>
    ) : null,
};

export default function Slots() {
  const runtime = useChatRuntime({ sessionId: 'mock-session-debug', adapter });
  if (!runtime) return null;
  return <Chatkit runtime={runtime} slots={slots} />;
}
每个位置放了一块带名字的色块;选中回答里的文字、点「打开 panel」看另外两个位置新窗口打开

用法

<Chatkit
  sessions={sessions}
  slots={{
    'session.top': <Announcement />, // 节点
    'message.footer': ({ message }) => <Sources messageId={message.id} />, // 函数:拿到这个位置的上下文
    'composer.toolbarStart': [<SkillButton key="skill" />, <KnowledgeBaseButton key="kb" />], // 一个位置放多个
  }}
/>

17 个位置

区域位置代码里的名字典型用途
顶栏消息区上方的顶栏header会话标题、分享按钮
消息区消息列表上方session.top公告、试用提示
消息区消息列表下方、输入框上方session.bottom后续问题推荐
消息区空会话session.empty自己的欢迎页
消息回答的头部,正文之前message.header回答者名称、来源标记
消息每条消息的尾部message.footer引用来源、免责说明
消息回答下方的操作栏message.actions收藏、分享、导出
消息选中文字时的浮条message.selection记笔记、翻译、搜索
输入框输入框上方(输入框外)composer.top已选的知识库、连接器状态
输入框输入框下方(输入框外)composer.bottom免责声明
输入框输入框内、正文上方composer.bodyTop引用的消息、已选的技能
输入框输入框内、正文下方composer.bodyBottom字数提示
输入框盖在正文输入区上的一层composer.overlay空输入时的引导文字与快捷按钮
输入框工具栏左侧composer.toolbarStart技能、知识库、连接器的入口
输入框工具栏右侧、发送按钮之前composer.toolbarEnd字数统计、快捷设置
输入框发送按钮composer.sendButton换成自己的发送按钮
面板右侧面板panel预览、详情、设置

会话列表上的两个位置

会话列表不在 <Chatkit> 里,它是单独的组件 SessionList,自己带两个位置,通过属性传入:

位置属性上下文典型用途
每一行的操作菜单里itemActions{ session }置顶、分享、归档
每一行标题后itemBadge{ session }未读标记、定时任务标记
<SessionList
  sessions={items}
  activeSessionId={activeSessionId}
  onSelect={setActiveSessionId}
  itemBadge={({ session }) => (session.metadata?.pinned ? <PinIcon /> : null)}
  itemActions={({ session }) => <button onClick={() => share(session.id)}>分享会话</button>}
/>

写法、排序、错误隔离的规则与下面讲的插槽相同。自定义列表直接读取应用持有的会话数组,选择时更新 activeSessionId,见会话列表。

这里的 items 是宿主维护的 ChatkitSessionView[],不是 useChatSessions 返回的运行时管理对象;即使外面有 Provider,数组和选择回调也不能省略。

三种写法

写法例子什么时候用
节点<Announcement />不需要这个位置的信息
函数({ message }) => <Sources … />需要上下文:当前消息、会话……
带顺序{ render, order?, key? }要排在内置功能之前,或指定先后

每个位置拿到的上下文

位置上下文
header{ session? }:当前会话
session.top、session.bottom空对象
session.empty{ defaultNode, loading }:默认的欢迎内容、历史是否还在加载
message.header、message.footer、message.actions{ message, run? }:这条消息、它所属那一轮的运行信息
message.selection{ message, run?, text, clear }:外加选中的文字、取消选中
composer.*(除发送按钮){ content, plainText, disabled, isSending, setText, insertText, focus, submit, … }
composer.sendButton同上,外加 { defaultNode, handleSubmit, disabledSend }
panel{ id?, close }:打开面板时给的标识、关闭面板

插槽里的组件能做什么

想做的事用什么
知道这个位置上的消息、会话、选中的文字插槽函数的参数(上表的上下文)
读当前会话useCurrentSession
读最近一轮的状态:是否在运行、阶段、错误useCurrentRun
往输入框里填文字、插入引用、加附件、聚焦useComposer
发送、停止、重新生成、开关右侧面板useChatkitActions
读会话列表、切换、新建会话应用的会话数组与业务函数
在某件事发生时做点什么(一轮结束、发送失败)<Chatkit> 的 onEvent,把结果存进页面自己的状态
把页面上选的东西随消息带给后端<Chatkit> 的 onSend,放进 metadata

hooks 的完整说明见 Hook 参考。

一个位置放多个

写成数组。内置功能也放在这些位置上(比如附件按钮在 composer.toolbarStart),业务方的内容和它们排在一起。

顺序:order

slots={{
  'composer.toolbarStart': [
    { render: <SkillButton />, order: -1, key: 'skill' }, // 排在内置功能之前
    <KnowledgeBaseButton key="kb" />, // 不写 order:排在内置功能之后
    { render: <MoreButton />, order: 10, key: 'more' }, // 排在最后
  ],
}}

从前到后:

  1. order 为负数的,小的在前
  2. 内置功能
  3. order 为 0 的
  4. 没写 order 的,按书写顺序
  5. order 为正数的,小的在前

同一个位置放多项、并且会增减时,给每一项一个 key。

工具栏放不下时

composer.toolbarStart 与 composer.toolbarEnd 一行放不下时,靠后的项收进「更多」菜单。

只能放一个的位置

session.empty、composer.sendButton 只能放一项,放了就替换默认的显示:

slots={{
  'composer.sendButton': ({ defaultNode }) => (
    <div className="send-area">
      {defaultNode}
      <span>回车发送</span>
    </div>
  ),
}}

defaultNode 保留内置发送/停止切换和禁用逻辑。完全替换时,handleSubmit 只负责提交,不能自动代替停止操作;需要结合 isSending、disabledSend 和 useChatkitActions().abort 实现完整交互。这里的 isSending 是当前按钮的停止态,允许排队且已有待发内容时会变回发送态。

单项位置直接写 null / false 会被视为没有配置;需要明确隐藏默认内容时使用 () => null。

右侧面板

panel 里放了内容,面板就显示。也可以按需打开:

const { openPanel, closePanel } = useChatkitActions();
openPanel('order-detail'); // 面板里的内容通过上下文的 id 知道该显示什么

slots={{ panel: ({ id, close }) => (id === 'order-detail' ? <OrderDetail onClose={close} /> : null) }}

错误隔离

每一项各自隔离。某一项渲染时抛错:

  • 这一项不显示;同一位置的其他项、对话本身不受影响。
  • onError 收到完整的错误。
  • onEvent 收到 render_error 事件,scope 为 slot,slot 是出错的位置名。

此隔离只覆盖渲染错误,不覆盖按钮事件处理函数或异步请求的异常;业务组件需要自行处理后两者。

配置项

配置项类型默认说明
slots.headerChatkitStackSlot<ChatkitHeaderSlotContext>—顶栏,在消息区上方;默认没有内容。
slots.session.topChatkitStackSlot<ChatkitEmptySlotContext>—消息列表上方,比如公告。
slots.message.actionsChatkitStackSlot<ChatkitMessageSlotContext>—回答下方的操作栏,排在复制、赞踩之后。
slots.composer.toolbarStartChatkitStackSlot<ChatkitComposerRenderContext>—输入框工具栏左侧;放不下时靠后的项收进「更多」。
slots.composer.sendButtonChatkitSlotItem<ChatkitComposerSendButtonRenderContext>—发送按钮;只放一项,替换默认按钮。
slots.panelChatkitStackSlot<ChatkitPanelSlotContext>—右侧面板;放了内容就显示,也可以用 `openPanel` 打开。

说明

  • 位置是固定的,不能新增。需要的位置不在表里时,先看能不能用业务卡片或右侧面板解决。
  • 插槽组件可以继承文字样式,并使用 --chatkit-* CSS 变量;自带的颜色和布局不会自动转换成 chatkit 主题,见主题。
  • 业务方用插槽实现完整功能的例子见业务做法。