# 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**: Cloudflare Turnstile 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 | Cloudflare Turnstile (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:@postgres:5432/pedquiz SECRET_KEY= # 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= 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= # AWS (for Polly TTS + optional direct Bedrock embedding fallback) AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_REGION=us-east-1 AWS_BEDROCK_REGION=us-east-1 # ElevenLabs TTS (optional) ELEVENLABS_API_KEY= # Google Cloud TTS (optional) GOOGLE_TTS_API_KEY= # Email MAIL_SERVER=mail.smtp2go.com MAIL_PORT=587 MAIL_USERNAME= MAIL_PASSWORD= MAIL_FROM=noreply@yourdomain.com MAIL_STARTTLS=true # Bot protection — Cloudflare Turnstile (backend secret) TURNSTILE_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 — Cloudflare Turnstile (public site key) TURNSTILE_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) ## Cloudflare Turnstile (Bot Protection) ### How it works Turnstile protects the **registration** and **contact** forms from bot submissions. It does NOT require Cloudflare DNS/proxy — it works standalone on any domain. **Flow:** ``` 1. Page loads → Turnstile JS loads from challenges.cloudflare.com 2. Widget renders (invisible or interactive depending on risk score) 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 `turnstile_token` in POST body 6. Backend receives token → POSTs to Cloudflare's siteverify API: POST https://challenges.cloudflare.com/turnstile/v0/siteverify Body: { secret: TURNSTILE_SECRET_KEY, response: turnstile_token } 7. Cloudflare returns { success: true/false } 8. If false → 400 "Bot verification failed" 9. If true → registration proceeds normally ``` **Where Turnstile 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 ### Setup 1. Go to https://dash.cloudflare.com → Turnstile → Add widget 2. Add your domain(s), choose "Managed" widget type 3. Copy the Site Key and Secret Key ```bash # frontend/.env TURNSTILE_SITE_KEY=0x4AAAAAAA... # backend/.env TURNSTILE_SECRET_KEY=0x4AAAAAAA... # Restart (no rebuild needed) docker compose restart frontend backend ``` ### Testing Cloudflare provides test keys for development: | Purpose | Site Key | Secret Key | |---|---|---| | Always passes | `1x00000000000000000000AA` | `1x0000000000000000000000000000000AA` | | Always blocks | `2x00000000000000000000AB` | `2x0000000000000000000000000000000AB` | ### 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 $TURNSTILE_SITE_KEY from environment └─ Writes /usr/share/nginx/html/config.js: window.__APP_CONFIG__ = { TURNSTILE_SITE_KEY: "0x4AAA..." }; Browser loads index.html: └─