title: Beacon
emoji: 🕯️
colorFrom: yellow
colorTo: red
sdk: docker
app_port: 7860
pinned: false
Handoff: Astroparse — annotated astronomy paper reader
Overview
Astroparse turns an astronomy PDF into a cleanly typeset reading view with paragraph-level AI marginalia. The pipeline: PDF → text extraction (pymupdf4llm) → header/footer cleaning → paragraph segmentation → SPECTER2 embeddings + BM25 index → literature retrieval (Pathfinder / NASA ADS) → per-paragraph LLM commentary rendered as Tufte-style margin notes beside the text. Users pick a commentary mode per paragraph (historical context, referee critique, related-literature observations, plain-language explainer, methods comparison), inspect the retrieved papers behind each note (abstract, relevance scores, ADS/arXiv links), choose their LLM provider (Claude / OpenAI / Gemini) with their own API key, and save/reload the whole annotated state as a JSON session.
Primary user: a single researcher (the project owner), with intent to expand to a public web app if useful.
About the Design Files
The files in this bundle are design references created in HTML — a working prototype showing the intended look and behavior, not production code to copy directly. The task is to recreate this design in a real application environment. There is no existing codebase; the recommended stack (chosen to fit the Python-native pipeline) is:
- Frontend: React + Vite (the prototype's JSX components port almost directly; they currently run via in-browser Babel, which must not ship to production)
- Backend: Python + FastAPI (pymupdf4llm, SPECTER2, BM25, and Pathfinder/ADS access are all Python)
- Hosting: Hugging Face Spaces (Docker) for the first deployment; Fly.io/Render later if a custom domain is needed
Important: the prototype simulates parsing and retrieval (it ships one hard-coded paper, Mowla & Iyer 2024, and a curated 10-paper retrieval set in data.js). The pipeline screen plays a scripted animation. In production these become real backend calls — see "Backend pipeline specification" below. The LLM commentary generation in the prototype is REAL (it calls providers from the browser) and its prompt design should be kept.
Fidelity
High-fidelity. Colors, typography, spacing, and interactions are final design intent. Recreate the UI pixel-faithfully. The only placeholder behaviors are the simulated parsing/retrieval described above.
Design language (read first)
"Annotated encyclopedia / manuscript marginalia." A parchment folio on a darker desk; EB Garamond for all reading text; IBM Plex Mono for machine traces (pipeline logs, scores, file names, buttons); rubrication red for marks of structure (pilcrows, drop caps, rules, active states); five mode colors at matched oklch lightness/chroma. No rounded-corner cards, no gradients, no icons — typography does all the work. Buttons are underlined mono text, not pills.
Design tokens
Colors
Theme is driven by 3 CSS variables (user-switchable palettes via Tweaks):
| Token | Default ("Parchment") | Alt 1 ("Ivory") | Alt 2 ("Chalk") |
|---|---|---|---|
--paper (background) |
#f6f1e6 |
#fbf8f1 |
#f3f2ee |
--ink (text) |
#2b2418 |
#231f17 |
#22241f |
--rubric (accent) |
#9a3b20 |
#8a5a18 |
#3f5a96 |
Desk (page surround): color-mix(in oklab, var(--paper) 86%, var(--ink) 14%).
All secondary grays are mixes of ink into paper (e.g. 70% ink for secondary text, 50% for captions, 25% for hairline borders, 14% for rules, 8% for faint dividers) — never neutral hex grays, so every tint stays warm with the palette.
Mode colors (fixed, all oklch(0.52 C H)):
| Mode | Color | Glyph |
|---|---|---|
| Historical context | oklch(0.52 0.10 70) amber |
H |
| Referee mode | oklch(0.52 0.13 28) red |
R |
| From the literature | oklch(0.52 0.10 250) blue |
L |
| Plain-language | oklch(0.52 0.10 150) green |
P |
| Methods comparison | oklch(0.52 0.10 310) violet |
M |
Typography
- EB Garamond (Google Fonts, 400/500/600 + italics): all reading text, headings, wordmark
- IBM Plex Mono (400/500): logs, scores, chips, text-buttons, hints, metadata
- Body: 19px default (user-adjustable 16–23px via Tweaks), line-height 1.58, justified +
hyphens: auto(toggleable) - Paper title: 31px / 600 / line-height 1.22, centered,
text-wrap: balance - Section headings: 13px / 500 / uppercase / letter-spacing 0.18em
- Margin notes: 13.5px / line-height 1.5; note mode label: 12.5px small-caps, letter-spacing 0.08em, weight 500, in mode color
- Mono UI text: 10–12px, letter-spacing 0.04–0.14em
- Wordmark "astroparse": uppercase, letter-spacing 0.28–0.32em, 12–14px, rubric color
Spacing & chrome
- Folio: max-width 1180px, centered, padding 64px 72px 48px, 1px border (ink 18%), shadow
0 1px 2px rgba(40,30,10,0.08), 0 18px 50px -24px rgba(40,30,10,0.4) - Border radius: 0 everywhere except 2px on chips/inputs and 50% on mode dots
- Text column: flex
0 1 660px, 48px gap to margin column - Margin column: flex
1 0 300px, max-width 330px, 1px left rule (ink 14%), notes inset 28px from the rule - Paragraph spacing: 1.15em; note-to-note minimum gap: 26px
Screens / Views
1. Upload ("Place a manuscript here")
Centered column on the desk background: wordmark, then a folio drop-zone card (min(620px, 92vw); padding 58px 48px 44px) with a double border — 1px solid outer (ink 18%) plus 1px dashed inset outline (outline-offset: -10px, ink 30%; turns rubric on hover/drag, card scales 1.01 on drag-over). Contents, centered: a 58px rubric pilcrow ¶; "Place a manuscript here" (26px / 500); italic subline "drop an astronomy PDF, or click to browse" (16px, ink 70%); mono caption "parsed with pymupdf4llm · annotated against the literature via Pathfinder" (10.5px, ink 52%, 26px above-gap).
Below the card: two mono text-buttons — "open ‹last/sample manuscript›" and "reload a saved session (.json)" — separated by "·". Bottom mono caption explains prototype limitations (drop in production).
Behavior: click anywhere on card opens file picker (accept application/pdf); drag-and-drop accepted; choosing a .json via the second button restores a session directly.
2. Pipeline ("in preparation")
Same desk + wordmark. A folio card (min(600px, 92vw), padding 30px 38px 24px). Header row: italic filename (17px) left, "IN PREPARATION" mono uppercase rubric right; hairline rule below. Then six stages as a list, each separated by faint rules (ink 8%):
- Row: roman numeral (mono 11px, rubric, right-aligned 26px col) · stage name (17px / 500) · tool name (mono 10.5px, ink 50%, right) · status mark (✓ done / … active, rubric)
- Pending stages at 38% opacity; active stage name in rubric
- Under active/done stages: mono log lines (11px, ink 58%, indented 36px) appear one-by-one (~420ms apart, fade+2px rise 300ms)
Stages (names are part of the design voice): i. Reading manuscript (pymupdf4llm) · ii. Cleaning the leaves (header/footer removal) · iii. Ruling the columns (paragraph segmentation) · iv. Committing to memory (SPECTER2 embeddings) · v. Indexing the lexicon (BM25) · vi. Consulting the library (Pathfinder). Log lines report real counts from the backend in production. Footer: right-aligned "skip to the manuscript →" text-button. Auto-advance ~1.1s after the last stage. In production, drive stage transitions from real pipeline progress (SSE or polling).
3. Reader (the core screen)
Toolbar (sticky, backdrop-blur paper at 88% opacity, hairline bottom border, padding 13px 28px): wordmark · filename (mono 10.5px ink 50%) · spacer · text-buttons: "annotator: ‹provider›", "save session ↓", "load", "new manuscript".
Folio (margin 36px auto 70px): centered paper header — title, italic author line (16px ink 75%), mono metadata line "arXiv:2402.08696 · 20 pp · Nature 636, 332–338 (2024)" (10.5px ink 50%).
Mode legend: a centered row between hairline rules; five items (8px color dot + small-caps label, 13px, ink 70%, hover → mode color). Clicking a legend item sets every note to that mode (tooltip: "set every note to …").
Two-column body: text column + margin column (rule between). Notes can flip to the left side via Tweaks (flex-direction: row-reverse; rule flips side).
Text column:
- Section headings as specced above
- Paragraphs with a rubric pilcrow ¶ hanging in the left gutter (absolute,
left: -1.5em, 0.85em, 55% opacity) - First paragraph of each section gets a drop cap (
::first-letter: float left, 3.05em, line-height 0.82, weight 500, rubric) when the Tweak is on - Hovering a paragraph tints it AND its note with the note's mode color at 7% (
color-mix(... 7%, transparent)), and vice versa — this cross-highlight is the key affordance linking the columns
Margin column — each paragraph has exactly one note, absolutely positioned:
- Alignment rule (critical): every note's top is pinned EXACTLY level with its paragraph's top. If a paragraph is shorter than its note, the text column adds extra space below that paragraph (extra
margin-bottomin px) so the next paragraph/note pair is also level. Algorithm: measure natural paragraph tops (subtracting previously applied spacers), walk pairs in order,spacer[i] = max(0, noteTop[i] + noteHeight[i] + 26 − nextParagraphNaturalTop). Re-run on resize, font load, and any note content/mode change; must be idempotent (no oscillation). Disable spacers and absolute positioning below 900px viewport (notes become static blocks under their paragraphs). - Note anatomy, top to bottom: 1.5px top rule in mode color; header row — small-caps mode label in mode color + "✦" marker if LLM-generated + "↻" regenerate button (hidden until note hover; ink 40% → rubric on hover); body text; citation chips; mode dots.
- Citation chips: mono 10px,
[n] AuthorYY(rank number in mode color), 1px border (ink 22%), 2px radius, padding 2px 7px 2px 5px; hover → mode color border/text. Click opens the citation popover. - Mode dots: five 19px circles, mono 9px glyph letters (H R L P M); inactive: 1px border ink 25%, text ink 50%; hover: mode color; active: filled with mode color, paper-colored glyph. Clicking switches that note's mode; if no commentary exists yet for (paragraph, mode), generation starts.
- Loading state: italic "consulting the literature…" with an animated ellipsis (steps(4), 1.2s)
- Error state: italic message (include the provider error, e.g. "no API key set — open the annotator settings") + "try again" text-button
Folio footer (rule + centered mono 10px, ink 45%): "annotations are machine-generated against retrieved literature — verify before citing". Keep this disclaimer.
4. Citation popover
Fixed-position card (360px; paper bg; 1px border ink 30%; shadow 0 16px 44px -16px rgba(40,30,10,0.5); padding 18px 20px 14px) anchored under the clicked chip, clamped to viewport (flips above if needed); invisible full-screen backdrop click-to-close; Esc closes; × button top-right.
Contents: mono overline "RETRIEVED · RANK n" (9.5px, rubric, letter-spacing 0.1em) · title (16.5px / 600) · italic authors+year+journal (13px, ink 65%) · abstract (13.5px / 1.5) · score bars (when "Retrieval scores" tweak is on): rows of label | track | value — mono 10px, 3px track (ink 12%) with rubric fill; "specter" normalized /1.0, "bm25" /20 · footer links "ADS ↗" and "arXiv ↗" (mono 11px rubric) → https://ui.adsabs.harvard.edu/abs/{bibcode}/abstract and https://arxiv.org/abs/{arxiv}, new tab.
5. Annotator settings
Fixed panel, top-right (top 64px, right 24px, width 360px; same chrome as popover). Title "The Annotator" (19px / 600), italic subtitle "which mind writes in your margins". Fields:
- Provider: four toggle chips (mono 10.5px, 2px radius) — Built-in (Claude) / Anthropic / OpenAI / Gemini; active chip filled rubric. (In production, drop "Built-in" unless you ship a server-funded default.) Switching provider resets the model field to that provider's default:
claude-haiku-4-5/gpt-4o-mini/gemini-2.0-flash. - Model: mono text input (12px; 1px border ink 22%; focus border rubric; bg ink 4%).
- API key (hidden for built-in): password input + mono hint "‹console URL› · stored only in this browser, sent only to ‹provider›".
- Footer rule + right-aligned "done". Field labels: 11px / 500 / uppercase / letter-spacing 0.14em / ink 70%.
6. Tweaks panel (prototype-only)
The prototype has a design-review Tweaks panel (palette, body size, drop caps, justification, notes side, score visibility). In production, promote notes side, body size, justification, and palette to a small user-facing "display" preferences menu; persist with the session/app preferences.
Interactions & Behavior
- Upload → pipeline → reader is a linear state machine (
screen: 'upload' | 'pipeline' | 'reader'). - Mode switch (dot click): updates
noteModes[paraId]; ifcommentary[paraId+':'+mode]is missing, fire generation (show loading state). Concurrent requests for the same key must be deduped; multiple different notes may generate in parallel. - Regenerate (↻): always re-fires generation for the current (paragraph, mode), overwriting.
- Legend click: sets all paragraphs' modes (generating any missing ones).
- Hover cross-highlight paragraph ↔ note (7% mode-color tint both sides).
- Toast: fixed bottom-center, ink bg / paper text, mono 11px, padding 9px 18px, ~2.6s, for session events ("session written to …", "session restored", "could not read that session file").
- No animations on note re-positioning (snap, don't tween — tweening reads as drift).
- Responsive: below 900px, single column; notes render as static blocks beneath their paragraphs; folio padding 40px 26px.
State Management
screen 'upload' | 'pipeline' | 'reader'
fileName string
paper { title, authors, venue, arxivId, pages, paragraphs: [{ id, section, firstOfSection?, text }] }
litPapers { [litId]: { id, short, title, authors, year, journal, bibcode, arxiv, abstract,
scores: { specter, bm25, rank } } }
litByPara { [paraId]: litId[] } // top-3, ranked
noteModes { [paraId]: modeId } // per-paragraph current mode
commentary { [`${paraId}:${modeId}`]: { text, cites: litId[], source: 'canned'|'llm',
error?, message? } }
generating { [`${paraId}:${modeId}`]: true } // in-flight
annotator { provider: 'anthropic'|'openai'|'gemini', model: string, key: string }
Autosave the session to localStorage on every commentary/mode change; restore on load. Annotator config persists separately (localStorage, key never leaves the browser in the prototype — see security note below).
Session JSON schema (export/import — keep stable)
{
"version": 1,
"savedAt": "ISO-8601",
"fileName": "string",
"screen": "reader",
"noteModes": { "p1": "explainer", ... },
"commentary": { "p1:explainer": { "text": "...", "cites": ["mowla22"], "source": "llm" }, ... },
"paper": { ...full paper object... }
}
Production additions: a content hash of the source PDF (to re-anchor sessions), and litPapers/litByPara (the prototype omits them from import since they're static). Never serialize the API key into sessions.
LLM commentary generation
Prompt template (keep; it produces good notes):
You are an erudite astronomical annotator writing a marginal note beside a paragraph of a
research paper, in the tradition of an annotated encyclopedia. Scholarly, precise, a little
wry. Write 55–85 words, a single paragraph, no markdown, no preamble. Refer to the retrieved
works by bracketed number, e.g. [1], where relevant.
{MODE_INSTRUCTION}
PARAGRAPH (from section "{section}"):
{paragraph text}
RETRIEVED LITERATURE (via Pathfinder):
[1] {short} — "{title}" ({journal}). {abstract}
[2] ...
Marginal note:
Mode instructions (verbatim in app.jsx): history = how the claim emerged, which earlier works set it up; referee = strongest assumption / unstated caveat / a check to report, specific not generic; literature = how retrieved works support/contradict/extend, compare numbers; explainer = plain language for a beginning grad student, unpack jargon, don't dumb down; methods = contrast technique with retrieved works' approaches, note trade-offs.
Provider routing (see annotator.jsx for working request shapes): Anthropic POST /v1/messages (max_tokens 400), OpenAI POST /v1/chat/completions, Gemini POST /v1beta/models/{model}:generateContent. Surface provider errors verbatim-but-truncated in the note error state.
Security note: the prototype calls provider APIs directly from the browser with the user's key (acceptable for a personal tool). For the public app, proxy LLM calls through the backend: accept the user's key per-request over HTTPS, hold it in memory only, never log or persist it. Cache completions server-side by hash(paragraph + mode + retrievedSetIds + model).
Backend pipeline specification (replaces the simulation)
FastAPI service; suggested endpoints:
POST /api/parse(multipart PDF) →{ paper }.pymupdf4llm.to_markdown()for extraction- Cleaning: drop lines repeating across ≥ half the pages (running headers/footers), page numbers, arXiv/DOI stamps; repair hyphenated line breaks; set aside figure/table captions and the references section
- Segment into paragraphs (min length ~200 chars; merge continuation fragments across page breaks); detect section headings; mark
firstOfSection
POST /api/index→ embeds paragraphs with SPECTER2 (sentence-transformers, CPU fine at this scale) and builds arank_bm25index. Can be merged into /parse.POST /api/retrieve→ per-paragraph top-3 papers{ litByPara, litPapers }.- Pathfinder (the authors' astronomy literature retrieval tool) or equivalent hybrid search over NASA ADS: SPECTER cosine + BM25, normalized as in the popover's score display. Requires an ADS API token (server-side secret)
POST /api/annotate{ paragraph, mode, lit, provider, model, key }→{ text }(the proxy above)- Progress for the pipeline screen via SSE or a polled job-status endpoint, mapping 1:1 to the six stage rows (with real counts in the log lines).
Assets
No images or icon fonts. Everything is typography: ¶ (U+00B6), ✦ (U+2726), ↻ (U+21BB), ✓, ↗, →, ×. Fonts from Google Fonts: EB Garamond, IBM Plex Mono. The bundled manuscript content in data.js (Mowla & Iyer 2024 + 10 retrieval papers with real bibcodes/arXiv IDs) is useful as a test fixture for the real pipeline.
Files
Astroparse.html— shell; ALL CSS lives here (the canonical style reference)app.jsx— root state machine, session save/load, generation orchestration, prompt + mode instructions, Tweaks wiringreader.jsx— reader screen: note alignment algorithm (recompute), MarginNote, CitePopover, score bars, legendpipeline.jsx— upload screen + staged pipeline animationannotator.jsx— provider settings panel +apComplete()multi-provider router (working request shapes for all three APIs)data.js— data model exemplar: modes, paper, litPapers, litByPara, defaultModes, canned commentary, pipeline stage copytweaks-panel.jsx— prototype-only design-review scaffolding; do not port
Open Astroparse.html in a browser to interact with the reference implementation.