/**
* Standalone HTML visualization of the knowledge graph.
*
* Uses vis.js (vis-network) for interactive graph visualization with:
* - ForceAtlas2 physics for natural clustering
* - Sidebar with node details on click
* - Community-based coloring
* - Search with dropdown
* - Legend with toggle per community
* - Node/edge caps for large graphs
*/
import type { KnowledgeGraph } from "./graph.ts";
const VIS_JS = "https://unpkg.com/vis-network@9.1.6/dist/vis-network.min.js";
const VIS_CSS = "https://unpkg.com/vis-network@9.1.6/dist/vis-network.min.css";
const MAX_VIZ_NODES = 1500;
const MAX_EDGES = 4000;
export function generateHtml(kg: KnowledgeGraph, title: string = "Mind Place"): string {
// Sort non-file nodes by degree, take top
const nonFileNodes = [...kg.nodes.values()]
.filter(n => n.type !== "file")
.map(n => ({ node: n, degree: kg.adjacency.get(n.id)?.size ?? 0 }))
.sort((a, b) => b.degree - a.degree);
const cappedNodes = nonFileNodes.slice(0, MAX_VIZ_NODES);
const cappedIds = new Set(cappedNodes.map(n => n.node.id));
// Colors from graphify's palette
const COLORS = [
"#4E79A7","#F28E2B","#E15759","#76B7B2","#59A14F",
"#EDC948","#B07AA1","#FF9DA7","#9C755F","#BAB0AC",
"#6A3D9A","#FF7F00","#33A02C","#1F78B4","#E31A1C",
"#FDBF6F","#A6CEE3","#B2DF8A","#FB9A99","#CAB2D6",
"#FFFF99","#B15928","#8DD3C7","#FFFFB3","#BEBADA",
];
// Build nodes
const nodes = cappedNodes.map(n => {
const node = n.node;
const comm = node.community ?? 0;
return {
id: node.id, label: node.label, color: COLORS[comm % COLORS.length],
size: Math.max(5, Math.min(25, Math.log2(n.degree + 2) * 8)),
font: { size: 10, color: "#ddd" },
title: `${node.label} (${node.type})
${node.sourceFile}${node.sourceLocation ? " " + node.sourceLocation : ""}
${n.degree} connections`,
community: comm,
sourceFile: node.sourceFile, fileType: node.type, degree: n.degree,
};
});
// Build edges (only between capped nodes)
const edges = [];
for (const edge of kg.edges) {
if (edges.length >= MAX_EDGES) break;
if (!cappedIds.has(edge.source) || !cappedIds.has(edge.target)) continue;
edges.push({
from: edge.source, to: edge.target,
title: `${edge.relation} [${edge.confidence}]`,
dashes: edge.confidence !== "EXTRACTED",
width: edge.confidence === "EXTRACTED" ? 1 : 0.5,
color: { color: edge.confidence === "EXTRACTED" ? "#555" : "#444", opacity: edge.confidence === "EXTRACTED" ? 0.4 : 0.25 },
});
}
// Legend
const legend = new Map();
for (const n of cappedNodes) {
const comm = n.node.community ?? 0;
const c = legend.get(comm) ?? { label: `Community ${comm}`, color: COLORS[comm % COLORS.length], count: 0 };
c.count++;
if (c.label === `Community ${comm}`) c.label = n.node.label;
legend.set(comm, c);
}
const legendArr = [...legend.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, 50);
const totalNodes = kg.nodes.size;
const shownInfo = cappedNodes.length < nonFileNodes.length
? `Showing ${cappedNodes.length} of ${nonFileNodes.length} nodes (top by connections)`
: `${cappedNodes.length} nodes`;
return `
${title} - Mind Place
`;
}