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
});
},