Merge branch 'main' of github.com:lcjqyml/LightRAG

This commit is contained in:
Milin
2025-03-20 14:22:01 +08:00
22 changed files with 394 additions and 379 deletions

3
.gitattributes vendored
View File

@@ -1 +1,2 @@
lightrag/api/webui/** -diff lightrag/api/webui/** binary
lightrag/api/webui/** linguist-generated

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-CSrxfS-k.js"></script> <script type="module" crossorigin src="/webui/assets/index-4I5HV9Fr.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-mPRIIErN.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-BSOt8Nur.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -5,11 +5,9 @@ import MessageAlert from '@/components/MessageAlert'
import ApiKeyAlert from '@/components/ApiKeyAlert' import ApiKeyAlert from '@/components/ApiKeyAlert'
import StatusIndicator from '@/components/graph/StatusIndicator' import StatusIndicator from '@/components/graph/StatusIndicator'
import { healthCheckInterval } from '@/lib/constants' import { healthCheckInterval } from '@/lib/constants'
import { useBackendState, useAuthStore } from '@/stores/state' import { useBackendState } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { navigationService } from '@/services/navigation'
import SiteHeader from '@/features/SiteHeader' import SiteHeader from '@/features/SiteHeader'
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag' import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
@@ -21,22 +19,13 @@ import ApiSite from '@/features/ApiSite'
import { Tabs, TabsContent } from '@/components/ui/Tabs' import { Tabs, TabsContent } from '@/components/ui/Tabs'
function App() { function App() {
const navigate = useNavigate();
const message = useBackendState.use.message() const message = useBackendState.use.message()
// Initialize navigation service
useEffect(() => {
navigationService.setNavigate(navigate);
}, [navigate]);
const enableHealthCheck = useSettingsStore.use.enableHealthCheck() const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
const currentTab = useSettingsStore.use.currentTab() const currentTab = useSettingsStore.use.currentTab()
const [apiKeyInvalid, setApiKeyInvalid] = useState(false) const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
// Health check // Health check
useEffect(() => { useEffect(() => {
const { isAuthenticated } = useAuthStore.getState();
if (!enableHealthCheck || !isAuthenticated) return
// Check immediately // Check immediately
useBackendState.getState().check() useBackendState.getState().check()

View File

@@ -1,6 +1,7 @@
import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom' import { HashRouter as Router, Routes, Route, useNavigate } from 'react-router-dom'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useAuthStore } from '@/stores/state' import { useAuthStore } from '@/stores/state'
import { navigationService } from '@/services/navigation'
import { getAuthStatus } from '@/api/lightrag' import { getAuthStatus } from '@/api/lightrag'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Toaster } from 'sonner' import { Toaster } from 'sonner'
@@ -15,6 +16,12 @@ interface ProtectedRouteProps {
const ProtectedRoute = ({ children }: ProtectedRouteProps) => { const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
const { isAuthenticated } = useAuthStore() const { isAuthenticated } = useAuthStore()
const [isChecking, setIsChecking] = useState(true) const [isChecking, setIsChecking] = useState(true)
const navigate = useNavigate()
// Set navigate function for navigation service
useEffect(() => {
navigationService.setNavigate(navigate)
}, [navigate])
useEffect(() => { useEffect(() => {
let isMounted = true; // Flag to prevent state updates after unmount let isMounted = true; // Flag to prevent state updates after unmount
@@ -60,22 +67,42 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
} }
}, [isAuthenticated]) }, [isAuthenticated])
// Show nothing while checking auth status // Handle navigation when authentication status changes
if (isChecking) { useEffect(() => {
return null if (!isChecking && !isAuthenticated) {
const currentPath = window.location.hash.slice(1); // Remove the '#' from hash
const isLoginPage = currentPath === '/login';
if (!isLoginPage) {
// Use navigation service for redirection
console.log('Not authenticated, redirecting to login');
navigationService.navigateToLogin();
}
}
}, [isChecking, isAuthenticated]);
// Show nothing while checking auth status or when not authenticated on login page
if (isChecking || (!isAuthenticated && window.location.hash.slice(1) === '/login')) {
return null;
} }
// After checking, if still not authenticated, redirect to login // Show children only when authenticated
if (!isAuthenticated) { if (!isAuthenticated) {
return <Navigate to="/login" replace /> return null;
} }
return <>{children}</> return <>{children}</>;
} }
const AppRouter = () => { const AppContent = () => {
const [initializing, setInitializing] = useState(true) const [initializing, setInitializing] = useState(true)
const { isAuthenticated } = useAuthStore() const { isAuthenticated } = useAuthStore()
const navigate = useNavigate()
// Set navigate function for navigation service
useEffect(() => {
navigationService.setNavigate(navigate)
}, [navigate])
// Check token validity and auth configuration on app initialization // Check token validity and auth configuration on app initialization
useEffect(() => { useEffect(() => {
@@ -135,8 +162,6 @@ const AppRouter = () => {
} }
return ( return (
<ThemeProvider>
<Router>
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route <Route
@@ -148,7 +173,15 @@ const AppRouter = () => {
} }
/> />
</Routes> </Routes>
<Toaster position="top-center" /> )
}
const AppRouter = () => {
return (
<ThemeProvider>
<Router>
<AppContent />
<Toaster position="bottom-center" />
</Router> </Router>
</ThemeProvider> </ThemeProvider>
) )

View File

@@ -2,7 +2,6 @@ import axios, { AxiosError } from 'axios'
import { backendBaseUrl } from '@/lib/constants' import { backendBaseUrl } from '@/lib/constants'
import { errorMessage } from '@/lib/utils' import { errorMessage } from '@/lib/utils'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useAuthStore } from '@/stores/state'
import { navigationService } from '@/services/navigation' import { navigationService } from '@/services/navigation'
// Types // Types
@@ -174,13 +173,12 @@ axiosInstance.interceptors.response.use(
(error: AxiosError) => { (error: AxiosError) => {
if (error.response) { if (error.response) {
if (error.response?.status === 401) { if (error.response?.status === 401) {
localStorage.removeItem('LIGHTRAG-API-TOKEN'); // For login API, throw error directly
sessionStorage.clear(); if (error.config?.url?.includes('/login')) {
useAuthStore.getState().logout(); throw error;
}
// Use navigation service to handle redirection // For other APIs, navigate to login page
navigationService.navigateToLogin(); navigationService.navigateToLogin();
// Return a never-resolving promise to prevent further execution // Return a never-resolving promise to prevent further execution
return new Promise(() => {}); return new Promise(() => {});
} }

View File

@@ -32,7 +32,7 @@ export default function AppSettings({ className }: AppSettingsProps) {
return ( return (
<Popover open={opened} onOpenChange={setOpened}> <Popover open={opened} onOpenChange={setOpened}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost" size="icon" className={cn("h-9 w-9", className)}> <Button variant="ghost" size="icon" className={cn('h-9 w-9', className)}>
<PaletteIcon className="h-5 w-5" /> <PaletteIcon className="h-5 w-5" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>

View File

@@ -18,7 +18,7 @@ const GraphLabels = () => {
// Track if a fetch is in progress to prevent multiple simultaneous fetches // Track if a fetch is in progress to prevent multiple simultaneous fetches
const fetchInProgressRef = useRef(false) const fetchInProgressRef = useRef(false)
// Fetch labels once on component mount, using global flag to prevent duplicates // Fetch labels and trigger initial data load
useEffect(() => { useEffect(() => {
// Check if we've already attempted to fetch labels in this session // Check if we've already attempted to fetch labels in this session
const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
@@ -29,8 +29,6 @@ const GraphLabels = () => {
// Set global flag to indicate we've attempted to fetch in this session // Set global flag to indicate we've attempted to fetch in this session
useGraphStore.getState().setLabelsFetchAttempted(true) useGraphStore.getState().setLabelsFetchAttempted(true)
console.log('Fetching graph labels (once per session)...')
useGraphStore.getState().fetchAllDatabaseLabels() useGraphStore.getState().fetchAllDatabaseLabels()
.then(() => { .then(() => {
labelsLoadedRef.current = true labelsLoadedRef.current = true
@@ -45,6 +43,14 @@ const GraphLabels = () => {
} }
}, []) // Empty dependency array ensures this only runs once on mount }, []) // 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
const searchEngine = new MiniSearch({ const searchEngine = new MiniSearch({
@@ -87,13 +93,25 @@ const GraphLabels = () => {
) )
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
const currentLabel = useSettingsStore.getState().queryLabel // Reset labels fetch status to allow fetching labels again
useGraphStore.getState().setLabelsFetchAttempted(false)
// Reset graph data fetch status directly, not depending on allDatabaseLabels changes
useGraphStore.getState().setGraphDataFetchAttempted(false) useGraphStore.getState().setGraphDataFetchAttempted(false)
useGraphStore.getState().reset() // 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) useSettingsStore.getState().setQueryLabel(currentLabel)
}, 0)
})
.catch((error) => {
console.error('Failed to refresh labels:', error)
})
}, []) }, [])
return ( return (
@@ -130,22 +148,13 @@ const GraphLabels = () => {
newLabel = '*' newLabel = '*'
} }
// Reset the fetch attempted flag to force a new data fetch // Handle reselecting the same label
useGraphStore.getState().setGraphDataFetchAttempted(false)
// Clear current graph data to ensure complete reload when label changes
if (newLabel !== currentLabel) {
const graphStore = useGraphStore.getState();
// Reset the all graph objects and status
graphStore.reset();
}
if (newLabel === currentLabel && newLabel !== '*') { if (newLabel === currentLabel && newLabel !== '*') {
// reselect the same itme means qery all newLabel = '*'
useSettingsStore.getState().setQueryLabel('*')
} else {
useSettingsStore.getState().setQueryLabel(newLabel)
} }
// Update the label, which will trigger the useEffect to handle data loading
useSettingsStore.getState().setQueryLabel(newLabel)
}} }}
clearable={false} // Prevent clearing value on reselect clearable={false} // Prevent clearing value on reselect
/> />

View File

@@ -218,20 +218,20 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
className="h-7 w-7 border border-gray-400 hover:bg-gray-200" className="h-7 w-7 border border-gray-400 hover:bg-gray-200 dark:border-gray-600 dark:hover:bg-gray-700"
onClick={handleExpandNode} onClick={handleExpandNode}
tooltip={t('graphPanel.propertiesView.node.expandNode')} tooltip={t('graphPanel.propertiesView.node.expandNode')}
> >
<GitBranchPlus className="h-4 w-4 text-gray-700" /> <GitBranchPlus className="h-4 w-4 text-gray-700 dark:text-gray-300" />
</Button> </Button>
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
className="h-7 w-7 border border-gray-400 hover:bg-gray-200" className="h-7 w-7 border border-gray-400 hover:bg-gray-200 dark:border-gray-600 dark:hover:bg-gray-700"
onClick={handlePruneNode} onClick={handlePruneNode}
tooltip={t('graphPanel.propertiesView.node.pruneNode')} tooltip={t('graphPanel.propertiesView.node.pruneNode')}
> >
<Scissors className="h-4 w-4 text-gray-900" /> <Scissors className="h-4 w-4 text-gray-900 dark:text-gray-300" />
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from 'react' import { useState, useCallback} from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
import Checkbox from '@/components/ui/Checkbox' import Checkbox from '@/components/ui/Checkbox'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
@@ -7,7 +7,6 @@ import Input from '@/components/ui/Input'
import { controlButtonVariant } from '@/lib/constants' import { controlButtonVariant } from '@/lib/constants'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useBackendState } from '@/stores/state'
import { SettingsIcon } from 'lucide-react' import { SettingsIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -113,7 +112,6 @@ const LabeledNumberInput = ({
*/ */
export default function Settings() { export default function Settings() {
const [opened, setOpened] = useState<boolean>(false) const [opened, setOpened] = useState<boolean>(false)
const [tempApiKey, setTempApiKey] = useState<string>('')
const showPropertyPanel = useSettingsStore.use.showPropertyPanel() const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
@@ -127,11 +125,6 @@ export default function Settings() {
const graphLayoutMaxIterations = useSettingsStore.use.graphLayoutMaxIterations() const graphLayoutMaxIterations = useSettingsStore.use.graphLayoutMaxIterations()
const enableHealthCheck = useSettingsStore.use.enableHealthCheck() const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
const apiKey = useSettingsStore.use.apiKey()
useEffect(() => {
setTempApiKey(apiKey || '')
}, [apiKey, opened])
const setEnableNodeDrag = useCallback( const setEnableNodeDrag = useCallback(
() => useSettingsStore.setState((pre) => ({ enableNodeDrag: !pre.enableNodeDrag })), () => useSettingsStore.setState((pre) => ({ enableNodeDrag: !pre.enableNodeDrag })),
@@ -180,11 +173,22 @@ export default function Settings() {
const setGraphQueryMaxDepth = useCallback((depth: number) => { const setGraphQueryMaxDepth = useCallback((depth: number) => {
if (depth < 1) return if (depth < 1) return
useSettingsStore.setState({ graphQueryMaxDepth: depth }) useSettingsStore.setState({ graphQueryMaxDepth: depth })
const currentLabel = useSettingsStore.getState().queryLabel
useSettingsStore.getState().setQueryLabel('')
setTimeout(() => {
useSettingsStore.getState().setQueryLabel(currentLabel)
}, 300)
}, []) }, [])
const setGraphMinDegree = useCallback((degree: number) => { const setGraphMinDegree = useCallback((degree: number) => {
if (degree < 0) return if (degree < 0) return
useSettingsStore.setState({ graphMinDegree: degree }) useSettingsStore.setState({ graphMinDegree: degree })
const currentLabel = useSettingsStore.getState().queryLabel
useSettingsStore.getState().setQueryLabel('')
setTimeout(() => {
useSettingsStore.getState().setQueryLabel(currentLabel)
}, 300)
}, []) }, [])
const setGraphLayoutMaxIterations = useCallback((iterations: number) => { const setGraphLayoutMaxIterations = useCallback((iterations: number) => {
@@ -192,26 +196,19 @@ export default function Settings() {
useSettingsStore.setState({ graphLayoutMaxIterations: iterations }) useSettingsStore.setState({ graphLayoutMaxIterations: iterations })
}, []) }, [])
const setApiKey = useCallback(async () => {
useSettingsStore.setState({ apiKey: tempApiKey || null })
await useBackendState.getState().check()
setOpened(false)
}, [tempApiKey])
const handleTempApiKeyChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setTempApiKey(e.target.value)
},
[setTempApiKey]
)
const { t } = useTranslation(); const { t } = useTranslation();
const saveSettings = () => setOpened(false);
return ( return (
<> <>
<Popover open={opened} onOpenChange={setOpened}> <Popover open={opened} onOpenChange={setOpened}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant={controlButtonVariant} tooltip={t('graphPanel.sideBar.settings.settings')} size="icon"> <Button
variant={controlButtonVariant}
tooltip={t('graphPanel.sideBar.settings.settings')}
size="icon"
>
<SettingsIcon /> <SettingsIcon />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -293,30 +290,15 @@ export default function Settings() {
onEditFinished={setGraphLayoutMaxIterations} onEditFinished={setGraphLayoutMaxIterations}
/> />
<Separator /> <Separator />
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">{t('graphPanel.sideBar.settings.apiKey')}</label>
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
<div className="w-0 flex-1">
<Input
type="password"
value={tempApiKey}
onChange={handleTempApiKeyChange}
placeholder={t('graphPanel.sideBar.settings.enterYourAPIkey')}
className="max-h-full w-full min-w-0"
autoComplete="off"
/>
</div>
<Button <Button
onClick={setApiKey} onClick={saveSettings}
variant="outline" variant="outline"
size="sm" size="sm"
className="max-h-full shrink-0" className="ml-auto px-4"
> >
{t('graphPanel.sideBar.settings.save')} {t('graphPanel.sideBar.settings.save')}
</Button> </Button>
</form>
</div>
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -11,7 +11,7 @@ const SettingsDisplay = () => {
const graphMinDegree = useSettingsStore.use.graphMinDegree() const graphMinDegree = useSettingsStore.use.graphMinDegree()
return ( return (
<div className="absolute bottom-2 left-[calc(2rem+2.5rem)] flex items-center gap-2 text-xs text-gray-400"> <div className="absolute bottom-4 left-[calc(1rem+2.5rem)] flex items-center gap-2 text-xs text-gray-400">
<div>{t('graphPanel.sideBar.settings.depth')}: {graphQueryMaxDepth}</div> <div>{t('graphPanel.sideBar.settings.depth')}: {graphQueryMaxDepth}</div>
<div>{t('graphPanel.sideBar.settings.degree')}: {graphMinDegree}</div> <div>{t('graphPanel.sideBar.settings.degree')}: {graphMinDegree}</div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useTabVisibility } from '@/contexts/useTabVisibility' import { useSettingsStore } from '@/stores/settings'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import { import {
Table, Table,
@@ -27,9 +27,7 @@ export default function DocumentManager() {
const { t } = useTranslation() const { t } = useTranslation()
const health = useBackendState.use.health() const health = useBackendState.use.health()
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null) const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
const { isTabVisible } = useTabVisibility() const currentTab = useSettingsStore.use.currentTab()
const isDocumentsTabVisible = isTabVisible('documents')
const initialLoadRef = useRef(false)
const fetchDocuments = useCallback(async () => { const fetchDocuments = useCallback(async () => {
try { try {
@@ -53,13 +51,12 @@ export default function DocumentManager() {
} }
}, [setDocs, t]) }, [setDocs, t])
// Only fetch documents when the tab becomes visible for the first time // Fetch documents when the tab becomes visible
useEffect(() => { useEffect(() => {
if (isDocumentsTabVisible && !initialLoadRef.current) { if (currentTab === 'documents') {
fetchDocuments() fetchDocuments()
initialLoadRef.current = true
} }
}, [isDocumentsTabVisible, fetchDocuments]) }, [currentTab, fetchDocuments])
const scanDocuments = useCallback(async () => { const scanDocuments = useCallback(async () => {
try { try {
@@ -70,9 +67,9 @@ export default function DocumentManager() {
} }
}, [t]) }, [t])
// Only set up polling when the tab is visible and health is good // Set up polling when the documents tab is active and health is good
useEffect(() => { useEffect(() => {
if (!isDocumentsTabVisible || !health) { if (currentTab !== 'documents' || !health) {
return return
} }
@@ -85,7 +82,7 @@ export default function DocumentManager() {
}, 5000) }, 5000)
return () => clearInterval(interval) return () => clearInterval(interval)
}, [health, fetchDocuments, t, isDocumentsTabVisible]) }, [health, fetchDocuments, t, currentTab])
return ( return (
<Card className="!size-full !rounded-none !border-none"> <Card className="!size-full !rounded-none !border-none">

View File

@@ -1,5 +1,4 @@
import { useEffect, useState, useCallback, useMemo, useRef } from 'react' import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { useTabVisibility } from '@/contexts/useTabVisibility'
// import { MiniMap } from '@react-sigma/minimap' // import { MiniMap } from '@react-sigma/minimap'
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core' import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
import { Settings as SigmaSettings } from 'sigma/settings' import { Settings as SigmaSettings } from 'sigma/settings'
@@ -108,52 +107,39 @@ const GraphEvents = () => {
const GraphViewer = () => { const GraphViewer = () => {
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings) const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
const sigmaRef = useRef<any>(null) const sigmaRef = useRef<any>(null)
const initAttemptedRef = useRef(false)
const selectedNode = useGraphStore.use.selectedNode() const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode() const focusedNode = useGraphStore.use.focusedNode()
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode() const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
const isFetching = useGraphStore.use.isFetching() const isFetching = useGraphStore.use.isFetching()
const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
// Get tab visibility
const { isTabVisible } = useTabVisibility()
const isGraphTabVisible = isTabVisible('knowledge-graph')
const showPropertyPanel = useSettingsStore.use.showPropertyPanel() const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
const enableNodeDrag = useSettingsStore.use.enableNodeDrag() const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
// Handle component mount/unmount and tab visibility
useEffect(() => {
// When component mounts or tab becomes visible
if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) {
// If tab is visible but graph is not rendering, try to enable rendering
useGraphStore.getState().setShouldRender(true)
initAttemptedRef.current = true
console.log('Graph viewer initialized')
}
// Cleanup function when component unmounts
return () => {
// Only log cleanup, don't actually clean up the WebGL context
// This allows the WebGL context to persist across tab switches
console.log('Graph viewer cleanup')
}
}, [isGraphTabVisible, shouldRender, isFetching])
// Initialize sigma settings once on component mount // Initialize sigma settings once on component mount
// All dynamic settings will be updated in GraphControl using useSetSettings // All dynamic settings will be updated in GraphControl using useSetSettings
useEffect(() => { useEffect(() => {
setSigmaSettings(defaultSigmaSettings) setSigmaSettings(defaultSigmaSettings)
console.log('Initialized sigma settings')
}, []) }, [])
// Clean up sigma instance when component unmounts // Clean up sigma instance when component unmounts
useEffect(() => { useEffect(() => {
return () => { return () => {
// Clear the sigma instance when component unmounts // TAB is mount twice in vite dev mode, this is a workaround
const sigma = useGraphStore.getState().sigmaInstance;
if (sigma) {
try {
// Destroy sigmaand clear WebGL context
sigma.kill();
useGraphStore.getState().setSigmaInstance(null); useGraphStore.getState().setSigmaInstance(null);
console.log('Cleared sigma instance on unmount'); console.log('Cleared sigma instance on Graphviewer unmount');
} catch (error) {
console.error('Error cleaning up sigma instance:', error);
}
}
}; };
}, []); }, []);

View File

@@ -4,7 +4,6 @@ import { useAuthStore } from '@/stores/state'
import { loginToServer, getAuthStatus } from '@/api/lightrag' import { loginToServer, getAuthStatus } from '@/api/lightrag'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader } from '@/components/ui/Card' import { Card, CardContent, CardHeader } from '@/components/ui/Card'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
@@ -20,7 +19,11 @@ const LoginPage = () => {
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [checkingAuth, setCheckingAuth] = useState(true) const [checkingAuth, setCheckingAuth] = useState(true)
// Check if authentication is configured useEffect(() => {
console.log('LoginPage mounted')
}, []);
// Check if authentication is configured, skip login if not
useEffect(() => { useEffect(() => {
let isMounted = true; // Flag to prevent state updates after unmount let isMounted = true; // Flag to prevent state updates after unmount
@@ -45,7 +48,7 @@ const LoginPage = () => {
toast.info(status.message) toast.info(status.message)
} }
navigate('/') navigate('/')
return; // Exit early, no need to set checkingAuth to false return // Exit early, no need to set checkingAuth to false
} }
} catch (error) { } catch (error) {
console.error('Failed to check auth configuration:', error) console.error('Failed to check auth configuration:', error)
@@ -93,10 +96,16 @@ const LoginPage = () => {
toast.success(t('login.successMessage')) toast.success(t('login.successMessage'))
} }
// Navigate to home page after successful login
navigate('/') navigate('/')
} catch (error) { } catch (error) {
console.error('Login failed...', error) console.error('Login failed...', error)
toast.error(t('login.errorInvalidCredentials')) toast.error(t('login.errorInvalidCredentials'))
// Clear any existing auth state
useAuthStore.getState().logout()
// Clear local storage
localStorage.removeItem('LIGHTRAG-API-TOKEN')
} finally { } finally {
setLoading(false) setLoading(false)
} }

View File

@@ -6,8 +6,7 @@ import { useSettingsStore } from '@/stores/settings'
import { useAuthStore } from '@/stores/state' import { useAuthStore } from '@/stores/state'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom' import { navigationService } from '@/services/navigation'
import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react' import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
interface NavigationTabProps { interface NavigationTabProps {
@@ -56,12 +55,10 @@ function TabsNavigation() {
export default function SiteHeader() { export default function SiteHeader() {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const { isGuestMode } = useAuthStore()
const { logout, isGuestMode } = useAuthStore()
const handleLogout = () => { const handleLogout = () => {
logout() navigationService.navigateToLogin();
navigate('/login')
} }
return ( return (

View File

@@ -8,7 +8,6 @@ import { toast } from 'sonner'
import { queryGraphs } from '@/api/lightrag' import { queryGraphs } from '@/api/lightrag'
import { useBackendState } from '@/stores/state' import { useBackendState } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useTabVisibility } from '@/contexts/useTabVisibility'
import seedrandom from 'seedrandom' import seedrandom from 'seedrandom'
@@ -190,23 +189,10 @@ const useLightrangeGraph = () => {
const nodeToExpand = useGraphStore.use.nodeToExpand() const nodeToExpand = useGraphStore.use.nodeToExpand()
const nodeToPrune = useGraphStore.use.nodeToPrune() const nodeToPrune = useGraphStore.use.nodeToPrune()
// Get tab visibility
const { isTabVisible } = useTabVisibility()
const isGraphTabVisible = isTabVisible('knowledge-graph')
// Track previous parameters to detect actual changes
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
// 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)
// Check if parameters have changed
const paramsChanged =
prevParamsRef.current.queryLabel !== queryLabel ||
prevParamsRef.current.maxQueryDepth !== maxQueryDepth ||
prevParamsRef.current.minDegree !== minDegree
const getNode = useCallback( const getNode = useCallback(
(nodeId: string) => { (nodeId: string) => {
return rawGraph?.getNode(nodeId) || null return rawGraph?.getNode(nodeId) || null
@@ -224,43 +210,33 @@ const useLightrangeGraph = () => {
// Track if a fetch is in progress to prevent multiple simultaneous fetches // Track if a fetch is in progress to prevent multiple simultaneous fetches
const fetchInProgressRef = useRef(false) const fetchInProgressRef = useRef(false)
// Data fetching logic - simplified but preserving TAB visibility check // Reset graph when query label is cleared
useEffect(() => { useEffect(() => {
// Skip if fetch is already in progress if (!queryLabel && (rawGraph !== null || sigmaGraph !== null)) {
if (fetchInProgressRef.current) {
return
}
// If there's no query label, reset the graph
if (!queryLabel) {
if (rawGraph !== null || sigmaGraph !== null) {
const state = useGraphStore.getState() const state = useGraphStore.getState()
state.reset() state.reset()
state.setGraphDataFetchAttempted(false) state.setGraphDataFetchAttempted(false)
state.setLabelsFetchAttempted(false) state.setLabelsFetchAttempted(false)
}
dataLoadedRef.current = false dataLoadedRef.current = false
initialLoadRef.current = false initialLoadRef.current = false
}
}, [queryLabel, rawGraph, sigmaGraph])
// Data fetching logic
useEffect(() => {
// Skip if fetch is already in progress or no query label
if (fetchInProgressRef.current || !queryLabel) {
return return
} }
// Check if parameters have changed // Only fetch data when graphDataFetchAttempted is false (avoids re-fetching on vite dev mode)
if (!isFetching && !fetchInProgressRef.current && if (!isFetching && !useGraphStore.getState().graphDataFetchAttempted) {
(paramsChanged || !useGraphStore.getState().graphDataFetchAttempted)) {
// Only fetch data if the Graph tab is visible and we haven't attempted a fetch yet
if (!isGraphTabVisible) {
console.log('Graph tab not visible, skipping data fetch');
return;
}
// Set flags // Set flags
fetchInProgressRef.current = true fetchInProgressRef.current = true
useGraphStore.getState().setGraphDataFetchAttempted(true) useGraphStore.getState().setGraphDataFetchAttempted(true)
const state = useGraphStore.getState() const state = useGraphStore.getState()
state.setIsFetching(true) state.setIsFetching(true)
state.setShouldRender(false) // Disable rendering during data loading
// Clear selection and highlighted nodes before fetching new graph // Clear selection and highlighted nodes before fetching new graph
state.clearSelection() state.clearSelection()
@@ -270,9 +246,6 @@ const useLightrangeGraph = () => {
}) })
} }
// Update parameter reference
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree }
console.log('Fetching graph data...') console.log('Fetching graph data...')
// Use a local copy of the parameters // Use a local copy of the parameters
@@ -295,8 +268,6 @@ const useLightrangeGraph = () => {
state.setSigmaGraph(newSigmaGraph) state.setSigmaGraph(newSigmaGraph)
state.setRawGraph(data) state.setRawGraph(data)
// No longer need to extract labels from graph data
// Update flags // Update flags
dataLoadedRef.current = true dataLoadedRef.current = true
initialLoadRef.current = true initialLoadRef.current = true
@@ -305,8 +276,6 @@ const useLightrangeGraph = () => {
// Reset camera view // Reset camera view
state.setMoveToSelectedNode(true) state.setMoveToSelectedNode(true)
// Enable rendering if the tab is visible
state.setShouldRender(isGraphTabVisible)
state.setIsFetching(false) state.setIsFetching(false)
}).catch((error) => { }).catch((error) => {
console.error('Error fetching graph data:', error) console.error('Error fetching graph data:', error)
@@ -314,29 +283,12 @@ const useLightrangeGraph = () => {
// Reset state on error // Reset state on error
const state = useGraphStore.getState() const state = useGraphStore.getState()
state.setIsFetching(false) state.setIsFetching(false)
state.setShouldRender(isGraphTabVisible)
dataLoadedRef.current = false dataLoadedRef.current = false
fetchInProgressRef.current = false fetchInProgressRef.current = false
state.setGraphDataFetchAttempted(false) state.setGraphDataFetchAttempted(false)
}) })
} }
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph]) }, [queryLabel, maxQueryDepth, minDegree, isFetching])
// Update rendering state and handle tab visibility changes
useEffect(() => {
// When tab becomes visible
if (isGraphTabVisible) {
// If we have data, enable rendering
if (rawGraph) {
useGraphStore.getState().setShouldRender(true)
}
// We no longer reset the fetch attempted flag here to prevent continuous API calls
} else {
// When tab becomes invisible, disable rendering
useGraphStore.getState().setShouldRender(false)
}
}, [isGraphTabVisible, rawGraph])
// Handle node expansion // Handle node expansion
useEffect(() => { useEffect(() => {

View File

@@ -99,7 +99,7 @@
"hideUnselectedEdges": "隐藏未选中的边", "hideUnselectedEdges": "隐藏未选中的边",
"edgeEvents": "边事件", "edgeEvents": "边事件",
"maxQueryDepth": "最大查询深度", "maxQueryDepth": "最大查询深度",
"minDegree": "最小数", "minDegree": "最小邻边数",
"maxLayoutIterations": "最大布局迭代次数", "maxLayoutIterations": "最大布局迭代次数",
"depth": "深度", "depth": "深度",
"degree": "邻边", "degree": "邻边",

View File

@@ -1,4 +1,7 @@
import { NavigateFunction } from 'react-router-dom'; import { NavigateFunction } from 'react-router-dom';
import { useAuthStore, useBackendState } from '@/stores/state';
import { useGraphStore } from '@/stores/graph';
import { useSettingsStore } from '@/stores/settings';
class NavigationService { class NavigationService {
private navigate: NavigateFunction | null = null; private navigate: NavigateFunction | null = null;
@@ -7,11 +10,81 @@ class NavigationService {
this.navigate = navigate; this.navigate = navigate;
} }
navigateToLogin() { /**
if (this.navigate) { * Reset all application state to ensure a clean environment.
this.navigate('/login'); * This function should be called when:
* 1. User logs out
* 2. Authentication token expires
* 3. Direct access to login page
*/
resetAllApplicationState() {
console.log('Resetting all application state...');
// Reset graph state
const graphStore = useGraphStore.getState();
const sigma = graphStore.sigmaInstance;
graphStore.reset();
graphStore.setGraphDataFetchAttempted(false);
graphStore.setLabelsFetchAttempted(false);
graphStore.setSigmaInstance(null);
graphStore.setIsFetching(false); // Reset isFetching state to prevent data loading issues
// Reset backend state
useBackendState.getState().clear();
// Reset retrieval history while preserving other user preferences
useSettingsStore.getState().setRetrievalHistory([]);
// Clear authentication state
sessionStorage.clear();
if (sigma) {
sigma.getGraph().clear();
sigma.kill();
useGraphStore.getState().setSigmaInstance(null);
} }
} }
/**
* Handle direct access to login page
* @returns true if it's a direct access, false if navigated from another page
*/
handleDirectLoginAccess() {
const isDirectAccess = !document.referrer;
if (isDirectAccess) {
this.resetAllApplicationState();
}
return isDirectAccess;
}
/**
* Navigate to login page and reset application state
* @param skipReset whether to skip state reset (used for direct access scenario where reset is already handled)
*/
navigateToLogin() {
if (!this.navigate) {
console.error('Navigation function not set');
return;
}
// First navigate to login page
this.navigate('/login');
// Then reset state after navigation
setTimeout(() => {
this.resetAllApplicationState();
useAuthStore.getState().logout();
}, 0);
}
navigateToHome() {
if (!this.navigate) {
console.error('Navigation function not set');
return;
}
this.navigate('/');
}
} }
export const navigationService = new NavigationService(); export const navigationService = new NavigationService();

View File

@@ -74,7 +74,6 @@ interface GraphState {
moveToSelectedNode: boolean moveToSelectedNode: boolean
isFetching: boolean isFetching: boolean
shouldRender: boolean
// Global flags to track data fetching attempts // Global flags to track data fetching attempts
graphDataFetchAttempted: boolean graphDataFetchAttempted: boolean
@@ -95,7 +94,6 @@ interface GraphState {
setAllDatabaseLabels: (labels: string[]) => void setAllDatabaseLabels: (labels: string[]) => void
fetchAllDatabaseLabels: () => Promise<void> fetchAllDatabaseLabels: () => Promise<void>
setIsFetching: (isFetching: boolean) => void setIsFetching: (isFetching: boolean) => void
setShouldRender: (shouldRender: boolean) => void
// 搜索引擎方法 // 搜索引擎方法
setSearchEngine: (engine: MiniSearch | null) => void setSearchEngine: (engine: MiniSearch | null) => void
@@ -122,7 +120,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
moveToSelectedNode: false, moveToSelectedNode: false,
isFetching: false, isFetching: false,
shouldRender: false,
// Initialize global flags // Initialize global flags
graphDataFetchAttempted: false, graphDataFetchAttempted: false,
@@ -137,7 +134,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
setIsFetching: (isFetching: boolean) => set({ isFetching }), setIsFetching: (isFetching: boolean) => set({ isFetching }),
setShouldRender: (shouldRender: boolean) => set({ shouldRender }),
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
set({ selectedNode: nodeId, moveToSelectedNode }), set({ selectedNode: nodeId, moveToSelectedNode }),
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }), setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
@@ -159,8 +155,7 @@ 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
shouldRender: false
}); });
}, },

View File

@@ -18,11 +18,9 @@ interface BackendState {
interface AuthState { interface AuthState {
isAuthenticated: boolean; isAuthenticated: boolean;
showLoginModal: boolean;
isGuestMode: boolean; // Add guest mode flag isGuestMode: boolean; // Add guest mode flag
login: (token: string, isGuest?: boolean) => void; login: (token: string, isGuest?: boolean) => void;
logout: () => void; logout: () => void;
setShowLoginModal: (show: boolean) => void;
} }
const useBackendStateStoreBase = create<BackendState>()((set) => ({ const useBackendStateStoreBase = create<BackendState>()((set) => ({
@@ -104,14 +102,12 @@ export const useAuthStore = create<AuthState>(set => {
return { return {
isAuthenticated: initialState.isAuthenticated, isAuthenticated: initialState.isAuthenticated,
showLoginModal: false,
isGuestMode: initialState.isGuestMode, isGuestMode: initialState.isGuestMode,
login: (token, isGuest = false) => { login: (token, isGuest = false) => {
localStorage.setItem('LIGHTRAG-API-TOKEN', token); localStorage.setItem('LIGHTRAG-API-TOKEN', token);
set({ set({
isAuthenticated: true, isAuthenticated: true,
showLoginModal: false,
isGuestMode: isGuest isGuestMode: isGuest
}); });
}, },
@@ -122,8 +118,6 @@ export const useAuthStore = create<AuthState>(set => {
isAuthenticated: false, isAuthenticated: false,
isGuestMode: false isGuestMode: false
}); });
}, }
setShowLoginModal: (show) => set({ showLoginModal: show })
}; };
}); });