Paste this file at the start of any Claude conversation to restore full context instantly. Update it after every meaningful session. Last updated: 2026-04-17. See VISION.md for the product north star and feature priority order — check it before building anything. See research/CLAUDE.md for thesis-specific routing rules (schema, grading, study design).
A document-grounded active recall system implementing FSRS-based spaced repetition and generative evaluation to improve long-term retention of academic literature.
Core loop (current):
- Upload a PDF → chunked, embedded, stored.
- Read in the viewer; highlight any text.
- Hover menu: Explain / Simplify / Quiz / Summarise / Voice / Save note.
- Answers go to chat. Save any highlight + Q&A pair to a per-PDF Highlight Index.
- The Index is proto-flashcard material — browsable by page, starrable, searchable.
Core loop (fully operational as of Phase 9): 6. Saved Q&A pairs enter FSRS scheduling → review queue. 7. Review session (dedicated screen): recall from memory → confidence rating → Claude grades (3D rubric) → FSRS updates interval. 8. Quiz Me (per-card): same grading engine, PDF-scoped due queue. 9. Retention dashboard — not yet built (Tier 1.5).
Scientific foundation: Testing Effect (Roediger & Karpicke 2006), Spaced Repetition (FSRS/Ye et al. 2022), Elaborative Interrogation (Pressley), Generative Learning (Fiorella & Mayer 2015).
See VISION.md for full thesis, priority order, and feature filter.
| Layer | Tech |
|---|---|
| Frontend | React 18 + Vite, Zustand, react-pdf (v9 / PDF.js) |
| Backend | FastAPI (Python 3.12 via conda), SQLAlchemy, PostgreSQL |
| Vector DB | ChromaDB (local HTTP server) |
| Embeddings | sentence-transformers all-MiniLM-L6-v2 |
| LLM | Anthropic Claude claude-sonnet-4-20250514 (prompt caching on system prompt) |
| Voice | OpenAI Whisper (whisper-1) |
| Web search | Tavily (fallback when RAG relevance < 0.75 cosine distance) |
| Storage | AWS S3 (PDFs), PostgreSQL (metadata), ChromaDB (chunks) |
- Monorepo:
backend/(FastAPI) +frontend/(React/Vite) - Docker Compose for Postgres + ChromaDB
- Conda env
pdf-workspace(Python 3.12 — avoids 3.13 incompatibilities)
- Upload → PyMuPDF text extract → 512-token chunks (50 overlap, tiktoken cl100k) → embed → ChromaDB
- Chat: embed question → cosine search → Claude with retrieved context
- Tavily web search fallback when no chunk hits threshold
- PDF streamed from S3 via backend proxy (avoids CORS)
- PDF.js worker loaded via CDN (avoids Vite worker path issues)
- MediaRecorder API,
audio/webm;codecs=opus, timeslice=100ms - Toggle click (start / stop), minimum 1.5s enforced
- Backend strips codec params before MIME validation
- Passes audio as tuple
("recording.webm", bytes, "audio/webm")to OpenAI SDK
mouseuphandler captures selection range, positions floatingSelectionMenu- 7 actions: Explain, Simplify, Key Terms, Quiz Me, Summarise, Ask by voice, Save note
- 8th action: Index — shows Q&A count for related entries, or "empty" if none
onMouseDown={(e) => e.preventDefault()}on ALL menu buttons (prevents selection clear)- Selected text injected directly into system prompt as "HIGHLIGHTED PASSAGE" — bypasses RAG for the selection itself, RAG still runs for supporting context
- Per-PDF index of highlight + Q&A pairs, stored in Zustand
- Organised by page (sticky page headers), secondary view: Starred
- Fuzzy matching (
indexMatch.js) links re-selections to existing entries - Star/unstar entries and individual Q&As
- "Save to Index" prompt appears after any selection-based Q&A
- Fixed blank-page crash: rewrote from module-level sub-components (caused Vite Fast Refresh issue) to single exported component with inline rendering
- Scrolling: windowed render ±3 pages around current; IntersectionObserver updates page counter as user scrolls
- IntersectionObserver fix:
ratioMappersists across callback batches so the MOST-VISIBLE page wins, not just the last-entered one - Editable page number: click the
X / Ydisplay → becomes<input>→ Enter/blur navigates navigateToPage(): setspendingScrollPageref; a no-depsuseEffectretries after every render until the target page mounts — fixes navigation to pages outside the current render window- Persistent highlight: CSS Custom Highlight API (
::highlight(pdf-selection)) keeps the blue highlight visible while using the hover menu or chat;usePersistentHighlighthook - Table of Contents panel:
GET /api/pdfs/{id}/tocendpoint; side panel with search filter; ☰ toolbar button always visible once loaded
- Strategy 1: PyMuPDF built-in outline (
doc.get_toc()) — used when present, returnsmode: "native" - Strategy 2: Two-pass font-size heuristic across ALL pages — size-aware granularity:
- ≤ 150 pages →
mode: "fine"— detects H1 (≥1.6×) + H2 (≥1.25×), levels preserved. Good for articles, short reports, papers. - > 150 pages →
mode: "chapter"— H1 only (≥1.6×). Good for textbooks, encyclopedias, references. - Last-occurrence wins: actual content page beats any front-matter printed ToC page listing the same title
- No item cap — large references get full entry coverage
- ≤ 150 pages →
- Response shape:
{items, generated, mode}— frontend usesmodefor banner/tooltip text - ToC prompt banner above page 1 when no native outline; "Yes, show me" / "No thanks"
- Search filter in panel (critical for encyclopedias with hundreds of entries)
- H2 entries visually lighter + indented vs H1
GET /health/detailed— checks Postgres, ChromaDB, S3 connectivity + API key presence; paste response to Claude to diagnose service issuesGET /api/pdfs/{id}/toc/debug— font-size stats, threshold, first/last 10 candidates; paste to Claude to diagnose ToC issuesErrorBoundarywraps entire React app — blank-page crashes now show error message + "Copy error for Claude" buttonstart.sh/status.sh/stop.sh— one-command session management with PID tracking and log files
Chat service overhaul (chat_service.py):
RELEVANCE_THRESHOLDraised0.75 → 1.3: old threshold was too strict — high-level questions ("what is this book about") embed generically and all chunks scored > 0.75, causing Tavily to fire for document questionspdf_titlenow passed from router → chat service for every request; used in all system prompts and web search queries_is_document_question()guard: questions that are clearly about the document itself (overview, scope, structure) are forced to the document path — never routed to web search regardless of chunk distance- Enriched Tavily query: when web search genuinely fires, query becomes
"{pdf_title}: {user_message}"instead of raw message — prevents off-topic results (e.g. searching "what is the book all about" now searches "Gale's Encyclopedia of Medicine: what is the book all about") - Three research-oriented system prompt templates:
_document_prompt— WHAT + WHY + HOW, page citations, markdown formatting, synthesis across excerpts_selection_only_prompt— deep explanation of highlighted passage: meaning, significance, technical terms, field connections_web_fallback_prompt— citation-focused, filters irrelevant results, keeps PDF title in context
max_tokensraised1024 → 1536for richer responses
Markdown rendering in chat (ChatPanel.jsx):
- Assistant messages now rendered with
react-markdown(v10) — bold, lists, code blocks, headers all render correctly classNameprop removed (deprecated in react-markdown v10); wrapped in a plain<div className="message-bubble">instead- User messages stay as plain text with
pre-wrap - CSS added for scoped markdown styles inside
.message-assistant .message-bubble
Automated test suite (backend/tests/):
conftest.py— session fixtures:client(httpx),uploaded_pdf_id(uploads real test PDF, deletes after session)test_health.py—/health,/health/detailed, per-service connectivity, API key presencetest_pdfs.py— list schema, upload (wrong type → 400), URL, stream, TOC shape, debug endpoint, 404stest_chat.py— basic response, with selection, with history, sources schema, 404 on bad idrequirements-dev.txt—pytest==8.2.2,pytest-asyncio==0.23.7
diagnose.sh — comprehensive diagnostic script:
- 5-section check: port listening → health endpoints → PDF API smoke tests → env/.env/conda/node checks → log error scan
--fixflag: auto-restarts failed services (postgres brew, chroma, backend, frontend)--testsflag: installs pytest if needed, runs full suite against live server- Reports pass/fail counts and fix commands; exits non-zero if any check fails
- LLM-as-judge + deterministic structure checks + Playwright UI tests
- Playwright MCP wired to Claude settings for in-session browser access
- Response Intelligence System (6-category / 5-layer template) baked into
chat_service.pysystem prompts
Part 1 — Deep Section Index
get_page_heading_path(): two-phase visual-order scan, 8% header bands, ALL_CAPS + bold L3 rules, forward-order ancestor chainresolve_chunk_for_highlight(): stable chunk ID from ChromaDBPOST /api/pdfs/{id}/resolve-chunkendpoint; frontendresolveChunk()API- Every saved highlight carries
chunkId+deepSectionPath;groupByHierarchyuses deepSectionPath with L3→H2 fallback
Part 2 — Concept Tagging
- Haiku extracts 2–4 lowercase noun-phrase tags per save (parallel with resolveChunk)
- Stored on
highlight_entries.concepts JSONB - By Concept view with chip-bar filter; inline concept tags in By Page view
Part 3 — Curation States + Related Passages
- Flag/Anchor/Reviewed toggles on every entry (ORM + Zustand + API)
- Lazy-loaded related passages via ChromaDB embedding similarity (3 similar chunks, click to jump)
- Curated tab with Stars/Anchored/Flagged sub-filters
Lens Fixes (prerequisite)
- react-pdf v9 inline arrow on
onRenderTextLayerSuccesschanged reference every render → TextLayer rebuild → empty nodes. Fixed with stable per-page handler cache via refs. - Highlight lens only showed first 60 chars. Fixed
findTextRangeto usefullNorm.length. highlightTexts[]array accumulates all distinct selections from the same chunk.
Part 1 — Question Visibility
- Collapsed card = single row with inline first-sentence preview + relative timestamp. Click to expand.
firstSentence()andrelativeTime()module-level utilities.
Part 2 — Annotation Field
- Free-text marginalia on entries. Three states: ghost (dashed), editing (amber tint), saved.
setEntryNoteZustand action;note: ''on every new entry.
Part 3 — Synthesis Layer
- On-demand Haiku distillation → 2–3 sentence understanding summary per passage.
POST /api/chat/synthesize-entry;synthesis: nullon entries; spinner + regenerate button.
Part 4 — Chip Density
ConceptChipsinner component (MAX_VISIBLE_CHIPS=5, overflow+N/−toggle, navigable prop).- Concepts tab chip bar with BAR_LIMIT=12 toggle.
DB schema (8 tables):
pdf_documents,highlight_entries,qa_pairs(FSRS state inline: stability, difficulty, step, state, due_at)review_log(28 cols, thesis-critical — all grading + FSRS transition data logged per review)rubric_versions,prompt_versions(versioned + freezable for pre-registered study)session_events(home for visual_chat + feynman events — NOT review_log)human_grades(IRR sub-study storage)
FSRS v6.3.1 integration:
Scheduler,Card,Rating,State(noFSRSclass — v6 API)reconstruct_card()/persist_card_to_qa()helpers- DB state → fsrs.State: 'new'→Learning/step=0, 'learning'→Learning/step=1, 'review'→Review, 'relearning'→Relearning
- Claude score → FSRS rating: 1→Again, 2→Hard, 3→Good, 4–5→Easy (both stored per Research A3)
- Default
Scheduler()weights frozen for pre-registered study period
3D analytical rubric (grading_service.py):
core_claim,supporting_detail,faithfulnesseach 1–5; overall = round(mean)temperature=0, model pinned to"claude-sonnet-4-20250514"- RAG-grounded: source chunks fetched from ChromaDB and passed to Claude (Research C6)
- JSON output: scores + rationales + rubric_hits + missing + confidence + feedback
- Default rubric
v1.0seeded on startup; versioned viarubric_versionstable
Review Session UI (ReviewSession.jsx):
- Full-screen overlay (no react-router),
reviewModeboolean in Zustand - Phases: loading → recall → graded → done → empty
- Confidence rating (1–5) + recall textarea submitted in ONE call before grade revealed (Research D2)
- Graded overlay: overall score (color-coded), 3 DimRow scores, feedback, rubric_hits/missing chips, correct answer, next review date
cardShownAtref →recall_latency_mscomputed at submit time- Handles
reviewScope.cards(single-card) andreviewScope.pdfId/null (due queue)
API endpoints (routers/review.py):
POST /api/review/submit— single call: confidence + recall → grade + FSRS update → review_log rowGET /api/review/due— global due queue (all PDFs)GET /api/pdfs/{pdf_id}/review/due— PDF-scoped due queue (Quiz Me)GET /api/qa/{qa_id}/review-data— single card data (per-card ▶ button)GET /api/review/stats— {total, due_now, new}DueCardResponseincludessource_passage(full ChromaDB chunk text) +highlight_text(stored selection)
Research export (routers/research.py):
GET /api/research/export/review-log— 28-col CSV/JSON thesis primary datasetGET /api/research/export/irr-sample— blinded rows for human grader (no Claude scores)POST /api/research/irr/sample— randomly flag target_n rows for IRR sub-study
Zustand migration:
- All actions async, API-first with optimistic updates + rollback
selectPdfasync — fetches highlights on PDF opennormalizeEntry(row, pdfTitle)/normalizeQA(row)— DB→UI field mappingopenReview(scope)where scope: null | {pdfId} | {cards: [...]}
- ReviewSession question display: Action Q&A detection (
resolveDisplayQuestion()). Quiz Me: extracts**Question:**block from answer; falls back to "What do you recall about this passage?" badge. Other actions (Explain, Simplify, etc.): show action badge + generic prompt. Manual Q&As: unchanged. - Source passage in review:
DueCardResponse.source_passage— full ChromaDB chunk text fetched in_qa_to_due_response(); ReviewSession prefers this over rawhighlight_text. - "Highlight" label: Added
idx-entry-source-labelabove passage text buttons in By Page and By Section views — visually separates source zone from Q&A list. - Concept chip cap:
MAX_VISIBLE_CHIPSraised 3→5. - Quiz Me structured output: SelectionMenu prompt now requests
**Question:** / **Answer:**format so future saves are reviewable. - Test data cleanup: Patched Entry 1
highlight_texts(had "original selection" from dev testing).
- Full upload → embed → chat pipeline
- Voice input (toggle, 1.5s min, Whisper)
- Text selection hover menu with all actions
- Highlight Index (By Page / By Section / By Concept / Curated views)
- Deep section index (chunk IDs, deepSectionPath, concept tags, curation states, related passages)
- Annotation field + synthesis layer on entries
- PDF scrolling with page tracking, editable page number
- Navigable ToC (native or auto-generated, size-aware)
- Persistent text highlight during menu interaction
- Tavily web search fallback (document-context guard + enriched queries)
- Error boundary + debug endpoints
- Markdown rendering in assistant chat messages
- Automated backend test suite (pytest, live server)
diagnose.shwith--fixand--tests- Full DB persistence — all highlight entries and Q&A pairs stored in PostgreSQL
- FSRS v6.3.1 scheduling — inline on
qa_pairs; Claude score → FSRS rating - 3D analytical rubric grading — core_claim / supporting_detail / faithfulness; RAG-grounded
- Review Session UI — full-screen overlay, 3-phase per-card, confidence-before-grade (Research D2)
- Quiz Me upgrade — per-card ▶ button, PDF-scoped + global due queues, live due badge
- Thesis instrumentation — review_log (28 cols), CSV/JSON export, IRR sampling endpoint
- Action Q&A detection in review — Quiz Me cards show extracted question; others show generic prompt
TIER 1B — Demo-skeleton features (do after Tier 1A is solid):
- Learning graph — D3 force graph, nodes per Q&A colored by FSRS stability, edges by cosine sim (threshold 0.75, top-K=3). Budget: 5–7 days. Cluster tags from
deepSectionPath[0].title(free, no schema change needed). - Visual intelligence — 2x DPR canvas drag-select, "Ask about this" → Claude vision. Budget: 4–6 days. Verify multimodal API on chosen model early — highest-risk demo-skeleton feature.
- Feynman mode — fixed 4-turn,
session_eventslogging (session_type='feynman'). Budget: 3–5 days.
TIER 1.5: 4. Retention dashboard — items due today, R = e^(-elapsed/stability) per doc, study streak.
TIER 2 (post-thesis): 5. Pre-reading question priming, cross-document synthesis, full-vision features.
Research compliance (parallel with build):
- IRB approval — hard blocker on human-subjects data collection; start 6+ weeks before planned data collection
- OSF pre-registration recommended before main study
- Rubric/model freeze before data collection begins (Research C4, C5)
backend/
app/
main.py FastAPI app, lifespan (create_all + seed rubric), /health, /health/detailed
core/config.py Pydantic Settings (env vars)
models/
pdf.py PDFDocument ORM
highlight.py HighlightEntry + QAPair (FSRS state inline: stability, difficulty, step, state, due_at, source_chunk_ids JSONB)
review.py ReviewLog (28 cols), RubricVersion, PromptVersion, SessionEvent, HumanGrade
routers/
pdfs.py Upload, list, stream, TOC, TOC debug, resolve-chunk, delete
chat.py RAG chat + synthesize-entry endpoint
voice.py Whisper transcription
highlights.py CRUD: highlights (7 endpoints), qa_pairs
review.py submit, due (global + PDF-scoped), review-data, stats
research.py export/review-log (CSV/JSON), export/irr-sample, irr/sample
services/
pdf_service.py extract_pages(), chunk_pages(), get_page_count()
chroma_service.py upsert_chunks(), query_chunks(), delete_pdf_chunks(), get_collection()
embedding_service.py embed() via sentence-transformers
chat_service.py 3 prompt templates, _is_document_question(), Tavily fallback, Response Intelligence System
toc_service.py get_toc() — native outline or size-aware font heuristic
s3_service.py upload, get_presigned_url, get_file_bytes
grading_service.py 3D rubric, run_grading(), FSRS helpers, ensure_default_rubric_version()
section_service.py get_page_heading_path(), resolve_chunk_for_highlight()
frontend/src/
store/index.js Zustand: async API-first, optimistic updates + rollback, openReview(scope)
api/
client.js Axios base (baseURL: /api)
pdfs.js listPdfs, uploadPdf, deletePdf, getPdfFileUrl, getToc, resolveChunk, getRelatedChunks, synthesizeEntry, extractConcepts
chat.js sendMessage(...)
highlights.js fetchHighlights, postHighlight, patchHighlight, deleteHighlight, postQA, patchQA, deleteQA
review.js fetchDueCards(pdfId?), submitReview, fetchReviewStats, fetchCardReviewData
components/
PDFViewer.jsx Viewer, scrolling, ToC panel, selection detection
SelectionMenu.jsx Hover menu (8 actions); Quiz Me prompt requests **Question:**/**Answer:** format
ChatPanel.jsx Chat + Index tab switcher, logPrompt, voice, react-markdown, saveToIndex
HighlightIndex.jsx By Page / By Section / By Concept / Curated; QACard, ConceptChips (MAX=5),
RelatedPassages, AnnotationField, SynthesisSection, CurationBar, review bar
ReviewSession.jsx Full-screen overlay; 3-phase recall→graded→done; resolveDisplayQuestion()
extracts quiz Q from answer; action badge + generic prompt fallback
PDFSidebar.jsx PDF library sidebar
ErrorBoundary.jsx Blank-page crash catcher
hooks/
useVoiceRecorder.js IDLE→REQUESTING→RECORDING→TRANSCRIBING→ERROR state machine
usePersistentHighlight.js CSS Custom Highlight API wrapper
utils/
indexMatch.js findRelatedEntries() — fuzzy word-overlap + page bonus
backend/tests/ pytest suite (conftest, test_health, test_pdfs, test_chat)
backend/requirements-dev.txt pytest + pytest-asyncio
research/ Thesis research folder (CLAUDE.md routes all task types to specific files)
diagnose.sh 5-section system check; --fix restarts services; --tests runs pytest
| Decision | Rationale |
|---|---|
| Selected text injected into system prompt directly (not via RAG) | RAG couldn't find the exact passage; direct injection guarantees Claude sees it |
onMouseDown prevention on all menu buttons |
Prevents focus-steal which clears the browser selection |
| CSS Custom Highlight API for persistent highlight | Only way to keep a visual highlight after selection is cleared; degrades gracefully |
pendingScrollPage + no-deps useEffect for navigation |
React renders pages async; effect retries each cycle until target page mounts |
ratioMap persisting across IntersectionObserver callbacks |
Observer fires per-changed-entry, not all-entries; map gives true most-visible page |
| Last-occurrence wins in TOC deduplication | Front-matter printed ToC lists headings before content; content page is the right target |
| No item cap on generated TOC | Encyclopedias/references have hundreds of H1 entries; cap was cutting off at B |
| Size-aware TOC granularity (≤150 = fine, >150 = chapter) | Short docs benefit from subheading detail; long references need chapter-level only |
| Single exported component for HighlightIndex | Vite Fast Refresh blanks page with module-level hook-using sub-components |
| Conda Python 3.12 (not 3.13) | psycopg2-binary, tiktoken, pydantic-core incompatible with 3.13 at project start |
ChromaDB _client = None (no type annotation) |
chromadb.HttpClient is a factory function, not a class; union type annotation crashed |
| Tavily fallback threshold: cosine distance 1.3 (was 0.75) | 0.75 was too strict — generic questions ("what is this book about") embed generically; all chunks scored > 0.75 causing Tavily to fire inappropriately. 1.3 ≈ essentially random noise. |
_is_document_question() guard in chat_service |
Document-level questions ("what is this book about", "what topics does it cover") must never go to web search regardless of chunk distance. Pattern-matched and forced to document path. |
| Tavily query enriched with PDF title | Raw message sent to Tavily returned garbage ("what is the book all about" → random "All About" books). Query is now "{pdf_title}: {message}". |
| Three chat system prompt templates | _document_prompt (RAG path), _selection_only_prompt (highlight with no strong chunks), _web_fallback_prompt. Each oriented toward research/learning: explain WHY + HOW, cite pages, use markdown. |
pdf_title passed router → chat service |
Chat service had no knowledge of which document was open. Now threaded through for prompts, web search enrichment, and document-question detection. |
Voice: tuple ("recording.webm", bytes, "audio/webm") to OpenAI SDK |
BytesIO approach didn't work; tuple form is what the SDK expects for multipart |
FSRS state inline on qa_pairs (not separate memory_items table) |
Eliminates a join on every review; simpler migration; FSRS fields are 1:1 with the Q&A (Research B1) |
FSRS v6.3.1 uses Scheduler, not FSRS class |
v6 rewrote the API; FSRS() doesn't exist. Use Scheduler(), scheduler.review_card(card, rating) → (Card, ReviewLog) |
| Claude score → FSRS rating, NOT user confidence | This mapping IS the thesis contribution — replacing self-rated SRS signals with LLM-graded generative recall (Research A3) |
temperature=0 + pinned model string for grading |
Reproducibility requirement for a pre-registered study — must be logged on every call (Research C5) |
| RAG-grounded grading: source chunks passed to Claude | Prevents Claude from grading against its priors rather than the document (Research C6) |
review_log stores both raw Claude score AND derived FSRS rating |
Allows post-hoc revision of the score→rating mapping without re-running grading |
| confidence_rating + recall_text in ONE submit call | If user sees grade first before rating confidence, calibration measurement is contaminated (Research D2, Cardinal Rule 2) |
session_events for visual_chat + feynman (NOT review_log) |
Keeps thesis primary data clean — review_log is for the empirical-core graded recall only (Research K2, L2) |
rubric_versions table with frozen_at timestamp |
Rubric must be frozen before main study data collection; create new version rather than mutating frozen one (Research C4) |
source_passage in DueCardResponse (ChromaDB full chunk) |
highlight_text (user's raw selection) may have PDF extraction artifacts; ChromaDB chunk gives clean full context for review display |
resolveDisplayQuestion() in ReviewSession |
Quiz Me stores the raw prompt as qa.question; Claude's actual Q is in qa.answer; must extract at review time to avoid showing the raw prompt |
deepSectionPath[0].title as cluster tag for knowledge graph |
Already stored, free — no schema change needed for coarse concept clustering in graph view |
| URL | What it tells you |
|---|---|
GET /health/detailed |
Postgres / ChromaDB / S3 connectivity + API key presence |
GET /api/pdfs/{id}/toc/debug |
Font-size stats, threshold, candidate headings — diagnoses ToC issues |
Frontend crashes: ErrorBoundary catches blank-page crashes and shows the error + "Copy error for Claude" button.
./start.sh # start all 4 services (checks if already running)
./status.sh # see what's up/down + last log lines for anything broken
./stop.sh # graceful shutdown via saved PIDs
./diagnose.sh # full system check: ports, HTTP, API smoke tests, env, logs
./diagnose.sh --fix # same + auto-restart any failed service
./diagnose.sh --tests # same + run full pytest suiteLogs written to logs/ (gitignored). Tail live: tail -f logs/backend.log
Full manual instructions: START.md
# Backend
conda create -n pdf-workspace python=3.12
conda activate pdf-workspace
cd backend && pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
# ChromaDB (separate terminal)
chroma run --path ./chroma_data --port 8001
# Frontend
cd frontend && npm install && npm run devRequired .env in backend/:
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
TAVILY_API_KEY=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_BUCKET_NAME=
DATABASE_URL=postgresql://...
CHROMA_HOST=localhost
CHROMA_PORT=8001
- TOC heuristics: font-size based detection is best-effort. Works well for structured books; imperfect for heavily typeset reference works. Native PDF bookmarks always preferred.
- Persistence: fully built as of Phase 9. PostgreSQL is the source of truth; Zustand is a cache that reloads on PDF select + tab refocus.
- FSRS study freeze: default
Scheduler()weights are frozen for the pre-registered study period. Do NOT enable per-user FSRS parameter optimization until after data collection ends. - Action Q&A in review: Quiz Me stores raw prompt as
qa.question; Claude's answer is inqa.answer.resolveDisplayQuestion()extracts the actual question. Old cards (pre-format-instruction prompt) fall through to generic "What do you recall?" prompt — this is correct. - Source passage truncation:
highlight_textis the user's raw PDF selection, may have hyphenated line breaks.source_passage(ChromaDB full chunk) preferred in ReviewSession. - Knowledge graph (Tier 1B): all data already exists. Nodes = one per Q&A, color = FSRS stability, size = review count. Edges = cosine sim over source chunks, threshold 0.75, top-K=3. Cluster labels =
deepSectionPath[0].title(free, no schema change). Budget: 5–7 days. Build AFTER Tier 1A is solid — check Research rule 4. - Visual intelligence (Tier 1B): canvas drag-select → 2x DPR capture → Claude vision → response. Logged in
session_events(NOTreview_log). Verify multimodal API on pinned model EARLY — highest-risk demo-skeleton feature. - Image highlighting (Tier 1B): same as visual intelligence. "Ask about this" one-button approach (no multi-button toolbar). No saving as reviewable Q&A cards — extension point.
- Demo-skeleton features: Groups G (graph), K (visual), L (Feynman) have explicit IN/NOT-in-scope lists in research/04_build_decisions.md. Push back on scope creep.