diff --git a/lightrag_webui/src/components/graph/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx index 305f63bd..d554fa5b 100644 --- a/lightrag_webui/src/components/graph/GraphLabels.tsx +++ b/lightrag_webui/src/components/graph/GraphLabels.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react' +import { useCallback } from 'react' import { AsyncSelect } from '@/components/ui/AsyncSelect' import { useSettingsStore } from '@/stores/settings' import { useGraphStore } from '@/stores/graph' @@ -12,44 +12,8 @@ const GraphLabels = () => { const { t } = useTranslation() const label = useSettingsStore.use.queryLabel() const allDatabaseLabels = useGraphStore.use.allDatabaseLabels() - const rawGraph = useGraphStore.use.rawGraph() - const labelsLoadedRef = useRef(false) - // Track if a fetch is in progress to prevent multiple simultaneous fetches - const fetchInProgressRef = useRef(false) - - // Fetch labels and trigger initial data load - 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) - - 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 - - // Trigger data load when labels are loaded - useEffect(() => { - if (labelsLoadedRef.current) { - // Reset the fetch attempted flag to force a new data fetch - useGraphStore.getState().setGraphDataFetchAttempted(false) - } - }, [label]) + // Remove initial label fetch effect as it's now handled by fetchGraph based on lastSuccessfulQueryLabel const getSearchEngine = useCallback(() => { // Create search engine @@ -93,40 +57,40 @@ const GraphLabels = () => { ) const handleRefresh = useCallback(() => { - // Reset labels fetch status to allow fetching labels again + // Reset fetch status flags useGraphStore.getState().setLabelsFetchAttempted(false) - - // Reset graph data fetch status directly, not depending on allDatabaseLabels changes useGraphStore.getState().setGraphDataFetchAttempted(false) - - // Fetch all labels again - useGraphStore.getState().fetchAllDatabaseLabels() - .then(() => { - // Trigger a graph data reload by changing the query label back and forth - const currentLabel = useSettingsStore.getState().queryLabel - useSettingsStore.getState().setQueryLabel('') - setTimeout(() => { - useSettingsStore.getState().setQueryLabel(currentLabel) - }, 0) - }) - .catch((error) => { - console.error('Failed to refresh labels:', error) - }) - }, []) + + // Clear last successful query label to ensure labels are fetched + useGraphStore.getState().setLastSuccessfulQueryLabel('') + + // Get current label + const currentLabel = useSettingsStore.getState().queryLabel + + // If current label is empty, use default label '*' + if (!currentLabel) { + useSettingsStore.getState().setQueryLabel('*') + } else { + // Trigger data reload + useSettingsStore.getState().setQueryLabel('') + setTimeout(() => { + useSettingsStore.getState().setQueryLabel(currentLabel) + }, 0) + } + }, []); return (
- {rawGraph && ( - - )} + {/* Always show refresh button */} + className="ml-2" triggerClassName="max-h-8" @@ -141,20 +105,23 @@ const GraphLabels = () => { placeholder={t('graphPanel.graphLabels.placeholder')} value={label !== null ? label : '*'} onChange={(newLabel) => { - const currentLabel = useSettingsStore.getState().queryLabel + const currentLabel = useSettingsStore.getState().queryLabel; // select the last item means query all if (newLabel === '...') { - newLabel = '*' + newLabel = '*'; } // Handle reselecting the same label if (newLabel === currentLabel && newLabel !== '*') { - newLabel = '*' + newLabel = '*'; } - // Update the label, which will trigger the useEffect to handle data loading - useSettingsStore.getState().setQueryLabel(newLabel) + // Reset graphDataFetchAttempted flag to ensure data fetch is triggered + useGraphStore.getState().setGraphDataFetchAttempted(false); + + // Update the label to trigger data loading + useSettingsStore.getState().setQueryLabel(newLabel); }} clearable={false} // Prevent clearing value on reselect /> diff --git a/lightrag_webui/src/hooks/useLightragGraph.tsx b/lightrag_webui/src/hooks/useLightragGraph.tsx index f83d396d..89d3f20a 100644 --- a/lightrag_webui/src/hooks/useLightragGraph.tsx +++ b/lightrag_webui/src/hooks/useLightragGraph.tsx @@ -12,34 +12,52 @@ import { useSettingsStore } from '@/stores/settings' import seedrandom from 'seedrandom' const validateGraph = (graph: RawGraph) => { + // Check if graph exists if (!graph) { - return false - } - if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) { - return false + console.log('Graph validation failed: graph is null'); + return false; } + // Check if nodes and edges are arrays + if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) { + console.log('Graph validation failed: nodes or edges is not an array'); + return false; + } + + // Check if nodes array is empty + if (graph.nodes.length === 0) { + console.log('Graph validation failed: nodes array is empty'); + return false; + } + + // Validate each node for (const node of graph.nodes) { if (!node.id || !node.labels || !node.properties) { - return false + console.log('Graph validation failed: invalid node structure'); + return false; } } + // Validate each edge for (const edge of graph.edges) { if (!edge.id || !edge.source || !edge.target) { - return false + console.log('Graph validation failed: invalid edge structure'); + return false; } } + // Validate edge connections for (const edge of graph.edges) { - const source = graph.getNode(edge.source) - const target = graph.getNode(edge.target) + const source = graph.getNode(edge.source); + const target = graph.getNode(edge.target); if (source == undefined || target == undefined) { - return false + console.log('Graph validation failed: edge references non-existent node'); + return false; } } - return true + console.log('Graph validation passed'); + return true; } export type NodeType = { @@ -53,16 +71,32 @@ export type NodeType = { export type EdgeType = { label: string } const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => { - let rawData: any = null - - try { - rawData = await queryGraphs(label, maxDepth, minDegree) - } catch (e) { - useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!') - return null + let rawData: any = null; + + // Check if we need to fetch all database labels first + const lastSuccessfulQueryLabel = useGraphStore.getState().lastSuccessfulQueryLabel; + if (!lastSuccessfulQueryLabel) { + console.log('Last successful query label is empty, fetching all database labels first...'); + try { + await useGraphStore.getState().fetchAllDatabaseLabels(); + } catch (e) { + console.error('Failed to fetch all database labels:', e); + // Continue with graph fetch even if labels fetch fails + } } - let rawGraph = null + // If label is empty, use default label '*' + const queryLabel = label || '*'; + + try { + console.log(`Fetching graph data with label: ${queryLabel}, maxDepth: ${maxDepth}, minDegree: ${minDegree}`); + rawData = await queryGraphs(queryLabel, maxDepth, minDegree); + } catch (e) { + useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!'); + return null; + } + + let rawGraph = null; if (rawData) { const nodeIdMap: Record = {} @@ -260,22 +294,48 @@ const useLightrangeGraph = () => { // Reset state state.reset() - // Create and set new graph directly - const newSigmaGraph = createSigmaGraph(data) - data?.buildDynamicMap() + // Check if data is empty or invalid + if (!data || !data.nodes || data.nodes.length === 0) { + // Create an empty graph + const emptyGraph = new DirectedGraph(); + + // Set empty graph to store + state.setSigmaGraph(emptyGraph); + state.setRawGraph(null); + + // Show "Graph Is Empty" placeholder + state.setGraphIsEmpty(true); + + // Clear current label + useSettingsStore.getState().setQueryLabel(''); + + // Clear last successful query label to ensure labels are fetched next time + state.setLastSuccessfulQueryLabel(''); + + console.log('Graph data is empty or invalid, set empty graph'); + } else { + // Create and set new graph + const newSigmaGraph = createSigmaGraph(data); + data.buildDynamicMap(); - // Set new graph data - state.setSigmaGraph(newSigmaGraph) - state.setRawGraph(data) + // Set new graph data + state.setSigmaGraph(newSigmaGraph); + state.setRawGraph(data); + state.setGraphIsEmpty(false); + + // Update last successful query label + state.setLastSuccessfulQueryLabel(currentQueryLabel); + + // Reset camera view + state.setMoveToSelectedNode(true); + + console.log('Graph data loaded successfully'); + } // Update flags dataLoadedRef.current = true initialLoadRef.current = true fetchInProgressRef.current = false - - // Reset camera view - state.setMoveToSelectedNode(true) - state.setIsFetching(false) }).catch((error) => { console.error('Error fetching graph data:', error) @@ -283,9 +343,10 @@ const useLightrangeGraph = () => { // Reset state on error const state = useGraphStore.getState() state.setIsFetching(false) - dataLoadedRef.current = false + dataLoadedRef.current = false; fetchInProgressRef.current = false state.setGraphDataFetchAttempted(false) + state.setLastSuccessfulQueryLabel('') // Clear last successful query label on error }) } }, [queryLabel, maxQueryDepth, minDegree, isFetching]) diff --git a/lightrag_webui/src/stores/graph.ts b/lightrag_webui/src/stores/graph.ts index 637a3845..cfcebc1c 100644 --- a/lightrag_webui/src/stores/graph.ts +++ b/lightrag_webui/src/stores/graph.ts @@ -74,6 +74,8 @@ interface GraphState { moveToSelectedNode: boolean isFetching: boolean + graphIsEmpty: boolean + lastSuccessfulQueryLabel: string // Global flags to track data fetching attempts graphDataFetchAttempted: boolean @@ -88,6 +90,8 @@ interface GraphState { reset: () => void setMoveToSelectedNode: (moveToSelectedNode: boolean) => void + setGraphIsEmpty: (isEmpty: boolean) => void + setLastSuccessfulQueryLabel: (label: string) => void setRawGraph: (rawGraph: RawGraph | null) => void setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void @@ -120,6 +124,8 @@ const useGraphStoreBase = create()((set) => ({ moveToSelectedNode: false, isFetching: false, + graphIsEmpty: false, + lastSuccessfulQueryLabel: '', // Initialize as empty to ensure fetchAllDatabaseLabels runs on first query // Initialize global flags graphDataFetchAttempted: false, @@ -132,6 +138,9 @@ const useGraphStoreBase = create()((set) => ({ searchEngine: null, + setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }), + setLastSuccessfulQueryLabel: (label: string) => set({ lastSuccessfulQueryLabel: label }), + setIsFetching: (isFetching: boolean) => set({ isFetching }), setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => @@ -155,7 +164,9 @@ const useGraphStoreBase = create()((set) => ({ rawGraph: null, sigmaGraph: null, // to avoid other components from acccessing graph objects searchEngine: null, - moveToSelectedNode: false + moveToSelectedNode: false, + graphIsEmpty: false + // Do not reset lastSuccessfulQueryLabel here as it's used to track query history }); },