From 0674ba3f39e25a763e15e228e3e45d74714fb7ba Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Thu, 2 Apr 2026 12:34:06 +0800 Subject: [PATCH] feat(dashboard): add DomainGraphView with domain overview and detail views Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/dashboard/src/App.tsx | 7 +- .../src/components/DomainGraphView.tsx | 240 ++++++++++++++++++ 2 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 77cf275..9b9b74b 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -3,6 +3,7 @@ import { validateGraph } from "@understand-anything/core/schema"; import type { GraphIssue } from "@understand-anything/core/schema"; import { useDashboardStore } from "./store"; import GraphView from "./components/GraphView"; +import DomainGraphView from "./components/DomainGraphView"; import CodeViewer from "./components/CodeViewer"; import SearchBar from "./components/SearchBar"; import NodeInfo from "./components/NodeInfo"; @@ -448,7 +449,11 @@ function Dashboard({ accessToken }: { accessToken: string }) {
{/* Graph area */}
- + {viewMode === "domain" && domainGraph ? ( + + ) : ( + + )}
Press ? for keyboard shortcuts
diff --git a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx new file mode 100644 index 0000000..3b3bdfb --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx @@ -0,0 +1,240 @@ +import { useCallback, useMemo } from "react"; +import { + ReactFlow, + ReactFlowProvider, + Background, + BackgroundVariant, + Controls, + MiniMap, +} from "@xyflow/react"; +import type { Edge, Node } from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; + +import DomainClusterNode from "./DomainClusterNode"; +import FlowNode from "./FlowNode"; +import StepNode from "./StepNode"; +import { useDashboardStore } from "../store"; +import { useTheme } from "../themes/index.ts"; +import { applyDagreLayout } from "../utils/layout"; +import type { KnowledgeGraph, GraphNode } from "@understand-anything/core/types"; + +const nodeTypes = { + "domain-cluster": DomainClusterNode, + "flow-node": FlowNode, + "step-node": StepNode, +}; + +// Dimensions for domain-specific nodes +const DOMAIN_NODE_DIMENSIONS = new Map(); + +function getDomainMeta(node: GraphNode): Record | undefined { + return (node as any).domainMeta; +} + +function buildDomainOverview(graph: KnowledgeGraph): { nodes: Node[]; edges: Edge[] } { + const domainNodes = graph.nodes.filter((n) => n.type === "domain"); + + // Count flows per domain + const flowCountMap = new Map(); + for (const edge of graph.edges) { + if (edge.type === "contains_flow") { + flowCountMap.set(edge.source, (flowCountMap.get(edge.source) ?? 0) + 1); + } + } + + const rfNodes: Node[] = domainNodes.map((node) => { + const meta = getDomainMeta(node); + const data = { + label: node.name, + summary: node.summary, + entities: meta?.entities as string[] | undefined, + flowCount: flowCountMap.get(node.id) ?? 0, + businessRules: meta?.businessRules as string[] | undefined, + domainId: node.id, + }; + DOMAIN_NODE_DIMENSIONS.set(node.id, { width: 320, height: 180 }); + return { + id: node.id, + type: "domain-cluster" as const, + position: { x: 0, y: 0 }, + data, + }; + }); + + const rfEdges: Edge[] = graph.edges + .filter((e) => e.type === "cross_domain") + .map((e) => ({ + id: `${e.source}-${e.target}`, + source: e.source, + target: e.target, + label: e.description ?? "", + style: { stroke: "var(--color-accent)", strokeDasharray: "6 3", strokeWidth: 2 }, + labelStyle: { fill: "var(--color-text-muted)", fontSize: 10 }, + animated: true, + })); + + return applyDagreLayout(rfNodes, rfEdges, "LR", DOMAIN_NODE_DIMENSIONS); +} + +function buildDomainDetail( + graph: KnowledgeGraph, + domainId: string, +): { nodes: Node[]; edges: Edge[] } { + // Find flows for this domain + const flowIds = new Set( + graph.edges + .filter((e) => e.type === "contains_flow" && e.source === domainId) + .map((e) => e.target), + ); + + const flowNodes = graph.nodes.filter((n) => flowIds.has(n.id)); + const stepEdges = graph.edges.filter( + (e) => e.type === "flow_step" && flowIds.has(e.source), + ); + const stepIds = new Set(stepEdges.map((e) => e.target)); + const stepNodes = graph.nodes.filter((n) => stepIds.has(n.id)); + + // Build step order map + const stepOrderMap = new Map(); + for (const edge of stepEdges) { + stepOrderMap.set(edge.target, edge.weight); + } + + // Count steps per flow + const stepCountMap = new Map(); + for (const edge of stepEdges) { + stepCountMap.set(edge.source, (stepCountMap.get(edge.source) ?? 0) + 1); + } + + const dims = new Map(); + + const rfNodes: Node[] = [ + ...flowNodes.map((node) => { + const meta = getDomainMeta(node); + dims.set(node.id, { width: 260, height: 120 }); + return { + id: node.id, + type: "flow-node" as const, + position: { x: 0, y: 0 }, + data: { + label: node.name, + summary: node.summary, + entryPoint: meta?.entryPoint as string | undefined, + entryType: meta?.entryType as string | undefined, + stepCount: stepCountMap.get(node.id) ?? 0, + flowId: node.id, + }, + }; + }), + ...stepNodes.map((node) => { + dims.set(node.id, { width: 200, height: 90 }); + return { + id: node.id, + type: "step-node" as const, + position: { x: 0, y: 0 }, + data: { + label: node.name, + summary: node.summary, + filePath: node.filePath, + stepId: node.id, + order: Math.round((stepOrderMap.get(node.id) ?? 0) * 10), + }, + }; + }), + ]; + + const rfEdges: Edge[] = stepEdges.map((e) => ({ + id: `${e.source}-${e.target}`, + source: e.source, + target: e.target, + style: { stroke: "var(--color-border-medium)", strokeWidth: 1.5 }, + animated: false, + })); + + return applyDagreLayout(rfNodes, rfEdges, "LR", dims); +} + +function DomainGraphViewInner() { + const domainGraph = useDashboardStore((s) => s.domainGraph); + const activeDomainId = useDashboardStore((s) => s.activeDomainId); + const navigateToDomain = useDashboardStore((s) => s.navigateToDomain); + const theme = useTheme(); + + const { nodes, edges } = useMemo(() => { + if (!domainGraph) return { nodes: [], edges: [] }; + if (activeDomainId) { + return buildDomainDetail(domainGraph, activeDomainId); + } + return buildDomainOverview(domainGraph); + }, [domainGraph, activeDomainId]); + + const onNodeDoubleClick = useCallback( + (_: React.MouseEvent, node: Node) => { + if (node.type === "domain-cluster" && node.data && "domainId" in node.data) { + navigateToDomain(node.data.domainId as string); + } + }, + [navigateToDomain], + ); + + if (!domainGraph) { + return ( +
+ No domain graph available. Run /understand-domain to generate one. +
+ ); + } + + return ( +
+ {activeDomainId && ( +
+ +
+ )} + + + + theme.preset.colors.accent ?? "#d4a574"} + maskColor="rgba(0,0,0,0.7)" + style={{ bottom: 16, right: 16, width: 160, height: 100 }} + /> + +
+ ); +} + +export default function DomainGraphView() { + return ( + + + + ); +}