ArnoChen
commited on
Commit
·
506c5f2
1
Parent(s):
5583338
enhance graph viewer with settings, status and api key
Browse files- lightrag/api/graph_viewer_webui/bun.lock +30 -27
- lightrag/api/graph_viewer_webui/package.json +4 -3
- lightrag/api/graph_viewer_webui/src/App.tsx +12 -4
- lightrag/api/graph_viewer_webui/src/GraphViewer.tsx +22 -11
- lightrag/api/graph_viewer_webui/src/api/lightrag.ts +195 -15
- lightrag/api/graph_viewer_webui/src/components/MessageAlert.tsx +28 -11
- lightrag/api/graph_viewer_webui/src/components/PropertiesView.tsx +4 -4
- lightrag/api/graph_viewer_webui/src/components/Settings.tsx +118 -9
- lightrag/api/graph_viewer_webui/src/components/StatusCard.tsx +65 -0
- lightrag/api/graph_viewer_webui/src/components/StatusIndicator.tsx +48 -0
- lightrag/api/graph_viewer_webui/src/components/ui/Button.tsx +5 -2
- lightrag/api/graph_viewer_webui/src/components/ui/Input.tsx +21 -0
- lightrag/api/graph_viewer_webui/src/components/ui/Separator.tsx +24 -0
- lightrag/api/graph_viewer_webui/src/stores/settings.ts +39 -8
- lightrag/api/graph_viewer_webui/src/stores/state.ts +21 -3
lightrag/api/graph_viewer_webui/bun.lock
CHANGED
@@ -8,6 +8,7 @@
|
|
8 |
"@radix-ui/react-checkbox": "^1.1.4",
|
9 |
"@radix-ui/react-dialog": "^1.1.6",
|
10 |
"@radix-ui/react-popover": "^1.1.6",
|
|
|
11 |
"@radix-ui/react-slot": "^1.1.2",
|
12 |
"@radix-ui/react-tooltip": "^1.1.8",
|
13 |
"@react-sigma/core": "^5.0.2",
|
@@ -38,7 +39,7 @@
|
|
38 |
"devDependencies": {
|
39 |
"@eslint/js": "^9.20.0",
|
40 |
"@stylistic/eslint-plugin-js": "^3.1.0",
|
41 |
-
"@tailwindcss/vite": "^4.0.
|
42 |
"@types/bun": "^1.2.2",
|
43 |
"@types/node": "^22.13.1",
|
44 |
"@types/react": "^19.0.8",
|
@@ -54,10 +55,10 @@
|
|
54 |
"graphology-types": "^0.24.8",
|
55 |
"prettier": "^3.5.0",
|
56 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
57 |
-
"tailwindcss": "^4.0.
|
58 |
"tailwindcss-animate": "^1.0.7",
|
59 |
"typescript": "~5.7.3",
|
60 |
-
"typescript-eslint": "^8.
|
61 |
"vite": "^6.1.0",
|
62 |
},
|
63 |
},
|
@@ -235,6 +236,8 @@
|
|
235 |
|
236 |
"@radix-ui/react-primitive": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
|
237 |
|
|
|
|
|
238 |
"@radix-ui/react-slot": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
|
239 |
|
240 |
"@radix-ui/react-tooltip": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
|
@@ -347,33 +350,33 @@
|
|
347 |
|
348 |
"@swc/types": ["@swc/[email protected]", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ=="],
|
349 |
|
350 |
-
"@tailwindcss/node": ["@tailwindcss/[email protected].
|
351 |
|
352 |
-
"@tailwindcss/oxide": ["@tailwindcss/[email protected].
|
353 |
|
354 |
-
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/[email protected].
|
355 |
|
356 |
-
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/[email protected].
|
357 |
|
358 |
-
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/[email protected].
|
359 |
|
360 |
-
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/[email protected].
|
361 |
|
362 |
-
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/[email protected].
|
363 |
|
364 |
-
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/[email protected].
|
365 |
|
366 |
-
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/[email protected].
|
367 |
|
368 |
-
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/[email protected].
|
369 |
|
370 |
-
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/[email protected].
|
371 |
|
372 |
-
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/[email protected].
|
373 |
|
374 |
-
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/[email protected].
|
375 |
|
376 |
-
"@tailwindcss/vite": ["@tailwindcss/[email protected].
|
377 |
|
378 |
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
|
379 |
|
@@ -395,21 +398,21 @@
|
|
395 |
|
396 |
"@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
397 |
|
398 |
-
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.
|
399 |
|
400 |
-
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.
|
401 |
|
402 |
-
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.
|
403 |
|
404 |
-
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.
|
405 |
|
406 |
-
"@typescript-eslint/types": ["@typescript-eslint/types@8.
|
407 |
|
408 |
-
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.
|
409 |
|
410 |
-
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.
|
411 |
|
412 |
-
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.
|
413 |
|
414 |
"@vitejs/plugin-react-swc": ["@vitejs/[email protected]", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="],
|
415 |
|
@@ -933,7 +936,7 @@
|
|
933 |
|
934 |
"tailwind-merge": ["[email protected]", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="],
|
935 |
|
936 |
-
"tailwindcss": ["[email protected].
|
937 |
|
938 |
"tailwindcss-animate": ["[email protected]", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
939 |
|
@@ -957,7 +960,7 @@
|
|
957 |
|
958 |
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
959 |
|
960 |
-
"typescript-eslint": ["typescript-eslint@8.
|
961 |
|
962 |
"unbox-primitive": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
963 |
|
|
|
8 |
"@radix-ui/react-checkbox": "^1.1.4",
|
9 |
"@radix-ui/react-dialog": "^1.1.6",
|
10 |
"@radix-ui/react-popover": "^1.1.6",
|
11 |
+
"@radix-ui/react-separator": "^1.1.2",
|
12 |
"@radix-ui/react-slot": "^1.1.2",
|
13 |
"@radix-ui/react-tooltip": "^1.1.8",
|
14 |
"@react-sigma/core": "^5.0.2",
|
|
|
39 |
"devDependencies": {
|
40 |
"@eslint/js": "^9.20.0",
|
41 |
"@stylistic/eslint-plugin-js": "^3.1.0",
|
42 |
+
"@tailwindcss/vite": "^4.0.6",
|
43 |
"@types/bun": "^1.2.2",
|
44 |
"@types/node": "^22.13.1",
|
45 |
"@types/react": "^19.0.8",
|
|
|
55 |
"graphology-types": "^0.24.8",
|
56 |
"prettier": "^3.5.0",
|
57 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
58 |
+
"tailwindcss": "^4.0.6",
|
59 |
"tailwindcss-animate": "^1.0.7",
|
60 |
"typescript": "~5.7.3",
|
61 |
+
"typescript-eslint": "^8.24.0",
|
62 |
"vite": "^6.1.0",
|
63 |
},
|
64 |
},
|
|
|
236 |
|
237 |
"@radix-ui/react-primitive": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
|
238 |
|
239 |
+
"@radix-ui/react-separator": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
|
240 |
+
|
241 |
"@radix-ui/react-slot": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
|
242 |
|
243 |
"@radix-ui/react-tooltip": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
|
|
|
350 |
|
351 |
"@swc/types": ["@swc/[email protected]", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ=="],
|
352 |
|
353 |
+
"@tailwindcss/node": ["@tailwindcss/[email protected].6", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.6" } }, "sha512-jb6E0WeSq7OQbVYcIJ6LxnZTeC4HjMvbzFBMCrQff4R50HBlo/obmYNk6V2GCUXDeqiXtvtrQgcIbT+/boB03Q=="],
|
354 |
|
355 |
+
"@tailwindcss/oxide": ["@tailwindcss/[email protected].6", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.6", "@tailwindcss/oxide-darwin-arm64": "4.0.6", "@tailwindcss/oxide-darwin-x64": "4.0.6", "@tailwindcss/oxide-freebsd-x64": "4.0.6", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.6", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.6", "@tailwindcss/oxide-linux-arm64-musl": "4.0.6", "@tailwindcss/oxide-linux-x64-gnu": "4.0.6", "@tailwindcss/oxide-linux-x64-musl": "4.0.6", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.6", "@tailwindcss/oxide-win32-x64-msvc": "4.0.6" } }, "sha512-lVyKV2y58UE9CeKVcYykULe9QaE1dtKdxDEdrTPIdbzRgBk6bdxHNAoDqvcqXbIGXubn3VOl1O/CFF77v/EqSA=="],
|
356 |
|
357 |
+
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/[email protected].6", "", { "os": "android", "cpu": "arm64" }, "sha512-xDbym6bDPW3D2XqQqX3PjqW3CKGe1KXH7Fdkc60sX5ZLVUbzPkFeunQaoP+BuYlLc2cC1FoClrIRYnRzof9Sow=="],
|
358 |
|
359 |
+
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/[email protected].6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1f71/ju/tvyGl5c2bDkchZHy8p8EK/tDHCxlpYJ1hGNvsYihZNurxVpZ0DefpN7cNc9RTT8DjrRoV8xXZKKRjg=="],
|
360 |
|
361 |
+
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/[email protected].6", "", { "os": "darwin", "cpu": "x64" }, "sha512-s/hg/ZPgxFIrGMb0kqyeaqZt505P891buUkSezmrDY6lxv2ixIELAlOcUVTkVh245SeaeEiUVUPiUN37cwoL2g=="],
|
362 |
|
363 |
+
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/[email protected].6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Z3Wo8FWZnmio8+xlcbb7JUo/hqRMSmhQw8IGIRoRJ7GmLR0C+25Wq+bEX/135xe/yEle2lFkhu9JBHd4wZYiig=="],
|
364 |
|
365 |
+
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/[email protected].6", "", { "os": "linux", "cpu": "arm" }, "sha512-SNSwkkim1myAgmnbHs4EjXsPL7rQbVGtjcok5EaIzkHkCAVK9QBQsWeP2Jm2/JJhq4wdx8tZB9Y7psMzHYWCkA=="],
|
366 |
|
367 |
+
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/[email protected].6", "", { "os": "linux", "cpu": "arm64" }, "sha512-tJ+mevtSDMQhKlwCCuhsFEFg058kBiSy4TkoeBG921EfrHKmexOaCyFKYhVXy4JtkaeeOcjJnCLasEeqml4i+Q=="],
|
368 |
|
369 |
+
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/[email protected].6", "", { "os": "linux", "cpu": "arm64" }, "sha512-IoArz1vfuTR4rALXMUXI/GWWfx2EaO4gFNtBNkDNOYhlTD4NVEwE45nbBoojYiTulajI4c2XH8UmVEVJTOJKxA=="],
|
370 |
|
371 |
+
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/[email protected].6", "", { "os": "linux", "cpu": "x64" }, "sha512-QtsUfLkEAeWAC3Owx9Kg+7JdzE+k9drPhwTAXbXugYB9RZUnEWWx5x3q/au6TvUYcL+n0RBqDEO2gucZRvRFgQ=="],
|
372 |
|
373 |
+
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/[email protected].6", "", { "os": "linux", "cpu": "x64" }, "sha512-QthvJqIji2KlGNwLcK/PPYo7w1Wsi/8NK0wAtRGbv4eOPdZHkQ9KUk+oCoP20oPO7i2a6X1aBAFQEL7i08nNMA=="],
|
374 |
|
375 |
+
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/[email protected].6", "", { "os": "win32", "cpu": "arm64" }, "sha512-+oka+dYX8jy9iP00DJ9Y100XsqvbqR5s0yfMZJuPR1H/lDVtDfsZiSix1UFBQ3X1HWxoEEl6iXNJHWd56TocVw=="],
|
376 |
|
377 |
+
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/[email protected].6", "", { "os": "win32", "cpu": "x64" }, "sha512-+o+juAkik4p8Ue/0LiflQXPmVatl6Av3LEZXpBTfg4qkMIbZdhCGWFzHdt2NjoMiLOJCFDddoV6GYaimvK1Olw=="],
|
378 |
|
379 |
+
"@tailwindcss/vite": ["@tailwindcss/[email protected].6", "", { "dependencies": { "@tailwindcss/node": "^4.0.6", "@tailwindcss/oxide": "^4.0.6", "lightningcss": "^1.29.1", "tailwindcss": "4.0.6" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-O25vZ/URWbZ2JHdk2o8wH7jOKqEGCsYmX3GwGmYS5DjE4X3mpf93a72Rn7VRnefldNauBzr5z2hfZptmBNtTUQ=="],
|
380 |
|
381 |
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
|
382 |
|
|
|
398 |
|
399 |
"@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
400 |
|
401 |
+
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/type-utils": "8.24.0", "@typescript-eslint/utils": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ=="],
|
402 |
|
403 |
+
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA=="],
|
404 |
|
405 |
+
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0" } }, "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw=="],
|
406 |
|
407 |
+
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/utils": "8.24.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA=="],
|
408 |
|
409 |
+
"@typescript-eslint/types": ["@typescript-eslint/types@8.24.0", "", {}, "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw=="],
|
410 |
|
411 |
+
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ=="],
|
412 |
|
413 |
+
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ=="],
|
414 |
|
415 |
+
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg=="],
|
416 |
|
417 |
"@vitejs/plugin-react-swc": ["@vitejs/[email protected]", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="],
|
418 |
|
|
|
936 |
|
937 |
"tailwind-merge": ["[email protected]", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="],
|
938 |
|
939 |
+
"tailwindcss": ["[email protected].6", "", {}, "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw=="],
|
940 |
|
941 |
"tailwindcss-animate": ["[email protected]", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
942 |
|
|
|
960 |
|
961 |
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
962 |
|
963 |
+
"typescript-eslint": ["typescript-eslint@8.24.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/parser": "8.24.0", "@typescript-eslint/utils": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ=="],
|
964 |
|
965 |
"unbox-primitive": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
966 |
|
lightrag/api/graph_viewer_webui/package.json
CHANGED
@@ -14,6 +14,7 @@
|
|
14 |
"@radix-ui/react-checkbox": "^1.1.4",
|
15 |
"@radix-ui/react-dialog": "^1.1.6",
|
16 |
"@radix-ui/react-popover": "^1.1.6",
|
|
|
17 |
"@radix-ui/react-slot": "^1.1.2",
|
18 |
"@radix-ui/react-tooltip": "^1.1.8",
|
19 |
"@react-sigma/core": "^5.0.2",
|
@@ -44,7 +45,7 @@
|
|
44 |
"devDependencies": {
|
45 |
"@eslint/js": "^9.20.0",
|
46 |
"@stylistic/eslint-plugin-js": "^3.1.0",
|
47 |
-
"@tailwindcss/vite": "^4.0.
|
48 |
"@types/bun": "^1.2.2",
|
49 |
"@types/node": "^22.13.1",
|
50 |
"@types/react": "^19.0.8",
|
@@ -60,10 +61,10 @@
|
|
60 |
"graphology-types": "^0.24.8",
|
61 |
"prettier": "^3.5.0",
|
62 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
63 |
-
"tailwindcss": "^4.0.
|
64 |
"tailwindcss-animate": "^1.0.7",
|
65 |
"typescript": "~5.7.3",
|
66 |
-
"typescript-eslint": "^8.
|
67 |
"vite": "^6.1.0"
|
68 |
}
|
69 |
}
|
|
|
14 |
"@radix-ui/react-checkbox": "^1.1.4",
|
15 |
"@radix-ui/react-dialog": "^1.1.6",
|
16 |
"@radix-ui/react-popover": "^1.1.6",
|
17 |
+
"@radix-ui/react-separator": "^1.1.2",
|
18 |
"@radix-ui/react-slot": "^1.1.2",
|
19 |
"@radix-ui/react-tooltip": "^1.1.8",
|
20 |
"@react-sigma/core": "^5.0.2",
|
|
|
45 |
"devDependencies": {
|
46 |
"@eslint/js": "^9.20.0",
|
47 |
"@stylistic/eslint-plugin-js": "^3.1.0",
|
48 |
+
"@tailwindcss/vite": "^4.0.6",
|
49 |
"@types/bun": "^1.2.2",
|
50 |
"@types/node": "^22.13.1",
|
51 |
"@types/react": "^19.0.8",
|
|
|
61 |
"graphology-types": "^0.24.8",
|
62 |
"prettier": "^3.5.0",
|
63 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
64 |
+
"tailwindcss": "^4.0.6",
|
65 |
"tailwindcss-animate": "^1.0.7",
|
66 |
"typescript": "~5.7.3",
|
67 |
+
"typescript-eslint": "^8.24.0",
|
68 |
"vite": "^6.1.0"
|
69 |
}
|
70 |
}
|
lightrag/api/graph_viewer_webui/src/App.tsx
CHANGED
@@ -1,27 +1,35 @@
|
|
1 |
import ThemeProvider from '@/components/ThemeProvider'
|
2 |
import MessageAlert from '@/components/MessageAlert'
|
3 |
-
import
|
4 |
-
import
|
5 |
import { healthCheckInterval } from '@/lib/constants'
|
6 |
import { useBackendState } from '@/stores/state'
|
|
|
7 |
import { useEffect } from 'react'
|
8 |
|
9 |
function App() {
|
10 |
const message = useBackendState.use.message()
|
|
|
11 |
|
12 |
// health check
|
13 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
14 |
const interval = setInterval(async () => {
|
15 |
await useBackendState.getState().check()
|
16 |
}, healthCheckInterval * 1000)
|
17 |
return () => clearInterval(interval)
|
18 |
-
}, [])
|
19 |
|
20 |
return (
|
21 |
<ThemeProvider>
|
22 |
-
<div className=
|
23 |
<GraphViewer />
|
24 |
</div>
|
|
|
25 |
{message !== null && <MessageAlert />}
|
26 |
</ThemeProvider>
|
27 |
)
|
|
|
1 |
import ThemeProvider from '@/components/ThemeProvider'
|
2 |
import MessageAlert from '@/components/MessageAlert'
|
3 |
+
import StatusIndicator from '@/components/StatusIndicator'
|
4 |
+
import GraphViewer from '@/GraphViewer'
|
5 |
import { healthCheckInterval } from '@/lib/constants'
|
6 |
import { useBackendState } from '@/stores/state'
|
7 |
+
import { useSettingsStore } from '@/stores/settings'
|
8 |
import { useEffect } from 'react'
|
9 |
|
10 |
function App() {
|
11 |
const message = useBackendState.use.message()
|
12 |
+
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
13 |
|
14 |
// health check
|
15 |
useEffect(() => {
|
16 |
+
if (!enableHealthCheck) return
|
17 |
+
|
18 |
+
// Check immediately
|
19 |
+
useBackendState.getState().check()
|
20 |
+
|
21 |
const interval = setInterval(async () => {
|
22 |
await useBackendState.getState().check()
|
23 |
}, healthCheckInterval * 1000)
|
24 |
return () => clearInterval(interval)
|
25 |
+
}, [enableHealthCheck])
|
26 |
|
27 |
return (
|
28 |
<ThemeProvider>
|
29 |
+
<div className="h-screen w-screen">
|
30 |
<GraphViewer />
|
31 |
</div>
|
32 |
+
{enableHealthCheck && <StatusIndicator />}
|
33 |
{message !== null && <MessageAlert />}
|
34 |
</ThemeProvider>
|
35 |
)
|
lightrag/api/graph_viewer_webui/src/GraphViewer.tsx
CHANGED
@@ -99,13 +99,17 @@ const GraphEvents = () => {
|
|
99 |
return null
|
100 |
}
|
101 |
|
102 |
-
|
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 enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
110 |
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
111 |
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
@@ -114,9 +118,10 @@ export const GraphViewer = () => {
|
|
114 |
setSigmaSettings({
|
115 |
...defaultSigmaSettings,
|
116 |
enableEdgeEvents,
|
117 |
-
renderEdgeLabels
|
|
|
118 |
})
|
119 |
-
}, [enableEdgeEvents, renderEdgeLabels])
|
120 |
|
121 |
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
122 |
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
@@ -147,11 +152,13 @@ export const GraphViewer = () => {
|
|
147 |
|
148 |
<div className="absolute top-2 left-2 flex items-start gap-2">
|
149 |
<GraphLabels />
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
|
|
|
|
155 |
</div>
|
156 |
|
157 |
<div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
|
@@ -162,9 +169,11 @@ export const GraphViewer = () => {
|
|
162 |
<ThemeToggle />
|
163 |
</div>
|
164 |
|
165 |
-
|
166 |
-
<
|
167 |
-
|
|
|
|
|
168 |
|
169 |
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
|
170 |
<MiniMap width="100px" height="100px" />
|
@@ -172,3 +181,5 @@ export const GraphViewer = () => {
|
|
172 |
</SigmaContainer>
|
173 |
)
|
174 |
}
|
|
|
|
|
|
99 |
return null
|
100 |
}
|
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()
|
|
|
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)
|
|
|
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">
|
|
|
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" />
|
|
|
181 |
</SigmaContainer>
|
182 |
)
|
183 |
}
|
184 |
+
|
185 |
+
export default GraphViewer
|
lightrag/api/graph_viewer_webui/src/api/lightrag.ts
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
import { backendBaseUrl } from '@/lib/constants'
|
2 |
import { errorMessage } from '@/lib/utils'
|
|
|
3 |
|
|
|
4 |
export type LightragNodeType = {
|
5 |
id: string
|
6 |
labels: string[]
|
@@ -49,21 +51,85 @@ export type LightragDocumentsScanProgress = {
|
|
49 |
progress: number
|
50 |
}
|
51 |
|
52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
if (!response.ok) {
|
54 |
-
throw new Error(
|
|
|
|
|
55 |
}
|
|
|
|
|
56 |
}
|
57 |
|
|
|
58 |
export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
|
59 |
-
const response = await
|
60 |
-
checkResponse(response)
|
61 |
return await response.json()
|
62 |
}
|
63 |
|
64 |
export const getGraphLabels = async (): Promise<string[]> => {
|
65 |
-
const response = await
|
66 |
-
checkResponse(response)
|
67 |
return await response.json()
|
68 |
}
|
69 |
|
@@ -71,13 +137,7 @@ export const checkHealth = async (): Promise<
|
|
71 |
LightragStatus | { status: 'error'; message: string }
|
72 |
> => {
|
73 |
try {
|
74 |
-
const response = await
|
75 |
-
if (!response.ok) {
|
76 |
-
return {
|
77 |
-
status: 'error',
|
78 |
-
message: `Health check failed. Service is currently unavailable.\n${response.status} ${response.statusText} ${response.url}`
|
79 |
-
}
|
80 |
-
}
|
81 |
return await response.json()
|
82 |
} catch (e) {
|
83 |
return {
|
@@ -88,11 +148,131 @@ export const checkHealth = async (): Promise<
|
|
88 |
}
|
89 |
|
90 |
export const getDocuments = async (): Promise<string[]> => {
|
91 |
-
const response = await
|
92 |
return await response.json()
|
93 |
}
|
94 |
|
95 |
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
|
96 |
-
const response = await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
return await response.json()
|
98 |
}
|
|
|
1 |
import { backendBaseUrl } from '@/lib/constants'
|
2 |
import { errorMessage } from '@/lib/utils'
|
3 |
+
import { useSettingsStore } from '@/stores/settings'
|
4 |
|
5 |
+
// Types
|
6 |
export type LightragNodeType = {
|
7 |
id: string
|
8 |
labels: string[]
|
|
|
51 |
progress: number
|
52 |
}
|
53 |
|
54 |
+
export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
|
55 |
+
|
56 |
+
export type QueryRequest = {
|
57 |
+
query: string
|
58 |
+
mode: QueryMode
|
59 |
+
stream?: boolean
|
60 |
+
only_need_context?: boolean
|
61 |
+
}
|
62 |
+
|
63 |
+
export type QueryResponse = {
|
64 |
+
response: string
|
65 |
+
}
|
66 |
+
|
67 |
+
export const InvalidApiKeyError = 'Invalid API Key'
|
68 |
+
export const RequireApiKeError = 'API Key required'
|
69 |
+
|
70 |
+
// Helper functions
|
71 |
+
const getResponseContent = async (response: Response) => {
|
72 |
+
const contentType = response.headers.get('content-type')
|
73 |
+
if (contentType) {
|
74 |
+
if (contentType.includes('application/json')) {
|
75 |
+
const data = await response.json()
|
76 |
+
return JSON.stringify(data, undefined, 2)
|
77 |
+
} else if (contentType.startsWith('text/')) {
|
78 |
+
return await response.text()
|
79 |
+
} else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
|
80 |
+
return await response.text()
|
81 |
+
} else if (contentType.includes('application/octet-stream')) {
|
82 |
+
const buffer = await response.arrayBuffer()
|
83 |
+
const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true })
|
84 |
+
return decoder.decode(buffer)
|
85 |
+
} else {
|
86 |
+
try {
|
87 |
+
return await response.text()
|
88 |
+
} catch (error) {
|
89 |
+
console.warn('Failed to decode as text, may be binary:', error)
|
90 |
+
return `[Could not decode response body. Content-Type: ${contentType}]`
|
91 |
+
}
|
92 |
+
}
|
93 |
+
} else {
|
94 |
+
try {
|
95 |
+
return await response.text()
|
96 |
+
} catch (error) {
|
97 |
+
console.warn('Failed to decode as text, may be binary:', error)
|
98 |
+
return '[Could not decode response body. No Content-Type header.]'
|
99 |
+
}
|
100 |
+
}
|
101 |
+
return ''
|
102 |
+
}
|
103 |
+
|
104 |
+
const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise<Response> => {
|
105 |
+
const apiKey = useSettingsStore.getState().apiKey
|
106 |
+
const headers = {
|
107 |
+
...(options.headers || {}),
|
108 |
+
...(apiKey ? { 'X-API-Key': apiKey } : {})
|
109 |
+
}
|
110 |
+
|
111 |
+
const response = await fetch(backendBaseUrl + url, {
|
112 |
+
...options,
|
113 |
+
headers
|
114 |
+
})
|
115 |
+
|
116 |
if (!response.ok) {
|
117 |
+
throw new Error(
|
118 |
+
`${response.status} ${response.statusText}\n${await getResponseContent(response)}\n${response.url}`
|
119 |
+
)
|
120 |
}
|
121 |
+
|
122 |
+
return response
|
123 |
}
|
124 |
|
125 |
+
// API methods
|
126 |
export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
|
127 |
+
const response = await fetchWithAuth(`/graphs?label=${label}`)
|
|
|
128 |
return await response.json()
|
129 |
}
|
130 |
|
131 |
export const getGraphLabels = async (): Promise<string[]> => {
|
132 |
+
const response = await fetchWithAuth('/graph/label/list')
|
|
|
133 |
return await response.json()
|
134 |
}
|
135 |
|
|
|
137 |
LightragStatus | { status: 'error'; message: string }
|
138 |
> => {
|
139 |
try {
|
140 |
+
const response = await fetchWithAuth('/health')
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
return await response.json()
|
142 |
} catch (e) {
|
143 |
return {
|
|
|
148 |
}
|
149 |
|
150 |
export const getDocuments = async (): Promise<string[]> => {
|
151 |
+
const response = await fetchWithAuth('/documents')
|
152 |
return await response.json()
|
153 |
}
|
154 |
|
155 |
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
|
156 |
+
const response = await fetchWithAuth('/documents/scan-progress')
|
157 |
+
return await response.json()
|
158 |
+
}
|
159 |
+
|
160 |
+
export const uploadDocument = async (
|
161 |
+
file: File
|
162 |
+
): Promise<{
|
163 |
+
status: string
|
164 |
+
message: string
|
165 |
+
total_documents: number
|
166 |
+
}> => {
|
167 |
+
const formData = new FormData()
|
168 |
+
formData.append('file', file)
|
169 |
+
|
170 |
+
const response = await fetchWithAuth('/documents/upload', {
|
171 |
+
method: 'POST',
|
172 |
+
body: formData
|
173 |
+
})
|
174 |
+
return await response.json()
|
175 |
+
}
|
176 |
+
|
177 |
+
export const startDocumentScan = async (): Promise<{ status: string }> => {
|
178 |
+
const response = await fetchWithAuth('/documents/scan', {
|
179 |
+
method: 'POST'
|
180 |
+
})
|
181 |
+
return await response.json()
|
182 |
+
}
|
183 |
+
|
184 |
+
export const queryText = async (request: QueryRequest): Promise<QueryResponse> => {
|
185 |
+
const response = await fetchWithAuth('/query', {
|
186 |
+
method: 'POST',
|
187 |
+
headers: {
|
188 |
+
'Content-Type': 'application/json'
|
189 |
+
},
|
190 |
+
body: JSON.stringify(request)
|
191 |
+
})
|
192 |
+
return await response.json()
|
193 |
+
}
|
194 |
+
|
195 |
+
export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: string) => void) => {
|
196 |
+
const response = await fetchWithAuth('/query/stream', {
|
197 |
+
method: 'POST',
|
198 |
+
headers: {
|
199 |
+
'Content-Type': 'application/json'
|
200 |
+
},
|
201 |
+
body: JSON.stringify(request)
|
202 |
+
})
|
203 |
+
|
204 |
+
const reader = response.body?.getReader()
|
205 |
+
if (!reader) throw new Error('No response body')
|
206 |
+
|
207 |
+
const decoder = new TextDecoder()
|
208 |
+
while (true) {
|
209 |
+
const { done, value } = await reader.read()
|
210 |
+
if (done) break
|
211 |
+
|
212 |
+
const chunk = decoder.decode(value)
|
213 |
+
const lines = chunk.split('\n')
|
214 |
+
for (const line of lines) {
|
215 |
+
if (line) {
|
216 |
+
try {
|
217 |
+
const data = JSON.parse(line)
|
218 |
+
if (data.response) {
|
219 |
+
onChunk(data.response)
|
220 |
+
}
|
221 |
+
} catch (e) {
|
222 |
+
console.error('Error parsing stream chunk:', e)
|
223 |
+
}
|
224 |
+
}
|
225 |
+
}
|
226 |
+
}
|
227 |
+
}
|
228 |
+
|
229 |
+
// Text insertion API
|
230 |
+
export const insertText = async (
|
231 |
+
text: string,
|
232 |
+
description?: string
|
233 |
+
): Promise<{
|
234 |
+
status: string
|
235 |
+
message: string
|
236 |
+
document_count: number
|
237 |
+
}> => {
|
238 |
+
const response = await fetchWithAuth('/documents/text', {
|
239 |
+
method: 'POST',
|
240 |
+
headers: {
|
241 |
+
'Content-Type': 'application/json'
|
242 |
+
},
|
243 |
+
body: JSON.stringify({ text, description })
|
244 |
+
})
|
245 |
+
return await response.json()
|
246 |
+
}
|
247 |
+
|
248 |
+
// Batch file upload API
|
249 |
+
export const uploadBatchDocuments = async (
|
250 |
+
files: File[]
|
251 |
+
): Promise<{
|
252 |
+
status: string
|
253 |
+
message: string
|
254 |
+
document_count: number
|
255 |
+
}> => {
|
256 |
+
const formData = new FormData()
|
257 |
+
files.forEach((file) => {
|
258 |
+
formData.append('files', file)
|
259 |
+
})
|
260 |
+
|
261 |
+
const response = await fetchWithAuth('/documents/batch', {
|
262 |
+
method: 'POST',
|
263 |
+
body: formData
|
264 |
+
})
|
265 |
+
return await response.json()
|
266 |
+
}
|
267 |
+
|
268 |
+
// Clear all documents API
|
269 |
+
export const clearDocuments = async (): Promise<{
|
270 |
+
status: string
|
271 |
+
message: string
|
272 |
+
document_count: number
|
273 |
+
}> => {
|
274 |
+
const response = await fetchWithAuth('/documents', {
|
275 |
+
method: 'DELETE'
|
276 |
+
})
|
277 |
return await response.json()
|
278 |
}
|
lightrag/api/graph_viewer_webui/src/components/MessageAlert.tsx
CHANGED
@@ -1,7 +1,10 @@
|
|
1 |
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
|
2 |
-
import Button from '@/components/ui/Button'
|
3 |
import { useBackendState } from '@/stores/state'
|
4 |
-
import {
|
|
|
|
|
|
|
|
|
5 |
|
6 |
import { AlertCircle } from 'lucide-react'
|
7 |
|
@@ -9,18 +12,32 @@ const MessageAlert = () => {
|
|
9 |
const health = useBackendState.use.health()
|
10 |
const message = useBackendState.use.message()
|
11 |
const messageTitle = useBackendState.use.messageTitle()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
return (
|
14 |
<Alert
|
15 |
variant={health ? 'default' : 'destructive'}
|
16 |
-
className=
|
|
|
|
|
|
|
17 |
>
|
18 |
-
{!health &&
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
<div
|
|
|
|
|
|
|
|
|
24 |
<div className="flex-auto" />
|
25 |
<Button
|
26 |
size="sm"
|
@@ -28,9 +45,9 @@ const MessageAlert = () => {
|
|
28 |
className="text-primary max-h-8 border !p-2 text-xs"
|
29 |
onClick={() => useBackendState.getState().clear()}
|
30 |
>
|
31 |
-
|
32 |
</Button>
|
33 |
-
</div>
|
34 |
</Alert>
|
35 |
)
|
36 |
}
|
|
|
1 |
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
|
|
|
2 |
import { useBackendState } from '@/stores/state'
|
3 |
+
import { useEffect, useState } from 'react'
|
4 |
+
import { cn } from '@/lib/utils'
|
5 |
+
|
6 |
+
// import Button from '@/components/ui/Button'
|
7 |
+
// import { controlButtonVariant } from '@/lib/constants'
|
8 |
|
9 |
import { AlertCircle } from 'lucide-react'
|
10 |
|
|
|
12 |
const health = useBackendState.use.health()
|
13 |
const message = useBackendState.use.message()
|
14 |
const messageTitle = useBackendState.use.messageTitle()
|
15 |
+
const [isMounted, setIsMounted] = useState(false)
|
16 |
+
|
17 |
+
useEffect(() => {
|
18 |
+
setTimeout(() => {
|
19 |
+
setIsMounted(true)
|
20 |
+
}, 50)
|
21 |
+
}, [])
|
22 |
|
23 |
return (
|
24 |
<Alert
|
25 |
variant={health ? 'default' : 'destructive'}
|
26 |
+
className={cn(
|
27 |
+
'bg-background/90 absolute top-2 left-1/2 flex w-auto -translate-x-1/2 transform items-center gap-4 shadow-md backdrop-blur-lg transition-all duration-500 ease-in-out',
|
28 |
+
isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0'
|
29 |
+
)}
|
30 |
>
|
31 |
+
{!health && (
|
32 |
+
<div>
|
33 |
+
<AlertCircle className="size-4" />
|
34 |
+
</div>
|
35 |
+
)}
|
36 |
+
<div>
|
37 |
+
<AlertTitle className="font-bold">{messageTitle}</AlertTitle>
|
38 |
+
<AlertDescription>{message}</AlertDescription>
|
39 |
+
</div>
|
40 |
+
{/* <div className="flex">
|
41 |
<div className="flex-auto" />
|
42 |
<Button
|
43 |
size="sm"
|
|
|
45 |
className="text-primary max-h-8 border !p-2 text-xs"
|
46 |
onClick={() => useBackendState.getState().clear()}
|
47 |
>
|
48 |
+
Close
|
49 |
</Button>
|
50 |
+
</div> */}
|
51 |
</Alert>
|
52 |
)
|
53 |
}
|
lightrag/api/graph_viewer_webui/src/components/PropertiesView.tsx
CHANGED
@@ -59,7 +59,7 @@ const PropertiesView = () => {
|
|
59 |
return <></>
|
60 |
}
|
61 |
return (
|
62 |
-
<div className="bg-background/80 max-w-
|
63 |
{currentType == 'node' ? (
|
64 |
<NodePropertiesView node={currentElement as any} />
|
65 |
) : (
|
@@ -132,7 +132,7 @@ const PropertyRow = ({
|
|
132 |
tooltip?: string
|
133 |
}) => {
|
134 |
return (
|
135 |
-
<div className="flex items-center gap-2
|
136 |
<label className="text-primary/60 tracking-wide">{name}</label>:
|
137 |
<Text
|
138 |
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
|
@@ -150,7 +150,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|
150 |
return (
|
151 |
<div className="flex flex-col gap-2">
|
152 |
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
|
153 |
-
<div className="bg-primary/5 max-h-96 overflow-auto rounded
|
154 |
<PropertyRow name={'Id'} value={node.id} />
|
155 |
<PropertyRow
|
156 |
name={'Labels'}
|
@@ -162,7 +162,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|
162 |
<PropertyRow name={'Degree'} value={node.degree} />
|
163 |
</div>
|
164 |
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
|
165 |
-
<div className="bg-primary/5 max-h-96 overflow-auto rounded
|
166 |
{Object.keys(node.properties)
|
167 |
.sort()
|
168 |
.map((name) => {
|
|
|
59 |
return <></>
|
60 |
}
|
61 |
return (
|
62 |
+
<div className="bg-background/80 max-w-xs rounded-lg border-2 p-2 text-xs backdrop-blur-lg">
|
63 |
{currentType == 'node' ? (
|
64 |
<NodePropertiesView node={currentElement as any} />
|
65 |
) : (
|
|
|
132 |
tooltip?: string
|
133 |
}) => {
|
134 |
return (
|
135 |
+
<div className="flex items-center gap-2">
|
136 |
<label className="text-primary/60 tracking-wide">{name}</label>:
|
137 |
<Text
|
138 |
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
|
|
|
150 |
return (
|
151 |
<div className="flex flex-col gap-2">
|
152 |
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
|
153 |
+
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
154 |
<PropertyRow name={'Id'} value={node.id} />
|
155 |
<PropertyRow
|
156 |
name={'Labels'}
|
|
|
162 |
<PropertyRow name={'Degree'} value={node.degree} />
|
163 |
</div>
|
164 |
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
|
165 |
+
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
166 |
{Object.keys(node.properties)
|
167 |
.sort()
|
168 |
.map((name) => {
|
lightrag/api/graph_viewer_webui/src/components/Settings.tsx
CHANGED
@@ -1,9 +1,12 @@
|
|
1 |
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
2 |
import { Checkbox } from '@/components/ui/Checkbox'
|
3 |
import Button from '@/components/ui/Button'
|
4 |
-
import
|
|
|
|
|
5 |
import { controlButtonVariant } from '@/lib/constants'
|
6 |
import { useSettingsStore } from '@/stores/settings'
|
|
|
7 |
|
8 |
import { SettingsIcon } from 'lucide-react'
|
9 |
|
@@ -20,7 +23,7 @@ const LabeledCheckBox = ({
|
|
20 |
label: string
|
21 |
}) => {
|
22 |
return (
|
23 |
-
<div className="flex gap-2">
|
24 |
<Checkbox checked={checked} onCheckedChange={onCheckedChange} />
|
25 |
<label
|
26 |
htmlFor="terms"
|
@@ -37,12 +40,24 @@ const LabeledCheckBox = ({
|
|
37 |
*/
|
38 |
export default function Settings() {
|
39 |
const [opened, setOpened] = useState<boolean>(false)
|
|
|
|
|
|
|
|
|
|
|
40 |
|
41 |
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
42 |
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
43 |
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
44 |
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
|
45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
const setEnableNodeDrag = useCallback(
|
47 |
() => useSettingsStore.setState((pre) => ({ enableNodeDrag: !pre.enableNodeDrag })),
|
48 |
[]
|
@@ -66,6 +81,40 @@ export default function Settings() {
|
|
66 |
[]
|
67 |
)
|
68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
return (
|
70 |
<Popover open={opened} onOpenChange={setOpened}>
|
71 |
<PopoverTrigger asChild>
|
@@ -73,17 +122,43 @@ export default function Settings() {
|
|
73 |
<SettingsIcon />
|
74 |
</Button>
|
75 |
</PopoverTrigger>
|
76 |
-
<PopoverContent
|
|
|
|
|
|
|
|
|
|
|
77 |
<div className="flex flex-col gap-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
<LabeledCheckBox
|
79 |
checked={enableNodeDrag}
|
80 |
onCheckedChange={setEnableNodeDrag}
|
81 |
label="Node Draggable"
|
82 |
/>
|
|
|
|
|
|
|
83 |
<LabeledCheckBox
|
84 |
-
checked={
|
85 |
-
onCheckedChange={
|
86 |
-
label="Edge
|
87 |
/>
|
88 |
<LabeledCheckBox
|
89 |
checked={enableHideUnselectedEdges}
|
@@ -91,10 +166,44 @@ export default function Settings() {
|
|
91 |
label="Hide Unselected Edges"
|
92 |
/>
|
93 |
<LabeledCheckBox
|
94 |
-
checked={
|
95 |
-
onCheckedChange={
|
96 |
-
label="
|
97 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
</div>
|
99 |
</PopoverContent>
|
100 |
</Popover>
|
|
|
1 |
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
2 |
import { Checkbox } from '@/components/ui/Checkbox'
|
3 |
import Button from '@/components/ui/Button'
|
4 |
+
import Separator from '@/components/ui/Separator'
|
5 |
+
import Input from '@/components/ui/Input'
|
6 |
+
import { useState, useCallback, useEffect } from 'react'
|
7 |
import { controlButtonVariant } from '@/lib/constants'
|
8 |
import { useSettingsStore } from '@/stores/settings'
|
9 |
+
import { useBackendState } from '@/stores/state'
|
10 |
|
11 |
import { SettingsIcon } from 'lucide-react'
|
12 |
|
|
|
23 |
label: string
|
24 |
}) => {
|
25 |
return (
|
26 |
+
<div className="flex items-center gap-2">
|
27 |
<Checkbox checked={checked} onCheckedChange={onCheckedChange} />
|
28 |
<label
|
29 |
htmlFor="terms"
|
|
|
40 |
*/
|
41 |
export default function Settings() {
|
42 |
const [opened, setOpened] = useState<boolean>(false)
|
43 |
+
const [tempApiKey, setTempApiKey] = useState<string>('') // 用于临时存储输入的API Key
|
44 |
+
|
45 |
+
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
46 |
+
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
47 |
+
const showNodeLabel = useSettingsStore.use.showNodeLabel()
|
48 |
|
49 |
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
50 |
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
51 |
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
52 |
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
|
53 |
|
54 |
+
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
55 |
+
const apiKey = useSettingsStore.use.apiKey()
|
56 |
+
|
57 |
+
useEffect(() => {
|
58 |
+
setTempApiKey(apiKey || '')
|
59 |
+
}, [apiKey, opened])
|
60 |
+
|
61 |
const setEnableNodeDrag = useCallback(
|
62 |
() => useSettingsStore.setState((pre) => ({ enableNodeDrag: !pre.enableNodeDrag })),
|
63 |
[]
|
|
|
81 |
[]
|
82 |
)
|
83 |
|
84 |
+
//
|
85 |
+
const setShowPropertyPanel = useCallback(
|
86 |
+
() => useSettingsStore.setState((pre) => ({ showPropertyPanel: !pre.showPropertyPanel })),
|
87 |
+
[]
|
88 |
+
)
|
89 |
+
|
90 |
+
const setShowNodeSearchBar = useCallback(
|
91 |
+
() => useSettingsStore.setState((pre) => ({ showNodeSearchBar: !pre.showNodeSearchBar })),
|
92 |
+
[]
|
93 |
+
)
|
94 |
+
|
95 |
+
const setShowNodeLabel = useCallback(
|
96 |
+
() => useSettingsStore.setState((pre) => ({ showNodeLabel: !pre.showNodeLabel })),
|
97 |
+
[]
|
98 |
+
)
|
99 |
+
|
100 |
+
const setEnableHealthCheck = useCallback(
|
101 |
+
() => useSettingsStore.setState((pre) => ({ enableHealthCheck: !pre.enableHealthCheck })),
|
102 |
+
[]
|
103 |
+
)
|
104 |
+
|
105 |
+
const setApiKey = useCallback(async () => {
|
106 |
+
useSettingsStore.setState({ apiKey: tempApiKey || null })
|
107 |
+
await useBackendState.getState().check()
|
108 |
+
setOpened(false)
|
109 |
+
}, [tempApiKey])
|
110 |
+
|
111 |
+
const handleTempApiKeyChange = useCallback(
|
112 |
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
113 |
+
setTempApiKey(e.target.value)
|
114 |
+
},
|
115 |
+
[setTempApiKey]
|
116 |
+
)
|
117 |
+
|
118 |
return (
|
119 |
<Popover open={opened} onOpenChange={setOpened}>
|
120 |
<PopoverTrigger asChild>
|
|
|
122 |
<SettingsIcon />
|
123 |
</Button>
|
124 |
</PopoverTrigger>
|
125 |
+
<PopoverContent
|
126 |
+
side="right"
|
127 |
+
align="start"
|
128 |
+
className="mb-2 p-2"
|
129 |
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
130 |
+
>
|
131 |
<div className="flex flex-col gap-2">
|
132 |
+
<LabeledCheckBox
|
133 |
+
checked={showPropertyPanel}
|
134 |
+
onCheckedChange={setShowPropertyPanel}
|
135 |
+
label="Show Property Panel"
|
136 |
+
/>
|
137 |
+
<LabeledCheckBox
|
138 |
+
checked={showNodeSearchBar}
|
139 |
+
onCheckedChange={setShowNodeSearchBar}
|
140 |
+
label="Show Search Bar"
|
141 |
+
/>
|
142 |
+
|
143 |
+
<Separator />
|
144 |
+
|
145 |
+
<LabeledCheckBox
|
146 |
+
checked={showNodeLabel}
|
147 |
+
onCheckedChange={setShowNodeLabel}
|
148 |
+
label="Show Node Label"
|
149 |
+
/>
|
150 |
<LabeledCheckBox
|
151 |
checked={enableNodeDrag}
|
152 |
onCheckedChange={setEnableNodeDrag}
|
153 |
label="Node Draggable"
|
154 |
/>
|
155 |
+
|
156 |
+
<Separator />
|
157 |
+
|
158 |
<LabeledCheckBox
|
159 |
+
checked={showEdgeLabel}
|
160 |
+
onCheckedChange={setShowEdgeLabel}
|
161 |
+
label="Show Edge Label"
|
162 |
/>
|
163 |
<LabeledCheckBox
|
164 |
checked={enableHideUnselectedEdges}
|
|
|
166 |
label="Hide Unselected Edges"
|
167 |
/>
|
168 |
<LabeledCheckBox
|
169 |
+
checked={enableEdgeEvents}
|
170 |
+
onCheckedChange={setEnableEdgeEvents}
|
171 |
+
label="Edge Events"
|
172 |
/>
|
173 |
+
|
174 |
+
<Separator />
|
175 |
+
|
176 |
+
<LabeledCheckBox
|
177 |
+
checked={enableHealthCheck}
|
178 |
+
onCheckedChange={setEnableHealthCheck}
|
179 |
+
label="Health Check"
|
180 |
+
/>
|
181 |
+
|
182 |
+
<Separator />
|
183 |
+
|
184 |
+
<div className="flex flex-col gap-2">
|
185 |
+
<label className="text-sm font-medium">API Key</label>
|
186 |
+
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
|
187 |
+
<div className="w-0 flex-1">
|
188 |
+
<Input
|
189 |
+
type="password"
|
190 |
+
value={tempApiKey}
|
191 |
+
onChange={handleTempApiKeyChange}
|
192 |
+
placeholder="Enter your API key"
|
193 |
+
className="max-h-full w-full min-w-0"
|
194 |
+
autoComplete="off"
|
195 |
+
/>
|
196 |
+
</div>
|
197 |
+
<Button
|
198 |
+
onClick={setApiKey}
|
199 |
+
variant="outline"
|
200 |
+
size="sm"
|
201 |
+
className="max-h-full shrink-0"
|
202 |
+
>
|
203 |
+
Save
|
204 |
+
</Button>
|
205 |
+
</form>
|
206 |
+
</div>
|
207 |
</div>
|
208 |
</PopoverContent>
|
209 |
</Popover>
|
lightrag/api/graph_viewer_webui/src/components/StatusCard.tsx
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { LightragStatus } from '@/api/lightrag'
|
2 |
+
|
3 |
+
const StatusCard = ({ status }: { status: LightragStatus | null }) => {
|
4 |
+
if (!status) {
|
5 |
+
return <div className="text-muted-foreground text-sm">Status information unavailable</div>
|
6 |
+
}
|
7 |
+
|
8 |
+
return (
|
9 |
+
<div className="min-w-[300px] space-y-3 text-sm">
|
10 |
+
<div className="space-y-1">
|
11 |
+
<h4 className="font-medium">Storage Info</h4>
|
12 |
+
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
13 |
+
<span>Working Directory:</span>
|
14 |
+
<span className="truncate">{status.working_directory}</span>
|
15 |
+
<span>Input Directory:</span>
|
16 |
+
<span className="truncate">{status.input_directory}</span>
|
17 |
+
<span>Indexed Files:</span>
|
18 |
+
<span>{status.indexed_files_count}</span>
|
19 |
+
</div>
|
20 |
+
</div>
|
21 |
+
|
22 |
+
<div className="space-y-1">
|
23 |
+
<h4 className="font-medium">LLM Configuration</h4>
|
24 |
+
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
25 |
+
<span>LLM Binding:</span>
|
26 |
+
<span>{status.configuration.llm_binding}</span>
|
27 |
+
<span>LLM Binding Host:</span>
|
28 |
+
<span>{status.configuration.llm_binding_host}</span>
|
29 |
+
<span>LLM Model:</span>
|
30 |
+
<span>{status.configuration.llm_model}</span>
|
31 |
+
<span>Max Tokens:</span>
|
32 |
+
<span>{status.configuration.max_tokens}</span>
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
|
36 |
+
<div className="space-y-1">
|
37 |
+
<h4 className="font-medium">Embedding Configuration</h4>
|
38 |
+
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
39 |
+
<span>Embedding Binding:</span>
|
40 |
+
<span>{status.configuration.embedding_binding}</span>
|
41 |
+
<span>Embedding Binding Host:</span>
|
42 |
+
<span>{status.configuration.embedding_binding_host}</span>
|
43 |
+
<span>Embedding Model:</span>
|
44 |
+
<span>{status.configuration.embedding_model}</span>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
|
48 |
+
<div className="space-y-1">
|
49 |
+
<h4 className="font-medium">Storage Configuration</h4>
|
50 |
+
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
51 |
+
<span>KV Storage:</span>
|
52 |
+
<span>{status.configuration.kv_storage}</span>
|
53 |
+
<span>Doc Status Storage:</span>
|
54 |
+
<span>{status.configuration.doc_status_storage}</span>
|
55 |
+
<span>Graph Storage:</span>
|
56 |
+
<span>{status.configuration.graph_storage}</span>
|
57 |
+
<span>Vector Storage:</span>
|
58 |
+
<span>{status.configuration.vector_storage}</span>
|
59 |
+
</div>
|
60 |
+
</div>
|
61 |
+
</div>
|
62 |
+
)
|
63 |
+
}
|
64 |
+
|
65 |
+
export default StatusCard
|
lightrag/api/graph_viewer_webui/src/components/StatusIndicator.tsx
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from '@/lib/utils'
|
2 |
+
import { useBackendState } from '@/stores/state'
|
3 |
+
import { useEffect, useState } from 'react'
|
4 |
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
5 |
+
import StatusCard from '@/components/StatusCard'
|
6 |
+
|
7 |
+
const StatusIndicator = () => {
|
8 |
+
const health = useBackendState.use.health()
|
9 |
+
const lastCheckTime = useBackendState.use.lastCheckTime()
|
10 |
+
const status = useBackendState.use.status()
|
11 |
+
const [animate, setAnimate] = useState(false)
|
12 |
+
|
13 |
+
// listen to health change
|
14 |
+
useEffect(() => {
|
15 |
+
setAnimate(true)
|
16 |
+
const timer = setTimeout(() => setAnimate(false), 300)
|
17 |
+
return () => clearTimeout(timer)
|
18 |
+
}, [lastCheckTime])
|
19 |
+
|
20 |
+
return (
|
21 |
+
<div className="fixed right-4 bottom-4 flex items-center gap-2 opacity-80 select-none">
|
22 |
+
<Popover>
|
23 |
+
<PopoverTrigger asChild>
|
24 |
+
<div className="flex cursor-help items-center gap-2">
|
25 |
+
<div
|
26 |
+
className={cn(
|
27 |
+
'h-3 w-3 rounded-full transition-all duration-300',
|
28 |
+
'shadow-[0_0_8px_rgba(0,0,0,0.2)]',
|
29 |
+
health ? 'bg-green-500' : 'bg-red-500',
|
30 |
+
animate && 'scale-125',
|
31 |
+
animate && health && 'shadow-[0_0_12px_rgba(34,197,94,0.4)]',
|
32 |
+
animate && !health && 'shadow-[0_0_12px_rgba(239,68,68,0.4)]'
|
33 |
+
)}
|
34 |
+
/>
|
35 |
+
<span className="text-muted-foreground text-xs">
|
36 |
+
{health ? 'Connected' : 'Disconnected'}
|
37 |
+
</span>
|
38 |
+
</div>
|
39 |
+
</PopoverTrigger>
|
40 |
+
<PopoverContent className="w-auto" side="top" align="end">
|
41 |
+
<StatusCard status={status} />
|
42 |
+
</PopoverContent>
|
43 |
+
</Popover>
|
44 |
+
</div>
|
45 |
+
)
|
46 |
+
}
|
47 |
+
|
48 |
+
export default StatusIndicator
|
lightrag/api/graph_viewer_webui/src/components/ui/Button.tsx
CHANGED
@@ -20,7 +20,7 @@ const buttonVariants = cva(
|
|
20 |
default: 'h-10 px-4 py-2',
|
21 |
sm: 'h-9 rounded-md px-3',
|
22 |
lg: 'h-11 rounded-md px-8',
|
23 |
-
icon: '
|
24 |
}
|
25 |
},
|
26 |
defaultVariants: {
|
@@ -39,7 +39,10 @@ interface ButtonProps
|
|
39 |
}
|
40 |
|
41 |
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
42 |
-
(
|
|
|
|
|
|
|
43 |
const Comp = asChild ? Slot : 'button'
|
44 |
if (!tooltip) {
|
45 |
return (
|
|
|
20 |
default: 'h-10 px-4 py-2',
|
21 |
sm: 'h-9 rounded-md px-3',
|
22 |
lg: 'h-11 rounded-md px-8',
|
23 |
+
icon: 'size-8'
|
24 |
}
|
25 |
},
|
26 |
defaultVariants: {
|
|
|
39 |
}
|
40 |
|
41 |
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
42 |
+
(
|
43 |
+
{ className, variant, tooltip, size = 'icon', side = 'right', asChild = false, ...props },
|
44 |
+
ref
|
45 |
+
) => {
|
46 |
const Comp = asChild ? Slot : 'button'
|
47 |
if (!tooltip) {
|
48 |
return (
|
lightrag/api/graph_viewer_webui/src/components/ui/Input.tsx
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from 'react'
|
2 |
+
import { cn } from '@/lib/utils'
|
3 |
+
|
4 |
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
5 |
+
({ className, type, ...props }, ref) => {
|
6 |
+
return (
|
7 |
+
<input
|
8 |
+
type={type}
|
9 |
+
className={cn(
|
10 |
+
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
11 |
+
className
|
12 |
+
)}
|
13 |
+
ref={ref}
|
14 |
+
{...props}
|
15 |
+
/>
|
16 |
+
)
|
17 |
+
}
|
18 |
+
)
|
19 |
+
Input.displayName = 'Input'
|
20 |
+
|
21 |
+
export default Input
|
lightrag/api/graph_viewer_webui/src/components/ui/Separator.tsx
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from 'react'
|
2 |
+
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
3 |
+
|
4 |
+
import { cn } from '@/lib/utils'
|
5 |
+
|
6 |
+
const Separator = React.forwardRef<
|
7 |
+
React.ComponentRef<typeof SeparatorPrimitive.Root>,
|
8 |
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
9 |
+
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
10 |
+
<SeparatorPrimitive.Root
|
11 |
+
ref={ref}
|
12 |
+
decorative={decorative}
|
13 |
+
orientation={orientation}
|
14 |
+
className={cn(
|
15 |
+
'bg-border shrink-0',
|
16 |
+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
17 |
+
className
|
18 |
+
)}
|
19 |
+
{...props}
|
20 |
+
/>
|
21 |
+
))
|
22 |
+
Separator.displayName = SeparatorPrimitive.Root.displayName
|
23 |
+
|
24 |
+
export default Separator
|
lightrag/api/graph_viewer_webui/src/stores/settings.ts
CHANGED
@@ -7,39 +7,63 @@ type Theme = 'dark' | 'light' | 'system'
|
|
7 |
|
8 |
interface SettingsState {
|
9 |
theme: Theme
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
enableNodeDrag: boolean
|
11 |
-
enableEdgeEvents: boolean
|
12 |
-
enableHideUnselectedEdges: boolean
|
13 |
-
showEdgeLabel: boolean
|
14 |
|
15 |
-
|
|
|
|
|
16 |
|
17 |
queryLabel: string
|
18 |
setQueryLabel: (queryLabel: string) => void
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
}
|
20 |
|
21 |
const useSettingsStoreBase = create<SettingsState>()(
|
22 |
persist(
|
23 |
(set) => ({
|
24 |
theme: 'system',
|
|
|
|
|
|
|
|
|
|
|
25 |
enableNodeDrag: true,
|
26 |
-
|
27 |
-
enableHideUnselectedEdges: true,
|
28 |
showEdgeLabel: false,
|
|
|
|
|
29 |
|
30 |
queryLabel: defaultQueryLabel,
|
|
|
|
|
|
|
31 |
|
32 |
setTheme: (theme: Theme) => set({ theme }),
|
33 |
|
34 |
setQueryLabel: (queryLabel: string) =>
|
35 |
set({
|
36 |
queryLabel
|
37 |
-
})
|
|
|
|
|
|
|
|
|
38 |
}),
|
39 |
{
|
40 |
name: 'settings-storage',
|
41 |
storage: createJSONStorage(() => localStorage),
|
42 |
-
version:
|
43 |
migrate: (state: any, version: number) => {
|
44 |
if (version < 2) {
|
45 |
state.showEdgeLabel = false
|
@@ -47,6 +71,13 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
47 |
if (version < 3) {
|
48 |
state.queryLabel = defaultQueryLabel
|
49 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
}
|
51 |
}
|
52 |
)
|
|
|
7 |
|
8 |
interface SettingsState {
|
9 |
theme: Theme
|
10 |
+
setTheme: (theme: Theme) => void
|
11 |
+
|
12 |
+
showPropertyPanel: boolean
|
13 |
+
showNodeSearchBar: boolean
|
14 |
+
|
15 |
+
showNodeLabel: boolean
|
16 |
enableNodeDrag: boolean
|
|
|
|
|
|
|
17 |
|
18 |
+
showEdgeLabel: boolean
|
19 |
+
enableHideUnselectedEdges: boolean
|
20 |
+
enableEdgeEvents: boolean
|
21 |
|
22 |
queryLabel: string
|
23 |
setQueryLabel: (queryLabel: string) => void
|
24 |
+
|
25 |
+
enableHealthCheck: boolean
|
26 |
+
setEnableHealthCheck: (enable: boolean) => void
|
27 |
+
|
28 |
+
apiKey: string | null
|
29 |
+
setApiKey: (key: string | null) => void
|
30 |
}
|
31 |
|
32 |
const useSettingsStoreBase = create<SettingsState>()(
|
33 |
persist(
|
34 |
(set) => ({
|
35 |
theme: 'system',
|
36 |
+
|
37 |
+
showPropertyPanel: true,
|
38 |
+
showNodeSearchBar: true,
|
39 |
+
|
40 |
+
showNodeLabel: true,
|
41 |
enableNodeDrag: true,
|
42 |
+
|
|
|
43 |
showEdgeLabel: false,
|
44 |
+
enableHideUnselectedEdges: true,
|
45 |
+
enableEdgeEvents: false,
|
46 |
|
47 |
queryLabel: defaultQueryLabel,
|
48 |
+
enableHealthCheck: true,
|
49 |
+
|
50 |
+
apiKey: null,
|
51 |
|
52 |
setTheme: (theme: Theme) => set({ theme }),
|
53 |
|
54 |
setQueryLabel: (queryLabel: string) =>
|
55 |
set({
|
56 |
queryLabel
|
57 |
+
}),
|
58 |
+
|
59 |
+
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
60 |
+
|
61 |
+
setApiKey: (apiKey: string | null) => set({ apiKey })
|
62 |
}),
|
63 |
{
|
64 |
name: 'settings-storage',
|
65 |
storage: createJSONStorage(() => localStorage),
|
66 |
+
version: 4,
|
67 |
migrate: (state: any, version: number) => {
|
68 |
if (version < 2) {
|
69 |
state.showEdgeLabel = false
|
|
|
71 |
if (version < 3) {
|
72 |
state.queryLabel = defaultQueryLabel
|
73 |
}
|
74 |
+
if (version < 4) {
|
75 |
+
state.showPropertyPanel = true
|
76 |
+
state.showNodeSearchBar = true
|
77 |
+
state.showNodeLabel = true
|
78 |
+
state.enableHealthCheck = true
|
79 |
+
state.apiKey = null
|
80 |
+
}
|
81 |
}
|
82 |
}
|
83 |
)
|
lightrag/api/graph_viewer_webui/src/stores/state.ts
CHANGED
@@ -1,12 +1,16 @@
|
|
1 |
import { create } from 'zustand'
|
2 |
import { createSelectors } from '@/lib/utils'
|
3 |
-
import { checkHealth } from '@/api/lightrag'
|
4 |
|
5 |
interface BackendState {
|
6 |
health: boolean
|
7 |
message: string | null
|
8 |
messageTitle: string | null
|
9 |
|
|
|
|
|
|
|
|
|
10 |
check: () => Promise<boolean>
|
11 |
clear: () => void
|
12 |
setErrorMessage: (message: string, messageTitle: string) => void
|
@@ -16,14 +20,28 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
|
16 |
health: true,
|
17 |
message: null,
|
18 |
messageTitle: null,
|
|
|
|
|
19 |
|
20 |
check: async () => {
|
21 |
const health = await checkHealth()
|
22 |
if (health.status === 'healthy') {
|
23 |
-
set({
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
return true
|
25 |
}
|
26 |
-
set({
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
return false
|
28 |
},
|
29 |
|
|
|
1 |
import { create } from 'zustand'
|
2 |
import { createSelectors } from '@/lib/utils'
|
3 |
+
import { checkHealth, LightragStatus } from '@/api/lightrag'
|
4 |
|
5 |
interface BackendState {
|
6 |
health: boolean
|
7 |
message: string | null
|
8 |
messageTitle: string | null
|
9 |
|
10 |
+
status: LightragStatus | null
|
11 |
+
|
12 |
+
lastCheckTime: number
|
13 |
+
|
14 |
check: () => Promise<boolean>
|
15 |
clear: () => void
|
16 |
setErrorMessage: (message: string, messageTitle: string) => void
|
|
|
20 |
health: true,
|
21 |
message: null,
|
22 |
messageTitle: null,
|
23 |
+
lastCheckTime: Date.now(),
|
24 |
+
status: null,
|
25 |
|
26 |
check: async () => {
|
27 |
const health = await checkHealth()
|
28 |
if (health.status === 'healthy') {
|
29 |
+
set({
|
30 |
+
health: true,
|
31 |
+
message: null,
|
32 |
+
messageTitle: null,
|
33 |
+
lastCheckTime: Date.now(),
|
34 |
+
status: health
|
35 |
+
})
|
36 |
return true
|
37 |
}
|
38 |
+
set({
|
39 |
+
health: false,
|
40 |
+
message: health.message,
|
41 |
+
messageTitle: 'Backend Health Check Error!',
|
42 |
+
lastCheckTime: Date.now(),
|
43 |
+
status: null
|
44 |
+
})
|
45 |
return false
|
46 |
},
|
47 |
|