yangdx
commited on
Commit
·
5611aed
1
Parent(s):
1d5d470
Minimized API request between Tab view change
Browse files- lightrag/utils.py +2 -2
- lightrag_webui/src/App.tsx +1 -1
- lightrag_webui/src/components/graph/GraphLabels.tsx +32 -1
- lightrag_webui/src/components/ui/TabContent.tsx +3 -5
- lightrag_webui/src/contexts/TabVisibilityProvider.tsx +17 -2
- lightrag_webui/src/features/ApiSite.tsx +34 -1
- lightrag_webui/src/features/DocumentManager.tsx +18 -7
- lightrag_webui/src/features/GraphViewer.tsx +35 -0
- lightrag_webui/src/hooks/useLightragGraph.tsx +136 -84
- lightrag_webui/src/stores/graph.ts +26 -2
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 |
-
|
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
|
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 |
-
|
29 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
55 |
-
|
|
|
|
|
|
|
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 |
-
//
|
173 |
-
const
|
174 |
-
|
|
|
175 |
// Track previous parameters to detect actual changes
|
176 |
-
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
|
177 |
-
|
178 |
-
//
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
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
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
state.
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
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 |
-
//
|
261 |
-
|
262 |
-
state.
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
}
|
276 |
-
}, [
|
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)
|