Minimized API request between Tab view change

This commit is contained in:
yangdx
2025-03-13 19:50:37 +08:00
parent 6893e3c4e2
commit e30162e50a
10 changed files with 304 additions and 105 deletions

View File

@@ -75,8 +75,8 @@ class LightragPathFilter(logging.Filter):
def __init__(self):
super().__init__()
# Define paths to be filtered
# self.filtered_paths = ["/documents", "/health", "/webui/"]
self.filtered_paths = ["/health", "/webui/"]
self.filtered_paths = ["/documents", "/health", "/webui/"]
# self.filtered_paths = ["/health", "/webui/"]
def filter(self, record):
try:

View File

@@ -22,7 +22,7 @@ import { Tabs, TabsContent } from '@/components/ui/Tabs'
function App() {
const message = useBackendState.use.message()
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
const currentTab = useSettingsStore.use.currentTab()
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
// Health check

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { AsyncSelect } from '@/components/ui/AsyncSelect'
import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph'
@@ -10,6 +10,37 @@ const GraphLabels = () => {
const { t } = useTranslation()
const label = useSettingsStore.use.queryLabel()
const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
const labelsLoadedRef = useRef(false)
// Track if a fetch is in progress to prevent multiple simultaneous fetches
const fetchInProgressRef = useRef(false)
// Fetch labels once on component mount, using global flag to prevent duplicates
useEffect(() => {
// Check if we've already attempted to fetch labels in this session
const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
// Only fetch if we haven't attempted in this session and no fetch is in progress
if (!labelsFetchAttempted && !fetchInProgressRef.current) {
fetchInProgressRef.current = true
// Set global flag to indicate we've attempted to fetch in this session
useGraphStore.getState().setLabelsFetchAttempted(true)
console.log('Fetching graph labels (once per session)...')
useGraphStore.getState().fetchAllDatabaseLabels()
.then(() => {
labelsLoadedRef.current = true
fetchInProgressRef.current = false
})
.catch((error) => {
console.error('Failed to fetch labels:', error)
fetchInProgressRef.current = false
// Reset global flag to allow retry
useGraphStore.getState().setLabelsFetchAttempted(false)
})
}
}, []) // Empty dependency array ensures this only runs once on mount
const getSearchEngine = useCallback(() => {
// Create search engine

View File

@@ -25,12 +25,10 @@ const TabContent: React.FC<TabContentProps> = ({ tabId, children, className = ''
};
}, [tabId, setTabVisibility]);
if (!isVisible) {
return null;
}
// Use CSS to hide content instead of not rendering it
// This prevents components from unmounting when tabs are switched
return (
<div className={className}>
<div className={`${className} ${isVisible ? '' : 'hidden'}`}>
{children}
</div>
);

View File

@@ -1,6 +1,7 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { TabVisibilityContext } from './context';
import { TabVisibilityContextType } from './types';
import { useSettingsStore } from '@/stores/settings';
interface TabVisibilityProviderProps {
children: React.ReactNode;
@@ -11,7 +12,21 @@ interface TabVisibilityProviderProps {
* Manages the visibility state of tabs throughout the application
*/
export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ children }) => {
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>({});
// Get current tab from settings store
const currentTab = useSettingsStore.use.currentTab();
// Initialize visibility state with current tab as visible
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
[currentTab]: true
}));
// Update visibility when current tab changes
useEffect(() => {
setVisibleTabs((prev) => ({
...prev,
[currentTab]: true
}));
}, [currentTab]);
// Create the context value with memoization to prevent unnecessary re-renders
const contextValue = useMemo<TabVisibilityContextType>(

View File

@@ -1,5 +1,38 @@
import { useState, useEffect } from 'react'
import { useTabVisibility } from '@/contexts/useTabVisibility'
import { backendBaseUrl } from '@/lib/constants'
export default function ApiSite() {
return <iframe src={backendBaseUrl + '/docs'} className="size-full" />
const { isTabVisible } = useTabVisibility()
const isApiTabVisible = isTabVisible('api')
const [iframeLoaded, setIframeLoaded] = useState(false)
// Load the iframe once on component mount
useEffect(() => {
if (!iframeLoaded) {
setIframeLoaded(true)
}
}, [iframeLoaded])
// Use CSS to hide content when tab is not visible
return (
<div className={`size-full ${isApiTabVisible ? '' : 'hidden'}`}>
{iframeLoaded ? (
<iframe
src={backendBaseUrl + '/docs'}
className="size-full w-full h-full"
style={{ width: '100%', height: '100%', border: 'none' }}
// Use key to ensure iframe doesn't reload
key="api-docs-iframe"
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-background">
<div className="text-center">
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
<p>Loading API Documentation...</p>
</div>
</div>
)}
</div>
)
}

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useTabVisibility } from '@/contexts/useTabVisibility'
import Button from '@/components/ui/Button'
import {
Table,
@@ -26,6 +27,9 @@ export default function DocumentManager() {
const { t } = useTranslation()
const health = useBackendState.use.health()
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
const { isTabVisible } = useTabVisibility()
const isDocumentsTabVisible = isTabVisible('documents')
const initialLoadRef = useRef(false)
const fetchDocuments = useCallback(async () => {
try {
@@ -50,9 +54,13 @@ export default function DocumentManager() {
}
}, [setDocs, t])
// Only fetch documents when the tab becomes visible for the first time
useEffect(() => {
fetchDocuments()
}, [fetchDocuments, t])
if (isDocumentsTabVisible && !initialLoadRef.current) {
fetchDocuments()
initialLoadRef.current = true
}
}, [isDocumentsTabVisible, fetchDocuments])
const scanDocuments = useCallback(async () => {
try {
@@ -63,19 +71,22 @@ export default function DocumentManager() {
}
}, [t])
// Only set up polling when the tab is visible and health is good
useEffect(() => {
if (!isDocumentsTabVisible || !health) {
return
}
const interval = setInterval(async () => {
if (!health) {
return
}
try {
await fetchDocuments()
} catch (err) {
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
}
}, 5000)
return () => clearInterval(interval)
}, [health, fetchDocuments, t])
}, [health, fetchDocuments, t, isDocumentsTabVisible])
return (
<Card className="!size-full !rounded-none !border-none">

View File

@@ -1,4 +1,5 @@
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { useTabVisibility } from '@/contexts/useTabVisibility'
// import { MiniMap } from '@react-sigma/minimap'
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
import { Settings as SigmaSettings } from 'sigma/settings'
@@ -107,10 +108,17 @@ const GraphEvents = () => {
const GraphViewer = () => {
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
const sigmaRef = useRef<any>(null)
const initAttemptedRef = useRef(false)
const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode()
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
const isFetching = useGraphStore.use.isFetching()
const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
// Get tab visibility
const { isTabVisible } = useTabVisibility()
const isGraphTabVisible = isTabVisible('knowledge-graph')
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
@@ -120,6 +128,15 @@ const GraphViewer = () => {
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
// Ensure rendering is enabled when tab becomes visible
useEffect(() => {
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
}
}, [isGraphTabVisible, shouldRender, isFetching])
useEffect(() => {
setSigmaSettings({
...defaultSigmaSettings,
@@ -148,6 +165,24 @@ const GraphViewer = () => {
[selectedNode]
)
// If we shouldn't render, show loading state or empty state
if (!shouldRender) {
return (
<div className="flex h-full w-full items-center justify-center bg-background">
{isFetching ? (
<div className="text-center">
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
<p>Reloading Graph Data...</p>
</div>
) : (
<div className="text-center text-muted-foreground">
{/* Empty or hint message */}
</div>
)}
</div>
)
}
return (
<SigmaContainer
settings={sigmaSettings}

View File

@@ -6,6 +6,7 @@ import { useGraphStore, RawGraph } from '@/stores/graph'
import { queryGraphs } from '@/api/lightrag'
import { useBackendState } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings'
import { useTabVisibility } from '@/contexts/useTabVisibility'
import seedrandom from 'seedrandom'
@@ -169,25 +170,22 @@ const useLightrangeGraph = () => {
const minDegree = useSettingsStore.use.graphMinDegree()
const isFetching = useGraphStore.use.isFetching()
// Use ref to track fetch status
const fetchStatusRef = useRef<Record<string, boolean>>({});
// Get tab visibility
const { isTabVisible } = useTabVisibility()
const isGraphTabVisible = isTabVisible('knowledge-graph')
// Track previous parameters to detect actual changes
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree });
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
// Reset fetch status only when parameters actually change
useEffect(() => {
const prevParams = prevParamsRef.current;
if (prevParams.queryLabel !== queryLabel ||
prevParams.maxQueryDepth !== maxQueryDepth ||
prevParams.minDegree !== minDegree) {
useGraphStore.getState().setIsFetching(false);
// Reset fetch status for new parameters
fetchStatusRef.current = {};
// Update previous parameters
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree };
}
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
// Use ref to track if data has been loaded and initial load
const dataLoadedRef = useRef(false)
const initialLoadRef = useRef(false)
// Check if parameters have changed
const paramsChanged =
prevParamsRef.current.queryLabel !== queryLabel ||
prevParamsRef.current.maxQueryDepth !== maxQueryDepth ||
prevParamsRef.current.minDegree !== minDegree
const getNode = useCallback(
(nodeId: string) => {
@@ -203,77 +201,131 @@ const useLightrangeGraph = () => {
[rawGraph]
)
// 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
useEffect(() => {
if (queryLabel) {
const fetchKey = `${queryLabel}-${maxQueryDepth}-${minDegree}`;
// Skip if fetch is already in progress
if (fetchInProgressRef.current) {
return
}
// Only fetch if we haven't fetched this combination in the current component lifecycle
if (!isFetching && !fetchStatusRef.current[fetchKey]) {
const state = useGraphStore.getState();
// Clear selection and highlighted nodes before fetching new graph
state.clearSelection();
if (state.sigmaGraph) {
state.sigmaGraph.forEachNode((node) => {
state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false);
});
}
// If there's no query label, reset the graph only if it hasn't been reset already
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)
}
dataLoadedRef.current = false
initialLoadRef.current = false
return
}
state.setIsFetching(true);
fetchStatusRef.current[fetchKey] = true;
fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
const state = useGraphStore.getState()
const newSigmaGraph = createSigmaGraph(data)
data?.buildDynamicMap()
// Check if we've already attempted to fetch this data in this session
const graphDataFetchAttempted = useGraphStore.getState().graphDataFetchAttempted
// Update all graph data at once to minimize UI flicker
state.clearSelection()
state.setMoveToSelectedNode(false)
state.setSigmaGraph(newSigmaGraph)
state.setRawGraph(data)
// 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
fetchInProgressRef.current = true
// Set global flag to indicate we've attempted to fetch in this session
useGraphStore.getState().setGraphDataFetchAttempted(true)
// Extract labels from current graph data
if (data) {
const labelSet = new Set<string>();
for (const node of data.nodes) {
if (node.labels && Array.isArray(node.labels)) {
for (const label of node.labels) {
if (label !== '*') { // filter out label "*"
labelSet.add(label);
}
const state = useGraphStore.getState()
// Set rendering control state
state.setIsFetching(true)
state.setShouldRender(false) // Disable rendering during data loading
// Clear selection and highlighted nodes before fetching new graph
state.clearSelection()
if (state.sigmaGraph) {
state.sigmaGraph.forEachNode((node) => {
state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false)
})
}
// Update parameter reference
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree }
console.log('Fetching graph data (once per session unless params change)...')
// Use a local copy of the parameters to avoid closure issues
const currentQueryLabel = queryLabel
const currentMaxQueryDepth = maxQueryDepth
const currentMinDegree = minDegree
fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
const state = useGraphStore.getState()
const newSigmaGraph = createSigmaGraph(data)
data?.buildDynamicMap()
// Update all graph data at once to minimize UI flicker
state.clearSelection()
state.setMoveToSelectedNode(false)
state.setSigmaGraph(newSigmaGraph)
state.setRawGraph(data)
// Extract labels from current graph data for local use
if (data) {
const labelSet = new Set<string>()
for (const node of data.nodes) {
if (node.labels && Array.isArray(node.labels)) {
for (const label of node.labels) {
if (label !== '*') { // filter out label "*"
labelSet.add(label)
}
}
}
// Put * on top of other labels
const sortedLabels = Array.from(labelSet).sort();
state.setGraphLabels(['*', ...sortedLabels]);
} else {
// Ensure * is there eventhough there is no graph data
state.setGraphLabels(['*']);
}
// Put * on top of other labels
const sortedLabels = Array.from(labelSet).sort()
state.setGraphLabels(['*', ...sortedLabels])
} else {
// Ensure * is there eventhough there is no graph data
state.setGraphLabels(['*'])
}
// Fetch all database labels after graph update
state.fetchAllDatabaseLabels();
if (!data) {
// If data is invalid, remove the fetch flag to allow retry
delete fetchStatusRef.current[fetchKey];
}
// Reset fetching state after all updates are complete
// Reset camera view by triggering FocusOnNode component
state.setMoveToSelectedNode(true);
state.setIsFetching(false);
}).catch(() => {
// Reset fetching state and remove flag in case of error
useGraphStore.getState().setIsFetching(false);
delete fetchStatusRef.current[fetchKey];
})
}
} else {
const state = useGraphStore.getState()
state.reset()
state.setSigmaGraph(new DirectedGraph())
state.setGraphLabels(['*'])
// Mark data as loaded and initial load completed
dataLoadedRef.current = true
initialLoadRef.current = true
fetchInProgressRef.current = false
// Reset camera view by triggering FocusOnNode component
state.setMoveToSelectedNode(true)
// Enable rendering if the tab is visible
state.setShouldRender(isGraphTabVisible)
state.setIsFetching(false)
}).catch((error) => {
console.error('Error fetching graph data:', error)
// Reset fetching state and remove flag in case of error
const state = useGraphStore.getState()
state.setIsFetching(false)
state.setShouldRender(isGraphTabVisible) // Restore rendering state
dataLoadedRef.current = false // Allow retry
fetchInProgressRef.current = false
// Reset global flag to allow retry
state.setGraphDataFetchAttempted(false)
})
}
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph]) // Added missing dependencies
// Update rendering state when tab visibility changes
useEffect(() => {
// Only update rendering state if data is loaded and not fetching
if (rawGraph) {
useGraphStore.getState().setShouldRender(isGraphTabVisible)
}
}, [isGraphTabVisible, rawGraph])
const lightrageGraph = useCallback(() => {
if (sigmaGraph) {

View File

@@ -71,6 +71,11 @@ interface GraphState {
moveToSelectedNode: boolean
isFetching: boolean
shouldRender: boolean
// Global flags to track data fetching attempts
graphDataFetchAttempted: boolean
labelsFetchAttempted: boolean
refreshLayout: () => void
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
@@ -88,6 +93,11 @@ interface GraphState {
setAllDatabaseLabels: (labels: string[]) => void
fetchAllDatabaseLabels: () => Promise<void>
setIsFetching: (isFetching: boolean) => void
setShouldRender: (shouldRender: boolean) => void
// Methods to set global flags
setGraphDataFetchAttempted: (attempted: boolean) => void
setLabelsFetchAttempted: (attempted: boolean) => void
}
const useGraphStoreBase = create<GraphState>()((set, get) => ({
@@ -98,6 +108,11 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
moveToSelectedNode: false,
isFetching: false,
shouldRender: false,
// Initialize global flags
graphDataFetchAttempted: false,
labelsFetchAttempted: false,
rawGraph: null,
sigmaGraph: null,
@@ -116,6 +131,7 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
},
setIsFetching: (isFetching: boolean) => set({ isFetching }),
setShouldRender: (shouldRender: boolean) => set({ shouldRender }),
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
set({ selectedNode: nodeId, moveToSelectedNode }),
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
@@ -137,7 +153,8 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
rawGraph: null,
sigmaGraph: null,
graphLabels: ['*'],
moveToSelectedNode: false
moveToSelectedNode: false,
shouldRender: false
}),
setRawGraph: (rawGraph: RawGraph | null) =>
@@ -153,15 +170,22 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
fetchAllDatabaseLabels: async () => {
try {
console.log('Fetching all database labels...');
const labels = await getGraphLabels();
set({ allDatabaseLabels: ['*', ...labels] });
return;
} catch (error) {
console.error('Failed to fetch all database labels:', error);
set({ allDatabaseLabels: ['*'] });
throw error;
}
},
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),
// Methods to set global flags
setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted })
}))
const useGraphStore = createSelectors(useGraphStoreBase)