Merge branch 'main' of github.com:lcjqyml/LightRAG
This commit is contained in:
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -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
1
lightrag/api/webui/assets/index-BSOt8Nur.css
generated
Normal file
1
lightrag/api/webui/assets/index-BSOt8Nur.css
generated
Normal file
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-mPRIIErN.css
generated
1
lightrag/api/webui/assets/index-mPRIIErN.css
generated
File diff suppressed because one or more lines are too long
4
lightrag/api/webui/index.html
generated
4
lightrag/api/webui/index.html
generated
@@ -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>
|
||||||
|
@@ -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()
|
||||||
|
|
||||||
|
@@ -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(() => {
|
||||||
@@ -134,21 +161,27 @@ const AppRouter = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route
|
||||||
|
path="/*"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<App />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppRouter = () => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<AppContent />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Toaster position="bottom-center" />
|
||||||
<Route
|
|
||||||
path="/*"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<App />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
<Toaster position="top-center" />
|
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
@@ -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(() => {});
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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()
|
||||||
useSettingsStore.getState().setQueryLabel(currentLabel)
|
.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)
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
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
|
||||||
/>
|
/>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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 />
|
||||||
|
<Button
|
||||||
|
onClick={saveSettings}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="ml-auto px-4"
|
||||||
|
>
|
||||||
|
{t('graphPanel.sideBar.settings.save')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<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
|
|
||||||
onClick={setApiKey}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="max-h-full shrink-0"
|
|
||||||
>
|
|
||||||
{t('graphPanel.sideBar.settings.save')}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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">
|
||||||
|
@@ -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
|
||||||
useGraphStore.getState().setSigmaInstance(null);
|
|
||||||
console.log('Cleared sigma instance on unmount');
|
const sigma = useGraphStore.getState().sigmaInstance;
|
||||||
|
if (sigma) {
|
||||||
|
try {
|
||||||
|
// Destroy sigma,and clear WebGL context
|
||||||
|
sigma.kill();
|
||||||
|
useGraphStore.getState().setSigmaInstance(null);
|
||||||
|
console.log('Cleared sigma instance on Graphviewer unmount');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning up sigma instance:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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 (
|
||||||
|
@@ -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) {
|
const state = useGraphStore.getState()
|
||||||
return
|
state.reset()
|
||||||
}
|
state.setGraphDataFetchAttempted(false)
|
||||||
|
state.setLabelsFetchAttempted(false)
|
||||||
// If there's no query label, reset the graph
|
|
||||||
if (!queryLabel) {
|
|
||||||
if (rawGraph !== null || sigmaGraph !== null) {
|
|
||||||
const state = useGraphStore.getState()
|
|
||||||
state.reset()
|
|
||||||
state.setGraphDataFetchAttempted(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(() => {
|
||||||
|
@@ -99,7 +99,7 @@
|
|||||||
"hideUnselectedEdges": "隐藏未选中的边",
|
"hideUnselectedEdges": "隐藏未选中的边",
|
||||||
"edgeEvents": "边事件",
|
"edgeEvents": "边事件",
|
||||||
"maxQueryDepth": "最大查询深度",
|
"maxQueryDepth": "最大查询深度",
|
||||||
"minDegree": "最小度数",
|
"minDegree": "最小邻边数",
|
||||||
"maxLayoutIterations": "最大布局迭代次数",
|
"maxLayoutIterations": "最大布局迭代次数",
|
||||||
"depth": "深度",
|
"depth": "深度",
|
||||||
"degree": "邻边",
|
"degree": "邻边",
|
||||||
|
@@ -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();
|
||||||
|
@@ -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
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -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 })
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user