调整内置功能

内置功能可以调整到三个程度:开关、换位置、自己摆放。

feature-placement.tsx
import '@volcengine/chatkit/styles.css';

import {
  AttachmentButton,
  Chatkit,
  type ChatkitSpeechRecognizer,
  type ChatkitToolbarSlotName,
  type ChatkitUploader,
  useChatRuntime,
  VoiceInput,
} from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import { useState } from 'react';

import { localUpload } from '../backends/local-upload';

const adapter = createMockAdapter({});

// 上传和语音识别由接入方实现,这里用本地地址和一段固定文字代替。
const upload: ChatkitUploader = localUpload;

const recognize: ChatkitSpeechRecognizer = ({ onText, onEnd }) => {
  const timer = setTimeout(() => {
    onText('帮我写一份本周周报', true);
    onEnd();
  }, 600);
  return () => clearTimeout(timer);
};

// 附件按钮和语音按钮的三种摆法:不写 slot 用默认位置;写位置名换到那里;写 false 表示自己摆。
const PLACEMENTS: {
  label: string;
  attachments?: ChatkitToolbarSlotName | false;
  voice?: ChatkitToolbarSlotName | false;
}[] = [
  { label: '默认(附件在左,语音在右)' },
  { label: '两个对调', attachments: 'composer.toolbarEnd', voice: 'composer.toolbarStart' },
  { label: 'slot: false,自己摆', attachments: false, voice: false },
];

export default function FeaturePlacement() {
  const runtime = useChatRuntime({ sessionId: 'feature-placement', adapter });
  const [index, setIndex] = useState(0);
  if (!runtime) return null;
  const { attachments, voice } = PLACEMENTS[index]!;
  const selfPlaced = attachments === false;
  return (
    <div className="demo-column">
      <div className="demo-bar">
        {PLACEMENTS.map((item, itemIndex) => (
          <label key={item.label}>
            <input type="radio" checked={index === itemIndex} onChange={() => setIndex(itemIndex)} /> {item.label}
          </label>
        ))}
      </div>
      <div className="demo-main">
        <Chatkit
          runtime={runtime}
          settings={{ welcome: '换一种摆法,看输入框里两个按钮的位置' }}
          features={{
            attachments: { upload, ...(attachments === undefined ? {} : { slot: attachments }) },
            voice: { recognize, ...(voice === undefined ? {} : { slot: voice }) },
          }}
          // 自己摆:把导出的组件放进任意插槽,这里是输入框上方
          slots={
            selfPlaced
              ? {
                  'composer.top': (
                    <div className="demo-line">
                      <AttachmentButton />
                      <VoiceInput />
                    </div>
                  ),
                }
              : {}
          }
        />
      </div>
    </div>
  );
}
附件按钮和语音按钮的三种摆法:默认位置、两个对调、自己摆新窗口打开

三档

档位写法效果
开关features.edit: false不显示这个功能
换位置features.attachments: { upload, slot: 'composer.toolbarEnd' }控件放到另一个支持的位置
自己摆放features.attachments: { upload, slot: false }不自动放,业务方把导出的组件放到想要的位置
<Chatkit
  sessions={sessions}
  features={{
    edit: false,
    attachments: { upload, slot: 'composer.toolbarEnd' },
    voice: { recognize, slot: 'composer.toolbarStart' },
  }}
/>

哪些功能能换位置

只有带独立控件的功能可以换位置:

功能配置项默认位置可以放的位置导出的组件
附件按钮features.attachments.slotcomposer.toolbarStartcomposer.toolbarStart、composer.toolbarEndAttachmentButton
语音按钮features.voice.slotcomposer.toolbarEndcomposer.toolbarStart、composer.toolbarEndVoiceInput

其余功能的位置是固定的,只能开关:复制、赞踩、重新生成在回答的操作栏;编辑在用户消息上;划词追问在选中文字旁边。

会话列表不属于这里:它是单独的组件 SessionList,本来就由业务方摆放,见会话列表。

自己摆放

写 slot: false 后,把导出的组件放进任意插槽:

import { AttachmentButton, Chatkit } from '@volcengine/chatkit';

<Chatkit
  sessions={sessions}
  features={{ attachments: { upload, slot: false } }}
  slots={{ 'composer.top': <AttachmentButton /> }}
/>;

放到聊天区之外:ChatkitProvider

要把内置组件放到页面头部、侧边栏这些聊天区之外的地方,用 <ChatkitProvider> 把页面包起来:

provider.tsx
import '@volcengine/chatkit/styles.css';

import {
  AttachmentButton,
  Chatkit,
  ChatkitProvider,
  type ChatkitUploader,
  useChatRuntime,
  useComposer,
} from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';

import { localUpload } from '../backends/local-upload';

const adapter = createMockAdapter({});

const TEMPLATE = '请按「背景 / 目标 / 方案 / 风险」四段,帮我写一份方案:';

// 上传由接入方实现,这里用本地地址代替。
const upload: ChatkitUploader = localUpload;

