Skip to content

feat: add generalized quiz widget (THU-607)#1001

Open
darkbanjo wants to merge 1 commit into
mainfrom
jkab/thu-607-generalize-quiz-widget
Open

feat: add generalized quiz widget (THU-607)#1001
darkbanjo wants to merge 1 commit into
mainfrom
jkab/thu-607-generalize-quiz-widget

Conversation

@darkbanjo

@darkbanjo darkbanjo commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

What

Brings the interactive quiz widget prototyped on the OUP demo branch into the product as a first-class, demo-agnostic widget.

  • Adds src/widgets/quiz: single / multiple / choice modes, client-side grading, answer persistence in the message cache, and restore-on-reload. Includes schema, parser, instructions, the presentational Quiz, stories, and lib tests.
  • Registers quiz in the widget registry and the cache-data union.
  • Surfaces persisted answers to the model via formatQuizResultsNote, injected as a system note in aiFetchStreamingResponse, 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 / ChatTurnActionsProvider mechanism 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, aiFetchStreamingResponse loads 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.

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>
@github-actions

Copy link
Copy Markdown

Semgrep Security Scan

No security issues found.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 55d46cb. Configure here.

onSubmit?.({
selectedIds: [...ids],
correct: gradeQuiz({ prompt, mode, options }, ids),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 55d46cb. Configure here.

Comment thread src/widgets/quiz/lib.ts
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}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 55d46cb. Configure here.

@github-actions

Copy link
Copy Markdown

Preview environment deployed 🚀

Service URL
Marketing / blog / docs https://thunderbolt-pr-1001.preview.thunderbolt.io
App https://app-pr-1001.preview.thunderbolt.io
API https://api-pr-1001.preview.thunderbolt.io
Keycloak https://auth-pr-1001.preview.thunderbolt.io
PowerSync https://powersync-pr-1001.preview.thunderbolt.io

Stack: preview-pr-1001 · Commit: 55d46cb977fb20e3a44c97e322faea583c0fdf50

Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — demo@thunderbolt.io / demo by default.

@github-actions

Copy link
Copy Markdown

PR Metrics

Metric Value
Lines changed (prod code) +571 / -1
JS bundle size (gzipped) 🟢 682.3 KB → 684.8 KB (+2.5 KB, +0.4%)
Test coverage 🟡 78.09% → 77.70% (-0.4%)
Performance (preview) Preview not ready — Render deploy may have timed out
Accessibility
Best Practices
SEO

Updated Thu, 18 Jun 2026 17:58:35 GMT · run #1948

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant