You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The quiz experience is one of the core loops (quiz → mastery → graph → XP) but today it's the plainest surface in the app: a single MCQ flow with three dropdowns, unstyled beyond inline styles, no history, no final breakdown, and several selector options that are outright broken. This epic covers a functional + visual revamp.
Current state (surveyed 2026-08-12)
Backend (backend/routes/quiz.py): two endpoints — POST /api/quiz/generate and POST /api/quiz/submit. Generation runs through the Pydantic AI quiz_agent (#145) with weak-area/misconception/recent-attempt tools and RAG course-material grounding; submit does an atomic completed_at claim, mastery via apply_graph_update, XP + achievements, and regenerates adaptive quiz_context in a background task. Questions are MCQ-only, single-correct, A–F labels. questions_json/answers_json are encrypted at rest (#521/#527).
Frontend: components/QuizPanel.tsx (398 lines, all inline styles) drives four phases: select (course → concept → count → difficulty) → active (one question at a time) → per-question review → results. Selection logic lives in lib/quizSelection.ts (unit-tested). Mounted at (shell)/quiz via components/screens/Quiz.tsx; deep-linkable via ?concept=.
Broken or half-wired today
These were found while surveying; each is independently fixable ahead of the redesign:
"Adaptive" difficulty is dead.QuizPanel.tsx:64-69 offers adaptive, but routes/quiz.py:39 (VALID_DIFFICULTIES = {easy, medium, hard}) 400s it — the request never reaches the agent. The dropdown option has been shipping a guaranteed error.
"15 questions" is dead.QuizPanel.tsx:58-62 offers 5/10/15, but models/__init__.py:49 caps num_questions at le=10 → Pydantic 422.
The results screen ignores the server's per-question results.submitQuiz returns a results[] array (per-question correctness, correct answer, explanation) that the results phase never renders — students get a bare % + mastery line with no breakdown and no way to review what they missed.
The full answer key ships to the client at generate time. Every option carries correct: true/false in the /generate response (the review phase grades client-side from it). Server re-scores at submit, but the key is one devtools tab away — grading should be server-side end to end.
Proposed scope (to be shaped before implementation)
Functionality
Question variety beyond single-correct MCQ: e.g. multi-select, true/false, cloze/short-answer (short-answer implies LLM grading — new agent task, needs a function-mode handler).
A real adaptive mode: difficulty adjusts within the quiz from answers so far, backed by the (fixed) quiz_context — replacing the broken dropdown option.
Resume/abandon handling: unanswered attempt rows currently just orphan.
Post-quiz review surface built from the server results[] (fixes 3): missed-question review with explanations and per-concept breakdown, linking into the tutor (Explain this already deep-links to /learn).
Server-side grading of the in-quiz review verdict (fixes 4).
UI/UX
Rebuild QuizPanel on the components/ui primitives + design tokens (Canopy: the component-system refactor retired inline-style surfaces; QuizPanel predates it).
Real visual identity for the quiz surface: progress indicator, question transitions, a results screen worth reaching (per-question breakdown, per-concept mastery deltas, XP earned — gamification feat(gamification): XP, levels, achievements catalog, and leaderboards #505 already awards it, the UI never shows it).
Keyboard support (A–D / 1–4 to select, Enter to submit), reduced-motion compliance, focus management between phases.
Constraints
LLM changes go through agents/ + agents/_providers.py slots only (ADR 0024); any new request-path agent task needs a handler in agents/function_handlers_e2e.py or the E2E lanes break with UnregisteredHandlerError.
The test(e2e): journey — quiz answer → mastery update (UI + DB) #393 quiz→mastery Playwright journey pins the current quiz-* testids (docs/frontend-testids.md) — the revamp must update the journey in the same PR, and per CLAUDE.md the E2E gate (hermetic + Chapter 1 + oracles) applies pre-merge.
quiz_attempts.difficulty is CHECK-constrained (0025): a real adaptive mode either fits the enum or ships a migration.
Items 1–4 under "Broken or half-wired" are small and independently shippable; they can be split into standalone bug issues if we want fixes ahead of the epic.
Why
The quiz experience is one of the core loops (quiz → mastery → graph → XP) but today it's the plainest surface in the app: a single MCQ flow with three dropdowns, unstyled beyond inline styles, no history, no final breakdown, and several selector options that are outright broken. This epic covers a functional + visual revamp.
Current state (surveyed 2026-08-12)
Backend (
backend/routes/quiz.py): two endpoints —POST /api/quiz/generateandPOST /api/quiz/submit. Generation runs through the Pydantic AIquiz_agent(#145) with weak-area/misconception/recent-attempt tools and RAG course-material grounding; submit does an atomic completed_at claim, mastery viaapply_graph_update, XP + achievements, and regenerates adaptivequiz_contextin a background task. Questions are MCQ-only, single-correct, A–F labels.questions_json/answers_jsonare encrypted at rest (#521/#527).Frontend:
components/QuizPanel.tsx(398 lines, all inline styles) drives four phases: select (course → concept → count → difficulty) → active (one question at a time) → per-question review → results. Selection logic lives inlib/quizSelection.ts(unit-tested). Mounted at(shell)/quizviacomponents/screens/Quiz.tsx; deep-linkable via?concept=.Broken or half-wired today
These were found while surveying; each is independently fixable ahead of the redesign:
QuizPanel.tsx:64-69offersadaptive, butroutes/quiz.py:39(VALID_DIFFICULTIES = {easy, medium, hard}) 400s it — the request never reaches the agent. The dropdown option has been shipping a guaranteed error.QuizPanel.tsx:58-62offers 5/10/15, butmodels/__init__.py:49capsnum_questionsatle=10→ Pydantic 422.submitQuizreturns aresults[]array (per-question correctness, correct answer, explanation) that the results phase never renders — students get a bare%+ mastery line with no breakdown and no way to review what they missed.correct: true/falsein the/generateresponse (the review phase grades client-side from it). Server re-scores at submit, but the key is one devtools tab away — grading should be server-side end to end.quiz_contextlost its UNIQUE, the upsert 42P10s and is swallowed). The agent's adaptive inputs are running on stale/absent context; any "truly adaptive" work depends on that fix.Proposed scope (to be shaped before implementation)
Functionality
quiz_context— replacing the broken dropdown option.quiz_attemptsalready stores plaintext score/total/difficulty/completed_at scalars precisely so analytics stay possible (Quiz performance data stored in plaintext (questions_json, answers_json, quiz_context) #521) — there is currently no route or UI reading them. Attempt list per concept/course, score trend, "retake the ones I missed".results[](fixes 3): missed-question review with explanations and per-concept breakdown, linking into the tutor (Explain thisalready deep-links to/learn).UI/UX
components/uiprimitives + design tokens (Canopy: the component-system refactor retired inline-style surfaces; QuizPanel predates it).Constraints
agents/+agents/_providers.pyslots only (ADR 0024); any new request-path agent task needs a handler inagents/function_handlers_e2e.pyor the E2E lanes break withUnregisteredHandlerError.questions_json/answers_json/context_jsonencryption boundaries (Quiz performance data stored in plaintext (questions_json, answers_json, quiz_context) #521) must survive any schema/shape changes; new sensitive columns need ciphertext-oracle manifest entries.quiz-*testids (docs/frontend-testids.md) — the revamp must update the journey in the same PR, and per CLAUDE.md the E2E gate (hermetic + Chapter 1 + oracles) applies pre-merge.quiz_attempts.difficultyis CHECK-constrained (0025): a real adaptive mode either fits the enum or ships a migration.Related
selected, backend requiresselected_label— every quiz submission 422s #438 — the last shell revamp broke quiz submits (422) and only the E2E journey caught it; same risk class applies hereItems 1–4 under "Broken or half-wired" are small and independently shippable; they can be split into standalone bug issues if we want fixes ahead of the epic.