feat(dashboard): add search bar, node info, and main 4-panel layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-03-14 17:58:01 +08:00
co-authored by Claude Opus 4.6
parent b07bdc2fb9
commit 7345d83376
4 changed files with 255 additions and 2 deletions
@@ -0,0 +1,27 @@
{
"version": "1.0.0",
"project": {
"name": "sample-project",
"languages": ["typescript"],
"frameworks": ["express"],
"description": "A sample Express API",
"analyzedAt": "2026-03-14T00:00:00.000Z",
"gitCommitHash": "abc123"
},
"nodes": [
{"id": "file:src/index.ts", "type": "file", "name": "index.ts", "filePath": "src/index.ts", "summary": "Application entry point, starts the Express server", "tags": ["entry", "server"], "complexity": "simple"},
{"id": "file:src/auth/login.ts", "type": "file", "name": "login.ts", "filePath": "src/auth/login.ts", "summary": "Handles user authentication and login flow", "tags": ["auth", "login"], "complexity": "moderate"},
{"id": "func:src/auth/login.ts:handleLogin", "type": "function", "name": "handleLogin", "filePath": "src/auth/login.ts", "lineRange": [10, 35], "summary": "Validates credentials and returns a JWT token", "tags": ["auth", "jwt"], "complexity": "moderate"},
{"id": "func:src/auth/login.ts:validateEmail", "type": "function", "name": "validateEmail", "filePath": "src/auth/login.ts", "lineRange": [37, 42], "summary": "Checks if an email address is valid using regex", "tags": ["validation", "email"], "complexity": "simple"},
{"id": "file:src/db/connection.ts", "type": "file", "name": "connection.ts", "filePath": "src/db/connection.ts", "summary": "Database connection pool using PostgreSQL", "tags": ["database", "postgres"], "complexity": "moderate"}
],
"edges": [
{"source": "file:src/index.ts", "target": "file:src/auth/login.ts", "type": "imports", "direction": "forward", "weight": 0.7},
{"source": "file:src/auth/login.ts", "target": "func:src/auth/login.ts:handleLogin", "type": "contains", "direction": "forward", "weight": 1.0},
{"source": "file:src/auth/login.ts", "target": "func:src/auth/login.ts:validateEmail", "type": "contains", "direction": "forward", "weight": 1.0},
{"source": "func:src/auth/login.ts:handleLogin", "target": "func:src/auth/login.ts:validateEmail", "type": "calls", "direction": "forward", "description": "handleLogin calls validateEmail to check the email format", "weight": 0.8},
{"source": "file:src/auth/login.ts", "target": "file:src/db/connection.ts", "type": "imports", "direction": "forward", "weight": 0.7}
],
"layers": [],
"tour": []
}
+65 -2
View File
@@ -1,7 +1,70 @@
import { useEffect } from "react";
import type { KnowledgeGraph } from "@understand-anything/core";
import { useDashboardStore } from "./store";
import GraphView from "./components/GraphView";
import CodeViewer from "./components/CodeViewer";
import SearchBar from "./components/SearchBar";
import NodeInfo from "./components/NodeInfo";
function App() {
const graph = useDashboardStore((s) => s.graph);
const setGraph = useDashboardStore((s) => s.setGraph);
useEffect(() => {
fetch("/knowledge-graph.json")
.then((res) => res.json())
.then((data: KnowledgeGraph) => setGraph(data))
.catch((err) => console.error("Failed to load knowledge graph:", err));
}, [setGraph]);
const nodeCount = graph?.nodes.length ?? 0;
const edgeCount = graph?.edges.length ?? 0;
return (
<div className="h-screen w-screen bg-gray-900 text-white flex items-center justify-center">
<h1 className="text-2xl font-bold">Understand Anything</h1>
<div className="h-screen w-screen flex flex-col bg-gray-900 text-white">
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 bg-gray-950 border-b border-gray-800 shrink-0">
<h1 className="text-sm font-bold tracking-widest text-gray-200">
UNDERSTAND ANYTHING
</h1>
<div className="flex items-center gap-4 text-xs text-gray-400">
{graph && (
<>
<span>{graph.project.name}</span>
<span className="text-gray-600">|</span>
<span>{nodeCount} nodes</span>
<span className="text-gray-600">|</span>
<span>{edgeCount} edges</span>
</>
)}
</div>
</div>
{/* Search Bar */}
<SearchBar />
{/* 4-panel grid */}
<div className="flex-1 grid grid-cols-2 grid-rows-2 gap-1 p-1 min-h-0">
{/* Top-left: Graph View */}
<div className="min-h-0 min-w-0">
<GraphView />
</div>
{/* Top-right: Code Viewer */}
<div className="min-h-0 min-w-0">
<CodeViewer />
</div>
{/* Bottom-left: Chat placeholder */}
<div className="min-h-0 min-w-0 bg-gray-800 rounded-lg flex items-center justify-center">
<p className="text-gray-400 text-sm">Chat panel (coming soon)</p>
</div>
{/* Bottom-right: Node Info */}
<div className="min-h-0 min-w-0">
<NodeInfo />
</div>
</div>
</div>
);
}
@@ -0,0 +1,126 @@
import { useDashboardStore } from "../store";
const typeBadgeColors: Record<string, string> = {
file: "bg-blue-800 text-blue-200",
function: "bg-green-800 text-green-200",
class: "bg-purple-800 text-purple-200",
module: "bg-orange-800 text-orange-200",
concept: "bg-pink-800 text-pink-200",
};
const complexityBadgeColors: Record<string, string> = {
simple: "bg-green-700 text-green-200",
moderate: "bg-yellow-700 text-yellow-200",
complex: "bg-red-700 text-red-200",
};
export default function NodeInfo() {
const graph = useDashboardStore((s) => s.graph);
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const node = graph?.nodes.find((n) => n.id === selectedNodeId) ?? null;
if (!node) {
return (
<div className="h-full w-full flex items-center justify-center bg-gray-800 rounded-lg">
<p className="text-gray-400 text-sm">Select a node to see details</p>
</div>
);
}
const connections = (graph?.edges ?? []).filter(
(e) => e.source === node.id || e.target === node.id,
);
const typeBadge = typeBadgeColors[node.type] ?? typeBadgeColors.file;
const complexityBadge =
complexityBadgeColors[node.complexity] ?? complexityBadgeColors.simple;
return (
<div className="h-full w-full bg-gray-800 rounded-lg overflow-auto p-4">
<div className="flex items-center gap-2 mb-3">
<span
className={`text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded ${typeBadge}`}
>
{node.type}
</span>
<span
className={`text-[10px] font-semibold px-2 py-0.5 rounded ${complexityBadge}`}
>
{node.complexity}
</span>
</div>
<h2 className="text-lg font-bold text-white mb-2">{node.name}</h2>
<p className="text-sm text-gray-300 mb-4 leading-relaxed">
{node.summary}
</p>
{node.filePath && (
<div className="text-xs text-gray-400 mb-2">
<span className="font-medium text-gray-500">File:</span>{" "}
{node.filePath}
{node.lineRange && (
<span className="ml-2">
(L{node.lineRange[0]}-{node.lineRange[1]})
</span>
)}
</div>
)}
{node.languageNotes && (
<div className="bg-blue-900/50 border border-blue-700 rounded p-3 mb-4 text-sm text-blue-200">
{node.languageNotes}
</div>
)}
{node.tags.length > 0 && (
<div className="mb-4">
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
Tags
</h3>
<div className="flex flex-wrap gap-1.5">
{node.tags.map((tag) => (
<span
key={tag}
className="text-[11px] bg-gray-700 text-gray-300 px-2 py-0.5 rounded-full"
>
{tag}
</span>
))}
</div>
</div>
)}
{connections.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
Connections ({connections.length})
</h3>
<div className="space-y-1.5">
{connections.map((edge, i) => {
const isSource = edge.source === node.id;
const otherId = isSource ? edge.target : edge.source;
const otherNode = graph?.nodes.find((n) => n.id === otherId);
const arrow = isSource ? "\u2192" : "\u2190";
return (
<div
key={i}
className="text-xs bg-gray-700/50 rounded px-2 py-1.5 flex items-center gap-2"
>
<span className="text-gray-400 font-mono">{arrow}</span>
<span className="text-gray-400">{edge.type}</span>
<span className="text-white truncate">
{otherNode?.name ?? otherId}
</span>
</div>
);
})}
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,37 @@
import { useDashboardStore } from "../store";
export default function SearchBar() {
const searchQuery = useDashboardStore((s) => s.searchQuery);
const searchResults = useDashboardStore((s) => s.searchResults);
const setSearchQuery = useDashboardStore((s) => s.setSearchQuery);
return (
<div className="flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700">
<svg
className="w-4 h-4 text-gray-400 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search nodes by name, summary, or tags..."
className="flex-1 bg-gray-700 text-white text-sm rounded px-3 py-1.5 border border-gray-600 focus:outline-none focus:border-blue-500 placeholder-gray-400"
/>
{searchQuery.trim() && (
<span className="text-xs text-gray-400 shrink-0">
{searchResults.length} result{searchResults.length !== 1 ? "s" : ""}
</span>
)}
</div>
);
}