From 3cacc088fe083db7fbb25250f92406032924ec25 Mon Sep 17 00:00:00 2001 From: ArnoChen Date: Tue, 11 Feb 2025 22:51:22 +0800 Subject: [PATCH] enhance graph viewer with settings, status and api key --- lightrag/api/graph_viewer_webui/bun.lock | 57 ++--- lightrag/api/graph_viewer_webui/package.json | 7 +- lightrag/api/graph_viewer_webui/src/App.tsx | 16 +- .../graph_viewer_webui/src/GraphViewer.tsx | 33 ++- .../graph_viewer_webui/src/api/lightrag.ts | 214 ++++++++++++++++-- .../src/components/MessageAlert.tsx | 39 +++- .../src/components/PropertiesView.tsx | 8 +- .../src/components/Settings.tsx | 127 ++++++++++- .../src/components/StatusCard.tsx | 65 ++++++ .../src/components/StatusIndicator.tsx | 48 ++++ .../src/components/ui/Button.tsx | 7 +- .../src/components/ui/Input.tsx | 21 ++ .../src/components/ui/Separator.tsx | 24 ++ .../graph_viewer_webui/src/stores/settings.ts | 49 +++- .../graph_viewer_webui/src/stores/state.ts | 24 +- 15 files changed, 639 insertions(+), 100 deletions(-) create mode 100644 lightrag/api/graph_viewer_webui/src/components/StatusCard.tsx create mode 100644 lightrag/api/graph_viewer_webui/src/components/StatusIndicator.tsx create mode 100644 lightrag/api/graph_viewer_webui/src/components/ui/Input.tsx create mode 100644 lightrag/api/graph_viewer_webui/src/components/ui/Separator.tsx diff --git a/lightrag/api/graph_viewer_webui/bun.lock b/lightrag/api/graph_viewer_webui/bun.lock index 21073f15..fc3d9df1 100644 --- a/lightrag/api/graph_viewer_webui/bun.lock +++ b/lightrag/api/graph_viewer_webui/bun.lock @@ -8,6 +8,7 @@ "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@react-sigma/core": "^5.0.2", @@ -38,7 +39,7 @@ "devDependencies": { "@eslint/js": "^9.20.0", "@stylistic/eslint-plugin-js": "^3.1.0", - "@tailwindcss/vite": "^4.0.5", + "@tailwindcss/vite": "^4.0.6", "@types/bun": "^1.2.2", "@types/node": "^22.13.1", "@types/react": "^19.0.8", @@ -54,10 +55,10 @@ "graphology-types": "^0.24.8", "prettier": "^3.5.0", "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^4.0.5", + "tailwindcss": "^4.0.6", "tailwindcss-animate": "^1.0.7", "typescript": "~5.7.3", - "typescript-eslint": "^8.23.0", + "typescript-eslint": "^8.24.0", "vite": "^6.1.0", }, }, @@ -235,6 +236,8 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "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=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "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=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "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=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "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 @@ "@swc/types": ["@swc/types@0.1.17", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ=="], - "@tailwindcss/node": ["@tailwindcss/node@4.0.5", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.5" } }, "sha512-ffTz4DX1cgr4XPuqjhm32YV6Lyx58R1CxAAnSFTamg6wXwfk3oWdb6exgAbGesPzvUgicTO0gwUdQGSsg4nNog=="], + "@tailwindcss/node": ["@tailwindcss/node@4.0.6", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.6" } }, "sha512-jb6E0WeSq7OQbVYcIJ6LxnZTeC4HjMvbzFBMCrQff4R50HBlo/obmYNk6V2GCUXDeqiXtvtrQgcIbT+/boB03Q=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.5", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.5", "@tailwindcss/oxide-darwin-arm64": "4.0.5", "@tailwindcss/oxide-darwin-x64": "4.0.5", "@tailwindcss/oxide-freebsd-x64": "4.0.5", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.5", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.5", "@tailwindcss/oxide-linux-arm64-musl": "4.0.5", "@tailwindcss/oxide-linux-x64-gnu": "4.0.5", "@tailwindcss/oxide-linux-x64-musl": "4.0.5", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.5", "@tailwindcss/oxide-win32-x64-msvc": "4.0.5" } }, "sha512-iWGyOCu0TuzvCBisWbGv2K9+7QCfE0ztgtrZOvb9iF7V7ChVkD15Obe3HevZrhjngAc34jDA+OMSuSvkrpTy4A=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.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=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.5", "", { "os": "android", "cpu": "arm64" }, "sha512-kK/ik8aIAKWDIEYDZGUCJcnU1qU5sPoMBlVzPvtsUqiV6cSHcnVRUdkcLwKqTeUowzZtjjRiamELLd9Gb0x5BQ=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.6", "", { "os": "android", "cpu": "arm64" }, "sha512-xDbym6bDPW3D2XqQqX3PjqW3CKGe1KXH7Fdkc60sX5ZLVUbzPkFeunQaoP+BuYlLc2cC1FoClrIRYnRzof9Sow=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vkbXFv0FfAEbrSa5NBjFEE+xi06ha7mxuxjY8LRn7d7/tBGrAZOEJnnsEbB6M1+x2pGRTjjei0XyTIXdVCglJA=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1f71/ju/tvyGl5c2bDkchZHy8p8EK/tDHCxlpYJ1hGNvsYihZNurxVpZ0DefpN7cNc9RTT8DjrRoV8xXZKKRjg=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-PedA64rHBXEa4e6abBWE4Yj4gHulfPb5T+rBNnX+WGkjjge5Txa2oS99TLmJ5BPDkXXqz/Ba7oweWIDDG7i5NQ=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-s/hg/ZPgxFIrGMb0kqyeaqZt505P891buUkSezmrDY6lxv2ixIELAlOcUVTkVh245SeaeEiUVUPiUN37cwoL2g=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-silz3nuZdEYDfic3v/ooVUQChj9hbxDSee43GCQNwr/iD9L4K/JsZtoNqr0w69pUkvWcKINOGOG0r7WqUqkAeg=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Z3Wo8FWZnmio8+xlcbb7JUo/hqRMSmhQw8IGIRoRJ7GmLR0C+25Wq+bEX/135xe/yEle2lFkhu9JBHd4wZYiig=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-ElneG75XS64B9I2G83A/Hc7EtNVOD5xahs7avq0aeW7mEX6CtMc8m8RCXMn3jGhz8enFE52l6QU0wO7iVkEtXQ=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6", "", { "os": "linux", "cpu": "arm" }, "sha512-SNSwkkim1myAgmnbHs4EjXsPL7rQbVGtjcok5EaIzkHkCAVK9QBQsWeP2Jm2/JJhq4wdx8tZB9Y7psMzHYWCkA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-8yoXpWTeIFaByUaKy2qRAppznLVaDHP9xYCAbS3FG7+uUwHi8CHE4TcomM7eyamo0U7dbUIDgKMGoAX5s2iVrA=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-tJ+mevtSDMQhKlwCCuhsFEFg058kBiSy4TkoeBG921EfrHKmexOaCyFKYhVXy4JtkaeeOcjJnCLasEeqml4i+Q=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-BDlVSiiJ08GRz9KKnXgaPFs2fkukPF3pym6uK3oWEKW45jKlVGgybLqulcV5nLEqREOuyq4Rn4vnZss4/bbQ/g=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-IoArz1vfuTR4rALXMUXI/GWWfx2EaO4gFNtBNkDNOYhlTD4NVEwE45nbBoojYiTulajI4c2XH8UmVEVJTOJKxA=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-DYgieNDRkTy69bWPgdsc47nAXa74P63P/RetUwYM9vYj5USyOfHCEcqIthkCuYw3dXKBhjgwe697TmL2g2jpAw=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-QtsUfLkEAeWAC3Owx9Kg+7JdzE+k9drPhwTAXbXugYB9RZUnEWWx5x3q/au6TvUYcL+n0RBqDEO2gucZRvRFgQ=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-z2RzUvOQl0ZqrZqmCFP53tJbBXQ3UmLD/E6J7+q0e+4VaFnXCcIYTfQbHgI8f3fash+q6gK80Ko/ywEQ+bvv6Q=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-QthvJqIji2KlGNwLcK/PPYo7w1Wsi/8NK0wAtRGbv4eOPdZHkQ9KUk+oCoP20oPO7i2a6X1aBAFQEL7i08nNMA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-ho1dJ4o5Q8nAOxdMkbfBu5aSqI+/bzQ0jEeHcXaEdEJzf2fSWs3HY7bIKtE6vQS8c4SmSBvls7IhGPuJxNg+2Q=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-+oka+dYX8jy9iP00DJ9Y100XsqvbqR5s0yfMZJuPR1H/lDVtDfsZiSix1UFBQ3X1HWxoEEl6iXNJHWd56TocVw=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.5", "", { "os": "win32", "cpu": "x64" }, "sha512-yjw6JhtyDXr+G0aZrj3L3NlEV7CobSqOdPyfo6G3d91WEZ5b8PyGm86IAreX08Jp9DChGXEd53gWysVpWCTs+w=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-+o+juAkik4p8Ue/0LiflQXPmVatl6Av3LEZXpBTfg4qkMIbZdhCGWFzHdt2NjoMiLOJCFDddoV6GYaimvK1Olw=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.0.5", "", { "dependencies": { "@tailwindcss/node": "^4.0.5", "@tailwindcss/oxide": "^4.0.5", "lightningcss": "^1.29.1", "tailwindcss": "4.0.5" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-/i4hjLTUYVjUG0MTUviQP3HR/hzwyzv8Sq4sz2pnsNuf+FIjjhJB0vcnIMH1KIX0k8ozD6CBv2Dl76tlm/JFFA=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.0.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=="], "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="], @@ -395,21 +398,21 @@ "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.23.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.23.0", "@typescript-eslint/type-utils": "8.23.0", "@typescript-eslint/utils": "8.23.0", "@typescript-eslint/visitor-keys": "8.23.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-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w=="], + "@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=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.23.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.23.0", "@typescript-eslint/types": "8.23.0", "@typescript-eslint/typescript-estree": "8.23.0", "@typescript-eslint/visitor-keys": "8.23.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q=="], + "@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=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.23.0", "", { "dependencies": { "@typescript-eslint/types": "8.23.0", "@typescript-eslint/visitor-keys": "8.23.0" } }, "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw=="], + "@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=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.23.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.23.0", "@typescript-eslint/utils": "8.23.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-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA=="], + "@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=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.23.0", "", {}, "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.24.0", "", {}, "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.23.0", "", { "dependencies": { "@typescript-eslint/types": "8.23.0", "@typescript-eslint/visitor-keys": "8.23.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-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ=="], + "@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=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.23.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.23.0", "@typescript-eslint/types": "8.23.0", "@typescript-eslint/typescript-estree": "8.23.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA=="], + "@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=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.23.0", "", { "dependencies": { "@typescript-eslint/types": "8.23.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ=="], + "@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=="], "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.0", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="], @@ -933,7 +936,7 @@ "tailwind-merge": ["tailwind-merge@3.0.1", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="], - "tailwindcss": ["tailwindcss@4.0.5", "", {}, "sha512-DZZIKX3tA23LGTjHdnwlJOTxfICD1cPeykLLsYF1RQBI9QsCR3i0szohJfJDVjr6aNRAIio5WVO7FGB77fRHwg=="], + "tailwindcss": ["tailwindcss@4.0.6", "", {}, "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], @@ -957,7 +960,7 @@ "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - "typescript-eslint": ["typescript-eslint@8.23.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.23.0", "@typescript-eslint/parser": "8.23.0", "@typescript-eslint/utils": "8.23.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ=="], + "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=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "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=="], diff --git a/lightrag/api/graph_viewer_webui/package.json b/lightrag/api/graph_viewer_webui/package.json index 1057f3f6..b35ee961 100644 --- a/lightrag/api/graph_viewer_webui/package.json +++ b/lightrag/api/graph_viewer_webui/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@react-sigma/core": "^5.0.2", @@ -44,7 +45,7 @@ "devDependencies": { "@eslint/js": "^9.20.0", "@stylistic/eslint-plugin-js": "^3.1.0", - "@tailwindcss/vite": "^4.0.5", + "@tailwindcss/vite": "^4.0.6", "@types/bun": "^1.2.2", "@types/node": "^22.13.1", "@types/react": "^19.0.8", @@ -60,10 +61,10 @@ "graphology-types": "^0.24.8", "prettier": "^3.5.0", "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^4.0.5", + "tailwindcss": "^4.0.6", "tailwindcss-animate": "^1.0.7", "typescript": "~5.7.3", - "typescript-eslint": "^8.23.0", + "typescript-eslint": "^8.24.0", "vite": "^6.1.0" } } diff --git a/lightrag/api/graph_viewer_webui/src/App.tsx b/lightrag/api/graph_viewer_webui/src/App.tsx index 11a5761a..efdf4c9d 100644 --- a/lightrag/api/graph_viewer_webui/src/App.tsx +++ b/lightrag/api/graph_viewer_webui/src/App.tsx @@ -1,27 +1,35 @@ import ThemeProvider from '@/components/ThemeProvider' import MessageAlert from '@/components/MessageAlert' -import { GraphViewer } from '@/GraphViewer' -import { cn } from '@/lib/utils' +import StatusIndicator from '@/components/StatusIndicator' +import GraphViewer from '@/GraphViewer' import { healthCheckInterval } from '@/lib/constants' import { useBackendState } from '@/stores/state' +import { useSettingsStore } from '@/stores/settings' import { useEffect } from 'react' function App() { const message = useBackendState.use.message() + const enableHealthCheck = useSettingsStore.use.enableHealthCheck() // health check useEffect(() => { + if (!enableHealthCheck) return + + // Check immediately + useBackendState.getState().check() + const interval = setInterval(async () => { await useBackendState.getState().check() }, healthCheckInterval * 1000) return () => clearInterval(interval) - }, []) + }, [enableHealthCheck]) return ( -
+
+ {enableHealthCheck && } {message !== null && } ) diff --git a/lightrag/api/graph_viewer_webui/src/GraphViewer.tsx b/lightrag/api/graph_viewer_webui/src/GraphViewer.tsx index 993b4b3c..cfdc1709 100644 --- a/lightrag/api/graph_viewer_webui/src/GraphViewer.tsx +++ b/lightrag/api/graph_viewer_webui/src/GraphViewer.tsx @@ -99,13 +99,17 @@ const GraphEvents = () => { return null } -export const GraphViewer = () => { +const GraphViewer = () => { const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings) const selectedNode = useGraphStore.use.selectedNode() const focusedNode = useGraphStore.use.focusedNode() const moveToSelectedNode = useGraphStore.use.moveToSelectedNode() + const showPropertyPanel = useSettingsStore.use.showPropertyPanel() + const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() + const renderLabels = useSettingsStore.use.showNodeLabel() + const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents() const enableNodeDrag = useSettingsStore.use.enableNodeDrag() const renderEdgeLabels = useSettingsStore.use.showEdgeLabel() @@ -114,9 +118,10 @@ export const GraphViewer = () => { setSigmaSettings({ ...defaultSigmaSettings, enableEdgeEvents, - renderEdgeLabels + renderEdgeLabels, + renderLabels }) - }, [enableEdgeEvents, renderEdgeLabels]) + }, [renderLabels, enableEdgeEvents, renderEdgeLabels]) const onSearchFocus = useCallback((value: GraphSearchOption | null) => { if (value === null) useGraphStore.getState().setFocusedNode(null) @@ -147,11 +152,13 @@ export const GraphViewer = () => {
- + {showNodeSearchBar && ( + + )}
@@ -162,9 +169,11 @@ export const GraphViewer = () => {
-
- -
+ {showPropertyPanel && ( +
+ +
+ )} {/*
@@ -172,3 +181,5 @@ export const GraphViewer = () => { ) } + +export default GraphViewer diff --git a/lightrag/api/graph_viewer_webui/src/api/lightrag.ts b/lightrag/api/graph_viewer_webui/src/api/lightrag.ts index 58e1b40e..a4e47b2f 100644 --- a/lightrag/api/graph_viewer_webui/src/api/lightrag.ts +++ b/lightrag/api/graph_viewer_webui/src/api/lightrag.ts @@ -1,6 +1,8 @@ import { backendBaseUrl } from '@/lib/constants' import { errorMessage } from '@/lib/utils' +import { useSettingsStore } from '@/stores/settings' +// Types export type LightragNodeType = { id: string labels: string[] @@ -49,21 +51,85 @@ export type LightragDocumentsScanProgress = { progress: number } -const checkResponse = (response: Response) => { - if (!response.ok) { - throw new Error(`${response.status} ${response.statusText} ${response.url}`) - } +export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix' + +export type QueryRequest = { + query: string + mode: QueryMode + stream?: boolean + only_need_context?: boolean } +export type QueryResponse = { + response: string +} + +export const InvalidApiKeyError = 'Invalid API Key' +export const RequireApiKeError = 'API Key required' + +// Helper functions +const getResponseContent = async (response: Response) => { + const contentType = response.headers.get('content-type') + if (contentType) { + if (contentType.includes('application/json')) { + const data = await response.json() + return JSON.stringify(data, undefined, 2) + } else if (contentType.startsWith('text/')) { + return await response.text() + } else if (contentType.includes('application/xml') || contentType.includes('text/xml')) { + return await response.text() + } else if (contentType.includes('application/octet-stream')) { + const buffer = await response.arrayBuffer() + const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }) + return decoder.decode(buffer) + } else { + try { + return await response.text() + } catch (error) { + console.warn('Failed to decode as text, may be binary:', error) + return `[Could not decode response body. Content-Type: ${contentType}]` + } + } + } else { + try { + return await response.text() + } catch (error) { + console.warn('Failed to decode as text, may be binary:', error) + return '[Could not decode response body. No Content-Type header.]' + } + } + return '' +} + +const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise => { + const apiKey = useSettingsStore.getState().apiKey + const headers = { + ...(options.headers || {}), + ...(apiKey ? { 'X-API-Key': apiKey } : {}) + } + + const response = await fetch(backendBaseUrl + url, { + ...options, + headers + }) + + if (!response.ok) { + throw new Error( + `${response.status} ${response.statusText}\n${await getResponseContent(response)}\n${response.url}` + ) + } + + return response +} + +// API methods export const queryGraphs = async (label: string): Promise => { - const response = await fetch(backendBaseUrl + `/graphs?label=${label}`) - checkResponse(response) + const response = await fetchWithAuth(`/graphs?label=${label}`) return await response.json() } export const getGraphLabels = async (): Promise => { - const response = await fetch(backendBaseUrl + '/graph/label/list') - checkResponse(response) + const response = await fetchWithAuth('/graph/label/list') return await response.json() } @@ -71,13 +137,7 @@ export const checkHealth = async (): Promise< LightragStatus | { status: 'error'; message: string } > => { try { - const response = await fetch(backendBaseUrl + '/health') - if (!response.ok) { - return { - status: 'error', - message: `Health check failed. Service is currently unavailable.\n${response.status} ${response.statusText} ${response.url}` - } - } + const response = await fetchWithAuth('/health') return await response.json() } catch (e) { return { @@ -88,11 +148,131 @@ export const checkHealth = async (): Promise< } export const getDocuments = async (): Promise => { - const response = await fetch(backendBaseUrl + '/documents') + const response = await fetchWithAuth('/documents') return await response.json() } export const getDocumentsScanProgress = async (): Promise => { - const response = await fetch(backendBaseUrl + '/documents/scan-progress') + const response = await fetchWithAuth('/documents/scan-progress') + return await response.json() +} + +export const uploadDocument = async ( + file: File +): Promise<{ + status: string + message: string + total_documents: number +}> => { + const formData = new FormData() + formData.append('file', file) + + const response = await fetchWithAuth('/documents/upload', { + method: 'POST', + body: formData + }) + return await response.json() +} + +export const startDocumentScan = async (): Promise<{ status: string }> => { + const response = await fetchWithAuth('/documents/scan', { + method: 'POST' + }) + return await response.json() +} + +export const queryText = async (request: QueryRequest): Promise => { + const response = await fetchWithAuth('/query', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }) + return await response.json() +} + +export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: string) => void) => { + const response = await fetchWithAuth('/query/stream', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }) + + const reader = response.body?.getReader() + if (!reader) throw new Error('No response body') + + const decoder = new TextDecoder() + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value) + const lines = chunk.split('\n') + for (const line of lines) { + if (line) { + try { + const data = JSON.parse(line) + if (data.response) { + onChunk(data.response) + } + } catch (e) { + console.error('Error parsing stream chunk:', e) + } + } + } + } +} + +// Text insertion API +export const insertText = async ( + text: string, + description?: string +): Promise<{ + status: string + message: string + document_count: number +}> => { + const response = await fetchWithAuth('/documents/text', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ text, description }) + }) + return await response.json() +} + +// Batch file upload API +export const uploadBatchDocuments = async ( + files: File[] +): Promise<{ + status: string + message: string + document_count: number +}> => { + const formData = new FormData() + files.forEach((file) => { + formData.append('files', file) + }) + + const response = await fetchWithAuth('/documents/batch', { + method: 'POST', + body: formData + }) + return await response.json() +} + +// Clear all documents API +export const clearDocuments = async (): Promise<{ + status: string + message: string + document_count: number +}> => { + const response = await fetchWithAuth('/documents', { + method: 'DELETE' + }) return await response.json() } diff --git a/lightrag/api/graph_viewer_webui/src/components/MessageAlert.tsx b/lightrag/api/graph_viewer_webui/src/components/MessageAlert.tsx index 392459aa..6a25baab 100644 --- a/lightrag/api/graph_viewer_webui/src/components/MessageAlert.tsx +++ b/lightrag/api/graph_viewer_webui/src/components/MessageAlert.tsx @@ -1,7 +1,10 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' -import Button from '@/components/ui/Button' import { useBackendState } from '@/stores/state' -import { controlButtonVariant } from '@/lib/constants' +import { useEffect, useState } from 'react' +import { cn } from '@/lib/utils' + +// import Button from '@/components/ui/Button' +// import { controlButtonVariant } from '@/lib/constants' import { AlertCircle } from 'lucide-react' @@ -9,18 +12,32 @@ const MessageAlert = () => { const health = useBackendState.use.health() const message = useBackendState.use.message() const messageTitle = useBackendState.use.messageTitle() + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + setTimeout(() => { + setIsMounted(true) + }, 50) + }, []) return ( - {!health && } - {messageTitle} - - {message} -
-
+ {!health && ( +
+ +
+ )} +
+ {messageTitle} + {message} +
+ {/*
-
+
*/} ) } diff --git a/lightrag/api/graph_viewer_webui/src/components/PropertiesView.tsx b/lightrag/api/graph_viewer_webui/src/components/PropertiesView.tsx index 885bfb10..078420e6 100644 --- a/lightrag/api/graph_viewer_webui/src/components/PropertiesView.tsx +++ b/lightrag/api/graph_viewer_webui/src/components/PropertiesView.tsx @@ -59,7 +59,7 @@ const PropertiesView = () => { return <> } return ( -
+
{currentType == 'node' ? ( ) : ( @@ -132,7 +132,7 @@ const PropertyRow = ({ tooltip?: string }) => { return ( -
+
: { return (
-
+
{
-
+
{Object.keys(node.properties) .sort() .map((name) => { diff --git a/lightrag/api/graph_viewer_webui/src/components/Settings.tsx b/lightrag/api/graph_viewer_webui/src/components/Settings.tsx index 2127e44f..0b8c20fc 100644 --- a/lightrag/api/graph_viewer_webui/src/components/Settings.tsx +++ b/lightrag/api/graph_viewer_webui/src/components/Settings.tsx @@ -1,9 +1,12 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' import { Checkbox } from '@/components/ui/Checkbox' import Button from '@/components/ui/Button' -import { useState, useCallback } from 'react' +import Separator from '@/components/ui/Separator' +import Input from '@/components/ui/Input' +import { useState, useCallback, useEffect } from 'react' import { controlButtonVariant } from '@/lib/constants' import { useSettingsStore } from '@/stores/settings' +import { useBackendState } from '@/stores/state' import { SettingsIcon } from 'lucide-react' @@ -20,7 +23,7 @@ const LabeledCheckBox = ({ label: string }) => { return ( -
+