v2 web interface: fix delivery races, real URLs, optimistic echo, thinking stream, button-based quiz UI
Root causes of the v1 browser bugs (all fixed + covered by deterministic tests): - duplicated messages: per-socket snapshot + global broadcast marking raced; replaced with a single append-only entry log (exactly-once per socket, structurally) — covered by tests/server.test.ts - delayed/missing echo of own messages: optimistic append with sending/failed status, reconciled by text when the real entry arrives - no thinking shown: thinking deltas streamed live (kind: thinking) and persisted in assistant entries so they survive reloads - ask answers graded as wrong: AnsweredCard no longer applies quiz grading to ask answers (no ✓/✗, no verdict) - broken option selection: native radio/checkbox inputs fighting React state replaced with button-based options (single source of truth, one-click select) - no URLs: History API routing — / home, /s/<file> per session (deep links, refresh, back button); session view resumes its file on mount - lost connection = dead UI: exponential-backoff reconnect with full resync - stick-to-bottom transcript with jump-to-latest pill Server: startServer() with injectable runtime factory (scripted model in tests); createSession returns file for routing; scripted model gained thinking turns. 51 deterministic tests green.
This commit is contained in:
@@ -4,10 +4,10 @@ import { GraphPlaceholder } from "../components/GraphPlaceholder.tsx";
|
||||
|
||||
interface HomeProps {
|
||||
sessions: SessionSummary[];
|
||||
onStart: (topic: string) => void;
|
||||
onResume: (file: string) => void;
|
||||
onStart: (topic: string) => Promise<void>;
|
||||
onOpen: (file: string) => void;
|
||||
onRefresh: () => void;
|
||||
hasActive: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
@@ -24,7 +24,7 @@ function formatDate(iso: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function Home({ sessions, onStart, onResume, onRefresh }: HomeProps) {
|
||||
export function Home({ sessions, onStart, onOpen, onRefresh, error }: HomeProps) {
|
||||
const [topic, setTopic] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
@@ -52,7 +52,7 @@ export function Home({ sessions, onStart, onResume, onRefresh }: HomeProps) {
|
||||
<div className="session-list">
|
||||
{sessions.length === 0 && <div className="empty">No sessions yet</div>}
|
||||
{sessions.map((s) => (
|
||||
<button key={s.id} className="session-item" onClick={() => onResume(s.file)}>
|
||||
<button key={s.id} className="session-item" onClick={() => onOpen(s.file)}>
|
||||
<div className="session-name">{s.name || s.preview || "Untitled"}</div>
|
||||
<div className="session-meta">
|
||||
{formatDate(s.updatedAt)} · {s.messageCount} msg{s.messageCount === 1 ? "" : "s"}
|
||||
@@ -75,9 +75,10 @@ export function Home({ sessions, onStart, onResume, onRefresh }: HomeProps) {
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => void submit()} disabled={busy || !topic.trim()}>
|
||||
{busy ? "…" : "Start"}
|
||||
{busy ? "Starting…" : "Start"}
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className="error-banner">{error}</div>}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user