Spaces:
Sleeping
Sleeping
| <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, "&").replace(/</g, "<") | |
| .replace(/>/g, ">").replace(/"/g, """); | |
| } | |
| /* ---------- 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> | |