diff --git a/README.md b/README.md index 48beb78..b2abae0 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 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. +**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), plus an optional free-form note the agents weigh but need not follow. 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/route.ts b/app/api/practice/sets/route.ts index 4ce615e..8985da4 100644 --- a/app/api/practice/sets/route.ts +++ b/app/api/practice/sets/route.ts @@ -1,8 +1,9 @@ /** * POST /api/practice/sets — create a problem set (202 {id}; generation * runs in the background — the fire-and-forget pipeline). Body: - * {chatId, params} — everything this session touched - * {nodeIds, params} — a manual node selection + * {nodeIds, params, title?, note?} — a node selection, the cluster's + * label, and the learner's optional + * free-form note * Validation + typed-error mapping live in the service (server/practice). * GET — the set list (createdAt desc, the store's order). */ diff --git a/components/practice-dialog.tsx b/components/practice-dialog.tsx index 1604c87..1133f6d 100644 --- a/components/practice-dialog.tsx +++ b/components/practice-dialog.tsx @@ -33,282 +33,364 @@ import type { GraphClusterGroup, PracticeParams } from "@/src/core"; /** The creation defaults (owner, 2026-09-29): rung 2 Direct exercise, * cue 1 Adjacent context, Theory. */ export const DEFAULT_PRACTICE_PARAMS: PracticeParams = { - ladderRung: 2, - cueDistance: 1, - presentation: "theory", + ladderRung: 2, + cueDistance: 1, + presentation: "theory", }; export interface PracticeFailure { - status: number; - error: string; - code?: string; + status: number; + error: string; + code?: string; } /** The dialog's pure body — exported for SSR pins (the delete-session / * settings body pattern). The wrapper owns state + fetch + portal. */ export function PracticeDialogBody({ - params, - onParam, - clusters, - selectedKey, - onSelect, - busy, - failure, - onOpenSettings, - onClose, - onRun, + params, + onParam, + note, + onNote, + clusters, + selectedKey, + onSelect, + busy, + failure, + onOpenSettings, + onClose, + onRun, }: { - params: PracticeParams; - onParam: (patch: Partial) => void; - clusters: GraphClusterGroup[]; - selectedKey: string | null; - onSelect: (key: string) => void; - busy: boolean; - failure: PracticeFailure | null; - onOpenSettings: () => void; - onClose: () => void; - onRun: () => void; + params: PracticeParams; + onParam: (patch: Partial) => void; + /** The learner's free-form note (owner, 2026-09-29) — optional, + * typing-only; carried to the partitioner + generators as additional + * input they weigh but need not follow. */ + note: string; + onNote: (note: string) => void; + clusters: GraphClusterGroup[]; + selectedKey: string | null; + onSelect: (key: string) => void; + busy: boolean; + failure: PracticeFailure | null; + onOpenSettings: () => void; + onClose: () => void; + onRun: () => void; }) { - const canRun = !busy && selectedKey !== null; - return ( -
e.stopPropagation()} - className="flex max-h-[85vh] w-[32rem] max-w-[calc(100vw-2rem)] flex-col rounded-xl border bg-card p-5 shadow-xl" - > -

New problem set

+ const canRun = !busy && selectedKey !== null; + return ( +
e.stopPropagation()} + className="flex max-h-[85vh] w-[32rem] max-w-[calc(100vw-2rem)] flex-col rounded-xl border bg-card p-5 shadow-xl" + > +

New problem set

- {/* The cluster: a birth-session group of the graph, resolved to + {/* The cluster: a birth-session group of the graph, resolved to node ids on create (graph-domain, owner 2026-09-29). */} -
- Cluster -
- {clusters.map((cluster) => ( - - ))} - {clusters.length === 0 && ( -
- No clusters yet — a cluster appears once a session births its first proposition. -
- )} -
-
+
+ + Cluster + +
+ {clusters.map((cluster) => ( + + ))} + {clusters.length === 0 && ( +
+ No clusters yet — a cluster appears once a session births its + first proposition. +
+ )} +
+
- {/* The three parameters — named levels, never a number to + {/* The three parameters — named levels, never a number to interpolate (owner, 2026-09-29). Stepped sliders snap to the detents; each shows the level's NAME + its one-line definition; presentation is a two-segment toggle. */} -
-
- - Ladder - 0 · 1 · 2 · 3 · 4 - - onParam({ ladderRung: Number(e.target.value) as PracticeParams["ladderRung"] })} - className="h-1.5 w-full accent-primary" - /> - - {LADDER_RUNGS[params.ladderRung].name} - — {LADDER_RUNGS[params.ladderRung].definition} - -
-
- - Cue distance - 0 · 1 · 2 - - onParam({ cueDistance: Number(e.target.value) as PracticeParams["cueDistance"] })} - className="h-1.5 w-full accent-primary" - /> - - {CUE_LEVELS[params.cueDistance].name} - — {CUE_LEVELS[params.cueDistance].definition} - -
-
- Presentation -
- {(["theory", "application"] as const).map((pres) => ( - - ))} -
- - {PRESENTATIONS[params.presentation].definition} - -
-
+
+
+ + Ladder + 0 · 1 · 2 · 3 · 4 + + + onParam({ + ladderRung: Number( + e.target.value, + ) as PracticeParams["ladderRung"], + }) + } + className="h-1.5 w-full accent-primary" + /> + + + {LADDER_RUNGS[params.ladderRung].name} + + + {" "} + — {LADDER_RUNGS[params.ladderRung].definition} + + +
+
+ + Cue distance + 0 · 1 · 2 + + + onParam({ + cueDistance: Number( + e.target.value, + ) as PracticeParams["cueDistance"], + }) + } + className="h-1.5 w-full accent-primary" + /> + + + {CUE_LEVELS[params.cueDistance].name} + + + {" "} + — {CUE_LEVELS[params.cueDistance].definition} + + +
+
+ + Presentation + +
+ {(["theory", "application"] as const).map((pres) => ( + + ))} +
+ + {PRESENTATIONS[params.presentation].definition} + +
+ {/* The learner's free-form note — the last word before create: + wishes for the problems, weighed by the agents, nothing owed. */} +
+ + Anything else? (optional) + +