Skip to content

04. Frontend Deep Dive

Stack and structure

Item Detail
Framework React 19 + TypeScript
Build Vite 7
Styling Tailwind CSS 4
UI kit shadcn / Base UI primitives under frontend/src/components/ui/
Motion Framer Motion
Icons Lucide
Package manager pnpm (frontend/pnpm-lock.yaml)

Entrypoints: frontend/src/main.tsxfrontend/src/App.tsx. No client-side router — one screen with conditional views.

API client

frontend/src/lib/api.ts:

  • Base: import.meta.env.VITE_API_URL or http://localhost:8000, paths under /ocr
  • Methods: submitJob, getStatus, downloadResult, downloadResultUrl, listJobs, deleteJob
  • Submit appends optional ?vlm_mode=

Types mirror backend enums: pending | processing | done | failed.

Main UI flows

stateDiagram-v2
  [*] --> Landing: App load
  Landing --> Uploading: Submit form
  Uploading --> Polling: job_id received
  Polling --> Results: status=done
  Polling --> Failed: status=failed
  Results --> Landing: Reset / back
  Failed --> Landing: Reset
  Landing --> History: Open sheet
  History --> Results: View job
  History --> Landing: Delete / close

Upload and mode select

  • Modes hardcoded in VLM_MODES: layout, vlm, nvidia, sarvam
  • Card copy claims max 100MB — UI copy only; no client file.size reject found beyond display
  • Accepts PDF/PNG/JPG/TIFF messaging aligned with backend allowlist (webp/bmp also allowed server-side)

Polling

  • setInterval every 2000 ms calling api.getStatus
  • Progress bar uses progress / total_blocks when available
  • On done: fetch both Markdown and HTML text into state
  • On failed: show error from status payload

Results

  • Tabs: HTML preview vs raw Markdown
  • HTML rendered via <iframe srcDoc={...}> with injected dark-theme CSS
  • ZIP download uses api.downloadResultUrl(jobId, 'zip') (navigation/link, not fetch-as-text)

History sheet

  • Lists jobs via api.listJobs
  • View rehydrates a past job into the results view
  • Delete calls api.deleteJob and refreshes list

State management

Local React useState / useRef only — no Redux, Zustand, or TanStack Query. Appropriate for a single-job-focused dashboard.

Loading / error / empty

State Behavior
Loading Spinner + disabled controls while submitting/polling
Error Alert with message
Empty landing Upload drop zone + mode select
History loading Separate isJobsLoading flag

Type safety and build quality

  • TypeScript project references (tsconfig.json, tsconfig.app.json)
  • ESLint config present (frontend/eslint.config.js)
  • Confirmed gap: no frontend unit/e2e tests in repo

Frontend interview Q&A

Q: Why polling instead of WebSockets?
A: ADR 6 — job frequency/duration make 2s polling simpler and more robust than persistent sockets.

Q: How is the API typed?
A: Hand-written interfaces in api.ts matching FastAPI response models; no OpenAPI codegen.

Q: What is the biggest UX risk?
A: Unsanitized HTML in srcDoc can execute scripts if OCR output contains markup; also the 100MB claim is not enforced.