Avoid graphics flickering during node operations

This commit is contained in:
yangdx
2025-03-14 23:25:38 +08:00
parent 1ae65c9272
commit 5decd03e2e
7 changed files with 300 additions and 121 deletions

View File

@@ -13,23 +13,37 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
* When the selected item changes, highlighted the node and center the camera on it. * When the selected item changes, highlighted the node and center the camera on it.
*/ */
useEffect(() => { useEffect(() => {
const graph = sigma.getGraph();
if (move) { if (move) {
if (node) { if (node && graph.hasNode(node)) {
sigma.getGraph().setNodeAttribute(node, 'highlighted', true) try {
gotoNode(node) graph.setNodeAttribute(node, 'highlighted', true);
gotoNode(node);
} catch (error) {
console.error('Error focusing on node:', error);
}
} else { } else {
// If no node is selected but move is true, reset to default view // If no node is selected but move is true, reset to default view
sigma.setCustomBBox(null) sigma.setCustomBBox(null);
sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 }) sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 });
}
useGraphStore.getState().setMoveToSelectedNode(false);
} else if (node && graph.hasNode(node)) {
try {
graph.setNodeAttribute(node, 'highlighted', true);
} catch (error) {
console.error('Error highlighting node:', error);
} }
useGraphStore.getState().setMoveToSelectedNode(false)
} else if (node) {
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
} }
return () => { return () => {
if (node) { if (node && graph.hasNode(node)) {
sigma.getGraph().setNodeAttribute(node, 'highlighted', false) try {
graph.setNodeAttribute(node, 'highlighted', false);
} catch (error) {
console.error('Error cleaning up node highlight:', error);
}
} }
} }
}, [node, move, sigma, gotoNode]) }, [node, move, sigma, gotoNode])

View File

