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" /> <link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-4I5HV9Fr.js"></script> <script type="module" crossorigin src="/webui/assets/index-C-CHRwmZ.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-BSOt8Nur.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-BViPRMGA.css">
</head> </head>
<body> <body>
<div id="root"></div> <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))) embedding_batch_num: int = field(default=int(os.getenv("EMBEDDING_BATCH_NUM", 32)))
"""Batch size for embedding computations.""" """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.""" """Maximum number of concurrent embedding function calls."""
embedding_cache_config: dict[str, Any] = field( 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 { AsyncSelect } from '@/components/ui/AsyncSelect'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph' import { useGraphStore } from '@/stores/graph'
@@ -12,44 +12,8 @@ const GraphLabels = () => {
const { t } = useTranslation() const { t } = useTranslation()
const label = useSettingsStore.use.queryLabel() const label = useSettingsStore.use.queryLabel()
const allDatabaseLabels = useGraphStore.use.allDatabaseLabels() 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 // Remove initial label fetch effect as it's now handled by fetchGraph based on lastSuccessfulQueryLabel
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])
const getSearchEngine = useCallback(() => { const getSearchEngine = useCallback(() => {
// Create search engine // Create search engine
@@ -93,30 +57,31 @@ const GraphLabels = () => {
) )
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
// Reset labels fetch status to allow fetching labels again // Reset fetch status flags
useGraphStore.getState().setLabelsFetchAttempted(false) useGraphStore.getState().setLabelsFetchAttempted(false)
// Reset graph data fetch status directly, not depending on allDatabaseLabels changes
useGraphStore.getState().setGraphDataFetchAttempted(false) useGraphStore.getState().setGraphDataFetchAttempted(false)
// Fetch all labels again // Clear last successful query label to ensure labels are fetched
useGraphStore.getState().fetchAllDatabaseLabels() useGraphStore.getState().setLastSuccessfulQueryLabel('')
.then(() => {
// Trigger a graph data reload by changing the query label back and forth // Get current label
const currentLabel = useSettingsStore.getState().queryLabel 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('') useSettingsStore.getState().setQueryLabel('')
setTimeout(() => { setTimeout(() => {
useSettingsStore.getState().setQueryLabel(currentLabel) useSettingsStore.getState().setQueryLabel(currentLabel)
}, 0) }, 0)
}) }
.catch((error) => { }, []);
console.error('Failed to refresh labels:', error)
})
}, [])
return ( return (
<div className="flex items-center"> <div className="flex items-center">
{rawGraph && ( {/* Always show refresh button */}
<Button <Button
size="icon" size="icon"
variant={controlButtonVariant} variant={controlButtonVariant}
@@ -126,7 +91,6 @@ const GraphLabels = () => {
> >
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-4 w-4" />
</Button> </Button>
)}
<AsyncSelect<string> <AsyncSelect<string>
className="ml-2" className="ml-2"
triggerClassName="max-h-8" triggerClassName="max-h-8"
@@ -141,20 +105,23 @@ const GraphLabels = () => {
placeholder={t('graphPanel.graphLabels.placeholder')} placeholder={t('graphPanel.graphLabels.placeholder')}
value={label !== null ? label : '*'} value={label !== null ? label : '*'}
onChange={(newLabel) => { onChange={(newLabel) => {
const currentLabel = useSettingsStore.getState().queryLabel const currentLabel = useSettingsStore.getState().queryLabel;
// select the last item means query all // select the last item means query all
if (newLabel === '...') { if (newLabel === '...') {
newLabel = '*' newLabel = '*';
} }
// Handle reselecting the same label // Handle reselecting the same label
if (newLabel === currentLabel && newLabel !== '*') { if (newLabel === currentLabel && newLabel !== '*') {
newLabel = '*' newLabel = '*';
} }
// Update the label, which will trigger the useEffect to handle data loading // Reset graphDataFetchAttempted flag to ensure data fetch is triggered
useSettingsStore.getState().setQueryLabel(newLabel) useGraphStore.getState().setGraphDataFetchAttempted(false);
// Update the label to trigger data loading
useSettingsStore.getState().setQueryLabel(newLabel);
}} }}
clearable={false} // Prevent clearing value on reselect clearable={false} // Prevent clearing value on reselect
/> />

View File

@@ -12,34 +12,52 @@ import { useSettingsStore } from '@/stores/settings'
import seedrandom from 'seedrandom' import seedrandom from 'seedrandom'
const validateGraph = (graph: RawGraph) => { const validateGraph = (graph: RawGraph) => {
// Check if graph exists
if (!graph) { if (!graph) {
return false console.log('Graph validation failed: graph is null');
} return false;
if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
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) { for (const node of graph.nodes) {
if (!node.id || !node.labels || !node.properties) { 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) { for (const edge of graph.edges) {
if (!edge.id || !edge.source || !edge.target) { 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) { for (const edge of graph.edges) {
const source = graph.getNode(edge.source) const source = graph.getNode(edge.source);
const target = graph.getNode(edge.target) const target = graph.getNode(edge.target);
if (source == undefined || target == undefined) { 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 = { export type NodeType = {
@@ -53,16 +71,32 @@ export type NodeType = {
export type EdgeType = { label: string } export type EdgeType = { label: string }
const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => { const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => {
let rawData: any = 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 { try {
rawData = await queryGraphs(label, maxDepth, minDegree) await useGraphStore.getState().fetchAllDatabaseLabels();
} catch (e) { } catch (e) {
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!') console.error('Failed to fetch all database labels:', e);
return null // 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) { if (rawData) {
const nodeIdMap: Record<string, number> = {} const nodeIdMap: Record<string, number> = {}
@@ -192,6 +226,8 @@ const useLightrangeGraph = () => {
// Use ref to track if data has been loaded and initial load // Use ref to track if data has been loaded and initial load
const dataLoadedRef = useRef(false) const dataLoadedRef = useRef(false)
const initialLoadRef = useRef(false) const initialLoadRef = useRef(false)
// Use ref to track if empty data has been handled
const emptyDataHandledRef = useRef(false)
const getNode = useCallback( const getNode = useCallback(
(nodeId: string) => { (nodeId: string) => {
@@ -224,11 +260,16 @@ const useLightrangeGraph = () => {
// Data fetching logic // Data fetching logic
useEffect(() => { useEffect(() => {
// Skip if fetch is already in progress or no query label // Skip if fetch is already in progress
if (fetchInProgressRef.current || !queryLabel) { if (fetchInProgressRef.current) {
return 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) // Only fetch data when graphDataFetchAttempted is false (avoids re-fetching on vite dev mode)
if (!isFetching && !useGraphStore.getState().graphDataFetchAttempted) { if (!isFetching && !useGraphStore.getState().graphDataFetchAttempted) {
// Set flags // 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 // Use a local copy of the parameters
const currentQueryLabel = queryLabel const currentQueryLabel = queryLabel
const currentMaxQueryDepth = maxQueryDepth const currentMaxQueryDepth = maxQueryDepth
const currentMinDegree = minDegree const currentMinDegree = minDegree
// Fetch graph data // Declare a variable to store data promise
fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => { 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() const state = useGraphStore.getState()
// Reset state // Reset state
state.reset() state.reset()
// Create and set new graph directly // Check if data is empty or invalid
const newSigmaGraph = createSigmaGraph(data) if (!data || !data.nodes || data.nodes.length === 0) {
data?.buildDynamicMap() // Create a graph with a single "Graph Is Empty" node
const emptyGraph = new DirectedGraph();
// 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 // Set new graph data
state.setSigmaGraph(newSigmaGraph) state.setSigmaGraph(newSigmaGraph);
state.setRawGraph(data) 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 // Update flags
dataLoadedRef.current = true dataLoadedRef.current = true
initialLoadRef.current = true initialLoadRef.current = true
fetchInProgressRef.current = false fetchInProgressRef.current = false
// Reset camera view
state.setMoveToSelectedNode(true)
state.setIsFetching(false) 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) => { }).catch((error) => {
console.error('Error fetching graph data:', error) console.error('Error fetching graph data:', error)
// Reset state on error // Reset state on error
const state = useGraphStore.getState() const state = useGraphStore.getState()
state.setIsFetching(false) state.setIsFetching(false)
dataLoadedRef.current = false dataLoadedRef.current = false;
fetchInProgressRef.current = false fetchInProgressRef.current = false
state.setGraphDataFetchAttempted(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 // Handle node expansion
useEffect(() => { useEffect(() => {

View File

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

View File

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

View File

@@ -74,6 +74,8 @@ interface GraphState {
moveToSelectedNode: boolean moveToSelectedNode: boolean
isFetching: boolean isFetching: boolean
graphIsEmpty: boolean
lastSuccessfulQueryLabel: string
// Global flags to track data fetching attempts // Global flags to track data fetching attempts
graphDataFetchAttempted: boolean graphDataFetchAttempted: boolean
@@ -88,6 +90,8 @@ interface GraphState {
reset: () => void reset: () => void
setMoveToSelectedNode: (moveToSelectedNode: boolean) => void setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
setGraphIsEmpty: (isEmpty: boolean) => void
setLastSuccessfulQueryLabel: (label: string) => void
setRawGraph: (rawGraph: RawGraph | null) => void setRawGraph: (rawGraph: RawGraph | null) => void
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
@@ -120,6 +124,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
moveToSelectedNode: false, moveToSelectedNode: false,
isFetching: false, isFetching: false,
graphIsEmpty: false,
lastSuccessfulQueryLabel: '', // Initialize as empty to ensure fetchAllDatabaseLabels runs on first query
// Initialize global flags // Initialize global flags
graphDataFetchAttempted: false, graphDataFetchAttempted: false,
@@ -132,6 +138,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
searchEngine: null, searchEngine: null,
setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
setLastSuccessfulQueryLabel: (label: string) => set({ lastSuccessfulQueryLabel: label }),
setIsFetching: (isFetching: boolean) => set({ isFetching }), setIsFetching: (isFetching: boolean) => set({ isFetching }),
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
@@ -155,7 +164,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
rawGraph: null, rawGraph: null,
sigmaGraph: null, // to avoid other components from acccessing graph objects sigmaGraph: null, // to avoid other components from acccessing graph objects
searchEngine: null, searchEngine: null,
moveToSelectedNode: false moveToSelectedNode: false,
graphIsEmpty: false
// Do not reset lastSuccessfulQueryLabel here as it's used to track query history
}); });
}, },