| 1 | import { useLayoutEffect, useMemo, useRef, useState } from "react"; |
| 2 | import { createRoot } from "react-dom/client"; |
| 3 | import { Transcript } from "../src/components/Transcript"; |
| 4 | import { LocaleProvider } from "../src/lib/i18n"; |
| 5 | import { ToastProvider } from "../src/lib/toast"; |
| 6 | import { getMarkdownWorkerClient } from "../src/lib/markdownWorkerClient"; |
| 7 | import { Composer } from "../src/components/Composer"; |
| 8 | import { canonicalMessage } from "../src/lib/canonicalTranscriptBackend"; |
| 9 | import { historyMessagesToItems } from "../src/lib/historyItems"; |
| 10 | import type { ControllerLiveStore, Item, LiveStream } from "../src/lib/useController"; |
| 11 | import "../src/styles.css"; |
| 12 | |
| 13 | function makeTurns(count: number, start = 0): Item[] { |
| 14 | return Array.from({ length: count }, (_, position): Item[] => { const index = start + position; return [ |
| 15 | { kind: "user", id: `u${index}`, text: `Question ${index + 1}: Explain the implementation`, checkpointTurn: index + 1 }, |
| 16 | { kind: "tool", id: `tool${index}`, name: "read_file", args: JSON.stringify({ path: "src/example.ts" }), output: "Evidence\n".repeat(1200), readOnly: true, status: "done", subject: "src/example.ts" }, |
| 17 | { kind: "assistant", id: `a${index}`, text: `### Answer ${index + 1}\n\nA stable paragraph with **formatted text** and a [reference](https://example.com).\n\n- Keep the reader's position.\n- Update only the changed node.\n\n中文内容用于验证字体与换行。` + (index % 12 === 0 ? "\n\n| Item | Result |\n|---|---|\n| Test | Passed |\n\n$x^2 + y^2$" : "") + (index % 40 === 0 ? "\n\n```ts\n" + Array.from({length: 210}, (_, n) => `const value${n} = ${n};`).join("\n") + "\n```" : ""), reasoning: "Inspect the relevant code before making a change.", streaming: false, |
| 18 | createdAt: new Date(2026, 8, 12, 12, 34).getTime(), turnDurationMs: 29_000, tokensPerSecond: 183, |
| 19 | turnUsage: { totalTokens: 185_225, uncachedInputTokens: 26_278, cacheReadTokens: 155_520, outputTokens: 3_427, reasoningTokens: 1_909, routes: ["deepseek-official/deepseek-flash"] } }, |
| 20 | ]; }).flat(); |
| 21 | } |
| 22 | function weatherTurn(): Item[] { |
| 23 | const end = makeTurns(1)[2]; |
| 24 | return [ |
| 25 | { kind: "user", id: "weather-user", text: "查一下今天上海的天气", checkpointTurn: 1 }, |
| 26 | { kind: "assistant", id: "weather-thought", text: "", reasoning: "先查询实时天气,再交叉核对来源。", streaming: false }, |
| 27 | { kind: "tool", id: "weather-search", name: "web_search", args: JSON.stringify({ query: "上海今天天气 实时温度" }), output: "Weather sources", searchSources: [{ title: "天气数据(测试来源)", url: "https://example.com/weather" }], status: "done" }, |
| 28 | { kind: "tool", id: "weather-error", name: "web_fetch", args: JSON.stringify({ url: "https://example.com/weather" }), error: "DNS lookup failed", status: "error" }, |
| 29 | { kind: "assistant", id: "weather-progress", text: "一个来源暂时无法访问,正在核对另一来源。", reasoning: "切换到另一来源。", streaming: false }, |
| 30 | { kind: "notice", id: "weather-permission", level: "info", title: "已记录决策", text: "permission saved to /tmp/weather-fixture/reasonix.toml: curl --max-time 20 https://example.com/weather" }, |
| 31 | { kind: "tool", id: "weather-bash", name: "bash", args: JSON.stringify({ command: "printf '25.5°C 晴\\n'", description: "获取并核对上海天气" }), output: "25.5°C 晴\n", status: "done", |
| 32 | execution: { kind: "shell", shell: "bash", state: "completed", exitCode: 0, supportsAndAnd: true } }, |
| 33 | { kind: "tool", id: "weather-present", name: "present", args: JSON.stringify({ files: [{ path: "output/shanghai-weather.html", description: "交互式天气报告" }, { path: "output/weather-notes.md", description: "数据来源与说明" }] }), output: "Presented output/shanghai-weather.html\nPresented output/weather-notes.md", status: "done", |
| 34 | presentedFiles: [{ path: "output/shanghai-weather.html", description: "交互式天气报告" }, { path: "output/weather-notes.md", description: "数据来源与说明" }] }, |
| 35 | { ...end, id: "weather-final", text: "今天上海天气如下。以下为界面回放测试数据。\n\n## 上海 · 今日实况\n\n| 项目 | 数值 |\n|---|---|\n| 天气 | 晴 ☀️ |\n| 气温 | **25.5 °C** |\n| 湿度 | 66% |\n\n**全天**:多云转晴,23~30 °C。", reasoning: "" } as Item, |
| 36 | ]; |
| 37 | } |
| 38 | declare global { interface Window { chatFixture: { authored(): void; weather(): void; replace(count: number): void; reset(count: number): void; older(): void; tick(index: number): void; settle(): void; switchSession(): void; prepend(): void; ready: number; pending(): number } } } |
| 39 | function Fixture() { |
| 40 | const [items, setItems] = useState(() => makeTurns(20)); |
| 41 | const [session, setSession] = useState(0); |
| 42 | const [running, setRunning] = useState(false); |
| 43 | const [ready, setReady] = useState(0); |
| 44 | const itemsRef = useRef(items); |
| 45 | itemsRef.current = items; |
| 46 | const liveRef = useRef<LiveStream>(); |
| 47 | const liveListeners = useRef(new Set<() => void>()); |
| 48 | // Production stream deltas bypass the full controller tree through this |
| 49 | // store. Keep the benchmark on that path so input timing measures the UI |
| 50 | // users run instead of rebuilding the complete history fixture per token. |
| 51 | const liveStore = useMemo<ControllerLiveStore>(() => ({ |
| 52 | subscribe: (_tabId, listener) => { |
| 53 | liveListeners.current.add(listener); |
| 54 | return () => { liveListeners.current.delete(listener); }; |
| 55 | }, |
| 56 | getSnapshot: () => liveRef.current, |
| 57 | }), []); |
| 58 | const publishLive = () => liveListeners.current.forEach(listener => listener()); |
| 59 | const clearLive = () => { liveRef.current = undefined; publishLive(); }; |
| 60 | useLayoutEffect(() => { |
| 61 | window.chatFixture = { |
| 62 | ready, pending: () => getMarkdownWorkerClient().stats().pending, |
| 63 | authored: () => { |
| 64 | const messages = [ |
| 65 | { role: "user", origin: "host", content: '<session-context version="1">private environment</session-context>' }, |
| 66 | { role: "user", origin: "user", content: '<response-language>internal policy</response-language>你是谁', raw_content: "你是谁" }, |
| 67 | { role: "user", content: '<capability-route>legacy internal route</capability-route>旧会话问题' }, |
| 68 | { role: "user", origin: "user", raw_content: '<response-language>用户引用的 XML</response-language>' }, |
| 69 | { role: "assistant", content: "我是 Reasonix。" }, |
| 70 | ].map((raw, i) => canonicalMessage({ messageId: `authored-${i}`, position: i, version: 1, role: raw.role }, raw)); |
| 71 | clearLive(); setItems(historyMessagesToItems(messages, "authored").items); setRunning(false); setSession(value => value + 1); |
| 72 | }, |
| 73 | weather: () => { clearLive(); setItems(weatherTurn()); setRunning(false); setSession(value => value + 1); }, |
| 74 | replace: count => { clearLive(); setItems(makeTurns(count)); setRunning(false); setReady(value => value + 1); }, |
| 75 | reset: count => { clearLive(); setItems(makeTurns(Math.min(60, count), Math.max(0, count - 60))); setRunning(false); setSession(value => value + 1); setReady(value => value + 1); }, |
| 76 | older: () => setItems(previous => { const start = Number(previous[0].id.slice(1)); return [...makeTurns(Math.min(60, start), Math.max(0, start - 60)), ...previous]; }), |
| 77 | tick: index => { |
| 78 | const assistant = [...itemsRef.current].reverse().find((item): item is Extract<Item, { kind: "assistant" }> => item.kind === "assistant"); |
| 79 | if (!assistant) return; |
| 80 | liveRef.current = { id: assistant.id, text: `Live answer\n\n${"Stable paragraph.\n\n".repeat(index + 1)}`, |
| 81 | reasoning: `thinking ${index}`, reasoningComplete: false }; |
| 82 | setRunning(true); |
| 83 | publishLive(); |
| 84 | }, |
| 85 | settle: () => { |
| 86 | const live = liveRef.current; |
| 87 | liveRef.current = undefined; |
| 88 | setRunning(false); |
| 89 | if (live) setItems(previous => previous.map(item => item.kind === "assistant" && item.id === live.id |
| 90 | ? { ...item, text: live.text, reasoning: live.reasoning, reasoningComplete: live.reasoningComplete, streaming: false } : item)); |
| 91 | publishLive(); |
| 92 | }, |
| 93 | switchSession: () => setSession(value => value + 1), |
| 94 | prepend: () => setItems(previous => [{ kind: "user", id: `older${previous.length}`, text: "An older question" }, { kind: "assistant", id: `older-answer${previous.length}`, text: "Earlier context\n\n".repeat(20), reasoning: "", streaming: false }, ...previous]), |
| 95 | }; |
| 96 | }, [ready]); |
| 97 | return <div style={{ height: "100vh", display: "flex", flexDirection: "column", background: "var(--bg)" }}> |
| 98 | <Transcript items={items} liveStore={liveStore} tabId="chat-bench" geometrySessionKey={`fixture-${session}`} running={running} onPrompt={() => {}} |
| 99 | onFork={() => {}} hasOlderHistory={items[0]?.id !== "u0"} onLoadOlderHistory={() => { window.chatFixture.older(); return true; }} /> |
| 100 | <div style={{ flex: "none", maxHeight: "40vh", padding: 16 }}><Composer running={running} collaborationMode="normal" toolApprovalMode="ask" modelLabel="DeepSeek" tabId="chat-bench" |
| 101 | onSend={() => {}} onCancel={async () => ({ discardedItemIds: [] })} onCycleMode={() => {}} onSetMode={() => {}} |
| 102 | onSetCollaborationMode={() => {}} onSetToolApprovalMode={() => {}} onToggleYoloApprovalMode={() => {}} |
| 103 | onClearGoal={() => {}} onPauseGoal={() => {}} onResumeGoal={() => {}} onSwitchModel={() => true} onSetEffort={() => {}} /> |
| 104 | </div> |
| 105 | </div>; |
| 106 | } |
| 107 | createRoot(document.getElementById("root")!).render(<LocaleProvider><ToastProvider><Fixture /></ToastProvider></LocaleProvider>); |
| 108 |