tokensave 4.0.0

Code intelligence tool that builds a semantic knowledge graph from Rust, Go, Java, Scala, TypeScript, Python, C, C++, Kotlin, C#, Swift, and many more codebases
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tokensave visualizer</title>
<script src="https://unpkg.com/cytoscape@3.30.4/dist/cytoscape.min.js"></script>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; background: #1a1b26; color: #c0caf5; display: flex; flex-direction: column; height: 100vh; }
  #header { padding: 12px 20px; background: #24283b; border-bottom: 1px solid #3b4261; display: flex; align-items: center; gap: 16px; }
  #header h1 { font-size: 16px; font-weight: 600; color: #7aa2f7; }
  #search-form { display: flex; gap: 8px; flex: 1; max-width: 600px; }
  #search-input { flex: 1; padding: 8px 12px; background: #1a1b26; border: 1px solid #3b4261; border-radius: 6px; color: #c0caf5; font-size: 14px; outline: none; }
  #search-input:focus { border-color: #7aa2f7; }
  #search-btn { padding: 8px 16px; background: #7aa2f7; color: #1a1b26; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 14px; }
  #search-btn:hover { background: #89b4fa; }
  #main { display: flex; flex: 1; overflow: hidden; }
  #cy { flex: 1; background: #1a1b26; }
  #detail { width: 420px; background: #24283b; border-left: 1px solid #3b4261; overflow-y: auto; padding: 16px; display: none; }
  #detail h2 { font-size: 14px; color: #7aa2f7; margin-bottom: 8px; }
  #detail .kind { font-size: 12px; color: #9ece6a; background: #1a1b26; padding: 2px 8px; border-radius: 4px; display: inline-block; margin-bottom: 8px; }
  #detail .file { font-size: 12px; color: #565f89; margin-bottom: 12px; }
  #detail .sig { font-size: 13px; color: #bb9af7; background: #1a1b26; padding: 8px; border-radius: 4px; margin-bottom: 12px; font-family: monospace; white-space: pre-wrap; word-break: break-all; }
  #detail .actions { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
  #detail .actions button { padding: 4px 10px; background: #3b4261; color: #c0caf5; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
  #detail .actions button:hover { background: #565f89; }
  #status-bar { padding: 6px 20px; background: #24283b; border-top: 1px solid #3b4261; font-size: 12px; color: #565f89; }
  .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #565f89; font-size: 14px; gap: 8px; }
  #ctx-menu { position: fixed; display: none; background: #24283b; border: 1px solid #3b4261; border-radius: 6px; padding: 4px 0; min-width: 160px; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
  #ctx-menu .ctx-item { padding: 6px 14px; font-size: 13px; color: #c0caf5; cursor: pointer; }
  #ctx-menu .ctx-item:hover { background: #3b4261; }
  #ctx-menu .ctx-header { padding: 6px 14px 4px; font-size: 11px; color: #565f89; border-bottom: 1px solid #3b4261; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
</style>
</head>
<body>
<div id="header">
  <h1>tokensave</h1>
  <form id="search-form">
    <input id="search-input" type="text" placeholder="Search symbols or ask a question..." autocomplete="off">
    <button id="search-btn" type="submit">Explore</button>
  </form>
</div>
<div id="main">
  <div id="cy">
    <div class="empty-state" id="empty-state">
      <div>Type a query to explore your code graph</div>
      <div style="font-size:12px">Try: "authentication", "database", or a function name</div>
    </div>
  </div>
  <div id="detail">
    <h2 id="detail-name"></h2>
    <span class="kind" id="detail-kind"></span>
    <div class="file" id="detail-file"></div>
    <div class="sig" id="detail-sig" style="display:none"></div>
    <div class="actions" id="detail-actions"></div>
  </div>
