# 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:@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 — Cap (backend secret) 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= ``` 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: └─