Function bodies 63 total
autoLoadSnapshot function · javascript · L55-L103 (49 LOC)js/app.js
async function autoLoadSnapshot() {
try {
const cfg = loadConfig();
const candidates = [];
if (cfg.scholarId) candidates.push(`data/${cfg.scholarId}_network.json`);
candidates.push('data/network.json');
let data = null;
for (const url of candidates) {
try {
const resp = await fetch(url);
if (resp.ok) {
data = await resp.json();
break;
}
} catch (e) { /* try next */ }
}
if (!data) return;
if (!data.publications || data.publications.length === 0) return;
currentPublications = data.publications;
updateStats(currentPublications);
// Restore geo data
if (data.geoData && Object.keys(data.geoData).length > 0) {
currentGeoData = data.geoData;
const agg = aggregateGeoData(currentGeoData);
currentGlobePoints = agg.points;
currentshowLoading function · javascript · L142-L147 (6 LOC)js/app.js
function showLoading(text, pct, detail) {
loadingOverlay.style.display = 'flex';
loadingText.textContent = text || 'Loading...';
progressFill.style.width = `${pct || 0}%`;
loadingDetail.textContent = detail || '';
}hideLoading function · javascript · L148-L150 (3 LOC)js/app.js
function hideLoading() {
loadingOverlay.style.display = 'none';
}updateStats function · javascript · L152-L157 (6 LOC)js/app.js
function updateStats(publications) {
const stats = computeStats(publications);
document.getElementById('statPubs').textContent = `📄 ${stats.totalPubs}`;
document.getElementById('statCitations').textContent = `📊 ${stats.totalCitations}`;
document.getElementById('statHIndex').textContent = `📈 H: ${stats.hIndex}`;
}showTooltip function · javascript · L160-L187 (28 LOC)js/app.js
function showTooltip(event, d) {
let html = '';
if (d.type === 'researcher') {
html = `<div class="tt-title">${d.label}</div><div class="tt-meta">Center researcher</div>`;
} else if (d.type === 'publication') {
html = `<div class="tt-title">${d.label}</div>`;
html += `<div class="tt-meta">Year: ${d.year || '—'} | Citations: ${d.citationCount || 0}</div>`;
if (d.theme && d.theme !== 'Uncategorized') {
html += `<span class="tt-theme" style="background:${d.themeColor}33;color:${d.themeColor}">${d.theme}</span>`;
}
if (d.summary) {
html += `<div class="tt-summary">${d.summary}</div>`;
}
html += `<div class="tt-meta" style="margin-top:6px;font-style:italic">Click to ${d.childrenExpanded ? 'collapse' : 'expand'} citations</div>`;
} else if (d.type === 'citation') {
html = `<div class="tt-title">${d.label}</div>`;
if (d.authors) html += `<div class="tt-meta">${d.hideTooltip function · javascript · L189-L191 (3 LOC)js/app.js
function hideTooltip() {
tooltip.classList.remove('visible');
}toggleCitations function · javascript · L194-L205 (12 LOC)js/app.js
function toggleCitations(pubNode) {
pubNode.childrenExpanded = !pubNode.childrenExpanded;
const expand = pubNode.childrenExpanded;
for (const node of currentNetwork.nodes) {
if (node.type === 'citation' && node.parentPubId === pubNode.id) {
node.hidden = !expand;
}
}
applyFiltersAndRender();
}About: code-quality intelligence by Repobility · https://repobility.com
renderCurrentNetwork function · javascript · L208-L221 (14 LOC)js/app.js
function renderCurrentNetwork() {
if (!currentNetwork || currentNetwork.nodes.length === 0) {
emptyState.style.display = 'flex';
return;
}
emptyState.style.display = 'none';
btnExport.disabled = false;
renderGraph(currentNetwork, {
onTooltip: showTooltip,
onHideTooltip: hideTooltip,
onToggleCitations: toggleCitations,
});
}applyFiltersAndRender function · javascript · L223-L241 (19 LOC)js/app.js
function applyFiltersAndRender() {
if (!currentNetwork) return;
const filters = {
yearFrom: parseInt(filterYearFrom.value) || null,
yearTo: parseInt(filterYearTo.value) || null,
recentN: parseInt(filterRecentN.value) || null,
search: filterSearch.value.trim() || null,
};
const hasFilters = Object.values(filters).some(v => v !== null);
const displayed = hasFilters ? filterNetwork(currentNetwork, filters) : currentNetwork;
renderGraph(displayed, {
onTooltip: showTooltip,
onHideTooltip: hideTooltip,
onToggleCitations: toggleCitations,
});
}renderRankings function · javascript · L388-L455 (68 LOC)js/app.js
function renderRankings() {
if (!currentGeoData || Object.keys(currentGeoData).length === 0) {
rankingsSection.style.display = 'none';
return;
}
const topK = parseInt(rankTopKInput.value) || 10;
rankingsSection.style.display = 'block';
// Aggregate by country
const countryCounts = {};
const institutionCounts = {};
for (const [, info] of Object.entries(currentGeoData)) {
if (info.country) {
countryCounts[info.country] = (countryCounts[info.country] || 0) + 1;
}
if (info.institution) {
const key = info.institution;
if (!institutionCounts[key]) {
institutionCounts[key] = { count: 0, country: info.country || '' };
}
institutionCounts[key].count += 1;
}
}
// Sort and take top K
const topCountries = Object.entries(countryCounts)
.map(([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count)
exportNetworkJSON function · javascript · L8-L24 (17 LOC)js/cache.js
export function exportNetworkJSON(networkData, scholarId) {
const payload = {
version: 1,
exportedAt: new Date().toISOString(),
scholarId,
...networkData,
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${scholarId}_network.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}importNetworkJSON function · javascript · L30-L56 (27 LOC)js/cache.js
export function importNetworkJSON() {
return new Promise((resolve) => {
const input = document.getElementById('fileImport');
input.value = '';
const handler = (e) => {
input.removeEventListener('change', handler);
const file = e.target.files[0];
if (!file) { resolve(null); return; }
const reader = new FileReader();
reader.onload = (ev) => {
try {
const data = JSON.parse(ev.target.result);
resolve(data);
} catch (err) {
alert('Invalid JSON file.');
resolve(null);
}
};
reader.readAsText(file);
};
input.addEventListener('change', handler);
input.click();
});
}mergePublications function · javascript · L65-L90 (26 LOC)js/cache.js
export function mergePublications(existingPubs, newPubs) {
const map = new Map();
// Index existing by title
for (const pub of existingPubs) {
map.set(pub.title, pub);
}
// Add/update with new data
for (const pub of newPubs) {
if (map.has(pub.title)) {
// Update citation count but keep cached citations list
const existing = map.get(pub.title);
existing.citationCount = pub.citationCount;
existing.year = pub.year || existing.year;
// If new data has more citations fetched, use those
if (pub.citations && pub.citations.length > (existing.citations || []).length) {
existing.citations = pub.citations;
}
} else {
map.set(pub.title, pub);
}
}
return Array.from(map.values());
}buildCacheMap function · javascript · L97-L103 (7 LOC)js/cache.js
export function buildCacheMap(pubs) {
const m = {};
for (const p of pubs) {
m[p.title] = p;
}
return m;
}loadConfig function · javascript · L14-L20 (7 LOC)js/config.js
export function loadConfig() {
try {
const raw = localStorage.getItem(CONFIG_KEY);
if (raw) return { ...DEFAULTS, ...JSON.parse(raw) };
} catch (e) { /* ignore */ }
return { ...DEFAULTS };
}Repobility · MCP-ready · https://repobility.com
saveConfig function · javascript · L22-L24 (3 LOC)js/config.js
export function saveConfig(cfg) {
localStorage.setItem(CONFIG_KEY, JSON.stringify(cfg));
}validateConfig function · javascript · L26-L32 (7 LOC)js/config.js
export function validateConfig(cfg) {
const missing = [];
if (!cfg.scholarId.trim()) missing.push('Scholar ID');
if (!cfg.researcherName.trim()) missing.push('Researcher Name');
if (!cfg.scraperKey.trim()) missing.push('ScraperAPI Key');
return missing;
}initSettingsModal function · javascript · L34-L88 (55 LOC)js/config.js
export function initSettingsModal() {
const modal = document.getElementById('settingsModal');
const btnOpen = document.getElementById('btnSettings');
const btnClose = document.getElementById('btnCloseSettings');
const btnCancel = document.getElementById('btnCancelSettings');
const btnSave = document.getElementById('btnSaveSettings');
const fields = {
scholarId: document.getElementById('cfgScholarId'),
researcherName: document.getElementById('cfgResearcherName'),
scraperKey: document.getElementById('cfgScraperKey'),
llmProvider: document.getElementById('cfgLlmProvider'),
llmKey: document.getElementById('cfgLlmKey'),
llmModel: document.getElementById('cfgLlmModel'),
};
function populateFields() {
const cfg = loadConfig();
for (const [key, el] of Object.entries(fields)) {
el.value = cfg[key] || '';
}
}
function openModal() {
populateFields();
modalpopulateFields function · javascript · L50-L55 (6 LOC)js/config.js
function populateFields() {
const cfg = loadConfig();
for (const [key, el] of Object.entries(fields)) {
el.value = cfg[key] || '';
}
}openModal function · javascript · L57-L60 (4 LOC)js/config.js
function openModal() {
populateFields();
modal.style.display = 'flex';
}closeModal function · javascript · L62-L64 (3 LOC)js/config.js
function closeModal() {
modal.style.display = 'none';
}getCountryCoords function · javascript · L150-L161 (12 LOC)js/countries.js
export function getCountryCoords(name) {
if (!name) return null;
const trimmed = name.trim();
// Direct lookup
if (COUNTRY_COORDS[trimmed]) return COUNTRY_COORDS[trimmed];
// Case-insensitive
const lower = trimmed.toLowerCase();
for (const [key, val] of Object.entries(COUNTRY_COORDS)) {
if (key.toLowerCase() === lower) return val;
}
return null;
}buildAliasMap function · javascript · L31-L40 (10 LOC)js/globe.js
function buildAliasMap() {
const map = {};
for (const [geoName, aliases] of Object.entries(COUNTRY_NAME_ALIASES)) {
for (const alias of aliases) {
map[alias.toLowerCase()] = geoName;
}
map[geoName.toLowerCase()] = geoName;
}
return map;
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
aggregateGeoData function · javascript · L47-L84 (38 LOC)js/globe.js
export function aggregateGeoData(geoData) {
const countryCounts = {};
const countryInstitutions = {};
for (const [, info] of Object.entries(geoData)) {
const country = info.country;
if (!country) continue;
countryCounts[country] = (countryCounts[country] || 0) + 1;
if (info.institution) {
if (!countryInstitutions[country]) countryInstitutions[country] = new Set();
countryInstitutions[country].add(info.institution);
}
}
const points = [];
let totalMapped = 0;
for (const [country, count] of Object.entries(countryCounts)) {
totalMapped += count;
const institutions = countryInstitutions[country]
? Array.from(countryInstitutions[country])
: [];
points.push({
country,
count,
institutions,
});
}
points.sort((a, b) => b.count - a.count);
return {
points,
countryCount: Object.keysloadGeoJson function · javascript · L89-L98 (10 LOC)js/globe.js
async function loadGeoJson() {
if (geoJsonData) return geoJsonData;
const resp = await fetch('https://unpkg.com/world-atlas@2/countries-110m.json');
const topoData = await resp.json();
// Convert TopoJSON to GeoJSON using topojson-client
// globe.gl can handle topojson directly via polygonsData, but we need
// to use the hexed polygon layer. Let's use the geojson URL instead.
geoJsonData = topoData;
return geoJsonData;
}matchCountryToFeature function · javascript · L103-L128 (26 LOC)js/globe.js
function matchCountryToFeature(countryName, features) {
const lower = countryName.toLowerCase();
// Direct match on feature properties.name or properties.NAME
for (const f of features) {
const fName = (f.properties.name || f.properties.NAME || '').toLowerCase();
if (fName === lower) return f;
}
// Try alias map
const canonical = aliasMap[lower];
if (canonical) {
for (const f of features) {
const fName = (f.properties.name || f.properties.NAME || '').toLowerCase();
if (fName === canonical.toLowerCase()) return f;
}
}
// Partial match
for (const f of features) {
const fName = (f.properties.name || f.properties.NAME || '').toLowerCase();
if (fName.includes(lower) || lower.includes(fName)) return f;
}
return null;
}initGlobe function · javascript · L133-L254 (122 LOC)js/globe.js
export async function initGlobe(container, points, stats) {
destroyGlobe();
const width = container.clientWidth;
const height = container.clientHeight;
// Find max count for brightness normalization
const maxCount = Math.max(...points.map(p => p.count), 1);
// Build country → data lookup
const countryDataMap = {};
for (const p of points) {
countryDataMap[p.country.toLowerCase()] = p;
// Also index by aliases
const canonical = aliasMap[p.country.toLowerCase()];
if (canonical) countryDataMap[canonical.toLowerCase()] = p;
}
// Load GeoJSON for country polygons
const geoUrl = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json';
globeInstance = Globe()
.globeImageUrl('//unpkg.com/three-globe/example/img/earth-night.jpg')
.bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png')
.backgroundImageUrl('//unpkg.com/three-globe/example/img/night-sky.png')
getCountryData function · javascript · L259-L272 (14 LOC)js/globe.js
function getCountryData(feature, countryDataMap) {
const name = (feature.properties.name || feature.properties.NAME || '').toLowerCase();
if (countryDataMap[name]) return countryDataMap[name];
// Try alias
const canonical = aliasMap[name];
if (canonical && countryDataMap[canonical.toLowerCase()]) {
return countryDataMap[canonical.toLowerCase()];
}
// Try partial
for (const [key, val] of Object.entries(countryDataMap)) {
if (name.includes(key) || key.includes(name)) return val;
}
return null;
}interpolateGlowColor function · javascript · L277-L297 (21 LOC)js/globe.js
function interpolateGlowColor(t) {
// Low: dim indigo → Medium: purple → High: bright cyan-white
if (t < 0.33) {
const r = Math.round(60 + t * 3 * 40);
const g = Math.round(50 + t * 3 * 50);
const b = Math.round(140 + t * 3 * 60);
return `rgba(${r}, ${g}, ${b}, ${0.4 + t * 0.3})`;
} else if (t < 0.66) {
const s = (t - 0.33) / 0.33;
const r = Math.round(100 + s * 68);
const g = Math.round(100 + s * 55);
const b = Math.round(200 + s * 47);
return `rgba(${r}, ${g}, ${b}, ${0.55 + s * 0.2})`;
} else {
const s = (t - 0.66) / 0.34;
const r = Math.round(168 + s * 87);
const g = Math.round(155 + s * 100);
const b = Math.round(247);
return `rgba(${r}, ${g}, ${b}, ${0.75 + s * 0.25})`;
}
}topoFeatures function · javascript · L302-L370 (69 LOC)js/globe.js
function topoFeatures(topology, objectName) {
const obj = topology.objects[objectName];
if (!obj) {
// Try first available object
const key = Object.keys(topology.objects)[0];
if (!key) return [];
return topoFeatures(topology, key);
}
const arcs = topology.arcs;
const transform = topology.transform;
function decodeArc(arcIdx) {
const reversed = arcIdx < 0;
const idx = reversed ? ~arcIdx : arcIdx;
const arc = arcs[idx];
const coords = [];
let x = 0, y = 0;
for (const [dx, dy] of arc) {
x += dx;
y += dy;
const lng = transform ? x * transform.scale[0] + transform.translate[0] : x;
const lat = transform ? y * transform.scale[1] + transform.translate[1] : y;
coords.push([lng, lat]);
}
return reversed ? coords.reverse() : coords;
}
function decodeRing(ring) {
const coords = [];
for (condecodeArc function · javascript · L314-L330 (17 LOC)js/globe.js
function decodeArc(arcIdx) {
const reversed = arcIdx < 0;
const idx = reversed ? ~arcIdx : arcIdx;
const arc = arcs[idx];
const coords = [];
let x = 0, y = 0;
for (const [dx, dy] of arc) {
x += dx;
y += dy;
const lng = transform ? x * transform.scale[0] + transform.translate[0] : x;
const lat = transform ? y * transform.scale[1] + transform.translate[1] : y;
coords.push([lng, lat]);
}
return reversed ? coords.reverse() : coords;
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
decodeRing function · javascript · L332-L340 (9 LOC)js/globe.js
function decodeRing(ring) {
const coords = [];
for (const arcIdx of ring) {
const arcCoords = decodeArc(arcIdx);
// Skip first point of subsequent arcs to avoid duplicates
coords.push(...(coords.length > 0 ? arcCoords.slice(1) : arcCoords));
}
return coords;
}decodeGeometry function · javascript · L342-L355 (14 LOC)js/globe.js
function decodeGeometry(geom) {
if (geom.type === 'Polygon') {
return {
type: 'Polygon',
coordinates: geom.arcs.map(decodeRing),
};
} else if (geom.type === 'MultiPolygon') {
return {
type: 'MultiPolygon',
coordinates: geom.arcs.map(polygon => polygon.map(decodeRing)),
};
}
return geom;
}updateGlobeStats function · javascript · L372-L390 (19 LOC)js/globe.js
function updateGlobeStats(container, stats) {
let overlay = container.querySelector('.globe-stats');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'globe-stats';
container.appendChild(overlay);
}
overlay.innerHTML = `
<div class="globe-stat-item">
<span class="globe-stat-number">${stats.totalMapped}</span>
<span class="globe-stat-label">Mapped Citations</span>
</div>
<div class="globe-stat-item">
<span class="globe-stat-number">${stats.countryCount}</span>
<span class="globe-stat-label">Countries</span>
</div>
`;
}updateGlobeData function · javascript · L392-L399 (8 LOC)js/globe.js
export function updateGlobeData(points, stats) {
if (!globeInstance) return;
// Would need to re-init to update polygon colors properly
const container = globeInstance.domElement?.parentElement;
if (container) {
initGlobe(container, points, stats);
}
}destroyGlobe function · javascript · L401-L410 (10 LOC)js/globe.js
export function destroyGlobe() {
if (globeInstance) {
const container = globeInstance.domElement?.parentElement;
if (container?._resizeHandler) {
window.removeEventListener('resize', container._resizeHandler);
}
globeInstance._destructor?.();
globeInstance = null;
}
}initGraph function · javascript · L13-L51 (39 LOC)js/graph.js
export function initGraph(container) {
width = container.clientWidth;
height = container.clientHeight;
svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
// Defs for gradients and glow
const defs = svg.append('defs');
const glowFilter = defs.append('filter').attr('id', 'glow');
glowFilter.append('feGaussianBlur').attr('stdDeviation', '3').attr('result', 'coloredBlur');
const feMerge = glowFilter.append('feMerge');
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
g = svg.append('g');
// Zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 5])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Handle window resize
window.addEventListener('resize', () => {
width = container.clientWidth;
height = containerrenderGraph function · javascript · L58-L164 (107 LOC)js/graph.js
export function renderGraph(network, callbacks = {}) {
currentNetwork = network;
// Clear previous
g.selectAll('*').remove();
const visibleNodes = network.nodes.filter(n => !n.hidden);
const visibleNodeIds = new Set(visibleNodes.map(n => n.id));
const visibleLinks = network.links.filter(l => {
const src = typeof l.source === 'object' ? l.source.id : l.source;
const tgt = typeof l.target === 'object' ? l.target.id : l.target;
return visibleNodeIds.has(src) && visibleNodeIds.has(tgt);
});
// Force simulation
simulation = d3.forceSimulation(visibleNodes)
.force('link', d3.forceLink(visibleLinks).id(d => d.id).distance(d => {
if (d.type === 'researcher-pub') return 120;
return 60;
}))
.force('charge', d3.forceManyBody().strength(d => {
if (d.type === 'researcher') return -400;
if (d.type === 'publication') return -100;
return -30;
}))
getNodeRadius function · javascript · L166-L172 (7 LOC)js/graph.js
function getNodeRadius(d) {
if (d.type === 'researcher') return NODE_SIZES.researcher;
if (d.type === 'publication') {
return Math.max(NODE_SIZES.publication, Math.min(20, 5 + Math.sqrt(d.citationCount || 0) * 1.5));
}
return NODE_SIZES.citation;
}About: code-quality intelligence by Repobility · https://repobility.com
getNodeColor function · javascript · L174-L178 (5 LOC)js/graph.js
function getNodeColor(d) {
if (d.type === 'researcher') return '#6366f1';
if (d.type === 'publication') return d.themeColor || '#6366f1';
return '#4a4a6a';
}dragStarted function · javascript · L180-L184 (5 LOC)js/graph.js
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}dragged function · javascript · L186-L189 (4 LOC)js/graph.js
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}dragEnded function · javascript · L191-L195 (5 LOC)js/graph.js
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}renderLegend function · javascript · L197-L218 (22 LOC)js/graph.js
function renderLegend(themes) {
const legend = document.getElementById('legend');
if (!themes || themes.length === 0) {
legend.style.display = 'none';
return;
}
legend.style.display = 'block';
legend.innerHTML = `
<h4>Research Themes</h4>
${themes.map(t => `
<div class="legend-item">
<span class="legend-dot" style="background:${t.color}"></span>
<span>${t.name}</span>
</div>
`).join('')}
<div class="legend-item" style="margin-top:8px;border-top:1px solid rgba(100,100,180,0.15);padding-top:6px;">
<span class="legend-dot" style="background:#4a4a6a"></span>
<span style="color:#8888aa">Citations (click pub to show)</span>
</div>
`;
}destroyGraph function · javascript · L220-L226 (7 LOC)js/graph.js
export function destroyGraph() {
if (simulation) simulation.stop();
if (svg) svg.remove();
svg = null;
g = null;
simulation = null;
}callLLM function · javascript · L17-L26 (10 LOC)js/llm.js
async function callLLM(provider, apiKey, model, prompt) {
if (provider === 'openai') {
return await callOpenAI(apiKey, model || 'gpt-4o-mini', prompt);
} else if (provider === 'claude') {
return await callClaude(apiKey, model || 'claude-sonnet-4-20250514', prompt);
} else if (provider === 'gemini') {
return await callGemini(apiKey, model || 'gemini-pro', prompt);
}
throw new Error(`Unknown LLM provider: ${provider}`);
}callOpenAI function · javascript · L28-L47 (20 LOC)js/llm.js
async function callOpenAI(apiKey, model, prompt) {
const resp = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.3,
}),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`OpenAI API error ${resp.status}: ${err}`);
}
const data = await resp.json();
return data.choices[0].message.content;
}Repobility · MCP-ready · https://repobility.com
callClaude function · javascript · L49-L73 (25 LOC)js/llm.js
async function callClaude(apiKey, model, prompt) {
// Note: Claude API requires server-side calls due to CORS.
// We use a CORS proxy approach or the user needs to handle this.
// For direct browser use, we'll try the API directly.
const resp = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true',
},
body: JSON.stringify({
model,
max_tokens: 4096,
messages: [{ role: 'user', content: prompt }],
}),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`Claude API error ${resp.status}: ${err}`);
}
const data = await resp.json();
return data.content[0].text;
}callGemini function · javascript · L75-L90 (16 LOC)js/llm.js
async function callGemini(apiKey, model, prompt) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
}),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`Gemini API error ${resp.status}: ${err}`);
}
const data = await resp.json();
return data.candidates[0].content.parts[0].text;
}analyzePapers function · javascript · L99-L181 (83 LOC)js/llm.js
export async function analyzePapers(publications, config, onProgress) {
if (!config.llmKey) {
return { summaries: {}, themes: {} };
}
const titles = publications.map((p, i) => `${i + 1}. ${p.title} (${p.year})`).join('\n');
// ── Step 1: Cluster into themes ──
onProgress('Clustering publications into themes...', 92);
const clusterPrompt = `You are a research analyst. Given these academic publication titles, categorize them into broad, high-level research disciplines.
Use broad categories like: Healthcare, Economics, NLP, Computer Vision, Reinforcement Learning, Operations Research, Robotics, Finance, Education, Climate Science, Cybersecurity, Bioinformatics, Social Science, Materials Science, etc.
Do NOT create narrow or paper-specific themes. Each theme should be a well-known academic field or discipline that a general audience would recognize. Aim for 3-7 broad themes total.
Publications:
${titles}
Respond ONLY with valid JSON (no markdown, no cpage 1 / 2next ›