297 lines
11 KiB
Python
297 lines
11 KiB
Python
"""Standalone 2D Agentic-OS brain viewer.
|
|
|
|
Run: . venv/bin/activate && python3 regen_graph_html_standalone.py
|
|
Then open: http://127.0.0.1:8080/graphify-out/graph-standalone.html
|
|
"""
|
|
from pathlib import Path
|
|
import json
|
|
from collections import Counter
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
GRAPH_JSON = ROOT / "brain" / "graph" / "agentic-os.json"
|
|
OUT = ROOT / "graphify-out" / "graph-standalone.html"
|
|
|
|
data = json.loads(GRAPH_JSON.read_text(encoding="utf-8"))
|
|
nodes = data.get("nodes", [])
|
|
links = data.get("links", [])
|
|
|
|
# Count top communities for legend
|
|
community_counter = Counter()
|
|
for n in nodes:
|
|
cn = (n.get("community_name") or n.get("community") or "").strip()
|
|
if cn:
|
|
community_counter[cn] += 1
|
|
top_communities = [cn for cn, _ in community_counter.most_common(25)]
|
|
|
|
PALETTE = [
|
|
"#E15759", "#4E79A7", "#F28E2B", "#76B7B2",
|
|
"#59A14F", "#EDC948", "#B07AA1", "#FF9DA7",
|
|
"#9C755F", "#BAB0AC", "#86BCB6", "#8CD17D",
|
|
"#B6992D", "#499894", "#D37295", "#F1CE63",
|
|
"#D4A6C8",
|
|
]
|
|
|
|
def community_color(name: str, index: int) -> str:
|
|
import hashlib
|
|
h = hashlib.md5(name.encode("utf-8")).hexdigest()
|
|
return PALETTE[int(h, 16) % len(PALETTE)]
|
|
|
|
community_color_map = {cn: community_color(cn, i) for i, cn in enumerate(top_communities)}
|
|
|
|
# Degree map
|
|
degree = {}
|
|
for link in links:
|
|
for key in ("source", "from", "target", "to"):
|
|
if key in link and link[key] is not None:
|
|
degree[link[key]] = degree.get(link[key], 0) + 1
|
|
break
|
|
|
|
for n in nodes:
|
|
nid = n.get("id") or n.get("name")
|
|
n["__degree__"] = degree.get(nid, 0)
|
|
|
|
# Escape for HTML attribute
|
|
import html as html_mod
|
|
esc = html_mod.escape
|
|
|
|
legend_items = []
|
|
for cn in top_communities:
|
|
color = community_color_map[cn]
|
|
legend_items.append(
|
|
f'<span class="legend-dot" style="background:{esc(color)}"></span>'
|
|
f'<span class="legend-label">{esc(cn)}</span>'
|
|
)
|
|
|
|
legend_html = "\n".join(
|
|
f'<div class="legend-item" data-community="{esc(cn)}">{item}</div>'
|
|
for cn, item in zip(top_communities, legend_items)
|
|
)
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Agentic-OS Brain</title>
|
|
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"
|
|
crossorigin="anonymous"></script>
|
|
<style>
|
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
html, body {{ height: 100%; background: #07080d; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }}
|
|
#mynetwork {{ position: fixed; inset: 0; }}
|
|
#sidebar {{
|
|
position: fixed; top: 0; right: 0; width: 300px; height: 100%; background: rgba(12,13,20,0.94);
|
|
border-left: 1px solid #252540; display: flex; flex-direction: column; z-index: 10;
|
|
backdrop-filter: blur(10px);
|
|
}}
|
|
#search-wrap {{ padding: 14px; border-bottom: 1px solid #252540; }}
|
|
#search {{
|
|
width: 100%; background: #0d0e16; border: 1px solid #353555; color: #e0e0e0;
|
|
padding: 8px 10px; border-radius: 6px; font-size: 13px; outline: none;
|
|
}}
|
|
#search:focus {{ border-color: #4E79A7; }}
|
|
#search-results {{ max-height: 180px; overflow-y: auto; padding: 6px 0; display: none; }}
|
|
.search-item {{ padding: 6px 14px; cursor: pointer; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
.search-item:hover {{ background: #252540; }}
|
|
#info-panel {{ padding: 16px; border-bottom: 1px solid #252540; min-height: 150px; }}
|
|
#info-panel h3 {{ font-size: 11px; color: #777; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.1em; }}
|
|
#info-content {{ font-size: 12px; color: #bbb; line-height: 1.6; }}
|
|
.field {{ margin-bottom: 5px; }} .field b {{ color: #eee; }}
|
|
.empty {{ color: #555; font-style: italic; }}
|
|
#legend-wrap {{ flex: 1; overflow-y: auto; padding: 14px; }}
|
|
#legend-wrap h3 {{ font-size: 11px; color: #777; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.1em; }}
|
|
.legend-item {{ display: flex; align-items: center; gap: 8px; padding: 5px 4px; cursor: pointer; border-radius: 4px; font-size: 11px; }}
|
|
.legend-item:hover {{ background: #252540; }}
|
|
.legend-item.dimmed {{ opacity: 0.25; }}
|
|
.legend-dot {{ width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }}
|
|
.legend-label {{ flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
|
|
#stats {{ padding: 12px 14px; border-top: 1px solid #252540; font-size: 11px; color: #666; }}
|
|
.controls {{ padding: 12px 14px; border-bottom: 1px solid #252540; }}
|
|
.controls label {{ display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: #aaa; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="mynetwork"></div>
|
|
<div id="sidebar">
|
|
<div id="search-wrap">
|
|
<input id="search" type="text" placeholder="Search nodes..." autocomplete="off">
|
|
<div id="search-results"></div>
|
|
</div>
|
|
<div class="controls">
|
|
<label><input type="checkbox" id="stabilize" checked> Stabilize layout</label>
|
|
</div>
|
|
<div id="info-panel">
|
|
<h3>Node Info</h3>
|
|
<div id="info-content"><span class="empty">Click a node to inspect it</span></div>
|
|
</div>
|
|
<div id="legend-wrap">
|
|
<h3>Top Communities</h3>
|
|
<div id="legend">{legend_html}</div>
|
|
</div>
|
|
<div id="stats">{len(nodes)} nodes · {len(links)} edges · {len(top_communities)} communities shown</div>
|
|
</div>
|
|
<script>
|
|
const RAW_NODES = {json.dumps(nodes, ensure_ascii=False)};
|
|
const RAW_LINKS = {json.dumps(links, ensure_ascii=False)};
|
|
const PALETTE = {json.dumps(PALETTE)};
|
|
|
|
function esc(s) {{
|
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
}}
|
|
|
|
RAW_NODES.forEach(n => {{
|
|
n.color = community_color_map[n.community_name || n.community || ""] || "#4E79A7";
|
|
var d = n.__degree__ || 0;
|
|
n.size = Math.max(6, Math.min(6 + Math.min(d, 20) * 1.4, 28));
|
|
}});
|
|
|
|
function community_color(name) {{
|
|
if (!name) return "#4E79A7";
|
|
if (community_color_map[name]) return community_color_map[name];
|
|
import_hashes = [];
|
|
for (let i = 0; i < name.length; i++) import_hashes.push(name.charCodeAt(i));
|
|
var h = 0;
|
|
for (let i = 0; i < import_hashes.length; i++) h = (h << 5) - h + import_hashes[i];
|
|
return PALETTE[Math.abs(h) % PALETTE.length];
|
|
}}
|
|
|
|
function updateInfo(node) {{
|
|
const fields = [
|
|
['id', node.id],
|
|
['label', node.label],
|
|
['type', node.type],
|
|
['community', node.community_name || node.community],
|
|
['source', node.source_file],
|
|
['location', node.source_location],
|
|
['degree', node.__degree__],
|
|
].filter(function(x) {{ return x[1] != null && x[1] !== ''; }});
|
|
document.getElementById('info-content').innerHTML = fields.map(function(f) {{
|
|
return '<div class="field"><b>' + esc(f[0]) + ':</b> ' + esc(String(f[1])) + '</div>';
|
|
}}).join('');
|
|
}}
|
|
|
|
function wireSearch(net) {{
|
|
const searchInput = document.getElementById('search');
|
|
const searchResults = document.getElementById('search-results');
|
|
searchInput.addEventListener('input', function() {{
|
|
const q = searchInput.value.trim().toLowerCase();
|
|
searchResults.style.display = 'none';
|
|
if (!q) return;
|
|
const matches = RAW_NODES.filter(function(n) {{
|
|
return (n.label || '').toLowerCase().includes(q) || (n.id || '').toLowerCase().includes(q);
|
|
}}).slice(0, 40);
|
|
searchResults.innerHTML = matches.map(function(n, i) {{
|
|
return '<div class="search-item" data-idx="' + i + '">' + esc(n.label || n.id) + '</div>';
|
|
}}).join('');
|
|
searchResults.style.display = matches.length ? 'block' : 'none';
|
|
}});
|
|
searchResults.addEventListener('click', function(e) {{
|
|
const item = e.target.closest('.search-item');
|
|
if (!item) return;
|
|
const idx = parseInt(item.getAttribute('data-idx'), 10);
|
|
const q = searchInput.value.trim().toLowerCase();
|
|
const match = RAW_NODES.find(function(n) {{
|
|
return (n.label || '').toLowerCase().includes(q) || (n.id || '').toLowerCase().includes(q);
|
|
}});
|
|
if (match) {{
|
|
net.focus(match.id, {{ scale: 2.5, animation: true }});
|
|
}}
|
|
}});
|
|
}}
|
|
|
|
function wireLegend(net) {{
|
|
document.querySelectorAll('.legend-item').forEach(function(el) {{
|
|
el.addEventListener('click', function() {{
|
|
const cn = el.getAttribute('data-community');
|
|
el.classList.toggle('dimmed');
|
|
const active = !el.classList.contains('dimmed');
|
|
const val = active ? 1 : 0.05;
|
|
net.setOptions({{ nodes: {{ opacity: function(opts, id) {{
|
|
var n = RAW_NODES.find(function(x) {{ return x.id === id; }});
|
|
if (!n) return val;
|
|
var cc = n.community_name || n.community || "";
|
|
return cc === cn ? 1 : 0.05;
|
|
}} }} }});
|
|
}});
|
|
}});
|
|
}}
|
|
|
|
function initGraph() {{
|
|
var visNodes = new vis.DataSet(RAW_NODES.map(function(n) {{
|
|
return {{
|
|
id: n.id,
|
|
label: n.label || n.id,
|
|
title: [n.id, n.label, n.type, n.source_file].filter(Boolean).join('\\n'),
|
|
color: {{ background: n.color || '#4E79A7', border: n.color || '#4E79A7', highlight: {{ background: '#ffffff', border: '#ffffff' }} }},
|
|
size: n.size || 8,
|
|
font: {{ color: '#e0e0e0', size: 10, face: 'Inter, -apple-system, sans-serif' }},
|
|
borderWidth: 1,
|
|
shadow: {{ enabled: true, color: n.color || '#4E79A7', size: 8 }}
|
|
}};
|
|
}}));
|
|
|
|
var visEdges = new vis.DataSet(RAW_LINKS.map(function(l) {{
|
|
return {{ from: l.source || l.from, to: l.target || l.to }};
|
|
}}));
|
|
|
|
var container = document.getElementById('mynetwork');
|
|
var options = {{
|
|
background: {{ color: '#07080d' }},
|
|
nodes: {{
|
|
shape: 'dot',
|
|
scaling: {{ min: 6, max: 28 }},
|
|
font: {{ color: '#e0e0e0', size: 10, face: 'Inter, sans-serif' }}
|
|
}},
|
|
edges: {{
|
|
color: {{ color: 'rgba(255,255,255,0.055)', highlight: 'rgba(255,255,255,0.3)', hover: 'rgba(255,255,255,0.2)' }},
|
|
smooth: {{ type: 'continuous', forceDirection: 'none', roundness: 0.4 }},
|
|
width: 0.5,
|
|
selectionWidth: 1.2
|
|
}},
|
|
physics: {{
|
|
stabilization: true,
|
|
stabilization.fit: true,
|
|
stabilization.iterations: 220,
|
|
barnesHut: {{
|
|
gravitationalConstant: -4800,
|
|
centralGravity: 0.12,
|
|
springLength: 180,
|
|
springConstant: 0.035,
|
|
damping: 0.35,
|
|
avoidOverlap: 0.15
|
|
}},
|
|
solver: 'barnesHut'
|
|
}},
|
|
interaction: {{
|
|
hover: true,
|
|
tooltipDelay: 100,
|
|
hideEdgesOnDrag: true,
|
|
navigationButtons: false,
|
|
keyboard: {{ enabled: true }}
|
|
}}
|
|
}};
|
|
|
|
var net = new vis.Network(container, {{ nodes: visNodes, edges: visEdges }}, options);
|
|
|
|
wireSearch(net);
|
|
wireLegend(net);
|
|
|
|
net.on('click', function(params) {{
|
|
if (params.nodes.length) {{
|
|
var nid = params.nodes[0];
|
|
var raw = RAW_NODES.find(function(x) {{ return x.id === nid; }});
|
|
if (raw) updateInfo(raw);
|
|
}}
|
|
}});
|
|
|
|
window.__brainNet = net;
|
|
}}
|
|
|
|
setTimeout(initGraph, 60);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
OUT.write_text(html, encoding="utf-8")
|
|
print(f"wrote {OUT} ({OUT.stat().st_size} bytes)")
|