Daniel.y commited on
Commit
c6d8b34
·
unverified ·
2 Parent(s): 77e334d 1f583de

Merge pull request #1064 from danielaskdd/improve-property-tooltip

Browse files
Files changed (41) hide show
  1. lightrag/api/lightrag_server.py +13 -1
  2. lightrag/api/webui/assets/index-BV5s8k-a.css +0 -0
  3. lightrag/api/webui/assets/index-CH-3l4_Z.css +0 -0
  4. lightrag/api/webui/assets/{index-BlVvSIic.js → index-DwcJE583.js} +0 -0
  5. lightrag/api/webui/index.html +0 -0
  6. lightrag_webui/bun.lock +3 -0
  7. lightrag_webui/index.html +3 -0
  8. lightrag_webui/package.json +1 -0
  9. lightrag_webui/src/App.tsx +31 -28
  10. lightrag_webui/src/components/AppSettings.tsx +66 -0
  11. lightrag_webui/src/components/Root.tsx +24 -0
  12. lightrag_webui/src/components/ThemeProvider.tsx +16 -14
  13. lightrag_webui/src/components/graph/FocusOnNode.tsx +13 -4
  14. lightrag_webui/src/components/graph/GraphControl.tsx +51 -22
  15. lightrag_webui/src/components/graph/GraphLabels.tsx +69 -37
  16. lightrag_webui/src/components/graph/GraphSearch.tsx +25 -4
  17. lightrag_webui/src/components/graph/PropertiesView.tsx +12 -4
  18. lightrag_webui/src/components/graph/Settings.tsx +115 -103
  19. lightrag_webui/src/components/graph/SettingsDisplay.tsx +21 -0
  20. lightrag_webui/src/components/retrieval/QuerySettings.tsx +1 -1
  21. lightrag_webui/src/components/ui/AsyncSearch.tsx +5 -5
  22. lightrag_webui/src/components/ui/TabContent.tsx +37 -0
  23. lightrag_webui/src/components/ui/Tabs.tsx +5 -1
  24. lightrag_webui/src/components/ui/Tooltip.tsx +33 -20
  25. lightrag_webui/src/contexts/TabVisibilityProvider.tsx +53 -0
  26. lightrag_webui/src/contexts/context.ts +12 -0
  27. lightrag_webui/src/contexts/types.ts +5 -0
  28. lightrag_webui/src/contexts/useTabVisibility.ts +17 -0
  29. lightrag_webui/src/features/ApiSite.tsx +36 -1
  30. lightrag_webui/src/features/DocumentManager.tsx +20 -9
  31. lightrag_webui/src/features/GraphViewer.tsx +102 -47
  32. lightrag_webui/src/features/SiteHeader.tsx +9 -7
  33. lightrag_webui/src/hooks/useLightragGraph.tsx +143 -21
  34. lightrag_webui/src/i18n.js +0 -21
  35. lightrag_webui/src/i18n.ts +37 -0
  36. lightrag_webui/src/lib/constants.ts +2 -2
  37. lightrag_webui/src/locales/en.json +22 -2
  38. lightrag_webui/src/locales/zh.json +92 -88
  39. lightrag_webui/src/main.tsx +2 -9
  40. lightrag_webui/src/stores/graph.ts +77 -7
  41. 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
- StaticFiles(directory=static_dir, html=True, check_dir=True),
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 [currentTab] = useState(() => useSettingsStore.getState().currentTab)
25
  const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
26
 
27
  // Health check
