"use strict"; const PAIR_COLORS = { Chemical: "#60a5fa", Disease: "#f87171", Virus: "#a78bfa", Gene: "#34d399", }; const ALL_TYPES = ["Chemical", "Disease", "Virus", "Gene"]; const state = { lastResponse: null, selectedEdgeId: null, network: null, filters: { pairTypes: new Set(), // empty = show all minConfidence: 0.5, granularity: "abstract", }, }; // ---------- DOM helpers ---------- const $ = (id) => document.getElementById(id); function show(el, on = true) { if (typeof el === "string") el = $(el); if (!el) return; el.classList.toggle("hidden", !on); } function escapeHtml(s) { return s .replace(/&/g, "&") .replace(//g, ">"); } // ---------- Pair type filter UI ---------- function buildPairFilters() { const pairs = []; for (let i = 0; i < ALL_TYPES.length; i++) { for (let j = i; j < ALL_TYPES.length; j++) { pairs.push(`${ALL_TYPES[i]}-${ALL_TYPES[j]}`); } } const wrap = $("pairFilters"); wrap.innerHTML = ""; pairs.forEach((p) => { const id = `pf_${p}`; const label = document.createElement("label"); label.className = "flex items-center gap-1 text-slate-300"; label.innerHTML = `${p}`; wrap.appendChild(label); label.querySelector("input").addEventListener("change", () => { if (state.lastResponse) renderGraph(state.lastResponse); }); }); } function selectedPairTypes() { const set = new Set(); document.querySelectorAll("#pairFilters input[type=checkbox]").forEach((cb) => { if (cb.checked) set.add(cb.parentNode.querySelector("span").textContent); }); return set; } // ---------- Analyze ---------- async function analyze() { const text = $("txtInput").value.trim(); if (!text) return; const gran = document.querySelector('input[name="gran"]:checked').value; state.filters.granularity = gran; state.filters.minConfidence = parseFloat($("sldConf").value); show("overlay", true); $("ovText").textContent = "Analyzing..."; try { const res = await fetch("/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, granularity: gran, min_confidence: state.filters.minConfidence, }), }); if (!res.ok) { const t = await res.text(); alert(`Analyze failed: ${res.status} ${t}`); return; } const data = await res.json(); state.lastResponse = data; updateStats(data.stats); renderGraph(data); show("evidencePanel", false); } catch (e) { alert(`Error: ${e}`); } finally { show("overlay", false); } } function updateStats(stats) { $("stAb").textContent = stats.total_abstracts; $("stSent").textContent = stats.total_sentences; $("stEnt").textContent = stats.total_entities; $("stEl").textContent = stats.elapsed_seconds; show("statsBox", true); } // ---------- Graph ---------- function renderGraph(resp) { const allowed = selectedPairTypes(); const edges = resp.edges.filter((e) => allowed.has(e.pair_type)); const keepKeys = new Set(); edges.forEach((e) => { keepKeys.add(e.source); keepKeys.add(e.target); }); const nodes = resp.nodes.filter((n) => keepKeys.has(n.id)); const maxCount = Math.max(1, ...nodes.map((n) => n.count)); const maxWeight = Math.max(1, ...edges.map((e) => e.weight)); const vnNodes = nodes.map((n) => ({ id: n.id, label: n.label, title: `${n.type} · count ${n.count}`, color: PAIR_COLORS[n.type], shape: "dot", size: 10 + 25 * (n.count / maxCount), font: { color: "#e2e8f0", size: 13 }, })); const vnEdges = edges.map((e) => ({ id: e.id, from: e.source, to: e.target, width: 1 + 6 * (e.weight / maxWeight), title: `${e.pair_type} · weight ${e.weight}`, color: { color: "rgba(148,163,184,0.45)", highlight: "#a78bfa" }, })); show("placeholder", nodes.length === 0); const data = { nodes: new vis.DataSet(vnNodes), edges: new vis.DataSet(vnEdges), }; const options = { physics: { stabilization: { iterations: 150 }, barnesHut: { gravitationalConstant: -8000 } }, interaction: { hover: true, tooltipDelay: 150 }, nodes: { borderWidth: 0 }, edges: { smooth: { type: "continuous" } }, }; if (state.network) state.network.destroy(); state.network = new vis.Network($("graph"), data, options); state.network.on("selectNode", (params) => { if (!params.nodes.length) return; const id = params.nodes[0]; const connected = new Set(state.network.getConnectedNodes(id)); connected.add(id); const updates = vnNodes.map((n) => ({ id: n.id, color: connected.has(n.id) ? PAIR_COLORS[nodes.find((x) => x.id === n.id).type] : "rgba(100,116,139,0.4)", })); data.nodes.update(updates); }); state.network.on("deselectNode", () => { const reset = vnNodes.map((n) => ({ id: n.id, color: PAIR_COLORS[nodes.find((x) => x.id === n.id).type] })); data.nodes.update(reset); }); state.network.on("selectEdge", (params) => { if (!params.edges.length) return; renderEvidence(params.edges[0]); }); } // ---------- Evidence ---------- function renderEvidence(edgeId) { state.selectedEdgeId = edgeId; const items = (state.lastResponse?.evidence || {})[edgeId] || []; const body = $("evidenceBody"); body.innerHTML = ""; if (!items.length) { body.innerHTML = `