"use client"; /** * The exam page (practice slice 5, one-attempt redesign 2026-09-29): * one problem set, numbered problems, markdown statements, a textarea + * dictation per problem (the bar renders only on the FOCUSED box — * thinking out loud is the primary answering mode). ONE ATTEMPT: each * problem has its OWN submit button; once submitted the box locks and * the MODEL SOLUTION reveals instantly (rubric collapsed by default). * The grade (score 0–10 + brief reasoning) lands via the events channel and * updates live — no reload. * * TOP BAR (owner, UI round 2026-09-29): the chat pattern — an h-15 * lh-drag header in flow above the scroller, the sidebar reopen toggle * first (it clears the traffic lights via .lh-closed-toggle), the set * title truncating beside it. The levels + status stay in the body. * * DISPLAY-SEAM MATH HEALING: statements/solutions/rubrics/reasoning ride * the same streaming-args parse as quiz/ask strings — restoreMathEscapes * heals them at render (the same helper the tool-part seam uses). * * POLLING: 2s while the set isn't ready (stubs → filled), and while a * grading run is live for this set (activity feed practice rows are * keyed setId:grade:problemId — the LIVE rows name exactly which * problems' grades are pending). A settled page goes quiet; a dropped * grade shows "grade interrupted" honestly. */ import { useCallback, useEffect, useRef, useState } from "react"; import { useServerEvent, useServerResync } from "@/components/server-events"; import type { Problem, ProblemSet } from "@/src/core"; import { CUE_LEVELS, LADDER_RUNGS, PRESENTATIONS } from "@/src/core"; import { restoreMathEscapes } from "@/src/core"; import { MessageResponse } from "@/components/ai-elements/message"; import { TopBar } from "@/components/top-bar"; import { BrailleSpinner } from "@/components/braille-spinner"; import { DictationBar, useDictation } from "@/components/dictation"; /** One answerable problem: textarea + focus-gated dictation bar. */ export function ProblemBox({ problem, draft, onDraft, }: { problem: Pick; draft: string; onDraft: (text: string) => void; }) { const [focused, setFocused] = useState(false); // The dictation appends into THIS box only (per-box instance). const dictation = useDictation({ append: (text) => onDraft(text), }); return (