@@ -1,5 +1,5 @@
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core' import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
import Graph from 'graphology' import { AbstractGraph } from 'graphology-types'
// import { useLayoutCircular } from '@react-sigma/layout-circular' // import { useLayoutCircular } from '@react-sigma/layout-circular'
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2' import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
import { useEffect } from 'react' import { useEffect } from 'react'
@@ -25,7 +25,6 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
const sigma = useSigma<NodeType, EdgeType>() const sigma = useSigma<NodeType, EdgeType>()
const registerEvents = useRegisterEvents<NodeType, EdgeType>() const registerEvents = useRegisterEvents<NodeType, EdgeType>()
const setSettings = useSetSettings<NodeType, EdgeType>() const setSettings = useSetSettings<NodeType, EdgeType>()
const loadGraph = useLoadGraph<NodeType, EdgeType>()
const maxIterations = useSettingsStore.use.graphLayoutMaxIterations() const maxIterations = useSettingsStore.use.graphLayoutMaxIterations()
const { assign: assignLayout } = useLayoutForceAtlas2({ const { assign: assignLayout } = useLayoutForceAtlas2({
@@ -45,14 +44,45 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
/** /**
* When component mount or maxIterations changes * When component mount or maxIterations changes
* => load the graph and apply layout * => ensure graph reference and apply layout
*/ */
useEffect(() => { useEffect(() => {
if (sigmaGraph) { if (sigmaGraph && sigma) {
loadGraph(sigmaGraph as unknown as Graph<NodeType, EdgeType>) // 确保 sigma 实例内部的 graph 引用被更新
assignLayout() try {
// 尝试直接设置 sigma 实例的 graph 引用
if (typeof sigma.setGraph === 'function') {
sigma.setGraph(sigmaGraph as unknown as AbstractGraph<NodeType, EdgeType>);
console.log('Directly set graph on sigma instance');
} else {
// 如果 setGraph 方法不存在,尝试直接设置 graph 属性
(sigma as any).graph = sigmaGraph;
console.log('Set graph property on sigma instance');
} }
}, [assignLayout, loadGraph, sigmaGraph, maxIterations]) } catch (error) {
console.error('Error setting graph on sigma instance:', error);
}
// 应用布局
assignLayout();
console.log('Layout applied to graph');
}
}, [sigma, sigmaGraph, assignLayout, maxIterations])
/**
* Ensure the sigma instance is set in the store
* This provides a backup in case the instance wasn't set in GraphViewer
*/
useEffect(() => {
if (sigma) {
// Double-check that the store has the sigma instance
const currentInstance = useGraphStore.getState().sigmaInstance;
if (!currentInstance) {
console.log('Setting sigma instance from GraphControl (backup)');
useGraphStore.getState().setSigmaInstance(sigma);
}
}
}, [sigma]);
/** /**
* When component mount * When component mount
@@ -138,14 +168,18 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
const _focusedNode = focusedNode || selectedNode const _focusedNode = focusedNode || selectedNode
const _focusedEdge = focusedEdge || selectedEdge const _focusedEdge = focusedEdge || selectedEdge
if (_focusedNode) { if (_focusedNode && graph.hasNode(_focusedNode)) {
try {
if (node === _focusedNode || graph.neighbors(_focusedNode).includes(node)) { if (node === _focusedNode || graph.neighbors(_focusedNode).includes(node)) {
newData.highlighted = true newData.highlighted = true
if (node === selectedNode) { if (node === selectedNode) {
newData.borderColor = Constants.nodeBorderColorSelected newData.borderColor = Constants.nodeBorderColorSelected
} }
} }
} else if (_focusedEdge) { } catch (error) {
console.error('Error in nodeReducer:', error);
}
} else if (_focusedEdge && graph.hasEdge(_focusedEdge)) {
if (graph.extremities(_focusedEdge).includes(node)) { if (graph.extremities(_focusedEdge).includes(node)) {
newData.highlighted = true newData.highlighted = true
newData.size = 3 newData.size = 3
@@ -173,7 +207,8 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
if (!disableHoverEffect) { if (!disableHoverEffect) {
const _focusedNode = focusedNode || selectedNode const _focusedNode = focusedNode || selectedNode
if (_focusedNode) { if (_focusedNode && graph.hasNode(_focusedNode)) {
try {
if (hideUnselectedEdges) { if (hideUnselectedEdges) {
if (!graph.extremities(edge).includes(_focusedNode)) { if (!graph.extremities(edge).includes(_focusedNode)) {
newData.hidden = true newData.hidden = true
@@ -183,11 +218,17 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
newData.color = Constants.edgeColorHighlighted newData.color = Constants.edgeColorHighlighted
} }
} }
} catch (error) {
console.error('Error in edgeReducer:', error);
}
} else { } else {
if (focusedEdge || selectedEdge) { const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null;
if (edge === selectedEdge) { const _focusedEdge = focusedEdge && graph.hasEdge(focusedEdge) ? focusedEdge : null;
if (_selectedEdge || _focusedEdge) {
if (edge === _selectedEdge) {
newData.color = Constants.edgeColorSelected newData.color = Constants.edgeColorSelected
} else if (edge === focusedEdge) { } else if (edge === _focusedEdge) {
newData.color = Constants.edgeColorHighlighted newData.color = Constants.edgeColorHighlighted
} else if (hideUnselectedEdges) { } else if (hideUnselectedEdges) {
newData.hidden = true newData.hidden = true

View File

@@ -90,11 +90,31 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
const relationships = [] const relationships = []
if (state.sigmaGraph && state.rawGraph) { if (state.sigmaGraph && state.rawGraph) {
for (const edgeId of state.sigmaGraph.edges(node.id)) { try {
// 检查节点是否还存在
if (!state.sigmaGraph.hasNode(node.id)) {
return {
...node,
relationships: []
}
}
// 获取所有边
const edges = state.sigmaGraph.edges(node.id)
// 处理每条边
for (const edgeId of edges) {
// 检查边是否还存在
if (!state.sigmaGraph.hasEdge(edgeId)) continue;
const edge = state.rawGraph.getEdge(edgeId, true) const edge = state.rawGraph.getEdge(edgeId, true)
if (edge) { if (edge) {
const isTarget = node.id === edge.source const isTarget = node.id === edge.source
const neighbourId = isTarget ? edge.target : edge.source const neighbourId = isTarget ? edge.target : edge.source
// 检查邻居节点是否存在
if (!state.sigmaGraph.hasNode(neighbourId)) continue;
const neighbour = state.rawGraph.getNode(neighbourId) const neighbour = state.rawGraph.getNode(neighbourId)
if (neighbour) { if (neighbour) {
relationships.push({ relationships.push({
@@ -105,7 +125,11 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
} }
} }
} }
} catch (error) {
console.error('Error refining node properties:', error)
} }
}
return { return {
...node, ...node,
relationships relationships
@@ -114,8 +138,34 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
const refineEdgeProperties = (edge: RawEdgeType): EdgeType => { const refineEdgeProperties = (edge: RawEdgeType): EdgeType => {
const state = useGraphStore.getState() const state = useGraphStore.getState()
const sourceNode = state.rawGraph?.getNode(edge.source) let sourceNode: RawNodeType | undefined = undefined
const targetNode = state.rawGraph?.getNode(edge.target) let targetNode: RawNodeType | undefined = undefined
if (state.sigmaGraph && state.rawGraph) {
try {
// 检查边是否还存在
if (!state.sigmaGraph.hasEdge(edge.id)) {
return {
...edge,
sourceNode: undefined,
targetNode: undefined
}
}
// 检查源节点是否存在
if (state.sigmaGraph.hasNode(edge.source)) {
sourceNode = state.rawGraph.getNode(edge.source)
}
// 检查目标节点是否存在
if (state.sigmaGraph.hasNode(edge.target)) {
targetNode = state.rawGraph.getNode(edge.target)
}
} catch (error) {
console.error('Error refining edge properties:', error)
}
}
return { return {
...edge, ...edge,
sourceNode, sourceNode,

View File

@@ -15,16 +15,22 @@ export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ ch
// Get current tab from settings store // Get current tab from settings store
const currentTab = useSettingsStore.use.currentTab(); const currentTab = useSettingsStore.use.currentTab();
// Initialize visibility state with current tab as visible // Initialize visibility state with all tabs visible
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({ const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
[currentTab]: true 'documents': true,
'knowledge-graph': true,
'retrieval': true,
'api': true
})); }));
// Update visibility when current tab changes // Keep all tabs visible when current tab changes
useEffect(() => { useEffect(() => {
setVisibleTabs((prev) => ({ setVisibleTabs((prev) => ({
...prev, ...prev,
[currentTab]: true 'documents': true,
'knowledge-graph': true,
'retrieval': true,
'api': true
})); }));
}, [currentTab]); }, [currentTab]);

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useCallback, useMemo, useRef } from 'react' import { useEffect, useLayoutEffect, useState, useCallback, useMemo, useRef } from 'react'
import { useTabVisibility } from '@/contexts/useTabVisibility' import { useTabVisibility } from '@/contexts/useTabVisibility'
// import { MiniMap } from '@react-sigma/minimap' // import { MiniMap } from '@react-sigma/minimap'
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core' import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
@@ -148,6 +148,49 @@ const GraphViewer = () => {
setSigmaSettings(defaultSigmaSettings) setSigmaSettings(defaultSigmaSettings)
}, []) }, [])
// Clean up sigma instance when component unmounts
useEffect(() => {
return () => {
// Clear the sigma instance when component unmounts
useGraphStore.getState().setSigmaInstance(null);
console.log('Cleared sigma instance on unmount');
};
}, []);
// Get the sigmaGraph from the store
const sigmaGraph = useGraphStore.use.sigmaGraph();
// Set the sigma instance in the graph store when it's available
// Using useLayoutEffect to ensure this runs before child components need the instance
useLayoutEffect(() => {
if (sigmaRef.current?.sigma) {
const instance = sigmaRef.current.sigma;
// Get the sigma instance from the ref and store it
console.log('Setting sigma instance in graph store (layout effect)');
useGraphStore.getState().setSigmaInstance(instance);
// If we also have a graph, bind it to the sigma instance
if (sigmaGraph) {
try {
// Try to set the graph on the sigma instance
if (typeof instance.setGraph === 'function') {
instance.setGraph(sigmaGraph);
console.log('Directly set graph on sigma instance in GraphViewer');
} else {
// If setGraph method doesn't exist, try to set the graph property directly
(instance as any).graph = sigmaGraph;
console.log('Set graph property on sigma instance in GraphViewer');
}
} catch (error) {
console.error('Error setting graph on sigma instance in GraphViewer:', error);
}
}
}
// We want this to run when either the ref or the graph changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sigmaRef.current, sigmaGraph]);
const onSearchFocus = useCallback((value: GraphSearchOption | null) => { const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
if (value === null) useGraphStore.getState().setFocusedNode(null) if (value === null) useGraphStore.getState().setFocusedNode(null)
else if (value.type === 'nodes') useGraphStore.getState().setFocusedNode(value.id) else if (value.type === 'nodes') useGraphStore.getState().setFocusedNode(value.id)
@@ -167,12 +210,9 @@ const GraphViewer = () => {
[selectedNode] [selectedNode]
) )
// Since TabsContent now forces mounting of all tabs, we need to conditionally render // Always render SigmaContainer but control its visibility with CSS
// the SigmaContainer based on visibility to avoid unnecessary rendering
return ( return (
<div className="relative h-full w-full"> <div className="relative h-full w-full">
{/* Only render the SigmaContainer when the tab is visible */}
{isGraphTabVisible ? (
<SigmaContainer <SigmaContainer
settings={sigmaSettings} settings={sigmaSettings}
className="!bg-background !size-full overflow-hidden" className="!bg-background !size-full overflow-hidden"
@@ -215,14 +255,6 @@ const GraphViewer = () => {
<SettingsDisplay /> <SettingsDisplay />
</SigmaContainer> </SigmaContainer>
) : (
// Placeholder when tab is not visible
<div className="flex h-full w-full items-center justify-center">
<div className="text-center text-muted-foreground">
{/* Placeholder content */}
</div>
</div>
)}
{/* Loading overlay - shown when data is loading */} {/* Loading overlay - shown when data is loading */}
{isFetching && ( {isFetching && (

View File

@@ -589,7 +589,8 @@ const useLightrangeGraph = () => {
} }
} }
// Refresh the layout // We need to keep the refreshLayout call because Sigma doesn't automatically detect
// changes to the DirectedGraph object. This is necessary to trigger a re-render.
useGraphStore.getState().refreshLayout(); useGraphStore.getState().refreshLayout();
} catch (error) { } catch (error) {
@@ -637,28 +638,27 @@ const useLightrangeGraph = () => {
if (!nodeId || !sigmaGraph || !rawGraph) return; if (!nodeId || !sigmaGraph || !rawGraph) return;
try { try {
// Check if the node exists const state = useGraphStore.getState();
// 1. 检查节点是否存在
if (!sigmaGraph.hasNode(nodeId)) { if (!sigmaGraph.hasNode(nodeId)) {
console.error('Node not found:', nodeId); console.error('Node not found:', nodeId);
return; return;
} }
// Get all nodes that will be deleted (including isolated nodes) // 2. 获取要删除的节点
const nodesToDelete = getNodesThatWillBeDeleted(nodeId, sigmaGraph); const nodesToDelete = getNodesThatWillBeDeleted(nodeId, sigmaGraph);
// Check if we would delete all nodes in the graph // 3. 检查是否会删除所有节点
if (nodesToDelete.size === sigmaGraph.nodes().length) { if (nodesToDelete.size === sigmaGraph.nodes().length) {
toast.error(t('graphPanel.propertiesView.node.deleteAllNodesError')); toast.error(t('graphPanel.propertiesView.node.deleteAllNodesError'));
return; return;
} }
// If the node is selected or focused, clear selection // 4. 清除选中状态 - 这会导致PropertiesView立即关闭
const state = useGraphStore.getState();
if (state.selectedNode === nodeId || state.focusedNode === nodeId) {
state.clearSelection(); state.clearSelection();
}
// Process all nodes that need to be deleted // 5. 删除节点和相关边
for (const nodeToDelete of nodesToDelete) { for (const nodeToDelete of nodesToDelete) {
// Remove the node from the sigma graph (this will also remove connected edges) // Remove the node from the sigma graph (this will also remove connected edges)
sigmaGraph.dropNode(nodeToDelete); sigmaGraph.dropNode(nodeToDelete);
@@ -713,7 +713,8 @@ const useLightrangeGraph = () => {
toast.info(t('graphPanel.propertiesView.node.nodesRemoved', { count: nodesToDelete.size })); toast.info(t('graphPanel.propertiesView.node.nodesRemoved', { count: nodesToDelete.size }));
} }
// Force a refresh of the graph layout // We need to keep the refreshLayout call because Sigma doesn't automatically detect
// changes to the DirectedGraph object. This is necessary to trigger a re-render.
useGraphStore.getState().refreshLayout(); useGraphStore.getState().refreshLayout();
} catch (error) { } catch (error) {

View File

@@ -66,6 +66,7 @@ interface GraphState {
rawGraph: RawGraph | null rawGraph: RawGraph | null
sigmaGraph: DirectedGraph | null sigmaGraph: DirectedGraph | null
sigmaInstance: any | null
allDatabaseLabels: string[] allDatabaseLabels: string[]
moveToSelectedNode: boolean moveToSelectedNode: boolean
@@ -77,6 +78,7 @@ interface GraphState {
labelsFetchAttempted: boolean labelsFetchAttempted: boolean
refreshLayout: () => void refreshLayout: () => void
setSigmaInstance: (instance: any) => void
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
setFocusedNode: (nodeId: string | null) => void setFocusedNode: (nodeId: string | null) => void
setSelectedEdge: (edgeId: string | null) => void setSelectedEdge: (edgeId: string | null) => void
@@ -122,17 +124,45 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
rawGraph: null, rawGraph: null,
sigmaGraph: null, sigmaGraph: null,
sigmaInstance: null,
allDatabaseLabels: ['*'], allDatabaseLabels: ['*'],
refreshLayout: () => { refreshLayout: () => {
const currentGraph = get().sigmaGraph; const { sigmaInstance, sigmaGraph } = get();
if (currentGraph) {
get().clearSelection(); // Debug information to help diagnose issues
get().setSigmaGraph(null); console.log('refreshLayout called with:', {
setTimeout(() => { hasSigmaInstance: !!sigmaInstance,
get().setSigmaGraph(currentGraph); hasSigmaGraph: !!sigmaGraph,
}, 10); sigmaInstanceType: sigmaInstance ? typeof sigmaInstance : 'null',
sigmaGraphNodeCount: sigmaGraph ? sigmaGraph.order : 0
});
if (sigmaInstance && sigmaGraph) {
try {
// 先尝试直接刷新
if (typeof sigmaInstance.refresh === 'function') {
sigmaInstance.refresh();
console.log('Graph refreshed using sigma.refresh()');
return;
} }
// 如果没有refresh方法,尝试重新绑定graph
if (typeof sigmaInstance.setGraph === 'function') {
sigmaInstance.setGraph(sigmaGraph);
console.log('Rebound graph to sigma instance');
} else {
// 如果setGraph方法不存在尝试直接设置graph属性
(sigmaInstance as any).graph = sigmaGraph;
console.log('Set graph property directly on sigma instance');
}
} catch (error) {
console.error('Error during refresh:', error);
}
}
// 通知UI需要重新渲染
set(state => ({ ...state }));
}, },
setIsFetching: (isFetching: boolean) => set({ isFetching }), setIsFetching: (isFetching: boolean) => set({ isFetching }),
@@ -198,6 +228,8 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }), setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),
setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }),
// Methods to set global flags // Methods to set global flags
setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }), setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted }), setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted }),
@@ -457,6 +489,9 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
state.rawGraph.buildDynamicMap(); state.rawGraph.buildDynamicMap();
} }
// Refresh the layout to update the visualization
state.refreshLayout();
} catch (error) { } catch (error) {
console.error('Error pruning node:', error); console.error('Error pruning node:', error);
} }