</div>
<div id="ctx-menu"></div>
<div id="status-bar">Loading...</div>
<script>
const cy = cytoscape({
  container: document.getElementById('cy'),
  style: [
    { selector: 'node', style: {
      'label': 'data(name)',
      'background-color': '#7aa2f7',
      'color': '#c0caf5',
      'font-size': '11px',
      'text-valign': 'bottom',
      'text-margin-y': 6,
      'text-max-width': 200,
      'text-wrap': 'ellipsis',
      'width': 24, 'height': 24,
      'border-width': 2, 'border-color': '#3b4261',
    }},
    { selector: 'node[kind="class"], node[kind="struct"], node[kind="interface"], node[kind="trait"]', style: {
      'background-color': '#bb9af7', 'shape': 'round-rectangle', 'width': 30, 'height': 24,
    }},
    { selector: 'node[kind="method"], node[kind="function"], node[kind="arrow_function"]', style: {
      'background-color': '#7aa2f7',
    }},
    { selector: 'node[kind="module"], node[kind="file"]', style: {
      'background-color': '#9ece6a', 'shape': 'diamond',
    }},
    { selector: 'node.root', style: {
      'border-color': '#f7768e', 'border-width': 3, 'width': 30, 'height': 30,
    }},
    { selector: 'node:selected', style: {
      'border-color': '#ff9e64', 'border-width': 3,
    }},
    { selector: 'edge', style: {
      'width': 1.5,
      'line-color': '#3b4261',
      'target-arrow-color': '#3b4261',
      'target-arrow-shape': 'triangle',
      'curve-style': 'bezier',
      'arrow-scale': 0.8,
    }},
    { selector: 'edge[kind="calls"]', style: { 'line-color': '#7aa2f7', 'target-arrow-color': '#7aa2f7' }},
    { selector: 'edge[kind="contains"]', style: { 'line-color': '#3b4261', 'line-style': 'dashed' }},
    { selector: 'edge[kind="extends"], edge[kind="implements"]', style: { 'line-color': '#bb9af7', 'target-arrow-color': '#bb9af7' }},
  ],
  layout: { name: 'cose', animate: false, nodeDimensionsIncludeLabels: true },
  minZoom: 0.2, maxZoom: 5,
});

// Status bar
fetch('/api/status').then(r=>r.json()).then(d=>{
  const s = d.stats;
  document.getElementById('status-bar').textContent =
    `${s.node_count.toLocaleString()} nodes | ${s.edge_count.toLocaleString()} edges | ${s.file_count.toLocaleString()} files | ${d.projectRoot}`;
}).catch(()=>{
  document.getElementById('status-bar').textContent = 'Failed to load status';
});

// Search / Explore
document.getElementById('search-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const q = document.getElementById('search-input').value.trim();
  if (!q) return;
  document.getElementById('empty-state').style.display = 'none';
  document.getElementById('status-bar').textContent = 'Exploring...';

  try {
    const res = await fetch(`/api/explore?q=${encodeURIComponent(q)}`);
    const data = await res.json();
    renderGraph(data);
    document.getElementById('status-bar').textContent =
      `${data.nodes.length} nodes, ${data.edges.length} edges`;
  } catch (err) {
    document.getElementById('status-bar').textContent = `Error: ${err.message}`;
  }
});

function renderGraph(data) {
  cy.elements().remove();
  const elements = [];

  for (const node of data.nodes) {
    elements.push({
      data: {
        id: node.id,
        kind: node.kind,
        name: node.name,
        file: node.file_path,
        line: node.start_line,
        sig: node.signature || '',
      },
      classes: data.roots && data.roots.includes(node.id) ? 'root' : '',
    });
  }

  for (const edge of data.edges) {
    elements.push({
      data: { source: edge.source, target: edge.target, kind: edge.kind },
    });
  }

  cy.add(elements);
  cy.layout({ name: 'cose', animate: false, nodeDimensionsIncludeLabels: true, padding: 40 }).run();
}

