# PedsHub Frontend Documentation ## Architecture Overview The frontend is a React SPA built with Vite and served via Nginx. It communicates with the backend exclusively through `/api` (proxied by Nginx to the backend container). ### Entry Point `main.jsx` renders `` inside ``. `App.jsx` provides the top-level provider hierarchy: ``` BrowserRouter ThemeProvider AuthProvider AppRoutes ``` ### Routing `AppRoutes` reads `useAuth()` to decide what to render: | Route | Component | Access | |---|---|---| | `/home` | LandingPage | Public (always) | | `/login` | LoginPage | Public (redirects to `/` if logged in) | | `/register` | RegisterPage | Public (redirects to `/` if logged in) | | `/verify-email` | VerifyEmailPage | Public | | `/forgot-password` | ForgotPasswordPage | Public | | `/reset-password` | ResetPasswordPage | Public | | `/` | DashboardPage | Authenticated | | `/quizzes` | QuizzesPage | Authenticated | | `/quizzes/:id` | QuizPage | Authenticated | | `/results/:id` | ResultsPage | Authenticated | | `/documents/:id` | DocumentDetailPage | Authenticated | | `/account` | AccountPage | Authenticated | | `/settings` | SettingsPage | Authenticated | | `/question-bank` | QuestionBankPage | Authenticated | | `/admin` | AdminPage | Authenticated | | `/upload` | UploadPage | Moderator only | | `/quizzes/:id/edit` | QuizEditPage | Moderator only | | `/jobs` | JobsPage | Moderator only | | `/trash` | TrashPage | Moderator only | | `*` | NotFoundPage or redirect to `/home` | Catch-all | Unauthenticated users hitting any protected route are redirected to `/home`. Non-moderators hitting moderator routes are redirected to `/`. `AppLayout` wraps authenticated routes with ``, a container div, and a footer. --- ## Context Providers ### AuthContext (`context/AuthContext.jsx`) Provides `{ user, loading, login, loginWithToken, logout }`. - On mount, checks `localStorage.getItem('token')` and calls `GET /auth/me` to hydrate the user object. - `login(email, password, turnstileToken)` posts to `/auth/login`, stores the JWT, then fetches `/auth/me`. - `loginWithToken(token)` stores a token directly (used after registration when verification is not required). - `logout()` clears the token and sets user to null. ### ThemeContext (`context/ThemeContext.jsx`) Provides `{ theme, setTheme }`. - Persists the active theme name in `localStorage` under key `pedquiz_theme`. - Applies the theme by setting `data-theme` attribute on `document.body`. --- ## Page Descriptions ### LandingPage (`/home`) The public-facing marketing page. Sections: 1. **Hero** - Gradient background with tagline "Turn your textbooks into tests". Shows Sign In / Register buttons for guests, or Dashboard / My Quizzes / Question Bank links for logged-in users. 2. **Features** - 6 feature cards (Quiz from Any PDF, AI Tutor, Audio Mode, Question Bank, Track Your Progress, Fast by Design) rendered from a static `FEATURES` array. Cards have hover lift animations. 3. **AI Scribe cross-sell** - Promotes the sibling PedsHub AI Scribe product (links to `https://peds.danvics.com`). 4. **Contact form** - `ContactForm` component with name, email, type selector (Question or Apply as Moderator), and message. Validates required fields. Includes a `TurnstileWidget` for Cloudflare captcha. Posts to `POST /contact`. 5. **Auth modal** - `AuthModal` component rendered as an overlay with Sign In / Register tabs. Handles login, registration, email verification resend, and unverified-email warnings. The Turnstile widget is embedded inside the modal. The Navbar receives `onSignIn` and `onRegister` callbacks to open the modal. **Turnstile integration**: The site key comes from `window.__APP_CONFIG__.TURNSTILE_SITE_KEY`. The Turnstile script is loaded dynamically once. `TurnstileWidget` retries rendering with a 200ms polling loop until `window.turnstile` is available. ### DashboardPage (`/`) The authenticated home screen. Loads three API calls in parallel on mount: - `GET /documents/` (moderators only, otherwise skipped) - `GET /attempts/stats/dashboard` (total quizzes, attempts, avg score, optionally documents) - `GET /attempts/history` (attempt history grouped by quiz) Features: - **Greeting** - Time-of-day greeting using the user's first name. - **Stats cards** - Grid of stat cards (Documents for moderators, Quizzes, Attempts, Avg Score). - **Performance chart** - `LineChart` SVG component showing score trend for a selected quiz. Dropdown to switch between quizzes. Shows latest score, best score, and a retake button. Warns if latest score is below 75%. - **Attempt history** - Listed under the chart. Each attempt row shows date, score link to results, and inline delete with two-click confirmation (`confirmAttempt` state pattern). - **Documents list** - Moderator-only card listing uploaded PDFs with status badges and an Upload PDF button. ### QuizzesPage (`/quizzes`) Loads quizzes and categories in parallel on mount. Features: - **Search bar** - Debounced search (350ms) across quizzes and questions. Three filter modes: All, Title only, Questions only. Results show quiz title matches and matching question snippets with `HighlightText` highlighting. - **Expanded search** - Toggle to view all matching questions in a flat list. Each question has a Study button. - **QuestionStudyModal** - Interactive modal for answering a single question, showing correct/incorrect feedback, and displaying the explanation. Includes a lazy-loaded `TeachChat` with the `elevated` prop for higher z-index (above the modal). - **In-progress quizzes** (`InProgressSection`) - Shows quizzes the user started but hasn't finished. Resume and Delete buttons. - **Past attempts** (`PastAttemptsSection`) - Collapsible section, loads on demand. Delete buttons on each attempt. - **Quiz grid** - Quizzes grouped by category (moderator-created categories). `QuizCard` component shows title, question count, mode badge (Learning/Timed), time limit. Hover lift effect. - **Moderator controls** - Edit link, publish/unpublish toggle, category assignment dropdown, trash button with `ConfirmButton`. - **Category management** - Moderators can add categories inline, delete categories, and see quiz counts per category. ### QuizPage (`/quizzes/:id`) The core quiz-taking experience. Two phases: mode selection, then quiz. **Mode selection** (`ModeSelectScreen`): - Study Mode: answers and explanations revealed after each question. - Exam Mode: answers hidden until submitted. - Optional timer input (minutes) for exam mode. - TTS voice selector (if voices are configured). **Quiz phase**: - **Concurrent device lock** - A unique `SESSION_ID` is generated per tab (`Math.random() + Date.now()`). Sent as `x-quiz-session` header when fetching/saving progress. The backend returns 409 if the quiz is active on another device. - **Auto-resume** - On load, fetches `GET /attempts/progress?quiz_id=...`. If saved progress exists, calls `resumeQuiz()` which restores mode, answers, current index, timer state, and voice selection. For study mode, re-fetches the quiz with `?study=true` to get correct answers. - **Timer** - Uses the `timerStarted` pattern: `const timerStarted = timeLeft !== null` declared outside the effect, used as a dependency. The timer is calculated from `started_at + total_time`, so it survives page reloads. Auto-submits when `timeLeft` reaches 0. - **Progress save to Redis** - Debounced at 1.5 seconds. Saves `quiz_id, attempt_id, answers, current_idx, mode, voice, time_left, started_at, total_time` via `POST /attempts/progress`. Uses the session ID header. - **Answer tracking** - `answers` state is a `{ [questionId]: selectedOption }` object. `setAnswer` merges into the object. - **Navigation** - Previous/Next buttons, question number dots (desktop sidebar + mobile collapsible grid). `QuestionDot` shows active (primary), answered (green), and unanswered (gray) states. - **TTS** - `TTSButton` component calls `POST /tts/speak` to get audio blob, creates `Audio` object for playback. Voice selector disabled while audio is playing. - **Favorites** - Star toggle on each question, calls `POST /favorites` or `DELETE /favorites/:id`. - **Suspend** - "Suspend" button shows a confirmation dialog. Progress is already saved; timer warning shown for timed quizzes. Navigates to dashboard on confirm. - **beforeunload** - Browser tab close warning when mid-quiz. - **AI Tutor** - `TeachChat` lazy-loaded in study mode only. ### ResultsPage (`/results/:id`) Shows quiz results. Accepts `location.state.result` for instant display after submission, or fetches `GET /attempts/:id` if navigated directly. Features: - **Score card** - Large percentage display with color coding (green >= 75, yellow >= 50, red < 50). Score bar visualization. Contextual message. - **Summary row** - Correct, Incorrect, and Skipped counts with colored backgrounds. - **Question review** - Full review of each question with: - Correct/Incorrect/Skipped badge - Option display with letter badges, color-coded (correct = green, user's wrong answer = red) - Fill-in-the-blank display for non-MCQ - Explanation section - **Actions** - Retake Quiz, All Quizzes, Dashboard, Delete Attempt (with two-click confirmation). ### QuestionBankPage (`/question-bank`) The central question repository with advanced filtering. Features: - **Search modes** - Three modes available: `keyword` (text search), `semantic` (vector similarity), `hybrid` (combines both). Controlled by `searchMode` state, sent as `search_mode` param. - **Multi-category filter** - `filterCatIds` array state. Toggling a category adds/removes its ID. Uses `catIdsKey = filterCatIds.join(',')` as a useEffect dependency (avoids reference equality issues with arrays). - **Tag browser** (`TagBrowser` component) - Three sections: Subjects, Diseases, Keywords. Each section has its own search input. Tags shown as clickable buttons with counts. Selected tags turn colored. `selectedTagIds` array tracked similarly with `tagIdsKey = selectedTagIds.join(',')`. - **Debounced loading** - 300ms debounce on search query, category, tag, favorites, uncategorized, and page size changes. - **Pagination** - Configurable page size (25, 50, 100, All). Load more button appends to existing questions. - **Favorites filter** - Toggle to show only favorited questions. - **Uncategorized filter** - Toggle to show only questions without a category. - **Bulk operations** - Checkbox selection on questions. "Select All" fetches ALL matching IDs from `GET /questions/bank/ids` (not just the loaded page). Bulk assign category via `POST /questions/bulk-category`. - **Create quiz from selection** - `CreateQuizModal` creates a quiz from the currently selected questions or from all questions in a category. Inputs: title, mode (Timed/Learning), and optional time limit. - **Question study modal** - Same interactive study modal as QuizzesPage with TeachChat (elevated z-index). - **Question edit modal** (`QuestionEditModal`) - Moderators can edit question text, options (with radio button for correct answer), category, and explanation. Warning that changes apply to all quizzes sharing the question. - **Tag classification** - Moderators can trigger AI classification (`POST /tags/classify`). Progress is polled every 2 seconds. Tags reload on completion. - **Category management** - Add new categories inline. Delete categories with optional move-to target. ### DocumentDetailPage (`/documents/:id`) Moderator-focused document management page. Features: - **Document info** - Filename, page count, upload date, status badge. Delete document button. - **Processing progress** - When `doc.status === 'processing'`, polls `GET /documents/:id/status` and `GET /documents/:id/processing-steps` every 2 seconds. Shows step-by-step progress (text extraction, image extraction, etc.). - **Section management** - Form to create sections with name, start page, end page. List of existing sections with Extract & Create Quiz and Delete buttons. - **Quiz settings** (moderator-only panel): - Quiz title (optional, auto-generated if blank) - Mode: Timed or Learning - Time limit (for timed mode) - Extraction mode: Standard, Questions Only, Two-Step, AI + Regex, AI Decides, Generate - Extraction model selector (from `GET /admin/models/available?task=extraction`) - Question bank category selector with inline "New" button - **Extraction progress** (`ExtractionProgress` component) - Modal overlay that polls `GET /quizzes/job/:jobId` every 2 seconds. Shows step icons (text, AI, images, save, error, done) and messages. Job is tracked in `localStorage` under key `pedquiz_jobs` so the Navbar can show it. Close button dismisses the panel but the job continues in the background. ### UploadPage (`/upload`) Moderator-only page for uploading PDFs. Two tabs: 1. **Local File** - Drag-and-drop area or file picker. Accepts `.pdf` only, up to 500MB. Shows file name and size when selected. 2. **Nextcloud** - `NextcloudBrowser` component. Reads credentials from `localStorage` (configured in Settings). Browses folders via `POST /nextcloud/files`, downloads PDFs via `POST /nextcloud/download`. Importing a file auto-triggers upload. **Upload progress** - Uses axios upload progress. Reads `e.progress` (axios 1.x pattern) with fallback to `e.loaded / e.total`. Shows progress bar and percentage. **Auto-upload on Nextcloud import** - When `handleFile` is called with `fromNextcloud = true`, `doUpload(f)` is called immediately without waiting for the user to click Upload. ### AdminPage (`/admin`) Admin-only page with three tabs: Models, Users, Settings. **Access control**: Redirects to `/` if `user.role !== 'admin'`. **loadData(false) pattern**: After mutations (role change, model add/delete, etc.), calls `loadData(false)` which skips the loading spinner but refreshes all data. **Models tab**: - Lists models grouped by task (extraction, tts, teach, keyword). - Each model shows name, model_id, active/inactive toggle, default toggle, test button, delete button. - TTS models have an inline preview player. - **LiteLLM model search** - Posts to `POST /admin/litellm/models` with optional API key and base URL. Shows filterable results. Click "Add" to pre-fill the add-model form. - **TTS voice discovery** - Select provider (ElevenLabs, etc.), enter API key, search. Shows voice list with "Add" buttons. - Add model form: name, model_id, task dropdown, API key, active/default toggles. **Users tab**: - User list with name, email, role, verified status. - Role dropdown (user, moderator, admin). - Unthrottle toggle (removes rate limits for a user). - Delete user with Dialog confirmation. - Create user form. **Settings tab**: - Registration enabled toggle. - Embedding model selector with LiteLLM search, test button, and regeneration trigger if model changed. - Polly enabled toggle. --- ## Components ### Navbar (`components/Navbar.jsx`) Auth-aware navigation bar. Behavior changes based on login state: - **Logged out**: Shows Sign In and Register buttons/links. If `onSignIn` and `onRegister` props are provided (from LandingPage), they trigger callbacks instead of navigating. Otherwise, links to `/login` and `/register`. - **Logged in**: Desktop nav with links (Home, Dashboard, Quizzes, Question Bank, Upload PDF for moderators, Settings). Mobile hamburger menu with animated bars. Logout button. - **JobsBadge** - Polls `GET /quizzes/jobs` for active extraction jobs. Poll interval: 3 seconds if jobs are running, 30 seconds otherwise. Shows running count or total job count. Dropdown with job details and links to completed quizzes. - Auto-closes mobile menu on route change. ### TeachChat (`components/TeachChat.jsx`) Lazy-loaded AI tutor drawer. Appears as a floating action button (graduation cap icon). - **Mobile**: Bottom sheet, 70vh height, slides up. - **Desktop**: Right-side panel, 340px wide, slides in from right. - **Elevated mode**: When `elevated` prop is true, z-index is raised above modals (1060-1070 vs 380-400). Used when TeachChat appears alongside a study modal in QuizzesPage/QuestionBankPage. - **Features**: - Question context chip at top. - Model selector dropdown (if multiple teach models configured). - Markdown rendering with `react-markdown`, `remark-gfm` (GFM tables), and `rehype-raw`. - Follow-up suggestion chips on the last assistant message. Clicking fills the input. - Posts to `POST /teach/chat` with question_id, messages array, and model_id. - Shows "no model" error if backend returns 503. - Chat clears when question changes (keyed on `question.id`). ### Dialog (`components/Dialog.jsx`) Modal replacement for `window.confirm()` and `window.alert()`. Used via the `useDialog()` hook which returns `{ dialogProps, openConfirm, openAlert }`. Props: `open`, `title`, `message`, `confirmLabel`, `cancelLabel` (null = alert mode), `onConfirm`, `onCancel`, `danger` (red confirm button). ### ConfirmButton (`components/ConfirmButton.jsx`) Inline two-click confirmation. First click shows "Confirm" and "Cancel" buttons. Second click fires `onConfirm`. Used throughout for delete operations. ### LineChart (`components/LineChart.jsx`) Pure SVG line chart for performance trends. Renders a line with fill, dot markers (green >= 75%, red < 75%), grid lines, and a dashed 75% threshold line. Requires at least 2 data points. Responsive via `viewBox`. --- ## API Client (`api/client.js`) Axios instance with `baseURL: '/api'`. **Request interceptor**: Attaches `Authorization: Bearer ` from localStorage. **Response interceptor**: - **Token refresh**: Checks `x-new-token` response header on both success and error responses. If present, updates localStorage. This implements sliding token expiration. - **401 handling**: Clears token and redirects to `/login`. Skips redirect for auth endpoints (login, register, verify, reset, forgot). - **Unverified email (403)**: If response has `x-unverified: true` header, clears token and redirects to `/login` (which shows a resend verification option). --- ## State Patterns ### Set vs Array for useEffect deps `selectedIds` in QuestionBankPage is a `Set` (for O(1) lookups). Since React compares deps by reference, toggling creates a new `Set` each time, which correctly triggers re-renders. ### tagIdsKey / catIdsKey join pattern Arrays like `selectedTagIds` and `filterCatIds` would cause useEffect to fire on every render due to reference inequality. The pattern: ```js const tagIdsKey = selectedTagIds.join(',') const catIdsKey = filterCatIds.join(',') useEffect(() => { ... }, [searchQuery, catIdsKey, tagIdsKey, ...]) ``` Converts arrays to stable string keys so useEffect only fires when the actual content changes. ### timerStarted pattern In QuizPage, the timer effect must only start once when `timeLeft` transitions from null to a number: ```js const timerStarted = timeLeft !== null useEffect(() => { if (!timerStarted) return timerRef.current = setInterval(...) return () => clearInterval(timerRef.current) }, [timerStarted]) ``` This prevents the interval from being torn down and recreated every second as `timeLeft` decrements. --- ## Runtime Configuration The frontend Docker image uses `docker-entrypoint.sh` to inject runtime config at container startup: ```sh cat > /usr/share/nginx/html/config.js <