Merge pull request #486 from Hawksight-AI/fix/graph-motion

Fix explorer zooming and Loading Flicker
This commit is contained in:
Mohd Kaif
2026-04-23 19:13:37 +05:30
committed by GitHub
12 changed files with 1217 additions and 265 deletions
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState }
import { batchMergeEdges, batchMergeNodes, clearGraph, graph, type EdgeAttributes, type NodeAttributes } from "../../store/graphStore";
import { SigmaSceneAdapter } from "./SigmaSceneAdapter";
import { createGraphLoadProgress } from "./graphLoading";
import { resolveDisplayGraph } from "./graphSceneState";
import {
chooseColorAccessor,
colorForNodeKey,
@@ -41,6 +42,7 @@ const STAGE_EFFECTS_STATE: GraphEffectsState = {
lensMode: "neighborhood",
effectQuality: "bounded",
};
const EMPTY_PATH: string[] = [];
const socketProtocol = () => (window.location.protocol === "https:" ? "wss:" : "ws:");
@@ -117,6 +119,10 @@ export const GraphRuntimeStage = forwardRef<GraphStageHandle, GraphRuntimeStageP
const prevActiveIdsRef = useRef<Set<string>>(new Set());
const [graphVersion, setGraphVersion] = useState(0);
const [runtimeLayoutSource, setRuntimeLayoutSource] = useState<GraphLayoutSource>(snapshot?.summary.layoutSource ?? "runtime");
const displayResult = useMemo(
() => resolveDisplayGraph(selectedNodeId, activePath, EMPTY_PATH, viewMode, { aggregationEnabled: true }),
[activePath, graphVersion, selectedNodeId, viewMode],
);
const stageSignature = useMemo(() => (snapshot ? `${snapshot.fetchedAt}:${snapshot.summary.nodeCount}:${snapshot.summary.edgeCount}` : null), [snapshot]);
@@ -451,9 +457,15 @@ export const GraphRuntimeStage = forwardRef<GraphStageHandle, GraphRuntimeStageP
<SigmaSceneAdapter
ref={sceneRef}
onNodeSelect={onNodeSelect}
graphVersion={graphVersion}
graphReady={Boolean(snapshot)}
displayGraph={displayResult.graph}
displayMeta={displayResult.meta}
displayState={displayResult.state}
selectedEdgeId=""
selectedNodeId={selectedNodeId}
activePath={activePath}
activePathEdgeIds={EMPTY_PATH}
effectsState={STAGE_EFFECTS_STATE}
isLayoutRunning={isLayoutRunning}
onLayoutRunningChange={onLayoutRunningChange}
@@ -12,7 +12,8 @@ import { useLoadGraph, useReloadGraph } from "./useLoadGraph";
import { GraphLoadingOverlay } from "./GraphLoadingOverlay";
import { createGraphLoadProgress, getGraphLoadTitle } from "./graphLoading";
import { GRAPH_THEME, withAlpha } from "./graphTheme";
import { resolveDisplayGraph } from "./graphSceneState";
import { resolveDisplayGraph, resolveDisplayStateSnapshot } from "./graphSceneState";
import { computeGraphAnalyticsBase } from "./graphAnalytics";
import {
type GraphPlugin,
type GraphPluginActionRequest,
@@ -108,6 +109,15 @@ const LazyGraphInspectorPanel = lazy(() => import("./GraphInspectorPanel").then(
const loadExplorationEffectsPlugin = () => import("./plugins/explorationEffectsPluginPhaseC").then((module) => module.explorationEffectsPluginPhaseC);
const loadNeighborhoodPanelPlugin = () => import("./plugins/neighborhoodPanelPlugin").then((module) => module.neighborhoodPanelPlugin);
const loadTemporalOverlayPlugin = () => import("./plugins/temporalOverlayPlugin").then((module) => module.temporalOverlayPlugin);
const EMPTY_PATH: string[] = [];
const DEBUG_GRAPH_WORKSPACE = import.meta.env.DEV;
function debugGraphWorkspace(message: string, payload?: Record<string, unknown>) {
if (!DEBUG_GRAPH_WORKSPACE) {
return;
}
console.debug(`[GraphWorkspace] ${message}`, payload ?? {});
}
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
@@ -670,6 +680,8 @@ export function GraphWorkspace() {
const [selectedNodeId, setSelectedNodeId] = useState("");
const [selectedEdgeId, setSelectedEdgeId] = useState("");
const [isLayoutRunning, setIsLayoutRunning] = useState(false);
const [graphReady, setGraphReady] = useState(false);
const [graphVersion, setGraphVersion] = useState(0);
const [viewMode, setViewMode] = useState<GraphViewMode>("full");
const [aggregationEnabled] = useState(true);
const [collapsedNeighborhoodNodeIds, setCollapsedNeighborhoodNodeIds] = useState<string[]>([]);
@@ -715,9 +727,18 @@ export function GraphWorkspace() {
});
const reload = useReloadGraph();
const handleLoadProgress = useCallback((progress: GraphLoadProgress) => {
setLoadingProgress(progress);
if (progress.phase !== "ready" && progress.phase !== "stabilizing_layout") {
setGraphReady(false);
}
}, []);
const { data: summary, isLoading, isFetching } = useLoadGraph({
enabled: true,
onGraphReady: (graphSummary) => {
setGraphReady(true);
setGraphVersion((current) => current + 1);
setIsLayoutRunning(!graphSummary.layoutReady);
if (settlingOverlayTimeoutRef.current !== null) {
window.clearTimeout(settlingOverlayTimeoutRef.current);
@@ -746,7 +767,7 @@ export function GraphWorkspace() {
settlingOverlayTimeoutRef.current = null;
}, 900);
},
onProgress: setLoadingProgress,
onProgress: handleLoadProgress,
});
useEffect(() => {
@@ -808,6 +829,7 @@ export function GraphWorkspace() {
});
prevActiveIdsRef.current = nextActiveIds;
setActiveNodeCount(data.active_node_count);
setGraphVersion((current) => current + 1);
sceneRef.current?.getRuntime()?.requestRender();
});
} catch (fetchError) {
@@ -877,8 +899,10 @@ export function GraphWorkspace() {
setPathResult(null);
setSearchResults([]);
setSearchError("");
setIsLayoutRunning(false);
}, []);
if (viewMode === "focused") {
setIsLayoutRunning(false);
}
}, [viewMode]);
const handleEdgeSelect = useCallback((edgeId: string) => {
setSelectedEdgeId(edgeId);
@@ -997,6 +1021,7 @@ export function GraphWorkspace() {
},
]);
logEvent("add-node", `Added node ${payload.label ?? payload.id}${payload.nodeType ? ` (${payload.nodeType})` : ""} via realtime ws`, { nodeId: payload.id, nodeType: payload.nodeType });
setGraphVersion((current) => current + 1);
sceneRef.current?.getRuntime()?.requestRender();
}
if (eventType === "ADD_EDGE") {
@@ -1010,6 +1035,7 @@ export function GraphWorkspace() {
},
]);
logEvent("add-edge", `Added edge ${payload.edgeType ?? payload.id} (${payload.source_id}${payload.target_id}) via realtime ws`, { edgeId: payload.id, edgeType: payload.edgeType, source: payload.source_id, target: payload.target_id });
setGraphVersion((current) => current + 1);
sceneRef.current?.getRuntime()?.requestRender();
}
} catch (socketError) {
@@ -1026,18 +1052,77 @@ export function GraphWorkspace() {
setCollapsedNeighborhoodNodeIds([]);
}, [summary?.edgeCount, summary?.nodeCount]);
const showLoadingOverlay = isLoading || isFetching || loadingProgress?.phase === "stabilizing_layout";
const showLoadingOverlay = !graphReady && (isLoading || isFetching || Boolean(loadingProgress));
const showSettlingStatus = graphReady && loadingProgress?.phase === "stabilizing_layout";
const hasGraphContent = Boolean(summary?.nodeCount);
const activePath = pathResult?.path ?? [];
const activePathEdgeIds = pathResult?.edge_ids ?? [];
const activePath = pathResult?.path ?? EMPTY_PATH;
const activePathEdgeIds = pathResult?.edge_ids ?? EMPTY_PATH;
const structuralSelectedNodeId = useMemo(() => {
if (!selectedNodeId || !graph.hasNode(selectedNodeId)) {
return "";
}
if (viewMode === "focused") {
return selectedNodeId;
}
return collapsedNeighborhoodNodeIds.includes(selectedNodeId) ? selectedNodeId : "";
}, [collapsedNeighborhoodNodeIds, selectedNodeId, viewMode]);
const structuralActivePath = structuralSelectedNodeId ? activePath : EMPTY_PATH;
const structuralActivePathEdgeIds = structuralSelectedNodeId ? activePathEdgeIds : EMPTY_PATH;
const groupedViewAvailable = useMemo(
() => computeGraphAnalyticsBase(graph, { computeCommunities: true, computeCentrality: false }).communitiesByNode.size > 0,
[graphVersion],
);
const displayResult = useMemo(
() => resolveDisplayGraph(selectedNodeId, activePath, activePathEdgeIds, viewMode, {
() => resolveDisplayGraph(structuralSelectedNodeId, structuralActivePath, structuralActivePathEdgeIds, viewMode, {
aggregationEnabled,
collapsedNeighborhoodNodeIds,
groupedViewAvailable,
}),
[activePath, activePathEdgeIds, aggregationEnabled, collapsedNeighborhoodNodeIds, selectedNodeId, viewMode],
[
aggregationEnabled,
collapsedNeighborhoodNodeIds,
graphVersion,
groupedViewAvailable,
structuralActivePath,
structuralActivePathEdgeIds,
structuralSelectedNodeId,
viewMode,
],
);
const displayState = displayResult.state;
const displayState = useMemo(
() => resolveDisplayStateSnapshot(selectedNodeId, activePath, viewMode, {
aggregationEnabled,
collapsedNeighborhoodNodeIds,
groupedViewAvailable,
}),
[activePath, aggregationEnabled, collapsedNeighborhoodNodeIds, groupedViewAvailable, selectedNodeId, viewMode],
);
const displayMeta = displayResult.meta;
const previousDisplayGraphRef = useRef(displayResult.graph);
const previousDisplayStateRef = useRef(displayState);
useEffect(() => {
const graphRebuilt = previousDisplayGraphRef.current !== displayResult.graph;
const displayStateChanged = previousDisplayStateRef.current !== displayState;
debugGraphWorkspace("display-state-derived", {
selectedNodeId,
structuralSelectedNodeId,
viewMode,
graphRebuilt,
displayStateChanged,
aggregationEnabled,
collapsedNeighborhoodActive: Boolean(structuralSelectedNodeId && collapsedNeighborhoodNodeIds.includes(structuralSelectedNodeId)),
});
previousDisplayGraphRef.current = displayResult.graph;
previousDisplayStateRef.current = displayState;
}, [
aggregationEnabled,
collapsedNeighborhoodNodeIds,
displayResult.graph,
displayState,
selectedNodeId,
structuralSelectedNodeId,
viewMode,
]);
const focusedSummary = useMemo(() => {
if (!selectedNodeId || !graph.hasNode(selectedNodeId)) {
if (viewMode === "grouped") {
@@ -1473,25 +1558,13 @@ export function GraphWorkspace() {
id: "zoom-in",
label: " Zoom In",
title: "Zoom in (or scroll up on the canvas)",
onClick: () => {
const runtime = sceneRef.current?.getRuntime();
if (runtime?.renderer === "sigma") {
const camera = (runtime.scene as import("sigma").default).getCamera();
camera.animatedZoom({ duration: 200 });
}
},
onClick: () => sceneRef.current?.zoomIn(),
},
{
id: "zoom-out",
label: " Zoom Out",
title: "Zoom out (or scroll down on the canvas)",
onClick: () => {
const runtime = sceneRef.current?.getRuntime();
if (runtime?.renderer === "sigma") {
const camera = (runtime.scene as import("sigma").default).getCamera();
camera.animatedUnzoom({ duration: 200 });
}
},
onClick: () => sceneRef.current?.zoomOut(),
},
{
id: "fit-view",
@@ -1550,6 +1623,11 @@ export function GraphWorkspace() {
const sceneAdapterProps = {
onNodeSelect: focusNode,
onEdgeSelect: handleEdgeSelect,
graphVersion,
graphReady,
displayGraph: displayResult.graph,
displayMeta,
displayState,
selectedNodeId,
selectedEdgeId,
activePath,
@@ -1557,9 +1635,8 @@ export function GraphWorkspace() {
effectsState,
temporalState,
isLayoutRunning,
layoutSource: graphSummary?.layoutSource,
viewMode,
aggregationEnabled,
collapsedNeighborhoodNodeIds,
showFitViewButton: false,
pluginOverlays: pluginOverlays.map((overlay) => overlay.element),
onRuntimeChange: handleSceneRuntimeChange,
@@ -1580,7 +1657,7 @@ export function GraphWorkspace() {
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div className="explore-toolbar">
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
{showLoadingOverlay && loadingProgress ? (
{(showLoadingOverlay || showSettlingStatus) && loadingProgress ? (
<MetricChip>{getGraphLoadTitle(loadingProgress.phase)}</MetricChip>
) : null}
{summary ? (
@@ -24,6 +24,8 @@ export const SigmaSceneAdapter = forwardRef<GraphSceneHandle, GraphSceneProps>(
useImperativeHandle(ref, () => ({
fitView: () => canvasRef.current?.fitView(),
focusNode: (nodeId: string) => canvasRef.current?.focusNode(nodeId),
zoomIn: () => canvasRef.current?.zoomIn(),
zoomOut: () => canvasRef.current?.zoomOut(),
getRuntime: () => runtimeRef.current,
setLayoutRunning: onLayoutRunningChange
? (running: boolean) => {
@@ -5,11 +5,16 @@ export const focusCameraBehavior: GraphBehavior = {
attach: () => {},
detach: () => {},
performAction: (context, action) => {
if (action.type !== "focusNode") {
return false;
if (action.type === "focusNode") {
context.focusNodeInView(action.nodeId);
return true;
}
context.focusNodeInView(action.nodeId);
return true;
if (action.type === "centerSelection") {
context.centerSelectionInView(action.nodeId);
return true;
}
return false;
},
};
@@ -1,23 +1,23 @@
import type { GraphBehavior } from "./types";
export function createSearchFocusBehavior(): GraphBehavior {
let lastFocusedNodeId = "";
let lastSelectedNodeId = "";
return {
id: "search-focus",
attach: () => {},
detach: () => {
lastFocusedNodeId = "";
lastSelectedNodeId = "";
},
onStateChange: (context, interactionState) => {
const nextFocusedNodeId = interactionState.focusedNodeId;
if (!nextFocusedNodeId || nextFocusedNodeId === lastFocusedNodeId) {
lastFocusedNodeId = nextFocusedNodeId;
const nextSelectedNodeId = interactionState.selectedNodeId;
if (!nextSelectedNodeId || nextSelectedNodeId === lastSelectedNodeId) {
lastSelectedNodeId = nextSelectedNodeId;
return;
}
lastFocusedNodeId = nextFocusedNodeId;
context.dispatchAction({ type: "focusNode", nodeId: nextFocusedNodeId });
lastSelectedNodeId = nextSelectedNodeId;
context.dispatchAction({ type: "centerSelection", nodeId: nextSelectedNodeId });
},
};
}
@@ -6,7 +6,8 @@ import type { GraphCameraState, GraphInteractionState } from "../types";
export type GraphBehaviorActionRequest =
| { type: "fitView" }
| { type: "focusNode"; nodeId: string };
| { type: "focusNode"; nodeId: string }
| { type: "centerSelection"; nodeId: string };
export interface GraphBehaviorContext {
sigma: Sigma;
@@ -17,6 +18,7 @@ export interface GraphBehaviorContext {
onNodeSelectionChange: (nodeId: string) => void;
onEdgeSelectionChange: (edgeId: string) => void;
focusNodeInView: (nodeId: string) => void;
centerSelectionInView: (nodeId: string) => void;
fitCurrentView: () => void;
dispatchAction: (action: GraphBehaviorActionRequest) => void;
}
@@ -16,9 +16,10 @@ export function createViewModeSwitchBehavior(): GraphBehavior {
}
lastViewMode = interactionState.viewMode;
const nextSelectedNodeId = interactionState.selectedNodeId;
if (interactionState.focusedNodeId) {
context.dispatchAction({ type: "focusNode", nodeId: interactionState.focusedNodeId });
if (interactionState.viewMode === "focused" && nextSelectedNodeId) {
context.dispatchAction({ type: "focusNode", nodeId: nextSelectedNodeId });
return;
}
@@ -18,7 +18,7 @@ import {
zoomTierAtLeast,
} from "./graphTheme";
import { computeGraphAnalyticsBase } from "./graphAnalytics";
import type { GraphDisplayStateSnapshot, GraphInteractionState, GraphViewMode } from "./types";
import type { GraphDisplayMeta, GraphDisplayStateSnapshot, GraphInteractionState, GraphViewMode } from "./types";
const MAX_FOCUS_NEIGHBORS = GRAPH_THEME.focus.maxNeighbors;
const FOCUS_RING_CAPACITY = GRAPH_THEME.focus.ringCapacity;
@@ -34,6 +34,28 @@ type GraphRef = typeof graph | Graph<NodeAttributes, EdgeAttributes>;
export type GraphDisplayResult = {
graph: GraphRef;
state: GraphDisplayStateSnapshot;
meta: GraphDisplayMeta;
};
const BASE_DISPLAY_META: GraphDisplayMeta = {
layoutMode: "base",
positionSource: "store",
tracksStoreNodePositions: true,
hasSyntheticNodes: false,
};
const MIRRORED_DISPLAY_META: GraphDisplayMeta = {
layoutMode: "mirrored",
positionSource: "store",
tracksStoreNodePositions: true,
hasSyntheticNodes: false,
};
const OWNED_DISPLAY_META: GraphDisplayMeta = {
layoutMode: "owned",
positionSource: "display",
tracksStoreNodePositions: false,
hasSyntheticNodes: true,
};
function getOverviewPresenceBoost(cameraRatio: number) {
@@ -1068,7 +1090,7 @@ function buildCommunityGroupedGraph(): GraphDisplayResult {
const state = createEmptyDisplayState("", true);
state.groupedViewAvailable = base.communitiesByNode.size > 0;
if (base.communitiesByNode.size === 0) {
return { graph: aggregateDisplayGraph(graph), state };
return { graph: aggregateDisplayGraph(graph), state, meta: MIRRORED_DISPLAY_META };
}
const grouped = new Graph<NodeAttributes, EdgeAttributes>({
@@ -1209,9 +1231,51 @@ function buildCommunityGroupedGraph(): GraphDisplayResult {
selectedVisibleNeighborIds: [],
selectedCollapsedNeighborIds: [],
},
meta: OWNED_DISPLAY_META,
};
}
export function resolveDisplayStateSnapshot(
selectedNodeId: string,
activePath: string[],
viewMode: GraphViewMode,
options?: {
aggregationEnabled?: boolean;
collapsedNeighborhoodNodeIds?: Iterable<string>;
groupedViewAvailable?: boolean;
},
): GraphDisplayStateSnapshot {
const aggregationEnabled = options?.aggregationEnabled ?? true;
const collapsedNeighborhoodNodeIds = new Set(
Array.from(options?.collapsedNeighborhoodNodeIds ?? []).filter((nodeId) => typeof nodeId === "string"),
);
const displayState = createEmptyDisplayState(selectedNodeId, aggregationEnabled);
displayState.groupedViewAvailable = options?.groupedViewAvailable ?? computeGraphAnalyticsBase(graph, {
computeCommunities: true,
computeCentrality: false,
}).communitiesByNode.size > 0;
if (!selectedNodeId || !graph.hasNode(selectedNodeId)) {
return displayState;
}
const shouldCollapseNeighborhood = Boolean(
viewMode !== "grouped"
&& collapsedNeighborhoodNodeIds.has(selectedNodeId),
);
const collapsedState = shouldCollapseNeighborhood
? buildCollapsedNeighborhoodState(selectedNodeId, activePath)
: {
selectedVisibleNeighborIds: rankNeighbors(selectedNodeId),
selectedCollapsedNeighborIds: [],
};
displayState.selectedRootNodeId = selectedNodeId;
displayState.selectedVisibleNeighborIds = collapsedState.selectedVisibleNeighborIds;
displayState.selectedCollapsedNeighborIds = collapsedState.selectedCollapsedNeighborIds;
return displayState;
}
export function createFocusedGraph(
nodeId: string,
activePath: string[],
@@ -1247,8 +1311,8 @@ export function createFocusedGraph(
const selectedState = resolveNodeElementStyle(GRAPH_THEME, "inspection", "selected", selectedAttrs, selectedAttrs.label);
addNode(nodeId, {
...selectedAttrs,
x: 0,
y: 0,
x: Number.isFinite(selectedAttrs.x) ? selectedAttrs.x : 0,
y: Number.isFinite(selectedAttrs.y) ? selectedAttrs.y : 0,
color: selectedState.color,
size: Math.max(selectedState.size, 22),
baseColor: selectedState.color,
@@ -1286,8 +1350,8 @@ export function createFocusedGraph(
addNode(neighborId, {
...baseAttrs,
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius,
x: Number.isFinite(baseAttrs.x) ? baseAttrs.x : Math.cos(angle) * radius,
y: Number.isFinite(baseAttrs.y) ? baseAttrs.y : Math.sin(angle) * radius,
color: style.color,
size: Math.max(style.size, 8.5),
baseColor: style.color,
@@ -1332,33 +1396,22 @@ export function resolveDisplayGraph(
options?: {
aggregationEnabled?: boolean;
collapsedNeighborhoodNodeIds?: Iterable<string>;
groupedViewAvailable?: boolean;
},
): GraphDisplayResult {
const aggregationEnabled = options?.aggregationEnabled ?? true;
const collapsedNeighborhoodNodeIds = new Set(
Array.from(options?.collapsedNeighborhoodNodeIds ?? []).filter((nodeId) => typeof nodeId === "string"),
);
const displayState = createEmptyDisplayState(selectedNodeId, aggregationEnabled);
displayState.groupedViewAvailable = computeGraphAnalyticsBase(graph, {
computeCommunities: true,
computeCentrality: false,
}).communitiesByNode.size > 0;
const displayState = resolveDisplayStateSnapshot(selectedNodeId, activePath, viewMode, {
aggregationEnabled,
collapsedNeighborhoodNodeIds,
groupedViewAvailable: options?.groupedViewAvailable,
});
const isFocusedView = viewMode === "focused" && Boolean(selectedNodeId) && graph.hasNode(selectedNodeId);
const isGroupedView = viewMode === "grouped";
const shouldCollapseNeighborhood = Boolean(selectedNodeId && collapsedNeighborhoodNodeIds.has(selectedNodeId));
if (selectedNodeId && graph.hasNode(selectedNodeId)) {
const collapsedState = shouldCollapseNeighborhood
? buildCollapsedNeighborhoodState(selectedNodeId, activePath)
: {
selectedVisibleNeighborIds: rankNeighbors(selectedNodeId),
selectedCollapsedNeighborIds: [],
};
displayState.selectedRootNodeId = selectedNodeId;
displayState.selectedVisibleNeighborIds = collapsedState.selectedVisibleNeighborIds;
displayState.selectedCollapsedNeighborIds = collapsedState.selectedCollapsedNeighborIds;
}
if (isGroupedView) {
const grouped = buildCommunityGroupedGraph();
return {
@@ -1369,6 +1422,7 @@ export function resolveDisplayGraph(
selectedVisibleNeighborIds: displayState.selectedVisibleNeighborIds,
selectedCollapsedNeighborIds: displayState.selectedCollapsedNeighborIds,
},
meta: grouped.meta,
};
}
@@ -1376,6 +1430,7 @@ export function resolveDisplayGraph(
return {
graph: createFocusedGraph(selectedNodeId, activePath, activePathEdgeIds, shouldCollapseNeighborhood),
state: displayState,
meta: MIRRORED_DISPLAY_META,
};
}
@@ -1386,6 +1441,11 @@ export function resolveDisplayGraph(
return {
graph: aggregationEnabled ? aggregateDisplayGraph(baseGraph) : baseGraph,
state: displayState,
meta: aggregationEnabled
? MIRRORED_DISPLAY_META
: baseGraph === graph
? BASE_DISPLAY_META
: MIRRORED_DISPLAY_META,
};
}
@@ -5,6 +5,8 @@ import { graph, type EdgeAttributes, type NodeAttributes } from "../../store/gra
import type {
GraphAnalyticsSnapshot,
GraphCameraState,
GraphDisplayMeta,
GraphDisplayStateSnapshot,
GraphDiagnosticsSnapshot,
GraphEffectsState,
GraphInteractionState,
@@ -22,6 +24,8 @@ export interface GraphSceneRuntime {
scene: unknown;
graph: GraphSceneGraph;
displayGraph: GraphSceneGraph;
graphVersion: number;
layoutMode?: GraphDisplayMeta["layoutMode"];
requestRender: () => void;
getCameraState: () => GraphCameraState | null;
}
@@ -37,6 +41,11 @@ export interface GraphSceneEventMap {
}
export interface GraphSceneProps extends GraphSceneEventMap {
graphVersion: number;
graphReady: boolean;
displayGraph: GraphSceneGraph;
displayMeta: GraphDisplayMeta;
displayState?: GraphDisplayStateSnapshot;
selectedNodeId: string;
selectedEdgeId: string;
activePath?: string[];
@@ -48,8 +57,6 @@ export interface GraphSceneProps extends GraphSceneEventMap {
layoutSource?: GraphLayoutSource;
onLayoutStatusChange?: (status: GraphLayoutStatus) => void;
viewMode: GraphViewMode;
aggregationEnabled?: boolean;
collapsedNeighborhoodNodeIds?: string[];
className?: string;
showFitViewButton?: boolean;
pluginOverlays?: ReactNode[];
@@ -58,6 +65,8 @@ export interface GraphSceneProps extends GraphSceneEventMap {
export interface GraphSceneHandle {
fitView: () => void;
focusNode: (nodeId: string) => void;
zoomIn: () => void;
zoomOut: () => void;
getRuntime: () => GraphSceneRuntime | null;
setLayoutRunning?: (running: boolean) => void;
}
@@ -39,6 +39,15 @@ export interface GraphDisplayStateSnapshot {
selectedCollapsedNeighborIds: string[];
}
export type GraphDisplayLayoutMode = "base" | "mirrored" | "owned";
export interface GraphDisplayMeta {
layoutMode: GraphDisplayLayoutMode;
positionSource: "store" | "display";
tracksStoreNodePositions: boolean;
hasSyntheticNodes: boolean;
}
export type GraphEffectToggle =
| "pathPulseEnabled"
| "pathFlowEnabled"
+2 -1
View File
@@ -24,5 +24,6 @@
"extraction",
"mcp"
],
"skills": "./skills"
"skills": "./skills",
"agents": "./agents"
}