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
---
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)
```json
{
"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.