LMS / Course System: - Course → Module → Lesson hierarchy (any user can create) - Lesson types: text, video (Vimeo/YouTube/local), document, quiz, live_session - Video provider auto-detection from URL - BBB API integration (create/join meetings) with env config - Course enrollment with per-lesson progress tracking - AI content generation/refinement for text lessons - Draft/published/archived status workflow - Subscription gate placeholder (requires_subscription flag) - Thumbnail upload support - Module/lesson reorder with up/down controls User Quiz Creation: - Any user can create quizzes from question bank (was moderator-only) - User quizzes: is_published=0, is_shared=0 (private by default) - Fixed section_id fallback: None instead of hardcoded 1 Manual Question Creation: - POST /questions/create endpoint for manual MCQ entry - CreateQuestionModal on QuestionBankPage with options, radio for correct answer - Auto-generates embedding on creation Frontend: - CoursesPage: Browse/My Courses/Created tabs with search and pagination - CourseDetailPage: Student view with module accordion, lesson viewer, progress - CourseEditorPage: Full course builder with AI generate, question bank browser - Courses link in Navbar - Create Question button on Question Bank (available to all users) Backend: - 5 new tables: courses, course_modules, course_lessons, course_enrollments, course_lesson_progress - Course model + schemas + router (22 endpoints) - BBB_SERVER_URL + BBB_SECRET config - Updated CLAUDE.md with LMS documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
114 lines
8.8 KiB
Markdown
114 lines
8.8 KiB
Markdown
# PedsHub — AI Synopsis for Codebase Work
|
|
|
|
## What this is
|
|
PedsHub is a pediatric medical learning platform with an integrated LMS. Admins upload PREP exam PDFs, AI extracts MCQ questions (or generates flashcards), and users study them with an AI tutor. Any user can create courses, quizzes, and questions. 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)
|
|
|
|
## Course/LMS system
|
|
Any user can create courses (not just moderators). Course structure: Course → Modules → Lessons.
|
|
- **Lesson types**: text (markdown), video (Vimeo/YouTube/local), document, quiz (from question bank), live_session (BBB/Zoom/Meet)
|
|
- **Enrollment**: users enroll in published courses, progress tracked per-lesson
|
|
- **Video**: auto-detect provider from URL (vimeo.com → vimeo, youtube.com → youtube)
|
|
- **BBB**: full API integration (create/join/end). Config: `BBB_SERVER_URL` + `BBB_SECRET` in .env
|
|
- **AI**: generate/refine lesson text via `POST /courses/{id}/lessons/{id}/ai-generate`
|
|
- **Status**: draft (creator-only) → published (visible to all) → archived
|
|
- **Subscription gate**: `requires_subscription` flag on courses (402 on enroll if set — Stripe integration placeholder)
|
|
- **Question bank integration**: quiz lessons select questions from bank by category/tags, creates a quiz via `POST /questions/from-bank`
|
|
- **Any user can create quizzes** from the question bank (not just moderators). User quizzes are private by default (`is_published=0`, `is_shared=0`)
|
|
```
|
|
|
|
## 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
|
|
CoursesPage.jsx — Browse/enroll courses, my courses, create courses
|
|
CourseDetailPage.jsx — Student view: modules, lessons, video player, progress
|
|
CourseEditorPage.jsx — Course creator: modules, lessons, AI content, question bank browser
|
|
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 |
|
|
| courses | LMS courses (draft/published/archived) | user_id → users |
|
|
| course_modules | Sections within a course | course_id → courses |
|
|
| course_lessons | Items within a module (text/video/quiz/live) | module_id → course_modules, quiz_id → quizzes |
|
|
| course_enrollments | User enrollment + progress tracking | course_id, user_id (unique) |
|
|
| course_lesson_progress | Per-lesson completion status | enrollment_id, lesson_id (unique) |
|
|
|
|
## 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
|