Activity System
All learner activities — quizzes, flashcards, matching, interactive components, checkpoints, and the toughest-questions drill — route through a single renderer: ActivityRenderer.jsx.
The type column in the activities table is the canonical format identifier. The context query param (or pathname, for the drill) tells the renderer how the activity was launched and which API to call.
Activity Types
type | Description | Start Screen | Renderer |
|---|---|---|---|
quiz | Multiple-choice question pool with pass/fail tracking | No | QuizActivity |
flashcard | Flip-card term/definition review | No | Flashcards component |
matching | Drag-and-match term to definition | No | Matching component |
classifier | Read a scenario, pick one of N labeled choices | Yes | interactiveRegistry[component] |
true_false | Read a statement, answer True or False | Yes | interactiveRegistry[component] |
interactive | Freeform interactive component (diagram, profile, etc.) | Yes | interactiveRegistry[component] |
Start screen rule: quiz, flashcard, and matching skip the start screen because they have their own built-in ready state. All other kinds show a full-page start screen before launching.
URL Structure
All activities use a single route:
/activity/:activityId?context=<context>[&lessonId=<slug>][&type=<kind>]The toughest-questions drill has its own dedicated route (no DB activity ID needed):
/drill/toughestContext Values
context | When used | Data source |
|---|---|---|
lesson-native | Activity launched from a lesson page (default) | Activity row + lessons.quiz_questions |
recommended | Recommended activity from the dashboard | /api/study/recommended-activity/:id |
checkpoint | Missed checkpoint retry from dashboard | /api/study/missed-from-checkpoint/:id |
missed-quiz | Failed quiz retry (flashcard or matching) | /api/study/missed-from-quiz/:lessonId |
toughest-drill | Toughest Questions Drill — set by pathname, not param | /api/users/me/toughest-questions/drill |
The toughest-drill context is resolved automatically when the pathname is /drill/toughest. No ?context= param is used on that route.
Routing Logic
/drill/toughest
→ context = 'toughest-drill' (from pathname)
→ calls /api/users/me/toughest-questions/drill
→ renders QuizActivity with all returned questions, no lesson-native side effects
/activity/:activityId?context=lesson-native&lessonId=X
→ loads activity row by ID
→ loads lesson by lessonId for quiz_questions
→ tracks quiz pass/fail, cooldown, attempts
/activity/:activityId?context=recommended
→ calls /api/study/recommended-activity/:id
→ renders flashcard or matching based on activity.type
/activity/:activityId?context=checkpoint
→ calls /api/study/missed-from-checkpoint/:id
→ renders as flashcard (missed questions only)
/activity/:activityId?context=missed-quiz&lessonId=X&type=flashcard|matching
→ calls /api/study/missed-from-quiz/:lessonId
→ renders flashcard or matching based on ?type= paramToughest Questions Drill
- Route:
/drill/toughest(dedicated, no activity ID in path) - Data source:
/api/users/me/toughest-questions/drill— returns the student’s most-missed questions - Renderer:
QuizActivitywith all returned questions (no draw_count sampling) - Side effects: None — no cooldown, no pass tracking, no lesson-native progress
- Entry points: ToughestQuestionsZone (dashboard), ExamReadinessPage “Drill These Now” button
- DB record: seeded as
title = 'Toughest Questions Drill',lesson_slug = NULL,type = 'quiz'
Lesson-Native Quiz Rules
- The UI always shows
draw_count(questions per attempt) — never the full pool size - Retake lockout: 10 minutes, enforced server-side and persisted in localStorage
quiz_passed,quiz_best_score,quiz_attempts_countare server-authoritative — never override client-side
activities Table Schema
The valid columns for inserting activities via migration:
| Column | Type | Notes |
|---|---|---|
type | TEXT | quiz, flashcard, matching, classifier, true_false, interactive |
title | TEXT | Display name |
topics | JSONB | Array of topic tag strings (default {}) |
lesson_slug | TEXT | null for platform-level activities |
enabled | BOOLEAN | Default true |
classifier and true_false activities do not use items or choices columns — those do not exist on this table. Content is seeded via migration into quiz_questions or content_blocks.
Minimum Content Thresholds
| Type | Minimum before activity feels complete |
|---|---|
quiz | 5 questions in pool |
flashcard | 8 cards |
matching | 6 pairs |
classifier / true_false | 5 items |
Adding a New Activity
- Insert a row into
activitiesvia migration using only valid columns:type,title,topics,lesson_slug,enabled. - If introducing a new
type, updateresolveRenderer()inActivityRenderer.jsx. - If it renders via
interactiveRegistry, register the component insrc/app/components/lesson/interactive/index.js. - Decide whether it needs a start screen. If yes, add a case to
resolveStartScreenProps()inActivityRenderer.jsx. - Update
docs/Activities.mdin the platform monorepo and this page.