pdf-quiz-generator/docs/frontend.md
Daniel 56fdc57389 fix: gateway-agnostic URL handling for TTS and embeddings, docs cleanup
- Fix double /v1 in TTS audio/speech URL when LITELLM_API_BASE includes /v1
- Fix double /v1 in embedding service and vector service URLs
- Clean up docs: remove second-person language in deployment, frontend, migrations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 02:17:35 +02:00

19 KiB

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 <App /> inside <React.StrictMode>.

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 <Navbar />, 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 <token> 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:

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:

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:

cat > /usr/share/nginx/html/config.js <<EOF
window.__APP_CONFIG__ = {
  TURNSTILE_SITE_KEY: "${TURNSTILE_SITE_KEY:-}"
};
EOF
exec nginx -g 'daemon off;'

config.js is loaded by index.html before the app bundle. Components access it via window.__APP_CONFIG__?.TURNSTILE_SITE_KEY. This avoids baking environment-specific values into the build.

The frontend .env file sets TURNSTILE_SITE_KEY which is read by Docker Compose and passed to the entrypoint script.