Interactive quiz platform for reviewing books, podcasts, and courses through active recall. Live at quiz.hasit.in.
Gitquiz is a web-based quiz platform that helps you review and master knowledge from books, podcasts, and courses. Using active recall and spaced repetition techniques, it transforms learning content into interactive quizzes that improve retention and understanding.
Whether you're reviewing material for academic purposes, professional development, or personal growth, gitquiz provides a structured approach to learning through scenario-based questions that test real-world application of concepts.
- Modern web browser (Chrome, Firefox, Safari, Edge)
- Internet connection (content is served from GitHub Raw URLs)
- Serve the
quiz/directory with any static file server:
python -m http.server 8765 --directory quiz- Open your browser and navigate to
http://localhost:8765
If you want to modify course content or run tests:
cd quiz/tests
npm install
npx playwright install chromium├── courses/ # Quiz content (JSON)
│ ├── courses_list.txt # Catalog of all course folders (IDs only, alphabetically sorted)
│ ├── courses-meta.json # Course metadata (title, type, chapters, source, description)
│ ├── course-schema.json # JSON Schema for chapter validation
│ ├── book-<title>/ # One folder per book/course
│ │ ├── 001.json # Chapter 1 questions (array of 7-field objects)
│ │ ├── 002.json # Chapter 2 questions
│ │ └── ...
│ ├── podcast-<title>/...
│ └── coursera-<title>/...
├── quiz/ # Frontend application
│ ├── index.html # Single-page quiz app (vanilla JS + Tailwind CDN)
│ ├── styles-theme.css # Theme variables and dark/light overrides
│ ├── styles-components.css # UI components (headers, filters, options, etc.)
│ ├── styles.css # Animations and keyframes only
│ ├── styles-responsive.css # Mobile media queries
│ ├── lib/ # Modular JS (loaded via <script> tags, no bundler)
│ │ ├── state.js # Global state, BASE_URL, CATALOG_URL, streak utils
│ │ ├── catalog.js # Course catalog: load, filter, render, type filters
│ │ ├── preview.js # Preview screen: fetch module, show first question
│ │ ├── quiz.js # Quiz engine: timer, scoring, render, skip, completion
│ │ ├── sharing.js # Share modal, download PNG (html2canvas), Web Share API
│ │ ├── notifications.js # Toast notifications (showNotify, showNotifyWithAction)
│ │ ├── ai.js # AI Explain: calls Mistral via Cloudflare Worker proxy
│ │ └── main.js # Entry point: loads catalog, handles URL params, keyboard shortcuts
│ ├── tests/ # Playwright end-to-end + schema tests
│ │ ├── *.spec.mjs # Domain-split spec files
│ │ ├── test-utils.mjs # Shared mock data (CATALOG_CONTENT, MOCK_MODULES) & route setup
│ │ ├── affected-tests.mjs # Git-diff-based test selector
│ │ ├── schema.config.mjs # Lightweight config for schema tests
│ │ ├── playwright.config.mjs # Main Playwright config
│ │ └── package.json # Test dependencies
│ │ └── proxy/ # Cloudflare Worker for AI explanations
│ │ └── worker.js # Mistral AI proxy
├── quiz/scripts/ # Node.js utility scripts (run from project root)
│ ├── answer-length-audit.js # Detect answer-length bias (shortest/longest %)
│ ├── assemble-course.mjs # Assembly helper: ch-*.json → input.json
│ ├── coverage-check.js # Verify concept coverage against keyword inventory
│ ├── cross-chapter-repetition.js # Detect concepts appearing in 3+ chapters
│ ├── difficulty-audit.js # Print all questions with blank E/M/H brackets for labeling
│ ├── difficulty-tally.js # Tally difficulty distribution (E/M/H) across all courses
│ ├── fix-length-bias.mjs # Auto-fix longest-answer bias by truncating conjunctions
│ ├── generate-course.mjs # CLI generator: input.json → split chapter files + metadata update
│ ├── validate-all.js # Validate all courses: chapter counts, metadata sync, bias checks
│ └── validate.js # Validate a single course (arg: `<course-dir>`)
├── .opencode/ # OpenCode AI agent configuration
│ ├── skill/hasits-plan/SKILL.md # Plan persistence for LLM context survival
│ └── skill/syllabus-to-quiz/SKILL.md # Workflow for converting courses to quizzes
├── .github/workflows/validate.yml # CI: schema validation, validate-all, full Playwright suite
├── opencode.json # OpenCode AI config (model limits, compaction, instructions)
├── AGENTS.md # AI agent development instructions
└── README.md # This file (full reference documentation)
Each course folder contains numbered chapter files (001.json, 002.json, ...). Each file is a JSON array of question objects with 7 required fields + 1 optional field:
| Field | Type | Required | Description |
|---|---|---|---|
question |
string |
Yes | Short concept name (e.g., "Opportunity Cost") |
content |
string |
Yes | Brief 1-2 sentence explanation of the concept |
description |
string |
Yes | Real-world scenario ending with a question (e.g., "This illustrates:") |
options |
string[] |
Yes | Array of exactly 4 plausible answer strings. Must not reference other options by position (no "Both B and C", "All of the above") |
answer |
string |
Yes | Correct answer — must be identical (case, punctuation, whitespace) to one of the options entries |
explanation |
string |
Yes | Teaching explanation of why this answer is correct and the others are not |
difficulty |
string |
Yes | "easy", "medium", or "hard" |
blank |
string |
No | Cloze passage containing answer verbatim, for fill-in-the-blank mode |
[
{
"question": "Concept Name",
"content": "Brief explanation of the concept (1-2 sentences).",
"description": "Real-world scenario that illustrates the concept. What does this demonstrate?",
"options": [
"Incorrect option 1",
"Incorrect option 2",
"Correct option — exact text repeated in answer field",
"Incorrect option 4"
],
"answer": "Correct option — exact text repeated in options",
"explanation": "Clear explanation of why this is correct and the others are not.",
"difficulty": "easy"
}
]- 7 fields required: All fields above are mandatory in every question object. Missing or extra fields will fail validation.
- Answer match:
answermust match oneoptionsentry character-for-character. Common pitfalls: trailing spaces, mismatched punctuation, capitalization differences. - 4 options: Exactly 4 strings in
options. No fewer, no more. - No positional references: Options must not reference other options by letter (e.g., "Both A and B", "All of the above", "A & C"). These break when options are shuffled at runtime.
- Difficulty enum: Must be
"easy","medium", or"hard"— lowercase, no other values. - No answer-length bias: The answer must not be uniquely longer or shorter than all other options. Use
answer-length-audit.jsto detect bias andfix-length-bias.mjsto auto-fix.
Per-chapter targets (validated by difficulty-tally.js):
- Easy (30-40%): Straightforward recall / direct application
- Medium (35-45%): Apply to unfamiliar scenario, all options plausible
- Hard (15-25%): Synthesize multiple concepts, catch subtleties
Hard question techniques: trapdoor option, reverse application, boundary case, competing principles, option symmetry.
| Course | Chapters | Source |
|---|---|---|
| book-algorithms-to-live-by | 7 | Algorithms to Live By |
| book-almanack-of-naval-ravikant | 10 | The Almanack of Naval Ravikant |
| book-atomic-habits | 11 | Atomic Habits |
| book-beginning-of-infinity | 8 | The Beginning of Infinity |
| book-bhagavad-gita | 11 | Bhagavad Gita |
| book-cointelligence | 10 | Co-Intelligence |
| book-deep-work | 7 | Deep Work |
| book-get-better-at-anything | 8 | Get Better at Anything |
| book-influence | 8 | Influence |
| book-seeking-wisdom-darwin-to-munger | 8 | Seeking Wisdom |
| book-super-thinking | 11 | Super Thinking |
| book-the-adaptive-edge | 16 | The Adaptive Edge |
| book-the-changing-world-order | 15 | The Changing World Order |
| book-the-great-mental-models-v1 | 11 | The Great Mental Models Volume 1 |
| book-the-great-mental-models-v2 | 15 | The Great Mental Models Volume 2 |
| book-the-great-mental-models-v3 | 17 | The Great Mental Models Volume 3 |
| book-the-great-mental-models-v4 | 6 | The Great Mental Models Volume 4 |
| book-the-psychology-of-money | 20 | The Psychology of Money |
| book-the-startup-of-you | 9 | The Startup of You |
| coursera-financial-markets-global | 12 | Coursera: Financial Markets |
| coursera-finding-purpose-and-meaning-in-life | 6 | Coursera: Finding Purpose and Meaning in Life |
| coursera-genai-for-algorithmic-trading | 11 | Coursera: GenAI for Algorithmic Trading |
| podcast-age-of-async-agents | 8 | Latent Space Podcast |
| podcast-autonomous-company | 3 | The AI Industrial Revolution |
| podcast-daytona | 5 | Latent Space Podcast (Daytona) |
| podcast-nothing-ever-happens-is-over | 14 | Naval Podcast |
| podcast-regulatory-frontier | 1 | Naval Podcast (Blake Scholl) |
| podcast-vibe-coding-hardware | 3 | Naval Podcast |
| podcast-waste-tokens-save-time | 2 | Naval Podcast (Guillermo Rauch) |
Utility scripts in quiz/scripts/. Run with node quiz/scripts/<name> from the project root.
| Script | Purpose |
|---|---|
answer-length-audit.js |
Detect answer-length bias (shortest/longest %) — arg: <course-id> (default: all courses) |
validate.js |
Validate a single course — arg: <course-dir> (default: courses/course-identifier) |
validate-all.js |
Validate every course: chapter counts (7-12), metadata sync, answer-in-options, positional refs, filename format, answer-length bias |
difficulty-tally.js |
Auto-tally E/M/H distribution across all courses |
difficulty-audit.js |
Print all questions with blank E/M/H brackets for manual labeling — arg: <course-dir> (default: courses/course-identifier) |
coverage-check.js |
Verify concept inventory coverage — edit inventory array; arg: <course-dir> (default: courses/course-identifier) |
cross-chapter-repetition.js |
Detect concepts appearing in 3+ chapters — edit conceptGroups; arg: <course-dir> (default: courses/course-identifier) |
assemble-course.mjs |
Assembly helper: node quiz/scripts/assemble-course.mjs <course-id> — reads ch-*.json files from courses/<id>/ and outputs an input.json for the generator. Deletes input.json after use. |
fix-length-bias.mjs |
Auto-fix longest-answer bias: node quiz/scripts/fix-length-bias.mjs <course-id> — reduces over-long answers by truncating conjunction clauses ("because", "which"). Run after answer-length-audit.js detects bias. |
generate-course.mjs |
CLI generator: node quiz/scripts/generate-course.mjs input.json — reads a single input JSON (id, chapters[] with title+seq+questions[]), produces split 001-00N.json files, validates, creates dir, updates courses_list.txt and courses-meta.json. Supports --dry-run. |
For courses with many chapters, use the intermediate ch-*.json workflow to avoid monolithic file management:
- Create
courses/<course-id>/ - Write intermediate chapter files as
ch-001.json,ch-002.json, etc., each with the format:{ "title": "Chapter Name", "questions": [ /* question objects with 7 fields each */ ] } - Assemble into generator input:
This reads
node quiz/scripts/assemble-course.mjs <course-id>
ch-*.jsonfiles and writesinput.jsonat the project root. - Generate final chapter files:
This creates
node quiz/scripts/generate-course.mjs input.json
001-00N.jsonfiles, updatescourses_list.txtandcourses-meta.json. - Delete intermediate files:
rm courses/<course-id>/ch-*.json input.json
Preferred method — use the generator directly (for ≤4 chapters or structured input):
node quiz/scripts/generate-course.mjs input.jsonThis creates the directory, writes all chapter files, adds to courses_list.txt alphabetically, and creates/updates the courses-meta.json entry. Delete input.json after use.
Manual method (fallback):
- Create
courses/<course-id>/ - Add chapter files
001.json,002.json, etc. (7-12 questions per file) - Add the course ID to
courses/courses_list.txtin alphabetical order - Add matching entry to
courses/courses-meta.jsonwithtitle,type,chapters,source,description
Course ID convention: {type}-{slug} where type is book, podcast, or coursera. The type prefix is stripped for display.
Validation:
node quiz/scripts/validate-all.js # comprehensive check from root
npm run test:schema # schema validationEach course has a metadata entry in courses/courses-meta.json:
| Field | Type | Description |
|---|---|---|
title |
string |
Human-readable course name |
type |
string |
"book", "podcast", or "coursera" |
chapters |
number |
Number of chapter JSON files |
source |
string|null |
URL to original content or null |
description |
string |
One-line summary with host/author and topic |
Keys in courses-meta.json must match courses_list.txt exactly and be sorted identically. Validated by validate-all.js.
Catalog (setup-container) → Preview (preview-container) → Quiz (quiz-flow) → Results + Achievement Card (completion-screen)
- Setup/Catalog Screen (
#setup-container): Search bar, SVG type filter buttons (All/Books/Podcasts/Courses), custom<div>-based course list with row icons, NEW pill badge on unseen courses, Custom Quiz toggle in footer (lazy-created — not in DOM until first click), daily streak badge - Preview Screen (
#preview-container): Course badge, title, question count + chapter count, first question preview with options, chapter grid for multi-chapter navigation, Start Quiz / Cancel buttons - Quiz Screen (
#quiz-container): Progress bar, score/streak/timer stat cards, quiz flow with question, options (shuffled), explanation panel, AI Explain button, Continue button - Completion Screen (
#completion-screen): Trophy animation, final score, total time, Download Achievement Card button, Start Next Module / Return to Catalog
| Element | Purpose |
|---|---|
#course-dropdown |
Course selector (div-based custom list with .list-item rows, SVG type icons, NEW pill badge, data-value attribute, selected state with blue left border) |
#catalog-search |
Search input |
.type-filter-btn[data-type] |
Type filter buttons (all, book, podcast, coursera) |
#daily-streak-badge |
Streak notification (hidden when 0) |
#preview-badge |
Course ID badge (truncate max-w-[200px]) |
#preview-title |
Course name (h2, mobile truncation) |
#preview-meta |
Preview stats text (question count + chapter count) |
#preview-chapter-grid |
Chapter navigation grid (flex-wrap, .chapter-btn / .chapter-btn-active) |
#module-label |
Quiz header label between "← Menu" and "Skip Module" (max-width: 140px + mobile truncation) |
#topic-title, #description-text, #content-box |
Question content (wrapping OK, no truncation) |
#options-bin |
Shuffled multiple-choice buttons |
#explanation |
Post-answer teaching explanation |
#ai-section |
AI Explain More button + response area |
#progress-fill |
Progress bar width (0-100%) |
#score-val, #streak-val, #timer-val |
Quiz stat displays |
#ach-* |
Achievement card elements (hidden off-screen via left: -9999px) |
quizData[]— questions for current modulecurrentIdx— current question indexscore— cumulative points (100 base + streak bonus)streak— consecutive correct answers (resets on wrong)secondsElapsed— timer counteruserName— stored in localStorage (quizUserName)fullCatalog— array of course IDs fetched from GitHub RawpreviewData— cached module data for preview-to-quiz handoff
| Param | Effect |
|---|---|
?course=<id> |
Selects course in dropdown, auto-starts quiz |
?c=<n> |
Chapter number (used with ?course=) |
?q=<n> |
Jump to question index in preview (used with ?course=) |
| Key | Context | Action |
|---|---|---|
1-4 |
Quiz active | Select option by position |
Enter |
Quiz (after answer) | Click Continue/Next |
Enter |
Preview screen | Click Start Quiz |
screen-enterCSS class triggersfadeSlideInanimation (0.3s ease-out)- Applied to preview, quiz-flow, and completion-screen on reveal
- Base: 100 points per correct answer
- Streak bonus: +20 points when streak > 2
- Total:
score += 100 + (streak > 2 ? 20 : 0) - Wrong answer: streak resets to 0
- Stored in
localStorageunder keyquizDailyStreak - Format:
{ lastDate: "YYYY-MM-DD", count: <number> } - Updated on quiz start (
initializeQuiz) and on completion (checkNaturalEnd) - Consecutive day logic: if
lastDate === yesterday→ increment; iflastDate === today→ no change; else → reset to 1
- 4 persona buttons (
.ai-persona-btn[data-persona]) triggeraskAI(persona)inai.js - Personas:
child(simple),deep(expert),first-principles(fundamental truths),socratic(guiding questions) - POSTs to
MISTRAL_PROXY_URL(Cloudflare Worker) with question context, user's answer, and correctness - Worker URL:
https://quiz-ai-proxy.hasit-p-bhatt.workers.dev/ - Shows response in
#ai-responsediv
- Share button (
#share-btn) triggersshareHandler()insharing.js - Context-aware: shares certificate (completion screen), question (quiz active), or setup link (catalog)
- Uses
html2canvasfor PNG generation - Achievement card template hidden off-screen at
#achievement-card-template
- Module load failures show error overlay (
#error-overlay) with message and "Return to Menu" button - Network fetch errors in preview show toast notification via
showNotify() - AI API failures gracefully fall back to "AI explainer is not available right now"
- IDs follow
{type}-{slug}format (e.g.,book-git-basics,podcast-clean-code,coursera-machine-learning) - Type prefix is stripped from display via regex:
/^(book|podcast|coursera|course)-/i - Emoji prefix (
📘/🎙/📖) shown only whenactiveTypeFilter === 'all':book-→📘podcast-→🎙coursera-→📚- Fallback (unknown type) →
📖
- Display name: kebab-case split → Title Case (e.g.,
atomic-habits→Atomic Habits)
- Tailwind CSS via CDN (no build step) — utility classes in HTML
- Custom CSS (4 files):
styles-theme.css(variables, reset, dark/light themes),styles-components.css(components),styles.css(animations, cards),styles-responsive.css(mobile media queries) - Dark mode: automatic via system preference; variables swap to dark palette
- JavaScript toggles classes:
hidden,correct,wrong,screen-enter,score-pop - Mobile:
@media (max-width: 640px)overrides for truncation, dropdown height, z-index
Serve quiz/ with any static file server:
python -m http.server 8765 --directory quizThen open http://localhost:8765. No build step needed. The app loads course data from GitHub Raw URLs, so you need internet access.
Playwright end-to-end tests live in quiz/tests/. Domain-split into spec files:
| Test | Command | Module(s) |
|---|---|---|
| Setup | npm run test:setup |
main.js, state.js, notifications.js |
| Catalog | npm run test:catalog |
catalog.js |
| Preview | npm run test:preview |
preview.js |
| Quiz | npm run test:quiz |
quiz.js, state.js |
| URL Params | npm run test:params |
main.js |
| UI/Sharing | npm run test:ui |
sharing.js |
| AI | npm run test:ai |
ai.js |
| Visual | npm run test:visual |
visual-only |
| Unit | npm run test:unit |
state.js, catalog.js, sharing.js |
| Schema | npm run test:schema |
courses/**/*.json, course-schema.json |
| Affected | node quiz/tests/affected-tests.mjs |
auto-detect via git diff |
| All | npx playwright test |
full suite (desktop + mobile) |
First-time setup:
cd quiz/tests
npm install
npx playwright install chromiumThe Playwright config (playwright.config.mjs) runs two projects in sequence (not parallel):
- Desktop (1280×800)
- Mobile (Pixel 5)
Both use a webServer that starts python -m http.server 8765 on ../../quiz.
Screenshots are captured after every test and saved to quiz/tests/test-results/.
The schema config (schema.config.mjs) runs only schema.spec.mjs with fullyParallel: true and 4 workers — no web server needed as it reads files directly.
Note: All tests are Playwright-based. The modules are vanilla browser scripts (no
export/import), so traditional Node.js unit tests aren't possible.page.evaluate()-based unit-style tests are used instead for isolated function testing.
Use node quiz/tests/affected-tests.mjs to automatically determine which tests to run based on git diff. The script maps changed file paths to relevant test specs:
courses/ → schema.spec.mjs
quiz/lib/main.js → setup.spec.mjs, url-params.spec.mjs
quiz/lib/quiz.js → quiz.spec.mjs, navigation.spec.mjs
...etc
The "Explain More with AI" button in the quiz calls a Mistral AI API via a Cloudflare Worker proxy.
- Edit
quiz/proxy/worker.jsand set your Mistral API key asMISTRAL_API_KEY - Deploy to Cloudflare Workers via
wrangler deploy - Update the worker URL in
quiz/lib/ai.js(theMISTRAL_PROXY_URLvariable instate.js)
The worker proxy:
- Accepts POST requests with
{ model, messages }payload - Forwards to Mistral API
- Returns the model response
GitHub Actions workflow (.github/workflows/validate.yml) runs on push/PR to main and daily scheduled:
- schema job:
npm run test:schema— validates all course files against JSON Schema - validate-all job:
node quiz/scripts/validate-all.js— comprehensive check (chapter counts, metadata sync, positional refs) - full-suite job: Full Playwright test suite — runs on push to
mainor daily schedule; skips when onlycourses/or*.mdfiles changed
Node.js version: 24. Cache: npm for quiz/tests/package-lock.json.
The quiz app is hosted at quiz.hasit.in. Content is served via GitHub Raw URLs (https://raw.githubusercontent.com/hasitpbhatt/gitquiz/main/courses/...), so course updates are live as soon as they're pushed to main.
To deploy the frontend:
- Push changes to the
mainbranch - If using Cloudflare Pages / GitHub Pages, the site redeploys automatically
- Course catalog with search, type filters (book/podcast/coursera),
<div>-based custom list with SVG type icons per row, type-prefix-stripped display names, and new course indicator (NEW pill badge + unseen-first sort) - Preview screen showing summary card (course description, progress, difficulty tally, time estimate), chapter grid, and first question preview
- Quiz flow with multiple-choice options, letter badges (A/B/C/D), difficulty-tagged questions, and score tracking
- Keyboard shortcuts (1–4 to select options, Enter to continue/start)
- Screen transition animations between setup → preview → quiz → completion screens (
fadeSlideIn) - Daily streaks — localStorage-backed streak count, updated on quiz start and completion
- Share scorecards — Web Share API with auto-generated PNG card via html2canvas (share certificate, share question, share portal link)
- AI Explain — Mistral API via Cloudflare Worker proxy for deeper explanations
- URL-based deep linking —
?course=,?q=,?c=params for linking to specific courses, questions, and chapters - Custom URL input — Load any quiz JSON from an arbitrary URL
- Auto-start from URL —
?course=param bypasses the catalog and starts the quiz immediately - Scoring system — base 100pts + streak bonus
- Module chaining — auto-detects and offers next module on completion
- Error handling — overlay on load failures, toast notifications for operational errors
- Responsive design — Mobile-first with Tailwind CSS, dark mode support, mobile overflow handling
- Schema-validated course format — 7-field question structure validated via CI
- CI pipeline — GitHub Actions with validate-all, schema, and full Playwright test suite
- Affected test selection — auto-detect relevant tests from changed files
- Frontend: Vanilla JavaScript (ES6), Tailwind CSS (CDN), html2canvas
- Content: JSON (served via GitHub Raw)
- AI: Mistral API via Cloudflare Workers
- Testing: Playwright (end-to-end + schema validation)
- CI: GitHub Actions (Node 24)
- Hosting:
quiz.hasit.in
This repository includes OpenCode AI agent configuration (.opencode/ directory and opencode.json) with two specialized skills:
- hasits-plan: Persists hierarchical plan trees to
.hasit/so LLM context survives compaction during multi-step tasks - syllabus-to-quiz: Converts syllabus/transcript content into scenario-based quiz courses following the full workflow
The AGENTS.md file provides detailed development conventions for AI agents working on this codebase.