Proof-of-work rather than a puzzle, and — the reason for it — nothing
about the person signing up is described to a third party in order to let
them in. Turnstile and then hCaptcha were both here; both told Cloudflare
who was at the door.
The `cap` service runs on the compose network with its own Redis
database, kept apart from the app's so a flush of one cannot clear the
other's challenges. The widget talks to /cap/ on this origin, proxied by
the frontend's nginx, so the browser reaches nobody else either. Caddy
passes the whole host through to that container, so it needed no change.
Two things that had to be found rather than read:
Cap's key API is undocumented. The routes are `/auth/login` and
`/server/keys`, and the Bearer value is base64 JSON of `{token, hash}` —
not the session token itself, which is why the obvious call returns
"Malformed session token". The site key and secret were created that way
rather than by hand in a dashboard.
And an nginx proxy_pass whose target is a variable passes the URI through
untouched: the trailing slash that strips a location prefix on a literal
target does nothing. Cap was being asked for /cap/<key>/challenge and
answering NOT_FOUND until the prefix was stripped by an explicit rewrite.
Verified end to end against the running service: a challenge is issued
through the public path, and a token that was never issued is refused
rather than waved through.
Also here: the register modal's Name and Email were bare labels that
neither wrapped their input nor named it, so a screen reader met two
boxes with no names and clicking the word did nothing.
And the knowledge profile paginates ten to a page and expands each row to
its two bars beside the next step. "Correct using hints" is missing from
that bar because /study-tools/recommendations does not carry it per
topic — inferring it from the lifetime figure would be a different set of
answers, so the bar is honestly two-tone until the backend offers it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
472 lines
23 KiB
Markdown
472 lines
23 KiB
Markdown
# PedsHub
|
|
|
|
AI-powered pediatric learning platform. Upload PDF study materials, automatically extract or generate MCQ questions with AI, study with an AI tutor, and track your progress. Installable as a PWA.
|
|
|
|
## Features
|
|
|
|
- **PDF → Quiz**: Upload PREP PDFs, AI extracts questions, answers, and explanations
|
|
- **Generative Mode**: AI reads plain text/textbook chapters and creates MCQ questions from scratch
|
|
- **Quiz Modes**: Study (instant feedback with AI tutor) and Exam (timed, scored)
|
|
- **AI Tutor (TeachChat)**: Ask follow-up questions mid-study — AI knows the current question, correct answer, and related content. Renders markdown tables, code, and follow-up suggestion chips.
|
|
- **Tag Classification**: AI classifies questions with subjects, diseases, and keywords — filter your question bank by any combination of tags
|
|
- **Multi-Category Filtering**: Filter questions by question category, tags, or quiz source — combine multiple filters for precise study sets
|
|
- **Text-to-Speech**: LiteLLM-routed local TTS, OpenAI TTS, ElevenLabs, Google Cloud — voice selection per quiz
|
|
- **Semantic Search**: pgvector embeddings — finds questions by meaning, not just keywords
|
|
- **Question Bank**: All questions searchable, filterable by category and tags, with inline study mode
|
|
- **Image Validation**: AI `has_figure` gating — only links extracted images to questions the AI flagged as having a figure, preventing mismatched images
|
|
- **Performance Tracking**: Per-quiz attempt history with line charts, score trends, delete individual attempts
|
|
- **PDF Processing Progress**: Step-by-step progress reporting for PDF uploads and quiz extraction — see each stage in real time
|
|
- **Concurrent Quiz Protection**: Redis session locks prevent the same quiz from being resumed on multiple devices simultaneously
|
|
- **Landing Page**: Integrated with the app — Sign In / Register open as modal overlays, shared Navbar
|
|
- **PWA**: Installable on mobile/desktop (no caching — avoids stale JS issues)
|
|
- **Bot Protection**: Cap on registration and contact forms
|
|
- **Email Verification**: Required before first login; password reset via email
|
|
- **Role System**: Admin / Moderator / User with optional rate-limit exemption (unthrottle)
|
|
- **Admin User Management**: Delete users, change roles, toggle unthrottle — all from the admin dashboard
|
|
- **Nextcloud Integration**: Browse and import PDFs from your Nextcloud
|
|
- **Themes**: Default (blue) and Warm/Literary (brown, serif fonts)
|
|
- **Multi-worker**: 4 uvicorn workers with Redis singleton lock for scheduler
|
|
|
|
## Stack
|
|
|
|
| Layer | Tech |
|
|
|---|---|
|
|
| Frontend | React 18 + React Router 6, plain CSS, Nginx, PWA (network-only service worker) |
|
|
| Backend | FastAPI, SQLAlchemy, PostgreSQL 16 + pgvector |
|
|
| AI/LLM | LiteLLM proxy (Claude, Gemini, GPT, Bedrock, and more) |
|
|
| Embeddings | Configurable — any LiteLLM proxy model or direct AWS Bedrock (1024-dim) |
|
|
| Document vectors | ChromaDB |
|
|
| TTS | LiteLLM-routed local TTS, OpenAI (direct), ElevenLabs, Google Cloud TTS |
|
|
| Queue | Celery + Redis (4 fork workers) |
|
|
| Email | SMTP (smtp2go or any SMTP server) |
|
|
| Bot protection | Cap (runtime-configurable, no rebuild needed) |
|
|
|
|
For detailed architecture documentation, see [docs/architecture.md](docs/architecture.md).
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
git clone ssh://git.danvics.com:2222/danvics/pdf-quiz-generator.git
|
|
cd pdf-quiz-generator
|
|
|
|
# Configure environment
|
|
cp backend/.env.example backend/.env # edit with your keys
|
|
|
|
docker compose up -d
|
|
```
|
|
|
|
Frontend available at `http://localhost:8081`. The first registered user becomes admin automatically.
|
|
|
|
## Environment Variables
|
|
|
|
### Backend (`backend/.env`)
|
|
|
|
```env
|
|
# Database
|
|
DATABASE_URL=postgresql://pedquiz:<password>@postgres:5432/pedquiz
|
|
SECRET_KEY=<random-32-char-string>
|
|
|
|
# Redis
|
|
REDIS_URL=redis://redis:6379/0
|
|
|
|
# LLM — for question extraction (requires LiteLLM proxy or direct OpenAI)
|
|
LITELLM_MODEL=openai/claude-haiku-4.5 # prefix with openai/ when using proxy
|
|
LITELLM_API_KEY=<your-litellm-or-openai-key>
|
|
LITELLM_API_BASE=https://your-litellm-proxy.com # leave empty for direct OpenAI
|
|
|
|
# Embedding model — use the model name exactly as your proxy lists it (no prefix needed)
|
|
# Can also be changed live via Admin → More settings without redeploying
|
|
LITELLM_EMBEDDING_MODEL=gemini-embedding-001
|
|
|
|
# OpenAI (for TTS — calls api.openai.com directly, not the proxy)
|
|
OPENAI_API_KEY=<openai-api-key>
|
|
|
|
# AWS (for Polly TTS + optional direct Bedrock embedding fallback)
|
|
AWS_ACCESS_KEY_ID=<key>
|
|
AWS_SECRET_ACCESS_KEY=<secret>
|
|
AWS_REGION=us-east-1
|
|
AWS_BEDROCK_REGION=us-east-1
|
|
|
|
# ElevenLabs TTS (optional)
|
|
ELEVENLABS_API_KEY=<key>
|
|
|
|
# Google Cloud TTS (optional)
|
|
GOOGLE_TTS_API_KEY=<key>
|
|
|
|
# Email
|
|
MAIL_SERVER=mail.smtp2go.com
|
|
MAIL_PORT=587
|
|
MAIL_USERNAME=<smtp2go-username>
|
|
MAIL_PASSWORD=<smtp2go-password>
|
|
MAIL_FROM=noreply@yourdomain.com
|
|
MAIL_STARTTLS=true
|
|
|
|
# Bot protection — Cap (backend secret)
|
|
CAP_SECRET_KEY=<cap-secret-key>
|
|
|
|
# Contact form admin notifications
|
|
ADMIN_EMAIL=admin@yourdomain.com
|
|
|
|
# App
|
|
APP_URL=https://your-domain.com
|
|
UPLOAD_DIR=/app/uploads
|
|
MAX_UPLOAD_SIZE=524288000
|
|
CHROMA_PERSIST_DIR=/app/chroma_data
|
|
|
|
# Optional bootstrap admin. Leave blank to let the first registered user become admin.
|
|
DEFAULT_ADMIN_EMAIL=
|
|
DEFAULT_ADMIN_PASSWORD=
|
|
```
|
|
|
|
### Frontend (`frontend/.env`)
|
|
|
|
```env
|
|
# Bot protection — Cap (public site key)
|
|
CAP_SITE_KEY=<cap-site-key>
|
|
```
|
|
|
|
The frontend env file is **not** baked into the Docker image at build time. Instead, `docker-entrypoint.sh` generates a `/config.js` file from the env vars when the container **starts**. This means:
|
|
|
|
- Change keys by editing `frontend/.env` and restarting the container (no rebuild)
|
|
- Switch captcha providers by updating the entrypoint script and the widget component
|
|
- Remove bot protection by clearing the key (empty = disabled)
|
|
|
|
## Cap (Bot Protection)
|
|
|
|
### How it works
|
|
|
|
Cap protects the **registration** and **contact** forms from bot submissions. One shared component, `frontend/src/components/Captcha.jsx`, renders every widget; one backend service, `backend/app/services/captcha.py`, verifies every token.
|
|
|
|
**Flow:**
|
|
|
|
```
|
|
1. Page loads → Captcha component loads js.cap.com/1/api.js (once)
|
|
2. Widget renders where the component sits on the form
|
|
3. User completes challenge → widget calls onVerify(token)
|
|
4. Frontend stores token in state → Sign Up button becomes enabled
|
|
5. User submits form → token sent as `captcha_token` in POST body
|
|
6. Backend receives token → POSTs to Cap's siteverify API:
|
|
POST https://api.cap.com/siteverify
|
|
Body: { secret: CAP_SECRET_KEY, response: captcha_token }
|
|
7. Cap returns { success: true/false }
|
|
8. If false → 400 "Bot verification failed"
|
|
9. If true → registration proceeds normally
|
|
```
|
|
|
|
A solve expires after a couple of minutes; the widget says so, the component clears the stored token, and the submit button goes back to disabled rather than sending something that will be refused.
|
|
|
|
**Where Cap is active:**
|
|
- Register form (modal overlay on landing page)
|
|
- Register form (standalone `/register` page)
|
|
- Contact form (landing page)
|
|
|
|
**Where it is NOT active (by design):**
|
|
- Login — protected by IP-based rate limiting (10 attempts / 15 min) instead
|
|
|
|
**When Cap itself is unreachable**, registration is let through and the contact form is not: an outage that blocks sign-ups costs the site its users, while a bounced message costs the sender one retry.
|
|
|
|
### Setup
|
|
|
|
1. Go to https://dashboard.cap.com → Sites → New Site
|
|
2. Add your domain(s)
|
|
3. Copy the Site Key, and the account's Secret Key from Settings
|
|
|
|
```bash
|
|
# frontend/.env
|
|
CAP_SITE_KEY=10000000-ffff-...
|
|
|
|
# backend/.env
|
|
CAP_SECRET_KEY=ES_...
|
|
|
|
# Restart (no rebuild needed)
|
|
docker compose restart frontend backend
|
|
```
|
|
|
|
The CSP in `frontend/nginx.conf` already allows `cap.com` and its subdomains for scripts, frames, images and XHR; a provider change means editing that header too.
|
|
|
|
### Testing
|
|
|
|
Cap publishes a key pair that always passes:
|
|
|
|
| Purpose | Site Key | Secret Key |
|
|
|---|---|---|
|
|
| Always passes | `10000000-ffff-ffff-ffff-000000000001` | `0x0000000000000000000000000000000000000000` |
|
|
|
|
### Disabling
|
|
|
|
Set both keys to empty strings (or remove them), restart. The widget won't render and the backend skips verification.
|
|
|
|
### Runtime Config Architecture
|
|
|
|
```
|
|
docker-compose.yml
|
|
└─ frontend service: env_file: ./frontend/.env
|
|
|
|
Container startup (docker-entrypoint.sh):
|
|
└─ Reads $CAP_SITE_KEY from environment
|
|
└─ Writes /usr/share/nginx/html/config.js:
|
|
window.__APP_CONFIG__ = { CAP_SITE_KEY: "10000000-ffff-..." };
|
|
|
|
Browser loads index.html:
|
|
└─ <script src="/config.js"> → sets window.__APP_CONFIG__
|
|
└─ <script type="module" src="/src/main.jsx"> → React app starts
|
|
└─ Components read: window.__APP_CONFIG__?.CAP_SITE_KEY
|
|
```
|
|
|
|
This avoids Vite's `import.meta.env.VITE_*` which bakes values into the JS bundle at build time.
|
|
|
|
## Extraction Modes
|
|
|
|
| Mode | Use Case | How It Works |
|
|
|---|---|---|
|
|
| **Standard** | PREP PDFs with inline answers | Finds "Correct Answer: X" / "Preferred Response: X" after each question |
|
|
| **Two-Step** | PREP 2013-style (separate answer key) | Phase 1: extract questions, Phase 2: extract answer key, Phase 3: match |
|
|
| **Questions Only** | No answers available | Extracts Q + options, sets answer to "PENDING" for manual fill |
|
|
| **AI + Regex** | Non-standard answer formats | AI analyses format, generates regex, applies it for fast extraction |
|
|
| **AI Decides** | Unknown format | AI samples start + end of document, picks the best strategy |
|
|
| **Generate** | Textbook chapters, study notes | AI reads the text and **creates** MCQ questions (correct answer from text, distractors from AI knowledge) |
|
|
|
|
The extraction model is selected per-job from the dropdown on the document detail page. Models are configured in Admin → AI Models with task `extraction`.
|
|
|
|
## Tag Classification
|
|
|
|
PedsHub can automatically classify questions with structured tags using AI. Tags are organized into three types:
|
|
|
|
| Tag Type | Examples | Purpose |
|
|
|---|---|---|
|
|
| **Subject** | Cardiology, Infectious Disease, Neonatology | Medical specialty / rotation |
|
|
| **Disease** | Kawasaki Disease, Pneumonia, Type 1 Diabetes | Specific condition mentioned |
|
|
| **Keyword** | fever workup, antibiotic resistance, fluid management | Clinical concept / topic |
|
|
|
|
**How it works:**
|
|
|
|
1. Admin triggers classification from the admin dashboard (or via the `/api/admin/classify` endpoint)
|
|
2. A Celery background task (`classify_questions`) finds all untagged questions
|
|
3. Questions are sent to the AI in batches of 10 — the AI returns subjects, diseases, and keywords for each
|
|
4. Tags are stored in `question_tags` with case-insensitive deduplication and linked via `question_tag_links`
|
|
5. Users can filter the question bank by any combination of tags in the Question Bank page
|
|
|
|
The classification model is configured in Admin → AI Models with task `keyword`.
|
|
|
|
## AI Tutor (TeachChat)
|
|
|
|
The study-mode AI tutor opens as a slide-in drawer. It:
|
|
|
|
- Knows the current question, correct answer, and explanation
|
|
- Pulls related questions from the question bank via semantic similarity (pgvector)
|
|
- Supports model selection (dropdown when multiple teach models configured)
|
|
- Renders full markdown including GFM tables (via `remark-gfm` + `rehype-raw`)
|
|
- Generates follow-up suggestion chips after each response
|
|
- Uses `litellm.acompletion()` (async) so it doesn't block other requests
|
|
|
|
Models for teach are configured in Admin → AI Models with task `teach`.
|
|
|
|
## Rate Limiting
|
|
|
|
| Endpoint | Limit | Scope |
|
|
|---|---|---|
|
|
| Login | 10 attempts / 15 min | Per IP (Redis) |
|
|
| TeachChat | 30 calls / 10 min | Per user (Redis) |
|
|
| TTS | 20 calls / 5 min | Per user (Redis) |
|
|
|
|
Admins, moderators, and users marked as "unthrottled" are exempt. Rate limiting degrades gracefully if Redis is unavailable (allows requests through).
|
|
|
|
## PWA (Progressive Web App)
|
|
|
|
The app is installable on mobile and desktop. The service worker is **network-only** (no caching) — it exists solely to satisfy the PWA installability requirement. This avoids stale cached JS causing blank screens or module-not-found errors after deployments.
|
|
|
|
Files: `frontend/public/manifest.json`, `frontend/public/sw.js`, `frontend/public/icon.svg`
|
|
|
|
## Rebuild & Restart
|
|
|
|
Frontend and backend are built into Docker images — code changes require a `build` before they take effect. Environment variable changes only need a `restart`.
|
|
|
|
```bash
|
|
# Rebuild and restart everything
|
|
docker compose build && docker compose up -d
|
|
|
|
# Rebuild a single service
|
|
docker compose build backend && docker compose up -d backend
|
|
docker compose build frontend && docker compose up -d frontend
|
|
|
|
# Restart without rebuilding (for .env changes, including Cap keys)
|
|
docker compose restart frontend backend
|
|
|
|
# View logs
|
|
docker compose logs backend --tail=50
|
|
docker compose logs celery --tail=50
|
|
```
|
|
|
|
## Admin Dashboard
|
|
|
|
Accessible at `/admin` for admin users. Three tabs:
|
|
|
|
### AI Models
|
|
- **Search models** from your LiteLLM proxy — click any result to pre-fill the add form
|
|
- Configure models per task: `extraction` (PDF → questions), `teach` (AI tutor), `keyword` (tag classification), `tts` (voices)
|
|
- Same model can be added to multiple tasks (e.g. `bedrock-claude-haiku-4.5` for both extraction and teach)
|
|
- Set a default model per task; enable/disable individual models
|
|
- Extraction models from the proxy don't need an `openai/` prefix — the backend adds it automatically
|
|
|
|
### Users
|
|
- Create users directly (email auto-verified)
|
|
- Change user roles: admin / moderator / user
|
|
- Unthrottle users (exempt from rate limits)
|
|
- **Delete users** — removes the user and all associated data (documents, quizzes, attempts)
|
|
|
|
### More Settings
|
|
- **Public Registration** — enable/disable new user sign-ups
|
|
- **Classify Questions** — trigger AI tag classification for all untagged questions (runs as background task)
|
|
- **Embedding Model** — set the model used for semantic search vectors:
|
|
- Type a model name and click **Save**, or click **Search LiteLLM** to browse proxy models
|
|
- Click **Test** to verify the model works and returns the correct dimensions (must be 1024)
|
|
- Setting is stored in Redis and takes effect immediately — no restart needed
|
|
|
|
## CLI Management
|
|
|
|
```bash
|
|
# ── User management ──────────────────────────────────────────────────────────
|
|
docker compose exec backend python manage.py reset-password admin@example.com NewPassword123
|
|
docker compose exec backend python manage.py list-users
|
|
|
|
# ── Quiz extraction ───────────────────────────────────────────────────────────
|
|
docker compose exec backend python manage.py list-sections
|
|
docker compose exec backend python manage.py extract 6 --bg
|
|
docker compose exec backend python manage.py extract 6 --bg --title "PREP 2012 Full" --mode timed
|
|
|
|
# ── Embeddings ───────────────────────────────────────────────────────────────
|
|
docker compose exec backend python manage.py reembed
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Browser
|
|
│
|
|
▼
|
|
Caddy/Nginx (HTTPS termination)
|
|
│
|
|
▼
|
|
Nginx (frontend container — serves React SPA + proxies /api to backend)
|
|
├─► React SPA (static files + runtime config.js)
|
|
└─► FastAPI backend (4 uvicorn workers)
|
|
├─ PostgreSQL 16 + pgvector ← users, quizzes, questions + 1024-dim embeddings
|
|
├─ ChromaDB ← document page chunks for RAG context
|
|
├─ Redis ← Celery queue, rate limits, settings, quiz progress, singleton lock
|
|
├─ Celery workers (x4) ← background PDF processing, quiz extraction, tag classification, emails
|
|
├─ LiteLLM proxy ← Claude/Gemini/GPT for extraction + teach + classification; embedding models
|
|
├─ AWS Bedrock ← Polly TTS; embedding fallback
|
|
├─ OpenAI ← TTS (direct, not via proxy)
|
|
└─ Cap ← bot verification for registration + contact
|
|
```
|
|
|
|
For deep architecture documentation (database schema, request flow, background tasks, vector search, auth, and more), see [docs/architecture.md](docs/architecture.md).
|
|
|
|
### Multi-Worker Setup
|
|
|
|
The backend runs 4 uvicorn workers. Two coordination mechanisms prevent worker races at startup:
|
|
|
|
- **Postgres advisory lock** (`pg_advisory_lock(8472931)`) serializes startup DDL (`Base.metadata.create_all()` + `setup_pgvector()`). Only one worker runs the schema operations; the others wait, then see the idempotent `IF NOT EXISTS` statements as no-ops. This eliminates the deadlock that used to kill one worker on boot when concurrent `ALTER TABLE`s acquired locks in different orders.
|
|
- **Redis SETNX lock** (`startup:singleton_lock`, TTL 300s) ensures only one worker starts the APScheduler and backfill thread.
|
|
|
|
Stale `idle in transaction` DB connections are killed at startup to prevent DDL migration hangs.
|
|
|
|
### Database Migrations (Alembic)
|
|
|
|
Schema changes go through Alembic (`backend/alembic/`). See [docs/migrations.md](docs/migrations.md) for the developer workflow (create → review → apply → rollback).
|
|
|
|
Alembic reads `DATABASE_URL` from the container env; there is no hardcoded URL in `alembic.ini`. The live DB tracks its version in `alembic_version`. `Base.metadata.create_all()` remains as a fallback for fresh deploys and must not be removed without first generating a baseline migration.
|
|
|
|
### Landing Page
|
|
|
|
The landing page at `/home` uses the shared `Navbar` component. When not logged in:
|
|
- Navbar shows Sign In / Register buttons
|
|
- Clicking either opens a **modal overlay** (no page navigation) with tabbed Sign In / Register forms
|
|
- Hero CTA buttons also open the modal
|
|
- Standalone `/login` and `/register` pages still exist for direct links, email verification redirects, and bookmarks
|
|
|
|
### Security
|
|
|
|
- **SQL injection**: pgvector queries use parameterized bind variables (`CAST(:vec AS vector)`)
|
|
- **API key exposure**: LiteLLM/TTS model search uses POST body, not URL query params
|
|
- **Rate limiting**: Redis INCR + TTL keys on login, teach, and TTS endpoints
|
|
- **Bot protection**: Cap on registration and contact forms
|
|
- **CSP headers**: Configured in nginx.conf for fonts, Cap, and self
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
├── backend/
|
|
│ ├── app/
|
|
│ │ ├── main.py # App startup, DB migrations, Redis singleton lock
|
|
│ │ ├── config.py # Settings (pydantic-settings, reads .env)
|
|
│ │ ├── models/ # SQLAlchemy ORM models
|
|
│ │ ├── routers/
|
|
│ │ │ ├── auth.py # Login, register (with Cap), verify email, password reset
|
|
│ │ │ ├── quizzes.py # Quiz CRUD, async extraction jobs
|
|
│ │ │ ├── attempts.py # Quiz attempts, progress save/resume, history, stats
|
|
│ │ │ ├── teach.py # AI tutor chat (async, with follow-up suggestions)
|
|
│ │ │ ├── admin.py # Model management, user roles, settings, classification trigger
|
|
│ │ │ ├── tags.py # Tag listing and filtering endpoints
|
|
│ │ │ ├── contact.py # Contact form (with Cap)
|
|
│ │ │ └── ...
|
|
│ │ ├── services/
|
|
│ │ │ ├── ai_service.py # LLM calls + _proxy_model() routing
|
|
│ │ │ ├── extraction_modes.py # 6 extraction strategies (standard → generate)
|
|
│ │ │ ├── embedding_service.py # pgvector embeddings (httpx → proxy)
|
|
│ │ │ ├── vector_service.py # ChromaDB document page chunks
|
|
│ │ │ └── email_service.py # Email templates + sending
|
|
│ │ ├── tasks/
|
|
│ │ │ ├── quiz_tasks.py # Celery: quiz extraction, tag classification, re-embed
|
|
│ │ │ └── pdf_tasks.py # Celery: PDF text extraction + vectorization
|
|
│ │ └── utils/
|
|
│ │ └── auth.py # JWT, password hashing, rate limiting
|
|
│ ├── manage.py # CLI: reset-password, list-users, reembed, extract
|
|
│ └── Dockerfile
|
|
├── frontend/
|
|
│ ├── src/
|
|
│ │ ├── pages/
|
|
│ │ │ ├── LandingPage.jsx # Landing + contact form + auth modal + Cap
|
|
│ │ │ ├── DashboardPage.jsx # Stats, performance chart, attempt history with delete
|
|
│ │ │ ├── QuizzesPage.jsx # Quiz grid, search, past attempts with delete
|
|
│ │ │ ├── QuizPage.jsx # Quiz taking (study/exam), TTS, progress save
|
|
│ │ │ ├── ResultsPage.jsx # Score card, answer review, delete attempt
|
|
│ │ │ ├── DocumentDetailPage.jsx # Sections, extraction mode picker, model selector
|
|
│ │ │ ├── RegisterPage.jsx # Standalone register (with Cap)
|
|
│ │ │ └── ...
|
|
│ │ ├── components/
|
|
│ │ │ ├── Navbar.jsx # Shared navbar (auth-aware, optional modal callbacks)
|
|
│ │ │ ├── TeachChat.jsx # AI tutor drawer (markdown, tables, suggestions)
|
|
│ │ │ └── ...
|
|
│ │ └── context/ # AuthContext, ThemeContext
|
|
│ ├── public/
|
|
│ │ ├── manifest.json # PWA manifest
|
|
│ │ ├── sw.js # Service worker (network-only, no cache)
|
|
│ │ ├── icon.svg # App icon
|
|
│ │ └── config.js # Dev fallback (overwritten by entrypoint in prod)
|
|
│ ├── nginx.conf # API proxy + CSP headers
|
|
│ ├── docker-entrypoint.sh # Generates /config.js from env vars at runtime
|
|
│ └── Dockerfile
|
|
├── docs/
|
|
│ └── architecture.md # Deep architecture documentation
|
|
├── frontend/.env # Frontend runtime config (Cap site key)
|
|
├── backend/.env # Backend config (all secrets)
|
|
└── docker-compose.yml
|
|
```
|
|
|
|
## Deployment Notes
|
|
|
|
- The frontend Nginx binds to `127.0.0.1:8081` — put Caddy or Nginx in front for HTTPS
|
|
- PostgreSQL data persists in the `postgres_data` Docker volume — back it up regularly
|
|
- Uploads live in the `uploads_data` volume — includes extracted question images
|
|
- Redis data persists in `redis_data` volume — holds runtime settings and job state
|
|
- Set `APP_URL` to your public domain so email verification and password reset links work
|
|
- Frontend env vars (like the Cap site key) are injected at container startup, not build time — change and restart, no rebuild needed
|
|
|
|
## TTS Providers
|
|
|
|
| Provider | Model/voice ID format | Key needed |
|
|
|---|---|---|
|
|
| Kokoro via LiteLLM | `local-kokoro-tts:am_adam`, `local-kokoro-tts:af_bella` | `LITELLM_API_KEY` |
|
|
|
|
The part before `:` is the LiteLLM speech model route. The part after `:` is the Kokoro speaker voice.
|