// astroparse — root app: screens, session persistence, LLM generation, tweaks const AP_SESSION_KEY = 'astroparse_session_v2'; const AP_TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "paperPalette": ["#f6f1e6", "#2b2418", "#9a3b20"], "bodySize": 19, "dropCaps": true, "justify": true, "showScores": true, "notesSide": "right" }/*EDITMODE-END*/; const AP_MODE_INSTRUCTIONS = { history: 'Provide HISTORICAL CONTEXT: how this claim or approach emerged in the field, which earlier works set it up, and how thinking shifted over time.', referee: 'Act as a constructive but rigorous REFEREE: identify the strongest assumption, an unstated caveat, or a check the authors should report. Be specific, not generic.', literature: 'Offer OBSERVATIONS FROM THE RELATED LITERATURE: how the retrieved works support, contradict, or extend what this paragraph claims. Compare numbers where possible.', explainer: 'Write a PLAIN-LANGUAGE EXPLAINER for a beginning graduate student: unpack jargon and explain why this paragraph matters, without dumbing down.', methods: 'Provide a METHODS COMPARISON: contrast the technique in this paragraph with the approaches used in the retrieved works, noting trade-offs.', }; function buildPrompt(para, modeId, litList) { const litBlock = litList .map((l, i) => `[${i + 1}] ${l.short} — "${l.title}" (${l.journal}). ${l.abstract}`) .join('\n'); return ( '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.\n\n' + AP_MODE_INSTRUCTIONS[modeId] + '\n\n' + 'PARAGRAPH (from section "' + para.section + '"):\n' + para.text + '\n\n' + 'RETRIEVED LITERATURE (via Pathfinder):\n' + litBlock + '\n\n' + 'Marginal note:' ); } function App() { const data = window.ASTROPARSE_DATA; const [t, setTweak] = useTweaks(AP_TWEAK_DEFAULTS); const [screen, setScreen] = React.useState('upload'); // upload | pipeline | reader const [fileName, setFileName] = React.useState('Mowla_Iyer_2024_FireflySparkle.pdf'); const [noteModes, setNoteModes] = React.useState({ ...data.defaultModes }); const [commentary, setCommentary] = React.useState(() => { const init = {}; Object.entries(data.cannedCommentary).forEach(([k, v]) => { init[k] = { ...v, source: 'canned' }; }); return init; }); const [generating, setGenerating] = React.useState({}); const [toast, setToast] = React.useState(null); const [annotator, setAnnotator] = React.useState(apLoadAnnotator); const [settingsOpen, setSettingsOpen] = React.useState(false); const annotatorRef = React.useRef(annotator); annotatorRef.current = annotator; const inflight = React.useRef(new Set()); /* ---------- restore session from localStorage ---------- */ React.useEffect(() => { try { const raw = localStorage.getItem(AP_SESSION_KEY); if (raw) { const s = JSON.parse(raw); if (s && s.commentary && s.screen === 'reader') { setCommentary(s.commentary); setNoteModes(s.noteModes || { ...data.defaultModes }); setFileName(s.fileName || 'Mowla_Iyer_2024_FireflySparkle.pdf'); setScreen('reader'); } } } catch (e) { /* ignore corrupt session */ } }, []); /* ---------- autosave ---------- */ React.useEffect(() => { if (screen !== 'reader') return; const s = { version: 1, savedAt: new Date().toISOString(), fileName, screen, noteModes, commentary }; try { localStorage.setItem(AP_SESSION_KEY, JSON.stringify(s)); } catch (e) {} }, [screen, fileName, noteModes, commentary]); /* ---------- LLM generation ---------- */ const generate = React.useCallback(async (paraId, modeId, force) => { const key = paraId + ':' + modeId; if (inflight.current.has(key)) return; inflight.current.add(key); setGenerating((g) => ({ ...g, [key]: true })); const para = data.paper.paragraphs.find((p) => p.id === paraId); const litIds = data.litByPara[paraId] || []; const litList = litIds.map((id) => data.litPapers[id]).filter(Boolean); try { const text = await apComplete(annotatorRef.current, buildPrompt(para, modeId, litList)); setCommentary((c) => ({ ...c, [key]: { text: String(text).trim(), cites: litIds, source: 'llm' } })); } catch (e) { setCommentary((c) => ({ ...c, [key]: { error: true, message: e && e.message, cites: litIds } })); } finally { inflight.current.delete(key); setGenerating((g) => { const n = { ...g }; delete n[key]; return n; }); } }, []); const setMode = React.useCallback((paraId, modeId) => { setNoteModes((m) => (m[paraId] === modeId ? m : { ...m, [paraId]: modeId })); setCommentary((c) => { const key = paraId + ':' + modeId; if (!c[key]) generate(paraId, modeId); return c; }); }, [generate]); const regenerate = React.useCallback((paraId, modeId) => generate(paraId, modeId, true), [generate]); /* ---------- session export / import ---------- */ const exportSession = () => { const s = { version: 1, savedAt: new Date().toISOString(), fileName, screen: 'reader', noteModes, commentary, paper: data.paper }; const blob = new Blob([JSON.stringify(s, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'astroparse-session.json'; a.click(); URL.revokeObjectURL(a.href); flash('session written to astroparse-session.json'); }; const importSession = (text) => { try { const s = JSON.parse(text); if (!s || !s.commentary) throw new Error('bad session'); setCommentary(s.commentary); setNoteModes(s.noteModes || { ...data.defaultModes }); setFileName(s.fileName || 'restored-session.pdf'); setScreen('reader'); flash('session restored'); } catch (e) { flash('could not read that session file'); } }; const flash = (msg) => { setToast(msg); setTimeout(() => setToast(null), 2600); }; const reset = () => { setScreen('upload'); setNoteModes({ ...data.defaultModes }); setCommentary(() => { const init = {}; Object.entries(data.cannedCommentary).forEach(([k, v]) => { init[k] = { ...v, source: 'canned' }; }); return init; }); try { localStorage.removeItem(AP_SESSION_KEY); } catch (e) {} }; const importRef = React.useRef(null); const toolbar = (
astroparse {fileName} { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => importSession(r.result); r.readAsText(f); e.target.value = ''; }} />
); const pal = t.paperPalette || AP_TWEAK_DEFAULTS.paperPalette; const rootStyle = { '--paper': pal[0], '--ink': pal[1], '--rubric': pal[2], '--body-size': t.bodySize + 'px', }; return (
{screen === 'upload' && ( { setFileName(name); setScreen('pipeline'); }} onSample={() => { setFileName('Mowla_Iyer_2024_FireflySparkle.pdf'); setScreen('pipeline'); }} onImportSession={importSession} /> )} {screen === 'pipeline' && setScreen('reader')} />} {screen === 'reader' && ( )} {toast &&
{toast}
} {settingsOpen && ( { setAnnotator(c); apSaveAnnotator(c); }} onClose={() => setSettingsOpen(false)} /> )} setTweak('paperPalette', v)} /> setTweak('bodySize', v)} /> setTweak('dropCaps', v)} /> setTweak('justify', v)} /> setTweak('notesSide', v)} /> setTweak('showScores', v)} />
); } ReactDOM.createRoot(document.getElementById('root')).render();