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 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.5",
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.5",
58
  "tailwindcss-animate": "^1.0.7",
59
  "typescript": "~5.7.3",
60
- "typescript-eslint": "^8.23.0",
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].5", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.5" } }, "sha512-ffTz4DX1cgr4XPuqjhm32YV6Lyx58R1CxAAnSFTamg6wXwfk3oWdb6exgAbGesPzvUgicTO0gwUdQGSsg4nNog=="],
351
 
352
- "@tailwindcss/oxide": ["@tailwindcss/[email protected].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=="],
353
 
354
- "@tailwindcss/oxide-android-arm64": ["@tailwindcss/[email protected].5", "", { "os": "android", "cpu": "arm64" }, "sha512-kK/ik8aIAKWDIEYDZGUCJcnU1qU5sPoMBlVzPvtsUqiV6cSHcnVRUdkcLwKqTeUowzZtjjRiamELLd9Gb0x5BQ=="],
355
 
356
- "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/[email protected].5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vkbXFv0FfAEbrSa5NBjFEE+xi06ha7mxuxjY8LRn7d7/tBGrAZOEJnnsEbB6M1+x2pGRTjjei0XyTIXdVCglJA=="],
357
 
358
- "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/[email protected].5", "", { "os": "darwin", "cpu": "x64" }, "sha512-PedA64rHBXEa4e6abBWE4Yj4gHulfPb5T+rBNnX+WGkjjge5Txa2oS99TLmJ5BPDkXXqz/Ba7oweWIDDG7i5NQ=="],
359
 
360
- "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/[email protected].5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-silz3nuZdEYDfic3v/ooVUQChj9hbxDSee43GCQNwr/iD9L4K/JsZtoNqr0w69pUkvWcKINOGOG0r7WqUqkAeg=="],
361
 
362
- "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/[email protected].5", "", { "os": "linux", "cpu": "arm" }, "sha512-ElneG75XS64B9I2G83A/Hc7EtNVOD5xahs7avq0aeW7mEX6CtMc8m8RCXMn3jGhz8enFE52l6QU0wO7iVkEtXQ=="],
363
 
364
- "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/[email protected].5", "", { "os": "linux", "cpu": "arm64" }, "sha512-8yoXpWTeIFaByUaKy2qRAppznLVaDHP9xYCAbS3FG7+uUwHi8CHE4TcomM7eyamo0U7dbUIDgKMGoAX5s2iVrA=="],
365
 
366
- "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/[email protected].5", "", { "os": "linux", "cpu": "arm64" }, "sha512-BDlVSiiJ08GRz9KKnXgaPFs2fkukPF3pym6uK3oWEKW45jKlVGgybLqulcV5nLEqREOuyq4Rn4vnZss4/bbQ/g=="],
367
 
368
- "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/[email protected].5", "", { "os": "linux", "cpu": "x64" }, "sha512-DYgieNDRkTy69bWPgdsc47nAXa74P63P/RetUwYM9vYj5USyOfHCEcqIthkCuYw3dXKBhjgwe697TmL2g2jpAw=="],
369
 
370
- "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/[email protected].5", "", { "os": "linux", "cpu": "x64" }, "sha512-z2RzUvOQl0ZqrZqmCFP53tJbBXQ3UmLD/E6J7+q0e+4VaFnXCcIYTfQbHgI8f3fash+q6gK80Ko/ywEQ+bvv6Q=="],
371
 
372
- "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/[email protected].5", "", { "os": "win32", "cpu": "arm64" }, "sha512-ho1dJ4o5Q8nAOxdMkbfBu5aSqI+/bzQ0jEeHcXaEdEJzf2fSWs3HY7bIKtE6vQS8c4SmSBvls7IhGPuJxNg+2Q=="],
373
 
374
- "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/[email protected].5", "", { "os": "win32", "cpu": "x64" }, "sha512-yjw6JhtyDXr+G0aZrj3L3NlEV7CobSqOdPyfo6G3d91WEZ5b8PyGm86IAreX08Jp9DChGXEd53gWysVpWCTs+w=="],
375
 
