fischer-agentkit/.understand-anything/dashboard.html

316 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fischer AgentKit - Knowledge Graph Dashboard</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; overflow: hidden; height: 100vh; }
.header { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #334155; }
.header h1 { font-size: 18px; font-weight: 600; background: linear-gradient(135deg, #60a5fa, #a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.stats { display: flex; gap: 16px; font-size: 13px; color: #94a3b8; }
.stats span { padding: 4px 10px; background: #1e293b; border-radius: 6px; border: 1px solid #334155; }
.main { display: flex; height: calc(100vh - 52px); }
.sidebar { width: 340px; background: #1e293b; border-right: 1px solid #334155; display: flex; flex-direction: column; overflow: hidden; }
.search-box { padding: 12px; border-bottom: 1px solid #334155; }
.search-box input { width: 100%; padding: 8px 12px; background: #0f172a; border: 1px solid #475569; border-radius: 8px; color: #e2e8f0; font-size: 14px; outline: none; }
.search-box input:focus { border-color: #60a5fa; }
.tabs { display: flex; border-bottom: 1px solid #334155; }
.tab { flex: 1; padding: 8px; text-align: center; font-size: 13px; cursor: pointer; color: #94a3b8; border-bottom: 2px solid transparent; transition: all 0.2s; }
.tab.active { color: #60a5fa; border-bottom-color: #60a5fa; }
.tab:hover { color: #e2e8f0; }
.content { flex: 1; overflow-y: auto; padding: 8px; }
.content::-webkit-scrollbar { width: 6px; }
.content::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
.node-item { padding: 8px 12px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; transition: background 0.15s; border: 1px solid transparent; }
.node-item:hover { background: #334155; border-color: #475569; }
.node-item.selected { background: #1e3a5f; border-color: #60a5fa; }
.node-item .name { font-size: 13px; font-weight: 500; color: #e2e8f0; }
.node-item .meta { font-size: 11px; color: #64748b; margin-top: 2px; }
.badge { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 600; margin-left: 6px; }
.badge.file { background: #1e3a5f; color: #60a5fa; }
.badge.class { background: #3b1f5e; color: #c084fc; }
.badge.function { background: #1a3d2e; color: #4ade80; }
.badge.api { background: #7c2d12; color: #fb923c; }
.badge.service { background: #1e3a5f; color: #60a5fa; }
.badge.data { background: #3b1f5e; color: #c084fc; }
.badge.utility { background: #1a3d2e; color: #4ade80; }
.graph-container { flex: 1; position: relative; }
#graph { width: 100%; height: 100%; }
.detail-panel { position: absolute; right: 16px; top: 16px; width: 320px; background: rgba(30,41,59,0.95); border: 1px solid #475569; border-radius: 12px; padding: 16px; display: none; backdrop-filter: blur(12px); max-height: calc(100vh - 100px); overflow-y: auto; }
.detail-panel.show { display: block; }
.detail-panel h3 { font-size: 15px; color: #60a5fa; margin-bottom: 8px; }
.detail-panel .summary { font-size: 13px; color: #cbd5e1; line-height: 1.5; margin-bottom: 12px; }
.detail-panel .field { font-size: 12px; color: #94a3b8; margin-bottom: 6px; }
.detail-panel .field strong { color: #e2e8f0; }
.detail-panel .edges-list { margin-top: 8px; }
.detail-panel .edge-item { font-size: 12px; padding: 4px 8px; background: #0f172a; border-radius: 4px; margin-bottom: 3px; cursor: pointer; }
.detail-panel .edge-item:hover { background: #334155; }
.tour-panel { padding: 8px; }
.tour-card { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 12px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.2s; }
.tour-card:hover { border-color: #60a5fa; }
.tour-card h4 { font-size: 13px; color: #e2e8f0; margin-bottom: 4px; }
.tour-card p { font-size: 11px; color: #64748b; }
.tour-steps { margin-top: 8px; }
.tour-step { display: flex; align-items: center; gap: 8px; padding: 6px 8px; font-size: 12px; color: #94a3b8; border-radius: 4px; cursor: pointer; }
.tour-step:hover { background: #334155; color: #e2e8f0; }
.tour-step .num { width: 20px; height: 20px; background: #334155; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #60a5fa; flex-shrink: 0; }
.layer-filter { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px 12px; border-bottom: 1px solid #334155; }
.layer-btn { padding: 3px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; border: 1px solid #475569; background: transparent; color: #94a3b8; transition: all 0.15s; }
.layer-btn.active { background: #334155; color: #e2e8f0; border-color: #60a5fa; }
.close-btn { position: absolute; top: 8px; right: 8px; background: none; border: none; color: #94a3b8; cursor: pointer; font-size: 16px; }
.close-btn:hover { color: #e2e8f0; }
.loading { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 16px; color: #60a5fa; }
</style>
</head>
<body>
<div class="header">
<h1>Fischer AgentKit Knowledge Graph</h1>
<div class="stats" id="stats"></div>
</div>
<div class="main">
<div class="sidebar">
<div class="search-box">
<input type="text" id="search" placeholder="Search nodes..." />
</div>
<div class="layer-filter" id="layerFilter"></div>
<div class="tabs">
<div class="tab active" data-tab="nodes">Nodes</div>
<div class="tab" data-tab="tours">Tours</div>
</div>
<div class="content" id="nodeList"></div>
<div class="content" id="tourList" style="display:none"></div>
</div>
<div class="graph-container">
<div id="graph"></div>
<div class="detail-panel" id="detailPanel">
<button class="close-btn" onclick="document.getElementById('detailPanel').classList.remove('show')">&times;</button>
<div id="detailContent"></div>
</div>
</div>
</div>
<script>
let graphData = null;
let network = null;
let nodesDataset = null;
let edgesDataset = null;
let activeLayers = new Set(['api', 'service', 'data', 'utility', 'unknown']);
let selectedNodeId = null;
const LAYER_COLORS = {
api: { bg: '#fb923c', border: '#ea580c', highlight: '#fdba74' },
service: { bg: '#60a5fa', border: '#2563eb', highlight: '#93c5fd' },
data: { bg: '#c084fc', border: '#9333ea', highlight: '#d8b4fe' },
utility: { bg: '#4ade80', border: '#16a34a', highlight: '#86efac' },
unknown: { bg: '#94a3b8', border: '#64748b', highlight: '#cbd5e1' }
};
const TYPE_SHAPES = { file: 'box', class: 'diamond', function: 'dot' };
async function loadGraph() {
try {
const resp = await fetch('knowledge-graph.json');
graphData = await resp.json();
initDashboard();
} catch(e) {
document.getElementById('graph').innerHTML = '<div class="loading">Failed to load knowledge-graph.json</div>';
}
}
function initDashboard() {
const { nodes, edges, tours, project } = graphData;
document.getElementById('stats').innerHTML = `
<span>${nodes.length} Nodes</span>
<span>${edges.length} Edges</span>
<span>${tours.length} Tours</span>
<span>${project.languages.join(', ')}</span>
`;
renderLayerFilter(nodes);
renderNodeList(nodes);
renderTourList(tours);
initGraph(nodes, edges);
initSearch(nodes);
initTabs();
}
function renderLayerFilter(nodes) {
const layers = [...new Set(nodes.map(n => n.layer).filter(Boolean))];
const container = document.getElementById('layerFilter');
container.innerHTML = layers.map(l =>
`<button class="layer-btn active" data-layer="${l}">${l}</button>`
).join('');
container.querySelectorAll('.layer-btn').forEach(btn => {
btn.onclick = () => {
const layer = btn.dataset.layer;
if (activeLayers.has(layer)) { activeLayers.delete(layer); btn.classList.remove('active'); }
else { activeLayers.add(layer); btn.classList.add('active'); }
filterGraph();
};
});
}
function renderNodeList(nodes) {
const container = document.getElementById('nodeList');
const filtered = nodes.filter(n => activeLayers.has(n.layer));
container.innerHTML = filtered.slice(0, 200).map(n => `
<div class="node-item" data-id="${n.id}">
<div class="name">${n.name} <span class="badge ${n.type}">${n.type}</span> <span class="badge ${n.layer}">${n.layer}</span></div>
<div class="meta">${n.filePath || ''}</div>
</div>
`).join('');
container.querySelectorAll('.node-item').forEach(el => {
el.onclick = () => focusNode(el.dataset.id);
});
}
function renderTourList(tours) {
const container = document.getElementById('tourList');
container.innerHTML = tours.map((t, i) => `
<div class="tour-card" data-tour="${i}">
<h4>${t.name}</h4>
<p>${t.description}</p>
<div class="tour-steps">
${t.steps.map((s, j) => `
<div class="tour-step" data-node="${s.nodeId}">
<span class="num">${j+1}</span>
<span>${s.why || s.nodeId}</span>
</div>
`).join('')}
</div>
</div>
`).join('');
container.querySelectorAll('.tour-step').forEach(el => {
el.onclick = (e) => { e.stopPropagation(); focusNode(el.dataset.node); };
});
}
function initGraph(nodes, edges) {
const visNodes = nodes.filter(n => activeLayers.has(n.layer)).map(n => {
const colors = LAYER_COLORS[n.layer] || LAYER_COLORS.unknown;
return {
id: n.id,
label: n.name,
shape: TYPE_SHAPES[n.type] || 'dot',
color: colors,
font: { color: '#e2e8f0', size: n.type === 'file' ? 12 : 10 },
size: n.type === 'file' ? 20 : n.type === 'class' ? 15 : 8,
title: `${n.name}\n${n.summary || ''}\n[${n.layer}]`,
...n
};
});
const nodeIds = new Set(visNodes.map(n => n.id));
const visEdges = edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target)).map((e, i) => ({
id: e.id || `edge-${i}`,
from: e.source,
to: e.target,
arrows: e.type === 'contains' ? '' : 'to',
color: { color: '#334155', highlight: '#60a5fa', hover: '#475569' },
width: 0.5,
dashes: e.type === 'imports',
title: e.type
}));
nodesDataset = new vis.DataSet(visNodes);
edgesDataset = new vis.DataSet(visEdges);
const container = document.getElementById('graph');
const data = { nodes: nodesDataset, edges: edgesDataset };
const options = {
physics: { barnesHut: { gravitationalConstant: -3000, centralGravity: 0.3, springLength: 80, springConstant: 0.04 }, stabilization: { iterations: 100 } },
interaction: { hover: true, tooltipDelay: 200, navigationButtons: true, keyboard: true },
layout: { improvedLayout: true }
};
network = new vis.Network(container, data, options);
network.on('click', params => {
if (params.nodes.length > 0) focusNode(params.nodes[0]);
});
}
function filterGraph() {
const nodes = graphData.nodes.filter(n => activeLayers.has(n.layer));
renderNodeList(nodes);
if (nodesDataset) {
const nodeIds = new Set(nodes.map(n => n.id));
const edges = graphData.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
nodesDataset.clear();
nodesDataset.add(nodes.map(n => {
const colors = LAYER_COLORS[n.layer] || LAYER_COLORS.unknown;
return { id: n.id, label: n.name, shape: TYPE_SHAPES[n.type] || 'dot', color: colors, font: { color: '#e2e8f0', size: n.type === 'file' ? 12 : 10 }, size: n.type === 'file' ? 20 : n.type === 'class' ? 15 : 8, title: `${n.name}\n${n.summary || ''}\n[${n.layer}]`, ...n };
}));
edgesDataset.clear();
edgesDataset.add(edges.map((e, i) => ({ id: e.id || `edge-${i}`, from: e.source, to: e.target, arrows: e.type === 'contains' ? '' : 'to', color: { color: '#334155', highlight: '#60a5fa', hover: '#475569' }, width: 0.5, dashes: e.type === 'imports', title: e.type })));
}
}
function focusNode(nodeId) {
selectedNodeId = nodeId;
const node = graphData.nodes.find(n => n.id === nodeId);
if (!node) return;
document.querySelectorAll('.node-item').forEach(el => el.classList.remove('selected'));
const el = document.querySelector(`.node-item[data-id="${nodeId}"]`);
if (el) { el.classList.add('selected'); el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
if (network) {
network.focus(nodeId, { scale: 1.5, animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
network.selectNodes([nodeId]);
}
const inEdges = graphData.edges.filter(e => e.target === nodeId);
const outEdges = graphData.edges.filter(e => e.source === nodeId);
const panel = document.getElementById('detailPanel');
const content = document.getElementById('detailContent');
content.innerHTML = `
<h3>${node.name}</h3>
<div class="summary">${node.summary || 'No summary'}</div>
<div class="field"><strong>Type:</strong> ${node.type}</div>
<div class="field"><strong>Layer:</strong> ${node.layer}</div>
<div class="field"><strong>Path:</strong> ${node.filePath || '-'}</div>
<div class="field"><strong>Complexity:</strong> ${node.complexity || '-'}</div>
${node.tags ? `<div class="field"><strong>Tags:</strong> ${node.tags.join(', ')}</div>` : ''}
${inEdges.length > 0 ? `<div class="edges-list"><strong>Incoming (${inEdges.length}):</strong>${inEdges.slice(0,10).map(e => `<div class="edge-item" onclick="focusNode('${e.source}')">${e.type}${e.source.split(':').pop()}</div>`).join('')}</div>` : ''}
${outEdges.length > 0 ? `<div class="edges-list"><strong>Outgoing (${outEdges.length}):</strong>${outEdges.slice(0,10).map(e => `<div class="edge-item" onclick="focusNode('${e.target}')">${e.type}${e.target.split(':').pop()}</div>`).join('')}</div>` : ''}
`;
panel.classList.add('show');
}
function initSearch(nodes) {
const input = document.getElementById('search');
input.oninput = () => {
const q = input.value.toLowerCase();
const filtered = nodes.filter(n =>
activeLayers.has(n.layer) &&
(n.name.toLowerCase().includes(q) || (n.summary || '').toLowerCase().includes(q) || (n.filePath || '').toLowerCase().includes(q))
);
renderNodeList(filtered);
if (q.length > 1 && network) {
const matchIds = filtered.map(n => n.id);
if (matchIds.length > 0 && matchIds.length < 50) {
network.fit({ nodes: matchIds, animation: { duration: 500 } });
}
}
};
}
function initTabs() {
document.querySelectorAll('.tab').forEach(tab => {
tab.onclick = () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const target = tab.dataset.tab;
document.getElementById('nodeList').style.display = target === 'nodes' ? 'block' : 'none';
document.getElementById('tourList').style.display = target === 'tours' ? 'block' : 'none';
};
});
}
loadGraph();
</script>
</body>
</html>