From 8af097a8e411f0b6bfd4d7def2305bb894047500 Mon Sep 17 00:00:00 2001 From: yangdx Date: Mon, 22 Sep 2025 03:03:53 +0800 Subject: [PATCH] fix(webui): resolve theme switching crashes and optimize graph rendering - Implement theme switching isolation to prevent component access during transitions by hidding GraphSearch - Fix NotFoundGraphError during theme switching by adding defensive programming - Replace problematic NodeById component with custom safe implementation - Add comprehensive error handling and node/edge existence checks - Optimize sigma settings with memoization to prevent unnecessary re-renders - Remove redundant theme update logic and simplify event listener management --- .../src/components/graph/GraphControl.tsx | 37 ++++++++++++-- .../src/components/graph/GraphSearch.tsx | 48 +++++++++++++++---- lightrag_webui/src/features/GraphViewer.tsx | 39 +++++++++++---- 3 files changed, 101 insertions(+), 23 deletions(-) diff --git a/lightrag_webui/src/components/graph/GraphControl.tsx b/lightrag_webui/src/components/graph/GraphControl.tsx index af7cf250..e4e0a96a 100644 --- a/lightrag_webui/src/components/graph/GraphControl.tsx +++ b/lightrag_webui/src/components/graph/GraphControl.tsx @@ -142,6 +142,15 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) // Register the events registerEvents(events) + + // Cleanup function - basic cleanup without relying on specific APIs + return () => { + try { + console.log('Cleaning up graph event listeners') + } catch (error) { + console.warn('Error cleaning up graph event listeners:', error) + } + } }, [registerEvents, enableEdgeEvents, sigma]) /** @@ -189,6 +198,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) } }, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize]) + /** * When component mount or hovered node change * => Setting the sigma reducers @@ -208,6 +218,13 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) // Node reducer for node appearance nodeReducer: (node, data) => { const graph = sigma.getGraph() + + // Add defensive check for node existence during theme switching + if (!graph.hasNode(node)) { + console.warn(`Node ${node} not found in graph during theme switch, returning default data`) + return { ...data, highlighted: false, labelColor } + } + const newData: NodeType & { labelColor?: string borderColor?: string @@ -228,11 +245,17 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) } } catch (error) { console.error('Error in nodeReducer:', error); + return { ...data, highlighted: false, labelColor } } } else if (_focusedEdge && graph.hasEdge(_focusedEdge)) { - if (graph.extremities(_focusedEdge).includes(node)) { - newData.highlighted = true - newData.size = 3 + try { + if (graph.extremities(_focusedEdge).includes(node)) { + newData.highlighted = true + newData.size = 3 + } + } catch (error) { + console.error('Error accessing edge extremities in nodeReducer:', error); + return { ...data, highlighted: false, labelColor } } } else { return newData @@ -252,6 +275,13 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) // Edge reducer for edge appearance edgeReducer: (edge, data) => { const graph = sigma.getGraph() + + // Add defensive check for edge existence during theme switching + if (!graph.hasEdge(edge)) { + console.warn(`Edge ${edge} not found in graph during theme switch, returning default data`) + return { ...data, hidden: false, labelColor, color: edgeColor } + } + const newData = { ...data, hidden: false, labelColor, color: edgeColor } if (!disableHoverEffect) { @@ -270,6 +300,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) } } catch (error) { console.error('Error in edgeReducer:', error); + return { ...data, hidden: false, labelColor, color: edgeColor } } } else { const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null; diff --git a/lightrag_webui/src/components/graph/GraphSearch.tsx b/lightrag_webui/src/components/graph/GraphSearch.tsx index 40840f59..d181c6b3 100644 --- a/lightrag_webui/src/components/graph/GraphSearch.tsx +++ b/lightrag_webui/src/components/graph/GraphSearch.tsx @@ -1,7 +1,6 @@ import { FC, useCallback, useEffect } from 'react' import { EdgeById, - NodeById, GraphSearchInputProps, GraphSearchContextProviderProps } from '@react-sigma/graph-search' @@ -23,10 +22,31 @@ export interface OptionItem { const NodeOption = ({ id }: { id: string }) => { const graph = useGraphStore.use.sigmaGraph() + + // Early return if no graph or node doesn't exist if (!graph?.hasNode(id)) { return null } - return + + // Safely get node attributes with fallbacks + const label = graph.getNodeAttribute(id, 'label') || id + const color = graph.getNodeAttribute(id, 'color') || '#666' + const size = graph.getNodeAttribute(id, 'size') || 4 + + // Custom node display component that doesn't rely on @react-sigma/graph-search + return ( +
+
+ {label} +
+ ) } function OptionComponent(item: OptionItem) { @@ -83,12 +103,17 @@ export const GraphSearchInput = ({ } }) - // Add nodes to search engine - const documents = graph.nodes().map((id: string) => ({ - id: id, - label: graph.getNodeAttribute(id, 'label') - })) - newSearchEngine.addAll(documents) + // Add nodes to search engine with safety checks + const documents = graph.nodes() + .filter(id => graph.hasNode(id)) // Ensure node exists before accessing attributes + .map((id: string) => ({ + id: id, + label: graph.getNodeAttribute(id, 'label') + })) + + if (documents.length > 0) { + newSearchEngine.addAll(documents) + } // Update search engine in store useGraphStore.getState().setSearchEngine(newSearchEngine) @@ -136,13 +161,16 @@ export const GraphSearchInput = ({ // Get already matched IDs to avoid duplicates const matchedIds = new Set(result.map(item => item.id)) - // Perform middle-content matching on all nodes + // Perform middle-content matching on all nodes with safety checks const middleMatchResults = graph.nodes() .filter(id => { // Skip already matched nodes if (matchedIds.has(id)) return false - // Get node label + // Ensure node exists before accessing attributes + if (!graph.hasNode(id)) return false + + // Get node label safely const label = graph.getNodeAttribute(id, 'label') // Match if label contains query string but doesn't start with it return label && diff --git a/lightrag_webui/src/features/GraphViewer.tsx b/lightrag_webui/src/features/GraphViewer.tsx index 3f1839f8..bc62bc31 100644 --- a/lightrag_webui/src/features/GraphViewer.tsx +++ b/lightrag_webui/src/features/GraphViewer.tsx @@ -108,8 +108,9 @@ const GraphEvents = () => { } const GraphViewer = () => { - const [sigmaSettings, setSigmaSettings] = useState>({}) + const [isThemeSwitching, setIsThemeSwitching] = useState(false) const sigmaRef = useRef(null) + const prevTheme = useRef('') const selectedNode = useGraphStore.use.selectedNode() const focusedNode = useGraphStore.use.focusedNode() @@ -122,11 +123,29 @@ const GraphViewer = () => { const showLegend = useSettingsStore.use.showLegend() const theme = useSettingsStore.use.theme() - // Initialize sigma settings based on theme - useEffect(() => { + // Memoize sigma settings to prevent unnecessary re-creation + const memoizedSigmaSettings = useMemo(() => { const isDarkTheme = theme === 'dark' - const settings = createSigmaSettings(isDarkTheme) - setSigmaSettings(settings) + return createSigmaSettings(isDarkTheme) + }, [theme]) + + // Initialize sigma settings based on theme with theme switching protection + useEffect(() => { + // Detect theme change + const isThemeChange = prevTheme.current && prevTheme.current !== theme + if (isThemeChange) { + setIsThemeSwitching(true) + console.log('Theme switching detected:', prevTheme.current, '->', theme) + + // Reset theme switching state after a short delay + const timer = setTimeout(() => { + setIsThemeSwitching(false) + console.log('Theme switching completed') + }, 150) + + return () => clearTimeout(timer) + } + prevTheme.current = theme console.log('Initialized sigma settings for theme:', theme) }, [theme]) @@ -176,7 +195,7 @@ const GraphViewer = () => { return (
@@ -188,7 +207,7 @@ const GraphViewer = () => {
- {showNodeSearchBar && ( + {showNodeSearchBar && !isThemeSwitching && ( { - {/* Loading overlay - shown when data is loading */} - {isFetching && ( + {/* Loading overlay - shown when data is loading or theme is switching */} + {(isFetching || isThemeSwitching) && (
-

Loading Graph Data...

+

{isThemeSwitching ? 'Switching Theme...' : 'Loading Graph Data...'}

)}