diff --git a/lightrag_webui/src/components/graph/FocusOnNode.tsx b/lightrag_webui/src/components/graph/FocusOnNode.tsx index 70af7525..ddc609b9 100644 --- a/lightrag_webui/src/components/graph/FocusOnNode.tsx +++ b/lightrag_webui/src/components/graph/FocusOnNode.tsx @@ -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. */ useEffect(() => { + const graph = sigma.getGraph(); + if (move) { - if (node) { - sigma.getGraph().setNodeAttribute(node, 'highlighted', true) - gotoNode(node) + if (node && graph.hasNode(node)) { + try { + graph.setNodeAttribute(node, 'highlighted', true); + gotoNode(node); + } catch (error) { + console.error('Error focusing on node:', error); + } } else { // If no node is selected but move is true, reset to default view - sigma.setCustomBBox(null) - sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 }) + sigma.setCustomBBox(null); + 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 () => { - if (node) { - sigma.getGraph().setNodeAttribute(node, 'highlighted', false) + if (node && graph.hasNode(node)) { + try { + graph.setNodeAttribute(node, 'highlighted', false); + } catch (error) { + console.error('Error cleaning up node highlight:', error); + } } } }, [node, move, sigma, gotoNode]) diff --git a/lightrag_webui/src/components/graph/GraphControl.tsx b/lightrag_webui/src/components/graph/GraphControl.tsx index 7d014316..289c7575 100644 --- a/lightrag_webui/src/components/graph/GraphControl.tsx +++ b/lightrag_webui/src/components/graph/GraphControl.tsx @@ -1,5 +1,5 @@ -import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core' -import Graph from 'graphology' +import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core' +import { AbstractGraph } from 'graphology-types' // import { useLayoutCircular } from '@react-sigma/layout-circular' import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2' import { useEffect } from 'react' @@ -25,7 +25,6 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) const sigma = useSigma() const registerEvents = useRegisterEvents() const setSettings = useSetSettings() - const loadGraph = useLoadGraph() const maxIterations = useSettingsStore.use.graphLayoutMaxIterations() const { assign: assignLayout } = useLayoutForceAtlas2({ @@ -45,14 +44,45 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) /** * When component mount or maxIterations changes - * => load the graph and apply layout + * => ensure graph reference and apply layout */ useEffect(() => { - if (sigmaGraph) { - loadGraph(sigmaGraph as unknown as Graph) - assignLayout() + if (sigmaGraph && sigma) { + // 确保 sigma 实例内部的 graph 引用被更新 + try { + // 尝试直接设置 sigma 实例的 graph 引用 + if (typeof sigma.setGraph === 'function') { + sigma.setGraph(sigmaGraph as unknown as AbstractGraph); + console.log('Directly set graph on sigma instance'); + } else { + // 如果 setGraph 方法不存在,尝试直接设置 graph 属性 + (sigma as any).graph = sigmaGraph; + console.log('Set graph property on sigma instance'); + } + } catch (error) { + console.error('Error setting graph on sigma instance:', error); + } + + // 应用布局 + assignLayout(); + console.log('Layout applied to graph'); } - }, [assignLayout, loadGraph, sigmaGraph, maxIterations]) + }, [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 @@ -138,14 +168,18 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) const _focusedNode = focusedNode || selectedNode const _focusedEdge = focusedEdge || selectedEdge - if (_focusedNode) { - if (node === _focusedNode || graph.neighbors(_focusedNode).includes(node)) { - newData.highlighted = true - if (node === selectedNode) { - newData.borderColor = Constants.nodeBorderColorSelected + if (_focusedNode && graph.hasNode(_focusedNode)) { + try { + if (node === _focusedNode || graph.neighbors(_focusedNode).includes(node)) { + newData.highlighted = true + if (node === selectedNode) { + newData.borderColor = Constants.nodeBorderColorSelected + } } + } catch (error) { + console.error('Error in nodeReducer:', error); } - } else if (_focusedEdge) { + } else if (_focusedEdge && graph.hasEdge(_focusedEdge)) { if (graph.extremities(_focusedEdge).includes(node)) { newData.highlighted = true newData.size = 3 @@ -173,21 +207,28 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) if (!disableHoverEffect) { const _focusedNode = focusedNode || selectedNode - if (_focusedNode) { - if (hideUnselectedEdges) { - if (!graph.extremities(edge).includes(_focusedNode)) { - newData.hidden = true - } - } else { - if (graph.extremities(edge).includes(_focusedNode)) { - newData.color = Constants.edgeColorHighlighted + if (_focusedNode && graph.hasNode(_focusedNode)) { + try { + if (hideUnselectedEdges) { + if (!graph.extremities(edge).includes(_focusedNode)) { + newData.hidden = true + } + } else { + if (graph.extremities(edge).includes(_focusedNode)) { + newData.color = Constants.edgeColorHighlighted + } } + } catch (error) { + console.error('Error in edgeReducer:', error); } } else { - if (focusedEdge || selectedEdge) { - if (edge === selectedEdge) { + const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null; + const _focusedEdge = focusedEdge && graph.hasEdge(focusedEdge) ? focusedEdge : null; + + if (_selectedEdge || _focusedEdge) { + if (edge === _selectedEdge) { newData.color = Constants.edgeColorSelected - } else if (edge === focusedEdge) { + } else if (edge === _focusedEdge) { newData.color = Constants.edgeColorHighlighted } else if (hideUnselectedEdges) { newData.hidden = true diff --git a/lightrag_webui/src/components/graph/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx index 3a5ea990..9cb74dd1 100644 --- a/lightrag_webui/src/components/graph/PropertiesView.tsx +++ b/lightrag_webui/src/components/graph/PropertiesView.tsx @@ -90,22 +90,46 @@ const refineNodeProperties = (node: RawNodeType): NodeType => { const relationships = [] if (state.sigmaGraph && state.rawGraph) { - for (const edgeId of state.sigmaGraph.edges(node.id)) { - const edge = state.rawGraph.getEdge(edgeId, true) - if (edge) { - const isTarget = node.id === edge.source - const neighbourId = isTarget ? edge.target : edge.source - const neighbour = state.rawGraph.getNode(neighbourId) - if (neighbour) { - relationships.push({ - type: 'Neighbour', - id: neighbourId, - label: neighbour.properties['entity_id'] ? neighbour.properties['entity_id'] : neighbour.labels.join(', ') - }) + 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) + if (edge) { + const isTarget = node.id === edge.source + const neighbourId = isTarget ? edge.target : edge.source + + // 检查邻居节点是否存在 + if (!state.sigmaGraph.hasNode(neighbourId)) continue; + + const neighbour = state.rawGraph.getNode(neighbourId) + if (neighbour) { + relationships.push({ + type: 'Neighbour', + id: neighbourId, + label: neighbour.properties['entity_id'] ? neighbour.properties['entity_id'] : neighbour.labels.join(', ') + }) + } + } + } + } catch (error) { + console.error('Error refining node properties:', error) } } + return { ...node, relationships @@ -114,8 +138,34 @@ const refineNodeProperties = (node: RawNodeType): NodeType => { const refineEdgeProperties = (edge: RawEdgeType): EdgeType => { const state = useGraphStore.getState() - const sourceNode = state.rawGraph?.getNode(edge.source) - const targetNode = state.rawGraph?.getNode(edge.target) + let sourceNode: RawNodeType | undefined = undefined + 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 { ...edge, sourceNode, diff --git a/lightrag_webui/src/contexts/TabVisibilityProvider.tsx b/lightrag_webui/src/contexts/TabVisibilityProvider.tsx index 5db8f634..f4e2b044 100644 --- a/lightrag_webui/src/contexts/TabVisibilityProvider.tsx +++ b/lightrag_webui/src/contexts/TabVisibilityProvider.tsx @@ -15,16 +15,22 @@ export const TabVisibilityProvider: React.FC = ({ ch // Get current tab from settings store const currentTab = useSettingsStore.use.currentTab(); - // Initialize visibility state with current tab as visible + // Initialize visibility state with all tabs visible const [visibleTabs, setVisibleTabs] = useState>(() => ({ - [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(() => { setVisibleTabs((prev) => ({ ...prev, - [currentTab]: true + 'documents': true, + 'knowledge-graph': true, + 'retrieval': true, + 'api': true })); }, [currentTab]); diff --git a/lightrag_webui/src/features/GraphViewer.tsx b/lightrag_webui/src/features/GraphViewer.tsx index a12e2324..97721612 100644 --- a/lightrag_webui/src/features/GraphViewer.tsx +++ b/lightrag_webui/src/features/GraphViewer.tsx @@ -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 { MiniMap } from '@react-sigma/minimap' import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core' @@ -147,6 +147,49 @@ const GraphViewer = () => { useEffect(() => { 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) => { if (value === null) useGraphStore.getState().setFocusedNode(null) @@ -167,62 +210,51 @@ const GraphViewer = () => { [selectedNode] ) - // Since TabsContent now forces mounting of all tabs, we need to conditionally render - // the SigmaContainer based on visibility to avoid unnecessary rendering + // Always render SigmaContainer but control its visibility with CSS return (
- {/* Only render the SigmaContainer when the tab is visible */} - {isGraphTabVisible ? ( - - + + - {enableNodeDrag && } + {enableNodeDrag && } - + -
- - {showNodeSearchBar && ( - - )} -
- -
- - - - - {/* */} -
- - {showPropertyPanel && ( -
- -
+
+ + {showNodeSearchBar && ( + )} - - {/*
- -
*/} - - - - ) : ( - // Placeholder when tab is not visible -
-
- {/* Placeholder content */} -
- )} + +
+ + + + + {/* */} +
+ + {showPropertyPanel && ( +
+ +
+ )} + + {/*
+ +
*/} + + + {/* Loading overlay - shown when data is loading */} {isFetching && ( diff --git a/lightrag_webui/src/hooks/useLightragGraph.tsx b/lightrag_webui/src/hooks/useLightragGraph.tsx index adee940d..d02c0f8c 100644 --- a/lightrag_webui/src/hooks/useLightragGraph.tsx +++ b/lightrag_webui/src/hooks/useLightragGraph.tsx @@ -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(); } catch (error) { @@ -637,28 +638,27 @@ const useLightrangeGraph = () => { if (!nodeId || !sigmaGraph || !rawGraph) return; try { - // Check if the node exists + const state = useGraphStore.getState(); + + // 1. 检查节点是否存在 if (!sigmaGraph.hasNode(nodeId)) { console.error('Node not found:', nodeId); return; } - // Get all nodes that will be deleted (including isolated nodes) + // 2. 获取要删除的节点 const nodesToDelete = getNodesThatWillBeDeleted(nodeId, sigmaGraph); - // Check if we would delete all nodes in the graph + // 3. 检查是否会删除所有节点 if (nodesToDelete.size === sigmaGraph.nodes().length) { toast.error(t('graphPanel.propertiesView.node.deleteAllNodesError')); return; } - // If the node is selected or focused, clear selection - const state = useGraphStore.getState(); - if (state.selectedNode === nodeId || state.focusedNode === nodeId) { - state.clearSelection(); - } + // 4. 清除选中状态 - 这会导致PropertiesView立即关闭 + state.clearSelection(); - // Process all nodes that need to be deleted + // 5. 删除节点和相关边 for (const nodeToDelete of nodesToDelete) { // Remove the node from the sigma graph (this will also remove connected edges) sigmaGraph.dropNode(nodeToDelete); @@ -713,7 +713,8 @@ const useLightrangeGraph = () => { 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(); } catch (error) { diff --git a/lightrag_webui/src/stores/graph.ts b/lightrag_webui/src/stores/graph.ts index f31cd2ee..c68b78c5 100644 --- a/lightrag_webui/src/stores/graph.ts +++ b/lightrag_webui/src/stores/graph.ts @@ -66,6 +66,7 @@ interface GraphState { rawGraph: RawGraph | null sigmaGraph: DirectedGraph | null + sigmaInstance: any | null allDatabaseLabels: string[] moveToSelectedNode: boolean @@ -77,6 +78,7 @@ interface GraphState { labelsFetchAttempted: boolean refreshLayout: () => void + setSigmaInstance: (instance: any) => void setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void setFocusedNode: (nodeId: string | null) => void setSelectedEdge: (edgeId: string | null) => void @@ -122,17 +124,45 @@ const useGraphStoreBase = create()((set, get) => ({ rawGraph: null, sigmaGraph: null, + sigmaInstance: null, allDatabaseLabels: ['*'], refreshLayout: () => { - const currentGraph = get().sigmaGraph; - if (currentGraph) { - get().clearSelection(); - get().setSigmaGraph(null); - setTimeout(() => { - get().setSigmaGraph(currentGraph); - }, 10); + const { sigmaInstance, sigmaGraph } = get(); + + // Debug information to help diagnose issues + console.log('refreshLayout called with:', { + hasSigmaInstance: !!sigmaInstance, + hasSigmaGraph: !!sigmaGraph, + 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 }), @@ -197,6 +227,8 @@ const useGraphStoreBase = create()((set, get) => ({ }, setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }), + + setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }), // Methods to set global flags setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }), @@ -456,6 +488,9 @@ const useGraphStoreBase = create()((set, get) => ({ // Rebuild the dynamic edge map state.rawGraph.buildDynamicMap(); } + + // Refresh the layout to update the visualization + state.refreshLayout(); } catch (error) { console.error('Error pruning node:', error);