From 4e58da35835f0a2bc0a4e7ea933a304e7e21395f Mon Sep 17 00:00:00 2001 From: clssck Date: Sun, 30 Nov 2025 20:15:27 +0100 Subject: [PATCH] =?UTF-8?q?style(lightrag=5Fwebui):=20fix=20indentation,?= =?UTF-8?q?=20color=20palette,=20and=20component=20optimization=20-=20Fix?= =?UTF-8?q?=20inconsistent=20indentation=20in=20App.tsx=20(66=20=E2=86=92?= =?UTF-8?q?=2068=20chars)=20-=20Refactor=20GraphControl=20reducer=20logic:?= =?UTF-8?q?=20cache=20selection/theme=20in=20refs=20to=20prevent=20expensi?= =?UTF-8?q?ve=20re-renders=20on=20every=20hover/selection=20change;=20extr?= =?UTF-8?q?act=20nodeReducer=20and=20edgeReducer=20to=20useCallback=20with?= =?UTF-8?q?=20stable=20dependencies=20-=20Improve=20GraphViewer=20performa?= =?UTF-8?q?nce:=20extract=20FocusSync=20and=20GraphSearchWithSelection=20c?= =?UTF-8?q?omponents=20to=20prevent=20re-renders=20from=20unrelated=20stor?= =?UTF-8?q?e=20updates=20-=20Remove=20unused=20imports=20(X=20icon,=20ZapI?= =?UTF-8?q?con,=20i18n)=20-=20Remove=20unused=20function=20parameter=20(st?= =?UTF-8?q?orageConfig)=20-=20Standardize=20dark=20theme=20colors:=20impro?= =?UTF-8?q?ve=20contrast=20and=20visual=20hierarchy=20(hsl=20values);=20up?= =?UTF-8?q?date=20scrollbar=20colors=20for=20better=20visibility=20-=20Nor?= =?UTF-8?q?malize=20quote=20style:=20double=20quotes=20=E2=86=92=20single?= =?UTF-8?q?=20quotes=20in=20className=20attributes=20-=20Fix=20form=20elem?= =?UTF-8?q?ent=20styling:=20improve=20dark=20mode=20button=20hover=20state?= =?UTF-8?q?s=20(gray-800/900=20=E2=86=92=20gray-700/800,=20red-900=20?= =?UTF-8?q?=E2=86=92=20red-800)=20-=20Optimize=20dropdown=20menu=20colors:?= =?UTF-8?q?=20dark=20mode=20backgrounds=20(gray-900/gray-800)=20-=20Reloca?= =?UTF-8?q?te=20HIDDEN=5FCOLUMNS=20constant=20to=20module=20level=20in=20T?= =?UTF-8?q?ableExplorer=20-=20Optimize=20RowDetailModal:=20move=20entries?= =?UTF-8?q?=20computation=20to=20useMemo=20for=20perf=20-=20Fix=20useLight?= =?UTF-8?q?ragGraph=20dependency=20array:=20add=20missing=20minDegree=20an?= =?UTF-8?q?d=20includeOrphans=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lightrag_webui/src/App.tsx | 2 +- .../src/components/graph/GraphControl.tsx | 319 ++++++++++-------- .../graph/OrphanConnectionDialog.tsx | 2 +- .../src/components/status/StatusIndicator.tsx | 2 +- .../ui/UserPromptInputWithHistory.tsx | 8 +- .../src/features/DocumentManager.tsx | 103 ++---- lightrag_webui/src/features/GraphViewer.tsx | 51 ++- lightrag_webui/src/features/LoginPage.tsx | 4 +- lightrag_webui/src/features/SiteHeader.tsx | 7 +- lightrag_webui/src/features/TableExplorer.tsx | 67 ++-- lightrag_webui/src/hooks/useLightragGraph.tsx | 2 +- lightrag_webui/src/index.css | 62 ++-- 12 files changed, 314 insertions(+), 315 deletions(-) diff --git a/lightrag_webui/src/App.tsx b/lightrag_webui/src/App.tsx index fd16ed0a..70f81c75 100644 --- a/lightrag_webui/src/App.tsx +++ b/lightrag_webui/src/App.tsx @@ -63,7 +63,7 @@ function App() { if (isMountedRef.current) { const status = await useBackendState.getState().check(); if (status && 'status' in status && status.status === 'healthy' && status.configuration) { - useSettingsStore.getState().setStorageConfig(status.configuration); + useSettingsStore.getState().setStorageConfig(status.configuration); } } } catch (error) { diff --git a/lightrag_webui/src/components/graph/GraphControl.tsx b/lightrag_webui/src/components/graph/GraphControl.tsx index bd2e9f41..3c378c71 100644 --- a/lightrag_webui/src/components/graph/GraphControl.tsx +++ b/lightrag_webui/src/components/graph/GraphControl.tsx @@ -2,7 +2,7 @@ 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, useState } from 'react' +import { useEffect, useRef, useState, useCallback } from 'react' // import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph' import { EdgeType, NodeType } from '@/hooks/useLightragGraph' @@ -212,158 +212,191 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) } }, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize]) + // Cache selection state in refs so we don't trigger expensive reducer recreation on hover/selection changes + const selectionRef = useRef({ + selectedNode: null as string | null, + focusedNode: null as string | null, + selectedEdge: null as string | null, + focusedEdge: null as string | null, + hideUnselectedEdges + }) + + const focusedNeighborsRef = useRef<{ key: string | null; neighbors: Set | null }>( + { key: null, neighbors: null } + ) + + useEffect(() => { + selectionRef.current = { + selectedNode, + focusedNode, + selectedEdge, + focusedEdge, + hideUnselectedEdges + } + + // Invalidate neighbor cache when focused node changes + if (focusedNeighborsRef.current.key !== (focusedNode || null)) { + focusedNeighborsRef.current = { key: focusedNode || null, neighbors: null } + } + }, [selectedNode, focusedNode, selectedEdge, focusedEdge, hideUnselectedEdges]) + + // Theme values used inside reducers; kept in refs to avoid re-creating reducer functions + const themeRef = useRef({ + isDarkTheme: false, + labelColor: undefined as string | undefined, + edgeColor: undefined as string | undefined + }) - /** - * When component mount or hovered node change - * => Setting the sigma reducers - */ useEffect(() => { - // Check if dark mode is actually applied (handles both 'dark' theme and 'system' theme when OS is dark) const isDarkTheme = theme === 'dark' || (theme === 'system' && window.document.documentElement.classList.contains('dark')) - const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined - const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined - // Update all dynamic settings directly without recreating the sigma container + themeRef.current = { + isDarkTheme, + labelColor: isDarkTheme ? Constants.labelColorDarkTheme : undefined, + edgeColor: isDarkTheme ? Constants.edgeColorDarkTheme : undefined + } + }, [theme, systemThemeIsDark]) + + // Helper to lazily compute focused node neighbors and reuse across reducer calls + const getFocusedNeighbors = useCallback((graph: AbstractGraph, nodeId: string): Set => { + if (focusedNeighborsRef.current.key === nodeId && focusedNeighborsRef.current.neighbors) { + return focusedNeighborsRef.current.neighbors + } + const neighbors = new Set(graph.neighbors(nodeId)) + focusedNeighborsRef.current = { key: nodeId, neighbors } + return neighbors + }, []) + + const nodeReducer = useCallback((node: string, data: NodeType) => { + const graph = sigma.getGraph() + const { selectedNode, focusedNode, selectedEdge, focusedEdge } = selectionRef.current + const { isDarkTheme, labelColor } = themeRef.current + + if (!graph.hasNode(node)) { + return { ...data, highlighted: false, labelColor } + } + + const newData: NodeType & { labelColor?: string; borderColor?: string; borderSize?: number } = { + ...data, + highlighted: data.highlighted || false, + labelColor + } + + // Hidden connections indicator + const dbDegree = graph.getNodeAttribute(node, 'db_degree') || 0 + const visualDegree = graph.degree(node) + if (dbDegree > visualDegree) { + newData.borderColor = Constants.nodeBorderColorHiddenConnections + newData.borderSize = 1.5 + } + + if (disableHoverEffect) { + return newData + } + + const targetNode = focusedNode || selectedNode + const targetEdge = focusedEdge || selectedEdge + + if (targetNode && graph.hasNode(targetNode)) { + try { + const neighbors = getFocusedNeighbors(graph, targetNode) + if (node === targetNode || neighbors.has(node)) { + newData.highlighted = true + if (node === selectedNode) { + newData.borderColor = Constants.nodeBorderColorSelected + } + } + } catch (error) { + console.error('Error in nodeReducer:', error) + return { ...data, highlighted: false, labelColor } + } + } else if (targetEdge && graph.hasEdge(targetEdge)) { + try { + if (graph.extremities(targetEdge).includes(node)) { + newData.highlighted = true + newData.size = 3 + } + } catch (error) { + console.error('Error accessing edge extremities in nodeReducer:', error) + return { ...data, highlighted: false, labelColor } + } + } + + if (newData.highlighted) { + if (isDarkTheme) { + newData.labelColor = Constants.LabelColorHighlightedDarkTheme + } + } else { + newData.color = Constants.nodeColorDisabled + } + + return newData + }, [sigma, disableHoverEffect, getFocusedNeighbors]) + + const edgeReducer = useCallback((edge: string, data: EdgeType) => { + const graph = sigma.getGraph() + const { selectedNode, focusedNode, selectedEdge, focusedEdge, hideUnselectedEdges } = selectionRef.current + const { isDarkTheme, labelColor, edgeColor } = themeRef.current + + if (!graph.hasEdge(edge)) { + return { ...data, hidden: false, labelColor, color: edgeColor } + } + + const newData = { ...data, hidden: false, labelColor, color: edgeColor } + + if (disableHoverEffect) { + return newData + } + + const targetNode = focusedNode || selectedNode + const edgeHighlightColor = isDarkTheme + ? Constants.edgeColorHighlightedDarkTheme + : Constants.edgeColorHighlightedLightTheme + + if (targetNode && graph.hasNode(targetNode)) { + try { + const includesNode = graph.extremities(edge).includes(targetNode) + if (hideUnselectedEdges && !includesNode) { + newData.hidden = true + } else if (includesNode) { + newData.color = edgeHighlightColor + } + } catch (error) { + console.error('Error in edgeReducer:', error) + return { ...data, hidden: false, labelColor, color: edgeColor } + } + } else { + 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) { + newData.color = edgeHighlightColor + } else if (hideUnselectedEdges) { + newData.hidden = true + } + } + } + + return newData + }, [sigma, disableHoverEffect]) + + /** + * Keep sigma reducers stable; selection/theme changes are read from refs to avoid + * re-registering reducers on every hover and maintain frame budget. + */ + useEffect(() => { setSettings({ - // Update display settings enableEdgeEvents, renderEdgeLabels, renderLabels, - - // 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 - borderSize?: number - } = { ...data, highlighted: data.highlighted || false, labelColor } - - // Check for hidden connections (db_degree > visual_degree) - const dbDegree = graph.getNodeAttribute(node, 'db_degree') || 0 - const visualDegree = graph.degree(node) - if (dbDegree > visualDegree) { - newData.borderColor = Constants.nodeBorderColorHiddenConnections - newData.borderSize = 1.5 - } - - if (!disableHoverEffect) { - newData.highlighted = false - const _focusedNode = focusedNode || selectedNode - const _focusedEdge = focusedEdge || selectedEdge - - 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); - return { ...data, highlighted: false, labelColor } - } - } else if (_focusedEdge && graph.hasEdge(_focusedEdge)) { - 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 - } - - if (newData.highlighted) { - if (isDarkTheme) { - newData.labelColor = Constants.LabelColorHighlightedDarkTheme - } - } else { - newData.color = Constants.nodeColorDisabled - } - } - return newData - }, - - // 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) { - const _focusedNode = focusedNode || selectedNode - // Choose edge highlight color based on theme - const edgeHighlightColor = isDarkTheme - ? Constants.edgeColorHighlightedDarkTheme - : Constants.edgeColorHighlightedLightTheme - - 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 = edgeHighlightColor - } - } - } catch (error) { - console.error('Error in edgeReducer:', error); - return { ...data, hidden: false, labelColor, color: edgeColor } - } - } else { - 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) { - newData.color = edgeHighlightColor - } else if (hideUnselectedEdges) { - newData.hidden = true - } - } - } - } - return newData - } + nodeReducer, + edgeReducer }) - }, [ - selectedNode, - focusedNode, - selectedEdge, - focusedEdge, - setSettings, - sigma, - disableHoverEffect, - theme, - systemThemeIsDark, - hideUnselectedEdges, - enableEdgeEvents, - renderEdgeLabels, - renderLabels - ]) + }, [setSettings, enableEdgeEvents, renderEdgeLabels, renderLabels, nodeReducer, edgeReducer]) return null } diff --git a/lightrag_webui/src/components/graph/OrphanConnectionDialog.tsx b/lightrag_webui/src/components/graph/OrphanConnectionDialog.tsx index ced9c83c..cc6ff366 100644 --- a/lightrag_webui/src/components/graph/OrphanConnectionDialog.tsx +++ b/lightrag_webui/src/components/graph/OrphanConnectionDialog.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' -import { AlignLeft, AlignCenter, AlignRight, Link, Loader2, CheckCircle2, AlertCircle, X, Play, Square } from 'lucide-react' +import { AlignLeft, AlignCenter, AlignRight, Link, Loader2, CheckCircle2, AlertCircle, Play, Square } from 'lucide-react' import { Dialog, diff --git a/lightrag_webui/src/components/status/StatusIndicator.tsx b/lightrag_webui/src/components/status/StatusIndicator.tsx index d1add109..ddc39985 100644 --- a/lightrag_webui/src/components/status/StatusIndicator.tsx +++ b/lightrag_webui/src/components/status/StatusIndicator.tsx @@ -20,7 +20,7 @@ const StatusIndicator = ({ className }: { className?: string }) => { }, [lastCheckTime]) return ( -
+
setDialogOpen(true)} diff --git a/lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx b/lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx index a4e45da8..80ad9a2a 100644 --- a/lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx +++ b/lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx @@ -150,7 +150,7 @@ export default function UserPromptInputWithHistory({