// Node click -> detail panel
cy.on('tap', 'node', async function(evt) {
  const node = evt.target;
  const d = node.data();
  document.getElementById('detail').style.display = 'block';
  document.getElementById('detail-name').textContent = d.name;
  document.getElementById('detail-kind').textContent = d.kind;
  document.getElementById('detail-file').textContent = `${d.file}:${d.line}`;
  if (d.sig) {
    document.getElementById('detail-sig').textContent = d.sig;
    document.getElementById('detail-sig').style.display = 'block';
  } else {
    document.getElementById('detail-sig').style.display = 'none';
  }

  const actions = document.getElementById('detail-actions');
  actions.innerHTML = '';
  const btns = [
    { label: 'Callers', endpoint: 'callers' },
    { label: 'Callees', endpoint: 'callees' },
    { label: 'Call Graph', endpoint: 'callgraph' },
    { label: 'Impact', endpoint: 'impact' },
  ];
  for (const btn of btns) {
    const b = document.createElement('button');
    b.textContent = btn.label;
    b.onclick = async () => {
      const res = await fetch(`/api/node/${encodeURIComponent(d.id)}/${btn.endpoint}?depth=2`);
      const data = await res.json();
      renderGraph(data);
      document.getElementById('status-bar').textContent =
        `${btn.label} of ${d.name}: ${data.nodes.length} nodes`;
    };
    actions.appendChild(b);
  }
});

// Click background -> hide detail
cy.on('tap', function(evt) {
  if (evt.target === cy) {
    document.getElementById('detail').style.display = 'none';
  }
});

// Right-click context menu
const ctxMenu = document.getElementById('ctx-menu');
let ctxNodeData = null;

function hideCtxMenu() { ctxMenu.style.display = 'none'; ctxNodeData = null; }

function showCtxMenu(x, y, d) {
  ctxNodeData = d;
  const items = [
    { label: 'Show Callers', endpoint: 'callers' },
    { label: 'Show Callees', endpoint: 'callees' },
    { label: 'Show Call Graph', endpoint: 'callgraph' },
    { label: 'Show Impact', endpoint: 'impact' },
  ];
  ctxMenu.innerHTML = `<div class="ctx-header" title="${d.name}">${d.name}</div>` +
    items.map(i => `<div class="ctx-item" data-ep="${i.endpoint}">${i.label}</div>`).join('');
  ctxMenu.style.display = 'block';

  // Position within viewport
  const rect = ctxMenu.getBoundingClientRect();
  const mx = Math.min(x, window.innerWidth - rect.width - 4);
  const my = Math.min(y, window.innerHeight - rect.height - 4);
  ctxMenu.style.left = mx + 'px';
  ctxMenu.style.top = my + 'px';
}

ctxMenu.addEventListener('click', async (e) => {
  const item = e.target.closest('.ctx-item');
  if (!item || !ctxNodeData) return;
  const ep = item.dataset.ep;
  const d = ctxNodeData;
  hideCtxMenu();
  try {
    const res = await fetch(`/api/node/${encodeURIComponent(d.id)}/${ep}?depth=2`);
    const data = await res.json();
    renderGraph(data);
    const label = item.textContent;
    document.getElementById('status-bar').textContent =
      `${label.replace('Show ', '')} of ${d.name}: ${data.nodes.length} nodes`;
  } catch (err) {
    document.getElementById('status-bar').textContent = `Error: ${err.message}`;
  }
});

// Cytoscape right-click on node
cy.on('cxttap', 'node', function(evt) {
  const d = evt.target.data();
  const pos = evt.originalEvent;
  showCtxMenu(pos.clientX, pos.clientY, d);
});

// Dismiss context menu on click/tap/pan/zoom
document.addEventListener('click', (e) => { if (!ctxMenu.contains(e.target)) hideCtxMenu(); });
cy.on('tap pan zoom', hideCtxMenu);

// Suppress browser context menu on the graph
document.getElementById('cy').addEventListener('contextmenu', (e) => e.preventDefault());
</script>
</body>
</html>