research-agent / server /static /index.html
abhid1234's picture
cost controls: concise mode, read-only-survivors, flash-lite extraction
4a9ccd7 verified
Raw
History Blame Contribute Delete
25.2 kB
<!DOCTYPE html>
<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 !important; }
.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 &amp; 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 &amp; 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>