-
+ {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 (
+
+
+
+ );
+}