| 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, State } from "../src/lib/useController"; |
| 11 | import { initialState, reducer } from "../src/lib/useController"; |
| 12 | import type { RuntimeState } from "../src/lib/runtimeStateStore"; |
| 13 | import "../src/styles.css"; |
| 14 | |
| 15 | function makeTurns(count: number, start = 0): Item[] { |
| 16 | return Array.from({ length: count }, (_, position): Item[] => { const index = start + position; return [ |
| 17 | { kind: "user", id: `u${index}`, text: `Question ${index + 1}: Explain the implementation`, checkpointTurn: index + 1 }, |
| 18 | { 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" }, |
| 19 | { 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, |
| 20 | createdAt: new Date(2026, 8, 12, 12, 34).getTime(), turnDurationMs: 29_000, tokensPerSecond: 183, |
| 21 | turnUsage: { totalTokens: 185_225, uncachedInputTokens: 26_278, cacheReadTokens: 155_520, outputTokens: 3_427, reasoningTokens: 1_909, routes: ["deepseek-official/deepseek-flash"] } }, |
| 22 | ]; }).flat(); |
| 23 | } |
| 24 | function weatherTurn(): Item[] { |
| 25 | const end = makeTurns(1)[2]; |
| 26 | return [ |
| 27 | { kind: "user", id: "weather-user", text: "查一下今天上海的天气", checkpointTurn: 1 }, |
| 28 | { kind: "assistant", id: "weather-thought", text: "", reasoning: "先查询实时天气,再交叉核对来源。", streaming: false }, |
| 29 | { kind: "tool", id: "weather-search", name: "web_search", args: JSON.stringify({ query: "上海今天天气 实时温度" }), output: "Weather sources", searchSources: [{ title: "天气数据(测试来源)", url: "https://example.com/weather" }], status: "done" }, |
| 30 | { kind: "tool", id: "weather-error", name: "web_fetch", args: JSON.stringify({ url: "https://example.com/weather" }), error: "DNS lookup failed", status: "error" }, |
| 31 | { kind: "assistant", id: "weather-progress", text: "一个来源暂时无法访问,正在核对另一来源。", reasoning: "切换到另一来源。", streaming: false }, |
| 32 | { 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" }, |
| 33 | { kind: "tool", id: "weather-bash", name: "bash", args: JSON.stringify({ command: "printf '25.5°C 晴\\n'", description: "获取并核对上海天气" }), output: "25.5°C 晴\n", status: "done", |
| 34 | execution: { kind: "shell", shell: "bash", state: "completed", exitCode: 0, supportsAndAnd: true } }, |
| 35 | { kind: "tool", id: "weather-write-html", name: "write_file", args: JSON.stringify({ path: "output/shanghai-weather.html" }), output: "wrote output/shanghai-weather.html", readOnly: false, status: "done", fileDiff: { diff: "", added: 4, removed: 0 } }, |
| 36 | { kind: "tool", id: "weather-write-notes", name: "write_file", args: JSON.stringify({ path: "output/weather-notes.md" }), output: "wrote output/weather-notes.md", readOnly: false, status: "done", fileDiff: { diff: "", added: 1, removed: 1 } }, |
| 37 | { 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", |
| 38 | presentedFiles: [{ path: "output/shanghai-weather.html", description: "交互式天气报告" }, { path: "output/weather-notes.md", description: "数据来源与说明" }] }, |
| 39 | { kind: "notice", id: "weather-result", level: "info", text: "Turn complete", completionSummary: { |
| 40 | preset: "balanced", verdict: "complete", mutations: 2, changed_files: 2, checks_passed: 1, checks_failed: 0, |
| 41 | checks_suppressed: 0, review: "passed", constraint_degraded: false, |
| 42 | receipt: { verdict: "complete", diff: { id: "weather-diff", turn: 0, coverage: "complete", added: 5, removed: 1, reasons: [], files: [ |
| 43 | { path: "output/shanghai-weather.html", kind: "create", added: 4, removed: 0 }, |
| 44 | { path: "output/weather-notes.md", kind: "create", added: 1, removed: 1 }, |
| 45 | ] } }, |
| 46 | } }, |
| 47 | { ...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, |
| 48 | ]; |
| 49 | } |
| 50 | declare global { interface Window { chatFixture: { steer(order: "record-first" | "event-first", count: number): void; unappliedSteer(order: "record-first" | "event-first", count: number): void; maintenance(stage: "start" | "refresh" | "completed" | "unknown"): void; authored(): void; toolAliasRegression(): void; backgroundLaunch(): void; backgroundOutputHistory(): 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 } } } |
| 51 | function Fixture() { |
| 52 | const [items, setItems] = useState(() => new URLSearchParams(window.location.search).has("deliverables") ? weatherTurn() : makeTurns(20)); |
| 53 | const [session, setSession] = useState(0); |
| 54 | const [running, setRunning] = useState(false); |
| 55 | const [ready, setReady] = useState(0); |
| 56 | const itemsRef = useRef(items); |
| 57 | const maintenanceState = useRef(initialState); |
| 58 | itemsRef.current = items; |
| 59 | const liveRef = useRef<LiveStream>(); |
| 60 | const liveListeners = useRef(new Set<() => void>()); |
| 61 | // Production stream deltas bypass the full controller tree through this |
| 62 | // store. Keep the benchmark on that path so input timing measures the UI |
| 63 | // users run instead of rebuilding the complete history fixture per token. |
| 64 | const liveStore = useMemo<ControllerLiveStore>(() => ({ |
| 65 | subscribe: (_tabId, listener) => { |
| 66 | liveListeners.current.add(listener); |
| 67 | return () => { liveListeners.current.delete(listener); }; |
| 68 | }, |
| 69 | getSnapshot: () => liveRef.current, |
| 70 | }), []); |
| 71 | const publishLive = () => liveListeners.current.forEach(listener => listener()); |
| 72 | const clearLive = () => { liveRef.current = undefined; publishLive(); }; |
| 73 | useLayoutEffect(() => { |
| 74 | window.chatFixture = { |
| 75 | ready, pending: () => getMarkdownWorkerClient().stats().pending, |
| 76 | steer: (order, count) => { |
| 77 | let state: State = { ...initialState, transcriptProtocol: 2 }; |
| 78 | const text = "对比源码信息应该更准一些"; |
| 79 | const formal: Item[] = []; |
| 80 | for (let i = 0; i < count; i++) { |
| 81 | const messageId = `steer-${i}`; |
| 82 | const event = { type: "event" as const, e: { kind: "steer" as const, messageId, itemId: `inbox-${i}`, text } }; |
| 83 | if (order === "event-first") state = reducer(state, event); |
| 84 | formal.push(...historyMessagesToItems([{ role: "notice", messageId, recordId: `m:${messageId}`, content: `↪ ${text}` }], "steer").items); |
| 85 | state = reducer(state, { type: "transcript_records", confirmedUsers: [], projection: { |
| 86 | items: [...formal], removeIds: [], startTurn: 1, endTurn: 1, totalTurns: 1, hasOlder: false, hasNewer: false, |
| 87 | revision: i + 1, revisionKnown: true, digest: "steer", |
| 88 | } }); |
| 89 | state = reducer(state, event); |
| 90 | } |
| 91 | clearLive(); setItems(state.items); setRunning(false); setSession(value => value + 1); |
| 92 | }, |
| 93 | unappliedSteer: (order, count) => { |
| 94 | let state: State = { ...initialState, transcriptProtocol: 2 }; |
| 95 | const text = "Guidance was not applied:\n对比源码信息应该更准一些"; |
| 96 | const formal: Item[] = []; |
| 97 | for (let i = 0; i < count; i++) { |
| 98 | const messageId = `unapplied-${i}`; |
| 99 | const event = { type: "event" as const, e: { kind: "notice" as const, code: "unapplied_steer", level: "warn" as const, messageId, text } }; |
| 100 | if (order === "event-first") state = reducer(state, event); |
| 101 | formal.push(...historyMessagesToItems([{ role: "notice", messageId, recordId: `m:${messageId}:notice:0`, |
| 102 | code: "unapplied_steer", level: "warn", content: text }], "unapplied").items); |
| 103 | state = reducer(state, { type: "transcript_records", confirmedUsers: [], projection: { |
| 104 | items: [...formal], removeIds: [], startTurn: 1, endTurn: 1, totalTurns: 1, hasOlder: false, hasNewer: false, |
| 105 | revision: i + 1, revisionKnown: true, digest: "unapplied", |
| 106 | } }); |
| 107 | state = reducer(state, event); |
| 108 | } |
| 109 | clearLive(); setItems(state.items); setRunning(false); setSession(value => value + 1); |
| 110 | }, |
| 111 | maintenance: stage => { |
| 112 | const op = { operationId: "bench-maintenance", kind: "compact", status: "running", activity: "running", operationRevision: 1, runtimeEpoch: "bench-runtime" }; |
| 113 | const idle: RuntimeState = { schemaVersion: 1, projectionEpoch: "bench-projection", runtimeEpoch: "bench-runtime", activityRevision: 1, revision: 1, |
| 114 | phase: "idle", running: false, turnId: "", turnStatus: "", turnEventSeq: 0, pendingPrompt: false, cancelRequested: false, cancellable: false, backgroundJobs: 0, activity: "" }; |
| 115 | let state = maintenanceState.current; |
| 116 | if (stage === "start" || stage === "unknown") { |
| 117 | state = reducer({ ...initialState, items: makeTurns(1) }, { type: "runtime_snapshot", snapshot: idle }); |
| 118 | state = reducer(state, { type: "event", e: { kind: "session_operation", sessionOperation: { ...op, ...(stage === "unknown" ? { status: "future_state", activity: "" } : {}) } } }); |
| 119 | clearLive(); setSession(value => value + 1); |
| 120 | } else if (stage === "refresh") { |
| 121 | const message = canonicalMessage({ messageId: `maintenance:${op.operationId}`, position: 3, version: 1, role: "compaction" }, { role: "compaction", content: JSON.stringify(op) }); |
| 122 | state = reducer(state, { type: "history", messages: [{ role: "user", messageId: "u0", content: "Question 1" }, { role: "assistant", messageId: "a0", content: "Existing answer" }, message] }); |
| 123 | state = reducer(state, { type: "runtime_snapshot", snapshot: { ...idle, revision: 2, phase: "executing", running: true, maintenance: op } }); |
| 124 | } else { |
| 125 | state = reducer(state, { type: "event", e: { kind: "session_operation", sessionOperation: { ...op, status: "completed", activity: "finalizing", operationRevision: 2, summary: "Durable compression summary", inputTokens: 1200, resultTokens: 400, applied: true } } }); |
| 126 | } |
| 127 | maintenanceState.current = state; setItems(state.items); setRunning(false); |
| 128 | }, |
| 129 | authored: () => { |
| 130 | const messages = [ |
| 131 | { role: "user", origin: "host", content: '<session-context version="1">private environment</session-context>' }, |
| 132 | { role: "user", origin: "user", content: '<response-language>internal policy</response-language>你是谁', raw_content: "你是谁" }, |
| 133 | { role: "user", content: '<capability-route>legacy internal route</capability-route>旧会话问题' }, |
| 134 | { role: "user", origin: "user", raw_content: '<response-language>用户引用的 XML</response-language>' }, |
| 135 | { role: "assistant", content: "我是 Reasonix。" }, |
| 136 | ].map((raw, i) => canonicalMessage({ messageId: `authored-${i}`, position: i, version: 1, role: raw.role }, raw)); |
| 137 | clearLive(); setItems(historyMessagesToItems(messages, "authored").items); setRunning(false); setSession(value => value + 1); |
| 138 | }, |
| 139 | weather: () => { clearLive(); setItems(weatherTurn()); setRunning(false); setSession(value => value + 1); }, |
| 140 | backgroundLaunch: () => { |
| 141 | clearLive(); setItems([ |
| 142 | { kind: "user", id: "launch-user", text: "Run the build" }, |
| 143 | ...["pwsh", "bash"].map((name): Item => ({ kind: "tool", id: `launch-${name}`, name, args: "{}", status: "done", output: "Background job started", execution: { state: "background_started", shell: name } })), |
| 144 | { kind: "assistant", id: "launch-final", text: "Build started.", streaming: false }, |
| 145 | ]); setRunning(false); setSession(value => value + 1); |
| 146 | }, |
| 147 | backgroundOutputHistory: () => { |
| 148 | const calls = ["running", "failed", "cancelled", "completed"].map(state => ({ |
| 149 | id: `poll-${state}`, name: "job_output", arguments: "{}", |
| 150 | resultObservation: { state: "completed" as const, messageId: `result-${state}`, version: 1 }, |
| 151 | })); |
| 152 | const projected = historyMessagesToItems([ |
| 153 | { role: "user", content: "Check the background jobs" }, |
| 154 | { role: "assistant", content: "", toolCalls: calls }, |
| 155 | ...calls.map(call => ({ role: "tool", messageId: call.resultObservation.messageId, toolName: call.name, |
| 156 | toolCallId: call.id, content: "Output retrieved", execution: { state: call.id.slice("poll-".length) } })), |
| 157 | { role: "assistant", content: "Output checked." }, |
| 158 | ], "output-history"); |
| 159 | clearLive(); setItems(projected.items); setRunning(false); setSession(value => value + 1); |
| 160 | }, |
| 161 | toolAliasRegression: () => { |
| 162 | const user: Item = { kind: "user", id: "alias-user", text: "创建文件", checkpointTurn: 1 }; |
| 163 | const tool: Item = { kind: "tool", id: "alias-call", name: "bash", args: "printf ok", readOnly: false, status: "done", output: "ok", |
| 164 | execution: { kind: "shell", shell: "bash", state: "completed", exitCode: 0, supportsAndAnd: true } }; |
| 165 | const final: Item = { kind: "assistant", id: "alias-final", text: "已完成。", reasoning: "", streaming: false }; |
| 166 | const projected = reducer({ ...initialState, items: [user, final, tool, { ...tool }] }, { type: "transcript_records", confirmedUsers: [], projection: { |
| 167 | items: [user, tool, final], removeIds: [], startTurn: 1, endTurn: 1, totalTurns: 1, hasOlder: false, hasNewer: false, |
| 168 | revision: 1, revisionKnown: true, digest: "alias-regression", |
| 169 | } }); |
| 170 | clearLive(); setItems(projected.items); setRunning(false); setSession(value => value + 1); |
| 171 | }, |
| 172 | replace: count => { clearLive(); setItems(makeTurns(count)); setRunning(false); setReady(value => value + 1); }, |
| 173 | reset: count => { clearLive(); setItems(makeTurns(Math.min(60, count), Math.max(0, count - 60))); setRunning(false); setSession(value => value + 1); setReady(value => value + 1); }, |
| 174 | older: () => setItems(previous => { const start = Number(previous[0].id.slice(1)); return [...makeTurns(Math.min(60, start), Math.max(0, start - 60)), ...previous]; }), |
| 175 | tick: index => { |
| 176 | const assistant = [...itemsRef.current].reverse().find((item): item is Extract<Item, { kind: "assistant" }> => item.kind === "assistant"); |
| 177 | if (!assistant) return; |
| 178 | liveRef.current = { id: assistant.id, text: `Live answer\n\n${"Stable paragraph.\n\n".repeat(index + 1)}`, |
| 179 | reasoning: `thinking ${index}`, reasoningComplete: false }; |
| 180 | setRunning(true); |
| 181 | publishLive(); |
| 182 | }, |
| 183 | settle: () => { |
| 184 | const live = liveRef.current; |
| 185 | liveRef.current = undefined; |
| 186 | setRunning(false); |
| 187 | if (live) setItems(previous => previous.map(item => item.kind === "assistant" && item.id === live.id |
| 188 | ? { ...item, text: live.text, reasoning: live.reasoning, reasoningComplete: live.reasoningComplete, streaming: false } : item)); |
| 189 | publishLive(); |
| 190 | }, |
| 191 | switchSession: () => setSession(value => value + 1), |
| 192 | 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]), |
| 193 | }; |
| 194 | }, [ready]); |
| 195 | return <div style={{ height: "100vh", display: "flex", flexDirection: "column", background: "var(--bg)" }}> |
| 196 | {new URLSearchParams(window.location.search).has("deliverables") && <div role="note" style={{ padding: "8px 16px", color: "var(--fg-dim)" }}>组件回放 · 示例数据。此页未连接桌面文件与 Diff 服务;完整交互请在桌面应用中验证。</div>} |
| 197 | <Transcript items={items} liveStore={liveStore} tabId="chat-bench" geometrySessionKey={`fixture-${session}`} running={running} onPrompt={() => {}} |
| 198 | onFork={() => {}} |
| 199 | hasOlderHistory={/^u[1-9]\d*$/.test(items[0]?.id ?? "")} onLoadOlderHistory={() => { window.chatFixture.older(); return true; }} /> |
| 200 | <div style={{ flex: "none", maxHeight: "40vh", padding: 16 }}><Composer running={running} collaborationMode="normal" toolApprovalMode="ask" modelLabel="DeepSeek" tabId="chat-bench" |
| 201 | onSend={() => {}} onCancel={async () => ({ discardedItemIds: [] })} onCycleMode={() => {}} onSetMode={() => {}} |
| 202 | onSetCollaborationMode={() => {}} onSetToolApprovalMode={() => {}} onToggleYoloApprovalMode={() => {}} |
| 203 | onClearGoal={() => {}} onPauseGoal={() => {}} onResumeGoal={() => {}} onSwitchModel={() => true} onSetEffort={() => {}} /> |
| 204 | </div> |
| 205 | </div>; |
| 206 | } |
| 207 | createRoot(document.getElementById("root")!).render(<LocaleProvider><ToastProvider><Fixture /></ToastProvider></LocaleProvider>); |
| 208 |