mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
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:
co-authored by
Claude Opus 4.6
parent
b07bdc2fb9
commit
7345d83376
@@ -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": []
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user