From cf0ec4a2f014bd8e1dbac21609741c5f4decedba Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Tue, 29 Sep 2026 21:54:32 +0300 Subject: [PATCH] =?UTF-8?q?practice=20UI=20round=20=E2=80=94=20top=20bars?= =?UTF-8?q?=20+=20sidebar=20reopen,=20one-attempt=20per-problem=20submit?= =?UTF-8?q?=20with=20instant=20reveal,=20score-only=20grading=20(owner=202?= =?UTF-8?q?026-09-29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- app/api/practice/sets/[id]/answers/route.ts | 5 +- components/practice-exam.tsx | 648 ++++++++++++-------- components/practice-list.tsx | 446 ++++++++------ docs/architecture.md | 12 +- next-env.d.ts | 4 +- src/agent/subagents/practice-pipeline.ts | 25 +- src/agent/subagents/practice.ts | 30 +- src/core/contracts/practice.ts | 20 +- src/store/practice.ts | 30 +- tests/practice-agents.test.ts | 18 +- tests/practice-pipeline.test.ts | 32 +- tests/practice-store.test.ts | 22 +- tests/render-practice.test.tsx | 71 ++- 14 files changed, 811 insertions(+), 554 deletions(-) diff --git a/README.md b/README.md index 530368f..48beb78 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ data/ chats/ · graph/ (six ledgers + positions/source **The knowledge graph (two channels, one writer).** The write channel: the extraction pipeline — extractor (LLM: sees only the session, rendered as readable markdown by the shared `core/session-markdown.ts` engine — propositions as true statements, `follows`/`supports` edges with verbatim evidence; fully trusted, graph-blind) → auditor (LLM, an agentic loop that never sees the source: explores the existing graph through `find` / `list_sessions` / `list_propositions` tools, stamps each proposition with an identity verdict — link to an existing node (equivalence bias: paraphrases merge) or create — and may draw bridging edges between existing nodes and the new batch) → merger (`mergeFindings`, deterministic: applies the verdicts, link-appends phrasings/provenance, resolves and dedupes edges, checkpoints coverage) → layout settle. No quiz outcomes or understanding assessments live in the graph (2026-09-27 cleanup); practice (`/practice`) consumes the graph as feedback-only problem sets and writes nothing back. Triggered when a chat's **uncovered tokens** (message-view fingerprints, `coverage.jsonl`) cross `graph.extractionChunkTokens` (a settings knob, default 4096, floor 500 — read fresh per turn); failure checkpoints nothing, the next trigger re-runs. The same pipeline serves the **notes front door** (`POST /api/graph/import`: any `*.md`/`*.txt` becomes a source; re-imports are idempotent by path hash). The read channel: `GET /api/graph` derives the snapshot from the ledgers — a pure read; the home view is a hand-rolled Canvas 2D renderer (one rAF loop, dirty-flag idle, d3-zoom camera, zoom-gated labels, cluster hulls) over a d3-force layout, with server-persisted render settings and drag-committed positions. Clusters are **birth-session groupings**: a node joins the cluster of the session that birthed it (a chat, or an imported notes file as a file-kind session), derived at read — no embedding pass, no naming pass. The teacher reaches the graph on its own terms through the gated prober tools (`list_sessions` / `list_propositions`, the `agent.graphTools` setting) — there is no automatic context injection. -**Practice (problem sets).** The graph's `follows`-edges become reasoning chains; the `/practice` creation dialog picks a birth-session cluster (the graph view's own groups) and freezes its propositions + chains into a self-contained `ProblemSet`. Three named-level axes steer generation (ladder rung 0–4, cue distance 0–2, Theory|Application — never a number to interpolate; defaults Direct exercise · Adjacent context · Theory). A partitioner agent covers every chain exactly once, one generator per partition writes problems against the owner's problem-engineering framework (commit-before-pose: statement + solution + rubric together), and grading is **strengthening, not assessment** — one grader per answer, verdict (`solid`/`partial`/`missed`) + feedback, post-grade reveal, nothing ever written back to the graph. Runs are registry-backed background jobs on the activity dock (no steer, never wakes the teacher); sets live in `data/practice`, crash-swept on boot. +**Practice (problem sets).** The graph's `follows`-edges become reasoning chains; the `/practice` creation dialog picks a birth-session cluster (the graph view's own groups) and freezes its propositions + chains into a self-contained `ProblemSet`. Three named-level axes steer generation (ladder rung 0–4, cue distance 0–2, Theory|Application — never a number to interpolate; defaults Direct exercise · Adjacent context · Theory). A partitioner agent covers every chain exactly once, one generator per partition writes problems against the owner's problem-engineering framework (commit-before-pose: statement + solution + rubric together), and grading is **strengthening, not assessment** — one attempt per problem with its own submit button, instant solution/rubric reveal, then a score 0–10 with brief reasoning landing live; nothing ever written back to the graph. Runs are registry-backed background jobs on the activity dock (no steer, never wakes the teacher); sets live in `data/practice`, crash-swept on boot. **Dictation.** Optional speech-to-text: the server mints short-lived Deepgram tokens; audio streams browser→Deepgram over WebSocket, never through the server. diff --git a/app/api/practice/sets/[id]/answers/route.ts b/app/api/practice/sets/[id]/answers/route.ts index c6c3475..2e54636 100644 --- a/app/api/practice/sets/[id]/answers/route.ts +++ b/app/api/practice/sets/[id]/answers/route.ts @@ -2,8 +2,9 @@ * POST /api/practice/sets/[id]/answers — submit answers for grading * (202; one grading job per answered problem runs in the background). * Body: {answers: [{problemId, text}]}. Set must be ready; answers must - * be non-empty text on known, not-yet-graded problems (400/409 via the - * service's typed-error mapping). + * be non-empty text on known, not-yet-answered problems (400/409 via the + * service's typed-error mapping). ONE ATTEMPT (owner 2026-09-29): once a + * problem carries an answer it is done — re-submission is refused. */ import { submitPracticeAnswersResponse } from "@/src/server"; diff --git a/components/practice-exam.tsx b/components/practice-exam.tsx index 0a87f76..dd5954a 100644 --- a/components/practice-exam.tsx +++ b/components/practice-exam.tsx @@ -1,282 +1,438 @@ "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. + * 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 poll and + * updates live — no reload. * - * DISPLAY-SEAM MATH HEALING: statements/solutions/rubrics/feedback ride + * 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's practice rows) with - * answered-but-ungraded problems outstanding. A settled page goes quiet - * (a dropped grade terminates honestly: no live row → no poll). + * 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 type { GradeVerdict, Problem, ProblemSet } from "@/src/core"; +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 { 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"; -} +import { SidebarToggle } from "@/components/sidebar"; /** One answerable problem: textarea + focus-gated dictation bar. */ export function ProblemBox({ - problem, - draft, - onDraft, + problem, + draft, + onDraft, }: { - problem: Pick; - draft: string; - onDraft: (text: string) => void; + 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 ( -
-