@@ -54,33 +55,35 @@ function App() {
54
 
55
  return (
56
  <ThemeProvider>
57
- <main className="flex h-screen w-screen overflow-x-hidden">
58
- <Tabs
59
- defaultValue={currentTab}
60
- className="!m-0 flex grow flex-col !p-0"
61
- onValueChange={handleTabChange}
62
- >
63
- <SiteHeader />
64
- <div className="relative grow">
65
- <TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
66
- <DocumentManager />
67
- </TabsContent>
68
- <TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
69
- <GraphViewer />
70
- </TabsContent>
71
- <TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
72
- <RetrievalTesting />
73
- </TabsContent>
74
- <TabsContent value="api" className="absolute top-0 right-0 bottom-0 left-0">
75
- <ApiSite />
76
- </TabsContent>
77
- </div>
78
- </Tabs>
79
- {enableHealthCheck && <StatusIndicator />}
80
- {message !== null && !apiKeyInvalid && <MessageAlert />}
81
- {apiKeyInvalid && <ApiKeyAlert />}
82
- <Toaster />
83
- </main>
 
 
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, useState } from 'react'
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 [theme, setTheme] = useState<Theme>(useSettingsStore.getState().theme)
 
25
 
26
  useEffect(() => {
27
  const root = window.document.documentElement
28
  root.classList.remove('light', 'dark')
29
 
30
  if (theme === 'system') {
31
- const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
32
- ? 'dark'
33
- : 'light'
34
- root.classList.add(systemTheme)
35
- setTheme(systemTheme)
36
- return
 
 
 
 
 
 
37
  }
38
-
39
- root.classList.add(theme)
40
  }, [theme])
41
 
42
  const value = {
43
  theme,
44
- setTheme: (theme: Theme) => {
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
- gotoNode(node)
 
 
 
 
 
 
 
20
  useGraphStore.getState().setMoveToSelectedNode(false)
 
 
21
  }
22
 
23
  return () => {
24
- sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
 
 
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 useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph'
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
- // Create & load the graph
48
- const graph = lightrageGraph()
49
- loadGraph(graph)
50
- assignLayout()
51
- }, [assignLayout, loadGraph, lightrageGraph, maxIterations])
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
- // Register the events
62
- registerEvents({
63
- enterNode: (event) => {
 
 
 
 
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
- clickEdge: (event) => {
 
 
 
 
 
78
  setSelectedEdge(event.edge)
79
  setSelectedNode(null)
80
- },
81
- enterEdge: (event) => {
 
82
  if (!isButtonPressed(event.event.original)) {
83
  setFocusedEdge(event.edge)
84
  }
85
- },
86
- leaveEdge: (event) => {
 
87
  if (!isButtonPressed(event.event.original)) {
88
  setFocusedEdge(null)
89
  }
90
- },
91
- clickStage: () => clearSelection()
92
- })
93
- }, [registerEvents])
 
 
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 graph = useGraphStore.use.sigmaGraph()
 
20
 
