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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
<link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-4I5HV9Fr.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-BSOt8Nur.css">
<script type="module" crossorigin src="/webui/assets/index-C-CHRwmZ.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-BViPRMGA.css">
</head>
<body>
<div id="root"></div>

View File

@@ -186,7 +186,9 @@ class LightRAG:
embedding_batch_num: int = field(default=int(os.getenv("EMBEDDING_BATCH_NUM", 32)))
"""Batch size for embedding computations."""
embedding_func_max_async: int = field(default=int(os.getenv("EMBEDDING_FUNC_MAX_ASYNC", 16)))
embedding_func_max_async: int = field(
default=int(os.getenv("EMBEDDING_FUNC_MAX_ASYNC", 16))
)
"""Maximum number of concurrent embedding function calls."""
embedding_cache_config: dict[str, Any] = field(

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