const searchForm = document.getElementById('search-form'); const searchInput = document.getElementById('search-input'); const statusEl = document.getElementById('status'); const resultsEl = document.getElementById('search-results'); const resultsList = document.getElementById('results-list'); const graphContainer = document.getElementById('graph-container'); const infoPanel = document.getElementById('info-panel'); const infoContent = document.getElementById('info-content'); let network = null; function showStatus(msg, isError = false) { statusEl.textContent = msg; statusEl.className = isError ? 'error' : 'loading'; } function clearStatus() { statusEl.textContent = ''; statusEl.className = ''; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatAuthors(authors, maxLen = 3) { if (!authors || authors.length === 0) return 'Unknown'; const names = authors.map(a => a.name || 'Unknown'); if (names.length > maxLen) { return names.slice(0, maxLen).join(', ') + ' et al.'; } return names.join(', '); } function showResults(papers) { resultsList.innerHTML = ''; resultsEl.classList.remove('hidden'); graphContainer.innerHTML = ''; infoPanel.classList.add('hidden'); papers.forEach(paper => { const li = document.createElement('li'); const authors = formatAuthors(paper.authors); li.innerHTML = ` ${escapeHtml(paper.title || 'Untitled')} ${escapeHtml(authors)} — ${paper.year || 'n.d.'} — Citations: ${paper.citationCount || 0} `; li.addEventListener('click', () => { resultsEl.classList.add('hidden'); loadGraph(paper.paperId); }); resultsList.appendChild(li); }); } async function search(query) { showStatus('Searching...'); try { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`); const data = await res.json(); clearStatus(); if (data.error) throw new Error(data.error); if (!data.papers || data.papers.length === 0) { showStatus('No papers found.', true); return; } showResults(data.papers); } catch (err) { showStatus(err.message, true); } } async function loadGraph(paperId) { showStatus('Loading reference graph...'); try { const res = await fetch(`/api/graph/${encodeURIComponent(paperId)}`); const data = await res.json(); clearStatus(); if (data.error) throw new Error(data.error); renderGraph(data); } catch (err) { showStatus(err.message, true); } } function renderGraph(graph) { graphContainer.innerHTML = ''; infoPanel.classList.add('hidden'); const container = document.createElement('div'); container.id = 'graph'; graphContainer.appendChild(container); if (graph.nodes.length <= 1) { container.innerHTML = `

No references found in OpenAlex for this paper yet.

This often happens with very recent preprints — OpenAlex hasn't ingested the reference list.

`; const nodes = new vis.DataSet(graph.nodes); const edges = new vis.DataSet(graph.edges); const options = { nodes: { shape: 'dot', color: '#ff6b6b' } }; network = new vis.Network(container, { nodes, edges }, options); return; } const nodes = new vis.DataSet(graph.nodes); const edges = new vis.DataSet(graph.edges); const options = { nodes: { shape: 'dot', scaling: { min: 10, max: 60, label: { enabled: true, min: 12, max: 22 } }, font: { size: 14, color: '#333' } }, edges: { width: 1, color: { color: '#888', highlight: '#000' }, arrows: { to: { enabled: true, scaleFactor: 0.5 } }, smooth: { type: 'continuous' } }, groups: { seed: { color: '#ff6b6b' }, reference: { color: '#45b7d1' } }, physics: { stabilization: false, barnesHut: { gravitationalConstant: -3000, centralGravity: 0.3, springLength: 150, springConstant: 0.04, damping: 0.09 } }, interaction: { hover: true, tooltipDelay: 200 } }; network = new vis.Network(container, { nodes, edges }, options); network.on('click', (params) => { if (params.nodes.length > 0) { const nodeId = params.nodes[0]; const node = nodes.get(nodeId); showNodeInfo(node); } }); network.on('doubleClick', (params) => { if (params.nodes.length > 0) { const nodeId = params.nodes[0]; loadGraph(nodeId); } }); } function showNodeInfo(node) { infoPanel.classList.remove('hidden'); infoContent.innerHTML = `

${escapeHtml(node.label)}

Authors: ${escapeHtml(node.authors)}

Year: ${node.year || 'n.d.'}

Citations: ${node.citationCount}

Tip: you can also double-click a node to re-center.

`; document.getElementById('recenter-btn').addEventListener('click', () => { loadGraph(node.id); }); } searchForm.addEventListener('submit', (e) => { e.preventDefault(); const query = searchInput.value.trim(); if (query) search(query); });