← back to yubol-bobo__Research_Network

Function bodies 63 total

All specs Real LLM only Function bodies
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;
            current
showLoading 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 || '—'} &nbsp;|&nbsp; 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();
        modal
populateFields 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.keys
loadGeoJson 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 (con
decodeArc 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 = container
renderGraph 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 c
page 1 / 2next ›