// 聊天区之外的按钮:公开 hooks 在 ChatkitProvider 下面的任何组件里都能用
function InsertTemplate() {
  const composer = useComposer();
  return (
    <button
      type="button"
      className="demo-chip"
      onClick={() => {
        composer.setText(TEMPLATE);
        composer.focus();
      }}
    >
      插入模板
    </button>
  );
}

// ChatkitProvider 把页面包起来:附件按钮和「插入模板」都在页面自己的头部,不在聊天区里。
// props 写在 Provider 上,里面的 <Chatkit /> 从它取。
export default function Provider() {
  const runtime = useChatRuntime({ sessionId: 'provider', adapter });
  if (!runtime) return null;
  return (
    <ChatkitProvider
      runtime={runtime}
      features={{ attachments: { upload, slot: false } }}
      settings={{ welcome: '附件按钮在页面头部', recommendations: ['帮我写一份方案'] }}
    >
      <div className="demo-column">
        <header className="demo-bar">
          <strong>我的应用</strong>
          <InsertTemplate />
          <AttachmentButton />
        </header>
        <div className="demo-main">
          <Chatkit />
        </div>
      </div>
    </ChatkitProvider>
  );
}
附件按钮和「插入模板」按钮都在页面自己的头部新窗口打开
import { AttachmentButton, Chatkit, ChatkitProvider, useChatSessions } from '@volcengine/chatkit';

function App() {
  const sessions = useChatSessions({ adapter, activeSessionId });
  return (
    <ChatkitProvider
      sessions={sessions}
      theme={{ appearance: 'dark' }}
      root={{ className: 'my-chat', style: { height: 480 } }}
      features={{ attachments: { upload, slot: false } }}
    >
      <header>
        <Logo />
        <AttachmentButton />
      </header>
      <Chatkit />
    </ChatkitProvider>
  );
}
  • ChatkitProvider 接受聊天配置和 children。里面的 <Chatkit /> 继承配置,自身提供的值优先;settings、features、slots、renderers 只在各组第一层合并,嵌套对象整体替换。例如覆盖 settings.composer 时,需要一并保留所需的 hidden / disabled 等字段。
  • sessions 与 runtime 选择一个提供;完整应用使用前者,固定单会话使用后者。
  • 公开 hooks 在 Provider 下的组件里可用。共享配置应放在 Provider 上;仅写在 <Chatkit> 上的 onSend 等覆盖不影响其外部兄弟组件。输入框和面板必须挂载后才能接受对应命令。
  • SessionList 始终要显式传受控会话数组 sessions 和选择回调 onSelect,Provider 不提供目录数据。useChatSessions 的返回值传给 Chatkit/Provider 的 sessions,不是传给 SessionList 的数组,见受控会话列表。

主题和作用域

上例中 Chatkit 和外置附件按钮都使用暗色;root 只落在 Chatkit 根节点,外置按钮有自己的 .chatkit-wrapper。VoiceInput 在外置时也有独立 wrapper。Provider 的 root.className/style 不复制到这些 wrapper,因此覆写 Chatkit 根节点的品牌色不会自动影响外置控件。

Provider 的 theme 和 root 是默认配置;Chatkit 自己传入时按顶层整体替换,不深合并。例如实例只传 root={{ className: 'other-chat' }} 就不会保留 Provider 的 480px 高度。显式 theme={{ appearance: 'inherit' }} 会重新跟随该实例最近的 DOM 主题祖先。

需要共享品牌色时,给这组组件的宿主容器起一个业务类名,并直接命中它里面的 wrapper:

.support-panel .chatkit-wrapper {
  --chatkit-color-link: var(--support-link, #9cbcff);
}

这里会命中该容器内的每个 wrapper,包括嵌套实例;只覆盖一个实例时应使用更精确的选择器。明暗继承、独立入口分类和焦点模式差异见主题与样式覆盖。

哪些导出不能直接放在 Provider 旁边

ComposerAddAttachment 是 Chatkit 内的附件菜单,StaticMarkdownText 读取内部渲染配置;它们应通过 Chatkit 的插槽或消息渲染器使用。外层 ChatkitProvider 提供共享运行时和配置,并不等于内部 Chatkit context;仅手工加 .chatkit-wrapper 也不能补齐这个 context。

要在聊天区外选文件,用 AttachmentButton;要在聊天区外将语音转写写入输入框,用同一 Provider 下的 VoiceInput,且输入框需要已挂载。只有样式可见,不表示上传或输入框命令已就绪。TooltipIconButton 则可在没有 Provider 时独立显示,主题跟随自己的 DOM 祖先,不读取 Provider 的显式明暗配置。

配置项

配置项类型默认说明
features.attachments.slotfalse | ChatkitToolbarSlotName'composer.toolbarStart'附件按钮的位置;`false` 表示不自动放,由接入方自己摆。
features.voice.slotfalse | ChatkitToolbarSlotName'composer.toolbarEnd'语音按钮的位置;`false` 表示不自动放,由接入方自己摆。

说明

  • slot 写了这个功能不支持的位置时,控件放回默认位置。
  • 内置功能和业务方的内容走同一套插槽:排序、错误隔离的规则一样,内置功能的顺序固定在负数 order 之后。
  • 功能的显示条件不变:自己摆放的 AttachmentButton 在没有给 upload 时同样不显示。