376
- "@tailwindcss/vite": ["@tailwindcss/[email protected].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=="],
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.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=="],
399
 
400
- "@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=="],
401
 
402
- "@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=="],
403
 
404
- "@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=="],
405
 
406
- "@typescript-eslint/types": ["@typescript-eslint/types@8.23.0", "", {}, "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ=="],
407
 
408
- "@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=="],
409
 
410
- "@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=="],
411
 
412
- "@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=="],
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].5", "", {}, "sha512-DZZIKX3tA23LGTjHdnwlJOTxfICD1cPeykLLsYF1RQBI9QsCR3i0szohJfJDVjr6aNRAIio5WVO7FGB77fRHwg=="],
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.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=="],
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.5",
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.5",
64
  "tailwindcss-animate": "^1.0.7",
65
  "typescript": "~5.7.3",
66
- "typescript-eslint": "^8.23.0",
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 { GraphViewer } from '@/GraphViewer'
4
- import { cn } from '@/lib/utils'
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={cn('h-screen w-screen', message !== null && 'pointer-events-none')}>
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
- export 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 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
- <GraphSearch
151
- value={searchInitSelectedNode}
152
- onFocus={onSearchFocus}
153
- onChange={onSearchSelect}
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
- <div className="absolute top-2 right-2">
166
- <PropertiesView />
167
- </div>
 
 
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
- const checkResponse = (response: Response) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  if (!response.ok) {
54
- throw new Error(`${response.status} ${response.statusText} ${response.url}`)
 
 
55
  }
 
 
56
  }
57
 
 
58
  export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
59
- const response = await fetch(backendBaseUrl + `/graphs?label=${label}`)
60
- checkResponse(response)
61
  return await response.json()
62
  }
63
 
64
  export const getGraphLabels = async (): Promise<string[]> => {
65
- const response = await fetch(backendBaseUrl + '/graph/label/list')
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 fetch(backendBaseUrl + '/health')
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 fetch(backendBaseUrl + '/documents')
92
  return await response.json()
93
  }
94
 
95
  export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
96
- const response = await fetch(backendBaseUrl + '/documents/scan-progress')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { controlButtonVariant } from '@/lib/constants'
 
 
 
 
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="bg-background/90 absolute top-1/2 left-1/2 w-auto -translate-x-1/2 -translate-y-1/2 transform backdrop-blur-lg"
 
 
 
17
  >
18
- {!health && <AlertCircle className="h-4 w-4" />}
19
- <AlertTitle>{messageTitle}</AlertTitle>
20
-
21
- <AlertDescription>{message}</AlertDescription>
22
- <div className="h-2" />
23
- <div className="flex">
 
 
 
 
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
- Continue
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-sm rounded-xl border-2 p-2 backdrop-blur-lg">
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 text-sm">
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-md p-1">
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-md p-1">
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 { useState, useCallback } from 'react'
 
 
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 side="right" align="start" className="p-2">
 
 
 
 
 
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={enableEdgeEvents}
85
- onCheckedChange={setEnableEdgeEvents}
86
- label="Edge Events"
87
  />
88
  <LabeledCheckBox
89
  checked={enableHideUnselectedEdges}
@@ -91,10 +166,44 @@ export default function Settings() {
91
  label="Hide Unselected Edges"
92
  />
93
  <LabeledCheckBox
94
- checked={showEdgeLabel}
95
- onCheckedChange={setShowEdgeLabel}
96
- label="Show Edge 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: 'h-10 w-10'
24
  }
25
  },
26
  defaultVariants: {
@@ -39,7 +39,10 @@ interface ButtonProps
39
  }
40
 
41
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
42
- ({ className, variant, size, tooltip, side = 'right', asChild = false, ...props }, ref) => {
 
 
 
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
- setTheme: (theme: Theme) => void
 
 
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
- enableEdgeEvents: false,
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: 3,
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({ health: true, message: null, messageTitle: null })
 
 
 
 
 
 
24
  return true
25
  }
26
- set({ health: false, message: health.message, messageTitle: 'Backend Health Check Error!' })
 
 
 
 
 
 
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