- CLAUDE.md: comprehensive AI synopsis for working on the codebase (architecture, critical rules, common patterns, what not to do) - Fix ExtractionProgress showing "Extracting Questions" for flashcards — now shows "Generating Flashcards" with correct navigation on done Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
7 KiB
Markdown
94 lines
7 KiB
Markdown
# PedsHub — AI Synopsis for Codebase Work
|
|
|
|
## What this is
|
|
PedsHub is a pediatric medical learning platform. Admins upload PREP exam PDFs, AI extracts MCQ questions (or generates flashcards), and users study them with an AI tutor. Everything runs in Docker.
|
|
|
|
## Critical rules
|
|
- **Never restart services while a Celery task is running** — check `docker compose logs celery --tail=5` first
|
|
- **Backend and Celery share the same code but build separate images** — after changing backend code, you must `docker compose build --no-cache backend celery` then `docker compose up -d backend celery --force-recreate`
|
|
- **Frontend is a Vite build inside Docker** — source changes require `docker compose build frontend` then `docker compose up -d frontend`. Vite minifies function names, so `grep FunctionName` on the built JS won't work.
|
|
- **Never import inside a function body if the same name exists at module level** — Python treats it as a local variable for the entire function scope, causing `UnboundLocalError` before the import line executes. This was a real bug with `sa_text`.
|
|
- **`docker compose restart` does NOT pick up code changes** — it reuses the old image. Always `build` then `up -d --force-recreate`.
|
|
- **Pydantic schemas must match DB nullability** — if a column allows NULL, the schema field must be `type | None`. A mismatch causes 500 on serialization.
|
|
- **The Question model uses `source_quiz_id` as the Python attribute but `quiz_id` as the DB column** — use `Question.source_quiz_id` in SQLAlchemy filters, never `Question.quiz_id`.
|
|
|
|
## Stack
|
|
- **Backend**: FastAPI + SQLAlchemy + PostgreSQL 16 (pgvector) + Redis + Celery
|
|
- **Frontend**: React 18 + Vite + React Router 6 + plain CSS + Nginx
|
|
- **AI**: LiteLLM proxy routes to Claude/GPT/Gemini/Bedrock. `_proxy_model()` in ai_service.py adds `openai/` prefix for the proxy.
|
|
- **Vectors**: ChromaDB for document page chunks (RAG), pgvector for question embeddings (semantic search)
|
|
- **Config**: Backend reads `.env` via pydantic-settings. Frontend uses runtime `window.__APP_CONFIG__` injected by `docker-entrypoint.sh` (not Vite build-time env).
|
|
|
|
## Architecture
|
|
```
|
|
Browser → Nginx (frontend) → FastAPI (4 uvicorn workers)
|
|
├── PostgreSQL (users, quizzes, questions, flashcards, attempts + pgvector embeddings)
|
|
├── ChromaDB (document page chunks for extraction context)
|
|
├── Redis (Celery broker, rate limits, settings, job progress, session locks)
|
|
└── Celery (2 fork workers: PDF processing, quiz extraction, flashcard generation, classification, embedding regeneration)
|
|
```
|
|
|
|
## Key directories
|
|
```
|
|
backend/app/
|
|
main.py — App startup, DDL migrations (setup_pgvector), router mounting, singleton lock
|
|
config.py — All settings from .env
|
|
models/ — SQLAlchemy ORM (user, quiz, question, flashcard, attempt, section, pdf_document, ...)
|
|
schemas/ — Pydantic request/response models
|
|
routers/ — API endpoints (auth, quizzes, questions, flashcards, attempts, admin, teach, tts, tags, ...)
|
|
services/
|
|
ai_service.py — LLM calls, _proxy_model(), get_model_for_task() fallback chain
|
|
extraction_modes.py — 6 quiz extraction modes + flashcard generation prompt
|
|
vector_service.py — ChromaDB: store/query page chunks, LiteLLMEmbeddingFunction
|
|
embedding_service.py — pgvector: embed questions for semantic search
|
|
pdf_service.py — PyMuPDF: text extraction, image extraction with MD5 hash skip list
|
|
tasks/
|
|
quiz_tasks.py — Celery: extract_quiz, classify_questions, regenerate_embeddings, generate_flashcard_deck
|
|
pdf_tasks.py — Celery: process_pdf (text extraction + vectorization)
|
|
|
|
frontend/src/
|
|
App.jsx — Routes (public, authenticated, moderator-only)
|
|
context/AuthContext.jsx — Login/logout/register, JWT token management
|
|
pages/
|
|
DocumentDetailPage.jsx — Section management, "Extract Quiz" / "Create Flashcards" buttons, job progress
|
|
QuestionBankPage.jsx — Browse questions, multi-category + tag filtering, TagBrowser component
|
|
FlashcardsPage.jsx — Browse decks + card browser with search
|
|
FlashcardStudyPage.jsx — Flip cards, got-it/review, keyboard nav, progress
|
|
QuizPage.jsx — Take quiz (exam/study mode), timer, progress save to Redis
|
|
AdminPage.jsx — Model config, user management, settings
|
|
components/
|
|
Navbar.jsx — Auth-aware nav with jobs badge
|
|
TeachChat.jsx — AI tutor drawer (lazy loaded, markdown/GFM tables)
|
|
```
|
|
|
|
## Database tables (key ones)
|
|
| Table | Purpose | Key FKs |
|
|
|-------|---------|---------|
|
|
| users | Accounts with role (admin/moderator/user) | — |
|
|
| pdf_documents | Uploaded PDFs | user_id → users |
|
|
| sections | Page ranges within a document | document_id → pdf_documents |
|
|
| quizzes | Quiz metadata | section_id → sections (nullable), user_id → users |
|
|
| questions | MCQ questions with pgvector embedding | source_quiz_id → quizzes (nullable) |
|
|
| quiz_question_links | Quiz ↔ Question many-to-many | quiz_id, question_id |
|
|
| flashcard_decks | Flashcard deck metadata | section_id → sections, user_id → users |
|
|
| flashcards | Individual cards (front/back) | deck_id → flashcard_decks |
|
|
| question_tags | Tag definitions (subject/disease/keyword) | — |
|
|
| question_tag_links | Question ↔ Tag | question_id, tag_id |
|
|
| flashcard_tag_links | Flashcard ↔ Tag | flashcard_id, tag_id |
|
|
| quiz_attempts | User quiz sessions with score | quiz_id, user_id |
|
|
|
|
## Common patterns
|
|
- **Tag filtering SQL**: `WHERE tag_id = ANY(:tag_ids) GROUP BY ... HAVING COUNT(DISTINCT tag_id) = :cnt` — AND logic across tags
|
|
- **Multi-category filtering**: `category_ids` param (comma-separated), uses `.in_()` — OR logic within categories
|
|
- **Job progress**: Celery tasks push steps to Redis lists (`extraction:steps:{job_id}`), frontend polls `GET /quizzes/job/{job_id}`
|
|
- **Model selection**: Admin configures models per task (extraction, teach, tts, keyword, flashcard). `get_model_for_task(db, task)` returns (model_id, api_key) with fallback to `settings.LITELLM_MODEL`.
|
|
- **useEffect dependencies**: Use `.join(',')` on arrays to create a stable string key (e.g., `tagIdsKey`, `catIdsKey`)
|
|
- **Admin page data refresh**: `loadData(false)` — the `false` param skips the loading spinner on re-fetch after actions
|
|
|
|
## What NOT to do
|
|
- Don't add `from sqlalchemy import text as X` inside functions — import at module top only
|
|
- Don't use `Question.quiz_id` — it's `Question.source_quiz_id`
|
|
- Don't set `Content-Type: multipart/form-data` manually on axios FormData uploads — axios handles it
|
|
- Don't use `[someValue === null]` as a useEffect dependency — it evaluates to a constant boolean
|
|
- Don't `docker compose restart` expecting code changes to apply — must rebuild
|
|
- Don't use `window.confirm()` — user hates browser popups, use inline confirmation or the Dialog component
|