diff --git a/lightrag_webui/src/components/graph/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx index 243c26cc..fccda940 100644 --- a/lightrag_webui/src/components/graph/GraphLabels.tsx +++ b/lightrag_webui/src/components/graph/GraphLabels.tsx @@ -103,6 +103,22 @@ const GraphLabels = () => { if (newLabel === '...') { newLabel = '*' } + + // Reset the fetch attempted flag to force a new data fetch + useGraphStore.getState().setGraphDataFetchAttempted(false) + + // Clear current graph data to ensure complete reload when label changes + if (newLabel !== currentLabel) { + const graphStore = useGraphStore.getState(); + graphStore.clearSelection(); + + // Reset the graph state but preserve the instance + if (graphStore.sigmaGraph) { + const nodes = Array.from(graphStore.sigmaGraph.nodes()); + nodes.forEach(node => graphStore.sigmaGraph?.dropNode(node)); + } + } + if (newLabel === currentLabel && newLabel !== '*') { // 选择相同标签时切换到'*' useSettingsStore.getState().setQueryLabel('*') diff --git a/lightrag_webui/src/components/graph/GraphSearch.tsx b/lightrag_webui/src/components/graph/GraphSearch.tsx index 05a26c68..2ba36bda 100644 --- a/lightrag_webui/src/components/graph/GraphSearch.tsx +++ b/lightrag_webui/src/components/graph/GraphSearch.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useMemo } from 'react' +import { FC, useCallback, useEffect, useMemo } from 'react' import { EdgeById, NodeById, @@ -28,6 +28,7 @@ function OptionComponent(item: OptionItem) { } const messageId = '__message_item' +// Reset this cache when graph changes to ensure fresh search results const lastGraph: any = { graph: null, searchEngine: null @@ -48,6 +49,15 @@ export const GraphSearchInput = ({ const { t } = useTranslation() const graph = useGraphStore.use.sigmaGraph() + // Force reset the cache when graph changes + useEffect(() => { + if (graph) { + // Reset cache to ensure fresh search results with new graph data + lastGraph.graph = null; + lastGraph.searchEngine = null; + } + }, [graph]); + const searchEngine = useMemo(() => { if (lastGraph.graph == graph) { return lastGraph.searchEngine diff --git a/lightrag_webui/src/components/ui/Tabs.tsx b/lightrag_webui/src/components/ui/Tabs.tsx index 87df84be..5a6c3904 100644 --- a/lightrag_webui/src/components/ui/Tabs.tsx +++ b/lightrag_webui/src/components/ui/Tabs.tsx @@ -45,6 +45,8 @@ const TabsContent = React.forwardRef< 'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none', className )} + // We no longer force mounting of inactive tabs + // This prevents the Graph component from being mounted when it's not the active tab {...props} /> )) diff --git a/lightrag_webui/src/features/GraphViewer.tsx b/lightrag_webui/src/features/GraphViewer.tsx index 85773c7f..16801a0f 100644 --- a/lightrag_webui/src/features/GraphViewer.tsx +++ b/lightrag_webui/src/features/GraphViewer.tsx @@ -128,12 +128,21 @@ const GraphViewer = () => { const enableNodeDrag = useSettingsStore.use.enableNodeDrag() const renderEdgeLabels = useSettingsStore.use.showEdgeLabel() - // Ensure rendering is enabled when tab becomes visible + // Handle component mount/unmount and tab visibility useEffect(() => { + // When component mounts or tab becomes visible if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) { // If tab is visible but graph is not rendering, try to enable rendering useGraphStore.getState().setShouldRender(true) initAttemptedRef.current = true + console.log('Graph viewer initialized') + } + + // Cleanup function when component unmounts + return () => { + // If we're navigating away from the graph tab completely (not just switching tabs) + // we would clean up here, but for now we want to preserve the WebGL context + console.log('Graph viewer cleanup') } }, [isGraphTabVisible, shouldRender, isFetching]) @@ -165,67 +174,72 @@ const GraphViewer = () => { [selectedNode] ) - // If we shouldn't render, show loading state or empty state - if (!shouldRender) { - return ( -
- {isFetching ? ( -
-
-

Reloading Graph Data...

-
- ) : ( -
- {/* Empty or hint message */} -
- )} -
- ) - } - + // Only render the SigmaContainer when the tab is visible return ( - - +
+ {isGraphTabVisible ? ( + // Only render SigmaContainer when tab is visible + + - {enableNodeDrag && } + {enableNodeDrag && } - + -
- - {showNodeSearchBar && ( - - )} -
+
+ + {showNodeSearchBar && ( + + )} +
-
- - - - - {/* */} -
+
+ + + + + {/* */} +
- {showPropertyPanel && ( -
- + {showPropertyPanel && ( +
+ +
+ )} + + {/*
+ +
*/} + + + + ) : ( + // Placeholder when tab is not visible +
+
+ {/* Placeholder content */} +
)} - {/*
- -
*/} - - - + {/* Loading overlay - shown when data is loading */} + {isFetching && ( +
+
+
+

Loading Graph Data...

+
+
+ )} +
) } diff --git a/lightrag_webui/src/hooks/useLightragGraph.tsx b/lightrag_webui/src/hooks/useLightragGraph.tsx index 24924b6e..d1401cc8 100644 --- a/lightrag_webui/src/hooks/useLightragGraph.tsx +++ b/lightrag_webui/src/hooks/useLightragGraph.tsx @@ -137,15 +137,23 @@ const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => return rawGraph } +// Create a new graph instance with the raw graph data const createSigmaGraph = (rawGraph: RawGraph | null) => { + // Always create a new graph instance const graph = new DirectedGraph() + // Add nodes from raw graph data for (const rawNode of rawGraph?.nodes ?? []) { + // Ensure we have fresh random positions for nodes + seedrandom(rawNode.id + Date.now().toString(), { global: true }) + const x = Math.random() + const y = Math.random() + graph.addNode(rawNode.id, { label: rawNode.labels.join(', '), color: rawNode.color, - x: rawNode.x, - y: rawNode.y, + x: x, + y: y, size: rawNode.size, // for node-border borderColor: Constants.nodeBorderColor, @@ -153,6 +161,7 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => { }) } + // Add edges from raw graph data for (const rawEdge of rawGraph?.edges ?? []) { rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, { label: rawEdge.type || undefined @@ -204,21 +213,19 @@ const useLightrangeGraph = () => { // Track if a fetch is in progress to prevent multiple simultaneous fetches const fetchInProgressRef = useRef(false) - // Data fetching logic - use a separate effect with minimal dependencies to prevent multiple triggers + // Data fetching logic - simplified but preserving TAB visibility check useEffect(() => { // Skip if fetch is already in progress if (fetchInProgressRef.current) { return } - // If there's no query label, reset the graph only if it hasn't been reset already + // If there's no query label, reset the graph if (!queryLabel) { if (rawGraph !== null || sigmaGraph !== null) { const state = useGraphStore.getState() state.reset() - state.setSigmaGraph(new DirectedGraph()) state.setGraphLabels(['*']) - // Reset fetch attempt flags when resetting graph state.setGraphDataFetchAttempted(false) state.setLabelsFetchAttempted(false) } @@ -227,21 +234,21 @@ const useLightrangeGraph = () => { return } - // Check if we've already attempted to fetch this data in this session - const graphDataFetchAttempted = useGraphStore.getState().graphDataFetchAttempted - - // Fetch data if: - // 1. We're not already fetching - // 2. We haven't attempted to fetch in this session OR parameters have changed - if (!isFetching && !fetchInProgressRef.current && (!graphDataFetchAttempted || paramsChanged)) { - // Set flag to prevent multiple fetches + // Check if parameters have changed + if (!isFetching && !fetchInProgressRef.current && + (paramsChanged || !useGraphStore.getState().graphDataFetchAttempted)) { + + // Only fetch data if the Graph tab is visible + if (!isGraphTabVisible) { + console.log('Graph tab not visible, skipping data fetch'); + return; + } + + // Set flags fetchInProgressRef.current = true - // Set global flag to indicate we've attempted to fetch in this session useGraphStore.getState().setGraphDataFetchAttempted(true) const state = useGraphStore.getState() - - // Set rendering control state state.setIsFetching(true) state.setShouldRender(false) // Disable rendering during data loading @@ -256,25 +263,29 @@ const useLightrangeGraph = () => { // Update parameter reference prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree } - console.log('Fetching graph data (once per session unless params change)...') + console.log('Fetching graph data...') - // Use a local copy of the parameters to avoid closure issues + // Use a local copy of the parameters const currentQueryLabel = queryLabel const currentMaxQueryDepth = maxQueryDepth const currentMinDegree = minDegree + // Fetch graph data fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => { const state = useGraphStore.getState() + + // Reset state + state.reset() + + // Create and set new graph directly const newSigmaGraph = createSigmaGraph(data) data?.buildDynamicMap() - // Update all graph data at once to minimize UI flicker - state.clearSelection() - state.setMoveToSelectedNode(false) + // Set new graph data state.setSigmaGraph(newSigmaGraph) state.setRawGraph(data) - // Extract labels from current graph data for local use + // Extract labels from current graph data if (data) { const labelSet = new Set() for (const node of data.nodes) { @@ -290,16 +301,15 @@ const useLightrangeGraph = () => { const sortedLabels = Array.from(labelSet).sort() state.setGraphLabels(['*', ...sortedLabels]) } else { - // Ensure * is there eventhough there is no graph data state.setGraphLabels(['*']) } - // Mark data as loaded and initial load completed + // Update flags dataLoadedRef.current = true initialLoadRef.current = true fetchInProgressRef.current = false - // Reset camera view by triggering FocusOnNode component + // Reset camera view state.setMoveToSelectedNode(true) // Enable rendering if the tab is visible @@ -307,30 +317,42 @@ const useLightrangeGraph = () => { state.setIsFetching(false) }).catch((error) => { console.error('Error fetching graph data:', error) - // Reset fetching state and remove flag in case of error + + // Reset state on error const state = useGraphStore.getState() state.setIsFetching(false) - state.setShouldRender(isGraphTabVisible) // Restore rendering state - dataLoadedRef.current = false // Allow retry + state.setShouldRender(isGraphTabVisible) + dataLoadedRef.current = false fetchInProgressRef.current = false - // Reset global flag to allow retry state.setGraphDataFetchAttempted(false) }) } - }, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph]) // Added missing dependencies + }, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph]) - // Update rendering state when tab visibility changes + // Update rendering state and handle tab visibility changes useEffect(() => { - // Only update rendering state if data is loaded and not fetching - if (rawGraph) { - useGraphStore.getState().setShouldRender(isGraphTabVisible) + // When tab becomes visible + if (isGraphTabVisible) { + // If we have data, enable rendering + if (rawGraph) { + useGraphStore.getState().setShouldRender(true) + } + + // We no longer reset the fetch attempted flag here to prevent continuous API calls + } else { + // When tab becomes invisible, disable rendering + useGraphStore.getState().setShouldRender(false) } }, [isGraphTabVisible, rawGraph]) const lightrageGraph = useCallback(() => { + // If we already have a graph instance, return it if (sigmaGraph) { return sigmaGraph as Graph } + + // If no graph exists yet, create a new one and store it + console.log('Creating new Sigma graph instance') const graph = new DirectedGraph() useGraphStore.getState().setSigmaGraph(graph) return graph as Graph diff --git a/lightrag_webui/src/stores/graph.ts b/lightrag_webui/src/stores/graph.ts index 6c882165..a81048e6 100644 --- a/lightrag_webui/src/stores/graph.ts +++ b/lightrag_webui/src/stores/graph.ts @@ -144,25 +144,38 @@ const useGraphStoreBase = create()((set, get) => ({ selectedEdge: null, focusedEdge: null }), - reset: () => + reset: () => { + // Get the existing graph + const existingGraph = get().sigmaGraph; + + // If we have an existing graph, clear it by removing all nodes + if (existingGraph) { + const nodes = Array.from(existingGraph.nodes()); + nodes.forEach(node => existingGraph.dropNode(node)); + } + set({ selectedNode: null, focusedNode: null, selectedEdge: null, focusedEdge: null, rawGraph: null, - sigmaGraph: null, + // Keep the existing graph instance but with cleared data graphLabels: ['*'], moveToSelectedNode: false, shouldRender: false - }), + }); + }, setRawGraph: (rawGraph: RawGraph | null) => set({ rawGraph }), - setSigmaGraph: (sigmaGraph: DirectedGraph | null) => set({ sigmaGraph }), + setSigmaGraph: (sigmaGraph: DirectedGraph | null) => { + // 直接替换图形实例,不尝试保留WebGL上下文 + set({ sigmaGraph }); + }, setGraphLabels: (labels: string[]) => set({ graphLabels: labels }),