Merge pull request #1064 from danielaskdd/improve-property-tooltip
Browse files- lightrag/api/lightrag_server.py +13 -1
- lightrag/api/webui/assets/index-BV5s8k-a.css +0 -0
- lightrag/api/webui/assets/index-CH-3l4_Z.css +0 -0
- lightrag/api/webui/assets/{index-BlVvSIic.js → index-DwcJE583.js} +0 -0
- lightrag/api/webui/index.html +0 -0
- lightrag_webui/bun.lock +3 -0
- lightrag_webui/index.html +3 -0
- lightrag_webui/package.json +1 -0
- lightrag_webui/src/App.tsx +31 -28
- lightrag_webui/src/components/AppSettings.tsx +66 -0
- lightrag_webui/src/components/Root.tsx +24 -0
- lightrag_webui/src/components/ThemeProvider.tsx +16 -14
- lightrag_webui/src/components/graph/FocusOnNode.tsx +13 -4
- lightrag_webui/src/components/graph/GraphControl.tsx +51 -22
- lightrag_webui/src/components/graph/GraphLabels.tsx +69 -37
- lightrag_webui/src/components/graph/GraphSearch.tsx +25 -4
- lightrag_webui/src/components/graph/PropertiesView.tsx +12 -4
- lightrag_webui/src/components/graph/Settings.tsx +115 -103
- lightrag_webui/src/components/graph/SettingsDisplay.tsx +21 -0
- lightrag_webui/src/components/retrieval/QuerySettings.tsx +1 -1
- lightrag_webui/src/components/ui/AsyncSearch.tsx +5 -5
- lightrag_webui/src/components/ui/TabContent.tsx +37 -0
- lightrag_webui/src/components/ui/Tabs.tsx +5 -1
- lightrag_webui/src/components/ui/Tooltip.tsx +33 -20
- lightrag_webui/src/contexts/TabVisibilityProvider.tsx +53 -0
- lightrag_webui/src/contexts/context.ts +12 -0
- lightrag_webui/src/contexts/types.ts +5 -0
- lightrag_webui/src/contexts/useTabVisibility.ts +17 -0
- lightrag_webui/src/features/ApiSite.tsx +36 -1
- lightrag_webui/src/features/DocumentManager.tsx +20 -9
- lightrag_webui/src/features/GraphViewer.tsx +102 -47
- lightrag_webui/src/features/SiteHeader.tsx +9 -7
- lightrag_webui/src/hooks/useLightragGraph.tsx +143 -21
- lightrag_webui/src/i18n.js +0 -21
- lightrag_webui/src/i18n.ts +37 -0
- lightrag_webui/src/lib/constants.ts +2 -2
- lightrag_webui/src/locales/en.json +22 -2
- lightrag_webui/src/locales/zh.json +92 -88
- lightrag_webui/src/main.tsx +2 -9
- lightrag_webui/src/stores/graph.ts +77 -7
- lightrag_webui/src/stores/settings.ts +22 -4
lightrag/api/lightrag_server.py
CHANGED
@@ -391,12 +391,24 @@ def create_app(args):
|
|
391 |
"update_status": update_status,
|
392 |
}
|
393 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
394 |
# Webui mount webui/index.html
|
395 |
static_dir = Path(__file__).parent / "webui"
|
396 |
static_dir.mkdir(exist_ok=True)
|
397 |
app.mount(
|
398 |
"/webui",
|
399 |
-
|
400 |
name="webui",
|
401 |
)
|
402 |
|
|
|
391 |
"update_status": update_status,
|
392 |
}
|
393 |
|
394 |
+
# Custom StaticFiles class to prevent caching of HTML files
|
395 |
+
class NoCacheStaticFiles(StaticFiles):
|
396 |
+
async def get_response(self, path: str, scope):
|
397 |
+
response = await super().get_response(path, scope)
|
398 |
+
if path.endswith(".html"):
|
399 |
+
response.headers["Cache-Control"] = (
|
400 |
+
"no-cache, no-store, must-revalidate"
|
401 |
+
)
|
402 |
+
response.headers["Pragma"] = "no-cache"
|
403 |
+
response.headers["Expires"] = "0"
|
404 |
+
return response
|
405 |
+
|
406 |
# Webui mount webui/index.html
|
407 |
static_dir = Path(__file__).parent / "webui"
|
408 |
static_dir.mkdir(exist_ok=True)
|
409 |
app.mount(
|
410 |
"/webui",
|
411 |
+
NoCacheStaticFiles(directory=static_dir, html=True, check_dir=True),
|
412 |
name="webui",
|
413 |
)
|
414 |
|
lightrag/api/webui/assets/index-BV5s8k-a.css
ADDED
Binary file (48.6 kB). View file
|
|
lightrag/api/webui/assets/index-CH-3l4_Z.css
DELETED
Binary file (47.7 kB)
|
|
lightrag/api/webui/assets/{index-BlVvSIic.js → index-DwcJE583.js}
RENAMED
Binary files a/lightrag/api/webui/assets/index-BlVvSIic.js and b/lightrag/api/webui/assets/index-DwcJE583.js differ
|
|
lightrag/api/webui/index.html
CHANGED
Binary files a/lightrag/api/webui/index.html and b/lightrag/api/webui/index.html differ
|
|
lightrag_webui/bun.lock
CHANGED
@@ -62,6 +62,7 @@
|
|
62 |
"@types/node": "^22.13.5",
|
63 |
"@types/react": "^19.0.10",
|
64 |
"@types/react-dom": "^19.0.4",
|
|
|
65 |
"@types/react-syntax-highlighter": "^15.5.13",
|
66 |
"@types/seedrandom": "^3.0.8",
|
67 |
"@vitejs/plugin-react-swc": "^3.8.0",
|
@@ -443,6 +444,8 @@
|
|
443 |
|
444 |
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
|
445 |
|
|
|
|
|
446 |
"@types/react-syntax-highlighter": ["@types/[email protected]", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
|
447 |
|
448 |
"@types/react-transition-group": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
|
|
|
62 |
"@types/node": "^22.13.5",
|
63 |
"@types/react": "^19.0.10",
|
64 |
"@types/react-dom": "^19.0.4",
|
65 |
+
"@types/react-i18next": "^8.1.0",
|
66 |
"@types/react-syntax-highlighter": "^15.5.13",
|
67 |
"@types/seedrandom": "^3.0.8",
|
68 |
"@vitejs/plugin-react-swc": "^3.8.0",
|
|
|
444 |
|
445 |
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
|
446 |
|
447 |
+
"@types/react-i18next": ["@types/[email protected]", "", { "dependencies": { "react-i18next": "*" } }, "sha512-d4xhcjX5b3roNMObRNMfb1HinHQlQLPo8xlDj60dnHeeAw2bBymR2cy/l1giJpHzo/ZFgSvgVUvIWr4kCrenCg=="],
|
448 |
+
|
449 |
"@types/react-syntax-highlighter": ["@types/[email protected]", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
|
450 |
|
451 |
"@types/react-transition-group": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
|
lightrag_webui/index.html
CHANGED
@@ -2,6 +2,9 @@
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8" />
|
|
|
|
|
|
|
5 |
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
<title>Lightrag</title>
|
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8" />
|
5 |
+
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
6 |
+
<meta http-equiv="Pragma" content="no-cache" />
|
7 |
+
<meta http-equiv="Expires" content="0" />
|
8 |
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
9 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
10 |
<title>Lightrag</title>
|
lightrag_webui/package.json
CHANGED
@@ -71,6 +71,7 @@
|
|
71 |
"@types/node": "^22.13.5",
|
72 |
"@types/react": "^19.0.10",
|
73 |
"@types/react-dom": "^19.0.4",
|
|
|
74 |
"@types/react-syntax-highlighter": "^15.5.13",
|
75 |
"@types/seedrandom": "^3.0.8",
|
76 |
"@vitejs/plugin-react-swc": "^3.8.0",
|
|
|
71 |
"@types/node": "^22.13.5",
|
72 |
"@types/react": "^19.0.10",
|
73 |
"@types/react-dom": "^19.0.4",
|
74 |
+
"@types/react-i18next": "^8.1.0",
|
75 |
"@types/react-syntax-highlighter": "^15.5.13",
|
76 |
"@types/seedrandom": "^3.0.8",
|
77 |
"@vitejs/plugin-react-swc": "^3.8.0",
|
lightrag_webui/src/App.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import { useState, useCallback } from 'react'
|
2 |
import ThemeProvider from '@/components/ThemeProvider'
|
|
|
3 |
import MessageAlert from '@/components/MessageAlert'
|
4 |
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
5 |
import StatusIndicator from '@/components/graph/StatusIndicator'
|
@@ -21,7 +22,7 @@ import { Tabs, TabsContent } from '@/components/ui/Tabs'
|
|
21 |
function App() {
|
22 |
const message = useBackendState.use.message()
|
23 |
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
24 |
-
const
|
25 |
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
26 |
|
27 |
// Health check
|
@@ -54,33 +55,35 @@ function App() {
|
|
54 |
|
55 |
return (
|
56 |
<ThemeProvider>
|
57 |
-
<
|
58 |
-
<
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
<
|
66 |
-
<
|
67 |
-
|
68 |
-
|
69 |
-
<
|
70 |
-
|
71 |
-
|
72 |
-
<
|
73 |
-
|
74 |
-
|
75 |
-
<
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
|
|
|
|
84 |
</ThemeProvider>
|
85 |
)
|
86 |
}
|
|
|
1 |
import { useState, useCallback } from 'react'
|
2 |
import ThemeProvider from '@/components/ThemeProvider'
|
3 |
+
import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
|
4 |
import MessageAlert from '@/components/MessageAlert'
|
5 |
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
6 |
import StatusIndicator from '@/components/graph/StatusIndicator'
|
|
|
22 |
function App() {
|
23 |
const message = useBackendState.use.message()
|
24 |
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
25 |
+
const currentTab = useSettingsStore.use.currentTab()
|
26 |
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
27 |
|
28 |
// Health check
|
|
|
55 |
|
56 |
return (
|
57 |
<ThemeProvider>
|
58 |
+
<TabVisibilityProvider>
|
59 |
+
<main className="flex h-screen w-screen overflow-x-hidden">
|
60 |
+
<Tabs
|
61 |
+
defaultValue={currentTab}
|
62 |
+
className="!m-0 flex grow flex-col !p-0"
|
63 |
+
onValueChange={handleTabChange}
|
64 |
+
>
|
65 |
+
<SiteHeader />
|
66 |
+
<div className="relative grow">
|
67 |
+
<TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
|
68 |
+
<DocumentManager />
|
69 |
+
</TabsContent>
|
70 |
+
<TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
|
71 |
+
<GraphViewer />
|
72 |
+
</TabsContent>
|
73 |
+
<TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
|
74 |
+
<RetrievalTesting />
|
75 |
+
</TabsContent>
|
76 |
+
<TabsContent value="api" className="absolute top-0 right-0 bottom-0 left-0">
|
77 |
+
<ApiSite />
|
78 |
+
</TabsContent>
|
79 |
+
</div>
|
80 |
+
</Tabs>
|
81 |
+
{enableHealthCheck && <StatusIndicator />}
|
82 |
+
{message !== null && !apiKeyInvalid && <MessageAlert />}
|
83 |
+
{apiKeyInvalid && <ApiKeyAlert />}
|
84 |
+
<Toaster />
|
85 |
+
</main>
|
86 |
+
</TabVisibilityProvider>
|
87 |
</ThemeProvider>
|
88 |
)
|
89 |
}
|
lightrag_webui/src/components/AppSettings.tsx
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useCallback } from 'react'
|
2 |
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
3 |
+
import Button from '@/components/ui/Button'
|
4 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'
|
5 |
+
import { useSettingsStore } from '@/stores/settings'
|
6 |
+
import { PaletteIcon } from 'lucide-react'
|
7 |
+
import { useTranslation } from 'react-i18next'
|
8 |
+
|
9 |
+
export default function AppSettings() {
|
10 |
+
const [opened, setOpened] = useState<boolean>(false)
|
11 |
+
const { t } = useTranslation()
|
12 |
+
|
13 |
+
const language = useSettingsStore.use.language()
|
14 |
+
const setLanguage = useSettingsStore.use.setLanguage()
|
15 |
+
|
16 |
+
const theme = useSettingsStore.use.theme()
|
17 |
+
const setTheme = useSettingsStore.use.setTheme()
|
18 |
+
|
19 |
+
const handleLanguageChange = useCallback((value: string) => {
|
20 |
+
setLanguage(value as 'en' | 'zh')
|
21 |
+
}, [setLanguage])
|
22 |
+
|
23 |
+
const handleThemeChange = useCallback((value: string) => {
|
24 |
+
setTheme(value as 'light' | 'dark' | 'system')
|
25 |
+
}, [setTheme])
|
26 |
+
|
27 |
+
return (
|
28 |
+
<Popover open={opened} onOpenChange={setOpened}>
|
29 |
+
<PopoverTrigger asChild>
|
30 |
+
<Button variant="outline" size="icon" className="h-9 w-9">
|
31 |
+
<PaletteIcon className="h-5 w-5" />
|
32 |
+
</Button>
|
33 |
+
</PopoverTrigger>
|
34 |
+
<PopoverContent side="bottom" align="end" className="w-56">
|
35 |
+
<div className="flex flex-col gap-4">
|
36 |
+
<div className="flex flex-col gap-2">
|
37 |
+
<label className="text-sm font-medium">{t('settings.language')}</label>
|
38 |
+
<Select value={language} onValueChange={handleLanguageChange}>
|
39 |
+
<SelectTrigger>
|
40 |
+
<SelectValue />
|
41 |
+
</SelectTrigger>
|
42 |
+
<SelectContent>
|
43 |
+
<SelectItem value="en">English</SelectItem>
|
44 |
+
<SelectItem value="zh">中文</SelectItem>
|
45 |
+
</SelectContent>
|
46 |
+
</Select>
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<div className="flex flex-col gap-2">
|
50 |
+
<label className="text-sm font-medium">{t('settings.theme')}</label>
|
51 |
+
<Select value={theme} onValueChange={handleThemeChange}>
|
52 |
+
<SelectTrigger>
|
53 |
+
<SelectValue />
|
54 |
+
</SelectTrigger>
|
55 |
+
<SelectContent>
|
56 |
+
<SelectItem value="light">{t('settings.light')}</SelectItem>
|
57 |
+
<SelectItem value="dark">{t('settings.dark')}</SelectItem>
|
58 |
+
<SelectItem value="system">{t('settings.system')}</SelectItem>
|
59 |
+
</SelectContent>
|
60 |
+
</Select>
|
61 |
+
</div>
|
62 |
+
</div>
|
63 |
+
</PopoverContent>
|
64 |
+
</Popover>
|
65 |
+
)
|
66 |
+
}
|
lightrag_webui/src/components/Root.tsx
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { StrictMode, useEffect, useState } from 'react'
|
2 |
+
import { initializeI18n } from '@/i18n'
|
3 |
+
import App from '@/App'
|
4 |
+
|
5 |
+
export const Root = () => {
|
6 |
+
const [isI18nInitialized, setIsI18nInitialized] = useState(false)
|
7 |
+
|
8 |
+
useEffect(() => {
|
9 |
+
// Initialize i18n immediately with persisted language
|
10 |
+
initializeI18n().then(() => {
|
11 |
+
setIsI18nInitialized(true)
|
12 |
+
})
|
13 |
+
}, [])
|
14 |
+
|
15 |
+
if (!isI18nInitialized) {
|
16 |
+
return null // or a loading spinner
|
17 |
+
}
|
18 |
+
|
19 |
+
return (
|
20 |
+
<StrictMode>
|
21 |
+
<App />
|
22 |
+
</StrictMode>
|
23 |
+
)
|
24 |
+
}
|
lightrag_webui/src/components/ThemeProvider.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { createContext, useEffect
|
2 |
import { Theme, useSettingsStore } from '@/stores/settings'
|
3 |
|
4 |
type ThemeProviderProps = {
|
@@ -21,30 +21,32 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
|
21 |
* Component that provides the theme state and setter function to its children.
|
22 |
*/
|
23 |
export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
24 |
-
const
|
|
|
25 |
|
26 |
useEffect(() => {
|
27 |
const root = window.document.documentElement
|
28 |
root.classList.remove('light', 'dark')
|
29 |
|
30 |
if (theme === 'system') {
|
31 |
-
const
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
}
|
38 |
-
|
39 |
-
root.classList.add(theme)
|
40 |
}, [theme])
|
41 |
|
42 |
const value = {
|
43 |
theme,
|
44 |
-
setTheme
|
45 |
-
useSettingsStore.getState().setTheme(theme)
|
46 |
-
setTheme(theme)
|
47 |
-
}
|
48 |
}
|
49 |
|
50 |
return (
|
|
|
1 |
+
import { createContext, useEffect } from 'react'
|
2 |
import { Theme, useSettingsStore } from '@/stores/settings'
|
3 |
|
4 |
type ThemeProviderProps = {
|
|
|
21 |
* Component that provides the theme state and setter function to its children.
|
22 |
*/
|
23 |
export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
24 |
+
const theme = useSettingsStore.use.theme()
|
25 |
+
const setTheme = useSettingsStore.use.setTheme()
|
26 |
|
27 |
useEffect(() => {
|
28 |
const root = window.document.documentElement
|
29 |
root.classList.remove('light', 'dark')
|
30 |
|
31 |
if (theme === 'system') {
|
32 |
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
33 |
+
const handleChange = (e: MediaQueryListEvent) => {
|
34 |
+
root.classList.remove('light', 'dark')
|
35 |
+
root.classList.add(e.matches ? 'dark' : 'light')
|
36 |
+
}
|
37 |
+
|
38 |
+
root.classList.add(mediaQuery.matches ? 'dark' : 'light')
|
39 |
+
mediaQuery.addEventListener('change', handleChange)
|
40 |
+
|
41 |
+
return () => mediaQuery.removeEventListener('change', handleChange)
|
42 |
+
} else {
|
43 |
+
root.classList.add(theme)
|
44 |
}
|
|
|
|
|
45 |
}, [theme])
|
46 |
|
47 |
const value = {
|
48 |
theme,
|
49 |
+
setTheme
|
|
|
|
|
|
|
50 |
}
|
51 |
|
52 |
return (
|
lightrag_webui/src/components/graph/FocusOnNode.tsx
CHANGED
@@ -13,15 +13,24 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
|
|
13 |
* When the selected item changes, highlighted the node and center the camera on it.
|
14 |
*/
|
15 |
useEffect(() => {
|
16 |
-
if (!node) return
|
17 |
-
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
18 |
if (move) {
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
useGraphStore.getState().setMoveToSelectedNode(false)
|
|
|
|
|
21 |
}
|
22 |
|
23 |
return () => {
|
24 |
-
|
|
|
|
|
25 |
}
|
26 |
}, [node, move, sigma, gotoNode])
|
27 |
|
|
|
13 |
* When the selected item changes, highlighted the node and center the camera on it.
|
14 |
*/
|
15 |
useEffect(() => {
|
|
|
|
|
16 |
if (move) {
|
17 |
+
if (node) {
|
18 |
+
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
19 |
+
gotoNode(node)
|
20 |
+
} else {
|
21 |
+
// If no node is selected but move is true, reset to default view
|
22 |
+
sigma.setCustomBBox(null)
|
23 |
+
sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 })
|
24 |
+
}
|
25 |
useGraphStore.getState().setMoveToSelectedNode(false)
|
26 |
+
} else if (node) {
|
27 |
+
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
28 |
}
|
29 |
|
30 |
return () => {
|
31 |
+
if (node) {
|
32 |
+
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
33 |
+
}
|
34 |
}
|
35 |
}, [node, move, sigma, gotoNode])
|
36 |
|
lightrag_webui/src/components/graph/GraphControl.tsx
CHANGED
@@ -1,10 +1,11 @@
|
|
1 |
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
|
|
2 |
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
3 |
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
4 |
import { useEffect } from 'react'
|
5 |
|
6 |
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
7 |
-
import
|
8 |
import useTheme from '@/hooks/useTheme'
|
9 |
import * as Constants from '@/lib/constants'
|
10 |
|
@@ -21,7 +22,6 @@ const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
|
21 |
}
|
22 |
|
23 |
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
|
24 |
-
const { lightrageGraph } = useLightragGraph()
|
25 |
const sigma = useSigma<NodeType, EdgeType>()
|
26 |
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
27 |
const setSettings = useSetSettings<NodeType, EdgeType>()
|
@@ -34,21 +34,25 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|
34 |
|
35 |
const { theme } = useTheme()
|
36 |
const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
|
|
|
|
|
|
37 |
const selectedNode = useGraphStore.use.selectedNode()
|
38 |
const focusedNode = useGraphStore.use.focusedNode()
|
39 |
const selectedEdge = useGraphStore.use.selectedEdge()
|
40 |
const focusedEdge = useGraphStore.use.focusedEdge()
|
|
|
41 |
|
42 |
/**
|
43 |
* When component mount or maxIterations changes
|
44 |
* => load the graph and apply layout
|
45 |
*/
|
46 |
useEffect(() => {
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
}, [assignLayout, loadGraph,
|
52 |
|
53 |
/**
|
54 |
* When component mount
|
@@ -58,39 +62,52 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|
58 |
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
59 |
useGraphStore.getState()
|
60 |
|
61 |
-
//
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
64 |
if (!isButtonPressed(event.event.original)) {
|
65 |
setFocusedNode(event.node)
|
66 |
}
|
67 |
},
|
68 |
-
leaveNode: (event) => {
|
69 |
if (!isButtonPressed(event.event.original)) {
|
70 |
setFocusedNode(null)
|
71 |
}
|
72 |
},
|
73 |
-
clickNode: (event) => {
|
74 |
setSelectedNode(event.node)
|
75 |
setSelectedEdge(null)
|
76 |
},
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
78 |
setSelectedEdge(event.edge)
|
79 |
setSelectedNode(null)
|
80 |
-
}
|
81 |
-
|
|
|
82 |
if (!isButtonPressed(event.event.original)) {
|
83 |
setFocusedEdge(event.edge)
|
84 |
}
|
85 |
-
}
|
86 |
-
|
|
|
87 |
if (!isButtonPressed(event.event.original)) {
|
88 |
setFocusedEdge(null)
|
89 |
}
|
90 |
-
}
|
91 |
-
|
92 |
-
|
93 |
-
|
|
|
|
|
94 |
|
95 |
/**
|
96 |
* When component mount or hovered node change
|
@@ -101,7 +118,14 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|
101 |
const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined
|
102 |
const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined
|
103 |
|
|
|
104 |
setSettings({
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
nodeReducer: (node, data) => {
|
106 |
const graph = sigma.getGraph()
|
107 |
const newData: NodeType & {
|
@@ -140,6 +164,8 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|
140 |
}
|
141 |
return newData
|
142 |
},
|
|
|
|
|
143 |
edgeReducer: (edge, data) => {
|
144 |
const graph = sigma.getGraph()
|
145 |
const newData = { ...data, hidden: false, labelColor, color: edgeColor }
|
@@ -181,7 +207,10 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|
181 |
sigma,
|
182 |
disableHoverEffect,
|
183 |
theme,
|
184 |
-
hideUnselectedEdges
|
|
|
|
|
|
|
185 |
])
|
186 |
|
187 |
return null
|
|
|
1 |
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
2 |
+
import Graph from 'graphology'
|
3 |
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
4 |
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
5 |
import { useEffect } from 'react'
|
6 |
|
7 |
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
8 |
+
import { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
9 |
import useTheme from '@/hooks/useTheme'
|
10 |
import * as Constants from '@/lib/constants'
|
11 |
|
|
|
22 |
}
|
23 |
|
24 |
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
|
|
|
25 |
const sigma = useSigma<NodeType, EdgeType>()
|
26 |
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
27 |
const setSettings = useSetSettings<NodeType, EdgeType>()
|
|
|
34 |
|
35 |
const { theme } = useTheme()
|
36 |
const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
37 |
+
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
38 |
+
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
39 |
+
const renderLabels = useSettingsStore.use.showNodeLabel()
|
40 |
const selectedNode = useGraphStore.use.selectedNode()
|
41 |
const focusedNode = useGraphStore.use.focusedNode()
|
42 |
const selectedEdge = useGraphStore.use.selectedEdge()
|
43 |
const focusedEdge = useGraphStore.use.focusedEdge()
|
44 |
+
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
45 |
|
46 |
/**
|
47 |
* When component mount or maxIterations changes
|
48 |
* => load the graph and apply layout
|
49 |
*/
|
50 |
useEffect(() => {
|
51 |
+
if (sigmaGraph) {
|
52 |
+
loadGraph(sigmaGraph as unknown as Graph<NodeType, EdgeType>)
|
53 |
+
assignLayout()
|
54 |
+
}
|
55 |
+
}, [assignLayout, loadGraph, sigmaGraph, maxIterations])
|
56 |
|
57 |
/**
|
58 |
* When component mount
|
|
|
62 |
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
63 |
useGraphStore.getState()
|
64 |
|
65 |
+
// Define event types
|
66 |
+
type NodeEvent = { node: string; event: { original: MouseEvent | TouchEvent } }
|
67 |
+
type EdgeEvent = { edge: string; event: { original: MouseEvent | TouchEvent } }
|
68 |
+
|
69 |
+
// Register all events, but edge events will only be processed if enableEdgeEvents is true
|
70 |
+
const events: Record<string, any> = {
|
71 |
+
enterNode: (event: NodeEvent) => {
|
72 |
if (!isButtonPressed(event.event.original)) {
|
73 |
setFocusedNode(event.node)
|
74 |
}
|
75 |
},
|
76 |
+
leaveNode: (event: NodeEvent) => {
|
77 |
if (!isButtonPressed(event.event.original)) {
|
78 |
setFocusedNode(null)
|
79 |
}
|
80 |
},
|
81 |
+
clickNode: (event: NodeEvent) => {
|
82 |
setSelectedNode(event.node)
|
83 |
setSelectedEdge(null)
|
84 |
},
|
85 |
+
clickStage: () => clearSelection()
|
86 |
+
}
|
87 |
+
|
88 |
+
// Only add edge event handlers if enableEdgeEvents is true
|
89 |
+
if (enableEdgeEvents) {
|
90 |
+
events.clickEdge = (event: EdgeEvent) => {
|
91 |
setSelectedEdge(event.edge)
|
92 |
setSelectedNode(null)
|
93 |
+
}
|
94 |
+
|
95 |
+
events.enterEdge = (event: EdgeEvent) => {
|
96 |
if (!isButtonPressed(event.event.original)) {
|
97 |
setFocusedEdge(event.edge)
|
98 |
}
|
99 |
+
}
|
100 |
+
|
101 |
+
events.leaveEdge = (event: EdgeEvent) => {
|
102 |
if (!isButtonPressed(event.event.original)) {
|
103 |
setFocusedEdge(null)
|
104 |
}
|
105 |
+
}
|
106 |
+
}
|
107 |
+
|
108 |
+
// Register the events
|
109 |
+
registerEvents(events)
|
110 |
+
}, [registerEvents, enableEdgeEvents])
|
111 |
|
112 |
/**
|
113 |
* When component mount or hovered node change
|
|
|
118 |
const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined
|
119 |
const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined
|
120 |
|
121 |
+
// Update all dynamic settings directly without recreating the sigma container
|
122 |
setSettings({
|
123 |
+
// Update display settings
|
124 |
+
enableEdgeEvents,
|
125 |
+
renderEdgeLabels,
|
126 |
+
renderLabels,
|
127 |
+
|
128 |
+
// Node reducer for node appearance
|
129 |
nodeReducer: (node, data) => {
|
130 |
const graph = sigma.getGraph()
|
131 |
const newData: NodeType & {
|
|
|
164 |
}
|
165 |
return newData
|
166 |
},
|
167 |
+
|
168 |
+
// Edge reducer for edge appearance
|
169 |
edgeReducer: (edge, data) => {
|
170 |
const graph = sigma.getGraph()
|
171 |
const newData = { ...data, hidden: false, labelColor, color: edgeColor }
|
|
|
207 |
sigma,
|
208 |
disableHoverEffect,
|
209 |
theme,
|
210 |
+
hideUnselectedEdges,
|
211 |
+
enableEdgeEvents,
|
212 |
+
renderEdgeLabels,
|
213 |
+
renderLabels
|
214 |
])
|
215 |
|
216 |
return null
|
lightrag_webui/src/components/graph/GraphLabels.tsx
CHANGED
@@ -1,37 +1,48 @@
|
|
1 |
-
import { useCallback } from 'react'
|
2 |
import { AsyncSelect } from '@/components/ui/AsyncSelect'
|
3 |
-
import { getGraphLabels } from '@/api/lightrag'
|
4 |
import { useSettingsStore } from '@/stores/settings'
|
5 |
import { useGraphStore } from '@/stores/graph'
|
6 |
import { labelListLimit } from '@/lib/constants'
|
7 |
import MiniSearch from 'minisearch'
|
8 |
import { useTranslation } from 'react-i18next'
|
9 |
|
10 |
-
const lastGraph: any = {
|
11 |
-
graph: null,
|
12 |
-
searchEngine: null,
|
13 |
-
labels: []
|
14 |
-
}
|
15 |
-
|
16 |
const GraphLabels = () => {
|
17 |
const { t } = useTranslation()
|
18 |
const label = useSettingsStore.use.queryLabel()
|
19 |
-
const
|
|
|
20 |
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
const labels = ['*'].concat(await getGraphLabels())
|
29 |
|
30 |
-
//
|
31 |
-
if (!
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
}
|
|
|
34 |
|
|
|
35 |
// Create search engine
|
36 |
const searchEngine = new MiniSearch({
|
37 |
idField: 'id',
|
@@ -46,41 +57,32 @@ const GraphLabels = () => {
|
|
46 |
})
|
47 |
|
48 |
// Add documents
|
49 |
-
const documents =
|
50 |
searchEngine.addAll(documents)
|
51 |
|
52 |
-
lastGraph.graph = graph
|
53 |
-
lastGraph.searchEngine = searchEngine
|
54 |
-
lastGraph.labels = labels
|
55 |
-
|
56 |
return {
|
57 |
-
labels,
|
58 |
searchEngine
|
59 |
}
|
60 |
-
}, [
|
61 |
|
62 |
const fetchData = useCallback(
|
63 |
async (query?: string): Promise<string[]> => {
|
64 |
-
const { labels, searchEngine } =
|
65 |
|
66 |
let result: string[] = labels
|
67 |
if (query) {
|
68 |
// Search labels
|
69 |
-
result = searchEngine.search(query).map((r) => labels[r.id])
|
70 |
}
|
71 |
|
72 |
return result.length <= labelListLimit
|
73 |
? result
|
74 |
-
: [...result.slice(0, labelListLimit),
|
75 |
},
|
76 |
[getSearchEngine]
|
77 |
)
|
78 |
|
79 |
-
const setQueryLabel = useCallback((label: string) => {
|
80 |
-
if (label.startsWith('And ') && label.endsWith(' others')) return
|
81 |
-
useSettingsStore.getState().setQueryLabel(label)
|
82 |
-
}, [])
|
83 |
-
|
84 |
return (
|
85 |
<AsyncSelect<string>
|
86 |
className="ml-2"
|
@@ -94,8 +96,38 @@ const GraphLabels = () => {
|
|
94 |
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
95 |
label={t('graphPanel.graphLabels.label')}
|
96 |
placeholder={t('graphPanel.graphLabels.placeholder')}
|
97 |
-
value={label !== null ? label : ''}
|
98 |
-
onChange={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
/>
|
100 |
)
|
101 |
}
|
|
|
1 |
+
import { useCallback, useEffect, useRef } from 'react'
|
2 |
import { AsyncSelect } from '@/components/ui/AsyncSelect'
|
|
|
3 |
import { useSettingsStore } from '@/stores/settings'
|
4 |
import { useGraphStore } from '@/stores/graph'
|
5 |
import { labelListLimit } from '@/lib/constants'
|
6 |
import MiniSearch from 'minisearch'
|
7 |
import { useTranslation } from 'react-i18next'
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
const GraphLabels = () => {
|
10 |
const { t } = useTranslation()
|
11 |
const label = useSettingsStore.use.queryLabel()
|
12 |
+
const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
|
13 |
+
const labelsLoadedRef = useRef(false)
|
14 |
|
15 |
+
// Track if a fetch is in progress to prevent multiple simultaneous fetches
|
16 |
+
const fetchInProgressRef = useRef(false)
|
17 |
+
|
18 |
+
// Fetch labels once on component mount, using global flag to prevent duplicates
|
19 |
+
useEffect(() => {
|
20 |
+
// Check if we've already attempted to fetch labels in this session
|
21 |
+
const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
|
|
|
22 |
|
23 |
+
// Only fetch if we haven't attempted in this session and no fetch is in progress
|
24 |
+
if (!labelsFetchAttempted && !fetchInProgressRef.current) {
|
25 |
+
fetchInProgressRef.current = true
|
26 |
+
// Set global flag to indicate we've attempted to fetch in this session
|
27 |
+
useGraphStore.getState().setLabelsFetchAttempted(true)
|
28 |
+
|
29 |
+
console.log('Fetching graph labels (once per session)...')
|
30 |
+
|
31 |
+
useGraphStore.getState().fetchAllDatabaseLabels()
|
32 |
+
.then(() => {
|
33 |
+
labelsLoadedRef.current = true
|
34 |
+
fetchInProgressRef.current = false
|
35 |
+
})
|
36 |
+
.catch((error) => {
|
37 |
+
console.error('Failed to fetch labels:', error)
|
38 |
+
fetchInProgressRef.current = false
|
39 |
+
// Reset global flag to allow retry
|
40 |
+
useGraphStore.getState().setLabelsFetchAttempted(false)
|
41 |
+
})
|
42 |
}
|
43 |
+
}, []) // Empty dependency array ensures this only runs once on mount
|
44 |
|
45 |
+
const getSearchEngine = useCallback(() => {
|
46 |
// Create search engine
|
47 |
const searchEngine = new MiniSearch({
|
48 |
idField: 'id',
|
|
|
57 |
})
|
58 |
|
59 |
// Add documents
|
60 |
+
const documents = allDatabaseLabels.map((str, index) => ({ id: index, value: str }))
|
61 |
searchEngine.addAll(documents)
|
62 |
|
|
|
|
|
|
|
|
|
63 |
return {
|
64 |
+
labels: allDatabaseLabels,
|
65 |
searchEngine
|
66 |
}
|
67 |
+
}, [allDatabaseLabels])
|
68 |
|
69 |
const fetchData = useCallback(
|
70 |
async (query?: string): Promise<string[]> => {
|
71 |
+
const { labels, searchEngine } = getSearchEngine()
|
72 |
|
73 |
let result: string[] = labels
|
74 |
if (query) {
|
75 |
// Search labels
|
76 |
+
result = searchEngine.search(query).map((r: { id: number }) => labels[r.id])
|
77 |
}
|
78 |
|
79 |
return result.length <= labelListLimit
|
80 |
? result
|
81 |
+
: [...result.slice(0, labelListLimit), '...']
|
82 |
},
|
83 |
[getSearchEngine]
|
84 |
)
|
85 |
|
|
|
|
|
|
|
|
|
|
|
86 |
return (
|
87 |
<AsyncSelect<string>
|
88 |
className="ml-2"
|
|
|
96 |
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
97 |
label={t('graphPanel.graphLabels.label')}
|
98 |
placeholder={t('graphPanel.graphLabels.placeholder')}
|
99 |
+
value={label !== null ? label : '*'}
|
100 |
+
onChange={(newLabel) => {
|
101 |
+
const currentLabel = useSettingsStore.getState().queryLabel
|
102 |
+
|
103 |
+
// select the last item means query all
|
104 |
+
if (newLabel === '...') {
|
105 |
+
newLabel = '*'
|
106 |
+
}
|
107 |
+
|
108 |
+
// Reset the fetch attempted flag to force a new data fetch
|
109 |
+
useGraphStore.getState().setGraphDataFetchAttempted(false)
|
110 |
+
|
111 |
+
// Clear current graph data to ensure complete reload when label changes
|
112 |
+
if (newLabel !== currentLabel) {
|
113 |
+
const graphStore = useGraphStore.getState();
|
114 |
+
graphStore.clearSelection();
|
115 |
+
|
116 |
+
// Reset the graph state but preserve the instance
|
117 |
+
if (graphStore.sigmaGraph) {
|
118 |
+
const nodes = Array.from(graphStore.sigmaGraph.nodes());
|
119 |
+
nodes.forEach(node => graphStore.sigmaGraph?.dropNode(node));
|
120 |
+
}
|
121 |
+
}
|
122 |
+
|
123 |
+
if (newLabel === currentLabel && newLabel !== '*') {
|
124 |
+
// reselect the same itme means qery all
|
125 |
+
useSettingsStore.getState().setQueryLabel('*')
|
126 |
+
} else {
|
127 |
+
useSettingsStore.getState().setQueryLabel(newLabel)
|
128 |
+
}
|
129 |
+
}}
|
130 |
+
clearable={false} // Prevent clearing value on reselect
|
131 |
/>
|
132 |
)
|
133 |
}
|
lightrag_webui/src/components/graph/GraphSearch.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { FC, useCallback, useMemo } from 'react'
|
2 |
import {
|
3 |
EdgeById,
|
4 |
NodeById,
|
@@ -28,6 +28,7 @@ function OptionComponent(item: OptionItem) {
|
|
28 |
}
|
29 |
|
30 |
const messageId = '__message_item'
|
|
|
31 |
const lastGraph: any = {
|
32 |
graph: null,
|
33 |
searchEngine: null
|
@@ -48,6 +49,15 @@ export const GraphSearchInput = ({
|
|
48 |
const { t } = useTranslation()
|
49 |
const graph = useGraphStore.use.sigmaGraph()
|
50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
const searchEngine = useMemo(() => {
|
52 |
if (lastGraph.graph == graph) {
|
53 |
return lastGraph.searchEngine
|
@@ -85,8 +95,19 @@ export const GraphSearchInput = ({
|
|
85 |
const loadOptions = useCallback(
|
86 |
async (query?: string): Promise<OptionItem[]> => {
|
87 |
if (onFocus) onFocus(null)
|
88 |
-
if (!
|
89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
id: r.id,
|
91 |
type: 'nodes'
|
92 |
}))
|
@@ -103,7 +124,7 @@ export const GraphSearchInput = ({
|
|
103 |
}
|
104 |
]
|
105 |
},
|
106 |
-
[searchEngine, onFocus]
|
107 |
)
|
108 |
|
109 |
return (
|
|
|
1 |
+
import { FC, useCallback, useEffect, useMemo } from 'react'
|
2 |
import {
|
3 |
EdgeById,
|
4 |
NodeById,
|
|
|
28 |
}
|
29 |
|
30 |
const messageId = '__message_item'
|
31 |
+
// Reset this cache when graph changes to ensure fresh search results
|
32 |
const lastGraph: any = {
|
33 |
graph: null,
|
34 |
searchEngine: null
|
|
|
49 |
const { t } = useTranslation()
|
50 |
const graph = useGraphStore.use.sigmaGraph()
|
51 |
|
52 |
+
// Force reset the cache when graph changes
|
53 |
+
useEffect(() => {
|
54 |
+
if (graph) {
|
55 |
+
// Reset cache to ensure fresh search results with new graph data
|
56 |
+
lastGraph.graph = null;
|
57 |
+
lastGraph.searchEngine = null;
|
58 |
+
}
|
59 |
+
}, [graph]);
|
60 |
+
|
61 |
const searchEngine = useMemo(() => {
|
62 |
if (lastGraph.graph == graph) {
|
63 |
return lastGraph.searchEngine
|
|
|
95 |
const loadOptions = useCallback(
|
96 |
async (query?: string): Promise<OptionItem[]> => {
|
97 |
if (onFocus) onFocus(null)
|
98 |
+
if (!graph || !searchEngine) return []
|
99 |
+
|
100 |
+
// If no query, return first searchResultLimit nodes
|
101 |
+
if (!query) {
|
102 |
+
const nodeIds = graph.nodes().slice(0, searchResultLimit)
|
103 |
+
return nodeIds.map(id => ({
|
104 |
+
id,
|
105 |
+
type: 'nodes'
|
106 |
+
}))
|
107 |
+
}
|
108 |
+
|
109 |
+
// If has query, search nodes
|
110 |
+
const result: OptionItem[] = searchEngine.search(query).map((r: { id: string }) => ({
|
111 |
id: r.id,
|
112 |
type: 'nodes'
|
113 |
}))
|
|
|
124 |
}
|
125 |
]
|
126 |
},
|
127 |
+
[graph, searchEngine, onFocus, t]
|
128 |
)
|
129 |
|
130 |
return (
|
lightrag_webui/src/components/graph/PropertiesView.tsx
CHANGED
@@ -132,14 +132,22 @@ const PropertyRow = ({
|
|
132 |
onClick?: () => void
|
133 |
tooltip?: string
|
134 |
}) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
return (
|
136 |
<div className="flex items-center gap-2">
|
137 |
-
<label className="text-primary/60 tracking-wide">{name}</label>:
|
138 |
<Text
|
139 |
-
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
|
140 |
tooltipClassName="max-w-80"
|
141 |
text={value}
|
142 |
-
tooltip={tooltip || value}
|
143 |
side="left"
|
144 |
onClick={onClick}
|
145 |
/>
|
@@ -174,7 +182,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|
174 |
{node.relationships.length > 0 && (
|
175 |
<>
|
176 |
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
|
177 |
-
|
178 |
</label>
|
179 |
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
180 |
{node.relationships.map(({ type, id, label }) => {
|
|
|
132 |
onClick?: () => void
|
133 |
tooltip?: string
|
134 |
}) => {
|
135 |
+
const { t } = useTranslation()
|
136 |
+
|
137 |
+
const getPropertyNameTranslation = (name: string) => {
|
138 |
+
const translationKey = `graphPanel.propertiesView.node.propertyNames.${name}`
|
139 |
+
const translation = t(translationKey)
|
140 |
+
return translation === translationKey ? name : translation
|
141 |
+
}
|
142 |
+
|
143 |
return (
|
144 |
<div className="flex items-center gap-2">
|
145 |
+
<label className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</label>:
|
146 |
<Text
|
147 |
+
className="hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis"
|
148 |
tooltipClassName="max-w-80"
|
149 |
text={value}
|
150 |
+
tooltip={tooltip || (typeof value === 'string' ? value : JSON.stringify(value, null, 2))}
|
151 |
side="left"
|
152 |
onClick={onClick}
|
153 |
/>
|
|
|
182 |
{node.relationships.length > 0 && (
|
183 |
<>
|
184 |
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
|
185 |
+
{t('graphPanel.propertiesView.node.relationships')}
|
186 |
</label>
|
187 |
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
188 |
{node.relationships.map(({ type, id, label }) => {
|
lightrag_webui/src/components/graph/Settings.tsx
CHANGED
@@ -8,9 +8,10 @@ import Input from '@/components/ui/Input'
|
|
8 |
import { controlButtonVariant } from '@/lib/constants'
|
9 |
import { useSettingsStore } from '@/stores/settings'
|
10 |
import { useBackendState } from '@/stores/state'
|
|
|
11 |
|
12 |
-
import { SettingsIcon } from 'lucide-react'
|
13 |
-
import { useTranslation } from
|
14 |
|
15 |
/**
|
16 |
* Component that displays a checkbox with a label.
|
@@ -114,6 +115,7 @@ const LabeledNumberInput = ({
|
|
114 |
export default function Settings() {
|
115 |
const [opened, setOpened] = useState<boolean>(false)
|
116 |
const [tempApiKey, setTempApiKey] = useState<string>('')
|
|
|
117 |
|
118 |
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
119 |
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
@@ -208,116 +210,126 @@ export default function Settings() {
|
|
208 |
const { t } = useTranslation();
|
209 |
|
210 |
return (
|
211 |
-
|
212 |
-
<
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
<PopoverContent
|
218 |
-
side="right"
|
219 |
-
align="start"
|
220 |
-
className="mb-2 p-2"
|
221 |
-
onCloseAutoFocus={(e) => e.preventDefault()}
|
222 |
>
|
223 |
-
<
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
|
230 |
-
|
231 |
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
|
243 |
-
|
244 |
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
|
256 |
-
|
257 |
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
|
|
318 |
</div>
|
319 |
-
</
|
320 |
-
</
|
321 |
-
|
322 |
)
|
323 |
}
|
|
|
8 |
import { controlButtonVariant } from '@/lib/constants'
|
9 |
import { useSettingsStore } from '@/stores/settings'
|
10 |
import { useBackendState } from '@/stores/state'
|
11 |
+
import { useGraphStore } from '@/stores/graph'
|
12 |
|
13 |
+
import { SettingsIcon, RefreshCwIcon } from 'lucide-react'
|
14 |
+
import { useTranslation } from 'react-i18next';
|
15 |
|
16 |
/**
|
17 |
* Component that displays a checkbox with a label.
|
|
|
115 |
export default function Settings() {
|
116 |
const [opened, setOpened] = useState<boolean>(false)
|
117 |
const [tempApiKey, setTempApiKey] = useState<string>('')
|
118 |
+
const refreshLayout = useGraphStore.use.refreshLayout()
|
119 |
|
120 |
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
121 |
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
|
|
210 |
const { t } = useTranslation();
|
211 |
|
212 |
return (
|
213 |
+
<>
|
214 |
+
<Button
|
215 |
+
variant={controlButtonVariant}
|
216 |
+
tooltip={t('graphPanel.sideBar.settings.refreshLayout')}
|
217 |
+
size="icon"
|
218 |
+
onClick={refreshLayout}
|
|
|
|
|
|
|
|
|
|
|
219 |
>
|
220 |
+
<RefreshCwIcon />
|
221 |
+
</Button>
|
222 |
+
<Popover open={opened} onOpenChange={setOpened}>
|
223 |
+
<PopoverTrigger asChild>
|
224 |
+
<Button variant={controlButtonVariant} tooltip={t('graphPanel.sideBar.settings.settings')} size="icon">
|
225 |
+
<SettingsIcon />
|
226 |
+
</Button>
|
227 |
+
</PopoverTrigger>
|
228 |
+
<PopoverContent
|
229 |
+
side="right"
|
230 |
+
align="start"
|
231 |
+
className="mb-2 p-2"
|
232 |
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
233 |
+
>
|
234 |
+
<div className="flex flex-col gap-2">
|
235 |
+
<LabeledCheckBox
|
236 |
+
checked={enableHealthCheck}
|
237 |
+
onCheckedChange={setEnableHealthCheck}
|
238 |
+
label={t('graphPanel.sideBar.settings.healthCheck')}
|
239 |
+
/>
|
240 |
|
241 |
+
<Separator />
|
242 |
|
243 |
+
<LabeledCheckBox
|
244 |
+
checked={showPropertyPanel}
|
245 |
+
onCheckedChange={setShowPropertyPanel}
|
246 |
+
label={t('graphPanel.sideBar.settings.showPropertyPanel')}
|
247 |
+
/>
|
248 |
+
<LabeledCheckBox
|
249 |
+
checked={showNodeSearchBar}
|
250 |
+
onCheckedChange={setShowNodeSearchBar}
|
251 |
+
label={t('graphPanel.sideBar.settings.showSearchBar')}
|
252 |
+
/>
|
253 |
|
254 |
+
<Separator />
|
255 |
|
256 |
+
<LabeledCheckBox
|
257 |
+
checked={showNodeLabel}
|
258 |
+
onCheckedChange={setShowNodeLabel}
|
259 |
+
label={t('graphPanel.sideBar.settings.showNodeLabel')}
|
260 |
+
/>
|
261 |
+
<LabeledCheckBox
|
262 |
+
checked={enableNodeDrag}
|
263 |
+
onCheckedChange={setEnableNodeDrag}
|
264 |
+
label={t('graphPanel.sideBar.settings.nodeDraggable')}
|
265 |
+
/>
|
266 |
|
267 |
+
<Separator />
|
268 |
|
269 |
+
<LabeledCheckBox
|
270 |
+
checked={showEdgeLabel}
|
271 |
+
onCheckedChange={setShowEdgeLabel}
|
272 |
+
label={t('graphPanel.sideBar.settings.showEdgeLabel')}
|
273 |
+
/>
|
274 |
+
<LabeledCheckBox
|
275 |
+
checked={enableHideUnselectedEdges}
|
276 |
+
onCheckedChange={setEnableHideUnselectedEdges}
|
277 |
+
label={t('graphPanel.sideBar.settings.hideUnselectedEdges')}
|
278 |
+
/>
|
279 |
+
<LabeledCheckBox
|
280 |
+
checked={enableEdgeEvents}
|
281 |
+
onCheckedChange={setEnableEdgeEvents}
|
282 |
+
label={t('graphPanel.sideBar.settings.edgeEvents')}
|
283 |
+
/>
|
284 |
|
285 |
+
<Separator />
|
286 |
+
<LabeledNumberInput
|
287 |
+
label={t('graphPanel.sideBar.settings.maxQueryDepth')}
|
288 |
+
min={1}
|
289 |
+
value={graphQueryMaxDepth}
|
290 |
+
onEditFinished={setGraphQueryMaxDepth}
|
291 |
+
/>
|
292 |
+
<LabeledNumberInput
|
293 |
+
label={t('graphPanel.sideBar.settings.minDegree')}
|
294 |
+
min={0}
|
295 |
+
value={graphMinDegree}
|
296 |
+
onEditFinished={setGraphMinDegree}
|
297 |
+
/>
|
298 |
+
<LabeledNumberInput
|
299 |
+
label={t('graphPanel.sideBar.settings.maxLayoutIterations')}
|
300 |
+
min={1}
|
301 |
+
max={30}
|
302 |
+
value={graphLayoutMaxIterations}
|
303 |
+
onEditFinished={setGraphLayoutMaxIterations}
|
304 |
+
/>
|
305 |
+
<Separator />
|
306 |
|
307 |
+
<div className="flex flex-col gap-2">
|
308 |
+
<label className="text-sm font-medium">{t('graphPanel.sideBar.settings.apiKey')}</label>
|
309 |
+
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
|
310 |
+
<div className="w-0 flex-1">
|
311 |
+
<Input
|
312 |
+
type="password"
|
313 |
+
value={tempApiKey}
|
314 |
+
onChange={handleTempApiKeyChange}
|
315 |
+
placeholder={t('graphPanel.sideBar.settings.enterYourAPIkey')}
|
316 |
+
className="max-h-full w-full min-w-0"
|
317 |
+
autoComplete="off"
|
318 |
+
/>
|
319 |
+
</div>
|
320 |
+
<Button
|
321 |
+
onClick={setApiKey}
|
322 |
+
variant="outline"
|
323 |
+
size="sm"
|
324 |
+
className="max-h-full shrink-0"
|
325 |
+
>
|
326 |
+
{t('graphPanel.sideBar.settings.save')}
|
327 |
+
</Button>
|
328 |
+
</form>
|
329 |
+
</div>
|
330 |
</div>
|
331 |
+
</PopoverContent>
|
332 |
+
</Popover>
|
333 |
+
</>
|
334 |
)
|
335 |
}
|
lightrag_webui/src/components/graph/SettingsDisplay.tsx
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useSettingsStore } from '@/stores/settings'
|
2 |
+
import { useTranslation } from 'react-i18next'
|
3 |
+
|
4 |
+
/**
|
5 |
+
* Component that displays current values of important graph settings
|
6 |
+
* Positioned to the right of the toolbar at the bottom-left corner
|
7 |
+
*/
|
8 |
+
const SettingsDisplay = () => {
|
9 |
+
const { t } = useTranslation()
|
10 |
+
const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth()
|
11 |
+
const graphMinDegree = useSettingsStore.use.graphMinDegree()
|
12 |
+
|
13 |
+
return (
|
14 |
+
<div className="absolute bottom-2 left-[calc(2rem+2.5rem)] flex items-center gap-2 text-xs text-gray-400">
|
15 |
+
<div>{t('graphPanel.sideBar.settings.depth')}: {graphQueryMaxDepth}</div>
|
16 |
+
<div>{t('graphPanel.sideBar.settings.degree')}: {graphMinDegree}</div>
|
17 |
+
</div>
|
18 |
+
)
|
19 |
+
}
|
20 |
+
|
21 |
+
export default SettingsDisplay
|
lightrag_webui/src/components/retrieval/QuerySettings.tsx
CHANGED
@@ -25,7 +25,7 @@ export default function QuerySettings() {
|
|
25 |
}, [])
|
26 |
|
27 |
return (
|
28 |
-
<Card className="flex shrink-0 flex-col">
|
29 |
<CardHeader className="px-4 pt-4 pb-2">
|
30 |
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
31 |
<CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
|
|
25 |
}, [])
|
26 |
|
27 |
return (
|
28 |
+
<Card className="flex shrink-0 flex-col min-w-[180px]">
|
29 |
<CardHeader className="px-4 pt-4 pb-2">
|
30 |
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
31 |
<CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
lightrag_webui/src/components/ui/AsyncSearch.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { useState, useEffect, useCallback } from 'react'
|
2 |
import { Loader2 } from 'lucide-react'
|
3 |
import { useDebounce } from '@/hooks/useDebounce'
|
4 |
|
@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
|
|
193 |
</div>
|
194 |
)}
|
195 |
</div>
|
196 |
-
<CommandList hidden={!open
|
197 |
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
198 |
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
199 |
{!loading &&
|
@@ -204,7 +204,7 @@ export function AsyncSearch<T>({
|
|
204 |
))}
|
205 |
<CommandGroup>
|
206 |
{options.map((option, idx) => (
|
207 |
-
|
208 |
<CommandItem
|
209 |
key={getOptionValue(option) + `${idx}`}
|
210 |
value={getOptionValue(option)}
|
@@ -215,9 +215,9 @@ export function AsyncSearch<T>({
|
|
215 |
{renderOption(option)}
|
216 |
</CommandItem>
|
217 |
{idx !== options.length - 1 && (
|
218 |
-
<div key={idx} className="bg-foreground/10 h-[1px]" />
|
219 |
)}
|
220 |
-
|
221 |
))}
|
222 |
</CommandGroup>
|
223 |
</CommandList>
|
|
|
1 |
+
import React, { useState, useEffect, useCallback } from 'react'
|
2 |
import { Loader2 } from 'lucide-react'
|
3 |
import { useDebounce } from '@/hooks/useDebounce'
|
4 |
|
|
|
193 |
</div>
|
194 |
)}
|
195 |
</div>
|
196 |
+
<CommandList hidden={!open}>
|
197 |
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
198 |
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
199 |
{!loading &&
|
|
|
204 |
))}
|
205 |
<CommandGroup>
|
206 |
{options.map((option, idx) => (
|
207 |
+
<React.Fragment key={getOptionValue(option) + `-fragment-${idx}`}>
|
208 |
<CommandItem
|
209 |
key={getOptionValue(option) + `${idx}`}
|
210 |
value={getOptionValue(option)}
|
|
|
215 |
{renderOption(option)}
|
216 |
</CommandItem>
|
217 |
{idx !== options.length - 1 && (
|
218 |
+
<div key={`divider-${idx}`} className="bg-foreground/10 h-[1px]" />
|
219 |
)}
|
220 |
+
</React.Fragment>
|
221 |
))}
|
222 |
</CommandGroup>
|
223 |
</CommandList>
|
lightrag_webui/src/components/ui/TabContent.tsx
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useEffect } from 'react';
|
2 |
+
import { useTabVisibility } from '@/contexts/useTabVisibility';
|
3 |
+
|
4 |
+
interface TabContentProps {
|
5 |
+
tabId: string;
|
6 |
+
children: React.ReactNode;
|
7 |
+
className?: string;
|
8 |
+
}
|
9 |
+
|
10 |
+
/**
|
11 |
+
* TabContent component that manages visibility based on tab selection
|
12 |
+
* Works with the TabVisibilityContext to show/hide content based on active tab
|
13 |
+
*/
|
14 |
+
const TabContent: React.FC<TabContentProps> = ({ tabId, children, className = '' }) => {
|
15 |
+
const { isTabVisible, setTabVisibility } = useTabVisibility();
|
16 |
+
const isVisible = isTabVisible(tabId);
|
17 |
+
|
18 |
+
// Register this tab with the context when mounted
|
19 |
+
useEffect(() => {
|
20 |
+
setTabVisibility(tabId, true);
|
21 |
+
|
22 |
+
// Cleanup when unmounted
|
23 |
+
return () => {
|
24 |
+
setTabVisibility(tabId, false);
|
25 |
+
};
|
26 |
+
}, [tabId, setTabVisibility]);
|
27 |
+
|
28 |
+
// Use CSS to hide content instead of not rendering it
|
29 |
+
// This prevents components from unmounting when tabs are switched
|
30 |
+
return (
|
31 |
+
<div className={`${className} ${isVisible ? '' : 'hidden'}`}>
|
32 |
+
{children}
|
33 |
+
</div>
|
34 |
+
);
|
35 |
+
};
|
36 |
+
|
37 |
+
export default TabContent;
|
lightrag_webui/src/components/ui/Tabs.tsx
CHANGED
@@ -42,9 +42,13 @@ const TabsContent = React.forwardRef<
|
|
42 |
<TabsPrimitive.Content
|
43 |
ref={ref}
|
44 |
className={cn(
|
45 |
-
'ring-offset-background focus-visible:ring-ring
|
|
|
|
|
46 |
className
|
47 |
)}
|
|
|
|
|
48 |
{...props}
|
49 |
/>
|
50 |
))
|
|
|
42 |
<TabsPrimitive.Content
|
43 |
ref={ref}
|
44 |
className={cn(
|
45 |
+
'ring-offset-background focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
46 |
+
'data-[state=inactive]:invisible data-[state=active]:visible',
|
47 |
+
'h-full w-full',
|
48 |
className
|
49 |
)}
|
50 |
+
// Force mounting of inactive tabs to preserve WebGL contexts
|
51 |
+
forceMount
|
52 |
{...props}
|
53 |
/>
|
54 |
))
|
lightrag_webui/src/components/ui/Tooltip.tsx
CHANGED
@@ -10,30 +10,43 @@ const TooltipTrigger = TooltipPrimitive.Trigger
|
|
10 |
|
11 |
const processTooltipContent = (content: string) => {
|
12 |
if (typeof content !== 'string') return content
|
13 |
-
return
|
14 |
-
<
|
15 |
-
{
|
16 |
-
|
17 |
-
|
18 |
-
))
|
19 |
}
|
20 |
|
21 |
const TooltipContent = React.forwardRef<
|
22 |
React.ComponentRef<typeof TooltipPrimitive.Content>,
|
23 |
-
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
)
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
38 |
|
39 |
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
|
10 |
|
11 |
const processTooltipContent = (content: string) => {
|
12 |
if (typeof content !== 'string') return content
|
13 |
+
return (
|
14 |
+
<div className="relative top-0 pt-1 whitespace-pre-wrap break-words">
|
15 |
+
{content}
|
16 |
+
</div>
|
17 |
+
)
|
|
|
18 |
}
|
19 |
|
20 |
const TooltipContent = React.forwardRef<
|
21 |
React.ComponentRef<typeof TooltipPrimitive.Content>,
|
22 |
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
23 |
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
24 |
+
align?: 'start' | 'center' | 'end'
|
25 |
+
}
|
26 |
+
>(({ className, side = 'left', align = 'start', children, ...props }, ref) => {
|
27 |
+
const contentRef = React.useRef<HTMLDivElement>(null);
|
28 |
+
|
29 |
+
React.useEffect(() => {
|
30 |
+
if (contentRef.current) {
|
31 |
+
contentRef.current.scrollTop = 0;
|
32 |
+
}
|
33 |
+
}, [children]);
|
34 |
+
|
35 |
+
return (
|
36 |
+
<TooltipPrimitive.Content
|
37 |
+
ref={ref}
|
38 |
+
side={side}
|
39 |
+
align={align}
|
40 |
+
className={cn(
|
41 |
+
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-[60vh] overflow-y-auto whitespace-pre-wrap break-words rounded-md border px-3 py-2 text-sm shadow-md',
|
42 |
+
className
|
43 |
+
)}
|
44 |
+
{...props}
|
45 |
+
>
|
46 |
+
{typeof children === 'string' ? processTooltipContent(children) : children}
|
47 |
+
</TooltipPrimitive.Content>
|
48 |
+
);
|
49 |
+
})
|
50 |
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
51 |
|
52 |
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
lightrag_webui/src/contexts/TabVisibilityProvider.tsx
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect, useMemo } from 'react';
|
2 |
+
import { TabVisibilityContext } from './context';
|
3 |
+
import { TabVisibilityContextType } from './types';
|
4 |
+
import { useSettingsStore } from '@/stores/settings';
|
5 |
+
|
6 |
+
interface TabVisibilityProviderProps {
|
7 |
+
children: React.ReactNode;
|
8 |
+
}
|
9 |
+
|
10 |
+
/**
|
11 |
+
* Provider component for the TabVisibility context
|
12 |
+
* Manages the visibility state of tabs throughout the application
|
13 |
+
*/
|
14 |
+
export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ children }) => {
|
15 |
+
// Get current tab from settings store
|
16 |
+
const currentTab = useSettingsStore.use.currentTab();
|
17 |
+
|
18 |
+
// Initialize visibility state with current tab as visible
|
19 |
+
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
|
20 |
+
[currentTab]: true
|
21 |
+
}));
|
22 |
+
|
23 |
+
// Update visibility when current tab changes
|
24 |
+
useEffect(() => {
|
25 |
+
setVisibleTabs((prev) => ({
|
26 |
+
...prev,
|
27 |
+
[currentTab]: true
|
28 |
+
}));
|
29 |
+
}, [currentTab]);
|
30 |
+
|
31 |
+
// Create the context value with memoization to prevent unnecessary re-renders
|
32 |
+
const contextValue = useMemo<TabVisibilityContextType>(
|
33 |
+
() => ({
|
34 |
+
visibleTabs,
|
35 |
+
setTabVisibility: (tabId: string, isVisible: boolean) => {
|
36 |
+
setVisibleTabs((prev) => ({
|
37 |
+
...prev,
|
38 |
+
[tabId]: isVisible,
|
39 |
+
}));
|
40 |
+
},
|
41 |
+
isTabVisible: (tabId: string) => !!visibleTabs[tabId],
|
42 |
+
}),
|
43 |
+
[visibleTabs]
|
44 |
+
);
|
45 |
+
|
46 |
+
return (
|
47 |
+
<TabVisibilityContext.Provider value={contextValue}>
|
48 |
+
{children}
|
49 |
+
</TabVisibilityContext.Provider>
|
50 |
+
);
|
51 |
+
};
|
52 |
+
|
53 |
+
export default TabVisibilityProvider;
|
lightrag_webui/src/contexts/context.ts
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createContext } from 'react';
|
2 |
+
import { TabVisibilityContextType } from './types';
|
3 |
+
|
4 |
+
// Default context value
|
5 |
+
const defaultContext: TabVisibilityContextType = {
|
6 |
+
visibleTabs: {},
|
7 |
+
setTabVisibility: () => {},
|
8 |
+
isTabVisible: () => false,
|
9 |
+
};
|
10 |
+
|
11 |
+
// Create the context
|
12 |
+
export const TabVisibilityContext = createContext<TabVisibilityContextType>(defaultContext);
|
lightrag_webui/src/contexts/types.ts
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface TabVisibilityContextType {
|
2 |
+
visibleTabs: Record<string, boolean>;
|
3 |
+
setTabVisibility: (tabId: string, isVisible: boolean) => void;
|
4 |
+
isTabVisible: (tabId: string) => boolean;
|
5 |
+
}
|
lightrag_webui/src/contexts/useTabVisibility.ts
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useContext } from 'react';
|
2 |
+
import { TabVisibilityContext } from './context';
|
3 |
+
import { TabVisibilityContextType } from './types';
|
4 |
+
|
5 |
+
/**
|
6 |
+
* Custom hook to access the tab visibility context
|
7 |
+
* @returns The tab visibility context
|
8 |
+
*/
|
9 |
+
export const useTabVisibility = (): TabVisibilityContextType => {
|
10 |
+
const context = useContext(TabVisibilityContext);
|
11 |
+
|
12 |
+
if (!context) {
|
13 |
+
throw new Error('useTabVisibility must be used within a TabVisibilityProvider');
|
14 |
+
}
|
15 |
+
|
16 |
+
return context;
|
17 |
+
};
|
lightrag_webui/src/features/ApiSite.tsx
CHANGED
@@ -1,5 +1,40 @@
|
|
|
|
|
|
1 |
import { backendBaseUrl } from '@/lib/constants'
|
|
|
2 |
|
3 |
export default function ApiSite() {
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
}
|
|
|
1 |
+
import { useState, useEffect } from 'react'
|
2 |
+
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
3 |
import { backendBaseUrl } from '@/lib/constants'
|
4 |
+
import { useTranslation } from 'react-i18next'
|
5 |
|
6 |
export default function ApiSite() {
|
7 |
+
const { t } = useTranslation()
|
8 |
+
const { isTabVisible } = useTabVisibility()
|
9 |
+
const isApiTabVisible = isTabVisible('api')
|
10 |
+
const [iframeLoaded, setIframeLoaded] = useState(false)
|
11 |
+
|
12 |
+
// Load the iframe once on component mount
|
13 |
+
useEffect(() => {
|
14 |
+
if (!iframeLoaded) {
|
15 |
+
setIframeLoaded(true)
|
16 |
+
}
|
17 |
+
}, [iframeLoaded])
|
18 |
+
|
19 |
+
// Use CSS to hide content when tab is not visible
|
20 |
+
return (
|
21 |
+
<div className={`size-full ${isApiTabVisible ? '' : 'hidden'}`}>
|
22 |
+
{iframeLoaded ? (
|
23 |
+
<iframe
|
24 |
+
src={backendBaseUrl + '/docs'}
|
25 |
+
className="size-full w-full h-full"
|
26 |
+
style={{ width: '100%', height: '100%', border: 'none' }}
|
27 |
+
// Use key to ensure iframe doesn't reload
|
28 |
+
key="api-docs-iframe"
|
29 |
+
/>
|
30 |
+
) : (
|
31 |
+
<div className="flex h-full w-full items-center justify-center bg-background">
|
32 |
+
<div className="text-center">
|
33 |
+
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
34 |
+
<p>{t('apiSite.loading')}</p>
|
35 |
+
</div>
|
36 |
+
</div>
|
37 |
+
)}
|
38 |
+
</div>
|
39 |
+
)
|
40 |
}
|
lightrag_webui/src/features/DocumentManager.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
-
import { useState, useEffect, useCallback } from 'react'
|
2 |
import { useTranslation } from 'react-i18next'
|
|
|
3 |
import Button from '@/components/ui/Button'
|
4 |
import {
|
5 |
Table,
|
@@ -26,6 +27,9 @@ export default function DocumentManager() {
|
|
26 |
const { t } = useTranslation()
|
27 |
const health = useBackendState.use.health()
|
28 |
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
|
|
|
|
|
|
29 |
|
30 |
const fetchDocuments = useCallback(async () => {
|
31 |
try {
|
@@ -48,11 +52,15 @@ export default function DocumentManager() {
|
|
48 |
} catch (err) {
|
49 |
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
|
50 |
}
|
51 |
-
}, [setDocs])
|
52 |
|
|
|
53 |
useEffect(() => {
|
54 |
-
|
55 |
-
|
|
|
|
|
|
|
56 |
|
57 |
const scanDocuments = useCallback(async () => {
|
58 |
try {
|
@@ -61,21 +69,24 @@ export default function DocumentManager() {
|
|
61 |
} catch (err) {
|
62 |
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
|
63 |
}
|
64 |
-
}, [])
|
65 |
|
|
|
66 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
67 |
const interval = setInterval(async () => {
|
68 |
-
if (!health) {
|
69 |
-
return
|
70 |
-
}
|
71 |
try {
|
72 |
await fetchDocuments()
|
73 |
} catch (err) {
|
74 |
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
75 |
}
|
76 |
}, 5000)
|
|
|
77 |
return () => clearInterval(interval)
|
78 |
-
}, [health, fetchDocuments])
|
79 |
|
80 |
return (
|
81 |
<Card className="!size-full !rounded-none !border-none">
|
|
|
1 |
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
2 |
import { useTranslation } from 'react-i18next'
|
3 |
+
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
4 |
import Button from '@/components/ui/Button'
|
5 |
import {
|
6 |
Table,
|
|
|
27 |
const { t } = useTranslation()
|
28 |
const health = useBackendState.use.health()
|
29 |
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
30 |
+
const { isTabVisible } = useTabVisibility()
|
31 |
+
const isDocumentsTabVisible = isTabVisible('documents')
|
32 |
+
const initialLoadRef = useRef(false)
|
33 |
|
34 |
const fetchDocuments = useCallback(async () => {
|
35 |
try {
|
|
|
52 |
} catch (err) {
|
53 |
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
|
54 |
}
|
55 |
+
}, [setDocs, t])
|
56 |
|
57 |
+
// Only fetch documents when the tab becomes visible for the first time
|
58 |
useEffect(() => {
|
59 |
+
if (isDocumentsTabVisible && !initialLoadRef.current) {
|
60 |
+
fetchDocuments()
|
61 |
+
initialLoadRef.current = true
|
62 |
+
}
|
63 |
+
}, [isDocumentsTabVisible, fetchDocuments])
|
64 |
|
65 |
const scanDocuments = useCallback(async () => {
|
66 |
try {
|
|
|
69 |
} catch (err) {
|
70 |
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
|
71 |
}
|
72 |
+
}, [t])
|
73 |
|
74 |
+
// Only set up polling when the tab is visible and health is good
|
75 |
useEffect(() => {
|
76 |
+
if (!isDocumentsTabVisible || !health) {
|
77 |
+
return
|
78 |
+
}
|
79 |
+
|
80 |
const interval = setInterval(async () => {
|
|
|
|
|
|
|
81 |
try {
|
82 |
await fetchDocuments()
|
83 |
} catch (err) {
|
84 |
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
85 |
}
|
86 |
}, 5000)
|
87 |
+
|
88 |
return () => clearInterval(interval)
|
89 |
+
}, [health, fetchDocuments, t, isDocumentsTabVisible])
|
90 |
|
91 |
return (
|
92 |
<Card className="!size-full !rounded-none !border-none">
|
lightrag_webui/src/features/GraphViewer.tsx
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
-
import { useEffect, useState, useCallback, useMemo } from 'react'
|
|
|
2 |
// import { MiniMap } from '@react-sigma/minimap'
|
3 |
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
4 |
import { Settings as SigmaSettings } from 'sigma/settings'
|
@@ -17,6 +18,7 @@ import Settings from '@/components/graph/Settings'
|
|
17 |
import GraphSearch from '@/components/graph/GraphSearch'
|
18 |
import GraphLabels from '@/components/graph/GraphLabels'
|
19 |
import PropertiesView from '@/components/graph/PropertiesView'
|
|
|
20 |
|
21 |
import { useSettingsStore } from '@/stores/settings'
|
22 |
import { useGraphStore } from '@/stores/graph'
|
@@ -90,8 +92,12 @@ const GraphEvents = () => {
|
|
90 |
}
|
91 |
},
|
92 |
// Disable the autoscale at the first down interaction
|
93 |
-
mousedown: () => {
|
94 |
-
if (
|
|
|
|
|
|
|
|
|
95 |
}
|
96 |
})
|
97 |
}, [registerEvents, sigma, draggedNode])
|
@@ -101,27 +107,46 @@ const GraphEvents = () => {
|
|
101 |
|
102 |
const GraphViewer = () => {
|
103 |
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
|
|
|
|
104 |
|
105 |
const selectedNode = useGraphStore.use.selectedNode()
|
106 |
const focusedNode = useGraphStore.use.focusedNode()
|
107 |
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
|
109 |
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
110 |
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
111 |
-
const renderLabels = useSettingsStore.use.showNodeLabel()
|
112 |
-
|
113 |
-
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
114 |
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
115 |
-
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
116 |
|
|
|
117 |
useEffect(() => {
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
|
126 |
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
127 |
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
@@ -142,43 +167,73 @@ const GraphViewer = () => {
|
|
142 |
[selectedNode]
|
143 |
)
|
144 |
|
|
|
|
|
145 |
return (
|
146 |
-
<
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
<
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
</div>
|
176 |
)}
|
177 |
|
178 |
-
{/*
|
179 |
-
|
180 |
-
|
181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
)
|
183 |
}
|
184 |
|
|
|
1 |
+
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
2 |
+
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
3 |
// import { MiniMap } from '@react-sigma/minimap'
|
4 |
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
5 |
import { Settings as SigmaSettings } from 'sigma/settings'
|
|
|
18 |
import GraphSearch from '@/components/graph/GraphSearch'
|
19 |
import GraphLabels from '@/components/graph/GraphLabels'
|
20 |
import PropertiesView from '@/components/graph/PropertiesView'
|
21 |
+
import SettingsDisplay from '@/components/graph/SettingsDisplay'
|
22 |
|
23 |
import { useSettingsStore } from '@/stores/settings'
|
24 |
import { useGraphStore } from '@/stores/graph'
|
|
|
92 |
}
|
93 |
},
|
94 |
// Disable the autoscale at the first down interaction
|
95 |
+
mousedown: (e) => {
|
96 |
+
// Only set custom BBox if it's a drag operation (mouse button is pressed)
|
97 |
+
const mouseEvent = e.original as MouseEvent;
|
98 |
+
if (mouseEvent.buttons !== 0 && !sigma.getCustomBBox()) {
|
99 |
+
sigma.setCustomBBox(sigma.getBBox())
|
100 |
+
}
|
101 |
}
|
102 |
})
|
103 |
}, [registerEvents, sigma, draggedNode])
|
|
|
107 |
|
108 |
const GraphViewer = () => {
|
109 |
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
110 |
+
const sigmaRef = useRef<any>(null)
|
111 |
+
const initAttemptedRef = useRef(false)
|
112 |
|
113 |
const selectedNode = useGraphStore.use.selectedNode()
|
114 |
const focusedNode = useGraphStore.use.focusedNode()
|
115 |
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
116 |
+
const isFetching = useGraphStore.use.isFetching()
|
117 |
+
const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
|
118 |
+
|
119 |
+
// Get tab visibility
|
120 |
+
const { isTabVisible } = useTabVisibility()
|
121 |
+
const isGraphTabVisible = isTabVisible('knowledge-graph')
|
122 |
|
123 |
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
124 |
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
|
|
|
|
|
|
125 |
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
|
|
126 |
|
127 |
+
// Handle component mount/unmount and tab visibility
|
128 |
useEffect(() => {
|
129 |
+
// When component mounts or tab becomes visible
|
130 |
+
if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) {
|
131 |
+
// If tab is visible but graph is not rendering, try to enable rendering
|
132 |
+
useGraphStore.getState().setShouldRender(true)
|
133 |
+
initAttemptedRef.current = true
|
134 |
+
console.log('Graph viewer initialized')
|
135 |
+
}
|
136 |
+
|
137 |
+
// Cleanup function when component unmounts
|
138 |
+
return () => {
|
139 |
+
// Only log cleanup, don't actually clean up the WebGL context
|
140 |
+
// This allows the WebGL context to persist across tab switches
|
141 |
+
console.log('Graph viewer cleanup')
|
142 |
+
}
|
143 |
+
}, [isGraphTabVisible, shouldRender, isFetching])
|
144 |
+
|
145 |
+
// Initialize sigma settings once on component mount
|
146 |
+
// All dynamic settings will be updated in GraphControl using useSetSettings
|
147 |
+
useEffect(() => {
|
148 |
+
setSigmaSettings(defaultSigmaSettings)
|
149 |
+
}, [])
|
150 |
|
151 |
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
152 |
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
|
|
167 |
[selectedNode]
|
168 |
)
|
169 |
|
170 |
+
// Since TabsContent now forces mounting of all tabs, we need to conditionally render
|
171 |
+
// the SigmaContainer based on visibility to avoid unnecessary rendering
|
172 |
return (
|
173 |
+
<div className="relative h-full w-full">
|
174 |
+
{/* Only render the SigmaContainer when the tab is visible */}
|
175 |
+
{isGraphTabVisible ? (
|
176 |
+
<SigmaContainer
|
177 |
+
settings={sigmaSettings}
|
178 |
+
className="!bg-background !size-full overflow-hidden"
|
179 |
+
ref={sigmaRef}
|
180 |
+
>
|
181 |
+
<GraphControl />
|
182 |
+
|
183 |
+
{enableNodeDrag && <GraphEvents />}
|
184 |
+
|
185 |
+
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
186 |
+
|
187 |
+
<div className="absolute top-2 left-2 flex items-start gap-2">
|
188 |
+
<GraphLabels />
|
189 |
+
{showNodeSearchBar && (
|
190 |
+
<GraphSearch
|
191 |
+
value={searchInitSelectedNode}
|
192 |
+
onFocus={onSearchFocus}
|
193 |
+
onChange={onSearchSelect}
|
194 |
+
/>
|
195 |
+
)}
|
196 |
+
</div>
|
197 |
+
|
198 |
+
<div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
|
199 |
+
<Settings />
|
200 |
+
<ZoomControl />
|
201 |
+
<LayoutsControl />
|
202 |
+
<FullScreenControl />
|
203 |
+
{/* <ThemeToggle /> */}
|
204 |
+
</div>
|
205 |
+
|
206 |
+
{showPropertyPanel && (
|
207 |
+
<div className="absolute top-2 right-2">
|
208 |
+
<PropertiesView />
|
209 |
+
</div>
|
210 |
+
)}
|
211 |
+
|
212 |
+
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
|
213 |
+
<MiniMap width="100px" height="100px" />
|
214 |
+
</div> */}
|
215 |
+
|
216 |
+
<SettingsDisplay />
|
217 |
+
</SigmaContainer>
|
218 |
+
) : (
|
219 |
+
// Placeholder when tab is not visible
|
220 |
+
<div className="flex h-full w-full items-center justify-center">
|
221 |
+
<div className="text-center text-muted-foreground">
|
222 |
+
{/* Placeholder content */}
|
223 |
+
</div>
|
224 |
</div>
|
225 |
)}
|
226 |
|
227 |
+
{/* Loading overlay - shown when data is loading */}
|
228 |
+
{isFetching && (
|
229 |
+
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
|
230 |
+
<div className="text-center">
|
231 |
+
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
232 |
+
<p>Loading Graph Data...</p>
|
233 |
+
</div>
|
234 |
+
</div>
|
235 |
+
)}
|
236 |
+
</div>
|
237 |
)
|
238 |
}
|
239 |
|
lightrag_webui/src/features/SiteHeader.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import Button from '@/components/ui/Button'
|
2 |
import { SiteInfo } from '@/lib/constants'
|
3 |
-
import
|
4 |
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
5 |
import { useSettingsStore } from '@/stores/settings'
|
6 |
import { cn } from '@/lib/utils'
|
@@ -67,12 +67,14 @@ export default function SiteHeader() {
|
|
67 |
</div>
|
68 |
|
69 |
<nav className="flex items-center">
|
70 |
-
<
|
71 |
-
<
|
72 |
-
<
|
73 |
-
|
74 |
-
|
75 |
-
|
|
|
|
|
76 |
</nav>
|
77 |
</header>
|
78 |
)
|
|
|
1 |
import Button from '@/components/ui/Button'
|
2 |
import { SiteInfo } from '@/lib/constants'
|
3 |
+
import AppSettings from '@/components/AppSettings'
|
4 |
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
5 |
import { useSettingsStore } from '@/stores/settings'
|
6 |
import { cn } from '@/lib/utils'
|
|
|
67 |
</div>
|
68 |
|
69 |
<nav className="flex items-center">
|
70 |
+
<div className="flex items-center gap-2">
|
71 |
+
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
|
72 |
+
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
73 |
+
<GithubIcon className="size-4" aria-hidden="true" />
|
74 |
+
</a>
|
75 |
+
</Button>
|
76 |
+
<AppSettings />
|
77 |
+
</div>
|
78 |
</nav>
|
79 |
</header>
|
80 |
)
|
lightrag_webui/src/hooks/useLightragGraph.tsx
CHANGED
@@ -1,11 +1,12 @@
|
|
1 |
import Graph, { DirectedGraph } from 'graphology'
|
2 |
-
import { useCallback, useEffect } from 'react'
|
3 |
import { randomColor, errorMessage } from '@/lib/utils'
|
4 |
import * as Constants from '@/lib/constants'
|
5 |
import { useGraphStore, RawGraph } from '@/stores/graph'
|
6 |
import { queryGraphs } from '@/api/lightrag'
|
7 |
import { useBackendState } from '@/stores/state'
|
8 |
import { useSettingsStore } from '@/stores/settings'
|
|
|
9 |
|
10 |
import seedrandom from 'seedrandom'
|
11 |
|
@@ -136,15 +137,23 @@ const fetchGraph = async (label: string, maxDepth: number, minDegree: number) =>
|
|
136 |
return rawGraph
|
137 |
}
|
138 |
|
|
|
139 |
const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|
|
140 |
const graph = new DirectedGraph()
|
141 |
|
|
|
142 |
for (const rawNode of rawGraph?.nodes ?? []) {
|
|
|
|
|
|
|
|
|
|
|
143 |
graph.addNode(rawNode.id, {
|
144 |
label: rawNode.labels.join(', '),
|
145 |
color: rawNode.color,
|
146 |
-
x:
|
147 |
-
y:
|
148 |
size: rawNode.size,
|
149 |
// for node-border
|
150 |
borderColor: Constants.nodeBorderColor,
|
@@ -152,6 +161,7 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|
152 |
})
|
153 |
}
|
154 |
|
|
|
155 |
for (const rawEdge of rawGraph?.edges ?? []) {
|
156 |
rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
|
157 |
label: rawEdge.type || undefined
|
@@ -161,14 +171,30 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|
161 |
return graph
|
162 |
}
|
163 |
|
164 |
-
const lastQueryLabel = { label: '', maxQueryDepth: 0, minDegree: 0 }
|
165 |
-
|
166 |
const useLightrangeGraph = () => {
|
167 |
const queryLabel = useSettingsStore.use.queryLabel()
|
168 |
const rawGraph = useGraphStore.use.rawGraph()
|
169 |
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
170 |
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
171 |
const minDegree = useSettingsStore.use.graphMinDegree()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
|
173 |
const getNode = useCallback(
|
174 |
(nodeId: string) => {
|
@@ -184,35 +210,131 @@ const useLightrangeGraph = () => {
|
|
184 |
[rawGraph]
|
185 |
)
|
186 |
|
|
|
|
|
|
|
|
|
187 |
useEffect(() => {
|
188 |
-
if
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
lastQueryLabel.label = queryLabel
|
193 |
-
lastQueryLabel.maxQueryDepth = maxQueryDepth
|
194 |
-
lastQueryLabel.minDegree = minDegree
|
195 |
|
|
|
|
|
|
|
196 |
const state = useGraphStore.getState()
|
197 |
state.reset()
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
203 |
})
|
204 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
205 |
} else {
|
206 |
-
|
207 |
-
|
208 |
-
state.setSigmaGraph(new DirectedGraph())
|
209 |
}
|
210 |
-
}, [
|
211 |
|
212 |
const lightrageGraph = useCallback(() => {
|
|
|
213 |
if (sigmaGraph) {
|
214 |
return sigmaGraph as Graph<NodeType, EdgeType>
|
215 |
}
|
|
|
|
|
|
|
216 |
const graph = new DirectedGraph()
|
217 |
useGraphStore.getState().setSigmaGraph(graph)
|
218 |
return graph as Graph<NodeType, EdgeType>
|
|
|
1 |
import Graph, { DirectedGraph } from 'graphology'
|
2 |
+
import { useCallback, useEffect, useRef } from 'react'
|
3 |
import { randomColor, errorMessage } from '@/lib/utils'
|
4 |
import * as Constants from '@/lib/constants'
|
5 |
import { useGraphStore, RawGraph } from '@/stores/graph'
|
6 |
import { queryGraphs } from '@/api/lightrag'
|
7 |
import { useBackendState } from '@/stores/state'
|
8 |
import { useSettingsStore } from '@/stores/settings'
|
9 |
+
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
10 |
|
11 |
import seedrandom from 'seedrandom'
|
12 |
|
|
|
137 |
return rawGraph
|
138 |
}
|
139 |
|
140 |
+
// Create a new graph instance with the raw graph data
|
141 |
const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
142 |
+
// Always create a new graph instance
|
143 |
const graph = new DirectedGraph()
|
144 |
|
145 |
+
// Add nodes from raw graph data
|
146 |
for (const rawNode of rawGraph?.nodes ?? []) {
|
147 |
+
// Ensure we have fresh random positions for nodes
|
148 |
+
seedrandom(rawNode.id + Date.now().toString(), { global: true })
|
149 |
+
const x = Math.random()
|
150 |
+
const y = Math.random()
|
151 |
+
|
152 |
graph.addNode(rawNode.id, {
|
153 |
label: rawNode.labels.join(', '),
|
154 |
color: rawNode.color,
|
155 |
+
x: x,
|
156 |
+
y: y,
|
157 |
size: rawNode.size,
|
158 |
// for node-border
|
159 |
borderColor: Constants.nodeBorderColor,
|
|
|
161 |
})
|
162 |
}
|
163 |
|
164 |
+
// Add edges from raw graph data
|
165 |
for (const rawEdge of rawGraph?.edges ?? []) {
|
166 |
rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
|
167 |
label: rawEdge.type || undefined
|
|
|
171 |
return graph
|
172 |
}
|
173 |
|
|
|
|
|
174 |
const useLightrangeGraph = () => {
|
175 |
const queryLabel = useSettingsStore.use.queryLabel()
|
176 |
const rawGraph = useGraphStore.use.rawGraph()
|
177 |
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
178 |
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
179 |
const minDegree = useSettingsStore.use.graphMinDegree()
|
180 |
+
const isFetching = useGraphStore.use.isFetching()
|
181 |
+
|
182 |
+
// Get tab visibility
|
183 |
+
const { isTabVisible } = useTabVisibility()
|
184 |
+
const isGraphTabVisible = isTabVisible('knowledge-graph')
|
185 |
+
|
186 |
+
// Track previous parameters to detect actual changes
|
187 |
+
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
|
188 |
+
|
189 |
+
// Use ref to track if data has been loaded and initial load
|
190 |
+
const dataLoadedRef = useRef(false)
|
191 |
+
const initialLoadRef = useRef(false)
|
192 |
+
|
193 |
+
// Check if parameters have changed
|
194 |
+
const paramsChanged =
|
195 |
+
prevParamsRef.current.queryLabel !== queryLabel ||
|
196 |
+
prevParamsRef.current.maxQueryDepth !== maxQueryDepth ||
|
197 |
+
prevParamsRef.current.minDegree !== minDegree
|
198 |
|
199 |
const getNode = useCallback(
|
200 |
(nodeId: string) => {
|
|
|
210 |
[rawGraph]
|
211 |
)
|
212 |
|
213 |
+
// Track if a fetch is in progress to prevent multiple simultaneous fetches
|
214 |
+
const fetchInProgressRef = useRef(false)
|
215 |
+
|
216 |
+
// Data fetching logic - simplified but preserving TAB visibility check
|
217 |
useEffect(() => {
|
218 |
+
// Skip if fetch is already in progress
|
219 |
+
if (fetchInProgressRef.current) {
|
220 |
+
return
|
221 |
+
}
|
|
|
|
|
|
|
222 |
|
223 |
+
// If there's no query label, reset the graph
|
224 |
+
if (!queryLabel) {
|
225 |
+
if (rawGraph !== null || sigmaGraph !== null) {
|
226 |
const state = useGraphStore.getState()
|
227 |
state.reset()
|
228 |
+
state.setGraphDataFetchAttempted(false)
|
229 |
+
state.setLabelsFetchAttempted(false)
|
230 |
+
}
|
231 |
+
dataLoadedRef.current = false
|
232 |
+
initialLoadRef.current = false
|
233 |
+
return
|
234 |
+
}
|
235 |
+
|
236 |
+
// Check if parameters have changed
|
237 |
+
if (!isFetching && !fetchInProgressRef.current &&
|
238 |
+
(paramsChanged || !useGraphStore.getState().graphDataFetchAttempted)) {
|
239 |
+
|
240 |
+
// Only fetch data if the Graph tab is visible
|
241 |
+
if (!isGraphTabVisible) {
|
242 |
+
console.log('Graph tab not visible, skipping data fetch');
|
243 |
+
return;
|
244 |
+
}
|
245 |
+
|
246 |
+
// Set flags
|
247 |
+
fetchInProgressRef.current = true
|
248 |
+
useGraphStore.getState().setGraphDataFetchAttempted(true)
|
249 |
+
|
250 |
+
const state = useGraphStore.getState()
|
251 |
+
state.setIsFetching(true)
|
252 |
+
state.setShouldRender(false) // Disable rendering during data loading
|
253 |
+
|
254 |
+
// Clear selection and highlighted nodes before fetching new graph
|
255 |
+
state.clearSelection()
|
256 |
+
if (state.sigmaGraph) {
|
257 |
+
state.sigmaGraph.forEachNode((node) => {
|
258 |
+
state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false)
|
259 |
})
|
260 |
}
|
261 |
+
|
262 |
+
// Update parameter reference
|
263 |
+
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree }
|
264 |
+
|
265 |
+
console.log('Fetching graph data...')
|
266 |
+
|
267 |
+
// Use a local copy of the parameters
|
268 |
+
const currentQueryLabel = queryLabel
|
269 |
+
const currentMaxQueryDepth = maxQueryDepth
|
270 |
+
const currentMinDegree = minDegree
|
271 |
+
|
272 |
+
// Fetch graph data
|
273 |
+
fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
|
274 |
+
const state = useGraphStore.getState()
|
275 |
+
|
276 |
+
// Reset state
|
277 |
+
state.reset()
|
278 |
+
|
279 |
+
// Create and set new graph directly
|
280 |
+
const newSigmaGraph = createSigmaGraph(data)
|
281 |
+
data?.buildDynamicMap()
|
282 |
+
|
283 |
+
// Set new graph data
|
284 |
+
state.setSigmaGraph(newSigmaGraph)
|
285 |
+
state.setRawGraph(data)
|
286 |
+
|
287 |
+
// No longer need to extract labels from graph data
|
288 |
+
|
289 |
+
// Update flags
|
290 |
+
dataLoadedRef.current = true
|
291 |
+
initialLoadRef.current = true
|
292 |
+
fetchInProgressRef.current = false
|
293 |
+
|
294 |
+
// Reset camera view
|
295 |
+
state.setMoveToSelectedNode(true)
|
296 |
+
|
297 |
+
// Enable rendering if the tab is visible
|
298 |
+
state.setShouldRender(isGraphTabVisible)
|
299 |
+
state.setIsFetching(false)
|
300 |
+
}).catch((error) => {
|
301 |
+
console.error('Error fetching graph data:', error)
|
302 |
+
|
303 |
+
// Reset state on error
|
304 |
+
const state = useGraphStore.getState()
|
305 |
+
state.setIsFetching(false)
|
306 |
+
state.setShouldRender(isGraphTabVisible)
|
307 |
+
dataLoadedRef.current = false
|
308 |
+
fetchInProgressRef.current = false
|
309 |
+
state.setGraphDataFetchAttempted(false)
|
310 |
+
})
|
311 |
+
}
|
312 |
+
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph])
|
313 |
+
|
314 |
+
// Update rendering state and handle tab visibility changes
|
315 |
+
useEffect(() => {
|
316 |
+
// When tab becomes visible
|
317 |
+
if (isGraphTabVisible) {
|
318 |
+
// If we have data, enable rendering
|
319 |
+
if (rawGraph) {
|
320 |
+
useGraphStore.getState().setShouldRender(true)
|
321 |
+
}
|
322 |
+
|
323 |
+
// We no longer reset the fetch attempted flag here to prevent continuous API calls
|
324 |
} else {
|
325 |
+
// When tab becomes invisible, disable rendering
|
326 |
+
useGraphStore.getState().setShouldRender(false)
|
|
|
327 |
}
|
328 |
+
}, [isGraphTabVisible, rawGraph])
|
329 |
|
330 |
const lightrageGraph = useCallback(() => {
|
331 |
+
// If we already have a graph instance, return it
|
332 |
if (sigmaGraph) {
|
333 |
return sigmaGraph as Graph<NodeType, EdgeType>
|
334 |
}
|
335 |
+
|
336 |
+
// If no graph exists yet, create a new one and store it
|
337 |
+
console.log('Creating new Sigma graph instance')
|
338 |
const graph = new DirectedGraph()
|
339 |
useGraphStore.getState().setSigmaGraph(graph)
|
340 |
return graph as Graph<NodeType, EdgeType>
|
lightrag_webui/src/i18n.js
DELETED
@@ -1,21 +0,0 @@
|
|
1 |
-
import i18n from "i18next";
|
2 |
-
import { initReactI18next } from "react-i18next";
|
3 |
-
|
4 |
-
import en from "./locales/en.json";
|
5 |
-
import zh from "./locales/zh.json";
|
6 |
-
|
7 |
-
i18n
|
8 |
-
.use(initReactI18next)
|
9 |
-
.init({
|
10 |
-
resources: {
|
11 |
-
en: { translation: en },
|
12 |
-
zh: { translation: zh }
|
13 |
-
},
|
14 |
-
lng: "en", // default
|
15 |
-
fallbackLng: "en",
|
16 |
-
interpolation: {
|
17 |
-
escapeValue: false
|
18 |
-
}
|
19 |
-
});
|
20 |
-
|
21 |
-
export default i18n;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lightrag_webui/src/i18n.ts
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import i18n from 'i18next'
|
2 |
+
import { initReactI18next } from 'react-i18next'
|
3 |
+
import { useSettingsStore } from '@/stores/settings'
|
4 |
+
|
5 |
+
import en from './locales/en.json'
|
6 |
+
import zh from './locales/zh.json'
|
7 |
+
|
8 |
+
// Function to sync i18n with store state
|
9 |
+
export const initializeI18n = async (): Promise<typeof i18n> => {
|
10 |
+
// Get initial language from store
|
11 |
+
const initialLanguage = useSettingsStore.getState().language
|
12 |
+
|
13 |
+
// Initialize with store language
|
14 |
+
await i18n.use(initReactI18next).init({
|
15 |
+
resources: {
|
16 |
+
en: { translation: en },
|
17 |
+
zh: { translation: zh }
|
18 |
+
},
|
19 |
+
lng: initialLanguage,
|
20 |
+
fallbackLng: 'en',
|
21 |
+
interpolation: {
|
22 |
+
escapeValue: false
|
23 |
+
}
|
24 |
+
})
|
25 |
+
|
26 |
+
// Subscribe to language changes
|
27 |
+
useSettingsStore.subscribe((state) => {
|
28 |
+
const currentLanguage = state.language
|
29 |
+
if (i18n.language !== currentLanguage) {
|
30 |
+
i18n.changeLanguage(currentLanguage)
|
31 |
+
}
|
32 |
+
})
|
33 |
+
|
34 |
+
return i18n
|
35 |
+
}
|
36 |
+
|
37 |
+
export default i18n
|
lightrag_webui/src/lib/constants.ts
CHANGED
@@ -15,8 +15,8 @@ export const edgeColorDarkTheme = '#969696'
|
|
15 |
export const edgeColorSelected = '#F57F17'
|
16 |
export const edgeColorHighlighted = '#B2EBF2'
|
17 |
|
18 |
-
export const searchResultLimit =
|
19 |
-
export const labelListLimit =
|
20 |
|
21 |
export const minNodeSize = 4
|
22 |
export const maxNodeSize = 20
|
|
|
15 |
export const edgeColorSelected = '#F57F17'
|
16 |
export const edgeColorHighlighted = '#B2EBF2'
|
17 |
|
18 |
+
export const searchResultLimit = 50
|
19 |
+
export const labelListLimit = 100
|
20 |
|
21 |
export const minNodeSize = 4
|
22 |
export const maxNodeSize = 20
|
lightrag_webui/src/locales/en.json
CHANGED
@@ -1,4 +1,11 @@
|
|
1 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
"header": {
|
3 |
"documents": "Documents",
|
4 |
"knowledgeGraph": "Knowledge Graph",
|
@@ -79,9 +86,12 @@
|
|
79 |
"maxQueryDepth": "Max Query Depth",
|
80 |
"minDegree": "Minimum Degree",
|
81 |
"maxLayoutIterations": "Max Layout Iterations",
|
|
|
|
|
82 |
"apiKey": "API Key",
|
83 |
"enterYourAPIkey": "Enter your API key",
|
84 |
-
"save": "Save"
|
|
|
85 |
},
|
86 |
|
87 |
"zoomControl": {
|
@@ -140,7 +150,14 @@
|
|
140 |
"labels": "Labels",
|
141 |
"degree": "Degree",
|
142 |
"properties": "Properties",
|
143 |
-
"relationships": "Relationships"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
144 |
},
|
145 |
"edge": {
|
146 |
"title": "Relationship",
|
@@ -230,5 +247,8 @@
|
|
230 |
"streamResponse": "Stream Response",
|
231 |
"streamResponseTooltip": "If True, enables streaming output for real-time responses"
|
232 |
}
|
|
|
|
|
|
|
233 |
}
|
234 |
}
|
|
|
1 |
{
|
2 |
+
"settings": {
|
3 |
+
"language": "Language",
|
4 |
+
"theme": "Theme",
|
5 |
+
"light": "Light",
|
6 |
+
"dark": "Dark",
|
7 |
+
"system": "System"
|
8 |
+
},
|
9 |
"header": {
|
10 |
"documents": "Documents",
|
11 |
"knowledgeGraph": "Knowledge Graph",
|
|
|
86 |
"maxQueryDepth": "Max Query Depth",
|
87 |
"minDegree": "Minimum Degree",
|
88 |
"maxLayoutIterations": "Max Layout Iterations",
|
89 |
+
"depth": "Depth",
|
90 |
+
"degree": "Degree",
|
91 |
"apiKey": "API Key",
|
92 |
"enterYourAPIkey": "Enter your API key",
|
93 |
+
"save": "Save",
|
94 |
+
"refreshLayout": "Refresh Layout"
|
95 |
},
|
96 |
|
97 |
"zoomControl": {
|
|
|
150 |
"labels": "Labels",
|
151 |
"degree": "Degree",
|
152 |
"properties": "Properties",
|
153 |
+
"relationships": "Relationships",
|
154 |
+
"propertyNames": {
|
155 |
+
"description": "Description",
|
156 |
+
"entity_id": "Name",
|
157 |
+
"entity_type": "Type",
|
158 |
+
"source_id": "SrcID",
|
159 |
+
"Neighbour": "Neigh"
|
160 |
+
}
|
161 |
},
|
162 |
"edge": {
|
163 |
"title": "Relationship",
|
|
|
247 |
"streamResponse": "Stream Response",
|
248 |
"streamResponseTooltip": "If True, enables streaming output for real-time responses"
|
249 |
}
|
250 |
+
},
|
251 |
+
"apiSite": {
|
252 |
+
"loading": "Loading API Documentation..."
|
253 |
}
|
254 |
}
|
lightrag_webui/src/locales/zh.json
CHANGED
@@ -1,4 +1,11 @@
|
|
1 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
"header": {
|
3 |
"documents": "文档",
|
4 |
"knowledgeGraph": "知识图谱",
|
@@ -6,41 +13,41 @@
|
|
6 |
"api": "API",
|
7 |
"projectRepository": "项目仓库",
|
8 |
"themeToggle": {
|
9 |
-
"switchToLight": "
|
10 |
-
"switchToDark": "
|
11 |
}
|
12 |
},
|
13 |
"documentPanel": {
|
14 |
"clearDocuments": {
|
15 |
-
"button": "
|
16 |
-
"tooltip": "
|
17 |
-
"title": "
|
18 |
-
"confirm": "
|
19 |
"confirmButton": "确定",
|
20 |
-
"success": "
|
21 |
-
"failed": "
|
22 |
-
"error": "
|
23 |
},
|
24 |
"uploadDocuments": {
|
25 |
"button": "上传",
|
26 |
"tooltip": "上传文档",
|
27 |
"title": "上传文档",
|
28 |
-
"description": "
|
29 |
-
"uploading": "正在上传 {{name}}
|
30 |
-
"success": "
|
31 |
-
"failed": "
|
32 |
-
"error": "
|
33 |
"generalError": "上传失败\n{{error}}",
|
34 |
-
"fileTypes": "
|
35 |
},
|
36 |
"documentManager": {
|
37 |
"title": "文档管理",
|
38 |
"scanButton": "扫描",
|
39 |
"scanTooltip": "扫描文档",
|
40 |
"uploadedTitle": "已上传文档",
|
41 |
-
"uploadedDescription": "
|
42 |
-
"emptyTitle": "
|
43 |
-
"emptyDescription": "
|
44 |
"columns": {
|
45 |
"id": "ID",
|
46 |
"summary": "摘要",
|
@@ -54,7 +61,7 @@
|
|
54 |
"status": {
|
55 |
"completed": "已完成",
|
56 |
"processing": "处理中",
|
57 |
-
"pending": "
|
58 |
"failed": "失败"
|
59 |
},
|
60 |
"errors": {
|
@@ -74,39 +81,39 @@
|
|
74 |
"showNodeLabel": "显示节点标签",
|
75 |
"nodeDraggable": "节点可拖动",
|
76 |
"showEdgeLabel": "显示边标签",
|
77 |
-
"hideUnselectedEdges": "
|
78 |
"edgeEvents": "边事件",
|
79 |
"maxQueryDepth": "最大查询深度",
|
80 |
"minDegree": "最小度数",
|
81 |
"maxLayoutIterations": "最大布局迭代次数",
|
82 |
-
"
|
83 |
-
"
|
84 |
-
"
|
|
|
|
|
|
|
85 |
},
|
86 |
-
|
87 |
"zoomControl": {
|
88 |
"zoomIn": "放大",
|
89 |
"zoomOut": "缩小",
|
90 |
"resetZoom": "重置缩放"
|
91 |
},
|
92 |
-
|
93 |
"layoutsControl": {
|
94 |
"startAnimation": "开始布局动画",
|
95 |
"stopAnimation": "停止布局动画",
|
96 |
-
"layoutGraph": "
|
97 |
"layouts": {
|
98 |
-
"Circular": "
|
99 |
-
"Circlepack": "
|
100 |
-
"Random": "
|
101 |
-
"Noverlaps": "
|
102 |
-
"Force Directed": "
|
103 |
-
"Force Atlas": "
|
104 |
}
|
105 |
},
|
106 |
-
|
107 |
"fullScreenControl": {
|
108 |
"fullScreen": "全屏",
|
109 |
-
"windowed": "
|
110 |
}
|
111 |
},
|
112 |
"statusIndicator": {
|
@@ -118,17 +125,17 @@
|
|
118 |
"storageInfo": "存储信息",
|
119 |
"workingDirectory": "工作目录",
|
120 |
"inputDirectory": "输入目录",
|
121 |
-
"llmConfig": "LLM
|
122 |
-
"llmBinding": "LLM
|
123 |
-
"llmBindingHost": "LLM
|
124 |
-
"llmModel": "LLM
|
125 |
-
"maxTokens": "
|
126 |
"embeddingConfig": "嵌入配置",
|
127 |
"embeddingBinding": "嵌入绑定",
|
128 |
"embeddingBindingHost": "嵌入绑定主机",
|
129 |
"embeddingModel": "嵌入模型",
|
130 |
"storageConfig": "存储配置",
|
131 |
-
"kvStorage": "KV
|
132 |
"docStatusStorage": "文档状态存储",
|
133 |
"graphStorage": "图存储",
|
134 |
"vectorStorage": "向量存储"
|
@@ -140,96 +147,93 @@
|
|
140 |
"labels": "标签",
|
141 |
"degree": "度数",
|
142 |
"properties": "属性",
|
143 |
-
"relationships": "关系"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
144 |
},
|
145 |
"edge": {
|
146 |
"title": "关系",
|
147 |
"id": "ID",
|
148 |
"type": "类型",
|
149 |
-
"source": "
|
150 |
-
"target": "
|
151 |
"properties": "属性"
|
152 |
}
|
153 |
},
|
154 |
"search": {
|
155 |
"placeholder": "搜索节点...",
|
156 |
-
"message": "
|
157 |
},
|
158 |
"graphLabels": {
|
159 |
"selectTooltip": "选择查询标签",
|
160 |
"noLabels": "未找到标签",
|
161 |
"label": "标签",
|
162 |
"placeholder": "搜索标签...",
|
163 |
-
"andOthers": "
|
164 |
}
|
165 |
},
|
166 |
"retrievePanel": {
|
167 |
"chatMessage": {
|
168 |
"copyTooltip": "复制到剪贴板",
|
169 |
-
"copyError": "
|
170 |
},
|
171 |
-
|
172 |
"retrieval": {
|
173 |
-
"startPrompt": "
|
174 |
-
"clear": "
|
175 |
"send": "发送",
|
176 |
-
"placeholder": "
|
177 |
-
"error": "
|
178 |
},
|
179 |
"querySettings": {
|
180 |
-
"parametersTitle": "
|
181 |
"parametersDescription": "配置查询参数",
|
182 |
-
|
183 |
"queryMode": "查询模式",
|
184 |
-
"queryModeTooltip": "选择检索策略:\n•
|
185 |
"queryModeOptions": {
|
186 |
"naive": "朴素",
|
187 |
"local": "本地",
|
188 |
"global": "全局",
|
189 |
"hybrid": "混合",
|
190 |
-
"mix": "
|
191 |
},
|
192 |
-
|
193 |
"responseFormat": "响应格式",
|
194 |
-
"responseFormatTooltip": "定义响应格式。例如:\n•
|
195 |
"responseFormatOptions": {
|
196 |
-
"multipleParagraphs": "
|
197 |
-
"singleParagraph": "
|
198 |
-
"bulletPoints": "
|
199 |
},
|
200 |
-
|
201 |
-
"
|
202 |
-
"
|
203 |
-
"
|
204 |
-
|
205 |
-
"
|
206 |
-
"
|
207 |
-
|
208 |
-
"
|
209 |
-
"maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数",
|
210 |
-
|
211 |
-
"maxTokensLocalContext": "本地上下文最大 Token 数",
|
212 |
-
"maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数",
|
213 |
-
|
214 |
"historyTurns": "历史轮次",
|
215 |
-
"historyTurnsTooltip": "
|
216 |
-
"historyTurnsPlaceholder": "
|
217 |
-
|
218 |
"hlKeywords": "高级关键词",
|
219 |
-
"hlKeywordsTooltip": "
|
220 |
"hlkeywordsPlaceHolder": "输入关键词",
|
221 |
-
|
222 |
"llKeywords": "低级关键词",
|
223 |
-
"llKeywordsTooltip": "
|
224 |
-
|
225 |
-
"
|
226 |
-
"
|
227 |
-
|
228 |
-
"onlyNeedPrompt": "仅需要提示",
|
229 |
-
"onlyNeedPromptTooltip": "如果为 True,则仅返回生成的提示,而不会生成回复",
|
230 |
-
|
231 |
"streamResponse": "流式响应",
|
232 |
-
"streamResponseTooltip": "如果为
|
233 |
}
|
|
|
|
|
|
|
234 |
}
|
235 |
}
|
|
|
1 |
{
|
2 |
+
"settings": {
|
3 |
+
"language": "语言",
|
4 |
+
"theme": "主题",
|
5 |
+
"light": "浅色",
|
6 |
+
"dark": "深色",
|
7 |
+
"system": "系统"
|
8 |
+
},
|
9 |
"header": {
|
10 |
"documents": "文档",
|
11 |
"knowledgeGraph": "知识图谱",
|
|
|
13 |
"api": "API",
|
14 |
"projectRepository": "项目仓库",
|
15 |
"themeToggle": {
|
16 |
+
"switchToLight": "切换到浅色主题",
|
17 |
+
"switchToDark": "切换到深色主题"
|
18 |
}
|
19 |
},
|
20 |
"documentPanel": {
|
21 |
"clearDocuments": {
|
22 |
+
"button": "清空",
|
23 |
+
"tooltip": "清空文档",
|
24 |
+
"title": "清空文档",
|
25 |
+
"confirm": "确定要清空所有文档吗?",
|
26 |
"confirmButton": "确定",
|
27 |
+
"success": "文档清空成功",
|
28 |
+
"failed": "清空文档失败:\n{{message}}",
|
29 |
+
"error": "清空文档失败:\n{{error}}"
|
30 |
},
|
31 |
"uploadDocuments": {
|
32 |
"button": "上传",
|
33 |
"tooltip": "上传文档",
|
34 |
"title": "上传文档",
|
35 |
+
"description": "拖拽文件到此处或点击浏览",
|
36 |
+
"uploading": "正在上传 {{name}}:{{percent}}%",
|
37 |
+
"success": "上传成功:\n{{name}} 上传完成",
|
38 |
+
"failed": "上传失败:\n{{name}}\n{{message}}",
|
39 |
+
"error": "上传失败:\n{{name}}\n{{error}}",
|
40 |
"generalError": "上传失败\n{{error}}",
|
41 |
+
"fileTypes": "支持的文件类型:TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
42 |
},
|
43 |
"documentManager": {
|
44 |
"title": "文档管理",
|
45 |
"scanButton": "扫描",
|
46 |
"scanTooltip": "扫描文档",
|
47 |
"uploadedTitle": "已上传文档",
|
48 |
+
"uploadedDescription": "已上传文档列表及其状态",
|
49 |
+
"emptyTitle": "无文档",
|
50 |
+
"emptyDescription": "还没有上传任何文档",
|
51 |
"columns": {
|
52 |
"id": "ID",
|
53 |
"summary": "摘要",
|
|
|
61 |
"status": {
|
62 |
"completed": "已完成",
|
63 |
"processing": "处理中",
|
64 |
+
"pending": "等待中",
|
65 |
"failed": "失败"
|
66 |
},
|
67 |
"errors": {
|
|
|
81 |
"showNodeLabel": "显示节点标签",
|
82 |
"nodeDraggable": "节点可拖动",
|
83 |
"showEdgeLabel": "显示边标签",
|
84 |
+
"hideUnselectedEdges": "隐藏未选中的边",
|
85 |
"edgeEvents": "边事件",
|
86 |
"maxQueryDepth": "最大查询深度",
|
87 |
"minDegree": "最小度数",
|
88 |
"maxLayoutIterations": "最大布局迭代次数",
|
89 |
+
"depth": "深度",
|
90 |
+
"degree": "邻边",
|
91 |
+
"apiKey": "API密钥",
|
92 |
+
"enterYourAPIkey": "输入您的API密钥",
|
93 |
+
"save": "保存",
|
94 |
+
"refreshLayout": "刷新布局"
|
95 |
},
|
|
|
96 |
"zoomControl": {
|
97 |
"zoomIn": "放大",
|
98 |
"zoomOut": "缩小",
|
99 |
"resetZoom": "重置缩放"
|
100 |
},
|
|
|
101 |
"layoutsControl": {
|
102 |
"startAnimation": "开始布局动画",
|
103 |
"stopAnimation": "停止布局动画",
|
104 |
+
"layoutGraph": "图布局",
|
105 |
"layouts": {
|
106 |
+
"Circular": "环形",
|
107 |
+
"Circlepack": "圆形打包",
|
108 |
+
"Random": "随机",
|
109 |
+
"Noverlaps": "无重叠",
|
110 |
+
"Force Directed": "力导向",
|
111 |
+
"Force Atlas": "力图"
|
112 |
}
|
113 |
},
|
|
|
114 |
"fullScreenControl": {
|
115 |
"fullScreen": "全屏",
|
116 |
+
"windowed": "窗口"
|
117 |
}
|
118 |
},
|
119 |
"statusIndicator": {
|
|
|
125 |
"storageInfo": "存储信息",
|
126 |
"workingDirectory": "工作目录",
|
127 |
"inputDirectory": "输入目录",
|
128 |
+
"llmConfig": "LLM配置",
|
129 |
+
"llmBinding": "LLM绑定",
|
130 |
+
"llmBindingHost": "LLM绑定主机",
|
131 |
+
"llmModel": "LLM模型",
|
132 |
+
"maxTokens": "最大令牌数",
|
133 |
"embeddingConfig": "嵌入配置",
|
134 |
"embeddingBinding": "嵌入绑定",
|
135 |
"embeddingBindingHost": "嵌入绑定主机",
|
136 |
"embeddingModel": "嵌入模型",
|
137 |
"storageConfig": "存储配置",
|
138 |
+
"kvStorage": "KV存储",
|
139 |
"docStatusStorage": "文档状态存储",
|
140 |
"graphStorage": "图存储",
|
141 |
"vectorStorage": "向量存储"
|
|
|
147 |
"labels": "标签",
|
148 |
"degree": "度数",
|
149 |
"properties": "属性",
|
150 |
+
"relationships": "关系",
|
151 |
+
"propertyNames": {
|
152 |
+
"description": "描述",
|
153 |
+
"entity_id": "名称",
|
154 |
+
"entity_type": "类型",
|
155 |
+
"source_id": "信源ID",
|
156 |
+
"Neighbour": "邻接"
|
157 |
+
}
|
158 |
},
|
159 |
"edge": {
|
160 |
"title": "关系",
|
161 |
"id": "ID",
|
162 |
"type": "类型",
|
163 |
+
"source": "源节点",
|
164 |
+
"target": "目标节点",
|
165 |
"properties": "属性"
|
166 |
}
|
167 |
},
|
168 |
"search": {
|
169 |
"placeholder": "搜索节点...",
|
170 |
+
"message": "还有 {count} 个"
|
171 |
},
|
172 |
"graphLabels": {
|
173 |
"selectTooltip": "选择查询标签",
|
174 |
"noLabels": "未找到标签",
|
175 |
"label": "标签",
|
176 |
"placeholder": "搜索标签...",
|
177 |
+
"andOthers": "还有 {count} 个"
|
178 |
}
|
179 |
},
|
180 |
"retrievePanel": {
|
181 |
"chatMessage": {
|
182 |
"copyTooltip": "复制到剪贴板",
|
183 |
+
"copyError": "复制文本到剪贴板失败"
|
184 |
},
|
|
|
185 |
"retrieval": {
|
186 |
+
"startPrompt": "输入查询开始检索",
|
187 |
+
"clear": "清空",
|
188 |
"send": "发送",
|
189 |
+
"placeholder": "输入查询...",
|
190 |
+
"error": "错误:获取响应失败"
|
191 |
},
|
192 |
"querySettings": {
|
193 |
+
"parametersTitle": "参数",
|
194 |
"parametersDescription": "配置查询参数",
|
|
|
195 |
"queryMode": "查询模式",
|
196 |
+
"queryModeTooltip": "选择检索策略:\n• Naive:基础搜索,无高级技术\n• Local:上下文相关信息检索\n• Global:利用全局知识库\n• Hybrid:结合本地和全局检索\n• Mix:整合知识图谱和向量检索",
|
197 |
"queryModeOptions": {
|
198 |
"naive": "朴素",
|
199 |
"local": "本地",
|
200 |
"global": "全局",
|
201 |
"hybrid": "混合",
|
202 |
+
"mix": "混合"
|
203 |
},
|
|
|
204 |
"responseFormat": "响应格式",
|
205 |
+
"responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 要点",
|
206 |
"responseFormatOptions": {
|
207 |
+
"multipleParagraphs": "多段落",
|
208 |
+
"singleParagraph": "单段落",
|
209 |
+
"bulletPoints": "要点"
|
210 |
},
|
211 |
+
"topK": "Top K结果",
|
212 |
+
"topKTooltip": "检索的顶部项目数。在'local'模式下表示实体,在'global'模式下表示关系",
|
213 |
+
"topKPlaceholder": "结果数量",
|
214 |
+
"maxTokensTextUnit": "文本单元最大令牌数",
|
215 |
+
"maxTokensTextUnitTooltip": "每个检索文本块允许的最大令牌���",
|
216 |
+
"maxTokensGlobalContext": "全局上下文最大令牌数",
|
217 |
+
"maxTokensGlobalContextTooltip": "全局检索中关系描述的最大令牌数",
|
218 |
+
"maxTokensLocalContext": "本地上下文最大令牌数",
|
219 |
+
"maxTokensLocalContextTooltip": "本地检索中实体描述的最大令牌数",
|
|
|
|
|
|
|
|
|
|
|
220 |
"historyTurns": "历史轮次",
|
221 |
+
"historyTurnsTooltip": "响应上下文中考虑的完整对话轮次(用户-助手对)数量",
|
222 |
+
"historyTurnsPlaceholder": "历史轮次数",
|
|
|
223 |
"hlKeywords": "高级关键词",
|
224 |
+
"hlKeywordsTooltip": "检索中优先考虑的高级关键词列表。用逗号分隔",
|
225 |
"hlkeywordsPlaceHolder": "输入关键词",
|
|
|
226 |
"llKeywords": "低级关键词",
|
227 |
+
"llKeywordsTooltip": "用于细化检索重点的低级关键词列表。用逗号分隔",
|
228 |
+
"onlyNeedContext": "仅需上下文",
|
229 |
+
"onlyNeedContextTooltip": "如果为True,仅返回检索到的上下文而不生成响应",
|
230 |
+
"onlyNeedPrompt": "仅需提示",
|
231 |
+
"onlyNeedPromptTooltip": "如果为True,仅返回生成的提示而不产生响应",
|
|
|
|
|
|
|
232 |
"streamResponse": "流式响应",
|
233 |
+
"streamResponseTooltip": "如果为True,启用实时流式输出响应"
|
234 |
}
|
235 |
+
},
|
236 |
+
"apiSite": {
|
237 |
+
"loading": "正在加载 API 文档..."
|
238 |
}
|
239 |
}
|
lightrag_webui/src/main.tsx
CHANGED
@@ -1,12 +1,5 @@
|
|
1 |
-
import { StrictMode } from 'react'
|
2 |
import { createRoot } from 'react-dom/client'
|
3 |
import './index.css'
|
4 |
-
import
|
5 |
-
import "./i18n";
|
6 |
|
7 |
-
|
8 |
-
createRoot(document.getElementById('root')!).render(
|
9 |
-
<StrictMode>
|
10 |
-
<App />
|
11 |
-
</StrictMode>
|
12 |
-
)
|
|
|
|
|
1 |
import { createRoot } from 'react-dom/client'
|
2 |
import './index.css'
|
3 |
+
import { Root } from '@/components/Root'
|
|
|
4 |
|
5 |
+
createRoot(document.getElementById('root')!).render(<Root />)
|
|
|
|
|
|
|
|
|
|
lightrag_webui/src/stores/graph.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import { create } from 'zustand'
|
2 |
import { createSelectors } from '@/lib/utils'
|
3 |
import { DirectedGraph } from 'graphology'
|
|
|
4 |
|
5 |
export type RawNodeType = {
|
6 |
id: string
|
@@ -65,9 +66,17 @@ interface GraphState {
|
|
65 |
|
66 |
rawGraph: RawGraph | null
|
67 |
sigmaGraph: DirectedGraph | null
|
|
|
68 |
|
69 |
moveToSelectedNode: boolean
|
|
|
|
|
70 |
|
|
|
|
|
|
|
|
|
|
|
71 |
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
72 |
setFocusedNode: (nodeId: string | null) => void
|
73 |
setSelectedEdge: (edgeId: string | null) => void
|
@@ -79,19 +88,47 @@ interface GraphState {
|
|
79 |
|
80 |
setRawGraph: (rawGraph: RawGraph | null) => void
|
81 |
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
}
|
83 |
|
84 |
-
const useGraphStoreBase = create<GraphState>()((set) => ({
|
85 |
selectedNode: null,
|
86 |
focusedNode: null,
|
87 |
selectedEdge: null,
|
88 |
focusedEdge: null,
|
89 |
|
90 |
moveToSelectedNode: false,
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
rawGraph: null,
|
93 |
sigmaGraph: null,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
|
|
|
|
|
95 |
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
96 |
set({ selectedNode: nodeId, moveToSelectedNode }),
|
97 |
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
@@ -104,25 +141,58 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|
104 |
selectedEdge: null,
|
105 |
focusedEdge: null
|
106 |
}),
|
107 |
-
reset: () =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
set({
|
109 |
selectedNode: null,
|
110 |
focusedNode: null,
|
111 |
selectedEdge: null,
|
112 |
focusedEdge: null,
|
113 |
rawGraph: null,
|
114 |
-
|
115 |
-
moveToSelectedNode: false
|
116 |
-
|
|
|
|
|
117 |
|
118 |
setRawGraph: (rawGraph: RawGraph | null) =>
|
119 |
set({
|
120 |
rawGraph
|
121 |
}),
|
122 |
|
123 |
-
setSigmaGraph: (sigmaGraph: DirectedGraph | null) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
|
125 |
-
|
|
|
|
|
126 |
}))
|
127 |
|
128 |
const useGraphStore = createSelectors(useGraphStoreBase)
|
|
|
1 |
import { create } from 'zustand'
|
2 |
import { createSelectors } from '@/lib/utils'
|
3 |
import { DirectedGraph } from 'graphology'
|
4 |
+
import { getGraphLabels } from '@/api/lightrag'
|
5 |
|
6 |
export type RawNodeType = {
|
7 |
id: string
|
|
|
66 |
|
67 |
rawGraph: RawGraph | null
|
68 |
sigmaGraph: DirectedGraph | null
|
69 |
+
allDatabaseLabels: string[]
|
70 |
|
71 |
moveToSelectedNode: boolean
|
72 |
+
isFetching: boolean
|
73 |
+
shouldRender: boolean
|
74 |
|
75 |
+
// Global flags to track data fetching attempts
|
76 |
+
graphDataFetchAttempted: boolean
|
77 |
+
labelsFetchAttempted: boolean
|
78 |
+
|
79 |
+
refreshLayout: () => void
|
80 |
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
81 |
setFocusedNode: (nodeId: string | null) => void
|
82 |
setSelectedEdge: (edgeId: string | null) => void
|
|
|
88 |
|
89 |
setRawGraph: (rawGraph: RawGraph | null) => void
|
90 |
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
91 |
+
setAllDatabaseLabels: (labels: string[]) => void
|
92 |
+
fetchAllDatabaseLabels: () => Promise<void>
|
93 |
+
setIsFetching: (isFetching: boolean) => void
|
94 |
+
setShouldRender: (shouldRender: boolean) => void
|
95 |
+
|
96 |
+
// Methods to set global flags
|
97 |
+
setGraphDataFetchAttempted: (attempted: boolean) => void
|
98 |
+
setLabelsFetchAttempted: (attempted: boolean) => void
|
99 |
}
|
100 |
|
101 |
+
const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
102 |
selectedNode: null,
|
103 |
focusedNode: null,
|
104 |
selectedEdge: null,
|
105 |
focusedEdge: null,
|
106 |
|
107 |
moveToSelectedNode: false,
|
108 |
+
isFetching: false,
|
109 |
+
shouldRender: false,
|
110 |
+
|
111 |
+
// Initialize global flags
|
112 |
+
graphDataFetchAttempted: false,
|
113 |
+
labelsFetchAttempted: false,
|
114 |
|
115 |
rawGraph: null,
|
116 |
sigmaGraph: null,
|
117 |
+
allDatabaseLabels: ['*'],
|
118 |
+
|
119 |
+
refreshLayout: () => {
|
120 |
+
const currentGraph = get().sigmaGraph;
|
121 |
+
if (currentGraph) {
|
122 |
+
get().clearSelection();
|
123 |
+
get().setSigmaGraph(null);
|
124 |
+
setTimeout(() => {
|
125 |
+
get().setSigmaGraph(currentGraph);
|
126 |
+
}, 10);
|
127 |
+
}
|
128 |
+
},
|
129 |
|
130 |
+
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
131 |
+
setShouldRender: (shouldRender: boolean) => set({ shouldRender }),
|
132 |
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
133 |
set({ selectedNode: nodeId, moveToSelectedNode }),
|
134 |
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
|
|
141 |
selectedEdge: null,
|
142 |
focusedEdge: null
|
143 |
}),
|
144 |
+
reset: () => {
|
145 |
+
// Get the existing graph
|
146 |
+
const existingGraph = get().sigmaGraph;
|
147 |
+
|
148 |
+
// If we have an existing graph, clear it by removing all nodes
|
149 |
+
if (existingGraph) {
|
150 |
+
const nodes = Array.from(existingGraph.nodes());
|
151 |
+
nodes.forEach(node => existingGraph.dropNode(node));
|
152 |
+
}
|
153 |
+
|
154 |
set({
|
155 |
selectedNode: null,
|
156 |
focusedNode: null,
|
157 |
selectedEdge: null,
|
158 |
focusedEdge: null,
|
159 |
rawGraph: null,
|
160 |
+
// Keep the existing graph instance but with cleared data
|
161 |
+
moveToSelectedNode: false,
|
162 |
+
shouldRender: false
|
163 |
+
});
|
164 |
+
},
|
165 |
|
166 |
setRawGraph: (rawGraph: RawGraph | null) =>
|
167 |
set({
|
168 |
rawGraph
|
169 |
}),
|
170 |
|
171 |
+
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => {
|
172 |
+
// Replace graph instance, no need to keep WebGL context
|
173 |
+
set({ sigmaGraph });
|
174 |
+
},
|
175 |
+
|
176 |
+
setAllDatabaseLabels: (labels: string[]) => set({ allDatabaseLabels: labels }),
|
177 |
+
|
178 |
+
fetchAllDatabaseLabels: async () => {
|
179 |
+
try {
|
180 |
+
console.log('Fetching all database labels...');
|
181 |
+
const labels = await getGraphLabels();
|
182 |
+
set({ allDatabaseLabels: ['*', ...labels] });
|
183 |
+
return;
|
184 |
+
} catch (error) {
|
185 |
+
console.error('Failed to fetch all database labels:', error);
|
186 |
+
set({ allDatabaseLabels: ['*'] });
|
187 |
+
throw error;
|
188 |
+
}
|
189 |
+
},
|
190 |
+
|
191 |
+
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),
|
192 |
|
193 |
+
// Methods to set global flags
|
194 |
+
setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
|
195 |
+
setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted })
|
196 |
}))
|
197 |
|
198 |
const useGraphStore = createSelectors(useGraphStoreBase)
|
lightrag_webui/src/stores/settings.ts
CHANGED
@@ -5,6 +5,7 @@ import { defaultQueryLabel } from '@/lib/constants'
|
|
5 |
import { Message, QueryRequest } from '@/api/lightrag'
|
6 |
|
7 |
type Theme = 'dark' | 'light' | 'system'
|
|
|
8 |
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
9 |
|
10 |
interface SettingsState {
|
@@ -46,6 +47,9 @@ interface SettingsState {
|
|
46 |
theme: Theme
|
47 |
setTheme: (theme: Theme) => void
|
48 |
|
|
|
|
|
|
|
49 |
enableHealthCheck: boolean
|
50 |
setEnableHealthCheck: (enable: boolean) => void
|
51 |
|
@@ -57,7 +61,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
57 |
persist(
|
58 |
(set) => ({
|
59 |
theme: 'system',
|
60 |
-
|
61 |
showPropertyPanel: true,
|
62 |
showNodeSearchBar: true,
|
63 |
|
@@ -70,7 +74,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
70 |
|
71 |
graphQueryMaxDepth: 3,
|
72 |
graphMinDegree: 0,
|
73 |
-
graphLayoutMaxIterations:
|
74 |
|
75 |
queryLabel: defaultQueryLabel,
|
76 |
|
@@ -99,6 +103,16 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
99 |
|
100 |
setTheme: (theme: Theme) => set({ theme }),
|
101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
setGraphLayoutMaxIterations: (iterations: number) =>
|
103 |
set({
|
104 |
graphLayoutMaxIterations: iterations
|
@@ -129,7 +143,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
129 |
{
|
130 |
name: 'settings-storage',
|
131 |
storage: createJSONStorage(() => localStorage),
|
132 |
-
version:
|
133 |
migrate: (state: any, version: number) => {
|
134 |
if (version < 2) {
|
135 |
state.showEdgeLabel = false
|
@@ -166,7 +180,11 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
166 |
}
|
167 |
if (version < 7) {
|
168 |
state.graphQueryMaxDepth = 3
|
169 |
-
state.graphLayoutMaxIterations =
|
|
|
|
|
|
|
|
|
170 |
}
|
171 |
return state
|
172 |
}
|
|
|
5 |
import { Message, QueryRequest } from '@/api/lightrag'
|
6 |
|
7 |
type Theme = 'dark' | 'light' | 'system'
|
8 |
+
type Language = 'en' | 'zh'
|
9 |
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
10 |
|
11 |
interface SettingsState {
|
|
|
47 |
theme: Theme
|
48 |
setTheme: (theme: Theme) => void
|
49 |
|
50 |
+
language: Language
|
51 |
+
setLanguage: (lang: Language) => void
|
52 |
+
|
53 |
enableHealthCheck: boolean
|
54 |
setEnableHealthCheck: (enable: boolean) => void
|
55 |
|
|
|
61 |
persist(
|
62 |
(set) => ({
|
63 |
theme: 'system',
|
64 |
+
language: 'en',
|
65 |
showPropertyPanel: true,
|
66 |
showNodeSearchBar: true,
|
67 |
|
|
|
74 |
|
75 |
graphQueryMaxDepth: 3,
|
76 |
graphMinDegree: 0,
|
77 |
+
graphLayoutMaxIterations: 15,
|
78 |
|
79 |
queryLabel: defaultQueryLabel,
|
80 |
|
|
|
103 |
|
104 |
setTheme: (theme: Theme) => set({ theme }),
|
105 |
|
106 |
+
setLanguage: (language: Language) => {
|
107 |
+
set({ language })
|
108 |
+
// Update i18n after state is updated
|
109 |
+
import('i18next').then(({ default: i18n }) => {
|
110 |
+
if (i18n.language !== language) {
|
111 |
+
i18n.changeLanguage(language)
|
112 |
+
}
|
113 |
+
})
|
114 |
+
},
|
115 |
+
|
116 |
setGraphLayoutMaxIterations: (iterations: number) =>
|
117 |
set({
|
118 |
graphLayoutMaxIterations: iterations
|
|
|
143 |
{
|
144 |
name: 'settings-storage',
|
145 |
storage: createJSONStorage(() => localStorage),
|
146 |
+
version: 8,
|
147 |
migrate: (state: any, version: number) => {
|
148 |
if (version < 2) {
|
149 |
state.showEdgeLabel = false
|
|
|
180 |
}
|
181 |
if (version < 7) {
|
182 |
state.graphQueryMaxDepth = 3
|
183 |
+
state.graphLayoutMaxIterations = 15
|
184 |
+
}
|
185 |
+
if (version < 8) {
|
186 |
+
state.graphMinDegree = 0
|
187 |
+
state.language = 'en'
|
188 |
}
|
189 |
return state
|
190 |
}
|