sunee3 / ui /graph_view.html
sagarmythos
DevAI Studio HCBH v3.1 - clean deploy, no binaries
df6cd5e
Raw
History Blame Contribute Delete
23.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>DevAI Studio — Code Graph</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Cytoscape.js core + Force-directed (cose-bilkent) + extras -->
<script src="https://unpkg.com/cytoscape@3.28.1/dist/cytoscape.min.js"></script>
<script src="https://unpkg.com/layout-base@2.0.1/layout-base.js"></script>
<script src="https://unpkg.com/cose-base@2.2.0/cose-base.js"></script>
<script src="https://unpkg.com/cytoscape-cose-bilkent@4.1.0/cytoscape-cose-bilkent.js"></script>
<style>
/* ============================================================
DevAI Studio v4 — Code Graph (Light Pink + Light Purple)
============================================================ */
:root {
--bg: #fff5fa; /* light pink page background */
--panel: #ffffff; /* sidebar card */
--panel-2: #faf3ff; /* nested section */
--border: rgba(197, 168, 255, 0.35);
--text: #4a3a5c; /* soft ink */
--muted: #8a7ba0;
--accent: #a97cff; /* light purple primary */
--accent-2: #ff8fc4; /* light pink secondary */
--danger: #ff6b95;
--ok: #7ed3a6;
--gradient: linear-gradient(135deg, #ff8fc4 0%, #b28cff 100%);
--gradient-soft: linear-gradient(135deg, #ffd1e8 0%, #e6d4ff 100%);
--shadow: 0 10px 24px rgba(169, 124, 255, 0.14);
}
* { box-sizing: border-box; }
html, body { margin:0; padding:0; height:100%;
background: linear-gradient(180deg, #fff5fa 0%, #f7f2ff 100%);
color: var(--text);
font-family: 'Inter', 'Poppins', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
#app { display:grid; grid-template-columns: 320px 1fr; height: 100vh; }
/* ---------- Sidebar ---------- */
#sidebar { background: var(--panel); border-right: 1px solid var(--border);
padding: 18px; overflow-y: auto; box-shadow: 4px 0 20px rgba(169, 124, 255, 0.08); }
#sidebar h1 {
font-size: 18px; margin: 0 0 4px; letter-spacing: .3px; font-weight: 800;
background: var(--gradient); -webkit-background-clip: text; background-clip: text; color: transparent;
}
#sidebar .sub { font-size: 12px; color: var(--muted); margin-bottom: 18px; }
.section { background: var(--panel-2); border:1px solid var(--border);
border-radius: 14px; padding: 14px; margin-bottom: 14px;
box-shadow: var(--shadow); }
.section h2 { font-size: 11px; text-transform: uppercase; letter-spacing: 1.2px;
color: var(--accent); margin: 0 0 10px; font-weight: 700; }
input[type="text"], input[type="search"], select {
width: 100%; background: #ffffff; color: var(--text);
border: 1px solid #ffc9e2; border-radius: 10px;
padding: 9px 12px; font-size: 13px; outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
input:focus, select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(169, 124, 255, 0.20);
}
.row { display:flex; gap:8px; align-items:center; }
.chip { display:inline-flex; align-items:center; gap:6px;
padding: 5px 10px; border-radius: 999px; background: #ffffff;
border: 1px solid var(--border); font-size: 12px; cursor: pointer;
margin: 3px 4px 3px 0; user-select: none;
color: var(--text); transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(169,124,255,0.08);
}
.chip:hover { border-color: var(--accent); }
.chip input { accent-color: var(--accent); }
.chip.off { opacity: 0.45; }
button { background: var(--gradient); color: #ffffff; border: 0; border-radius: 999px;
padding: 8px 16px; font-weight: 600; font-size: 12px; cursor: pointer;
box-shadow: 0 4px 12px rgba(226,122,189,0.28);
transition: transform 0.15s ease, filter 0.2s ease, box-shadow 0.2s ease; }
button.ghost { background: #ece1ff; color: #8a5cf0; border: 1px solid #d9c4ff; box-shadow: none; }
button:hover { filter: brightness(1.06); transform: translateY(-1px); }
.stat { display:flex; justify-content: space-between; font-size: 12px; padding: 4px 0; }
.stat b { color: var(--accent); } .stat span { color: var(--muted); }
.legend-row { display:flex; align-items:center; gap:8px; font-size:12px; margin: 4px 0; }
.swatch { width:12px; height:12px; border-radius:4px; display:inline-block; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
/* ---------- Graph canvas ---------- */
#main { position: relative; }
#cy { width: 100%; height: 100%;
background: radial-gradient(circle at 50% 40%, #ffffff 0%, #faeaf5 60%, #efe3ff 100%); }
/* Floating top bar with quick actions */
#topbar { position: absolute; top: 14px; right: 14px; display:flex; gap:8px; z-index: 5; }
#topbar button { box-shadow: 0 6px 18px rgba(169,124,255,.28); }
/* Node inspector */
#inspector { position: absolute; bottom: 16px; left: 16px; max-width: 360px;
background: rgba(255, 255, 255, 0.96); border: 1px solid var(--border);
border-radius: 14px; padding: 14px 16px; font-size: 12px; line-height: 1.55;
color: var(--text); box-shadow: var(--shadow);
backdrop-filter: blur(8px); display:none; }
#inspector h3 {
margin: 0 0 8px; font-size: 14px;
color: var(--accent); font-weight: 700;
}
#inspector code { background: #fff5fa; padding: 2px 6px; border-radius: 6px;
color: #e754a0; font-size: 11px; border: 1px solid #ffc9e2; }
/* Toast */
#toast { position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
background: #ffffff; border: 1px solid var(--border); border-radius: 12px;
padding: 10px 18px; font-size: 12px; opacity: 0; transition: opacity .25s ease;
pointer-events: none; z-index: 10; color: var(--text);
box-shadow: var(--shadow); font-weight: 500; }
#toast.show { opacity: 1; }
.pill { font-size: 10px; padding: 3px 8px; border-radius: 6px;
background: var(--gradient-soft); border: 1px solid var(--border); color: var(--accent); font-weight: 600; }
</style>
</head>
<body>
<div id="app">
<!-- ============== Sidebar ============== -->
<aside id="sidebar">
<h1>🧭 Code Graph</h1>
<div class="sub">DevAI Studio — architecture explorer</div>
<!-- Data source -->
<div class="section">
<h2>Data source</h2>
<div class="row" style="margin-bottom:6px;">
<input id="dataUrl" type="text" placeholder="/graph.json" value="./graph.json" />
</div>
<div class="row">
<button id="btnLoad">Load</button>
<button class="ghost" id="btnFile">Open file…</button>
<input id="fileInput" type="file" accept="application/json" style="display:none" />
</div>
</div>
<!-- Search -->
<div class="section">
<h2>Search</h2>
<input id="search" type="search" placeholder="Type a function or class name…" autocomplete="off" />
<div class="sub" style="margin-top:6px;">
Press <span class="pill">Enter</span> to focus the first match.
</div>
</div>
<!-- Module filter -->
<div class="section">
<h2>Filter by module / cluster</h2>
<div id="clusterChips"></div>
<div class="row" style="margin-top:8px;">
<button class="ghost" id="btnAllClusters">All</button>
<button class="ghost" id="btnNoneClusters">None</button>
</div>
</div>
<!-- Edge filter -->
<div class="section">
<h2>Edge types</h2>
<label class="chip"><input type="checkbox" data-edge="calls" checked /> calls</label>
<label class="chip"><input type="checkbox" data-edge="imports" checked /> imports</label>
<label class="chip"><input type="checkbox" data-edge="inherits" checked /> inherits</label>
<label class="chip"><input type="checkbox" data-edge="contains" /> contains</label>
</div>
<!-- Layout + perf -->
<div class="section">
<h2>Layout & performance</h2>
<div class="row" style="margin-bottom: 8px;">
<select id="layoutPick">
<option value="cose-bilkent" selected>Force-directed (cose-bilkent)</option>
<option value="cose">Cose (built-in)</option>
<option value="grid">Grid</option>
<option value="circle">Circle</option>
<option value="concentric">Concentric (by degree)</option>
<option value="breadthfirst">Breadth-first</option>
</select>
</div>
<label class="chip"><input id="hqRender" type="checkbox" checked /> High-Quality rendering (1000+ nodes)</label>
<label class="chip"><input id="showLabels" type="checkbox" checked /> Show labels</label>
<div class="row" style="margin-top:8px;">
<button id="btnRelayout">Re-run layout</button>
<button class="ghost" id="btnFit">Fit</button>
</div>
</div>
<!-- Stats -->
<div class="section">
<h2>Stats</h2>
<div class="stat"><span>Nodes</span><b id="nNodes">0</b></div>
<div class="stat"><span>Edges</span><b id="nEdges">0</b></div>
<div class="stat"><span>Visible nodes</span><b id="nVisible">0</b></div>
<div class="stat"><span>Files scanned</span><b id="nFiles">0</b></div>
<div class="stat"><span>Parse errors</span><b id="nErrors">0</b></div>
</div>
<!-- Legend -->
<div class="section">
<h2>Legend</h2>
<div class="legend-row"><span class="swatch" style="background:#b28cff"></span> function</div>
<div class="legend-row"><span class="swatch" style="background:#ff8fc4"></span> method</div>
<div class="legend-row"><span class="swatch" style="background:#a97cff"></span> class</div>
<div class="legend-row"><span class="swatch" style="background:#e754a0"></span> module</div>
</div>
</aside>
<!-- ============== Main canvas ============== -->
<main id="main">
<div id="topbar">
<button class="ghost" id="btnZoomIn"></button>
<button class="ghost" id="btnZoomOut"></button>
<button class="ghost" id="btnReset">Reset view</button>
</div>
<div id="cy"></div>
<div id="inspector"></div>
<div id="toast">Ready</div>
</main>
</div>
<script>
/* =========================================================================
* DevAI Studio — Code Graph viewer
* Cytoscape.js + cose-bilkent force-directed layout
* ========================================================================= */
if (typeof cytoscape !== "undefined" &&
typeof cytoscapeCoseBilkent !== "undefined") {
cytoscape.use(cytoscapeCoseBilkent);
}
const $ = (sel) => document.querySelector(sel);
const toast = (msg, ms = 1600) => {
const t = $("#toast"); t.textContent = msg; t.classList.add("show");
clearTimeout(toast._t); toast._t = setTimeout(() => t.classList.remove("show"), ms);
};
/* ---------- Color palette by kind (light pink + light purple pastel) ---------- */
const KIND_COLORS = {
function: "#b28cff", /* light purple */
method: "#ff8fc4", /* light pink */
class: "#a97cff", /* medium purple */
module: "#e754a0", /* deep pink */
};
const EDGE_COLORS = {
calls: "#b28cff",
imports: "#c3a6ff",
inherits: "#ff8fc4",
contains: "#d9c4ff",
};
/* ---------- Cytoscape stylesheet ---------- */
const baseStyle = [
{
selector: "node",
style: {
"background-color": (ele) => KIND_COLORS[ele.data("kind")] || "#b28cff",
"label": "data(label)",
"color": "#4a3a5c",
"font-size": 10,
"font-weight": 600,
"text-outline-color": "#ffffff",
"text-outline-width": 2,
"text-valign": "center",
"text-halign": "center",
"width": (ele) => 14 + Math.min(28, (ele.data("degree") || 0) * 2),
"height": (ele) => 14 + Math.min(28, (ele.data("degree") || 0) * 2),
"border-width": 1.5,
"border-color": "#ffffff",
},
},
{
selector: 'node[kind="class"]',
style: { "shape": "round-rectangle" },
},
{
selector: 'node[kind="module"]',
style: { "shape": "diamond", "width": 20, "height": 20, "opacity": 0.7 },
},
{
selector: "edge",
style: {
"width": 1.4,
"line-color": (ele) => EDGE_COLORS[ele.data("type")] || "#d9c4ff",
"target-arrow-color": (ele) => EDGE_COLORS[ele.data("type")] || "#d9c4ff",
"target-arrow-shape": "triangle",
"arrow-scale": 0.9,
"curve-style": "bezier",
"opacity": 0.65,
},
},
{
selector: 'edge[type="imports"]',
style: { "line-style": "dashed", "opacity": 0.4 },
},
{
selector: 'edge[type="contains"]',
style: { "line-style": "dotted", "opacity": 0.35 },
},
/* highlight states */
{
selector: ".hl",
style: {
"background-color": "#ff6fb5",
"border-color": "#e754a0",
"border-width": 3,
"z-index": 999,
},
},
{
selector: ".hl-edge",
style: {
"line-color": "#ff6fb5",
"target-arrow-color": "#ff6fb5",
"width": 2.6,
"opacity": 1,
},
},
{
selector: ".dim",
style: { "opacity": 0.08 },
},
{
selector: ".dim-edge",
style: { "opacity": 0.04 },
},
];
/* ---------- Global state ---------- */
let cy = null;
let RAW = null; // last loaded payload
const state = {
clusters: new Set(), // visible clusters
edgeTypes: new Set(["calls", "imports", "inherits"]),
hq: true,
showLabels: true,
layoutName: "cose-bilkent",
};
/* ---------- Layout configs ---------- */
function layoutOptions(name, nodeCount) {
const hq = state.hq;
if (name === "cose-bilkent") {
return {
name: "cose-bilkent",
animate: hq ? "end" : false,
randomize: true,
nodeRepulsion: hq ? 8000 : 4500,
idealEdgeLength: hq ? 90 : 60,
edgeElasticity: 0.45,
gravity: 0.25,
numIter: hq ? 2500 : 1200,
tile: true,
nodeDimensionsIncludeLabels: hq,
fit: true,
padding: 30,
};
}
if (name === "concentric") {
return { name, concentric: (n) => n.degree(), levelWidth: () => 4, animate: false, padding: 30 };
}
if (name === "breadthfirst") {
return { name, directed: true, padding: 30, animate: false };
}
return { name, animate: false, padding: 30 };
}
/* ---------- Build / rebuild graph ---------- */
function buildCytoscape(payload) {
RAW = payload;
const els = payload.elements || { nodes: [], edges: [] };
// populate cluster chips
const clusters = (payload.meta && payload.meta.clusters) ||
[...new Set(els.nodes.map(n => n.data.cluster || "(root)"))].sort();
state.clusters = new Set(clusters);
renderClusterChips(clusters);
// stats
$("#nNodes").textContent = (payload.meta && payload.meta.node_count) ?? els.nodes.length;
$("#nEdges").textContent = (payload.meta && payload.meta.edge_count) ?? els.edges.length;
$("#nFiles").textContent = (payload.meta && payload.meta.python_files_scanned) ?? "—";
$("#nErrors").textContent = (payload.meta && (payload.meta.parse_errors || []).length) || 0;
if (cy) { cy.destroy(); cy = null; }
cy = cytoscape({
container: document.getElementById("cy"),
elements: [...els.nodes, ...els.edges],
style: baseStyle,
minZoom: 0.05,
maxZoom: 4.0,
wheelSensitivity: 0.25,
pixelRatio: state.hq ? "auto" : 1,
textureOnViewport: !state.hq, // HQ off when >1000 nodes for perf
motionBlur: state.hq,
hideEdgesOnViewport: !state.hq && els.nodes.length > 1000,
hideLabelsOnViewport: !state.hq && els.nodes.length > 1000,
});
// Force-directed first
runLayout();
applyFilters();
attachInteractions();
toast(`Loaded ${els.nodes.length} nodes / ${els.edges.length} edges`);
}
function runLayout() {
if (!cy) return;
const n = cy.nodes().length;
const opts = layoutOptions(state.layoutName, n);
try {
cy.layout(opts).run();
} catch (e) {
console.warn("layout failed, falling back to cose:", e);
cy.layout({ name: "cose", animate: false, padding: 30 }).run();
}
}
/* ---------- Cluster filter UI ---------- */
function renderClusterChips(clusters) {
const root = $("#clusterChips");
root.innerHTML = "";
clusters.forEach((c) => {
const id = `cl_${c.replace(/[^a-z0-9]/gi, "_")}`;
const wrap = document.createElement("label");
wrap.className = "chip";
wrap.innerHTML = `<input type="checkbox" id="${id}" data-cluster="${c}" checked /> ${c}`;
root.appendChild(wrap);
wrap.querySelector("input").addEventListener("change", (e) => {
if (e.target.checked) state.clusters.add(c);
else state.clusters.delete(c);
applyFilters();
});
});
}
/* ---------- Apply visibility filters ---------- */
function applyFilters() {
if (!cy) return;
cy.batch(() => {
cy.nodes().forEach((n) => {
const cluster = n.data("cluster") || "(root)";
const visible = state.clusters.has(cluster);
n.style("display", visible ? "element" : "none");
});
cy.edges().forEach((e) => {
const t = e.data("type") || "calls";
const typeOk = state.edgeTypes.has(t);
const srcVis = e.source().visible();
const tgtVis = e.target().visible();
e.style("display", (typeOk && srcVis && tgtVis) ? "element" : "none");
});
cy.style().selector("node").style({
"label": state.showLabels ? "data(label)" : "",
}).update();
});
$("#nVisible").textContent = cy.nodes(":visible").length;
}
/* ---------- Interactions ---------- */
function attachInteractions() {
cy.on("tap", "node", (evt) => {
const n = evt.target;
highlightNode(n);
showInspector(n);
});
cy.on("tap", (evt) => {
if (evt.target === cy) { clearHighlight(); hideInspector(); }
});
}
function clearHighlight() {
if (!cy) return;
cy.elements().removeClass("hl hl-edge dim dim-edge");
}
function highlightNode(n) {
clearHighlight();
const neighborhood = n.closedNeighborhood();
cy.elements().not(neighborhood).addClass("dim");
cy.edges().not(neighborhood.edgesWith(neighborhood)).addClass("dim-edge");
n.addClass("hl");
neighborhood.edges().addClass("hl-edge");
}
function showInspector(n) {
const d = n.data();
const incoming = n.incomers("edge").length;
const outgoing = n.outgoers("edge").length;
$("#inspector").innerHTML = `
<h3>${escapeHtml(d.label)}</h3>
<div><span class="pill">${d.kind || "?"}</span>
<span class="pill">${escapeHtml(d.cluster || "(root)")}</span></div>
<div style="margin-top:6px;">
<code>${escapeHtml(d.file || "")}:${d.lineno || 0}</code>
</div>
<div style="margin-top:6px;">
qualname: <code>${escapeHtml(d.qualname || d.label)}</code>
</div>
<div style="margin-top:6px; color: var(--muted);">
${incoming} incoming · ↓ ${outgoing} outgoing
</div>
`;
$("#inspector").style.display = "block";
}
function hideInspector() { $("#inspector").style.display = "none"; }
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;")
.replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
/* ---------- Search ---------- */
function searchNodes(query) {
if (!cy) return [];
const q = query.trim().toLowerCase();
if (!q) { clearHighlight(); return []; }
const matches = cy.nodes().filter((n) => {
const lbl = (n.data("label") || "").toLowerCase();
const qn = (n.data("qualname") || "").toLowerCase();
return lbl.includes(q) || qn.includes(q);
});
clearHighlight();
cy.elements().addClass("dim");
cy.edges().addClass("dim-edge");
matches.removeClass("dim").addClass("hl");
matches.connectedEdges().removeClass("dim-edge");
return matches;
}
/* ---------- Data loading ---------- */
async function loadFromUrl(url) {
try {
const r = await fetch(url, { cache: "no-store" });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const json = await r.json();
buildCytoscape(json);
} catch (e) {
console.error(e);
toast(`Failed to load: ${e.message}`, 3000);
}
}
function loadFromFile(file) {
const reader = new FileReader();
reader.onload = () => {
try { buildCytoscape(JSON.parse(reader.result)); }
catch (e) { toast("Invalid JSON file", 3000); }
};
reader.readAsText(file);
}
/* ---------- Event wiring ---------- */
$("#btnLoad").addEventListener("click", () => loadFromUrl($("#dataUrl").value || "./graph.json"));
$("#btnFile").addEventListener("click", () => $("#fileInput").click());
$("#fileInput").addEventListener("change", (e) => {
if (e.target.files && e.target.files[0]) loadFromFile(e.target.files[0]);
});
$("#search").addEventListener("input", (e) => searchNodes(e.target.value));
$("#search").addEventListener("keydown", (e) => {
if (e.key === "Enter") {
const matches = searchNodes(e.target.value);
if (matches && matches.length) {
cy.animate({ fit: { eles: matches, padding: 80 } }, { duration: 400 });
toast(`${matches.length} match${matches.length === 1 ? "" : "es"}`);
} else { toast("No matches"); }
}
});
document.querySelectorAll('input[data-edge]').forEach((cb) =>
cb.addEventListener("change", (e) => {
const t = e.target.dataset.edge;
if (e.target.checked) state.edgeTypes.add(t);
else state.edgeTypes.delete(t);
applyFilters();
})
);
$("#btnAllClusters").addEventListener("click", () => {
document.querySelectorAll('#clusterChips input[type=checkbox]').forEach(cb => {
cb.checked = true; state.clusters.add(cb.dataset.cluster);
});
applyFilters();
});
$("#btnNoneClusters").addEventListener("click", () => {
document.querySelectorAll('#clusterChips input[type=checkbox]').forEach(cb => {
cb.checked = false; state.clusters.delete(cb.dataset.cluster);
});
applyFilters();
});
$("#layoutPick").addEventListener("change", (e) => {
state.layoutName = e.target.value; runLayout();
});
$("#hqRender").addEventListener("change", (e) => {
state.hq = e.target.checked;
if (RAW) buildCytoscape(RAW); // rebuild — pixel ratio / textureOnViewport need re-init
});
$("#showLabels").addEventListener("change", (e) => {
state.showLabels = e.target.checked; applyFilters();
});
$("#btnRelayout").addEventListener("click", runLayout);
$("#btnFit").addEventListener("click", () => cy && cy.animate({ fit: { padding: 40 } }, { duration: 300 }));
$("#btnZoomIn").addEventListener("click", () => cy && cy.zoom({ level: cy.zoom() * 1.25, renderedPosition: { x: cy.width()/2, y: cy.height()/2 } }));
$("#btnZoomOut").addEventListener("click", () => cy && cy.zoom({ level: cy.zoom() * 0.8, renderedPosition: { x: cy.width()/2, y: cy.height()/2 } }));
$("#btnReset").addEventListener("click", () => cy && (cy.zoom(1), cy.center(), cy.animate({ fit: { padding: 40 } }, { duration: 300 })));
/* ---------- Auto-load on open ---------- */
window.addEventListener("DOMContentLoaded", () => {
// Allow ?src=... query-string override
const u = new URL(window.location.href);
const src = u.searchParams.get("src") || "./graph.json";
$("#dataUrl").value = src;
loadFromUrl(src);
});
</script>
</body>
</html>