diff options
| author | Agando <roduccomm@gmail.com> | 2026-11-09 15:36:23 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-11-09 15:36:23 +0100 |
| commit | a065a4c30be748b0c8fc6dd0b5ee1dc4d3de4b1c (patch) | |
| tree | 8a84adabfccee9994937cd71888170ab205b9361 /downloads/memgraph.html | |
| parent | a46ebee30cfbbac86abb45068411bc28e1c65503 (diff) | |
The daily collector and the feedback pipeline had left 57 modified files uncommitted (skills,
modules-index, CHANGELOG, feedback decisions, debug counters). The public mirror publishes git
HEAD, so without this the published repo stayed behind the tree - and it blocked the publisher
outright once uncommitted subtrees started failing loudly.
Diffstat (limited to 'downloads/memgraph.html')
| -rw-r--r-- | downloads/memgraph.html | 1007 |
1 files changed, 1007 insertions, 0 deletions
diff --git a/downloads/memgraph.html b/downloads/memgraph.html new file mode 100644 index 0000000..cd37848 --- /dev/null +++ b/downloads/memgraph.html @@ -0,0 +1,1007 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<meta name="color-scheme" content="dark"> +<title>π§ Memory Graph Explorer β WebX11 newOS4</title> +<style> +*{box-sizing:border-box;margin:0;padding:0} +html,body{height:100%;overflow:hidden;background:#070b14;color:#e2e8f0;font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif} +#cv{position:fixed;inset:0;cursor:grab;touch-action:none} +#cv.grabbing{cursor:grabbing} +.panel{position:fixed;background:rgba(13,20,36,.78);border:1px solid rgba(148,163,184,.15);border-radius:14px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 12px 40px rgba(0,0,0,.45)} +::-webkit-scrollbar{width:8px;height:8px} +::-webkit-scrollbar-thumb{background:rgba(100,116,139,.35);border-radius:8px} +::-webkit-scrollbar-track{background:transparent} + +/* ββ Top bar ββ */ +#topbar{top:10px;left:10px;right:10px;height:52px;display:flex;align-items:center;gap:10px;padding:0 12px;z-index:20;min-width:0} +#brand{display:flex;align-items:center;gap:9px;font-weight:700;font-size:15px;white-space:nowrap} +#brand .sub{font-size:10px;font-weight:600;color:#7c8db0;letter-spacing:.08em;text-transform:uppercase} +#srcChip{font-size:11px;color:#67e8f9;border:1px solid rgba(34,211,238,.3);background:rgba(8,47,73,.45);padding:4px 10px;border-radius:999px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px} +#searchWrap{flex:1;position:relative;min-width:120px;max-width:430px} +#search{width:100%;height:32px;border-radius:9px;border:1px solid rgba(148,163,184,.18);background:rgba(2,6,17,.6);color:#e2e8f0;padding:0 70px 0 30px;font-size:13px;outline:none} +#search:focus{border-color:rgba(34,211,238,.55);box-shadow:0 0 0 3px rgba(34,211,238,.12)} +#searchIcon{position:absolute;left:9px;top:8px;font-size:13px;opacity:.6;pointer-events:none} +#searchCount{position:absolute;right:8px;top:7px;font-size:11px;color:#94a3b8;background:rgba(30,41,59,.7);border-radius:6px;padding:2px 7px;display:none} +#tbBtns{display:flex;gap:6px;margin-left:auto} +.btn{height:32px;min-width:32px;padding:0 9px;border-radius:8px;border:1px solid rgba(148,163,184,.18);background:rgba(30,41,59,.6);color:#cbd5e1;font-size:13px;cursor:pointer;transition:.15s;display:inline-flex;align-items:center;justify-content:center;gap:5px;white-space:nowrap;user-select:none} +.btn:hover{background:rgba(51,65,85,.85);color:#fff;border-color:rgba(34,211,238,.4)} +.btn.act{border-color:rgba(34,211,238,.55);color:#67e8f9} + +/* ββ Sidebar ββ */ +#sidebar{top:72px;left:10px;bottom:10px;width:322px;padding:14px;overflow-y:auto;overflow-x:hidden;z-index:15;transition:transform .25s ease,opacity .25s ease} +#sidebar.hidden{transform:translateX(-115%);opacity:0;pointer-events:none} +.sec{margin-bottom:16px} +.sec h3{font-size:10.5px;letter-spacing:.09em;text-transform:uppercase;color:#7c8db0;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;gap:8px} +.chip{display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;border:1px solid rgba(148,163,184,.16);background:rgba(30,41,59,.5);font-size:12px;color:#cbd5e1;cursor:pointer;margin:0 6px 6px 0;transition:.15s;user-select:none} +.chip .dot{width:9px;height:9px;border-radius:50%;flex-shrink:0} +.chip:hover{border-color:rgba(34,211,238,.4)} +.chip.off{opacity:.35;filter:saturate(.2)} +.statgrid{display:grid;grid-template-columns:1fr 1fr;gap:8px} +.stat{background:rgba(30,41,59,.45);border:1px solid rgba(148,163,184,.12);border-radius:10px;padding:8px 10px} +.stat .v{font-size:17px;font-weight:700;color:#e2e8f0} +.stat .k{font-size:10px;color:#7c8db0;text-transform:uppercase;letter-spacing:.07em;margin-top:1px} +.row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;cursor:pointer;font-size:12.5px;color:#cbd5e1;min-width:0} +.row:hover{background:rgba(34,211,238,.09)} +.row .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} +.row .lab{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.row .val{font-size:11px;color:#7c8db0;flex-shrink:0} +.lrow{display:flex;align-items:center;gap:8px;font-size:11.5px;color:#94a3b8;padding:3px 4px} +.lrow .line{width:16px;height:3px;border-radius:2px;flex-shrink:0} +.lrow .n{margin-left:auto;color:#7c8db0} +.mini{font-size:11.5px;color:#94a3b8;line-height:1.55} +.mini code{background:rgba(2,6,17,.7);border:1px solid rgba(148,163,184,.14);border-radius:5px;padding:1px 5px;font-size:10.5px;color:#67e8f9;font-family:ui-monospace,Menlo,Consolas,monospace} +#foot{display:flex;gap:6px;margin-top:4px} +#foot .btn{flex:1;font-size:12px} + +/* ββ Details panel ββ */ +#details{top:72px;right:10px;width:332px;max-height:calc(100% - 90px);padding:14px;overflow-y:auto;z-index:16;display:none} +#details.open{display:block} +#dHead{display:flex;align-items:flex-start;gap:10px;margin-bottom:10px} +#dTitle{font-size:15px;font-weight:700;line-height:1.3;word-break:break-word} +.badge{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;padding:2px 8px;border-radius:999px;border:1px solid;font-weight:600;margin:4px 4px 0 0} +.kv{display:flex;gap:8px;font-size:12px;padding:4px 0;border-bottom:1px dashed rgba(148,163,184,.1)} +.kv .k{color:#7c8db0;width:86px;flex-shrink:0} +.kv .v{color:#e2e8f0;word-break:break-all} +pre.meta{background:rgba(2,6,17,.75);border:1px solid rgba(148,163,184,.14);border-radius:8px;padding:8px;font-size:11px;color:#a5b4fc;white-space:pre-wrap;word-break:break-word;margin-top:8px;font-family:ui-monospace,Menlo,Consolas,monospace} +.edgeRow{display:flex;align-items:center;gap:6px;font-size:11.5px;padding:5px 7px;border-radius:7px;cursor:pointer;color:#cbd5e1;min-width:0} +.edgeRow:hover{background:rgba(34,211,238,.09)} +.edgeRow .arrow{color:#7c8db0;flex-shrink:0} +.edgeRow .etype{font-size:10px;color:#67e8f9;background:rgba(8,47,73,.5);border-radius:5px;padding:1px 6px;flex-shrink:0} +.edgeRow .lab{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1} +.edgeRow .w{color:#7c8db0;font-size:10.5px;flex-shrink:0} + +/* ββ tooltip / toast / overlay ββ */ +#tooltip{position:fixed;pointer-events:none;background:rgba(2,6,17,.94);border:1px solid rgba(148,163,184,.28);border-radius:9px;padding:8px 10px;font-size:12px;z-index:50;display:none;max-width:280px;box-shadow:0 8px 24px rgba(0,0,0,.5);line-height:1.45} +#tooltip b{color:#fff} +#tooltip .t{font-size:10.5px;color:#7c8db0} +#toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%) translateY(20px);background:rgba(2,6,17,.94);border:1px solid rgba(34,211,238,.35);color:#e2e8f0;padding:9px 16px;border-radius:10px;font-size:13px;opacity:0;transition:.25s;z-index:60;pointer-events:none;max-width:88vw;text-align:center} +#toast.show{opacity:1;transform:translateX(-50%) translateY(0)} +#overlay{position:fixed;inset:0;background:rgba(4,7,14,.85);display:flex;align-items:center;justify-content:center;z-index:100;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)} +#overlay.hide{display:none} +.card{background:rgba(13,20,36,.95);border:1px solid rgba(148,163,184,.2);border-radius:16px;padding:28px 30px;max-width:460px;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.6)} +.spinner{width:36px;height:36px;border:3px solid rgba(148,163,184,.2);border-top-color:#22d3ee;border-radius:50%;margin:0 auto 16px;animation:spin .9s linear infinite} +@keyframes spin{to{transform:rotate(360deg)}} +.card h2{font-size:17px;margin-bottom:8px} +.card p{font-size:12.5px;color:#94a3b8;line-height:1.6} +.card .btn{margin-top:14px} +@media (max-width:900px){ + #sidebar{width:min(84vw,322px)} + #details{width:min(86vw,332px)} + #brand .sub{display:none} + #srcChip{display:none} +} +</style> +</head> +<body> +<canvas id="cv"></canvas> + +<header id="topbar" class="panel"> + <div id="brand"><span>π§ </span><div>Memory Graph<div class="sub">WebX11 newOS4</div></div></div> + <span id="srcChip" title="Data source">β¦</span> + <div id="searchWrap"> + <span id="searchIcon">π</span> + <input id="search" type="text" placeholder="Search nodesβ¦ ( / )" aria-label="Search nodes" autocomplete="off"> + <span id="searchCount"></span> + </div> + <div id="tbBtns"> + <button class="btn" id="btnSidebar" title="Toggle sidebar">β°</button> + <button class="btn" id="btnFit" title="Fit view (F)">βΆ</button> + <button class="btn" id="btnZoomOut" title="Zoom out">β</button> + <button class="btn" id="btnZoomIn" title="Zoom in">+</button> + <button class="btn" id="btnPause" title="Pause / resume physics">βΈ</button> + <button class="btn" id="btnRefresh" title="Reload live graph data">β³</button> + <button class="btn" id="btnPNG" title="Export PNG">πΌοΈ</button> + <button class="btn" id="btnJSON" title="Export JSON (like memgraph-export)">π</button> + </div> +</header> + +<aside id="sidebar" class="panel"></aside> +<div id="details" class="panel"></div> +<div id="tooltip"></div> +<div id="toast"></div> + +<div id="overlay"> + <div class="card"> + <div class="spinner" id="spin"></div> + <h2 id="ovTitle">Loading Memory Graphβ¦</h2> + <p id="ovText">Locating graph data in the WebX11 newOS4 VFSβ¦</p> + <button class="btn" id="ovRetry" style="display:none" onclick="location.reload()">β» Retry</button> + </div> +</div> + +<script> +'use strict'; +/* βββββββββββββββ Memory Graph Explorer βββββββββββββββ + Reads the LIVE WebX11 newOS4 memory graph: + 1. Live MemoryGraphModule (if opened inside newOS4, e.g. preview /memgraph.html) + 2. localStorage 'webx11_filesystem_v2' (gzip+base64 compressed VFS) + 3. localStorage legacy key / IndexedDB fallback + Files: /.memory_graph/{nodes,edges,config}.json or export.json + 100% client-side, no dependencies. */ + +const $ = s => document.querySelector(s); +const esc = s => String(s === undefined || s === null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + +const TYPE_COLORS = { + session:'#f59e0b', task:'#fb7185', file:'#22d3ee', tool:'#a78bfa', + concept:'#34d399', type:'#38bdf8', fact:'#fbbf24', pattern:'#f472b6', + user:'#e879f9', skill:'#c084fc' +}; +const FALLBACK_COLORS = ['#94a3b8','#fca5a5','#86efac','#fdba74','#93c5fd','#d8b4fe','#f9a8d4','#67e8f9']; +const EDGE_COLORS = { + accessed:'#22d3ee', used:'#a78bfa', failed:'#ef4444', executed:'#fb7185', + involves:'#34d399', has_type:'#38bdf8', maps_to:'#2dd4bf', tagged_as:'#5eead4', + has_fact:'#fbbf24', learned:'#f472b6' +}; +const FALLBACK_EDGE = '#64748b'; +function typeColor(t){ if (TYPE_COLORS[t]) return TYPE_COLORS[t]; return FALLBACK_COLORS[(t||'?').charCodeAt(0) % FALLBACK_COLORS.length]; } +function edgeColor(t){ return EDGE_COLORS[t] || FALLBACK_EDGE; } + +const state = { + nodes:[], edges:[], byId:{}, typeVis:{}, nsVis:{}, + tx:0, ty:0, k:1, alpha:1, alphaMin:0.012, running:true, + selected:null, hovered:null, matches:new Set(), search:'', + source:'', accesses:0, config:{}, built:false +}; + +/* βββββββββββββββ Data loading βββββββββββββββ */ +function b64ToU8(b64){ + const bin = atob(b64); + const u8 = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i); + return u8; +} +async function gunzip(b64){ + const blob = new Blob([b64ToU8(b64)]); + const stream = blob.stream().pipeThrough(new DecompressionStream('gzip')); + return await new Response(stream).text(); +} +function idbOpen(){ + return new Promise((res, rej) => { + const req = indexedDB.open('webx11_vfs', 1); + req.onupgradeneeded = () => req.result.createObjectStore('vfs'); + req.onsuccess = () => res(req.result); + req.onerror = () => rej(req.error); + }); +} +function idbGet(db, key){ + return new Promise((res, rej) => { + const tx = db.transaction('vfs', 'readonly'); + const rq = tx.objectStore('vfs').get(key); + rq.onsuccess = () => { db.close(); res(rq.result || null); }; + rq.onerror = () => { db.close(); rej(rq.error); }; + }); +} +function vfsRead(root, path){ + let node = root; + for (const seg of path.split('/').filter(Boolean)) { + if (!node || !node.content) return null; + node = node.content[seg]; + } + return (node && node.type === 'file') ? node.content : null; +} +function parseMaybe(v){ + if (typeof v !== 'string') return v; + return JSON.parse(v); +} +function readGraphFromRoot(root){ + if (!root) return null; + let nodes = null, edges = null, accesses = 0, config = {}; + try { + const ex = vfsRead(root, '/.memory_graph/export.json'); + if (ex) { + const d = parseMaybe(ex); + if (d && Array.isArray(d.nodes)) { + nodes = d.nodes; edges = d.edges || []; + accesses = (d.stats && d.stats.totalAccesses) || 0; + } + } + } catch (e) {} + if (!nodes) { + try { + const nr = vfsRead(root, '/.memory_graph/nodes.json'); + if (nr) { + const m = parseMaybe(nr); + nodes = Array.isArray(m) ? m : Object.values(m || {}); + } + const er = vfsRead(root, '/.memory_graph/edges.json'); + if (er) edges = parseMaybe(er) || []; + } catch (e) {} + } + if (!nodes || nodes.length === 0) return null; + if (!edges) edges = []; + try { + const cfg = vfsRead(root, '/.memory_graph/config.json'); + if (cfg) config = parseMaybe(cfg) || {}; + } catch (e) {} + return { nodes, edges, accesses, config }; +} +function scanForMemgraph(w){ + try { + if (!w || !w.webx11) return null; + const modMaps = []; + if (w.webx11.moduleManager && w.webx11.moduleManager.modules) modMaps.push(w.webx11.moduleManager.modules); + if (w.webx11.terminal && w.webx11.terminal.moduleManager && w.webx11.terminal.moduleManager.modules) + modMaps.push(w.webx11.terminal.moduleManager.modules); + for (const mods of modMaps) { + for (const key of Object.keys(mods)) { + const m = mods[key]; + if (m && m._nodes && typeof m._nodes === 'object' && Array.isArray(m._edges) + && Object.keys(m._nodes).length > 0 && typeof m.getContextSummary === 'function') return m; + } + } + } catch (e) {} + return null; +} +function findLiveFS(w){ + try { + const wx = w && w.webx11; + if (!wx) return null; + return wx.fs || (wx.terminal && wx.terminal.fs) || null; + } catch (e) { return null; } +} +async function loadData(){ + // 1) Live MemoryGraphModule (opened inside newOS4) + const wins = []; + try { if (window.parent && window.parent !== window) wins.push(window.parent); } catch (e) {} + wins.push(window); + try { if (window.opener) wins.push(window.opener); } catch (e) {} + for (const w of wins) { + const mg = scanForMemgraph(w); + if (mg) { + return { + nodes: Object.values(mg._nodes), edges: mg._edges.slice(), + accesses: mg._totalNodeAccesses || 0, + config: { decayDays: mg._decayDays, decayMinUses: mg._decayMinUses }, + source: 'live MemoryGraphModule' + }; + } + const fs = findLiveFS(w); + if (fs && typeof fs.readFile === 'function') { + try { + const viaFS = p => fs.readFile(p); + const ex = viaFS('/.memory_graph/export.json'); + const g = ex + ? (() => { const d = parseMaybe(ex); return d && d.nodes ? { nodes: d.nodes, edges: d.edges || [], accesses: (d.stats && d.stats.totalAccesses) || 0, config: {} } : null; })() + : (() => { + const nRaw = viaFS('/.memory_graph/nodes.json'); + if (!nRaw) return null; + const m = parseMaybe(nRaw); + const eRaw = viaFS('/.memory_graph/edges.json'); + let cfg = {}; + try { const c = viaFS('/.memory_graph/config.json'); if (c) cfg = parseMaybe(c) || {}; } catch (e) {} + return { nodes: Array.isArray(m) ? m : Object.values(m || {}), edges: eRaw ? (parseMaybe(eRaw) || []) : [], accesses: 0, config: cfg }; + })(); + if (g && g.nodes.length) return Object.assign(g, { source: 'live VFS' }); + } catch (e) {} + } + } + // 2) localStorage v2 (compressed) + try { + const b64 = localStorage.getItem('webx11_filesystem_v2'); + if (b64) { + const g = readGraphFromRoot(JSON.parse(await gunzip(b64)).root); + if (g) return Object.assign(g, { source: 'VFS Β· localStorage' }); + } + } catch (e) {} + // 3) legacy localStorage + try { + const legacy = localStorage.getItem('webx11_filesystem'); + if (legacy) { + const g = readGraphFromRoot(JSON.parse(legacy).root); + if (g) return Object.assign(g, { source: 'VFS Β· legacy' }); + } + } catch (e) {} + // 4) IndexedDB fallback + try { + const db = await idbOpen(); + const v = await idbGet(db, 'filesystem_v2'); + if (v) { + const g = readGraphFromRoot(JSON.parse(await gunzip(v)).root); + if (g) return Object.assign(g, { source: 'VFS Β· IndexedDB' }); + } + } catch (e) {} + throw new Error('No memory graph data found in this context.'); +} + +/* βββββββββββββββ Graph build βββββββββββββββ */ +function nodeRadius(n){ + const base = { session: 9, task: 4.5, file: 6, tool: 7, concept: 4, type: 5, fact: 5 }; + const b = base[n.type] !== undefined ? base[n.type] : 5; + return b + Math.sqrt(n.degree || 1) * 1.5 + Math.sqrt(n.access_count || 1) * 0.9; +} +function buildGraph(raw){ + const byId = {}; + state.nodes = raw.nodes.filter(n => n && n.id).map(n => Object.assign({}, n, { + x: 0, y: 0, vx: 0, vy: 0, degree: 0, fixed: false, r: 6 + })); + state.nodes.forEach(n => byId[n.id] = n); + state.edges = raw.edges.filter(e => e && byId[e.source] && byId[e.target]).map(e => Object.assign({}, e)); + state.edges.forEach(e => { byId[e.source].degree++; byId[e.target].degree++; }); + state.nodes.forEach(n => { n.r = nodeRadius(n); }); + + state.typeVis = {}; state.nsVis = {}; + state.nodes.forEach(n => { + state.typeVis[n.type] = true; + const ns = n.namespace || 'global'; + state.nsVis[ns] = true; + }); + state.byId = byId; + state.accesses = raw.accesses || state.nodes.reduce((s, n) => s + (n.access_count || 1), 0); + state.config = raw.config || {}; + state.source = raw.source; + state.selected = null; state.hovered = null; + layoutInit(); + state.built = true; +} +function layoutInit(){ + const types = Object.keys(state.typeVis); + state.nodes.forEach((n, i) => { + const ti = types.indexOf(n.type); + const ang = (ti / Math.max(types.length, 1)) * Math.PI * 2 - Math.PI / 2; + const a = (i / state.nodes.length) * Math.PI * 2; + n.x = Math.cos(ang) * 250 + Math.cos(a) * 90 + (Math.random() - 0.5) * 24; + n.y = Math.sin(ang) * 250 + Math.sin(a) * 90 + (Math.random() - 0.5) * 24; + n.vx = 0; n.vy = 0; n.fixed = false; + }); + state.alpha = 1; +} + +/* βββββββββββββββ Physics βββββββββββββββ */ +function reheat(v){ if (v > state.alpha) state.alpha = v; } +function step(){ + const N = state.nodes, byId = state.byId; + const alpha = state.alpha; + if (alpha <= state.alphaMin && !draggingNode()) return; + // repulsion + overlap resolution + for (let i = 0; i < N.length; i++) { + const a = N[i]; + for (let j = i + 1; j < N.length; j++) { + const b = N[j]; + let dx = b.x - a.x, dy = b.y - a.y; + let d2 = dx * dx + dy * dy; + if (d2 < 1e-6) { d2 = 1; dx = (Math.random() - 0.5); dy = (Math.random() - 0.5); } + const d = Math.sqrt(d2); + const minD = a.r + b.r + 8; + let f = 3000 / d2; + if (d < minD) f += (minD - d) * 0.55; + f = Math.min(f, 26) * alpha; + const fx = dx / d * f, fy = dy / d * f; + a.vx -= fx; a.vy -= fy; b.vx += fx; b.vy += fy; + } + } + // springs along edges + for (const e of state.edges) { + const a = byId[e.source], b = byId[e.target]; + const dx = b.x - a.x, dy = b.y - a.y; + const d = Math.max(Math.hypot(dx, dy), 0.5); + const w = e.weight || 0.5; + const target = 62 + 150 * (1 - w); + const f = (d - target) * 0.014 * (0.35 + w) * alpha; + const fx = dx / d * f, fy = dy / d * f; + a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy; + } + // gravity + integrate + for (const n of N) { + if (n.fixed) { n.vx = 0; n.vy = 0; continue; } + n.vx -= n.x * 0.004 * alpha; + n.vy -= n.y * 0.004 * alpha; + n.vx *= 0.86; n.vy *= 0.86; + n.x += Math.max(-28, Math.min(28, n.vx)); + n.y += Math.max(-28, Math.min(28, n.vy)); + } + state.alpha = Math.max(state.alphaMin, state.alpha * 0.987); +} +function draggingNode(){ + for (const n of state.nodes) if (n.fixed) return true; + return false; +} +function warmup(ticks){ for (let i = 0; i < ticks; i++) step(); } + +/* βββββββββββββββ View / canvas βββββββββββββββ */ +const canvas = $('#cv'); +const ctx = canvas.getContext('2d'); +const view = { w: 0, h: 0, dpr: 1, bg: null }; +function resize(){ + view.dpr = Math.min(window.devicePixelRatio || 1, 2); + view.w = window.innerWidth; view.h = window.innerHeight; + canvas.width = Math.round(view.w * view.dpr); + canvas.height = Math.round(view.h * view.dpr); + canvas.style.width = view.w + 'px'; canvas.style.height = view.h + 'px'; + const g = ctx.createRadialGradient(canvas.width * 0.72, -canvas.height * 0.12, 0, canvas.width * 0.72, -canvas.height * 0.12, Math.max(canvas.width, canvas.height) * 1.25); + g.addColorStop(0, '#0e1c31'); g.addColorStop(0.55, '#070b14'); g.addColorStop(1, '#05070d'); + view.bg = g; +} +window.addEventListener('resize', resize); + +function visNode(n){ + return state.typeVis[n.type] && state.nsVis[n.namespace || 'global']; +} +function screenToWorld(sx, sy){ return { x: (sx - state.tx) / state.k, y: (sy - state.ty) / state.k }; } +function zoomAt(sx, sy, newK){ + newK = Math.max(0.04, Math.min(4, newK)); + const w = screenToWorld(sx, sy); + state.k = newK; + state.tx = sx - w.x * newK; + state.ty = sy - w.y * newK; +} +function fitView(){ + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity, any = false; + for (const n of state.nodes) { + if (!visNode(n)) continue; + any = true; + minX = Math.min(minX, n.x - n.r); minY = Math.min(minY, n.y - n.r); + maxX = Math.max(maxX, n.x + n.r); maxY = Math.max(maxY, n.y + n.r); + } + if (!any) return; + const bw = Math.max(maxX - minX, 10), bh = Math.max(maxY - minY, 10); + state.k = Math.max(0.05, Math.min(2.5, 0.86 * Math.min(view.w / bw, view.h / bh))); + state.tx = view.w / 2 - ((minX + maxX) / 2) * state.k; + state.ty = view.h / 2 - ((minY + maxY) / 2) * state.k; +} +function centerOn(n){ + if (!n) return; + state.tx = view.w / 2 - n.x * state.k; + state.ty = view.h / 2 - n.y * state.k; +} +function shortLabel(n){ + let t = n.label || n.id; + if (t.length > 22) t = t.slice(0, 20) + 'β¦'; + return t; +} +function computeHighlight(){ + const s = new Set(); + const add = n => { + if (!n) return; + s.add(n.id); + for (const e of state.edges) { + if (e.source === n.id) s.add(e.target); + else if (e.target === n.id) s.add(e.source); + } + }; + add(state.selected); add(state.hovered); + return s; +} +function drawArrow(a, b, e){ + const dx = b.x - a.x, dy = b.y - a.y; + const d = Math.hypot(dx, dy) || 1; + const ux = dx / d, uy = dy / d; + const t = d - b.r - 3.5; + if (t < a.r + 5) return; + const x = a.x + ux * t, y = a.y + uy * t, s = 3.4; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x - ux * s * 2 - uy * s, y - uy * s * 2 + ux * s); + ctx.lineTo(x - ux * s * 2 + uy * s, y - uy * s * 2 - ux * s); + ctx.closePath(); + ctx.fillStyle = edgeColor(e.type); + ctx.fill(); +} +function render(){ + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = view.bg || '#070b14'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.setTransform(view.dpr * state.k, 0, 0, view.dpr * state.k, view.dpr * state.tx, view.dpr * state.ty); + const hi = computeHighlight(); + const q = state.search; + + // edges + for (const e of state.edges) { + const a = state.byId[e.source], b = state.byId[e.target]; + if (!visNode(a) || !visNode(b)) continue; + let alpha = 0.16 + (e.weight || 0.5) * 0.3 + Math.min((e.use_count || 1) * 0.045, 0.28); + if (hi.size) { + const on = hi.has(a.id) && hi.has(b.id); + alpha = on ? Math.min(alpha * 2.3, 0.95) : 0.045; + } + if (state.selected && (a === state.selected || b === state.selected)) alpha = Math.max(alpha, 0.92); + ctx.globalAlpha = alpha; + ctx.strokeStyle = edgeColor(e.type); + ctx.lineWidth = 0.7 + (e.weight || 0.5) * 1.5 + Math.min((e.use_count || 1) * 0.25, 2.2); + if (e.type === 'failed') ctx.setLineDash([4, 4]); else ctx.setLineDash([]); + ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); + if (state.k > 1.15 && (!hi.size || (a === state.selected || b === state.selected || a === state.hovered || b === state.hovered))) + drawArrow(a, b, e); + } + ctx.setLineDash([]); + + // nodes + let labels = 0; + for (const n of state.nodes) { + if (!visNode(n)) continue; + const inHi = !hi.size || hi.has(n.id); + const isSel = n === state.selected, isHov = n === state.hovered, isMatch = state.matches.has(n.id); + const col = typeColor(n.type); + ctx.globalAlpha = inHi ? 1 : 0.13; + if (isSel || isHov) { ctx.shadowColor = col; ctx.shadowBlur = 16; } + ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, 6.28318); + ctx.fillStyle = col; ctx.fill(); + ctx.shadowBlur = 0; + ctx.beginPath(); ctx.arc(n.x, n.y, n.r * 0.52, 0, 6.28318); + ctx.fillStyle = 'rgba(7,11,20,0.5)'; ctx.fill(); + if (isSel || isHov || isMatch) { + ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 3.5, 0, 6.28318); + ctx.strokeStyle = isMatch ? '#fbbf24' : '#ffffff'; + ctx.lineWidth = isSel ? 2 : 1.4; + ctx.stroke(); + } + if (n.namespace && n.namespace.startsWith('agent:')) { + ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 1.8, 0, 6.28318); + ctx.setLineDash([2.2, 3]); + ctx.strokeStyle = 'rgba(232,121,249,0.85)'; ctx.lineWidth = 1; ctx.stroke(); + ctx.setLineDash([]); + } + const showLabel = isSel || isHov || isMatch || n.type === 'session' + || (inHi && state.k * n.r > 6.4 && (n.degree >= 4 || state.k > 1.45)); + if (showLabel && labels < 230) { + ctx.font = '10.5px system-ui, sans-serif'; + ctx.textAlign = 'center'; + const ly = n.y + n.r + 11.5; + ctx.lineWidth = 3; + ctx.strokeStyle = 'rgba(5,8,16,0.92)'; + ctx.strokeText(shortLabel(n), n.x, ly); + ctx.fillStyle = (isSel || isHov) ? '#ffffff' : '#c9d5e6'; + ctx.fillText(shortLabel(n), n.x, ly); + labels++; + } + } + ctx.globalAlpha = 1; +} +function frame(){ + if (state.running && state.built) step(); + if (state.built) render(); + requestAnimationFrame(frame); +} + +/* βββββββββββββββ Interactions βββββββββββββββ */ +const pointers = new Map(); +let dragMode = null; // 'pan' | 'pinch' | 'node' +let dragNode = null; +let pinchStart = null; + +function hitNode(sx, sy){ + let best = null, bestD = Infinity; + for (const n of state.nodes) { + if (!visNode(n)) continue; + const px = n.x * state.k + state.tx, py = n.y * state.k + state.ty; + const d = Math.hypot(px - sx, py - sy); + const rad = n.r * state.k + 6; + if (d < rad && d < bestD) { best = n; bestD = d; } + } + return best; +} +canvas.addEventListener('pointerdown', e => { + canvas.setPointerCapture(e.pointerId); + pointers.set(e.pointerId, { x: e.clientX, y: e.clientY, sx: e.clientX, sy: e.clientY, moved: 0 }); + if (pointers.size === 1) { + const n = hitNode(e.clientX, e.clientY); + if (n) { dragMode = 'node'; dragNode = n; n.fixed = true; reheat(0.35); } + else { dragMode = 'pan'; canvas.classList.add('grabbing'); } + } else if (pointers.size === 2) { + dragMode = 'pinch'; dragNode = null; + const pts = [...pointers.values()]; + pinchStart = { + d: Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y) || 1, + cx: (pts[0].x + pts[1].x) / 2, cy: (pts[0].y + pts[1].y) / 2, + k: state.k, tx: state.tx, ty: state.ty + }; + } +}); +canvas.addEventListener('pointermove', e => { + const p = pointers.get(e.pointerId); + if (!p) { + // hover (no button pressed) + if (dragMode === null) { + const h = hitNode(e.clientX, e.clientY); + if (h !== state.hovered) { state.hovered = h; } + showTooltip(e, h); + } + return; + } + const dx = e.clientX - p.x, dy = e.clientY - p.y; + p.moved += Math.abs(dx) + Math.abs(dy); + p.x = e.clientX; p.y = e.clientY; + if (dragMode === 'node' && dragNode) { + const w = screenToWorld(e.clientX, e.clientY); + dragNode.x = w.x; dragNode.y = w.y; + reheat(0.3); + } else if (dragMode === 'pan') { + state.tx += dx; state.ty += dy; + } else if (dragMode === 'pinch' && pinchStart && pointers.size === 2) { + const pts = [...pointers.values()]; + const d = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y) || 1; + const cx = (pts[0].x + pts[1].x) / 2, cy = (pts[0].y + pts[1].y) / 2; + const wx = (pinchStart.cx - pinchStart.tx) / pinchStart.k; + const wy = (pinchStart.cy - pinchStart.ty) / pinchStart.k; + const newK = Math.max(0.04, Math.min(4, pinchStart.k * (d / pinchStart.d))); + state.k = newK; + state.tx = cx - wx * newK; + state.ty = cy - wy * newK; + } +}); +function endPointer(e){ + const p = pointers.get(e.pointerId); + pointers.delete(e.pointerId); + if (p && p.moved < 6 && pointers.size === 0) { + // treat as click + const n = hitNode(e.clientX, e.clientY); + selectNode(n); + } + if (dragNode) { dragNode.fixed = false; } + if (pointers.size === 0) { + dragMode = null; dragNode = null; pinchStart = null; + canvas.classList.remove('grabbing'); + } else if (pointers.size === 1) { + dragMode = 'pan'; pinchStart = null; dragNode = null; + const rem = [...pointers.values()][0]; + rem.x = rem.px !== undefined ? rem.px : rem.x; + } +} +canvas.addEventListener('pointerup', endPointer); +canvas.addEventListener('pointercancel', endPointer); +canvas.addEventListener('pointerleave', e => { state.hovered = null; hideTooltip(); }); +canvas.addEventListener('wheel', e => { + e.preventDefault(); + const f = Math.exp(-e.deltaY * 0.0016); + zoomAt(e.clientX, e.clientY, state.k * f); +}, { passive: false }); + +/* tooltip */ +const tooltip = $('#tooltip'); +function showTooltip(e, n){ + if (!n) { hideTooltip(); return; } + const deg = n.degree || 0; + tooltip.innerHTML = '<b>' + esc(shortLabel(n) === n.label ? n.label : n.label) + '</b>' + + '<div class="t">' + esc(n.type) + ' Β· ' + deg + ' edges Β· ' + (n.access_count || 1) + 'Γ access</div>' + + (n.namespace && n.namespace !== 'global' ? '<div class="t">agent: ' + esc(n.namespace) + '</div>' : '') + + '<div class="t">updated ' + esc(ago(n.updated || n.created)) + '</div>'; + tooltip.style.display = 'block'; + const r = tooltip.getBoundingClientRect(); + let x = e.clientX + 14, y = e.clientY + 14; + if (x + r.width > view.w - 8) x = e.clientX - r.width - 14; + if (y + r.height > view.h - 8) y = e.clientY - r.height - 14; + tooltip.style.left = x + 'px'; tooltip.style.top = y + 'px'; +} +function hideTooltip(){ tooltip.style.display = 'none'; } + +/* selection + details */ +function selectNode(n){ + state.selected = n; + renderDetails(n); +} +function renderDetails(n){ + const box = $('#details'); + if (!n) { box.classList.remove('open'); box.innerHTML = ''; return; } + const col = typeColor(n.type); + const myEdges = state.edges.filter(e => e.source === n.id || e.target === n.id) + .sort((a, b) => (b.weight || 0) - (a.weight || 0)); + const meta = n.metadata || {}; + const metaKeys = Object.keys(meta); + let html = ''; + html += '<div id="dHead"><div style="flex:1;min-width:0">' + + '<div id="dTitle">' + esc(n.label || n.id) + '</div>' + + '<span class="badge" style="color:' + col + ';border-color:' + col + '55">β ' + esc(n.type) + '</span>' + + '<span class="badge" style="color:' + (n.namespace && n.namespace.startsWith('agent:') ? '#e879f9' : '#94a3b8') + ';border-color:rgba(148,163,184,.3)">' + esc(n.namespace || 'global') + '</span>' + + '</div><button class="btn" id="dClose" title="Close">β</button></div>'; + html += '<div class="kv"><span class="k">ID</span><span class="v" style="font-family:ui-monospace,monospace;font-size:11px">' + esc(n.id) + '</span></div>'; + html += '<div class="kv"><span class="k">Accesses</span><span class="v">' + (n.access_count || 1) + 'Γ</span></div>'; + html += '<div class="kv"><span class="k">Degree</span><span class="v">' + (n.degree || 0) + ' edges</span></div>'; + html += '<div class="kv"><span class="k">Created</span><span class="v">' + esc(ago(n.created)) + '</span></div>'; + html += '<div class="kv"><span class="k">Updated</span><span class="v">' + esc(ago(n.updated || n.created)) + '</span></div>'; + if (n.source_session) html += '<div class="kv"><span class="k">Session</span><span class="v">' + esc(n.source_session) + '</span></div>'; + if (n.decay_score !== undefined) html += '<div class="kv"><span class="k">Decay score</span><span class="v">' + Number(n.decay_score).toFixed(2) + '</span></div>'; + if (metaKeys.length) { + html += '<pre class="meta">' + metaKeys.map(k => esc(k) + ': ' + esc(typeof meta[k] === 'object' ? JSON.stringify(meta[k]) : meta[k])).join('\n') + '</pre>'; + } + html += '<div class="sec" style="margin-top:12px"><h3>Connections (' + myEdges.length + ')</h3>'; + const shown = myEdges.slice(0, 30); + for (const e of shown) { + const out = e.source === n.id; + const otherId = out ? e.target : e.source; + const other = state.byId[otherId]; + const oc = typeColor(other.type); + html += '<div class="edgeRow" data-node="' + esc(otherId) + '">' + + '<span class="arrow">' + (out ? 'β' : 'β') + '</span>' + + '<span class="dot" style="width:8px;height:8px;border-radius:50%;background:' + oc + ';flex-shrink:0"></span>' + + '<span class="lab">' + esc(other.label || otherId) + '</span>' + + '<span class="etype">' + esc(e.type) + '</span>' + + '<span class="w">w ' + Number(e.weight || 0).toFixed(2) + ' Β· ' + (e.use_count || 1) + 'Γ</span></div>'; + } + if (myEdges.length > 30) html += '<div class="mini" style="padding:4px 8px">β¦ and ' + (myEdges.length - 30) + ' more</div>'; + html += '</div>'; + html += '<div style="display:flex;gap:6px;margin-top:10px"><button class="btn" id="dCenter" style="flex:1">π― Center</button></div>'; + box.innerHTML = html; + box.classList.add('open'); + $('#dClose').onclick = () => selectNode(null); + $('#dCenter').onclick = () => { centerOn(n); }; + box.querySelectorAll('.edgeRow').forEach(el => { + el.onclick = () => { + const t = state.byId[el.getAttribute('data-node')]; + if (t) { selectNode(t); centerOn(t); } + }; + }); +} + +/* βββββββββββββββ Sidebar UI βββββββββββββββ */ +function ago(iso){ + if (!iso) return 'β'; + const t = new Date(iso).getTime(); + if (isNaN(t)) return 'β'; + const s = (Date.now() - t) / 1000; + if (s < 60) return 'just now'; + if (s < 3600) return Math.floor(s / 60) + 'm ago'; + if (s < 86400) return Math.floor(s / 3600) + 'h ago'; + if (s < 86400 * 30) return Math.floor(s / 86400) + 'd ago'; + return new Date(t).toLocaleDateString(); +} +function buildSidebar(){ + const sb = $('#sidebar'); + const types = Object.keys(state.typeVis); + const typeCounts = {}; + state.nodes.forEach(n => typeCounts[n.type] = (typeCounts[n.type] || 0) + 1); + const nsList = Object.keys(state.nsVis); + const nsCounts = {}; + state.nodes.forEach(n => { const ns = n.namespace || 'global'; nsCounts[ns] = (nsCounts[ns] || 0) + 1; }); + + // top connected + const top = [...state.nodes].sort((a, b) => (b.degree || 0) - (a.degree || 0)).slice(0, 6); + const maxDeg = Math.max(...top.map(n => n.degree || 0), 1); + // strongest edges + const strong = [...state.edges].sort((a, b) => (b.weight || 0) - (a.weight || 0)).slice(0, 5); + // recent tasks + const tasks = state.nodes.filter(n => n.type === 'task') + .sort((a, b) => new Date(b.updated || 0) - new Date(a.updated || 0)).slice(0, 6); + // edge type counts + const edgeCounts = {}; + state.edges.forEach(e => edgeCounts[e.type] = (edgeCounts[e.type] || 0) + 1); + const density = state.nodes.length ? (state.edges.length / state.nodes.length).toFixed(2) : '0'; + + let h = ''; + h += '<div class="sec"><div class="statgrid">' + + '<div class="stat"><div class="v">' + state.nodes.length + '</div><div class="k">nodes</div></div>' + + '<div class="stat"><div class="v">' + state.edges.length + '</div><div class="k">edges</div></div>' + + '<div class="stat"><div class="v">' + state.accesses + '</div><div class="k">accesses</div></div>' + + '<div class="stat"><div class="v">' + density + '</div><div class="k">edges/node</div></div>' + + '</div></div>'; + + h += '<div class="sec"><h3>Node types β click to filter</h3><div id="typeChips">'; + types.sort((a, b) => typeCounts[b] - typeCounts[a]).forEach(t => { + h += '<span class="chip" data-type="' + esc(t) + '"><span class="dot" style="background:' + typeColor(t) + '"></span>' + + esc(t) + ' <b style="color:#94a3b8">' + typeCounts[t] + '</b></span>'; + }); + h += '</div></div>'; + + if (nsList.length > 1) { + h += '<div class="sec"><h3>Namespaces</h3>'; + nsList.forEach(ns => { + const isAgent = ns.startsWith('agent:'); + h += '<span class="chip" data-ns="' + esc(ns) + '"><span class="dot" style="background:' + (isAgent ? '#e879f9' : '#64748b') + '"></span>' + + esc(ns) + ' <b style="color:#94a3b8">' + nsCounts[ns] + '</b></span>'; + }); + h += '</div>'; + } + + h += '<div class="sec"><h3>Most connected <span style="text-transform:none;letter-spacing:0">memgraph-stats</span></h3>'; + top.forEach(n => { + h += '<div class="row" data-node="' + esc(n.id) + '"><span class="dot" style="background:' + typeColor(n.type) + '"></span>' + + '<span class="lab">' + esc(shortLabel(n)) + '</span>' + + '<span class="val">' + (n.degree || 0) + '</span></div>'; + }); + h += '</div>'; + + h += '<div class="sec"><h3>Strongest links</h3>'; + strong.forEach(e => { + const a = state.byId[e.source], b = state.byId[e.target]; + h += '<div class="row" data-node="' + esc(e.source) + '"><span class="dot" style="background:' + edgeColor(e.type) + '"></span>' + + '<span class="lab">' + esc(shortLabel(a)) + ' β ' + esc(shortLabel(b)) + '</span>' + + '<span class="val">w' + Number(e.weight || 0).toFixed(2) + '</span></div>'; + }); + h += '</div>'; + + if (tasks.length) { + h += '<div class="sec"><h3>Recent tasks</h3>'; + tasks.forEach(n => { + h += '<div class="row" data-node="' + esc(n.id) + '"><span class="dot" style="background:' + typeColor(n.type) + '"></span>' + + '<span class="lab">' + esc(shortLabel(n)) + '</span>' + + '<span class="val">' + esc(ago(n.updated || n.created)) + '</span></div>'; + }); + h += '</div>'; + } + + h += '<div class="sec"><h3>Edge types</h3>'; + Object.keys(edgeCounts).sort((a, b) => edgeCounts[b] - edgeCounts[a]).forEach(t => { + const dashed = t === 'failed' ? 'border-top:2px dashed ' + edgeColor(t) : ''; + h += '<div class="lrow"><span class="line" style="background:' + edgeColor(t) + ';' + (dashed ? 'background:none;' + dashed : '') + '"></span>' + + esc(t) + '<span class="n">' + edgeCounts[t] + '</span></div>'; + }); + h += '</div>'; + + const cfg = state.config || {}; + if (cfg.decayDays) { + h += '<div class="sec"><h3>Decay policy</h3><div class="mini">Threshold: <code>' + esc(cfg.decayDays) + 'd</code> Β· min uses: <code>' + esc(cfg.decayMinUses) + '</code> β stale, weak nodes fade and get auto-pruned.</div></div>'; + } + + h += '<div class="sec mini">Source: <code>/.memory_graph/</code> Β· ' + esc(state.source) + '<br>' + + 'Terminal: <code>memgraph-status</code> Β· <code>memgraph-stats</code> Β· <code>memgraph-search</code> Β· <code>memgraph-export</code> Β· <code>memgraph-prune</code></div>'; + h += '<div id="foot"><button class="btn" id="btnReset">π Reset layout</button></div>'; + + sb.innerHTML = h; + sb.querySelectorAll('.chip[data-type]').forEach(el => { + el.onclick = () => { + const t = el.getAttribute('data-type'); + state.typeVis[t] = !state.typeVis[t]; + el.classList.toggle('off', !state.typeVis[t]); + reheat(0.12); + }; + }); + sb.querySelectorAll('.chip[data-ns]').forEach(el => { + el.onclick = () => { + const ns = el.getAttribute('data-ns'); + state.nsVis[ns] = !state.nsVis[ns]; + el.classList.toggle('off', !state.nsVis[ns]); + }; + }); + sb.querySelectorAll('.row[data-node]').forEach(el => { + el.onclick = () => { + const n = state.byId[el.getAttribute('data-node')]; + if (n) { selectNode(n); centerOn(n); } + }; + }); + const rb = $('#btnReset'); + if (rb) rb.onclick = () => { layoutInit(); fitView(); toast('Layout reset'); }; +} + +/* βββββββββββββββ Top bar wiring βββββββββββββββ */ +function toast(msg){ + const t = $('#toast'); + t.textContent = msg; + t.classList.add('show'); + clearTimeout(t._tm); + t._tm = setTimeout(() => t.classList.remove('show'), 2600); +} +const searchEl = $('#search'); +function updateSearch(){ + const q = searchEl.value.trim().toLowerCase(); + state.search = q; + state.matches = new Set(); + const cnt = $('#searchCount'); + if (q) { + for (const n of state.nodes) { + const hay = ((n.label || '') + ' ' + n.id + ' ' + n.type).toLowerCase(); + if (hay.includes(q)) state.matches.add(n.id); + } + cnt.style.display = 'inline'; + cnt.textContent = state.matches.size + ' match' + (state.matches.size === 1 ? '' : 'es'); + } else { + cnt.style.display = 'none'; + } +} +searchEl.addEventListener('input', updateSearch); +searchEl.addEventListener('keydown', e => { + if (e.key === 'Enter' && state.matches.size) { + const first = state.byId[[...state.matches][0]]; + if (first) { selectNode(first); centerOn(first); } + } else if (e.key === 'Escape') { + searchEl.value = ''; updateSearch(); + } +}); +document.addEventListener('keydown', e => { + if (e.key === '/' && document.activeElement !== searchEl) { e.preventDefault(); searchEl.focus(); } + else if (e.key === 'Escape' && document.activeElement !== searchEl) { selectNode(null); } + else if ((e.key === 'f' || e.key === 'F') && document.activeElement !== searchEl && !e.ctrlKey && !e.metaKey) { fitView(); } +}); +$('#btnFit').onclick = () => fitView(); +$('#btnZoomIn').onclick = () => zoomAt(view.w / 2, view.h / 2, state.k * 1.28); +$('#btnZoomOut').onclick = () => zoomAt(view.w / 2, view.h / 2, state.k / 1.28); +$('#btnSidebar').onclick = () => $('#sidebar').classList.toggle('hidden'); +$('#btnPause').onclick = () => { + state.running = !state.running; + const b = $('#btnPause'); + b.textContent = state.running ? 'βΈ' : 'βΆ'; + b.classList.toggle('act', !state.running); + b.title = state.running ? 'Pause physics' : 'Resume physics'; + toast(state.running ? 'Physics resumed' : 'Physics paused'); +}; +$('#btnRefresh').onclick = async () => { + try { + toast('Refreshing graphβ¦'); + const raw = await loadData(); + buildGraph(raw); + buildSidebar(); + $('#srcChip').textContent = 'β‘ ' + state.source + ' Β· ' + state.nodes.length + 'n/' + state.edges.length + 'e'; + warmup(120); fitView(); + updateSearch(); + selectNode(null); + toast('β
' + state.nodes.length + ' nodes Β· ' + state.edges.length + ' edges β ' + state.source); + } catch (e) { + toast('β Refresh failed: ' + e.message); + } +}; +$('#btnPNG').onclick = () => { + try { + canvas.toBlob(b => { + const a = document.createElement('a'); + a.href = URL.createObjectURL(b); + a.download = 'memgraph-' + new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-') + '.png'; + a.click(); + setTimeout(() => URL.revokeObjectURL(a.href), 3000); + toast('πΌοΈ PNG exported'); + }); + } catch (e) { toast('Export failed: ' + e.message); } +}; +$('#btnJSON').onclick = () => { + try { + const data = { + version: 1, + exported: new Date().toISOString(), + source: 'memgraph.html viewer', + nodes: state.nodes.map(n => { const c = Object.assign({}, n); delete c.vx; delete c.vy; delete c.fixed; return c; }), + edges: state.edges, + stats: { totalNodes: state.nodes.length, totalEdges: state.edges.length, totalAccesses: state.accesses } + }; + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'memgraph-export.json'; + a.click(); + setTimeout(() => URL.revokeObjectURL(a.href), 3000); + toast('π JSON exported (memgraph-export format)'); + } catch (e) { toast('Export failed: ' + e.message); } +}; + +/* βββββββββββββββ Init βββββββββββββββ */ +function setLoad(title, text){ + $('#ovTitle').textContent = title; + $('#ovText').innerHTML = text; +} +async function init(){ + resize(); + setLoad('Loading Memory Graphβ¦', 'Locating graph data in the WebX11 newOS4 VFSβ¦'); + try { + const raw = await loadData(); + setLoad('Building graphβ¦', 'Found ' + raw.nodes.length + ' nodes Β· ' + raw.edges.length + ' edges β computing layoutβ¦'); + buildGraph(raw); + buildSidebar(); + $('#srcChip').textContent = 'β‘ ' + state.source + ' Β· ' + state.nodes.length + 'n/' + state.edges.length + 'e'; + warmup(140); + fitView(); + requestAnimationFrame(frame); + $('#overlay').classList.add('hide'); + toast('π§ ' + state.nodes.length + ' nodes Β· ' + state.edges.length + ' edges loaded β ' + state.source); + } catch (err) { + $('#spin').style.display = 'none'; + setLoad('β οΈ No graph data found', + 'This viewer reads the <b>live</b> memory graph from WebX11 newOS4 (<code>/.memory_graph/</code> in the VFS).<br><br>' + + 'To see your graph, open it from inside newOS4:<br><code>preview /memgraph.html</code><br><br>' + + 'Or serve this file from the same origin as newOS4.<br><br><span style="color:#64748b">' + esc(err.message) + '</span>'); + $('#ovRetry').style.display = 'inline-flex'; + } +} +requestAnimationFrame(frame); +init(); +</script> +</body> +</html>
\ No newline at end of file |