mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(dashboard): add graph view with React Flow and custom nodes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import type { NodeProps, Node } from "@xyflow/react";
|
||||
|
||||
const typeColors: Record<string, { bg: string; border: string; text: string }> =
|
||||
{
|
||||
file: {
|
||||
bg: "bg-blue-900",
|
||||
border: "border-blue-500",
|
||||
text: "text-blue-300",
|
||||
},
|
||||
function: {
|
||||
bg: "bg-green-900",
|
||||
border: "border-green-500",
|
||||
text: "text-green-300",
|
||||
},
|
||||
class: {
|
||||
bg: "bg-purple-900",
|
||||
border: "border-purple-500",
|
||||
text: "text-purple-300",
|
||||
},
|
||||
module: {
|
||||
bg: "bg-orange-900",
|
||||
border: "border-orange-500",
|
||||
text: "text-orange-300",
|
||||
},
|
||||
concept: {
|
||||
bg: "bg-pink-900",
|
||||
border: "border-pink-500",
|
||||
text: "text-pink-300",
|
||||
},
|
||||
};
|
||||
|
||||
const complexityColors: Record<string, string> = {
|
||||
simple: "bg-green-600",
|
||||
moderate: "bg-yellow-600",
|
||||
complex: "bg-red-600",
|
||||
};
|
||||
|
||||
export interface CustomNodeData extends Record<string, unknown> {
|
||||
label: string;
|
||||
nodeType: string;
|
||||
summary: string;
|
||||
complexity: string;
|
||||
isHighlighted: boolean;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export type CustomFlowNode = Node<CustomNodeData, "custom">;
|
||||
|
||||
export default function CustomNode({
|
||||
data,
|
||||
}: NodeProps<CustomFlowNode>) {
|
||||
const colors = typeColors[data.nodeType] ?? typeColors.file;
|
||||
const complexityColor =
|
||||
complexityColors[data.complexity] ?? complexityColors.simple;
|
||||
|
||||
let ringClass = "";
|
||||
if (data.isSelected) {
|
||||
ringClass = "ring-2 ring-white";
|
||||
} else if (data.isHighlighted) {
|
||||
ringClass = "ring-2 ring-yellow-400";
|
||||
}
|
||||
|
||||
const truncatedName =
|
||||
data.label.length > 24 ? data.label.slice(0, 22) + "..." : data.label;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg border-2 ${colors.bg} ${colors.border} ${ringClass} px-3 py-2 min-w-[180px] max-w-[220px] shadow-lg`}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="!bg-gray-400 !w-2 !h-2"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span
|
||||
className={`text-[10px] font-semibold uppercase tracking-wider ${colors.text}`}
|
||||
>
|
||||
{data.nodeType}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[9px] px-1.5 py-0.5 rounded-full text-white font-medium ${complexityColor}`}
|
||||
>
|
||||
{data.complexity}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm font-bold text-white truncate" title={data.label}>
|
||||
{truncatedName}
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] text-gray-300 mt-1 line-clamp-2 leading-tight">
|
||||
{data.summary}
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="!bg-gray-400 !w-2 !h-2"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import {
|
||||
ReactFlow,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
} from "@xyflow/react";
|
||||
import type { Edge } from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
|
||||
import CustomNode from "./CustomNode";
|
||||
import type { CustomFlowNode } from "./CustomNode";
|
||||
import { useDashboardStore } from "../store";
|
||||
|
||||
const nodeTypes = { custom: CustomNode };
|
||||
|
||||
export default function GraphView() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
const searchResults = useDashboardStore((s) => s.searchResults);
|
||||
const selectNode = useDashboardStore((s) => s.selectNode);
|
||||
|
||||
const initialNodes = useMemo<CustomFlowNode[]>(() => {
|
||||
if (!graph) return [];
|
||||
return graph.nodes.map((node, i) => ({
|
||||
id: node.id,
|
||||
type: "custom" as const,
|
||||
position: {
|
||||
x: (i % 3) * 300 + 50,
|
||||
y: Math.floor(i / 3) * 200 + 50,
|
||||
},
|
||||
data: {
|
||||
label: node.name,
|
||||
nodeType: node.type,
|
||||
summary: node.summary,
|
||||
complexity: node.complexity,
|
||||
isHighlighted: searchResults.includes(node.id),
|
||||
isSelected: selectedNodeId === node.id,
|
||||
},
|
||||
}));
|
||||
}, [graph, searchResults, selectedNodeId]);
|
||||
|
||||
const initialEdges = useMemo<Edge[]>(() => {
|
||||
if (!graph) return [];
|
||||
return graph.edges.map((edge, i) => ({
|
||||
id: `e-${i}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.type,
|
||||
animated: edge.type === "calls",
|
||||
style: { stroke: "#6b7280", strokeWidth: 1.5 },
|
||||
labelStyle: { fill: "#9ca3af", fontSize: 10 },
|
||||
}));
|
||||
}, [graph]);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes(initialNodes);
|
||||
}, [initialNodes, setNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
setEdges(initialEdges);
|
||||
}, [initialEdges, setEdges]);
|
||||
|
||||
const onNodeClick = useCallback(
|
||||
(_: React.MouseEvent, node: CustomFlowNode) => {
|
||||
selectNode(node.id);
|
||||
},
|
||||
[selectNode],
|
||||
);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
selectNode(null);
|
||||
}, [selectNode]);
|
||||
|
||||
if (!graph) {
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-800 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">No knowledge graph loaded</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
colorMode="dark"
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap
|
||||
nodeColor="#374151"
|
||||
maskColor="rgba(0,0,0,0.6)"
|
||||
className="!bg-gray-800"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user