();
+ if (selectedNodeId) {
+ for (const edge of filteredGraphEdges) {
+ if (edge.source === selectedNodeId) neighborNodeIds.add(edge.target);
+ if (edge.target === selectedNodeId) neighborNodeIds.add(edge.source);
+ }
+ neighborNodeIds.add(selectedNodeId);
+ }
+
const flowNodes: CustomFlowNode[] = filteredGraphNodes.map((node) => {
const matchResult = searchResults.find((r) => r.nodeId === node.id);
+ const hasSelection = !!selectedNodeId;
return {
id: node.id,
type: "custom" as const,
@@ -84,6 +112,8 @@ export default function GraphView() {
isDiffChanged: diffMode && changedNodeIds.has(node.id),
isDiffAffected: diffMode && affectedNodeIds.has(node.id),
isDiffFaded: diffMode && !changedNodeIds.has(node.id) && !affectedNodeIds.has(node.id),
+ isNeighbor: hasSelection && neighborNodeIds.has(node.id) && selectedNodeId !== node.id,
+ isSelectionFaded: hasSelection && !neighborNodeIds.has(node.id),
onNodeClick: handleNodeSelect,
},
};
@@ -95,25 +125,49 @@ export default function GraphView() {
const targetInDiff = diffMode && diffNodeIds.has(edge.target);
const isImpacted = diffMode && (sourceInDiff || targetInDiff);
+ // Selection-based edge highlighting
+ const isSelectedEdge = !!selectedNodeId && (edge.source === selectedNodeId || edge.target === selectedNodeId);
+ const hasSelection = !!selectedNodeId;
+
+ let edgeStyle: React.CSSProperties;
+ let edgeLabelStyle: React.CSSProperties;
+ let edgeAnimated: boolean;
+
+ if (isImpacted) {
+ edgeStyle = {
+ stroke: sourceInDiff && targetInDiff
+ ? "rgba(224, 82, 82, 0.7)"
+ : "rgba(212, 160, 48, 0.5)",
+ strokeWidth: 2.5,
+ };
+ edgeLabelStyle = { fill: "#a39787", fontSize: 10 };
+ edgeAnimated = true;
+ } else if (diffMode) {
+ edgeStyle = { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 };
+ edgeLabelStyle = { fill: "rgba(163,151,135,0.3)", fontSize: 10 };
+ edgeAnimated = false;
+ } else if (isSelectedEdge) {
+ edgeStyle = { stroke: "rgba(212,165,116,0.8)", strokeWidth: 2.5 };
+ edgeLabelStyle = { fill: "#d4a574", fontSize: 11, fontWeight: 600 };
+ edgeAnimated = true;
+ } else if (hasSelection) {
+ edgeStyle = { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 };
+ edgeLabelStyle = { fill: "rgba(163,151,135,0.2)", fontSize: 10 };
+ edgeAnimated = false;
+ } else {
+ edgeStyle = { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 };
+ edgeLabelStyle = { fill: "#a39787", fontSize: 10 };
+ edgeAnimated = edge.type === "calls";
+ }
+
return {
id: `e-${i}`,
source: edge.source,
target: edge.target,
label: edge.type,
- animated: edge.type === "calls" || isImpacted,
- style: isImpacted
- ? {
- stroke: sourceInDiff && targetInDiff
- ? "rgba(224, 82, 82, 0.7)"
- : "rgba(212, 160, 48, 0.5)",
- strokeWidth: 2.5,
- }
- : diffMode
- ? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }
- : { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
- labelStyle: diffMode && !isImpacted
- ? { fill: "rgba(163,151,135,0.3)", fontSize: 10 }
- : { fill: "#a39787", fontSize: 10 },
+ animated: edgeAnimated,
+ style: edgeStyle,
+ labelStyle: edgeLabelStyle,
};
});
@@ -167,7 +221,8 @@ export default function GraphView() {
const groupWidth = maxX - minX + LAYER_PADDING * 2;
const groupHeight = maxY - minY + LAYER_PADDING * 2 + 24;
- // Create the group node
+ // Create the group node with distinct color per layer
+ const layerColor = getLayerColor(layerIdx);
groupNodes.push({
id: layer.id,
type: "group",
@@ -176,13 +231,13 @@ export default function GraphView() {
style: {
width: groupWidth,
height: groupHeight,
- backgroundColor: "rgba(212,165,116,0.05)",
+ backgroundColor: layerColor.bg,
borderRadius: 12,
- border: `2px dashed rgba(212,165,116,0.25)`,
+ border: `2px solid ${layerColor.border}`,
padding: 8,
fontSize: 13,
fontWeight: 600,
- color: "#d4a574",
+ color: layerColor.label,
},
});
@@ -214,11 +269,13 @@ export default function GraphView() {
];
return { initialNodes: allNodes, initialEdges: laid.edges };
- }, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds]);
+ }, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds, focusNodeId]);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+ const { fitView } = useReactFlow();
+
useEffect(() => {
setNodes(initialNodes);
}, [initialNodes, setNodes]);
@@ -227,6 +284,19 @@ export default function GraphView() {
setEdges(initialEdges);
}, [initialEdges, setEdges]);
+ // Zoom-to-node when navigateToNode is called
+ const zoomToNodeId = useDashboardStore((s) => s.zoomToNodeId);
+ useEffect(() => {
+ if (zoomToNodeId) {
+ // Small delay to let React Flow update node positions first
+ const timer = setTimeout(() => {
+ fitView({ nodes: [{ id: zoomToNodeId }], duration: 400, padding: 0.5 });
+ useDashboardStore.setState({ zoomToNodeId: null });
+ }, 50);
+ return () => clearTimeout(timer);
+ }
+ }, [zoomToNodeId, fitView]);
+
const onNodeClick = useCallback(
(_: React.MouseEvent, node: { id: string }) => {
// Ignore clicks on group nodes
@@ -251,7 +321,18 @@ export default function GraphView() {
}
return (
-
+
+ {focusNodeId && (
+
+
+
+ )}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx b/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx
index 6d4fc24..c455f99 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx
@@ -1,35 +1,18 @@
import { useDashboardStore } from "../store";
-const LAYER_COLORS = [
- "rgba(59, 130, 246, 0.08)", // blue
- "rgba(16, 185, 129, 0.08)", // green
- "rgba(245, 158, 11, 0.08)", // amber
- "rgba(139, 92, 246, 0.08)", // violet
- "rgba(236, 72, 153, 0.08)", // pink
- "rgba(6, 182, 212, 0.08)", // cyan
- "rgba(249, 115, 22, 0.08)", // orange
- "rgba(168, 162, 158, 0.08)", // stone
+// Shared layer color palette — used by both LayerLegend dots and GraphView group nodes
+export const LAYER_PALETTE = [
+ { bg: "rgba(74, 124, 155, 0.12)", border: "rgba(74, 124, 155, 0.4)", label: "#4a7c9b" }, // blue (API)
+ { bg: "rgba(90, 158, 111, 0.12)", border: "rgba(90, 158, 111, 0.4)", label: "#5a9e6f" }, // green (Data)
+ { bg: "rgba(139, 111, 176, 0.12)", border: "rgba(139, 111, 176, 0.4)", label: "#8b6fb0" }, // purple (Service)
+ { bg: "rgba(201, 160, 108, 0.12)", border: "rgba(201, 160, 108, 0.4)", label: "#c9a06c" }, // gold (Config)
+ { bg: "rgba(176, 122, 138, 0.12)", border: "rgba(176, 122, 138, 0.4)", label: "#b07a8a" }, // pink (UI)
+ { bg: "rgba(74, 155, 140, 0.12)", border: "rgba(74, 155, 140, 0.4)", label: "#4a9b8c" }, // teal (Middleware)
+ { bg: "rgba(120, 130, 145, 0.12)", border: "rgba(120, 130, 145, 0.4)", label: "#788291" }, // slate (Test)
];
-export const LAYER_BORDER_COLORS = [
- "rgba(59, 130, 246, 0.5)", // blue
- "rgba(16, 185, 129, 0.5)", // green
- "rgba(245, 158, 11, 0.5)", // amber
- "rgba(139, 92, 246, 0.5)", // violet
- "rgba(236, 72, 153, 0.5)", // pink
- "rgba(6, 182, 212, 0.5)", // cyan
- "rgba(249, 115, 22, 0.5)", // orange
- "rgba(168, 162, 158, 0.5)", // stone
-];
-
-export { LAYER_COLORS };
-
-export function getLayerColor(index: number): string {
- return LAYER_COLORS[index % LAYER_COLORS.length];
-}
-
-export function getLayerBorderColor(index: number): string {
- return LAYER_BORDER_COLORS[index % LAYER_BORDER_COLORS.length];
+export function getLayerColor(index: number) {
+ return LAYER_PALETTE[index % LAYER_PALETTE.length];
}
export default function LayerLegend() {
@@ -65,20 +48,23 @@ export default function LayerLegend() {
{showLayers && hasLayers && (
- {layers.map((layer, i) => (
-
-
-
- {layer.name}
-
- ({layer.nodeIds.length})
+ {layers.map((layer, i) => {
+ const color = getLayerColor(i);
+ return (
+
+
+
+ {layer.name}
+
+ ({layer.nodeIds.length})
+
-
-
- ))}
+
+ );
+ })}
)}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx
index 78c1041..9500cfc 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx
@@ -20,6 +20,10 @@ export default function NodeInfo() {
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const [languageExpanded, setLanguageExpanded] = useState(true);
+ const navigateToNode = useDashboardStore((s) => s.navigateToNode);
+ const openCodeViewer = useDashboardStore((s) => s.openCodeViewer);
+ const setFocusNode = useDashboardStore((s) => s.setFocusNode);
+ const focusNodeId = useDashboardStore((s) => s.focusNodeId);
const node = graph?.nodes.find((n) => n.id === selectedNodeId) ?? null;
if (!node) {
@@ -53,7 +57,19 @@ export default function NodeInfo() {
- {node.name}
+
+
{node.name}
+
+
{node.summary}
@@ -130,7 +146,11 @@ export default function NodeInfo() {
return (
{
+ navigateToNode(otherId);
+ openCodeViewer(otherId);
+ }}
>
{arrow}
{edge.type}
diff --git a/understand-anything-plugin/packages/dashboard/src/store.ts b/understand-anything-plugin/packages/dashboard/src/store.ts
index b01a15c..2f097b9 100644
--- a/understand-anything-plugin/packages/dashboard/src/store.ts
+++ b/understand-anything-plugin/packages/dashboard/src/store.ts
@@ -32,8 +32,16 @@ interface DashboardStore {
changedNodeIds: Set;
affectedNodeIds: Set;
+ // Zoom-to-node: set a nodeId to trigger GraphView to pan/zoom to it
+ zoomToNodeId: string | null;
+
+ // Focus mode: isolate a node's 1-hop neighborhood
+ focusNodeId: string | null;
+
setGraph: (graph: KnowledgeGraph) => void;
selectNode: (nodeId: string | null) => void;
+ navigateToNode: (nodeId: string) => void;
+ setFocusNode: (nodeId: string | null) => void;
setSearchQuery: (query: string) => void;
toggleLayers: () => void;
setPersona: (persona: Persona) => void;
@@ -79,6 +87,9 @@ export const useDashboardStore = create()((set, get) => ({
changedNodeIds: new Set(),
affectedNodeIds: new Set(),
+ zoomToNodeId: null,
+ focusNodeId: null,
+
setGraph: (graph) => {
const searchEngine = new SearchEngine(graph.nodes);
const query = get().searchQuery;
@@ -86,6 +97,8 @@ export const useDashboardStore = create()((set, get) => ({
set({ graph, searchEngine, searchResults });
},
selectNode: (nodeId) => set({ selectedNodeId: nodeId }),
+ navigateToNode: (nodeId) => set({ selectedNodeId: nodeId, zoomToNodeId: nodeId }),
+ setFocusNode: (nodeId) => set({ focusNodeId: nodeId, selectedNodeId: nodeId }),
setSearchMode: (mode) => set({ searchMode: mode }),
setSearchQuery: (query) => {
const engine = get().searchEngine;
diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts
index 9d451ee..5910f40 100644
--- a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts
+++ b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts
@@ -11,10 +11,13 @@ export function applyDagreLayout(
): { nodes: Node[]; edges: Edge[] } {
const g = new dagre.graphlib.Graph();
g.setDefaultEdgeLabel(() => ({}));
+
+ // Scale spacing for larger graphs to reduce overlap
+ const isLarge = nodes.length > 50;
g.setGraph({
rankdir: direction,
- nodesep: 60,
- ranksep: 80,
+ nodesep: isLarge ? 80 : 60,
+ ranksep: isLarge ? 120 : 80,
marginx: 20,
marginy: 20,
});