Initial commit: connected-papers / linked-papers app
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
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 = `
|
||||
<strong>${escapeHtml(paper.title || 'Untitled')}</strong>
|
||||
<span>${escapeHtml(authors)} — ${paper.year || 'n.d.'} — Citations: ${paper.citationCount || 0}</span>
|
||||
`;
|
||||
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 = `
|
||||
<div class="empty-graph">
|
||||
<p><strong>No references found in OpenAlex for this paper yet.</strong></p>
|
||||
<p>This often happens with very recent preprints — OpenAlex hasn't ingested the reference list.</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<h3>${escapeHtml(node.label)}</h3>
|
||||
<p><strong>Authors:</strong> ${escapeHtml(node.authors)}</p>
|
||||
<p><strong>Year:</strong> ${node.year || 'n.d.'}</p>
|
||||
<p><strong>Citations:</strong> ${node.citationCount}</p>
|
||||
<button id="recenter-btn">Re-center graph on this paper</button>
|
||||
<p><em>Tip: you can also double-click a node to re-center.</em></p>
|
||||
`;
|
||||
document.getElementById('recenter-btn').addEventListener('click', () => {
|
||||
loadGraph(node.id);
|
||||
});
|
||||
}
|
||||
|
||||
searchForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const query = searchInput.value.trim();
|
||||
if (query) search(query);
|
||||
});
|
||||
Reference in New Issue
Block a user