enhance graph viewer with settings, status and api key
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-popover": "^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-slot": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@react-sigma/core": "^5.0.2",
|
"@react-sigma/core": "^5.0.2",
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.20.0",
|
"@eslint/js": "^9.20.0",
|
||||||
"@stylistic/eslint-plugin-js": "^3.1.0",
|
"@stylistic/eslint-plugin-js": "^3.1.0",
|
||||||
"@tailwindcss/vite": "^4.0.5",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.2",
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^22.13.1",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
@@ -54,10 +55,10 @@
|
|||||||
"graphology-types": "^0.24.8",
|
"graphology-types": "^0.24.8",
|
||||||
"prettier": "^3.5.0",
|
"prettier": "^3.5.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.0.5",
|
"tailwindcss": "^4.0.6",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "~5.7.3",
|
"typescript": "~5.7.3",
|
||||||
"typescript-eslint": "^8.23.0",
|
"typescript-eslint": "^8.24.0",
|
||||||
"vite": "^6.1.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-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-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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"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": ["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=="],
|
"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=="],
|
||||||
|
|
||||||
|
@@ -14,6 +14,7 @@
|
|||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-popover": "^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-slot": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@react-sigma/core": "^5.0.2",
|
"@react-sigma/core": "^5.0.2",
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.20.0",
|
"@eslint/js": "^9.20.0",
|
||||||
"@stylistic/eslint-plugin-js": "^3.1.0",
|
"@stylistic/eslint-plugin-js": "^3.1.0",
|
||||||
"@tailwindcss/vite": "^4.0.5",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.2",
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^22.13.1",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
@@ -60,10 +61,10 @@
|
|||||||
"graphology-types": "^0.24.8",
|
"graphology-types": "^0.24.8",
|
||||||
"prettier": "^3.5.0",
|
"prettier": "^3.5.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.0.5",
|
"tailwindcss": "^4.0.6",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "~5.7.3",
|
"typescript": "~5.7.3",
|
||||||
"typescript-eslint": "^8.23.0",
|
"typescript-eslint": "^8.24.0",
|
||||||
"vite": "^6.1.0"
|
"vite": "^6.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,27 +1,35 @@
|
|||||||
import ThemeProvider from '@/components/ThemeProvider'
|
import ThemeProvider from '@/components/ThemeProvider'
|
||||||
import MessageAlert from '@/components/MessageAlert'
|
import MessageAlert from '@/components/MessageAlert'
|
||||||
import { GraphViewer } from '@/GraphViewer'
|
import StatusIndicator from '@/components/StatusIndicator'
|
||||||
import { cn } from '@/lib/utils'
|
import GraphViewer from '@/GraphViewer'
|
||||||
import { healthCheckInterval } from '@/lib/constants'
|
import { healthCheckInterval } from '@/lib/constants'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const message = useBackendState.use.message()
|
const message = useBackendState.use.message()
|
||||||
|
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
||||||
|
|
||||||
// health check
|
// health check
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!enableHealthCheck) return
|
||||||
|
|
||||||
|
// Check immediately
|
||||||
|
useBackendState.getState().check()
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
await useBackendState.getState().check()
|
await useBackendState.getState().check()
|
||||||
}, healthCheckInterval * 1000)
|
}, healthCheckInterval * 1000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [])
|
}, [enableHealthCheck])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<div className={cn('h-screen w-screen', message !== null && 'pointer-events-none')}>
|
<div className="h-screen w-screen">
|
||||||
<GraphViewer />
|
<GraphViewer />
|
||||||
</div>
|
</div>
|
||||||
|
{enableHealthCheck && <StatusIndicator />}
|
||||||
{message !== null && <MessageAlert />}
|
{message !== null && <MessageAlert />}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
@@ -99,13 +99,17 @@ const GraphEvents = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GraphViewer = () => {
|
const GraphViewer = () => {
|
||||||
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
||||||
|
|
||||||
const selectedNode = useGraphStore.use.selectedNode()
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
const focusedNode = useGraphStore.use.focusedNode()
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
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 enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
||||||
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
||||||
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
||||||
@@ -114,9 +118,10 @@ export const GraphViewer = () => {
|
|||||||
setSigmaSettings({
|
setSigmaSettings({
|
||||||
...defaultSigmaSettings,
|
...defaultSigmaSettings,
|
||||||
enableEdgeEvents,
|
enableEdgeEvents,
|
||||||
renderEdgeLabels
|
renderEdgeLabels,
|
||||||
|
renderLabels
|
||||||
})
|
})
|
||||||
}, [enableEdgeEvents, renderEdgeLabels])
|
}, [renderLabels, enableEdgeEvents, renderEdgeLabels])
|
||||||
|
|
||||||
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
||||||
if (value === null) useGraphStore.getState().setFocusedNode(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">
|
<div className="absolute top-2 left-2 flex items-start gap-2">
|
||||||
<GraphLabels />
|
<GraphLabels />
|
||||||
<GraphSearch
|
{showNodeSearchBar && (
|
||||||
value={searchInitSelectedNode}
|
<GraphSearch
|
||||||
onFocus={onSearchFocus}
|
value={searchInitSelectedNode}
|
||||||
onChange={onSearchSelect}
|
onFocus={onSearchFocus}
|
||||||
/>
|
onChange={onSearchSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
|
<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 />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute top-2 right-2">
|
{showPropertyPanel && (
|
||||||
<PropertiesView />
|
<div className="absolute top-2 right-2">
|
||||||
</div>
|
<PropertiesView />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
|
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
|
||||||
<MiniMap width="100px" height="100px" />
|
<MiniMap width="100px" height="100px" />
|
||||||
@@ -172,3 +181,5 @@ export const GraphViewer = () => {
|
|||||||
</SigmaContainer>
|
</SigmaContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default GraphViewer
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { backendBaseUrl } from '@/lib/constants'
|
import { backendBaseUrl } from '@/lib/constants'
|
||||||
import { errorMessage } from '@/lib/utils'
|
import { errorMessage } from '@/lib/utils'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
|
// Types
|
||||||
export type LightragNodeType = {
|
export type LightragNodeType = {
|
||||||
id: string
|
id: string
|
||||||
labels: string[]
|
labels: string[]
|
||||||
@@ -49,21 +51,85 @@ export type LightragDocumentsScanProgress = {
|
|||||||
progress: number
|
progress: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkResponse = (response: Response) => {
|
export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`${response.status} ${response.statusText} ${response.url}`)
|
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> => {
|
export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
|
||||||
const response = await fetch(backendBaseUrl + `/graphs?label=${label}`)
|
const response = await fetchWithAuth(`/graphs?label=${label}`)
|
||||||
checkResponse(response)
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGraphLabels = async (): Promise<string[]> => {
|
export const getGraphLabels = async (): Promise<string[]> => {
|
||||||
const response = await fetch(backendBaseUrl + '/graph/label/list')
|
const response = await fetchWithAuth('/graph/label/list')
|
||||||
checkResponse(response)
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +137,7 @@ export const checkHealth = async (): Promise<
|
|||||||
LightragStatus | { status: 'error'; message: string }
|
LightragStatus | { status: 'error'; message: string }
|
||||||
> => {
|
> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(backendBaseUrl + '/health')
|
const response = await fetchWithAuth('/health')
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: `Health check failed. Service is currently unavailable.\n${response.status} ${response.statusText} ${response.url}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
@@ -88,11 +148,131 @@ export const checkHealth = async (): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getDocuments = async (): Promise<string[]> => {
|
export const getDocuments = async (): Promise<string[]> => {
|
||||||
const response = await fetch(backendBaseUrl + '/documents')
|
const response = await fetchWithAuth('/documents')
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
|
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()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
|
||||||
import Button from '@/components/ui/Button'
|
|
||||||
import { useBackendState } from '@/stores/state'
|
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'
|
import { AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
@@ -9,18 +12,32 @@ const MessageAlert = () => {
|
|||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
||||||
const message = useBackendState.use.message()
|
const message = useBackendState.use.message()
|
||||||
const messageTitle = useBackendState.use.messageTitle()
|
const messageTitle = useBackendState.use.messageTitle()
|
||||||
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsMounted(true)
|
||||||
|
}, 50)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
variant={health ? 'default' : 'destructive'}
|
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" />}
|
{!health && (
|
||||||
<AlertTitle>{messageTitle}</AlertTitle>
|
<div>
|
||||||
|
<AlertCircle className="size-4" />
|
||||||
<AlertDescription>{message}</AlertDescription>
|
</div>
|
||||||
<div className="h-2" />
|
)}
|
||||||
<div className="flex">
|
<div>
|
||||||
|
<AlertTitle className="font-bold">{messageTitle}</AlertTitle>
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</div>
|
||||||
|
{/* <div className="flex">
|
||||||
<div className="flex-auto" />
|
<div className="flex-auto" />
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -28,9 +45,9 @@ const MessageAlert = () => {
|
|||||||
className="text-primary max-h-8 border !p-2 text-xs"
|
className="text-primary max-h-8 border !p-2 text-xs"
|
||||||
onClick={() => useBackendState.getState().clear()}
|
onClick={() => useBackendState.getState().clear()}
|
||||||
>
|
>
|
||||||
Continue
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div> */}
|
||||||
</Alert>
|
</Alert>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -59,7 +59,7 @@ const PropertiesView = () => {
|
|||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
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' ? (
|
{currentType == 'node' ? (
|
||||||
<NodePropertiesView node={currentElement as any} />
|
<NodePropertiesView node={currentElement as any} />
|
||||||
) : (
|
) : (
|
||||||
@@ -132,7 +132,7 @@ const PropertyRow = ({
|
|||||||
tooltip?: string
|
tooltip?: string
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
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>:
|
<label className="text-primary/60 tracking-wide">{name}</label>:
|
||||||
<Text
|
<Text
|
||||||
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
|
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
|
||||||
@@ -150,7 +150,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
|
<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={'Id'} value={node.id} />
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
name={'Labels'}
|
name={'Labels'}
|
||||||
@@ -162,7 +162,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|||||||
<PropertyRow name={'Degree'} value={node.degree} />
|
<PropertyRow name={'Degree'} value={node.degree} />
|
||||||
</div>
|
</div>
|
||||||
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
|
<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)
|
{Object.keys(node.properties)
|
||||||
.sort()
|
.sort()
|
||||||
.map((name) => {
|
.map((name) => {
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||||
import { Checkbox } from '@/components/ui/Checkbox'
|
import { Checkbox } from '@/components/ui/Checkbox'
|
||||||
import Button from '@/components/ui/Button'
|
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 { controlButtonVariant } from '@/lib/constants'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useBackendState } from '@/stores/state'
|
||||||
|
|
||||||
import { SettingsIcon } from 'lucide-react'
|
import { SettingsIcon } from 'lucide-react'
|
||||||
|
|
||||||
@@ -20,7 +23,7 @@ const LabeledCheckBox = ({
|
|||||||
label: string
|
label: string
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox checked={checked} onCheckedChange={onCheckedChange} />
|
<Checkbox checked={checked} onCheckedChange={onCheckedChange} />
|
||||||
<label
|
<label
|
||||||
htmlFor="terms"
|
htmlFor="terms"
|
||||||
@@ -37,12 +40,24 @@ const LabeledCheckBox = ({
|
|||||||
*/
|
*/
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const [opened, setOpened] = useState<boolean>(false)
|
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 enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
||||||
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
||||||
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
||||||
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
|
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
|
||||||
|
|
||||||
|
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
||||||
|
const apiKey = useSettingsStore.use.apiKey()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTempApiKey(apiKey || '')
|
||||||
|
}, [apiKey, opened])
|
||||||
|
|
||||||
const setEnableNodeDrag = useCallback(
|
const setEnableNodeDrag = useCallback(
|
||||||
() => useSettingsStore.setState((pre) => ({ enableNodeDrag: !pre.enableNodeDrag })),
|
() => 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 (
|
return (
|
||||||
<Popover open={opened} onOpenChange={setOpened}>
|
<Popover open={opened} onOpenChange={setOpened}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -73,17 +122,43 @@ export default function Settings() {
|
|||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</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">
|
<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
|
<LabeledCheckBox
|
||||||
checked={enableNodeDrag}
|
checked={enableNodeDrag}
|
||||||
onCheckedChange={setEnableNodeDrag}
|
onCheckedChange={setEnableNodeDrag}
|
||||||
label="Node Draggable"
|
label="Node Draggable"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={enableEdgeEvents}
|
checked={showEdgeLabel}
|
||||||
onCheckedChange={setEnableEdgeEvents}
|
onCheckedChange={setShowEdgeLabel}
|
||||||
label="Edge Events"
|
label="Show Edge Label"
|
||||||
/>
|
/>
|
||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={enableHideUnselectedEdges}
|
checked={enableHideUnselectedEdges}
|
||||||
@@ -91,10 +166,44 @@ export default function Settings() {
|
|||||||
label="Hide Unselected Edges"
|
label="Hide Unselected Edges"
|
||||||
/>
|
/>
|
||||||
<LabeledCheckBox
|
<LabeledCheckBox
|
||||||
checked={showEdgeLabel}
|
checked={enableEdgeEvents}
|
||||||
onCheckedChange={setShowEdgeLabel}
|
onCheckedChange={setEnableEdgeEvents}
|
||||||
label="Show Edge Label"
|
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>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@@ -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
|
@@ -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
|
@@ -20,7 +20,7 @@ const buttonVariants = cva(
|
|||||||
default: 'h-10 px-4 py-2',
|
default: 'h-10 px-4 py-2',
|
||||||
sm: 'h-9 rounded-md px-3',
|
sm: 'h-9 rounded-md px-3',
|
||||||
lg: 'h-11 rounded-md px-8',
|
lg: 'h-11 rounded-md px-8',
|
||||||
icon: 'h-10 w-10'
|
icon: 'size-8'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -39,7 +39,10 @@ interface ButtonProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, 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'
|
const Comp = asChild ? Slot : 'button'
|
||||||
if (!tooltip) {
|
if (!tooltip) {
|
||||||
return (
|
return (
|
||||||
|
21
lightrag/api/graph_viewer_webui/src/components/ui/Input.tsx
Normal file
21
lightrag/api/graph_viewer_webui/src/components/ui/Input.tsx
Normal 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
|
@@ -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
|
@@ -7,39 +7,63 @@ type Theme = 'dark' | 'light' | 'system'
|
|||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
enableNodeDrag: boolean
|
|
||||||
enableEdgeEvents: boolean
|
|
||||||
enableHideUnselectedEdges: boolean
|
|
||||||
showEdgeLabel: boolean
|
|
||||||
|
|
||||||
setTheme: (theme: Theme) => void
|
setTheme: (theme: Theme) => void
|
||||||
|
|
||||||
|
showPropertyPanel: boolean
|
||||||
|
showNodeSearchBar: boolean
|
||||||
|
|
||||||
|
showNodeLabel: boolean
|
||||||
|
enableNodeDrag: boolean
|
||||||
|
|
||||||
|
showEdgeLabel: boolean
|
||||||
|
enableHideUnselectedEdges: boolean
|
||||||
|
enableEdgeEvents: boolean
|
||||||
|
|
||||||
queryLabel: string
|
queryLabel: string
|
||||||
setQueryLabel: (queryLabel: string) => void
|
setQueryLabel: (queryLabel: string) => void
|
||||||
|
|
||||||
|
enableHealthCheck: boolean
|
||||||
|
setEnableHealthCheck: (enable: boolean) => void
|
||||||
|
|
||||||
|
apiKey: string | null
|
||||||
|
setApiKey: (key: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSettingsStoreBase = create<SettingsState>()(
|
const useSettingsStoreBase = create<SettingsState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
|
|
||||||
|
showPropertyPanel: true,
|
||||||
|
showNodeSearchBar: true,
|
||||||
|
|
||||||
|
showNodeLabel: true,
|
||||||
enableNodeDrag: true,
|
enableNodeDrag: true,
|
||||||
enableEdgeEvents: false,
|
|
||||||
enableHideUnselectedEdges: true,
|
|
||||||
showEdgeLabel: false,
|
showEdgeLabel: false,
|
||||||
|
enableHideUnselectedEdges: true,
|
||||||
|
enableEdgeEvents: false,
|
||||||
|
|
||||||
queryLabel: defaultQueryLabel,
|
queryLabel: defaultQueryLabel,
|
||||||
|
enableHealthCheck: true,
|
||||||
|
|
||||||
|
apiKey: null,
|
||||||
|
|
||||||
setTheme: (theme: Theme) => set({ theme }),
|
setTheme: (theme: Theme) => set({ theme }),
|
||||||
|
|
||||||
setQueryLabel: (queryLabel: string) =>
|
setQueryLabel: (queryLabel: string) =>
|
||||||
set({
|
set({
|
||||||
queryLabel
|
queryLabel
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
||||||
|
|
||||||
|
setApiKey: (apiKey: string | null) => set({ apiKey })
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 3,
|
version: 4,
|
||||||
migrate: (state: any, version: number) => {
|
migrate: (state: any, version: number) => {
|
||||||
if (version < 2) {
|
if (version < 2) {
|
||||||
state.showEdgeLabel = false
|
state.showEdgeLabel = false
|
||||||
@@ -47,6 +71,13 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
if (version < 3) {
|
if (version < 3) {
|
||||||
state.queryLabel = defaultQueryLabel
|
state.queryLabel = defaultQueryLabel
|
||||||
}
|
}
|
||||||
|
if (version < 4) {
|
||||||
|
state.showPropertyPanel = true
|
||||||
|
state.showNodeSearchBar = true
|
||||||
|
state.showNodeLabel = true
|
||||||
|
state.enableHealthCheck = true
|
||||||
|
state.apiKey = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { createSelectors } from '@/lib/utils'
|
import { createSelectors } from '@/lib/utils'
|
||||||
import { checkHealth } from '@/api/lightrag'
|
import { checkHealth, LightragStatus } from '@/api/lightrag'
|
||||||
|
|
||||||
interface BackendState {
|
interface BackendState {
|
||||||
health: boolean
|
health: boolean
|
||||||
message: string | null
|
message: string | null
|
||||||
messageTitle: string | null
|
messageTitle: string | null
|
||||||
|
|
||||||
|
status: LightragStatus | null
|
||||||
|
|
||||||
|
lastCheckTime: number
|
||||||
|
|
||||||
check: () => Promise<boolean>
|
check: () => Promise<boolean>
|
||||||
clear: () => void
|
clear: () => void
|
||||||
setErrorMessage: (message: string, messageTitle: string) => void
|
setErrorMessage: (message: string, messageTitle: string) => void
|
||||||
@@ -16,14 +20,28 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
|||||||
health: true,
|
health: true,
|
||||||
message: null,
|
message: null,
|
||||||
messageTitle: null,
|
messageTitle: null,
|
||||||
|
lastCheckTime: Date.now(),
|
||||||
|
status: null,
|
||||||
|
|
||||||
check: async () => {
|
check: async () => {
|
||||||
const health = await checkHealth()
|
const health = await checkHealth()
|
||||||
if (health.status === 'healthy') {
|
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
|
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
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user