技能选择

在输入框里选一个技能(写周报、做表格、查资料),随消息一起发给后端。本页用插槽和公开 hooks 实现它。

recipe-skills.tsx
import { Chatkit, type ChatkitSendInput, useChatRuntime, useComposer } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import '@volcengine/chatkit/styles.css';
import { useState } from 'react';

const adapter = createMockAdapter({});

const SKILLS = [
  { id: 'weekly-report', label: '写周报' },
  { id: 'spreadsheet', label: '做表格' },
  { id: 'research', label: '查资料' },
];

// 工具栏上的「技能」按钮:选中的技能用 insertReference 作为标签插进输入框
function SkillMenu() {
  const composer = useComposer();
  const [open, setOpen] = useState(false);
  return (
    <span className="demo-menu">
      <button type="button" className="demo-chip" onClick={() => setOpen(value => !value)}>
        技能
      </button>
      {open ? (
        <ul>
          {SKILLS.map(skill => (
            <li key={skill.id}>
              <button
                type="button"
                onClick={() => {
                  composer.insertReference({ kind: 'skill', id: skill.id, label: skill.label });
                  composer.focus();
                  setOpen(false);
                }}
              >
                {skill.label}
              </button>
            </li>
          ))}
        </ul>
      ) : null}
    </span>
  );
}

// 技能选择只用到一个插槽和一个 hook。发送时引用随消息一起发出,后端从 references 里读到技能的 id;
// 右侧用 onSend 把发出的内容显示出来。
export default function RecipeSkills() {
  const runtime = useChatRuntime({ sessionId: 'recipe-skills', adapter });
  const [sent, setSent] = useState<ChatkitSendInput['content']>();
  if (!runtime) return null;
  return (
    <div className="demo-row">
      <div className="demo-main">
        <Chatkit
          runtime={runtime}
          settings={{ welcome: '先选一个技能', placeholder: '选一个技能,再写要求' }}
          slots={{ 'composer.toolbarStart': <SkillMenu /> }}
          onSend={input => setSent(input.content)}
        />
      </div>
      <aside className="demo-side">
        <h4>随消息发出的内容</h4>
        {sent ? <pre>{JSON.stringify(sent, null, 2)}</pre> : <p className="demo-muted">选一个技能,发一句话</p>}
      </aside>
    </div>
  );
}
点工具栏的「技能」选一项,输入框里出现技能标签;右侧是随消息发出的内容新窗口打开

在线示例使用示例数据;右侧是 onSend.content,不是真实后端请求或技能执行结果。技能选择只传递标识,后端需要按约定解析并执行相应能力。

这个功能是什么

用户点输入框工具栏上的「技能」按钮,从列表里选一项。选中的技能以标签形式出现在输入框里,发送时后端收到技能的 id,按这个技能处理请求。

用到的入口

入口用来做什么
slots['composer.toolbarStart']放「技能」按钮
useComposer().insertReference把选中的技能作为标签插进输入框
发送输入里的 references后端从这里读到技能 id
onSend示例里用它把发出的内容显示在右侧

代码

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

const SKILLS = [
  { id: 'weekly-report', label: '写周报' },
  { id: 'spreadsheet', label: '做表格' },
];

function SkillButton() {
  const composer = useComposer();
  return (
    <SkillMenu
      items={SKILLS}
      onPick={skill => {
        composer.insertReference({ kind: 'skill', id: skill.id, label: skill.label });
        composer.focus();
      }}
    />
  );
}

export function App() {
  const sessions = useChatSessions({ adapter, activeSessionId });
  return <Chatkit sessions={sessions} slots={{ 'composer.toolbarStart': <SkillButton /> }} />;
}

发送时,onSend 拿到的 content 里技能是一段引用:{ type: 'reference', protocolId: 'skill', sourceId: 'weekly-report', label: '写周报', … }。 交给适配器时,正文里技能写成 @写周报,同时输入的 references 里带上 { kind: 'skill', id: 'weekly-report', label: '写周报' }。 references 在请求中的位置由适配器决定,接入时需要与后端约定,见接入后端。不能假定所有协议都提供同名顶层字段。

变化

  • 技能列表来自后端:SkillButton 里自己请求接口,chatkit 不参与。
  • 用 / 选技能:候选项中同时提供 kind: 'skill'、value: skill.id 与 label;否则只是文本标签,不生成结构化引用。见 @ 与 /。
  • 一次只能选一个技能:选之前用 composer.getContent() 检查是否已有技能标签。
  • 不用标签,用固定的选中态:把选中的技能存在自己的状态里,显示在 composer.bodyTop,发送时用 onSend 放进 metadata。

为什么不内置

技能有哪些、怎么分类、后端怎么按技能处理请求,都需要业务方和自己的后端约定,不同产品差别很大。chatkit 提供位置(插槽)和通道(引用、metadata),功能本身由业务方实现。