feat: add generalized quiz widget (THU-607)#1001
Conversation
Bring the interactive quiz widget prototyped on the OUP demo branch into the product as a first-class, demo-agnostic widget. - Add src/widgets/quiz: single / multiple / choice modes, client-side grading, answer persistence in the message cache, and restore-on-reload. Includes schema, parser, instructions, presentational Quiz, stories, and lib tests. - Register `quiz` in the widget registry and the cache-data union. - Surface persisted answers to the model via formatQuizResultsNote, injected as a system note in aiFetchStreamingResponse, so it can report scores without asking the user to re-enter choices. Deliberately NOT ported: the hiddenFromUi / ChatTurnActionsProvider hidden-turn machinery from the demo branch. The quiz grades client-side and reports results via the system note instead of dispatching a hidden user turn; on the demo branch that machinery was wired into the provider tree but never actually invoked, so bringing it in would be dead code. It can be introduced separately if a real hidden-turn use case appears. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Semgrep Security ScanNo security issues found. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 55d46cb. Configure here.
| // is surfaced to the model on later turns via formatQuizResultsNote. Sending a | ||
| // turn per answer would also goad single-question-at-a-time backends into | ||
| // endlessly asking the next question. | ||
| await updateMessageCache(db, messageId, storageKey, entry) |
There was a problem hiding this comment.
Quiz cache stale after submit
Medium Severity
After updateMessageCache saves a quiz answer, the useQuery entry for quizState is never invalidated or updated, while staleTime is Infinity. If QuizWidget remounts (e.g. switching threads and returning), React Query still serves the initial null instead of refetching the DB, so the quiz renders unanswered and can be submitted again.
Reviewed by Cursor Bugbot for commit 55d46cb. Configure here.
| onSubmit?.({ | ||
| selectedIds: [...ids], | ||
| correct: gradeQuiz({ prompt, mode, options }, ids), | ||
| }) |
There was a problem hiding this comment.
Quiz persist not awaited
Medium Severity
commit calls onSubmit without awaiting the async handleSubmit that persists to the database. If the user sends the next chat message right after checking an answer, aiFetchStreamingResponse may run its getMessage/formatQuizResultsNote pass before the cache write finishes, so the model’s quiz results system note can omit that answer.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 55d46cb. Configure here.
| export const QUIZ_CACHE_PREFIX = 'quiz' | ||
|
|
||
| /** Cache key for a single quiz instance within a message (one tag = one prompt). */ | ||
| export const quizStorageKey = (prompt: string): string => `${QUIZ_CACHE_PREFIX}/${prompt}` |
There was a problem hiding this comment.
Duplicate prompt cache collision
Low Severity
Cache keys use only quiz/${prompt}, so two quiz widgets in the same assistant message with identical prompt text share one cache entry. Answers, restore state, and model-reported results for the second widget can reflect the first widget’s submission.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 55d46cb. Configure here.
|
Preview environment deployed 🚀
Stack: Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — |
PR Metrics
Updated Thu, 18 Jun 2026 17:58:35 GMT · run #1948 |


What
Brings the interactive quiz widget prototyped on the OUP demo branch into the product as a first-class, demo-agnostic widget.
src/widgets/quiz:single/multiple/choicemodes, client-side grading, answer persistence in the message cache, and restore-on-reload. Includes schema, parser, instructions, the presentationalQuiz, stories, and lib tests.quizin the widget registry and the cache-data union.formatQuizResultsNote, injected as a system note inaiFetchStreamingResponse, so the model can report scores without asking the user to re-enter their choices.The model emits
<widget:quiz mode="..." prompt="..." options='[...]' explanation="..." />.Decision: hidden-turn machinery left behind
The demo branch also had a
hiddenFromUi/ChatTurnActionsProvidermechanism for dispatching a hidden user turn on answer submit. The quiz grades client-side and reports via the system note instead — and on the demo branch that machinery was wired into the provider tree but never actually invoked. Porting it here would be dead code, so it's intentionally excluded. It can be introduced on its own if a real hidden-turn use case appears.Test
src/widgets/quiz/lib.test.ts: 9/9 pass (grading across modes, cache round-trip, results-note formatting). Typecheck clean.Closes THU-607
🤖 Generated with Claude Code
Note
Low Risk
Mostly new UI/widget code plus an additive system-note path in chat fetch; no auth or data-model breaking changes, though extra DB reads per assistant message on send could add minor latency in long threads.
Overview
Adds a first-class quiz widget so the assistant can emit inline multiple-choice questions via
widget:quiz, with single, multiple, and ungraded choice modes, client-side grading, and restore-on-reload from the message cache.Answers are written to the assistant message cache (no hidden follow-up turn). On each send,
aiFetchStreamingResponseloads quiz entries from prior assistant messages and injects a quiz results system note (including score when applicable) alongside skill system messages so the model can discuss scores without re-asking.The widget is registered in the central widget registry (prompts, parser, cache union) with schema, instructions, Storybook stories, and lib unit tests.
Reviewed by Cursor Bugbot for commit 55d46cb. Bugbot is set up for automated code reviews on this repo. Configure here.