// astroparse — annotated reading view: main text + marginalia const MODES = window.ASTROPARSE_MODES; const modeById = Object.fromEntries(MODES.map((m) => [m.id, m])); /* ---------- citation popover ---------- */ function ScoreBar({ label, value, max }) { const pct = Math.max(0, Math.min(1, value / max)); return (
{label} {value}
); } function CitePopover({ lit, pos, showScores, onClose }) { const ref = React.useRef(null); React.useLayoutEffect(() => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); let x = pos.x, y = pos.y + 10; if (x + r.width > window.innerWidth - 16) x = window.innerWidth - r.width - 16; if (y + r.height > window.innerHeight - 16) y = pos.y - r.height - 10; el.style.left = Math.max(8, x) + 'px'; el.style.top = Math.max(8, y) + 'px'; el.style.opacity = 1; }, [lit, pos]); React.useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); return (
retrieved · rank {lit.scores.rank}
{lit.title}
{lit.authors} ({lit.year}) · {lit.journal}
{lit.abstract}
{showScores && (
)}
ADS ↗ arXiv ↗
); } /* ---------- margin note ---------- */ function MarginNote({ para, mode, entry, generating, hovered, onHover, onSetMode, onRegenerate, onCite, top, noteRef }) { const m = modeById[mode]; const lit = (window.ASTROPARSE_DATA.litByPara[para.id] || []).map((id) => window.ASTROPARSE_DATA.litPapers[id]).filter(Boolean); return ( ); } /* ---------- reader ---------- */ function Reader({ paper, commentary, noteModes, generating, onSetMode, onRegenerate, tweaks, toolbar }) { const containerRef = React.useRef(null); const marginRef = React.useRef(null); const paraRefs = React.useRef({}); const noteRefs = React.useRef({}); const [tops, setTops] = React.useState({}); const [spacers, setSpacers] = React.useState({}); const spacersRef = React.useRef({}); const [marginH, setMarginH] = React.useState(0); const [hoverId, setHoverId] = React.useState(null); const [pop, setPop] = React.useState(null); // {lit, pos} const paras = paper.paragraphs; const recompute = React.useCallback(() => { const c = containerRef.current; if (!c) return; const cTop = c.getBoundingClientRect().top; const narrow = window.innerWidth <= 900; const applied = spacersRef.current; const NOTE_GAP = 26; // reconstruct each paragraph's natural top (as if no spacers were applied) const naturalTop = {}; let cum = 0; paras.forEach((p) => { const pe = paraRefs.current[p.id]; if (!pe) return; naturalTop[p.id] = pe.getBoundingClientRect().top - cTop - cum; cum += applied[p.id] || 0; }); // pin every note to its paragraph's top; pad the text column below any // paragraph shorter than its note so the next pair stays level too const nextTops = {}; const nextSpacers = {}; let cum2 = 0; let lastBottom = 0; paras.forEach((p, i) => { const ne = noteRefs.current[p.id]; if (naturalTop[p.id] == null || !ne) return; const top = naturalTop[p.id] + cum2; nextTops[p.id] = top; lastBottom = top + ne.offsetHeight + NOTE_GAP; const nxt = paras[i + 1]; if (!narrow && nxt && naturalTop[nxt.id] != null) { const delta = Math.max(0, Math.ceil(lastBottom - (naturalTop[nxt.id] + cum2))); if (delta > 0) nextSpacers[p.id] = delta; cum2 += delta; } }); setTops((old) => { const same = paras.every((p) => Math.abs((old[p.id] || 0) - (nextTops[p.id] || 0)) < 1); return same ? old : nextTops; }); const sameSp = paras.every((p) => Math.abs((applied[p.id] || 0) - (nextSpacers[p.id] || 0)) < 1); if (!sameSp) { spacersRef.current = nextSpacers; setSpacers(nextSpacers); } setMarginH(lastBottom); }, [paras]); React.useLayoutEffect(() => { recompute(); }); React.useEffect(() => { if (document.fonts && document.fonts.ready) document.fonts.ready.then(recompute); window.addEventListener('resize', recompute); return () => window.removeEventListener('resize', recompute); }, [recompute]); const showCite = (lit, e) => { const r = e.currentTarget.getBoundingClientRect(); setPop({ lit, pos: { x: r.left, y: r.bottom } }); }; // group paragraphs by section for headings const rows = []; let lastSection = null; paras.forEach((p) => { if (p.section !== lastSection) { rows.push({ type: 'heading', section: p.section, key: 'h-' + p.id }); lastSection = p.section; } rows.push({ type: 'para', para: p, key: p.id }); }); return (
{toolbar}
{paper.title}
{paper.authors}
arXiv:{paper.arxivId} · {paper.pages} pp · {paper.venue}
{MODES.map((m) => ( ))}
{rows.map((row) => row.type === 'heading' ? (

{row.section}

) : (

(paraRefs.current[row.para.id] = el)} className={'ap-para' + (row.para.firstOfSection ? ' is-first' : '') + (hoverId === row.para.id ? ' is-hovered' : '')} style={{ '--mode-c': 'var(--mc-' + (noteModes[row.para.id] || 'literature') + ')', marginBottom: spacers[row.para.id] ? 'calc(1.15em + ' + spacers[row.para.id] + 'px)' : undefined, }} onMouseEnter={() => setHoverId(row.para.id)} onMouseLeave={() => setHoverId(null)} data-comment-anchor={'para-' + row.para.id} > {row.para.text}

) )}
{paras.map((p) => { const mode = noteModes[p.id] || 'literature'; const key = p.id + ':' + mode; return ( (noteRefs.current[p.id] = el)} /> ); })}
annotations are machine-generated against retrieved literature — verify before citing
{pop && setPop(null)} />}
); } Object.assign(window, { Reader });