enhance graph viewer with settings, status and api key

This commit is contained in:
ArnoChen
2025-02-11 22:51:22 +08:00
parent bd7d4c0b17
commit 3cacc088fe
15 changed files with 639 additions and 100 deletions

View File

@@ -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=="],

View File

@@ -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"
}
}

View File

@@ -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 (
<ThemeProvider>
<div className={cn('h-screen w-screen', message !== null && 'pointer-events-none')}>
<div className="h-screen w-screen">
<GraphViewer />
</div>
{enableHealthCheck && <StatusIndicator />}
{message !== null && <MessageAlert />}
</ThemeProvider>
)

View File

@@ -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 = () => {
<div className="absolute top-2 left-2 flex items-start gap-2">
<GraphLabels />
{showNodeSearchBar && (
<GraphSearch
value={searchInitSelectedNode}
onFocus={onSearchFocus}
onChange={onSearchSelect}
/>
)}
</div>
<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 = () => {
<ThemeToggle />
</div>
{showPropertyPanel && (
<div className="absolute top-2 right-2">
<PropertiesView />
</div>
)}
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
<MiniMap width="100px" height="100px" />
@@ -172,3 +181,5 @@ export const GraphViewer = () => {
</SigmaContainer>
)
}
export default GraphViewer

View File

@@ -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<Response> => {
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<LightragGraphType> => {
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<string[]> => {
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<string[]> => {
const response = await fetch(backendBaseUrl + '/documents')
const response = await fetchWithAuth('/documents')
return await response.json()
}
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
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<QueryResponse> => {
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()
}

View File

@@ -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 (
<Alert
variant={health ? 'default' : 'destructive'}
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"
className={cn(
'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',
isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0'
)}
>
{!health && <AlertCircle className="h-4 w-4" />}
<AlertTitle>{messageTitle}</AlertTitle>
{!health && (
<div>
<AlertCircle className="size-4" />
</div>
)}
<div>
<AlertTitle className="font-bold">{messageTitle}</AlertTitle>
<AlertDescription>{message}</AlertDescription>
<div className="h-2" />
<div className="flex">
</div>
{/* <div className="flex">
<div className="flex-auto" />
<Button
size="sm"
@@ -28,9 +45,9 @@ const MessageAlert = () => {
className="text-primary max-h-8 border !p-2 text-xs"
onClick={() => useBackendState.getState().clear()}
>
Continue
Close
</Button>
</div>
</div> */}
</Alert>
)
}

View File

@@ -59,7 +59,7 @@ const PropertiesView = () => {
return <></>
}
return (
<div className="bg-background/80 max-w-sm rounded-xl border-2 p-2 backdrop-blur-lg">
<div className="bg-background/80 max-w-xs rounded-lg border-2 p-2 text-xs backdrop-blur-lg">
{currentType == 'node' ? (
<NodePropertiesView node={currentElement as any} />
) : (
@@ -132,7 +132,7 @@ const PropertyRow = ({
tooltip?: string
}) => {
return (
<div className="flex items-center gap-2 text-sm">
<div className="flex items-center gap-2">
<label className="text-primary/60 tracking-wide">{name}</label>:
<Text
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
@@ -150,7 +150,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
return (
<div className="flex flex-col gap-2">
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded-md p-1">
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
<PropertyRow name={'Id'} value={node.id} />
<PropertyRow
name={'Labels'}
@@ -162,7 +162,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
<PropertyRow name={'Degree'} value={node.degree} />
</div>
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded-md p-1">
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(node.properties)
.sort()
.map((name) => {

View File

@@ -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 (
<div className="flex gap-2">
<div className="flex items-center gap-2">
<Checkbox checked={checked} onCheckedChange={onCheckedChange} />
<label
htmlFor="terms"
@@ -37,12 +40,24 @@ const LabeledCheckBox = ({
*/
export default function Settings() {
const [opened, setOpened] = useState<boolean>(false)
const [tempApiKey, setTempApiKey] = useState<string>('') // 用于临时存储输入的API Key
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
const showNodeLabel = useSettingsStore.use.showNodeLabel()
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
const apiKey = useSettingsStore.use.apiKey()
useEffect(() => {
setTempApiKey(apiKey || '')
}, [apiKey, opened])
const setEnableNodeDrag = useCallback(
() => useSettingsStore.setState((pre) => ({ enableNodeDrag: !pre.enableNodeDrag })),
[]
@@ -66,6 +81,40 @@ export default function Settings() {
[]
)
//
const setShowPropertyPanel = useCallback(
() => useSettingsStore.setState((pre) => ({ showPropertyPanel: !pre.showPropertyPanel })),
[]
)
const setShowNodeSearchBar = useCallback(
() => useSettingsStore.setState((pre) => ({ showNodeSearchBar: !pre.showNodeSearchBar })),
[]
)
const setShowNodeLabel = useCallback(
() => useSettingsStore.setState((pre) => ({ showNodeLabel: !pre.showNodeLabel })),
[]
)
const setEnableHealthCheck = useCallback(
() => useSettingsStore.setState((pre) => ({ enableHealthCheck: !pre.enableHealthCheck })),
[]
)
const setApiKey = useCallback(async () => {
useSettingsStore.setState({ apiKey: tempApiKey || null })
await useBackendState.getState().check()
setOpened(false)
}, [tempApiKey])
const handleTempApiKeyChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setTempApiKey(e.target.value)
},
[setTempApiKey]
)
return (
<Popover open={opened} onOpenChange={setOpened}>
<PopoverTrigger asChild>
@@ -73,17 +122,43 @@ export default function Settings() {
<SettingsIcon />
</Button>
</PopoverTrigger>
<PopoverContent side="right" align="start" className="p-2">
<PopoverContent
side="right"
align="start"
className="mb-2 p-2"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="flex flex-col gap-2">
<LabeledCheckBox
checked={showPropertyPanel}
onCheckedChange={setShowPropertyPanel}
label="Show Property Panel"
/>
<LabeledCheckBox
checked={showNodeSearchBar}
onCheckedChange={setShowNodeSearchBar}
label="Show Search Bar"
/>
<Separator />
<LabeledCheckBox
checked={showNodeLabel}
onCheckedChange={setShowNodeLabel}
label="Show Node Label"
/>
<LabeledCheckBox
checked={enableNodeDrag}
onCheckedChange={setEnableNodeDrag}
label="Node Draggable"
/>
<Separator />
<LabeledCheckBox
checked={enableEdgeEvents}
onCheckedChange={setEnableEdgeEvents}
label="Edge Events"
checked={showEdgeLabel}
onCheckedChange={setShowEdgeLabel}
label="Show Edge Label"
/>
<LabeledCheckBox
checked={enableHideUnselectedEdges}
@@ -91,10 +166,44 @@ export default function Settings() {
label="Hide Unselected Edges"
/>
<LabeledCheckBox
checked={showEdgeLabel}
onCheckedChange={setShowEdgeLabel}
label="Show Edge Label"
checked={enableEdgeEvents}
onCheckedChange={setEnableEdgeEvents}
label="Edge Events"
/>
<Separator />
<LabeledCheckBox
checked={enableHealthCheck}
onCheckedChange={setEnableHealthCheck}
label="Health Check"
/>
<Separator />
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">API Key</label>
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
<div className="w-0 flex-1">
<Input
type="password"
value={tempApiKey}
onChange={handleTempApiKeyChange}
placeholder="Enter your API key"
className="max-h-full w-full min-w-0"
autoComplete="off"
/>
</div>
<Button
onClick={setApiKey}
variant="outline"
size="sm"
className="max-h-full shrink-0"
>
Save
</Button>
</form>
</div>
</div>
</PopoverContent>
</Popover>

View File

@@ -0,0 +1,65 @@
import { LightragStatus } from '@/api/lightrag'
const StatusCard = ({ status }: { status: LightragStatus | null }) => {
if (!status) {
return <div className="text-muted-foreground text-sm">Status information unavailable</div>
}
return (
<div className="min-w-[300px] space-y-3 text-sm">
<div className="space-y-1">
<h4 className="font-medium">Storage Info</h4>
<div className="text-muted-foreground grid grid-cols-2 gap-1">
<span>Working Directory:</span>
<span className="truncate">{status.working_directory}</span>
<span>Input Directory:</span>
<span className="truncate">{status.input_directory}</span>
<span>Indexed Files:</span>
<span>{status.indexed_files_count}</span>
</div>
</div>
<div className="space-y-1">
<h4 className="font-medium">LLM Configuration</h4>
<div className="text-muted-foreground grid grid-cols-2 gap-1">
<span>LLM Binding:</span>
<span>{status.configuration.llm_binding}</span>
<span>LLM Binding Host:</span>
<span>{status.configuration.llm_binding_host}</span>
<span>LLM Model:</span>
<span>{status.configuration.llm_model}</span>
<span>Max Tokens:</span>
<span>{status.configuration.max_tokens}</span>
</div>
</div>
<div className="space-y-1">
<h4 className="font-medium">Embedding Configuration</h4>
<div className="text-muted-foreground grid grid-cols-2 gap-1">
<span>Embedding Binding:</span>
<span>{status.configuration.embedding_binding}</span>
<span>Embedding Binding Host:</span>
<span>{status.configuration.embedding_binding_host}</span>
<span>Embedding Model:</span>
<span>{status.configuration.embedding_model}</span>
</div>
</div>
<div className="space-y-1">
<h4 className="font-medium">Storage Configuration</h4>
<div className="text-muted-foreground grid grid-cols-2 gap-1">
<span>KV Storage:</span>
<span>{status.configuration.kv_storage}</span>
<span>Doc Status Storage:</span>
<span>{status.configuration.doc_status_storage}</span>
<span>Graph Storage:</span>
<span>{status.configuration.graph_storage}</span>
<span>Vector Storage:</span>
<span>{status.configuration.vector_storage}</span>
</div>
</div>
</div>
)
}
export default StatusCard

View File

@@ -0,0 +1,48 @@
import { cn } from '@/lib/utils'
import { useBackendState } from '@/stores/state'
import { useEffect, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
import StatusCard from '@/components/StatusCard'
const StatusIndicator = () => {
const health = useBackendState.use.health()
const lastCheckTime = useBackendState.use.lastCheckTime()
const status = useBackendState.use.status()
const [animate, setAnimate] = useState(false)
// listen to health change
useEffect(() => {
setAnimate(true)
const timer = setTimeout(() => setAnimate(false), 300)
return () => clearTimeout(timer)
}, [lastCheckTime])
return (
<div className="fixed right-4 bottom-4 flex items-center gap-2 opacity-80 select-none">
<Popover>
<PopoverTrigger asChild>
<div className="flex cursor-help items-center gap-2">
<div
className={cn(
'h-3 w-3 rounded-full transition-all duration-300',
'shadow-[0_0_8px_rgba(0,0,0,0.2)]',
health ? 'bg-green-500' : 'bg-red-500',
animate && 'scale-125',
animate && health && 'shadow-[0_0_12px_rgba(34,197,94,0.4)]',
animate && !health && 'shadow-[0_0_12px_rgba(239,68,68,0.4)]'
)}
/>
<span className="text-muted-foreground text-xs">
{health ? 'Connected' : 'Disconnected'}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-auto" side="top" align="end">
<StatusCard status={status} />
</PopoverContent>
</Popover>
</div>
)
}
export default StatusIndicator

View File

@@ -20,7 +20,7 @@ const buttonVariants = cva(
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10'
icon: 'size-8'
}
},
defaultVariants: {
@@ -39,7 +39,10 @@ interface ButtonProps
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, tooltip, side = 'right', asChild = false, ...props }, ref) => {
(
{ className, variant, tooltip, size = 'icon', side = 'right', asChild = false, ...props },
ref
) => {
const Comp = asChild ? Slot : 'button'
if (!tooltip) {
return (

View File

@@ -0,0 +1,21 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'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',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export default Input

View File

@@ -0,0 +1,24 @@
import * as React from 'react'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import { cn } from '@/lib/utils'
const Separator = React.forwardRef<
React.ComponentRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
))
Separator.displayName = SeparatorPrimitive.Root.displayName
export default Separator

View File

@@ -7,39 +7,63 @@ type Theme = 'dark' | 'light' | 'system'
interface SettingsState {
theme: Theme
enableNodeDrag: boolean
enableEdgeEvents: boolean
enableHideUnselectedEdges: boolean
showEdgeLabel: boolean
setTheme: (theme: Theme) => void
showPropertyPanel: boolean
showNodeSearchBar: boolean
showNodeLabel: boolean
enableNodeDrag: boolean
showEdgeLabel: boolean
enableHideUnselectedEdges: boolean
enableEdgeEvents: boolean
queryLabel: string
setQueryLabel: (queryLabel: string) => void
enableHealthCheck: boolean
setEnableHealthCheck: (enable: boolean) => void
apiKey: string | null
setApiKey: (key: string | null) => void
}
const useSettingsStoreBase = create<SettingsState>()(
persist(
(set) => ({
theme: 'system',
showPropertyPanel: true,
showNodeSearchBar: true,
showNodeLabel: true,
enableNodeDrag: true,
enableEdgeEvents: false,
enableHideUnselectedEdges: true,
showEdgeLabel: false,
enableHideUnselectedEdges: true,
enableEdgeEvents: false,
queryLabel: defaultQueryLabel,
enableHealthCheck: true,
apiKey: null,
setTheme: (theme: Theme) => set({ theme }),
setQueryLabel: (queryLabel: string) =>
set({
queryLabel
})
}),
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
setApiKey: (apiKey: string | null) => set({ apiKey })
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
version: 3,
version: 4,
migrate: (state: any, version: number) => {
if (version < 2) {
state.showEdgeLabel = false
@@ -47,6 +71,13 @@ const useSettingsStoreBase = create<SettingsState>()(
if (version < 3) {
state.queryLabel = defaultQueryLabel
}
if (version < 4) {
state.showPropertyPanel = true
state.showNodeSearchBar = true
state.showNodeLabel = true
state.enableHealthCheck = true
state.apiKey = null
}
}
}
)

View File

@@ -1,12 +1,16 @@
import { create } from 'zustand'
import { createSelectors } from '@/lib/utils'
import { checkHealth } from '@/api/lightrag'
import { checkHealth, LightragStatus } from '@/api/lightrag'
interface BackendState {
health: boolean
message: string | null
messageTitle: string | null
status: LightragStatus | null
lastCheckTime: number
check: () => Promise<boolean>
clear: () => void
setErrorMessage: (message: string, messageTitle: string) => void
@@ -16,14 +20,28 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
health: true,
message: null,
messageTitle: null,
lastCheckTime: Date.now(),
status: null,
check: async () => {
const health = await checkHealth()
if (health.status === 'healthy') {
set({ health: true, message: null, messageTitle: null })
set({
health: true,
message: null,
messageTitle: null,
lastCheckTime: Date.now(),
status: health
})
return true
}
set({ health: false, message: health.message, messageTitle: 'Backend Health Check Error!' })
set({
health: false,
message: health.message,
messageTitle: 'Backend Health Check Error!',
lastCheckTime: Date.now(),
status: null
})
return false
},