21
- const getSearchEngine = useCallback(async () => {
22
- if (lastGraph.graph == graph) {
23
- return {
24
- labels: lastGraph.labels,
25
- searchEngine: lastGraph.searchEngine
26
- }
27
- }
28
- const labels = ['*'].concat(await getGraphLabels())
29
 
30
- // Ensure query label exists
31
- if (!labels.includes(useSettingsStore.getState().queryLabel)) {
32
- useSettingsStore.getState().setQueryLabel(labels[0])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = labels.map((str, index) => ({ id: index, value: str }))
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
- }, [graph])
61
 
62
  const fetchData = useCallback(
63
  async (query?: string): Promise<string[]> => {
64
- const { labels, searchEngine } = await getSearchEngine()
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), t('graphLabels.andOthers', { count: result.length - 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={setQueryLabel}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (!query || !searchEngine) return []
89
- const result: OptionItem[] = searchEngine.search(query).map((r) => ({
 
 
 
 
 
 
 
 
 
 
 
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
- {t('graphPanel.propertiesView.node.relationships')}
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 "react-i18next";
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
- <Popover open={opened} onOpenChange={setOpened}>
212
- <PopoverTrigger asChild>
213
- <Button variant={controlButtonVariant} tooltip={t("graphPanel.sideBar.settings.settings")} size="icon">
214
- <SettingsIcon />
215
- </Button>
216
- </PopoverTrigger>
217
- <PopoverContent
218
- side="right"
219
- align="start"
220
- className="mb-2 p-2"
221
- onCloseAutoFocus={(e) => e.preventDefault()}
222
  >
223
- <div className="flex flex-col gap-2">
224
- <LabeledCheckBox
225
- checked={enableHealthCheck}
226
- onCheckedChange={setEnableHealthCheck}
227
- label={t("graphPanel.sideBar.settings.healthCheck")}
228
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
- <Separator />
231
 
232
- <LabeledCheckBox
233
- checked={showPropertyPanel}
234
- onCheckedChange={setShowPropertyPanel}
235
- label={t("graphPanel.sideBar.settings.showPropertyPanel")}
236
- />
237
- <LabeledCheckBox
238
- checked={showNodeSearchBar}
239
- onCheckedChange={setShowNodeSearchBar}
240
- label={t("graphPanel.sideBar.settings.showSearchBar")}
241
- />
242
 
243
- <Separator />
244
 
245
- <LabeledCheckBox
246
- checked={showNodeLabel}
247
- onCheckedChange={setShowNodeLabel}
248
- label={t("graphPanel.sideBar.settings.showNodeLabel")}
249
- />
250
- <LabeledCheckBox
251
- checked={enableNodeDrag}
252
- onCheckedChange={setEnableNodeDrag}
253
- label={t("graphPanel.sideBar.settings.nodeDraggable")}
254
- />
255
 
256
- <Separator />
257
 
258
- <LabeledCheckBox
259
- checked={showEdgeLabel}
260
- onCheckedChange={setShowEdgeLabel}
261
- label={t("graphPanel.sideBar.settings.showEdgeLabel")}
262
- />
263
- <LabeledCheckBox
264
- checked={enableHideUnselectedEdges}
265
- onCheckedChange={setEnableHideUnselectedEdges}
266
- label={t("graphPanel.sideBar.settings.hideUnselectedEdges")}
267
- />
268
- <LabeledCheckBox
269
- checked={enableEdgeEvents}
270
- onCheckedChange={setEnableEdgeEvents}
271
- label={t("graphPanel.sideBar.settings.edgeEvents")}
272
- />
273
 
274
- <Separator />
275
- <LabeledNumberInput
276
- label={t("graphPanel.sideBar.settings.maxQueryDepth")}
277
- min={1}
278
- value={graphQueryMaxDepth}
279
- onEditFinished={setGraphQueryMaxDepth}
280
- />
281
- <LabeledNumberInput
282
- label={t("graphPanel.sideBar.settings.minDegree")}
283
- min={0}
284
- value={graphMinDegree}
285
- onEditFinished={setGraphMinDegree}
286
- />
287
- <LabeledNumberInput
288
- label={t("graphPanel.sideBar.settings.maxLayoutIterations")}
289
- min={1}
290
- max={20}
291
- value={graphLayoutMaxIterations}
292
- onEditFinished={setGraphLayoutMaxIterations}
293
- />
294
- <Separator />
295
 
296
- <div className="flex flex-col gap-2">
297
- <label className="text-sm font-medium">{t("graphPanel.sideBar.settings.apiKey")}</label>
298
- <form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
299
- <div className="w-0 flex-1">
300
- <Input
301
- type="password"
302
- value={tempApiKey}
303
- onChange={handleTempApiKeyChange}
304
- placeholder={t("graphPanel.sideBar.settings.enterYourAPIkey")}
305
- className="max-h-full w-full min-w-0"
306
- autoComplete="off"
307
- />
308
- </div>
309
- <Button
310
- onClick={setApiKey}
311
- variant="outline"
312
- size="sm"
313
- className="max-h-full shrink-0"
314
- >
315
- {t("graphPanel.sideBar.settings.save")}
316
- </Button>
317
- </form>
 
318
  </div>
319
- </div>
320
- </PopoverContent>
321
- </Popover>
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 || debouncedSearchTerm.length === 0}>
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 mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
 
 
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 content.split('\\n').map((line, i) => (
14
- <React.Fragment key={i}>
15
- {line}
16
- {i < content.split('\\n').length - 1 && <br />}
17
- </React.Fragment>
18
- ))
19
  }
20
 
21
  const TooltipContent = React.forwardRef<
22
  React.ComponentRef<typeof TooltipPrimitive.Content>,
23
- React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
24
- >(({ className, sideOffset = 4, children, ...props }, ref) => (
25
- <TooltipPrimitive.Content
26
- ref={ref}
27
- sideOffset={sideOffset}
28
- className={cn(
29
- '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 z-50 mx-1 max-w-sm overflow-hidden rounded-md border px-3 py-2 text-sm shadow-md',
30
- className
31
- )}
32
- {...props}
33
- >
34
- {typeof children === 'string' ? processTooltipContent(children) : children}
35
- </TooltipPrimitive.Content>
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
- return <iframe src={backendBaseUrl + '/docs'} className="size-full" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- fetchDocuments()
55
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
 
 
 
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 (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox())
 
 
 
 
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
- setSigmaSettings({
119
- ...defaultSigmaSettings,
120
- enableEdgeEvents,
121
- renderEdgeLabels,
122
- renderLabels
123
- })
124
- }, [renderLabels, enableEdgeEvents, renderEdgeLabels])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
147
- <GraphControl />
148
-
149
- {enableNodeDrag && <GraphEvents />}
150
-
151
- <FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
152
-
153
- <div className="absolute top-2 left-2 flex items-start gap-2">
154
- <GraphLabels />
155
- {showNodeSearchBar && (
156
- <GraphSearch
157
- value={searchInitSelectedNode}
158
- onFocus={onSearchFocus}
159
- onChange={onSearchSelect}
160
- />
161
- )}
162
- </div>
163
-
164
- <div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
165
- <Settings />
166
- <ZoomControl />
167
- <LayoutsControl />
168
- <FullScreenControl />
169
- {/* <ThemeToggle /> */}
170
- </div>
171
-
172
- {showPropertyPanel && (
173
- <div className="absolute top-2 right-2">
174
- <PropertiesView />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </div>
176
  )}
177
 
178
- {/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
179
- <MiniMap width="100px" height="100px" />
180
- </div> */}
181
- </SigmaContainer>
 
 
 
 
 
 
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 ThemeToggle from '@/components/ThemeToggle'
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
- <Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
71
- <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
72
- <GithubIcon className="size-4" aria-hidden="true" />
73
- </a>
74
- </Button>
75
- <ThemeToggle />
 
 
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: rawNode.x,
147
- y: rawNode.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 (queryLabel) {
189
- if (lastQueryLabel.label !== queryLabel ||
190
- lastQueryLabel.maxQueryDepth !== maxQueryDepth ||
191
- lastQueryLabel.minDegree !== minDegree) {
192
- lastQueryLabel.label = queryLabel
193
- lastQueryLabel.maxQueryDepth = maxQueryDepth
194
- lastQueryLabel.minDegree = minDegree
195
 
 
 
 
196
  const state = useGraphStore.getState()
197
  state.reset()
198
- fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
199
- // console.debug('Query label: ' + queryLabel)
200
- state.setSigmaGraph(createSigmaGraph(data))
201
- data?.buildDynamicMap()
202
- state.setRawGraph(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  })
204
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  } else {
206
- const state = useGraphStore.getState()
207
- state.reset()
208
- state.setSigmaGraph(new DirectedGraph())
209
  }
210
- }, [queryLabel, maxQueryDepth, minDegree])
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 = 20
19
- export const labelListLimit = 40
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": "清除文档失败:\n{{message}}",
22
- "error": "清除文档失败:\n{{error}}"
23
  },
24
  "uploadDocuments": {
25
  "button": "上传",
26
  "tooltip": "上传文档",
27
  "title": "上传文档",
28
- "description": "拖放文档到此处或点击浏览。",
29
- "uploading": "正在上传 {{name}}: {{percent}}%",
30
- "success": "上传成功:\n{{name}} 上传成功",
31
- "failed": "上传失败:\n{{name}}\n{{message}}",
32
- "error": "上传失败:\n{{name}}\n{{error}}",
33
  "generalError": "上传失败\n{{error}}",
34
- "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"
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
- "apiKey": "API 密钥",
83
- "enterYourAPIkey": "输入您的 API 密钥",
84
- "save": "保存"
 
 
 
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": "最大 Token 数",
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": "以及其它 {count} "
157
  },
158
  "graphLabels": {
159
  "selectTooltip": "选择查询标签",
160
  "noLabels": "未找到标签",
161
  "label": "标签",
162
  "placeholder": "搜索标签...",
163
- "andOthers": "以及其它 {count} 个"
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• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文的信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱与向量检索",
185
  "queryModeOptions": {
186
  "naive": "朴素",
187
  "local": "本地",
188
  "global": "全局",
189
  "hybrid": "混合",
190
- "mix": "综合"
191
  },
192
-
193
  "responseFormat": "响应格式",
194
- "responseFormatTooltip": "定义响应格式。例如:\n• 多个段落\n• 单个段落\n• 项目符号",
195
  "responseFormatOptions": {
196
- "multipleParagraphs": "多个段落",
197
- "singleParagraph": "单个段落",
198
- "bulletPoints": "项目符号"
199
  },
200
-
201
- "topK": "Top K 结果数",
202
- "topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系",
203
- "topKPlaceholder": "结果数",
204
-
205
- "maxTokensTextUnit": "文本单元最大 Token 数",
206
- "maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数",
207
-
208
- "maxTokensGlobalContext": "全局上下文最大 Token 数",
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
- "onlyNeedContext": "仅需要上下文",
226
- "onlyNeedContextTooltip": "如果为 True,则仅返回检索到的上下文,而不会生成回复",
227
-
228
- "onlyNeedPrompt": "仅需要提示",
229
- "onlyNeedPromptTooltip": "如果为 True,则仅返回生成的提示,而不会生成回复",
230
-
231
  "streamResponse": "流式响应",
232
- "streamResponseTooltip": "如果为 True,则启用流式输出以获得实时响应"
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 App from './App.tsx'
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
- sigmaGraph: null,
115
- moveToSelectedNode: false
116
- }),
 
 
117
 
118
  setRawGraph: (rawGraph: RawGraph | null) =>
119
  set({
120
  rawGraph
121
  }),
122
 
123
- setSigmaGraph: (sigmaGraph: DirectedGraph | null) => set({ sigmaGraph }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
 
 
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: 10,
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: 7,
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 = 10
 
 
 
 
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
  }