Merge branch 'main' into i18n-france-arabic

This commit is contained in:
yangdx
2025-03-22 00:32:13 +08:00
10 changed files with 342 additions and 270 deletions

View File

@@ -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 (
<div className="flex items-center">
{rawGraph && (
<Button
size="icon"
variant={controlButtonVariant}
onClick={handleRefresh}
tooltip={t('graphPanel.graphLabels.refreshTooltip')}
className="mr-1"
>
<RefreshCw className="h-4 w-4" />
</Button>
)}
{/* Always show refresh button */}
<Button
size="icon"
variant={controlButtonVariant}
onClick={handleRefresh}
tooltip={t('graphPanel.graphLabels.refreshTooltip')}
className="mr-1"
>
<RefreshCw className="h-4 w-4" />
</Button>
<AsyncSelect<string>
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
/>

View File

@@ -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
let rawData: any = null;
try {
rawData = await queryGraphs(label, maxDepth, minDegree)
} catch (e) {
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
return 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<string, number> = {}
@@ -192,6 +226,8 @@ const useLightrangeGraph = () => {
// Use ref to track if data has been loaded and initial load
const dataLoadedRef = useRef(false)
const initialLoadRef = useRef(false)
// Use ref to track if empty data has been handled
const emptyDataHandledRef = useRef(false)
const getNode = useCallback(
(nodeId: string) => {
@@ -224,11 +260,16 @@ const useLightrangeGraph = () => {
// Data fetching logic
useEffect(() => {
// Skip if fetch is already in progress or no query label
if (fetchInProgressRef.current || !queryLabel) {
// Skip if fetch is already in progress
if (fetchInProgressRef.current) {
return
}
// Empty queryLabel should be only handle once(avoid infinite loop)
if (!queryLabel && emptyDataHandledRef.current) {
return;
}
// Only fetch data when graphDataFetchAttempted is false (avoids re-fetching on vite dev mode)
if (!isFetching && !useGraphStore.getState().graphDataFetchAttempted) {
// Set flags
@@ -246,49 +287,106 @@ const useLightrangeGraph = () => {
})
}
console.log('Fetching graph data...')
console.log('Preparing graph data...')
// Use a local copy of the parameters
const currentQueryLabel = queryLabel
const currentMaxQueryDepth = maxQueryDepth
const currentMinDegree = minDegree
// Fetch graph data
fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
// Declare a variable to store data promise
let dataPromise;
// 1. If query label is not empty, use fetchGraph
if (currentQueryLabel) {
dataPromise = fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree);
} else {
// 2. If query label is empty, set data to null
console.log('Query label is empty, show empty graph')
dataPromise = Promise.resolve(null);
}
// 3. Process data
dataPromise.then((data) => {
const state = useGraphStore.getState()
// 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 a graph with a single "Graph Is Empty" node
const emptyGraph = new DirectedGraph();
// Set new graph data
state.setSigmaGraph(newSigmaGraph)
state.setRawGraph(data)
// Add a single node with "Graph Is Empty" label
emptyGraph.addNode('empty-graph-node', {
label: t('graphPanel.emptyGraph'),
color: '#cccccc', // gray color
x: 0.5,
y: 0.5,
size: 15,
borderColor: Constants.nodeBorderColor,
borderSize: 0.2
});
// Set graph to store
state.setSigmaGraph(emptyGraph);
state.setRawGraph(null);
// Still mark graph as empty for other logic
state.setGraphIsEmpty(true);
// Only clear current label if it's not already empty
if (currentQueryLabel) {
useSettingsStore.getState().setQueryLabel('');
}
// Clear last successful query label to ensure labels are fetched next time
state.setLastSuccessfulQueryLabel('');
console.log('Graph data is empty, created graph with empty graph node');
} else {
// Create and set new graph
const newSigmaGraph = createSigmaGraph(data);
data.buildDynamicMap();
// 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)
// Mark empty data as handled if data is empty and query label is empty
if ((!data || !data.nodes || data.nodes.length === 0) && !currentQueryLabel) {
emptyDataHandledRef.current = true;
}
}).catch((error) => {
console.error('Error fetching graph data:', error)
// 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])
}, [queryLabel, maxQueryDepth, minDegree, isFetching, t])
// Handle node expansion
useEffect(() => {

View File

@@ -201,7 +201,8 @@
"placeholder": "Search labels...",
"andOthers": "And {count} others",
"refreshTooltip": "Reload graph data"
}
},
"emptyGraph": "Graph Is Empty"
},
"retrievePanel": {
"chatMessage": {

View File

@@ -198,7 +198,8 @@
"placeholder": "搜索标签...",
"andOthers": "还有 {count} 个",
"refreshTooltip": "重新加载图形数据"
}
},
"emptyGraph": "图谱数据为空"
},
"retrievePanel": {
"chatMessage": {

View File

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