Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Research Agent — cited literature reviews from real papers</title> | |
| <meta name="description" content="Type a topic. Research Agent searches arXiv & Semantic Scholar, reads the PDFs, and writes a citation-verified literature review." /> | |
| <!-- Open Graph / Twitter (shared /r/{id} links render a card) --> | |
| <meta property="og:type" content="website" /> | |
| <meta property="og:title" content="Research Agent — cited literature reviews" /> | |
| <meta property="og:description" content="A topic in, a citation-verified literature review out — built from real arXiv & Semantic Scholar papers." /> | |
| <meta property="og:image" content="/static/og.png" /> | |
| <meta name="twitter:card" content="summary_large_image" /> | |
| <meta name="twitter:title" content="Research Agent — cited literature reviews" /> | |
| <meta name="twitter:description" content="A topic in, a citation-verified literature review out — from real papers." /> | |
| <meta name="twitter:image" content="/static/og.png" /> | |
| <link rel="icon" href="/static/favicon.svg" type="image/svg+xml" /> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;0,9..144,700;1,9..144,500&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet" /> | |
| <style> | |
| :root { | |
| --paper: #f3ece0; --card: #faf5ec; --ink: #211c15; --ink-soft: #6a604f; | |
| --rule: rgba(33,28,21,0.16); --rule-soft: rgba(33,28,21,0.09); | |
| --accent: #8c241b; --accent-deep: #6f1c15; --gold: #9a7637; | |
| --ok: #3f6b3a; --warn: #9a6b14; | |
| --display: "Fraunces", Georgia, serif; | |
| --read: "Newsreader", Georgia, serif; | |
| --mono: "IBM Plex Mono", ui-monospace, Menlo, monospace; | |
| } | |
| * { box-sizing: border-box; } | |
| html { -webkit-font-smoothing: antialiased; } | |
| body { | |
| margin: 0; color: var(--ink); background-color: var(--paper); | |
| /* fine paper grain */ | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E"); | |
| font-family: var(--read); font-size: 18px; line-height: 1.65; | |
| } | |
| .wrap { max-width: 760px; margin: 0 auto; padding: 56px 22px 120px; } | |
| a { color: var(--accent); } | |
| /* ---- masthead ---- */ | |
| .kicker { font-family: var(--mono); font-size: 11px; letter-spacing: 0.22em; | |
| text-transform: uppercase; color: var(--ink-soft); } | |
| header h1 { font-family: var(--display); font-weight: 600; font-size: clamp(38px, 7vw, 60px); | |
| line-height: 1.02; letter-spacing: -0.02em; margin: 12px 0 14px; } | |
| header h1 em { font-style: italic; color: var(--accent); font-weight: 500; } | |
| .dek { font-size: 19px; color: var(--ink-soft); max-width: 34em; margin: 0 0 22px; font-style: italic; } | |
| .rule-double { border: 0; border-top: 1px solid var(--rule); box-shadow: 0 3px 0 -2px var(--rule); margin: 0 0 34px; } | |
| .reveal { opacity: 0; transform: translateY(10px); animation: rise .7s cubic-bezier(.2,.7,.2,1) forwards; } | |
| @keyframes rise { to { opacity: 1; transform: none; } } | |
| /* ---- form ---- */ | |
| .field { display: flex; gap: 10px; align-items: stretch; } | |
| #topic { flex: 1; background: var(--card); border: 1px solid var(--rule); | |
| border-bottom: 2px solid var(--ink); border-radius: 4px 4px 0 0; color: var(--ink); | |
| font-family: var(--read); font-size: 19px; padding: 15px 16px; outline: none; } | |
| #topic:focus { border-bottom-color: var(--accent); } | |
| #topic::placeholder { color: #a99; opacity: .7; font-style: italic; } | |
| button { cursor: pointer; font-family: var(--mono); } | |
| #go { background: var(--accent); color: #f7efe2; border: 1px solid var(--accent-deep); | |
| border-radius: 4px; font-size: 14px; font-weight: 600; letter-spacing: 0.04em; | |
| padding: 0 22px; white-space: nowrap; transition: background .18s, transform .1s; } | |
| #go:hover { background: var(--accent-deep); } | |
| #go:active { transform: translateY(1px); } | |
| #go:disabled { opacity: .5; cursor: progress; } | |
| .chips { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 14px 0 4px; } | |
| .chips-label { font-family: var(--mono); font-size: 10.5px; letter-spacing: .18em; | |
| text-transform: uppercase; color: var(--ink-soft); margin-right: 2px; } | |
| .chip { background: transparent; border: 1px solid var(--rule); border-radius: 999px; | |
| color: var(--ink); font-family: var(--read); font-size: 14px; padding: 5px 13px; transition: .15s; } | |
| .chip:hover { border-color: var(--accent); color: var(--accent); } | |
| details.advanced { margin-top: 16px; border-top: 1px solid var(--rule-soft); padding-top: 12px; } | |
| details.advanced > summary { font-family: var(--mono); font-size: 11px; letter-spacing: .16em; | |
| text-transform: uppercase; color: var(--ink-soft); cursor: pointer; list-style: none; } | |
| details.advanced > summary::-webkit-details-marker { display: none; } | |
| details.advanced > summary::before { content: "+ "; color: var(--accent); } | |
| details.advanced[open] > summary::before { content: "– "; } | |
| .opts { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px 22px; margin-top: 16px; } | |
| .opt label { display: block; font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em; | |
| text-transform: uppercase; color: var(--ink-soft); margin-bottom: 7px; } | |
| .opt select, .opt input[type=range] { width: 100%; } | |
| select { background: var(--card); border: 1px solid var(--rule); color: var(--ink); | |
| font-family: var(--read); font-size: 15px; padding: 9px 8px; border-radius: 4px; } | |
| input[type=range] { accent-color: var(--accent); } | |
| .hint { font-family: var(--mono); font-size: 11.5px; color: var(--ink-soft); margin-top: 18px; } | |
| /* ---- progress / stepper ---- */ | |
| .panel { margin-top: 30px; display: none; } | |
| .panel.show { display: block; } | |
| .stepper { list-style: none; display: flex; gap: 0; padding: 0; margin: 0 0 16px; counter-reset: s; } | |
| .stepper li { flex: 1; position: relative; font-family: var(--mono); font-size: 10.5px; | |
| letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); text-align: center; | |
| padding-top: 22px; } | |
| .stepper li::before { counter-increment: s; content: counter(s); position: absolute; top: 0; | |
| left: 50%; transform: translateX(-50%); width: 15px; height: 15px; line-height: 15px; | |
| border-radius: 50%; border: 1px solid var(--rule); background: var(--paper); font-size: 9px; } | |
| .stepper li::after { content: ""; position: absolute; top: 7px; left: -50%; width: 100%; | |
| height: 1px; background: var(--rule); z-index: -1; } | |
| .stepper li:first-child::after { display: none; } | |
| .stepper li.done { color: var(--ink); } | |
| .stepper li.done::before { background: var(--accent); border-color: var(--accent); color: #f7efe2; content: "✓"; } | |
| .stepper li.active { color: var(--accent); } | |
| .stepper li.active::before { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 3px rgba(140,36,27,.13); } | |
| .prog-status { display: flex; align-items: center; gap: 10px; font-family: var(--mono); | |
| font-size: 13px; color: var(--ink); } | |
| .prog-status .elapsed { margin-left: auto; color: var(--ink-soft); } | |
| .spinner { width: 11px; height: 11px; border: 2px solid var(--rule); border-top-color: var(--accent); | |
| border-radius: 50%; animation: spin .8s linear infinite; flex: none; } | |
| .spinner.ok { border: 2px solid var(--ok); animation: none; } | |
| .spinner.err { border: 2px solid var(--accent); animation: none; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| details.logwrap { margin-top: 12px; } | |
| details.logwrap > summary { font-family: var(--mono); font-size: 11px; color: var(--ink-soft); cursor: pointer; } | |
| .log { font-family: var(--mono); font-size: 11.5px; color: var(--ink-soft); white-space: pre-wrap; | |
| max-height: 200px; overflow: auto; margin: 8px 0 0; padding: 10px 12px; | |
| background: var(--card); border: 1px solid var(--rule-soft); border-radius: 4px; } | |
| /* ---- result ---- */ | |
| .result { display: none; margin-top: 40px; } | |
| .result.show { display: block; } | |
| .result-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; | |
| border-bottom: 2px solid var(--ink); padding-bottom: 14px; } | |
| .result-head h2 { font-family: var(--display); font-weight: 600; font-size: 28px; line-height: 1.1; | |
| margin: 6px 0 8px; letter-spacing: -0.01em; } | |
| .stats { font-family: var(--mono); font-size: 11.5px; color: var(--ink-soft); } | |
| .badge { font-family: var(--mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; | |
| color: var(--gold); border: 1px solid var(--gold); border-radius: 999px; padding: 1px 7px; vertical-align: 3px; margin-left: 8px; } | |
| .actions { display: flex; flex-wrap: wrap; gap: 7px; justify-content: flex-end; } | |
| .act { background: transparent; border: 1px solid var(--rule); color: var(--ink); | |
| font-family: var(--mono); font-size: 11px; padding: 6px 11px; border-radius: 4px; transition: .15s; } | |
| .act:hover { border-color: var(--accent); color: var(--accent); } | |
| .warnings { font-family: var(--mono); font-size: 12px; background: rgba(154,107,20,.08); | |
| border: 1px solid rgba(154,107,20,.35); border-left-width: 3px; border-radius: 4px; | |
| padding: 11px 14px; margin: 18px 0; color: #6e4e0c; } | |
| .warnings b { display: block; margin-bottom: 4px; } | |
| .toc { font-family: var(--mono); font-size: 12px; margin: 22px 0; padding: 14px 16px; | |
| background: var(--card); border: 1px solid var(--rule-soft); border-radius: 4px; | |
| display: flex; flex-wrap: wrap; gap: 6px 18px; } | |
| .toc a { color: var(--ink-soft); text-decoration: none; } | |
| .toc a::before { content: "§ "; color: var(--accent); } | |
| .toc a:hover { color: var(--accent); } | |
| /* typeset review */ | |
| .md { margin-top: 26px; } | |
| .md h2 { font-family: var(--display); font-weight: 600; font-size: 25px; letter-spacing: -0.01em; | |
| margin: 38px 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--rule-soft); scroll-margin-top: 16px; } | |
| .md h3 { font-family: var(--display); font-weight: 600; font-size: 19px; margin: 24px 0 8px; } | |
| .md p { margin: 0 0 16px; } | |
| .md > p:first-of-type::first-letter { font-family: var(--display); font-weight: 700; | |
| float: left; font-size: 3.4em; line-height: .82; padding: 6px 10px 0 0; color: var(--accent); } | |
| .md a { text-underline-offset: 2px; } | |
| .md code { font-family: var(--mono); font-size: .85em; background: rgba(33,28,21,.06); | |
| padding: 1px 5px; border-radius: 3px; } | |
| .md table { border-collapse: collapse; width: 100%; font-size: 15px; margin: 18px 0; } | |
| .md th, .md td { border: 1px solid var(--rule); padding: 9px 11px; text-align: left; vertical-align: top; } | |
| .md th { background: rgba(33,28,21,.05); font-family: var(--mono); font-size: 11px; | |
| letter-spacing: .08em; text-transform: uppercase; font-weight: 500; } | |
| .md tr:nth-child(even) td { background: rgba(33,28,21,.02); } | |
| .md blockquote { border-left: 3px solid var(--accent); margin: 0 0 16px; padding-left: 16px; | |
| color: var(--ink-soft); font-style: italic; } | |
| /* bibliography */ | |
| .biblio { margin-top: 30px; border-top: 2px solid var(--ink); padding-top: 16px; } | |
| .biblio h3 { font-family: var(--display); font-weight: 600; font-size: 20px; margin: 0 0 12px; } | |
| .ref { display: flex; gap: 12px; padding: 11px 0; border-top: 1px solid var(--rule-soft); } | |
| .ref:first-of-type { border-top: 0; } | |
| .ref .num { font-family: var(--mono); font-size: 12px; color: var(--accent); padding-top: 3px; min-width: 1.6em; } | |
| .ref .t { font-size: 16px; } | |
| .ref .t a { text-decoration: none; } | |
| .ref .t a:hover { text-decoration: underline; } | |
| .ref .meta { font-family: var(--mono); font-size: 11.5px; color: var(--ink-soft); margin-top: 3px; } | |
| .ref .src { border: 1px solid var(--rule); border-radius: 3px; padding: 0 5px; margin-left: 7px; } | |
| footer { margin-top: 56px; padding-top: 18px; border-top: 1px solid var(--rule); | |
| font-family: var(--mono); font-size: 11px; color: var(--ink-soft); text-align: center; line-height: 1.7; } | |
| @media (max-width: 560px) { | |
| body { font-size: 17px; } | |
| .field { flex-direction: column; } | |
| #go { padding: 13px; } | |
| .actions { justify-content: flex-start; } | |
| .result-head { flex-direction: column; } | |
| } | |
| @media print { | |
| body { background: #fff; background-image: none; color: #000; } | |
| form, .panel, footer, .actions, .toc, .dek, .chips, .rule-double { display: none ; } | |
| .result-head { border-color: #000; } | |
| .md a, .ref a { color: #000; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <header class="reveal"> | |
| <div class="kicker">Research Agent · A reading machine</div> | |
| <h1>The literature review, <em>synthesized.</em></h1> | |
| <p class="dek">Give it a topic. It searches arXiv & Semantic Scholar, reads the papers, and writes you a cited review.</p> | |
| <hr class="rule-double" /> | |
| </header> | |
| <form id="form" class="reveal" style="animation-delay:.08s"> | |
| <div class="field"> | |
| <input id="topic" type="text" autocomplete="off" | |
| placeholder="e.g. parameter-efficient fine-tuning of large language models" /> | |
| <button id="go" type="submit">Synthesize →</button> | |
| </div> | |
| <div class="chips" id="chips"> | |
| <span class="chips-label">Try</span> | |
| </div> | |
| <details class="advanced"> | |
| <summary>Options</summary> | |
| <div class="opts"> | |
| <div class="opt"><label for="style">Length</label> | |
| <select id="style"> | |
| <option value="concise">Concise · cheaper</option> | |
| <option value="comprehensive">Comprehensive</option> | |
| </select></div> | |
| <div class="opt"><label for="n">Papers — <span id="nval">5</span></label> | |
| <input id="n" type="range" min="3" max="12" value="5" /></div> | |
| <div class="opt"><label for="depth">Depth</label> | |
| <select id="depth"> | |
| <option value="1">1 — single pass</option> | |
| <option value="2">2 — fill gaps</option> | |
| <option value="3">3 — deep dive</option> | |
| </select></div> | |
| <div class="opt"><label for="model">Model</label><select id="model"></select></div> | |
| <div class="opt"><label for="since">Since year</label> | |
| <select id="since"> | |
| <option value="0">Any year</option> | |
| <option value="2022">2022 +</option> | |
| <option value="2020">2020 +</option> | |
| <option value="2015">2015 +</option> | |
| </select></div> | |
| </div> | |
| </details> | |
| <div class="hint">Full-text runs take a couple of minutes — it reads real PDFs. Public demo · 5 runs/hour.</div> | |
| </form> | |
| <section class="panel" id="progress"> | |
| <ol class="stepper" id="stepper"> | |
| <li data-step="search">Search</li> | |
| <li data-step="filter">Filter</li> | |
| <li data-step="read">Read</li> | |
| <li data-step="score">Score</li> | |
| <li data-step="synthesize">Write</li> | |
| </ol> | |
| <div class="prog-status"> | |
| <span class="spinner" id="spin"></span> | |
| <span id="statusText">Queued…</span> | |
| <span class="elapsed" id="elapsed"></span> | |
| </div> | |
| <details class="logwrap"><summary>Details</summary><pre class="log" id="log"></pre></details> | |
| </section> | |
| <article class="result" id="reviewWrap"> | |
| <div class="result-head"> | |
| <div> | |
| <div class="kicker">Literature Review<span class="badge" id="cachedBadge" style="display:none">cached</span></div> | |
| <h2 id="resTitle"></h2> | |
| <div class="stats" id="stats"></div> | |
| </div> | |
| <div class="actions"> | |
| <button class="act" id="share" type="button">🔗 Share</button> | |
| <button class="act" id="bibtex" type="button">BibTeX</button> | |
| <button class="act" id="print" type="button">PDF</button> | |
| <button class="act" id="download" type="button">.md</button> | |
| </div> | |
| </div> | |
| <div id="warnings"></div> | |
| <nav class="toc" id="toc" style="display:none"></nav> | |
| <div class="md" id="md"></div> | |
| <div id="biblio"></div> | |
| </article> | |
| <footer> | |
| Reads abstracts & PDFs from arXiv and Semantic Scholar · synthesis by an LLM<br/> | |
| Not peer review — verify citations before you rely on them. | |
| </footer> | |
| </div> | |
| <script src="/static/marked.min.js"></script> | |
| <script> | |
| const $ = (id) => document.getElementById(id); | |
| const form = $('form'), go = $('go'), nInput = $('n'); | |
| let pollTimer = null, elapsedTimer = null, t0 = 0; | |
| let currentTopic = '', lastReview = '', currentJobId = '', currentPapers = []; | |
| const EXAMPLES = ['LoRA fine-tuning', 'retrieval-augmented generation', | |
| 'diffusion models', 'mixture-of-experts LLMs', 'graph neural networks']; | |
| const STEPS = ['search','filter','read','score','synthesize']; | |
| nInput.addEventListener('input', () => $('nval').textContent = nInput.value); | |
| EXAMPLES.forEach(t => { | |
| const b = document.createElement('button'); | |
| b.type = 'button'; b.className = 'chip'; b.textContent = t; | |
| b.addEventListener('click', () => { $('topic').value = t; $('topic').focus(); }); | |
| $('chips').appendChild(b); | |
| }); | |
| (async function init() { | |
| try { | |
| const cfg = await (await fetch('/api/config')).json(); | |
| $('model').innerHTML = (cfg.models || ['default']).map( | |
| (m, i) => `<option value="${m}">${m.replace('gemini-2.5-','')}${i===0?' · default':''}</option>`).join(''); | |
| } catch { $('model').innerHTML = '<option value="">default</option>'; } | |
| const m = location.pathname.match(/^\/r\/([a-z0-9]+)/i); | |
| if (m) { showProgress('Loading shared review…'); poll(m[1]); } | |
| })(); | |
| form.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const topic = $('topic').value.trim(); | |
| if (!topic) return; | |
| currentTopic = topic; | |
| go.disabled = true; go.textContent = 'Working…'; | |
| $('reviewWrap').classList.remove('show'); | |
| showProgress('Queued…'); $('log').textContent = ''; startClock(); | |
| let res; | |
| try { | |
| res = await fetch('/api/review', { method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ topic, max_papers: parseInt(nInput.value,10), | |
| depth: parseInt($('depth').value,10), model: $('model').value, | |
| year_min: parseInt($('since').value,10), style: $('style').value }) }); | |
| } catch { return fail('Network error — is the server up?'); } | |
| const data = await res.json(); | |
| if (!res.ok) return fail(data.error || 'Request failed.'); | |
| poll(data.job_id); | |
| }); | |
| function showProgress(text) { $('progress').classList.add('show'); setStatus('spin', text); } | |
| function startClock() { | |
| t0 = Date.now(); clearInterval(elapsedTimer); | |
| elapsedTimer = setInterval(() => { | |
| $('elapsed').textContent = Math.round((Date.now()-t0)/1000) + 's'; | |
| }, 1000); | |
| } | |
| function poll(jobId) { | |
| currentJobId = jobId; | |
| clearInterval(pollTimer); | |
| pollTimer = setInterval(async () => { | |
| let res; try { res = await fetch('/api/review/' + jobId); } catch { return; } | |
| if (!res.ok) { clearInterval(pollTimer); return fail('Job not found or expired.'); } | |
| const j = await res.json(); | |
| if (!t0) startClock(); | |
| renderLog(j.log); updateStepper(j.log); | |
| if (j.status === 'queued') setStatus('spin', 'Queued…'); | |
| if (j.status === 'running') setStatus('spin', friendly(j.log)); | |
| if (j.status === 'done') { clearInterval(pollTimer); clearInterval(elapsedTimer); finish(j); } | |
| if (j.status === 'error') { clearInterval(pollTimer); clearInterval(elapsedTimer); fail(j.error || 'Run failed.'); } | |
| }, 1500); | |
| } | |
| function friendly(log) { | |
| const last = (log && log.length) ? log[log.length-1] : ''; | |
| if (/Synthesi/i.test(last)) return 'Writing the review…'; | |
| if (/Scoring/i.test(last)) return 'Ranking papers…'; | |
| if (/Reading|read:/i.test(last)) return 'Reading papers…'; | |
| if (/Filtering/i.test(last)) return 'Filtering for relevance…'; | |
| if (/Deepening/i.test(last)) return 'Digging into the gaps…'; | |
| if (/Search|query:/i.test(last)) return 'Searching arXiv & Semantic Scholar…'; | |
| return 'Working…'; | |
| } | |
| function stepFromLog(log) { | |
| for (let i = log.length - 1; i >= 0; i--) { | |
| const l = log[i]; | |
| if (/Synthesi/i.test(l)) return 'synthesize'; | |
| if (/Scoring/i.test(l)) return 'score'; | |
| if (/Reading|read:/i.test(l)) return 'read'; | |
| if (/Filtering/i.test(l)) return 'filter'; | |
| if (/Deepening/i.test(l)) return 'read'; | |
| if (/Search|query:/i.test(l)) return 'search'; | |
| } | |
| return 'search'; | |
| } | |
| function updateStepper(log, complete) { | |
| const cur = complete ? STEPS.length : STEPS.indexOf(stepFromLog(log || [])); | |
| [...$('stepper').children].forEach((li, i) => { | |
| li.classList.toggle('done', i < cur); | |
| li.classList.toggle('active', !complete && i === cur); | |
| }); | |
| } | |
| function renderLog(lines) { const el = $('log'); el.textContent = (lines||[]).join('\n'); el.scrollTop = el.scrollHeight; } | |
| function setStatus(kind, text) { $('spin').className = 'spinner ' + (kind==='spin'?'':kind); $('statusText').textContent = text; } | |
| function finish(j) { | |
| lastReview = j.review; | |
| currentTopic = currentTopic || j.topic || ''; | |
| currentPapers = j.papers || []; | |
| updateStepper(j.log, true); | |
| setStatus('ok', 'Done'); | |
| const s = j.stats || {}; | |
| $('stats').textContent = s.elapsed_s ? `${s.elapsed_s}s · ${s.llm_calls} model calls · ${(s.in_tok||0)+(s.out_tok||0)} tokens · ~$${s.est_cost_usd}` : ''; | |
| $('cachedBadge').style.display = j.cached ? 'inline-block' : 'none'; | |
| $('resTitle').textContent = currentTopic; | |
| renderWarnings(j.warnings || []); | |
| $('md').innerHTML = marked.parse(j.review || ''); | |
| buildToc(); | |
| renderBiblio(currentPapers); | |
| $('reviewWrap').classList.add('show'); | |
| resetBtn(); | |
| $('reviewWrap').scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| function renderWarnings(w) { | |
| if (!w || !w.length) { $('warnings').innerHTML = ''; return; } | |
| $('warnings').innerHTML = `<div class="warnings"><b>⚠ ${w.length} citation check${w.length>1?'s':''} to verify</b>` + | |
| w.slice(0,6).map(x => esc(x)).join('<br/>') + '</div>'; | |
| } | |
| function buildToc() { | |
| const hs = $('md').querySelectorAll('h2'); | |
| if (hs.length < 2) { $('toc').style.display = 'none'; return; } | |
| const items = []; | |
| hs.forEach((h, i) => { | |
| const id = 'sec-' + i; h.id = id; | |
| const label = h.textContent.replace(/^\s*\d+[\.\)]\s*/, ''); | |
| items.push(`<a href="#${id}">${esc(label)}</a>`); | |
| }); | |
| $('toc').innerHTML = items.join(''); | |
| $('toc').style.display = 'flex'; | |
| } | |
| function renderBiblio(papers) { | |
| if (!papers || !papers.length) { $('biblio').innerHTML = ''; return; } | |
| const rows = papers.map((p, i) => { | |
| const title = p.url ? `<a href="${p.url}" target="_blank" rel="noopener">${esc(p.title)}</a>` : esc(p.title); | |
| const src = p.source === 'semantic_scholar' ? 'S2' : 'arXiv'; | |
| const cites = p.citation_count ? ` · ${p.citation_count.toLocaleString()} citations` : ''; | |
| const abs = p.read_from === 'abstract' ? ' · abstract only' : ''; | |
| return `<div class="ref"><div class="num">${i+1}</div><div><div class="t">${title}<span class="src">${src}</span></div> | |
| <div class="meta">${esc(p.authors)} · ${p.year}${cites}${abs}</div></div></div>`; | |
| }).join(''); | |
| $('biblio').innerHTML = `<div class="biblio"><h3>Papers (${papers.length})</h3>${rows}</div>`; | |
| } | |
| function fail(msg) { | |
| clearInterval(pollTimer); clearInterval(elapsedTimer); | |
| setStatus('err', msg); resetBtn(); | |
| } | |
| function resetBtn() { go.disabled = false; go.textContent = 'Synthesize →'; } | |
| function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } | |
| function slug() { return (currentTopic.toLowerCase().replace(/[^\w]+/g,'-').slice(0,60) || 'review'); } | |
| function saveBlob(text, ext, type) { | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(new Blob([text], { type })); a.download = slug()+ext; a.click(); | |
| } | |
| $('download').addEventListener('click', () => | |
| saveBlob('# Literature Review: '+currentTopic+'\n\n'+lastReview, '.md', 'text/markdown')); | |
| $('print').addEventListener('click', () => window.print()); | |
| $('share').addEventListener('click', async () => { | |
| if (!currentJobId) return; | |
| const url = location.origin + '/r/' + currentJobId; | |
| try { await navigator.clipboard.writeText(url); $('share').textContent = '✓ Copied'; } | |
| catch { prompt('Share link:', url); } | |
| setTimeout(() => $('share').textContent = '🔗 Share', 1500); | |
| }); | |
| $('bibtex').addEventListener('click', () => { | |
| if (!currentPapers.length) return; | |
| const out = currentPapers.map(p => { | |
| const sur = (p.authors||'anon').split(/[ ,]/)[0].toLowerCase().replace(/[^a-z]/g,'') || 'anon'; | |
| const fields = [` title={${p.title}}`, ` author={${(p.authors||'').replace(' et al.',' and others')}}`, | |
| p.year ? ` year={${p.year}}` : null, p.url ? ` note={${p.url}}` : null].filter(Boolean).join(',\n'); | |
| return `@article{${sur}${p.year||''},\n${fields}\n}`; | |
| }).join('\n\n'); | |
| saveBlob(out, '.bib', 'text/plain'); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |