| 1 | // Browser fixture for the transcript's turn-fork entry. It renders the real |
| 2 | // Transcript with the props the app shell passes and drives the real |
| 3 | // create-only binding, so a browser run shows exactly the states a user sees. |
| 4 | import { useCallback, useLayoutEffect, useState } from "react"; |
| 5 | import { createRoot } from "react-dom/client"; |
| 6 | import { Transcript } from "../src/components/Transcript"; |
| 7 | import { LocaleProvider, useI18n } from "../src/lib/i18n"; |
| 8 | import { app } from "../src/lib/bridge"; |
| 9 | import { forkCreateFailureText, type ForkTargetSetView, type ForkTargetView } from "../src/lib/forkTargets"; |
| 10 | import type { Item } from "../src/lib/useController"; |
| 11 | import "../src/styles.css"; |
| 12 | |
| 13 | // One persisted turn record: the boundary a fork may cut at, addressed by the |
| 14 | // message identity of the turn's final answer. |
| 15 | type TurnRecord = { turnId: string; user: string; answer: string; messageId?: string; open?: boolean }; |
| 16 | type SourceName = "completed" | "open" | "recordless"; |
| 17 | |
| 18 | const SOURCES: Record<SourceName, { records: TurnRecord[]; verifiable: boolean; running: boolean }> = { |
| 19 | completed: { |
| 20 | records: [ |
| 21 | { turnId: "turn-1", user: "Explain the retry policy", answer: "The retry policy backs off exponentially.", messageId: "a1" }, |
| 22 | { turnId: "turn-2", user: "Now summarize it", answer: "It retries with exponential backoff and a cap.", messageId: "a2" }, |
| 23 | ], |
| 24 | verifiable: true, |
| 25 | running: false, |
| 26 | }, |
| 27 | open: { |
| 28 | records: [ |
| 29 | { turnId: "turn-1", user: "Explain the retry policy", answer: "The retry policy backs off exponentially.", messageId: "a1" }, |
| 30 | { turnId: "turn-2", user: "Now summarize it", answer: "Summarizing the policy while the turn runs…", open: true }, |
| 31 | ], |
| 32 | verifiable: true, |
| 33 | running: true, |
| 34 | }, |
| 35 | recordless: { |
| 36 | records: [{ turnId: "turn-1", user: "Legacy question", answer: "A legacy answer with no turn record." }], |
| 37 | verifiable: false, |
| 38 | running: false, |
| 39 | }, |
| 40 | }; |
| 41 | |
| 42 | // The host derives one target per persisted turn; a turn that has not closed has |
| 43 | // no boundary, and a recordless source proves none at all. |
| 44 | function targetsOf(source: SourceName, records: TurnRecord[]): ForkTargetSetView { |
| 45 | return { |
| 46 | sourceSessionId: `bench-${source}`, sessionGeneration: 1, |
| 47 | targets: records.map((record, index) => record.open || !record.messageId |
| 48 | ? { sourceSessionId: `bench-${source}`, sessionGeneration: 1, turnId: record.turnId, boundarySequence: 0, turnNumber: index + 1, status: record.open ? "in_progress" : "committed", available: false, reason: "turn_open" } |
| 49 | : { sourceSessionId: `bench-${source}`, sessionGeneration: 1, turnId: record.turnId, boundarySequence: (index + 1) * 3, turnNumber: index + 1, status: "committed", messageId: record.messageId, available: true }), |
| 50 | verifiable: SOURCES[source].verifiable, |
| 51 | }; |
| 52 | } |
| 53 | |
| 54 | // Items carry the identity the transcript keys on: `m:<messageId>` when the |
| 55 | // record keeps one, the positional history key when it does not. |
| 56 | function itemsOf(records: TurnRecord[]): Item[] { |
| 57 | return records.flatMap((record): Item[] => [ |
| 58 | { kind: "user", id: `u:${record.turnId}`, text: record.user, checkpointTurn: Number(record.turnId.split("-")[1]) }, |
| 59 | { kind: "assistant", id: record.messageId ? `m:${record.messageId}` : `h:${record.turnId}`, text: record.answer, reasoning: "", streaming: Boolean(record.open) }, |
| 60 | ]); |
| 61 | } |
| 62 | |
| 63 | declare global { |
| 64 | interface Window { |
| 65 | forkFixture: { |
| 66 | source(name: SourceName): void; |
| 67 | locale(locale: "en" | "zh"): void; |
| 68 | calls(): Array<{ turnId: string; operationId: string }>; |
| 69 | notice(): string; |
| 70 | }; |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | function Fixture() { |
| 75 | const [source, setSource] = useState<SourceName>("completed"); |
| 76 | const [calls, setCalls] = useState<Array<{ turnId: string; operationId: string }>>([]); |
| 77 | const [notice, setNotice] = useState(""); |
| 78 | const { t, setPref } = useI18n(); |
| 79 | const { records, verifiable, running } = SOURCES[source]; |
| 80 | |
| 81 | // The same create-only binding the app shell clicks through: the host returns |
| 82 | // an operation id, and every refusal is surfaced as the user's notice. |
| 83 | const onFork = useCallback(async (target: ForkTargetView) => { |
| 84 | setNotice(""); |
| 85 | try { |
| 86 | const created = await app.CreateForkForTab("bench-fork-tab", target); |
| 87 | setCalls((current) => [...current, { turnId: target.turnId, operationId: created.operationId ?? "" }]); |
| 88 | if (created?.opened) { |
| 89 | if (created.operationId) await app.AcknowledgeForkOperation("bench-fork-tab", created.operationId); |
| 90 | return; |
| 91 | } |
| 92 | setNotice(created?.sessionId |
| 93 | ? t("chat.branchRecoverChild", { session: created.sessionId }) |
| 94 | : t("chat.branchFailedDetail", { detail: created?.error ?? "" })); |
| 95 | } catch (error) { |
| 96 | setNotice(forkCreateFailureText(error)); |
| 97 | } |
| 98 | }, [t]); |
| 99 | |
| 100 | useLayoutEffect(() => { |
| 101 | window.forkFixture = { |
| 102 | source: (name) => { setSource(name); setCalls([]); setNotice(""); }, |
| 103 | locale: (locale) => setPref(locale), |
| 104 | calls: () => calls, |
| 105 | notice: () => notice, |
| 106 | }; |
| 107 | }, [calls, notice, setPref]); |
| 108 | |
| 109 | return <div style={{ height: "40vh", display: "flex", flexDirection: "column", background: "var(--bg)" }}> |
| 110 | <Transcript items={itemsOf(records)} geometrySessionKey={`fixture-${source}`} running={running} tabId="bench-fork-tab" |
| 111 | onPrompt={() => {}} onFork={(target) => void onFork(target)} forkTargets={targetsOf(source, records)} forkBlocked={null} /> |
| 112 | {notice && <div role="status" data-fork-notice style={{ flex: "none", padding: "4px 16px", color: "var(--text)" }}>{notice}</div>} |
| 113 | <div data-fork-calls hidden>{calls.map((call, index) => <span key={index} data-fork-call data-turn={call.turnId}>{call.operationId}</span>)}</div> |
| 114 | </div>; |
| 115 | } |
| 116 | |
| 117 | createRoot(document.getElementById("root")!).render(<LocaleProvider><Fixture /></LocaleProvider>); |
| 118 |