Interactive workbook generator built with Next.js 14 App Router.
The app takes:
- one markdown file for chapter/content
- one markdown file for answers
Then it parses, matches, and renders a chapter-based workbook where learners can:
- open/collapse chapters
- answer inline exercises
- reveal expected answers
- check responses
- track chapter and overall completion
Turn plain markdown teaching material into an interactive practice workbook with automatic post-upload processing:
- local matching
- AI-assisted alignment (Gemini)
- optional run persistence (Supabase)
- Next.js 14 (App Router)
- React 18
- TypeScript
- Tailwind CSS
- React Markdown (
react-markdown) for rendering chapter markdown
- Next.js Route Handlers
- Zod schema validation for JSON conversion-related routes
- Gemini-powered answer alignment and markdown conversion routes
- Supabase client integration for processing run storage
- ESLint
- PostCSS
- TypeScript compiler
The app is organized into 3 functional layers:
-
Input Layer (Upload Components)
src/components/markdown-upload.tsxsrc/components/answer-markdown-upload.tsx- Validates
.mdfiles and max file size (10MB) - Reads file text in browser (
file.text()) - Sends loaded markdown up to parent state
-
Processing Layer (Parsing + Matching + API Processing)
src/lib/workbook.tssrc/lib/match-exercises.tssrc/lib/supabase-client.ts- Parses chapters and exercise IDs from content markdown
- Parses answer IDs and answer lines from answer markdown
- Builds local ID-based matches (
A1,B12, etc.) - Calls AI alignment for unmatched IDs
- Converts chapter markdown into structured block JSON
- Optionally stores run metadata in Supabase
-
Presentation Layer (Interactive Workbook UI)
src/components/interactive-workbook.tsx- Renders chapter content and exercise interaction
- Stores response/check/reveal state client-side
- Computes progress per chapter and overall
Main orchestration happens in:
src/app/page.tsx
page.tsx: main screen composition and state orchestrationapi/align-answers/route.ts: Gemini-based alignment for unmatched exercisesapi/convert-json/route.ts: Gemini + Zod conversion from markdown to block JSONapi/health/route.ts: app health endpoint
markdown-upload.tsx: chapter markdown uploadanswer-markdown-upload.tsx: answer markdown upload + extracted IDs displayinteractive-workbook.tsx: generated workbook interaction UImarkdown-editor.tsx: markdown editor + validation previewblock-renderer.tsx: existing block renderer (legacy/parallel workflow support)answer-alignment.tsx: existing manual alignment UI (available but not required for workbook generation)
workbook.ts: chapter parsing, exercise extraction, answer parsingmatch-exercises.ts: exercise-answer ID matching helpersupabase-client.ts: Supabase browser client factoryvalidators.ts: markdown structural validation utilitiesschema.ts: zod block schema types
- User uploads chapter markdown (
.md). - User uploads answer markdown (
.md). - Both files are read on the client and stored in page-level React state.
- Local matcher runs first using direct ID intersections.
- Unmatched exercise IDs are sent to
/api/align-answersfor Gemini alignment. - Chapter markdown is sent to
/api/convert-jsonto generate structured block JSON. - Processing metadata is written to Supabase table
workbook_runswhen configured. - Workbook parser splits content into chapters and extracts exercise IDs.
- Answer parser extracts answer entries using ID regex pattern.
- Interactive workbook renders:
- chapter markdown
- per-exercise answer input
- check/reveal controls
- User actions update local state:
- typed responses
- checked status
- revealed answers
- Progress bars update in real-time based on checked/completed items.
Implemented in src/lib/workbook.ts.
- Splits chapters by markdown headings (
## ...). - Creates chapter objects:
idtitlebodyexerciseIds
If no chapter headings exist, the full markdown is treated as one chapter.
Uses regex pattern for headings like:
## Exercise A1## Exercise B12
Answer markdown uses line format:
A1: answer textA1. answer text
Regex captures:
- answer ID (
A1) - answer content (
answer text)
src/lib/match-exercises.ts does ID-based matching:
- extract exercise IDs from content markdown
- extract answer IDs from answer markdown
- return intersections as initial matches
POST /api/align-answers- input:
unmatchedExercises,answerKeyText - output: aligned exercise-answer mappings with confidence
- input:
POST /api/convert-json- input: chapter markdown
- output: structured block JSON validated by Zod schema
Implemented in src/components/interactive-workbook.tsx.
- Collapsible chapter cards
- Smooth open/close transitions
- Inline input field per exercise ID
- "Check" button marks an exercise as checked
- "Reveal Answer" toggles expected answer visibility
checkedstate tracks completed checks by exercise ID- Per-chapter completion:
checked_in_chapter / total_exercises_in_chapter
- Overall completion:
total_checked / total_exercises
- Progress bars update instantly
- Compares user input and expected answer with trimmed + lowercase normalization
- Shows:
CorrectNot exact match yetNo answer found in answer markdown
Upload-level validation:
- file type must be markdown (
.md, markdown mime types) - max size 10MB
Editor-level validation (markdown-editor.tsx):
- page markers format checks (
<!-- page X -->) - blank token sequence checks (
[0],[1], ...) - exercise heading format checks
Processing-level resilience (src/app/page.tsx):
- local matching is always applied first
- if Gemini alignment fails, app continues with local matches
- if conversion fails, interactive workbook still works
- clear status/error messages are shown in the Auto Matching panel
This project has been refactored away from PDF extraction.
Removed:
- PDF upload UI/components
- PDF extraction API routes
- PDF debug docs/scripts
- PDF-related dependencies (
pdfjs-dist, OCR/canvas support stack)
Current primary ingestion path is markdown-only.
Create .env.local with:
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
GEMINI_API_KEY=your_gemini_api_keyNotes:
- Supabase storage is optional at runtime (processing still works without successful insert).
- Gemini key is required for
/api/align-answersand/api/convert-jsonAI steps. - Local matching + interactive workbook UI still operate even if AI steps fail.
npm install
npm run devOpen:
For production checks:
npm run lint
npm run buildIf markdown upload appears to "not work":
- Upload both files (
chapter.mdandanswers.md) and wait for Auto Matching status. - Confirm answer IDs follow expected format:
A1: ...orA1. ...
- Confirm chapter exercise headings include IDs:
## Exercise A1
- Check
.env.localhas valid values for:GEMINI_API_KEYNEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY
- If AI service is unavailable, app should still continue with local matching.
- richer chapter parsing (custom frontmatter/metadata)
- scoring policies beyond exact match
- persistent learner sessions
- export workbook results as markdown/json
- teacher dashboard for completion analytics