diff --git a/.gitignore b/.gitignore index c53c2817..ed5349ee 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ node_modules/ # Frontend build artifacts (generated by Vite — do not track in git) semantica/static/ + +# Local graph explorer test datasets +demo_out/ diff --git a/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx b/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx index df30b0e5..ec1554df 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx @@ -173,6 +173,14 @@ export function GraphInspectorPanel({ ); } + if (!graph.hasNode(nodeId)) { + return ( +
+ Selected grouped item is not directly inspectable. Activate Focused mode to resolve to a canonical node. +
+ ); + } + const attributes = graph.getNodeAttributes(nodeId) as { color?: string; content?: string; diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx index 48eeef03..404ebcad 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -823,9 +823,17 @@ export function GraphWorkspace() { }; }, [debouncedTime, isLoading]); - const focusNode = useCallback((nodeId: string) => { + const resolveNodeIdForFocusedMode = useCallback((nodeId: string): string | null => { + if (!nodeId) { + return null; + } + + if (graph.hasNode(nodeId)) { + return nodeId; + } + const currentDisplayGraph = pluginRuntimeRef.current?.displayGraph ?? graph; - if (viewMode === "grouped" && currentDisplayGraph.hasNode(nodeId)) { + if (currentDisplayGraph.hasNode(nodeId)) { const displayAttrs = currentDisplayGraph.getNodeAttributes(nodeId) as NodeAttributes; const communityGroup = displayAttrs.properties?.__communityGroup as | { @@ -833,28 +841,44 @@ export function GraphWorkspace() { sampleNodeIds?: string[]; } | undefined; + const anchorNodeId = communityGroup?.anchorNodeId || communityGroup?.sampleNodeIds?.[0] || ""; if (anchorNodeId && graph.hasNode(anchorNodeId)) { - setViewMode("focused"); - setSelectedNodeId(anchorNodeId); - setSelectedEdgeId(""); - setPathResult(null); - setSearchResults([]); - setSearchError(""); - setIsLayoutRunning(false); - return; + return anchorNodeId; } } + return null; + }, []); + + const activateFocusedMode = useCallback(() => { + const canonicalNodeId = resolveNodeIdForFocusedMode(selectedNodeId); + if (!canonicalNodeId) { + return; + } + + if (canonicalNodeId !== selectedNodeId) { + setSelectedNodeId(canonicalNodeId); + } + setViewMode("focused"); + }, [resolveNodeIdForFocusedMode, selectedNodeId]); + const canActivateFocusedMode = useMemo( + () => Boolean(resolveNodeIdForFocusedMode(selectedNodeId)), + [pluginRuntimeVersion, resolveNodeIdForFocusedMode, selectedNodeId], + ); + + const focusNode = useCallback((nodeId: string) => { + if (!nodeId) { + return; + } + setSelectedNodeId(nodeId); setSelectedEdgeId(""); setPathResult(null); setSearchResults([]); setSearchError(""); - if (nodeId) { - setIsLayoutRunning(false); - } - }, [viewMode]); + setIsLayoutRunning(false); + }, []); const handleEdgeSelect = useCallback((edgeId: string) => { setSelectedEdgeId(edgeId); @@ -1153,6 +1177,10 @@ export function GraphWorkspace() { focusNode(action.nodeId); return; case "setViewMode": + if (action.viewMode === "focused") { + activateFocusedMode(); + return; + } setViewMode(action.viewMode); return; case "collapseNeighborhood": @@ -1205,7 +1233,7 @@ export function GraphWorkspace() { setActiveDockPanelId((previous) => (previous === action.panelId ? null : previous)); return; } - }, [focusNode, selectedNodeId, setEffectToggle]); + }, [activateFocusedMode, focusNode, selectedNodeId, setEffectToggle]); const diagnosticsSnapshot = useMemo(() => { if (!GRAPH_THEME.effects.diagnostics.enabledInDev || !graphDiagnosticsState) { @@ -1401,8 +1429,8 @@ export function GraphWorkspace() { label: "Focused", title: "Inspect the selected node in a focused local graph", active: viewMode === "focused", - disabled: !selectedNodeId, - onClick: () => setViewMode("focused"), + disabled: !canActivateFocusedMode, + onClick: activateFocusedMode, }, ], });