<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Selfware CodeGraph & Context Builder</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');
:root {
--bg-base: #090a0f;
--panel-bg: rgba(18, 20, 26, 0.85);
--border-color: rgba(255, 255, 255, 0.08);
--text-main: #e6edf3;
--text-muted: #8b949e;
--accent: #58a6ff;
--success: #3fb950;
--danger: #f85149;
--node-file: #3fb950;
--node-struct: #d2a8ff;
--node-fn: #79c0ff;
--node-enum: #ff7b72;
--node-impl: #a5d6ff;
--node-section: #bc8cff;
--node-default: #8b949e;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg-base);
color: var(--text-main);
font-family: 'Inter', sans-serif;
overflow: hidden;
display: flex;
height: 100vh;
}
#graph-container {
flex: 1;
position: relative;
z-index: 1;
}
.sidebar {
width: 320px;
background: var(--panel-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
z-index: 10;
box-shadow: 2px 0 12px rgba(0,0,0,0.3);
}
.sidebar.right {
border-right: none;
border-left: 1px solid var(--border-color);
box-shadow: -2px 0 12px rgba(0,0,0,0.3);
width: 300px;
transform: translateX(100%);
position: absolute;
right: 0;
height: 100%;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.sidebar.right.visible {
transform: translateX(0);
}
.panel-header {
padding: 20px;
border-bottom: 1px solid var(--border-color);
}
.panel-header h1, .panel-header h2 {
margin: 0 0 8px 0;
font-weight: 600;
font-size: 1.1rem;
color: #fff;
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.token-budget {
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
text-align: center;
}
.token-budget .count {
font-size: 1.8rem;
font-weight: 600;
color: var(--success);
margin-bottom: 4px;
}
.token-budget .label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
}
.tree-node {
margin-bottom: 4px;
}
.tree-header {
display: flex;
align-items: center;
padding: 6px 8px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
}
.tree-header:hover {
background: rgba(255,255,255,0.05);
}
.tree-header input[type="checkbox"] {
margin-right: 10px;
accent-color: var(--success);
cursor: pointer;
}
.tree-label {
flex: 1;
font-size: 0.85rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tree-meta {
font-size: 0.7rem;
color: var(--text-muted);
background: rgba(255,255,255,0.05);
padding: 2px 6px;
border-radius: 4px;
}
.tree-children {
padding-left: 24px;
display: none;
}
.tree-node.expanded > .tree-children {
display: block;
}
.tree-node.expanded > .tree-header .chevron {
transform: rotate(90deg);
}
.chevron {
font-size: 0.7rem;
color: var(--text-muted);
margin-right: 6px;
transition: transform 0.2s;
display: inline-block;
width: 12px;
text-align: center;
}
.meta-stat {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 0.85rem;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.meta-stat .label { color: var(--text-muted); }
.meta-stat .value { font-weight: 500; color: var(--accent); max-width: 65%; text-align: right; word-break: break-all; }
.btn {
display: block;
width: 100%;
padding: 10px;
background: var(--success);
color: #000;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
margin-top: 20px;
transition: all 0.2s;
}
.btn:hover { background: #46cf59; transform: translateY(-1px); }
#search-box {
position: absolute;
top: 20px;
left: 340px;
z-index: 10;
width: 300px;
background: var(--panel-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px 16px;
display: flex;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
#search-box input {
background: transparent;
border: none;
color: #fff;
width: 100%;
outline: none;
font-family: inherit;
font-size: 0.9rem;
}
#loader {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-base); z-index: 100;
display: flex; justify-content: center; align-items: center;
transition: opacity 0.5s;
}
.spinner {
width: 40px; height: 40px;
border: 3px solid rgba(255,255,255,0.1); border-top-color: var(--accent);
border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 16px;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
</style>
<script src="https://unpkg.com/force-graph"></script>
</head>
<body>
<div id="loader">
<div style="text-align: center;">
<div class="spinner" style="margin: 0 auto 16px auto;"></div>
<div style="color: var(--text-muted); font-size: 0.85rem; letter-spacing: 1px;">BUILDING GRAPH...</div>
</div>
</div>
<div class="sidebar">
<div class="panel-header">
<h1>Context Builder</h1>
<p style="color: var(--text-muted); font-size: 0.75rem; margin: 0;">Select files and modules to include in your LLM context window.</p>
</div>
<div class="panel-body">
<div class="token-budget">
<div class="count" id="token-count">0</div>
<div class="label">Total Tokens in Context</div>
</div>
<div style="font-size: 0.8rem; font-weight: 600; color: #fff; margin-bottom: 12px; text-transform: uppercase;">Project Structure</div>
<div id="tree-container"></div>
</div>
</div>
<div id="search-box">
<input type="text" id="search-input" placeholder="Search files or functions..." autocomplete="off">
</div>
<div id="graph-container"></div>
<div class="sidebar right" id="info-panel">
<div class="panel-header" style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<div id="node-kind-badge" style="display:inline-block; padding:3px 8px; border-radius:4px; font-size:0.7rem; font-weight:600; text-transform:uppercase; margin-bottom:8px;">Node</div>
<h2 id="node-label" style="word-break: break-word; font-size: 1.1rem;">Node Name</h2>
</div>
<button style="background:transparent; border:none; color:var(--text-muted); cursor:pointer;" onclick="hideNodeDetails()">✕</button>
</div>
<div class="panel-body">
<div class="meta-stat">
<span class="label">ID</span>
<span class="value" id="node-id"></span>
</div>
<div class="meta-stat">
<span class="label">Path</span>
<span class="value" id="node-path"></span>
</div>
<div class="meta-stat">
<span class="label">Lines</span>
<span class="value" id="node-lines">0</span>
</div>
<div class="meta-stat">
<span class="label">Est. Tokens</span>
<span class="value" id="node-tokens">0</span>
</div>
<button class="btn" id="toggle-context-btn">Add to Context</button>
</div>
</div>
<script>
const colorMap = {
'file': getComputedStyle(document.documentElement).getPropertyValue('--node-file').trim(),
'struct': getComputedStyle(document.documentElement).getPropertyValue('--node-struct').trim(),
'fn': getComputedStyle(document.documentElement).getPropertyValue('--node-fn').trim(),
'function': getComputedStyle(document.documentElement).getPropertyValue('--node-fn').trim(),
'enum': getComputedStyle(document.documentElement).getPropertyValue('--node-enum').trim(),
'impl': getComputedStyle(document.documentElement).getPropertyValue('--node-impl').trim(),
'section': getComputedStyle(document.documentElement).getPropertyValue('--node-section').trim(),
};
const defaultColor = getComputedStyle(document.documentElement).getPropertyValue('--node-default').trim();
let rawNodes = [];
let rawLinks = [];
let nodeMap = {};
let sections = {};
let expandedSections = new Set();
let selectedContext = new Set();
let hoverNode = null;
let searchHighlightNodes = new Set();
let activeNode = null;
let currentRenderNodes = [];
let currentRenderLinks = [];
function getTokens(node) {
if (node.tokens_estimate && node.tokens_estimate > 0) return node.tokens_estimate;
if (node.lines && node.lines > 0) return node.lines * 10;
if (node.kind === 'section') return 0; return 50; }
const Graph = ForceGraph()
(document.getElementById('graph-container'))
.backgroundColor('#090a0f')
.nodeId('id')
.nodeLabel('label')
.nodeColor(n => {
if (hoverNode && hoverNode.id !== n.id && !isConnected(hoverNode, n)) return 'rgba(255,255,255,0.05)';
if (searchHighlightNodes.size > 0 && !searchHighlightNodes.has(n.id)) return 'rgba(255,255,255,0.05)';
return colorMap[n.kind] || defaultColor;
})
.nodeVal(n => {
if (n.kind === 'section') return Math.min(Math.max(Math.log2(n.children.length) * 2, 8), 24);
return Math.min(Math.max(Math.log2(getTokens(n)) / 1.5, 4), 16);
})
.nodeCanvasObjectMode(() => 'after')
.nodeCanvasObject((n, ctx, globalScale) => {
const isSelected = selectedContext.has(n.id) ||
(n.kind === 'section' && n.children.some(c => selectedContext.has(c.id)));
if (isSelected) {
const size = Math.sqrt(Graph.nodeVal()(n)) * 4; ctx.beginPath();
ctx.arc(n.x, n.y, size + 2, 0, 2 * Math.PI, false);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 2 / globalScale;
ctx.stroke();
}
})
.linkWidth(l => l.isHighlighted ? 2 : 0.5)
.linkColor(l => {
if (l.isHighlighted) return 'rgba(88, 166, 255, 0.8)';
if (hoverNode) return 'rgba(255,255,255,0.02)';
return 'rgba(255, 255, 255, 0.15)';
})
.linkDirectionalParticles(l => l.isHighlighted ? 3 : 0)
.onNodeHover(n => {
document.getElementById('graph-container').style.cursor = n ? 'pointer' : 'default';
hoverNode = n;
updateHighlights();
})
.onNodeClick(n => {
showNodeDetails(n);
if (n.kind === 'section') {
if (expandedSections.has(n.label)) expandedSections.delete(n.label);
else expandedSections.add(n.label);
updateGraphData();
setTimeout(() => { Graph.centerAt(n.x, n.y, 1000); Graph.zoom(4, 2000); }, 100);
} else {
Graph.centerAt(n.x, n.y, 1000);
Graph.zoom(8, 2000);
}
})
.onNodeRightClick(n => {
toggleContextSelection(n);
})
.onBackgroundClick(() => {
hideNodeDetails();
});
function isConnected(a, b) {
return currentRenderLinks.some(l => {
const sId = l.source.id || l.source;
const tId = l.target.id || l.target;
return (sId === a.id && tId === b.id) || (sId === b.id && tId === a.id);
});
}
function updateHighlights() {
currentRenderLinks.forEach(l => {
const sId = l.source.id || l.source;
const tId = l.target.id || l.target;
l.isHighlighted = hoverNode && (sId === hoverNode.id || tId === hoverNode.id);
});
Graph.nodeColor(Graph.nodeColor()).linkColor(Graph.linkColor());
}
function toggleContextSelection(node) {
if (node.kind === 'section') {
const allSelected = node.children.every(c => selectedContext.has(c.id));
node.children.forEach(c => {
if (allSelected) selectedContext.delete(c.id);
else selectedContext.add(c.id);
});
} else {
if (selectedContext.has(node.id)) selectedContext.delete(node.id);
else selectedContext.add(node.id);
}
updateContextUI();
}
function updateContextUI() {
let totalTokens = 0;
selectedContext.forEach(id => {
if (nodeMap[id]) totalTokens += getTokens(nodeMap[id]);
});
document.getElementById('token-count').innerText = totalTokens.toLocaleString();
document.querySelectorAll('.tree-checkbox').forEach(cb => {
const id = cb.dataset.id;
if (id.startsWith('section:')) {
const sec = sections[id.split(':')[1]];
if (sec) {
const countSelected = sec.children.filter(c => selectedContext.has(c.id)).length;
cb.checked = countSelected === sec.children.length && sec.children.length > 0;
cb.indeterminate = countSelected > 0 && countSelected < sec.children.length;
}
} else {
cb.checked = selectedContext.has(id);
}
});
if (activeNode) {
const btn = document.getElementById('toggle-context-btn');
const isSelected = activeNode.kind === 'section' ?
activeNode.children.every(c => selectedContext.has(c.id)) && activeNode.children.length > 0 :
selectedContext.has(activeNode.id);
btn.innerText = isSelected ? 'Remove from Context' : 'Add to Context';
btn.style.background = isSelected ? 'var(--danger)' : 'var(--success)';
}
Graph.nodeCanvasObject(Graph.nodeCanvasObject());
}
function buildTree() {
const container = document.getElementById('tree-container');
container.innerHTML = '';
Object.values(sections).sort((a,b) => a.label.localeCompare(b.label)).forEach(sec => {
const secTokens = sec.children.reduce((sum, c) => sum + getTokens(c), 0);
const nodeEl = document.createElement('div');
nodeEl.className = 'tree-node';
const header = document.createElement('div');
header.className = 'tree-header';
const chevron = document.createElement('span');
chevron.className = 'chevron';
chevron.innerText = '▶';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'tree-checkbox';
cb.dataset.id = sec.id;
cb.onclick = (e) => { e.stopPropagation(); toggleContextSelection(sec); };
const label = document.createElement('span');
label.className = 'tree-label';
label.innerText = sec.label;
const meta = document.createElement('span');
meta.className = 'tree-meta';
meta.innerText = secTokens >= 1000 ? (secTokens/1000).toFixed(1) + 'k' : secTokens;
header.appendChild(chevron);
header.appendChild(cb);
header.appendChild(label);
header.appendChild(meta);
header.onclick = () => {
nodeEl.classList.toggle('expanded');
};
const childrenContainer = document.createElement('div');
childrenContainer.className = 'tree-children';
sec.children.sort((a,b) => (a.label || a.id).localeCompare(b.label || b.id)).forEach(c => {
const cHeader = document.createElement('div');
cHeader.className = 'tree-header';
const cCb = document.createElement('input');
cCb.type = 'checkbox';
cCb.className = 'tree-checkbox';
cCb.dataset.id = c.id;
cCb.onclick = (e) => { e.stopPropagation(); toggleContextSelection(c); };
const cLabel = document.createElement('span');
cLabel.className = 'tree-label';
cLabel.innerText = c.label || c.id;
const cMeta = document.createElement('span');
cMeta.className = 'tree-meta';
cMeta.innerText = getTokens(c);
cHeader.appendChild(cCb);
cHeader.appendChild(cLabel);
cHeader.appendChild(cMeta);
cHeader.onclick = () => {
Graph.centerAt(c.x, c.y, 1000);
Graph.zoom(8, 1000);
showNodeDetails(c);
};
childrenContainer.appendChild(cHeader);
});
nodeEl.appendChild(header);
nodeEl.appendChild(childrenContainer);
container.appendChild(nodeEl);
});
}
function processGraphData() {
rawNodes.forEach(n => {
nodeMap[n.id] = n;
let sectionName = "other";
if (n.path) {
const parts = n.path.split('/');
if (parts.length > 1) sectionName = parts[1];
if (sectionName.endsWith('.rs') || (parts.length === 1 && n.path.endsWith('.rs'))) {
sectionName = "root";
}
} else if (n.id) {
sectionName = n.id.split('::')[0] || "other";
}
n.section = sectionName;
if (!sections[sectionName]) {
sections[sectionName] = {
id: 'section:' + sectionName,
label: sectionName,
kind: 'section',
children: [],
};
}
sections[sectionName].children.push(n);
});
}
function updateGraphData() {
currentRenderNodes = [];
currentRenderLinks = [];
Object.values(sections).forEach(sec => {
if (expandedSections.has(sec.label)) {
sec.children.forEach(c => currentRenderNodes.push(c));
} else {
currentRenderNodes.push(sec);
}
});
let linkMap = {};
rawLinks.forEach(l => {
let sourceNode = nodeMap[l.source];
let targetNode = nodeMap[l.target];
if(!sourceNode || !targetNode) return;
let sId = expandedSections.has(sourceNode.section) ? l.source : 'section:' + sourceNode.section;
let tId = expandedSections.has(targetNode.section) ? l.target : 'section:' + targetNode.section;
if (sId !== tId) {
let linkKey = sId + '|' + tId;
if(!linkMap[linkKey]) {
linkMap[linkKey] = { source: sId, target: tId, kind: l.kind || 'dependency', weight: 0 };
}
linkMap[linkKey].weight += l.weight || 1;
}
});
currentRenderLinks = Object.values(linkMap);
Graph.graphData({ nodes: currentRenderNodes, links: currentRenderLinks });
}
function showNodeDetails(node) {
activeNode = node;
const panel = document.getElementById('info-panel');
document.getElementById('node-label').innerText = node.label || node.id || "Unknown";
document.getElementById('node-id').innerText = node.id || "N/A";
if (node.kind === 'section') {
document.getElementById('node-path').innerText = `${node.children.length} items`;
document.getElementById('node-tokens').innerText = node.children.reduce((s, c) => s + getTokens(c), 0).toLocaleString();
document.getElementById('node-lines').innerText = node.children.reduce((s, c) => s + (c.lines||0), 0).toLocaleString();
} else {
document.getElementById('node-path').innerText = node.path || "N/A";
document.getElementById('node-tokens').innerText = getTokens(node).toLocaleString();
document.getElementById('node-lines').innerText = (node.lines || 0).toLocaleString();
}
const badge = document.getElementById('node-kind-badge');
badge.innerText = node.kind;
badge.style.backgroundColor = colorMap[node.kind] || defaultColor;
const btn = document.getElementById('toggle-context-btn');
btn.onclick = () => toggleContextSelection(node);
updateContextUI();
panel.classList.add('visible');
}
function hideNodeDetails() {
document.getElementById('info-panel').classList.remove('visible');
activeNode = null;
}
document.getElementById('search-input').addEventListener('input', e => {
const term = e.target.value.toLowerCase();
if (!term) {
searchHighlightNodes.clear();
} else {
searchHighlightNodes.clear();
currentRenderNodes.forEach(node => {
if ((node.label && node.label.toLowerCase().includes(term)) ||
(node.id && node.id.toLowerCase().includes(term))) {
searchHighlightNodes.add(node.id);
}
});
}
Graph.nodeColor(Graph.nodeColor()); });
fetch('codegraph.json')
.then(res => res.json())
.then(data => {
if (data.nodes) rawNodes = data.nodes;
if (data.edges) rawLinks = data.edges;
if (!data.nodes && Array.isArray(data)) {
rawNodes = data.filter(d => !d.source);
rawLinks = data.filter(d => d.source);
}
rawLinks = rawLinks.map(e => ({
source: e.source, target: e.target, kind: e.kind, weight: e.weight
}));
processGraphData();
buildTree();
updateGraphData();
updateContextUI();
setTimeout(() => {
const loader = document.getElementById('loader');
if(loader) {
loader.style.opacity = '0';
setTimeout(() => loader.remove(), 500);
}
}, 500);
Graph.d3Force('charge').strength(-60);
Graph.d3Force('link').distance(60);
})
.catch(err => {
document.getElementById('loader').innerHTML = `<div style="color: var(--danger)">Error loading codegraph.json: ${err.message}</div>`;
});
window.addEventListener('resize', () => {
Graph.width(document.getElementById('graph-container').clientWidth).height(window.innerHeight);
});
</script>
</body>
</html>