yangdx commited on
Commit
5611aed
·
1 Parent(s): 1d5d470

Minimized API request between Tab view change

Browse files
lightrag/utils.py CHANGED
@@ -75,8 +75,8 @@ class LightragPathFilter(logging.Filter):
75
  def __init__(self):
76
  super().__init__()
77
  # Define paths to be filtered
78
- # self.filtered_paths = ["/documents", "/health", "/webui/"]
79
- self.filtered_paths = ["/health", "/webui/"]
80
 
81
  def filter(self, record):
82
  try:
 
75
  def __init__(self):
76
  super().__init__()
77
  # Define paths to be filtered
78
+ self.filtered_paths = ["/documents", "/health", "/webui/"]
79
+ # self.filtered_paths = ["/health", "/webui/"]
80
 
81
  def filter(self, record):
82
  try:
lightrag_webui/src/App.tsx CHANGED
@@ -22,7 +22,7 @@ import { Tabs, TabsContent } from '@/components/ui/Tabs'
22
  function App() {
23
  const message = useBackendState.use.message()
24
  const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
25
- const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
26
  const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
27
 
28
  // Health check
 
22
  function App() {
23
  const message = useBackendState.use.message()
24
  const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
25
+ const currentTab = useSettingsStore.use.currentTab()
26
  const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
27
 
28
  // Health check
lightrag_webui/src/components/graph/GraphLabels.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback } from 'react'
2
  import { AsyncSelect } from '@/components/ui/AsyncSelect'
3
  import { useSettingsStore } from '@/stores/settings'
4
  import { useGraphStore } from '@/stores/graph'
@@ -10,6 +10,37 @@ const GraphLabels = () => {
10
  const { t } = useTranslation()
11
  const label = useSettingsStore.use.queryLabel()
12
  const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  const getSearchEngine = useCallback(() => {
15
  // Create search engine
 
1
+ import { useCallback, useEffect, useRef } from 'react'
2
  import { AsyncSelect } from '@/components/ui/AsyncSelect'
3
  import { useSettingsStore } from '@/stores/settings'
4
  import { useGraphStore } from '@/stores/graph'
 
10
  const { t } = useTranslation()
11
  const label = useSettingsStore.use.queryLabel()
12
  const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
13
+ const labelsLoadedRef = useRef(false)
14
+
15
+ // Track if a fetch is in progress to prevent multiple simultaneous fetches
16
+ const fetchInProgressRef = useRef(false)
17
+
18
+ // Fetch labels once on component mount, using global flag to prevent duplicates
19
+ useEffect(() => {
20
+ // Check if we've already attempted to fetch labels in this session
21
+ const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
22
+
23
+ // Only fetch if we haven't attempted in this session and no fetch is in progress
24
+ if (!labelsFetchAttempted && !fetchInProgressRef.current) {
25
+ fetchInProgressRef.current = true
26
+ // Set global flag to indicate we've attempted to fetch in this session
27
+ useGraphStore.getState().setLabelsFetchAttempted(true)
28
+
29
+ console.log('Fetching graph labels (once per session)...')
30
+
31
+ useGraphStore.getState().fetchAllDatabaseLabels()
32
+ .then(() => {
33
+ labelsLoadedRef.current = true
34
+ fetchInProgressRef.current = false
35
+ })
36
+ .catch((error) => {
37
+ console.error('Failed to fetch labels:', error)
38
+ fetchInProgressRef.current = false
39
+ // Reset global flag to allow retry
40
+ useGraphStore.getState().setLabelsFetchAttempted(false)
41
+ })
42
+ }
43
+ }, []) // Empty dependency array ensures this only runs once on mount
44
 
45
  const getSearchEngine = useCallback(() => {
46
  // Create search engine
lightrag_webui/src/components/ui/TabContent.tsx CHANGED
@@ -25,12 +25,10 @@ const TabContent: React.FC<TabContentProps> = ({ tabId, children, className = ''
25
  };
26
  }, [tabId, setTabVisibility]);
27
 
28
- if (!isVisible) {
29
- return null;
30
- }
31
-
32
  return (
33
- <div className={className}>
34
  {children}
35
  </div>
36
  );
 
25
  };
26
  }, [tabId, setTabVisibility]);
27
 
28
+ // Use CSS to hide content instead of not rendering it
29
+ // This prevents components from unmounting when tabs are switched
 
 
30
  return (
31
+ <div className={`${className} ${isVisible ? '' : 'hidden'}`}>
32
  {children}
33
  </div>
34
  );
lightrag_webui/src/contexts/TabVisibilityProvider.tsx CHANGED
@@ -1,6 +1,7 @@
1
- import React, { useState, useMemo } from 'react';
2
  import { TabVisibilityContext } from './context';
3
  import { TabVisibilityContextType } from './types';
 
4
 
5
  interface TabVisibilityProviderProps {
6
  children: React.ReactNode;
@@ -11,7 +12,21 @@ interface TabVisibilityProviderProps {
11
  * Manages the visibility state of tabs throughout the application
12
  */
13
  export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ children }) => {
14
- const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>({});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  // Create the context value with memoization to prevent unnecessary re-renders
17
  const contextValue = useMemo<TabVisibilityContextType>(
 
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
  import { TabVisibilityContext } from './context';
3
  import { TabVisibilityContextType } from './types';
4
+ import { useSettingsStore } from '@/stores/settings';
5
 
6
  interface TabVisibilityProviderProps {
7
  children: React.ReactNode;
 
12
  * Manages the visibility state of tabs throughout the application
13
  */
14
  export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ children }) => {
15
+ // Get current tab from settings store
16
+ const currentTab = useSettingsStore.use.currentTab();
17
+
18
+ // Initialize visibility state with current tab as visible
19
+ const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
20
+ [currentTab]: true
21
+ }));
22
+
23
+ // Update visibility when current tab changes
24
+ useEffect(() => {
25
+ setVisibleTabs((prev) => ({
26
+ ...prev,
27
+ [currentTab]: true
28
+ }));
29
+ }, [currentTab]);
30
 
31
  // Create the context value with memoization to prevent unnecessary re-renders
32
  const contextValue = useMemo<TabVisibilityContextType>(
lightrag_webui/src/features/ApiSite.tsx CHANGED
@@ -1,5 +1,38 @@
 
 
1
  import { backendBaseUrl } from '@/lib/constants'
2
 
3
  export default function ApiSite() {
4
- return <iframe src={backendBaseUrl + '/docs'} className="size-full" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  }
 
1
+ import { useState, useEffect } from 'react'
2
+ import { useTabVisibility } from '@/contexts/useTabVisibility'
3
  import { backendBaseUrl } from '@/lib/constants'
4
 
5
  export default function ApiSite() {
6
+ const { isTabVisible } = useTabVisibility()
7
+ const isApiTabVisible = isTabVisible('api')
8
+ const [iframeLoaded, setIframeLoaded] = useState(false)
9
+
10
+ // Load the iframe once on component mount
11
+ useEffect(() => {
12
+ if (!iframeLoaded) {
13
+ setIframeLoaded(true)
14
+ }
15
+ }, [iframeLoaded])
16
+
17
+ // Use CSS to hide content when tab is not visible
18
+ return (
19
+ <div className={`size-full ${isApiTabVisible ? '' : 'hidden'}`}>
20
+ {iframeLoaded ? (
21
+ <iframe
22
+ src={backendBaseUrl + '/docs'}
23
+ className="size-full w-full h-full"
24
+ style={{ width: '100%', height: '100%', border: 'none' }}
25
+ // Use key to ensure iframe doesn't reload
26
+ key="api-docs-iframe"
27
+ />
28
+ ) : (
29
+ <div className="flex h-full w-full items-center justify-center bg-background">
30
+ <div className="text-center">
31
+ <div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
32
+ <p>Loading API Documentation...</p>
33
+ </div>
34
+ </div>
35
+ )}
36
+ </div>
37
+ )
38
  }
lightrag_webui/src/features/DocumentManager.tsx CHANGED
@@ -1,5 +1,6 @@
1
- import { useState, useEffect, useCallback } from 'react'
2
  import { useTranslation } from 'react-i18next'
 
3
  import Button from '@/components/ui/Button'
4
  import {
5
  Table,
@@ -26,6 +27,9 @@ export default function DocumentManager() {
26
  const { t } = useTranslation()
27
  const health = useBackendState.use.health()
28
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
 
 
 
29
 
30
  const fetchDocuments = useCallback(async () => {
31
  try {
@@ -50,9 +54,13 @@ export default function DocumentManager() {
50
  }
51
  }, [setDocs, t])
52
 
 
53
  useEffect(() => {
54
- fetchDocuments()
55
- }, [fetchDocuments, t])
 
 
 
56
 
57
  const scanDocuments = useCallback(async () => {
58
  try {
@@ -63,19 +71,22 @@ export default function DocumentManager() {
63
  }
64
  }, [t])
65
 
 
66
  useEffect(() => {
 
 
 
 
67
  const interval = setInterval(async () => {
68
- if (!health) {
69
- return
70
- }
71
  try {
72
  await fetchDocuments()
73
  } catch (err) {
74
  toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
75
  }
76
  }, 5000)
 
77
  return () => clearInterval(interval)
78
- }, [health, fetchDocuments, t])
79
 
80
  return (
81
  <Card className="!size-full !rounded-none !border-none">
 
1
+ import { useState, useEffect, useCallback, useRef } from 'react'
2
  import { useTranslation } from 'react-i18next'
3
+ import { useTabVisibility } from '@/contexts/useTabVisibility'
4
  import Button from '@/components/ui/Button'
5
  import {
6
  Table,
 
27
  const { t } = useTranslation()
28
  const health = useBackendState.use.health()
29
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
30
+ const { isTabVisible } = useTabVisibility()
31
+ const isDocumentsTabVisible = isTabVisible('documents')
32
+ const initialLoadRef = useRef(false)
33
 
34
  const fetchDocuments = useCallback(async () => {
35
  try {
 
54
  }
55
  }, [setDocs, t])
56
 
57
+ // Only fetch documents when the tab becomes visible for the first time
58
  useEffect(() => {
59
+ if (isDocumentsTabVisible && !initialLoadRef.current) {
60
+ fetchDocuments()
61
+ initialLoadRef.current = true
62
+ }
63
+ }, [isDocumentsTabVisible, fetchDocuments])
64
 
65
  const scanDocuments = useCallback(async () => {
66
  try {
 
71
  }
72
  }, [t])
73
 
74
+ // Only set up polling when the tab is visible and health is good
75
  useEffect(() => {
76
+ if (!isDocumentsTabVisible || !health) {
77
+ return
78
+ }
79
+
80
  const interval = setInterval(async () => {
 
 
 
81
  try {
82
  await fetchDocuments()
83
  } catch (err) {
84
  toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
85
  }
86
  }, 5000)
87
+
88
  return () => clearInterval(interval)
89
+ }, [health, fetchDocuments, t, isDocumentsTabVisible])
90
 
91
  return (
92
  <Card className="!size-full !rounded-none !border-none">
lightrag_webui/src/features/GraphViewer.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
 
2
  // import { MiniMap } from '@react-sigma/minimap'
3
  import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
4
  import { Settings as SigmaSettings } from 'sigma/settings'
@@ -107,10 +108,17 @@ const GraphEvents = () => {
107
  const GraphViewer = () => {
108
  const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
109
  const sigmaRef = useRef<any>(null)
 
110
 
111
  const selectedNode = useGraphStore.use.selectedNode()
112
  const focusedNode = useGraphStore.use.focusedNode()
113
  const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
 
 
 
 
 
 
114
 
115
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
116
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
@@ -120,6 +128,15 @@ const GraphViewer = () => {
120
  const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
121
  const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
122
 
 
 
 
 
 
 
 
 
 
123
  useEffect(() => {
124
  setSigmaSettings({
125
  ...defaultSigmaSettings,
@@ -148,6 +165,24 @@ const GraphViewer = () => {
148
  [selectedNode]
149
  )
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  return (
152
  <SigmaContainer
153
  settings={sigmaSettings}
 
1
  import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
2
+ import { useTabVisibility } from '@/contexts/useTabVisibility'
3
  // import { MiniMap } from '@react-sigma/minimap'
4
  import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
5
  import { Settings as SigmaSettings } from 'sigma/settings'
 
108
  const GraphViewer = () => {
109
  const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
110
  const sigmaRef = useRef<any>(null)
111
+ const initAttemptedRef = useRef(false)
112
 
113
  const selectedNode = useGraphStore.use.selectedNode()
114
  const focusedNode = useGraphStore.use.focusedNode()
115
  const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
116
+ const isFetching = useGraphStore.use.isFetching()
117
+ const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
118
+
119
+ // Get tab visibility
120
+ const { isTabVisible } = useTabVisibility()
121
+ const isGraphTabVisible = isTabVisible('knowledge-graph')
122
 
123
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
124
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
 
128
  const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
129
  const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
130
 
131
+ // Ensure rendering is enabled when tab becomes visible
132
+ useEffect(() => {
133
+ if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) {
134
+ // If tab is visible but graph is not rendering, try to enable rendering
135
+ useGraphStore.getState().setShouldRender(true)
136
+ initAttemptedRef.current = true
137
+ }
138
+ }, [isGraphTabVisible, shouldRender, isFetching])
139
+
140
  useEffect(() => {
141
  setSigmaSettings({
142
  ...defaultSigmaSettings,
 
165
  [selectedNode]
166
  )
167
 
168
+ // If we shouldn't render, show loading state or empty state
169
+ if (!shouldRender) {
170
+ return (
171
+ <div className="flex h-full w-full items-center justify-center bg-background">
172
+ {isFetching ? (
173
+ <div className="text-center">
174
+ <div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
175
+ <p>Reloading Graph Data...</p>
176
+ </div>
177
+ ) : (
178
+ <div className="text-center text-muted-foreground">
179
+ {/* Empty or hint message */}
180
+ </div>
181
+ )}
182
+ </div>
183
+ )
184
+ }
185
+
186
  return (
187
  <SigmaContainer
188
  settings={sigmaSettings}
lightrag_webui/src/hooks/useLightragGraph.tsx CHANGED
@@ -6,6 +6,7 @@ import { useGraphStore, RawGraph } from '@/stores/graph'
6
  import { queryGraphs } from '@/api/lightrag'
7
  import { useBackendState } from '@/stores/state'
8
  import { useSettingsStore } from '@/stores/settings'
 
9
 
10
  import seedrandom from 'seedrandom'
11
 
@@ -168,26 +169,23 @@ const useLightrangeGraph = () => {
168
  const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
169
  const minDegree = useSettingsStore.use.graphMinDegree()
170
  const isFetching = useGraphStore.use.isFetching()
171
-
172
- // Use ref to track fetch status
173
- const fetchStatusRef = useRef<Record<string, boolean>>({});
174
-
 
175
  // Track previous parameters to detect actual changes
176
- const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree });
177
-
178
- // Reset fetch status only when parameters actually change
179
- useEffect(() => {
180
- const prevParams = prevParamsRef.current;
181
- if (prevParams.queryLabel !== queryLabel ||
182
- prevParams.maxQueryDepth !== maxQueryDepth ||
183
- prevParams.minDegree !== minDegree) {
184
- useGraphStore.getState().setIsFetching(false);
185
- // Reset fetch status for new parameters
186
- fetchStatusRef.current = {};
187
- // Update previous parameters
188
- prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree };
189
- }
190
- }, [queryLabel, maxQueryDepth, minDegree, isFetching])
191
 
192
  const getNode = useCallback(
193
  (nodeId: string) => {
@@ -203,77 +201,131 @@ const useLightrangeGraph = () => {
203
  [rawGraph]
204
  )
205
 
 
 
 
 
206
  useEffect(() => {
207
- if (queryLabel) {
208
- const fetchKey = `${queryLabel}-${maxQueryDepth}-${minDegree}`;
209
-
210
- // Only fetch if we haven't fetched this combination in the current component lifecycle
211
- if (!isFetching && !fetchStatusRef.current[fetchKey]) {
212
- const state = useGraphStore.getState();
213
- // Clear selection and highlighted nodes before fetching new graph
214
- state.clearSelection();
215
- if (state.sigmaGraph) {
216
- state.sigmaGraph.forEachNode((node) => {
217
- state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false);
218
- });
219
- }
220
-
221
- state.setIsFetching(true);
222
- fetchStatusRef.current[fetchKey] = true;
223
- fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
224
- const state = useGraphStore.getState()
225
- const newSigmaGraph = createSigmaGraph(data)
226
- data?.buildDynamicMap()
227
-
228
- // Update all graph data at once to minimize UI flicker
229
- state.clearSelection()
230
- state.setMoveToSelectedNode(false)
231
- state.setSigmaGraph(newSigmaGraph)
232
- state.setRawGraph(data)
233
-
234
- // Extract labels from current graph data
235
- if (data) {
236
- const labelSet = new Set<string>();
237
- for (const node of data.nodes) {
238
- if (node.labels && Array.isArray(node.labels)) {
239
- for (const label of node.labels) {
240
- if (label !== '*') { // filter out label "*"
241
- labelSet.add(label);
242
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
  }
245
  }
246
- // Put * on top of other labels
247
- const sortedLabels = Array.from(labelSet).sort();
248
- state.setGraphLabels(['*', ...sortedLabels]);
249
- } else {
250
- // Ensure * is there eventhough there is no graph data
251
- state.setGraphLabels(['*']);
252
- }
253
-
254
- // Fetch all database labels after graph update
255
- state.fetchAllDatabaseLabels();
256
- if (!data) {
257
- // If data is invalid, remove the fetch flag to allow retry
258
- delete fetchStatusRef.current[fetchKey];
259
  }
260
- // Reset fetching state after all updates are complete
261
- // Reset camera view by triggering FocusOnNode component
262
- state.setMoveToSelectedNode(true);
263
- state.setIsFetching(false);
264
- }).catch(() => {
265
- // Reset fetching state and remove flag in case of error
266
- useGraphStore.getState().setIsFetching(false);
267
- delete fetchStatusRef.current[fetchKey];
268
- })
269
- }
270
- } else {
271
- const state = useGraphStore.getState()
272
- state.reset()
273
- state.setSigmaGraph(new DirectedGraph())
274
- state.setGraphLabels(['*'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  }
276
- }, [queryLabel, maxQueryDepth, minDegree, isFetching])
277
 
278
  const lightrageGraph = useCallback(() => {
279
  if (sigmaGraph) {
 
6
  import { queryGraphs } from '@/api/lightrag'
7
  import { useBackendState } from '@/stores/state'
8
  import { useSettingsStore } from '@/stores/settings'
9
+ import { useTabVisibility } from '@/contexts/useTabVisibility'
10
 
11
  import seedrandom from 'seedrandom'
12
 
 
169
  const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
170
  const minDegree = useSettingsStore.use.graphMinDegree()
171
  const isFetching = useGraphStore.use.isFetching()
172
+
173
+ // Get tab visibility
174
+ const { isTabVisible } = useTabVisibility()
175
+ const isGraphTabVisible = isTabVisible('knowledge-graph')
176
+
177
  // Track previous parameters to detect actual changes
178
+ const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
179
+
180
+ // Use ref to track if data has been loaded and initial load
181
+ const dataLoadedRef = useRef(false)
182
+ const initialLoadRef = useRef(false)
183
+
184
+ // Check if parameters have changed
185
+ const paramsChanged =
186
+ prevParamsRef.current.queryLabel !== queryLabel ||
187
+ prevParamsRef.current.maxQueryDepth !== maxQueryDepth ||
188
+ prevParamsRef.current.minDegree !== minDegree
 
 
 
 
189
 
190
  const getNode = useCallback(
191
  (nodeId: string) => {
 
201
  [rawGraph]
202
  )
203
 
204
+ // Track if a fetch is in progress to prevent multiple simultaneous fetches
205
+ const fetchInProgressRef = useRef(false)
206
+
207
+ // Data fetching logic - use a separate effect with minimal dependencies to prevent multiple triggers
208
  useEffect(() => {
209
+ // Skip if fetch is already in progress
210
+ if (fetchInProgressRef.current) {
211
+ return
212
+ }
213
+
214
+ // If there's no query label, reset the graph only if it hasn't been reset already
215
+ if (!queryLabel) {
216
+ if (rawGraph !== null || sigmaGraph !== null) {
217
+ const state = useGraphStore.getState()
218
+ state.reset()
219
+ state.setSigmaGraph(new DirectedGraph())
220
+ state.setGraphLabels(['*'])
221
+ // Reset fetch attempt flags when resetting graph
222
+ state.setGraphDataFetchAttempted(false)
223
+ state.setLabelsFetchAttempted(false)
224
+ }
225
+ dataLoadedRef.current = false
226
+ initialLoadRef.current = false
227
+ return
228
+ }
229
+
230
+ // Check if we've already attempted to fetch this data in this session
231
+ const graphDataFetchAttempted = useGraphStore.getState().graphDataFetchAttempted
232
+
233
+ // Fetch data if:
234
+ // 1. We're not already fetching
235
+ // 2. We haven't attempted to fetch in this session OR parameters have changed
236
+ if (!isFetching && !fetchInProgressRef.current && (!graphDataFetchAttempted || paramsChanged)) {
237
+ // Set flag to prevent multiple fetches
238
+ fetchInProgressRef.current = true
239
+ // Set global flag to indicate we've attempted to fetch in this session
240
+ useGraphStore.getState().setGraphDataFetchAttempted(true)
241
+
242
+ const state = useGraphStore.getState()
243
+
244
+ // Set rendering control state
245
+ state.setIsFetching(true)
246
+ state.setShouldRender(false) // Disable rendering during data loading
247
+
248
+ // Clear selection and highlighted nodes before fetching new graph
249
+ state.clearSelection()
250
+ if (state.sigmaGraph) {
251
+ state.sigmaGraph.forEachNode((node) => {
252
+ state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false)
253
+ })
254
+ }
255
+
256
+ // Update parameter reference
257
+ prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree }
258
+
259
+ console.log('Fetching graph data (once per session unless params change)...')
260
+
261
+ // Use a local copy of the parameters to avoid closure issues
262
+ const currentQueryLabel = queryLabel
263
+ const currentMaxQueryDepth = maxQueryDepth
264
+ const currentMinDegree = minDegree
265
+
266
+ fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
267
+ const state = useGraphStore.getState()
268
+ const newSigmaGraph = createSigmaGraph(data)
269
+ data?.buildDynamicMap()
270
+
271
+ // Update all graph data at once to minimize UI flicker
272
+ state.clearSelection()
273
+ state.setMoveToSelectedNode(false)
274
+ state.setSigmaGraph(newSigmaGraph)
275
+ state.setRawGraph(data)
276
+
277
+ // Extract labels from current graph data for local use
278
+ if (data) {
279
+ const labelSet = new Set<string>()
280
+ for (const node of data.nodes) {
281
+ if (node.labels && Array.isArray(node.labels)) {
282
+ for (const label of node.labels) {
283
+ if (label !== '*') { // filter out label "*"
284
+ labelSet.add(label)
285
  }
286
  }
287
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
289
+ // Put * on top of other labels
290
+ const sortedLabels = Array.from(labelSet).sort()
291
+ state.setGraphLabels(['*', ...sortedLabels])
292
+ } else {
293
+ // Ensure * is there eventhough there is no graph data
294
+ state.setGraphLabels(['*'])
295
+ }
296
+
297
+ // Mark data as loaded and initial load completed
298
+ dataLoadedRef.current = true
299
+ initialLoadRef.current = true
300
+ fetchInProgressRef.current = false
301
+
302
+ // Reset camera view by triggering FocusOnNode component
303
+ state.setMoveToSelectedNode(true)
304
+
305
+ // Enable rendering if the tab is visible
306
+ state.setShouldRender(isGraphTabVisible)
307
+ state.setIsFetching(false)
308
+ }).catch((error) => {
309
+ console.error('Error fetching graph data:', error)
310
+ // Reset fetching state and remove flag in case of error
311
+ const state = useGraphStore.getState()
312
+ state.setIsFetching(false)
313
+ state.setShouldRender(isGraphTabVisible) // Restore rendering state
314
+ dataLoadedRef.current = false // Allow retry
315
+ fetchInProgressRef.current = false
316
+ // Reset global flag to allow retry
317
+ state.setGraphDataFetchAttempted(false)
318
+ })
319
+ }
320
+ }, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph]) // Added missing dependencies
321
+
322
+ // Update rendering state when tab visibility changes
323
+ useEffect(() => {
324
+ // Only update rendering state if data is loaded and not fetching
325
+ if (rawGraph) {
326
+ useGraphStore.getState().setShouldRender(isGraphTabVisible)
327
  }
328
+ }, [isGraphTabVisible, rawGraph])
329
 
330
  const lightrageGraph = useCallback(() => {
331
  if (sigmaGraph) {
lightrag_webui/src/stores/graph.ts CHANGED
@@ -71,6 +71,11 @@ interface GraphState {
71
 
72
  moveToSelectedNode: boolean
73
  isFetching: boolean
 
 
 
 
 
74
 
75
  refreshLayout: () => void
76
  setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
@@ -88,6 +93,11 @@ interface GraphState {
88
  setAllDatabaseLabels: (labels: string[]) => void
89
  fetchAllDatabaseLabels: () => Promise<void>
90
  setIsFetching: (isFetching: boolean) => void
 
 
 
 
 
91
  }
92
 
93
  const useGraphStoreBase = create<GraphState>()((set, get) => ({
@@ -98,6 +108,11 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
98
 
99
  moveToSelectedNode: false,
100
  isFetching: false,
 
 
 
 
 
101
 
102
  rawGraph: null,
103
  sigmaGraph: null,
@@ -116,6 +131,7 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
116
  },
117
 
118
  setIsFetching: (isFetching: boolean) => set({ isFetching }),
 
119
  setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
120
  set({ selectedNode: nodeId, moveToSelectedNode }),
121
  setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
@@ -137,7 +153,8 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
137
  rawGraph: null,
138
  sigmaGraph: null,
139
  graphLabels: ['*'],
140
- moveToSelectedNode: false
 
141
  }),
142
 
143
  setRawGraph: (rawGraph: RawGraph | null) =>
@@ -153,15 +170,22 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
153
 
154
  fetchAllDatabaseLabels: async () => {
155
  try {
 
156
  const labels = await getGraphLabels();
157
  set({ allDatabaseLabels: ['*', ...labels] });
 
158
  } catch (error) {
159
  console.error('Failed to fetch all database labels:', error);
160
  set({ allDatabaseLabels: ['*'] });
 
161
  }
162
  },
163
 
164
- setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
 
 
 
 
165
  }))
166
 
167
  const useGraphStore = createSelectors(useGraphStoreBase)
 
71
 
72
  moveToSelectedNode: boolean
73
  isFetching: boolean
74
+ shouldRender: boolean
75
+
76
+ // Global flags to track data fetching attempts
77
+ graphDataFetchAttempted: boolean
78
+ labelsFetchAttempted: boolean
79
 
80
  refreshLayout: () => void
81
  setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
 
93
  setAllDatabaseLabels: (labels: string[]) => void
94
  fetchAllDatabaseLabels: () => Promise<void>
95
  setIsFetching: (isFetching: boolean) => void
96
+ setShouldRender: (shouldRender: boolean) => void
97
+
98
+ // Methods to set global flags
99
+ setGraphDataFetchAttempted: (attempted: boolean) => void
100
+ setLabelsFetchAttempted: (attempted: boolean) => void
101
  }
102
 
103
  const useGraphStoreBase = create<GraphState>()((set, get) => ({
 
108
 
109
  moveToSelectedNode: false,
110
  isFetching: false,
111
+ shouldRender: false,
112
+
113
+ // Initialize global flags
114
+ graphDataFetchAttempted: false,
115
+ labelsFetchAttempted: false,
116
 
117
  rawGraph: null,
118
  sigmaGraph: null,
 
131
  },
132
 
133
  setIsFetching: (isFetching: boolean) => set({ isFetching }),
134
+ setShouldRender: (shouldRender: boolean) => set({ shouldRender }),
135
  setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
136
  set({ selectedNode: nodeId, moveToSelectedNode }),
137
  setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
 
153
  rawGraph: null,
154
  sigmaGraph: null,
155
  graphLabels: ['*'],
156
+ moveToSelectedNode: false,
157
+ shouldRender: false
158
  }),
159
 
160
  setRawGraph: (rawGraph: RawGraph | null) =>
 
170
 
171
  fetchAllDatabaseLabels: async () => {
172
  try {
173
+ console.log('Fetching all database labels...');
174
  const labels = await getGraphLabels();
175
  set({ allDatabaseLabels: ['*', ...labels] });
176
+ return;
177
  } catch (error) {
178
  console.error('Failed to fetch all database labels:', error);
179
  set({ allDatabaseLabels: ['*'] });
180
+ throw error;
181
  }
182
  },
183
 
184
+ setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),
185
+
186
+ // Methods to set global flags
187
+ setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
188
+ setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted })
189
  }))
190
 
191
  const useGraphStore = createSelectors(useGraphStoreBase)