import { Chatkit, useChatRuntime } from '@volcengine/chatkit';
import { createOpenAiAdapter } from '@volcengine/chatkit/adapters/openai';
import { useMemo, useState } from 'react';
type Mode = 'query' | 'resume' | 'unknown' | 'cancel';
const labels: Record<Mode, string> = {
query: '断线后刷新完整历史',
resume: '断线后按游标续流',
unknown: '没有恢复能力',
cancel: '停止受理后确认',
};
const frame = (text: string, id: string) =>
`id: ${id}\ndata: ${JSON.stringify({ choices: [{ index: 0, delta: { content: text }, finish_reason: null }] })}\n\n`;
const stream = (body: string) => new Response(body, { headers: { 'Content-Type': 'text/event-stream' } });
function Scenario({ mode }: { mode: Mode }) {
const [calls, setCalls] = useState({ run: 0, history: 0, resume: 0, abort: 0 });
const adapter = useMemo(() => {
let cancelled = false;
let currentRun: string | undefined;
const count = (key: keyof typeof calls) => setCalls(value => ({ ...value, [key]: value[key] + 1 }));
return createOpenAiAdapter({
run: async ({ runId }) => {
currentRun = runId;
count('run');
if (mode !== 'cancel') return stream(frame('已收到第一段。', 'e1'));
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode(frame('正在执行,请点击停止。', 'e1')));
},
})
);
},
...(mode === 'unknown'
? {}
: {
history: async () => {
count('history');
if (!currentRun)
return Response.json({ messages: [], runs: [], session: { status: 'idle' }, active_run: null });
const runId = currentRun;
return Response.json({
session: { status: mode === 'resume' ? 'running' : 'idle' },
runs: [
{
id: runId,
status: cancelled ? 'interrupted' : mode === 'resume' ? 'running' : 'completed',
last_event_id: 'e2',
},
],
messages: [
{ id: `user:${runId}`, run_id: runId, role: 'user', content: '开始验证' },
{
id: `assistant:${runId}`,
run_id: runId,
role: 'assistant',
content: '历史确认:后端已完成,最终结果已补齐。',
},
],
});
},
}),
...(mode === 'resume'
? {
resume: async ({ eventId }: { eventId: string }) => {
count('resume');
if (eventId !== 'e1') throw new Error('必须从已有消息前缀的 e1 之后接续');
return stream(frame('这是续接收到的第二段,没有重新发送问题。', 'e2') + 'data: [DONE]\n\n');
},
}
: {}),
...(mode === 'cancel'
? {
abort: async () => {
count('abort');
cancelled = true;
return new Response(null, { status: 202 });
},
}
: {}),
});
}, [mode]);
const runtime = useChatRuntime({ sessionId: `recovery-${mode}`, adapter });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<p style={{ margin: 12 }}>
本地模拟后端 · run {calls.run} / history {calls.history} / resume {calls.resume} / abort {calls.abort}
</p>
<div style={{ flex: 1, minHeight: 0 }}>
{runtime && (
<Chatkit
runtime={runtime}
settings={{
welcome: labels[mode],
recommendations: ['开始验证'],
}}
/>
)}
</div>
</div>
);
}
export default function Recovery() {
const [mode, setMode] = useState<Mode>('query');
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<label style={{ padding: 12 }}>
恢复场景{' '}
<select aria-label="恢复场景" value={mode} onChange={event => setMode(event.target.value as Mode)}>
{Object.entries(labels).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</label>
<div style={{ flex: 1, minHeight: 0 }}>
<Scenario key={mode} mode={mode} />
</div>
</div>
);
}