"use client"; /** * The exam page (practice slice 5): 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 submit for the set, and the post-grade reveal * (verdict chip, feedback, model solution, rubric) under the box. * * DISPLAY-SEAM MATH HEALING: statements/solutions/rubrics/feedback 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's practice rows) with * answered-but-ungraded problems outstanding. A settled page goes quiet * (a dropped grade terminates honestly: no live row → no poll). */ import { useCallback, useEffect, useRef, useState } from "react"; import type { GradeVerdict, 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 { BrailleSpinner } from "@/components/braille-spinner"; import { DictationBar, useDictation } from "@/components/dictation"; /** The verdict chip's tone — plain, no invented color language. */ export function verdictClass(verdict: GradeVerdict): string { if (verdict === "solid") return "bg-green-500/15 text-green-500"; if (verdict === "partial") return "bg-amber-500/15 text-amber-600 dark:text-amber-400"; return "bg-destructive/15 text-destructive"; } /** 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 (