import '@volcengine/chatkit/styles.css';
import { Chatkit, type ChatkitDataPartRenderer, useChatkitActions, useChatRuntime } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import { useState } from 'react';
const BASES = [
{ id: 'kb-handbook', label: '产品手册' },
{ id: 'kb-faq', label: '常见问题' },
];
const DOCUMENTS = [
{ id: 'doc-1', title: '接入指南', text: '接入方只需要实现适配器:把后端的输出翻译成界面认识的内容。' },
{ id: 'doc-2', title: '会话接口说明', text: '会话的新建、重命名、删除由适配器的 sessions 提供。' },
];
// 后端返回的引用来源由适配器的 transformOutput 变成一块数据;这里让 Mock 在最后一段回答之后带上它
const adapter = createMockAdapter({
transformOutput: (step, context) => {
const parts = (() => {
return step.kind === 'text' && step.text.includes('## 结论')
? [{ id: 'citations', type: 'data' as const, kind: 'acme.citations', data: DOCUMENTS.map(item => item.id) }]
: undefined;
})();
return parts && context.messageId
? [
...context.defaultChanges,
...parts.map(part => ({ type: 'set-part' as const, messageId: context.messageId!, part })),
]
: undefined;
},
});
// 引用来源:点一条在右侧面板看原文
function Citations({ ids }: { ids: string[] }) {
const { openPanel } = useChatkitActions();
return (
<div className="demo-chips">
引用来源:
{DOCUMENTS.filter(item => ids.includes(item.id)).map(item => (
<button key={item.id} type="button" className="demo-chip" onClick={() => openPanel(`doc:${item.id}`)}>
{item.title}
</button>
))}
</div>
);
}
const renderCitations: ChatkitDataPartRenderer = ({ data }) => <Citations ids={data as string[]} />;
// 知识库:工具栏上选范围(composer.toolbarStart),onSend 把选中的 id 放进 metadata,
// 回答里的引用来源用 renderers.parts 显示,原文在右侧面板(panel)里看。
export default function RecipeKnowledgeBase() {
const runtime = useChatRuntime({ sessionId: 'recipe-knowledge-base', adapter });
const [selected, setSelected] = useState(['kb-handbook']);
if (!runtime) return null;
const toggle = (id: string) =>
setSelected(current => (current.includes(id) ? current.filter(item => item !== id) : [...current, id]));
return (
<Chatkit
runtime={runtime}
settings={{ welcome: '回答会参考选中的知识库', recommendations: ['chatkit 怎么接入'] }}
slots={{
'composer.toolbarStart': BASES.map(base => (
<button
key={base.id}
type="button"
className="demo-chip"
aria-pressed={selected.includes(base.id)}
onClick={() => toggle(base.id)}
>
{base.label}
</button>
)),
panel: ({ id, close }) => {
const document = DOCUMENTS.find(item => `doc:${item.id}` === id);
return document ? (
<div className="demo-panel">
<header>
{document.title}
<button type="button" className="demo-chip" onClick={close}>
关闭
</button>
</header>
{document.text}
</div>
) : null;
},
}}
onSend={input => ({ ...input, metadata: { ...input.metadata, knowledgeBaseIds: selected } })}
renderers={{ parts: { 'acme.citations': renderCitations } }}
/>
);
}