beacon / README.md
kiyer's picture
feat: Hugging Face Spaces packaging — corpus bootstrap, static serving, Dockerfile
2d066db
|
Raw
History Blame Contribute Delete
19.6 kB
metadata
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-bottom in 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]; if commentary[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:

  1. 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
  2. POST /api/index → embeds paragraphs with SPECTER2 (sentence-transformers, CPU fine at this scale) and builds a rank_bm25 index. Can be merged into /parse.
  3. 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)
  4. POST /api/annotate { paragraph, mode, lit, provider, model, key }{ text } (the proxy above)
  5. 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 wiring
  • reader.jsx — reader screen: note alignment algorithm (recompute), MarginNote, CitePopover, score bars, legend
  • pipeline.jsx — upload screen + staged pipeline animation
  • annotator.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 copy
  • tweaks-panel.jsx — prototype-only design-review scaffolding; do not port

Open Astroparse.html in a browser to interact with the reference implementation.