add properties view
This commit is contained in:
@@ -37,23 +37,23 @@
|
|||||||
"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.4",
|
"@tailwindcss/vite": "^4.0.5",
|
||||||
"@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",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||||
"eslint": "^9.20.0",
|
"eslint": "^9.20.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.18",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"graphology-types": "^0.24.8",
|
"graphology-types": "^0.24.8",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.5.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.0.4",
|
"tailwindcss": "^4.0.5",
|
||||||
"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.23.0",
|
||||||
@@ -320,59 +320,59 @@
|
|||||||
|
|
||||||
"@stylistic/eslint-plugin-js": ["@stylistic/eslint-plugin-js@3.1.0", "", { "dependencies": { "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0" }, "peerDependencies": { "eslint": ">=8.40.0" } }, "sha512-lQktsOiCr8S6StG29C5fzXYxLOD6ID1rp4j6TRS+E/qY1xd59Fm7dy5qm9UauJIEoSTlYx6yGsCHYh5UkgXPyg=="],
|
"@stylistic/eslint-plugin-js": ["@stylistic/eslint-plugin-js@3.1.0", "", { "dependencies": { "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0" }, "peerDependencies": { "eslint": ">=8.40.0" } }, "sha512-lQktsOiCr8S6StG29C5fzXYxLOD6ID1rp4j6TRS+E/qY1xd59Fm7dy5qm9UauJIEoSTlYx6yGsCHYh5UkgXPyg=="],
|
||||||
|
|
||||||
"@swc/core": ["@swc/core@1.10.14", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.17" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.10.14", "@swc/core-darwin-x64": "1.10.14", "@swc/core-linux-arm-gnueabihf": "1.10.14", "@swc/core-linux-arm64-gnu": "1.10.14", "@swc/core-linux-arm64-musl": "1.10.14", "@swc/core-linux-x64-gnu": "1.10.14", "@swc/core-linux-x64-musl": "1.10.14", "@swc/core-win32-arm64-msvc": "1.10.14", "@swc/core-win32-ia32-msvc": "1.10.14", "@swc/core-win32-x64-msvc": "1.10.14" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WSrnE6JRnH20ZYjOOgSS4aOaPv9gxlkI2KRkN24kagbZnPZMnN8bZZyzw1rrLvwgpuRGv17Uz+hflosbR+SP6w=="],
|
"@swc/core": ["@swc/core@1.10.15", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.17" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.10.15", "@swc/core-darwin-x64": "1.10.15", "@swc/core-linux-arm-gnueabihf": "1.10.15", "@swc/core-linux-arm64-gnu": "1.10.15", "@swc/core-linux-arm64-musl": "1.10.15", "@swc/core-linux-x64-gnu": "1.10.15", "@swc/core-linux-x64-musl": "1.10.15", "@swc/core-win32-arm64-msvc": "1.10.15", "@swc/core-win32-ia32-msvc": "1.10.15", "@swc/core-win32-x64-msvc": "1.10.15" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-/iFeQuNaGdK7mfJbQcObhAhsMqLT7qgMYl7jX2GEIO+VDTejESpzAyKwaMeYXExN8D6e5BRHBCe7M5YlsuzjDA=="],
|
||||||
|
|
||||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.10.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Dh4VyrhDDb05tdRmqJ/MucOPMTnrB4pRJol18HVyLlqu1HOT5EzonUniNTCdQbUXjgdv5UVJSTE1lYTzrp+myA=="],
|
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.10.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zFdZ6/yHqMCPk7OhLFqHy/MQ1EqJhcZMpNHd1gXYT7VRU3FaqvvKETrUlG3VYl65McPC7AhMRfXPyJ0JO/jARQ=="],
|
||||||
|
|
||||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.10.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-KpzotL/I0O12RE3tF8NmQErINv0cQe/0mnN/Q50ESFzB5kU6bLgp2HMnnwDTm/XEZZRJCNe0oc9WJ5rKbAJFRQ=="],
|
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.10.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-8g4yiQwbr8fxOOjKXdot0dEkE5zgE8uNZudLy/ZyAhiwiZ8pbJ8/wVrDOu6dqbX7FBXAoDnvZ7fwN1jk4C8jdA=="],
|
||||||
|
|
||||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.10.14", "", { "os": "linux", "cpu": "arm" }, "sha512-20yRXZjMJVz1wp1TcscKiGTVXistG+saIaxOmxSNQia1Qun3hSWLL+u6+5kXbfYGr7R2N6kqSwtZbIfJI25r9Q=="],
|
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.10.15", "", { "os": "linux", "cpu": "arm" }, "sha512-rl+eVOltl2+7WXOnvmWBpMgh6aO13G5x0U0g8hjwlmD6ku3Y9iRcThpOhm7IytMEarUp5pQxItNoPq+VUGjVHg=="],
|
||||||
|
|
||||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.10.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gy7cGrNkiMfPxQyLGxdgXPwyWzNzbHuWycJFcoKBihxZKZIW8hkPBttkGivuLC+0qOgsV2/U+S7tlvAju7FtmQ=="],
|
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.10.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-qxWEQeyAJMWJqjaN4hi58WMpPdt3Tn0biSK9CYRegQtvZWCbewr6v2agtSu5AZ2rudeH6OfCWAMDQQeSgn6PJQ=="],
|
||||||
|
|
||||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.10.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-+oYVqJvFw62InZ8PIy1rBACJPC2WTe4vbVb9kM1jJj2D7dKLm9acnnYIVIDsM5Wo7Uab8RvPHXVbs19IBurzuw=="],
|
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.10.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-QcELd9/+HjZx0WCxRrKcyKGWTiQ0485kFb5w8waxcSNd0d9Lgk4EFfWWVyvIb5gIHpDQmhrgzI/yRaWQX4YSZQ=="],
|
||||||
|
|
||||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.10.14", "", { "os": "linux", "cpu": "x64" }, "sha512-OmEbVEKQFLQVHwo4EJl9osmlulURy46k232Opfpn/1ji0t2KcNCci3POsnfMuoZjLkGJv8vGNJdPQxX+CP+wSA=="],
|
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.10.15", "", { "os": "linux", "cpu": "x64" }, "sha512-S1+ZEEn3+a/MiMeQqQypbwTGoBG8/sPoCvpNbk+uValyygT+jSn3U0xVr45FbukpmMB+NhBMqfedMLqKA0QnJA=="],
|
||||||
|
|
||||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.10.14", "", { "os": "linux", "cpu": "x64" }, "sha512-OZW+Icm8DMPqHbhdxplkuG8qrNnPk5i7xJOZWYi1y5bTjgGFI4nEzrsmmeHKMdQTaWwsFrm3uK1rlyQ48MmXmg=="],
|
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.10.15", "", { "os": "linux", "cpu": "x64" }, "sha512-qW+H9g/2zTJ4jP7NDw4VAALY0ZlNEKzYsEoSj/HKi7k3tYEHjMzsxjfsY9I8WZCft23bBdV3RTCPoxCshaj1CQ=="],
|
||||||
|
|
||||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.10.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-sTvc+xrDQXy3HXZFtTEClY35Efvuc3D+busYm0+rb1+Thau4HLRY9WP+sOKeGwH9/16rzfzYEqD7Ds8A9ykrHw=="],
|
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.10.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-AhRB11aA6LxjIqut+mg7qsu/7soQDmbK6MKR9nP3hgBszpqtXbRba58lr24xIbBCMr+dpo6kgEapWt+t5Po6Zg=="],
|
||||||
|
|
||||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.10.14", "", { "os": "win32", "cpu": "ia32" }, "sha512-j2iQ4y9GWTKtES5eMU0sDsFdYni7IxME7ejFej25Tv3Fq4B+U9tgtYWlJwh1858nIWDXelHiKcSh/UICAyVMdQ=="],
|
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.10.15", "", { "os": "win32", "cpu": "ia32" }, "sha512-UGdh430TQwbDn6KjgvRTg1fO022sbQ4yCCHUev0+5B8uoBwi9a89qAz3emy2m56C8TXxUoihW9Y9OMfaRwPXUw=="],
|
||||||
|
|
||||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.10.14", "", { "os": "win32", "cpu": "x64" }, "sha512-TYtWkUSMkjs0jGPeWdtWbex4B+DlQZmN/ySVLiPI+EltYCLEXsFMkVFq6aWn48dqFHggFK0UYfvDrJUR2c3Qxg=="],
|
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.10.15", "", { "os": "win32", "cpu": "x64" }, "sha512-XJzBCqO1m929qbJsOG7FZXQWX26TnEoMctS3QjuCoyBmkHxxQmZsy78KjMes1aomTcKHCyFYgrRGWgVmk7tT4Q=="],
|
||||||
|
|
||||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||||
|
|
||||||
"@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.4", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.4" } }, "sha512-VLFq80IyoV1hsHPcCm1mmlyPyUT6NlovQLoO2y7PGm84mW94ZrNJ7ax5H6K4M7Aj/fdMfem5IX7Ka+LXWZpDGg=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.0.5", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.5" } }, "sha512-ffTz4DX1cgr4XPuqjhm32YV6Lyx58R1CxAAnSFTamg6wXwfk3oWdb6exgAbGesPzvUgicTO0gwUdQGSsg4nNog=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.4", "@tailwindcss/oxide-darwin-arm64": "4.0.4", "@tailwindcss/oxide-darwin-x64": "4.0.4", "@tailwindcss/oxide-freebsd-x64": "4.0.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.4", "@tailwindcss/oxide-linux-arm64-musl": "4.0.4", "@tailwindcss/oxide-linux-x64-gnu": "4.0.4", "@tailwindcss/oxide-linux-x64-musl": "4.0.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.4", "@tailwindcss/oxide-win32-x64-msvc": "4.0.4" } }, "sha512-vPpu30KFLiGyPOoElkYt8WRvzGKVrrOz49KpfiGGtnQGmyUpL8VCbJzzEEcpKT5BpaaQidhFok+OXscf6hHjOQ=="],
|
"@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-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.4", "", { "os": "android", "cpu": "arm64" }, "sha512-hiGUA8d15ynH/LdurQNObnuTjri7i4ApAzhesusNxoz4br7vhZ6QO5CFgniYAYNZvf8Q8wCTBg0nj61RalBeVQ=="],
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.5", "", { "os": "android", "cpu": "arm64" }, "sha512-kK/ik8aIAKWDIEYDZGUCJcnU1qU5sPoMBlVzPvtsUqiV6cSHcnVRUdkcLwKqTeUowzZtjjRiamELLd9Gb0x5BQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vTca+ysNl8BYmYJTni9pLC+L3S4bvrj0ai1eUV3yYXYa5Cpugr5Fni6ylV0gcTZOyETm2RCCJ/0azU6MgqE6HA=="],
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vkbXFv0FfAEbrSa5NBjFEE+xi06ha7mxuxjY8LRn7d7/tBGrAZOEJnnsEbB6M1+x2pGRTjjei0XyTIXdVCglJA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-rxPWb5AQJ/aAM/5UDCjaQaMYIcrZHe/Dr9xZu9+P9nJf3WAweNsGi+e+SW9EYGRiF3hkBtP2dvxVNAkTiEbNQQ=="],
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-PedA64rHBXEa4e6abBWE4Yj4gHulfPb5T+rBNnX+WGkjjge5Txa2oS99TLmJ5BPDkXXqz/Ba7oweWIDDG7i5NQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UOnRHzlS5V5cxaMgBo6rk1E92tTDUtO/falc9vOpNiRdWhNcofYNN9zvZP63Wuo5FC6/XCyAnJo6OXUm18TwrQ=="],
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-silz3nuZdEYDfic3v/ooVUQChj9hbxDSee43GCQNwr/iD9L4K/JsZtoNqr0w69pUkvWcKINOGOG0r7WqUqkAeg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-0Ry9Qfnf22rmJwHxsCFmHQIl5RZw+yOUUGHaqNT42REL8r308cU/bi4UqdrjqVRfAlu51gOGxTRf2NRueczuIA=="],
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-ElneG75XS64B9I2G83A/Hc7EtNVOD5xahs7avq0aeW7mEX6CtMc8m8RCXMn3jGhz8enFE52l6QU0wO7iVkEtXQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-5a7WD30nVdI7Rl1ohZ0Ojj9t5yRnZkJBizvh3uIW52h9UeNpon8TfoknF6rU/TwD32dQ0Cjo5CcCHtQ2wW9PCA=="],
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-8yoXpWTeIFaByUaKy2qRAppznLVaDHP9xYCAbS3FG7+uUwHi8CHE4TcomM7eyamo0U7dbUIDgKMGoAX5s2iVrA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-m6s5jKSqos07l6NtHFd49Ljcaw4jIWHE7jq6eNPNz9SCzQqRzs4esP1t7jH8UljQ7JffKOl7yZPwK5Nf+irliw=="],
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-BDlVSiiJ08GRz9KKnXgaPFs2fkukPF3pym6uK3oWEKW45jKlVGgybLqulcV5nLEqREOuyq4Rn4vnZss4/bbQ/g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-K5dBjGHzby9eyUBwy9YHFhKY+5i8fzIBZM1NBWp6L2xpM7OzW9WJDgNcgESkZami9g+EozkQLt3ZmMZHAieXkw=="],
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-DYgieNDRkTy69bWPgdsc47nAXa74P63P/RetUwYM9vYj5USyOfHCEcqIthkCuYw3dXKBhjgwe697TmL2g2jpAw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-J8sskt+fA5ooq+kxy0Tf4E2TRWZD9Y8j3K+pnBwp9zdilLmSd8OHrB3e0/rO78KveZ6BE9ae75cKOWrT6wONmw=="],
|
"@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-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-flFaaMc77NQbz0Fq73wBs9EH2lX1Oc2Z/3JuxoewpnGHpAGJ/j05tvBNMyTaGrKcHvf/+dk+mCDxb6+PmzGgnQ=="],
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-ho1dJ4o5Q8nAOxdMkbfBu5aSqI+/bzQ0jEeHcXaEdEJzf2fSWs3HY7bIKtE6vQS8c4SmSBvls7IhGPuJxNg+2Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-WzMA0aL/24/JyNrv2Yhr/Og24QGRPWJMjRyCJ4HRoGMs6/8svOQKrnnZ/9LUFwn56irAndFBjWWnlaqykH+g5Q=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.5", "", { "os": "win32", "cpu": "x64" }, "sha512-yjw6JhtyDXr+G0aZrj3L3NlEV7CobSqOdPyfo6G3d91WEZ5b8PyGm86IAreX08Jp9DChGXEd53gWysVpWCTs+w=="],
|
||||||
|
|
||||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.0.4", "", { "dependencies": { "@tailwindcss/node": "^4.0.4", "@tailwindcss/oxide": "^4.0.4", "lightningcss": "^1.29.1", "tailwindcss": "4.0.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-zrWGbluPeXeoetUQoDFmt1dQIeiOBThfznla7zPIqST69rMmiDD4SZwJrHVoL5CvXz06AYQXz/M/jELSakL7Rg=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -410,7 +410,7 @@
|
|||||||
|
|
||||||
"@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.23.0", "", { "dependencies": { "@typescript-eslint/types": "8.23.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.7.2", "", { "dependencies": { "@swc/core": "^1.7.26" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew=="],
|
"@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=="],
|
||||||
|
|
||||||
"@yomguithereal/helpers": ["@yomguithereal/helpers@1.1.1", "", {}, "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg=="],
|
"@yomguithereal/helpers": ["@yomguithereal/helpers@1.1.1", "", {}, "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg=="],
|
||||||
|
|
||||||
@@ -540,7 +540,7 @@
|
|||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="],
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="],
|
||||||
|
|
||||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.18", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw=="],
|
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
|
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
|
||||||
|
|
||||||
@@ -834,7 +834,7 @@
|
|||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
|
"prettier": ["prettier@3.5.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA=="],
|
||||||
|
|
||||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
|
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
|
||||||
|
|
||||||
@@ -932,7 +932,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.4", "", {}, "sha512-/ezDLEkOLf1lXkr9F2iI5BHJbexJpty5zkV2B8bGHCqAdbc9vk85Jgdkq+ZOvNkNPa3yAaqJ8DjRt584Bc84kw=="],
|
"tailwindcss": ["tailwindcss@4.0.5", "", {}, "sha512-DZZIKX3tA23LGTjHdnwlJOTxfICD1cPeykLLsYF1RQBI9QsCR3i0szohJfJDVjr6aNRAIio5WVO7FGB77fRHwg=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
21
lightrag/api/graph_viewer_webui/components.json
Normal file
21
lightrag/api/graph_viewer_webui/components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
@@ -43,23 +43,23 @@
|
|||||||
"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.4",
|
"@tailwindcss/vite": "^4.0.5",
|
||||||
"@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",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||||
"eslint": "^9.20.0",
|
"eslint": "^9.20.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.18",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"graphology-types": "^0.24.8",
|
"graphology-types": "^0.24.8",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.5.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.0.4",
|
"tailwindcss": "^4.0.5",
|
||||||
"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.23.0",
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||||
// import { MiniMap } from '@react-sigma/minimap'
|
// import { MiniMap } from '@react-sigma/minimap'
|
||||||
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
||||||
import { Settings as SigmaSettings } from 'sigma/settings'
|
import { Settings as SigmaSettings } from 'sigma/settings'
|
||||||
import { GraphSearchOption } from '@react-sigma/graph-search'
|
import { GraphSearchOption, OptionItem } from '@react-sigma/graph-search'
|
||||||
import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/rendering'
|
import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/rendering'
|
||||||
import { NodeBorderProgram } from '@sigma/node-border'
|
import { NodeBorderProgram } from '@sigma/node-border'
|
||||||
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
|
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
|
||||||
@@ -15,8 +15,10 @@ import ZoomControl from '@/components/ZoomControl'
|
|||||||
import FullScreenControl from '@/components/FullScreenControl'
|
import FullScreenControl from '@/components/FullScreenControl'
|
||||||
import Settings from '@/components/Settings'
|
import Settings from '@/components/Settings'
|
||||||
import GraphSearch from '@/components/GraphSearch'
|
import GraphSearch from '@/components/GraphSearch'
|
||||||
|
import PropertiesView from '@/components/PropertiesView'
|
||||||
|
|
||||||
import { useSettingsStore } from '@/lib/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
|
|
||||||
import '@react-sigma/core/lib/style.css'
|
import '@react-sigma/core/lib/style.css'
|
||||||
import '@react-sigma/graph-search/lib/style.css'
|
import '@react-sigma/graph-search/lib/style.css'
|
||||||
@@ -97,10 +99,11 @@ const GraphEvents = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GraphViewer = () => {
|
export const GraphViewer = () => {
|
||||||
const [selectedNode, setSelectedNode] = useState<string | null>(null)
|
|
||||||
const [focusedNode, setFocusedNode] = useState<string | null>(null)
|
|
||||||
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
||||||
const [autoMoveToFocused, setAutoMoveToFocused] = useState(false)
|
|
||||||
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
|
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
||||||
|
|
||||||
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
||||||
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
||||||
@@ -114,45 +117,39 @@ export const GraphViewer = () => {
|
|||||||
})
|
})
|
||||||
}, [enableEdgeEvents, renderEdgeLabels])
|
}, [enableEdgeEvents, renderEdgeLabels])
|
||||||
|
|
||||||
const onFocus = useCallback(
|
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
||||||
(value: GraphSearchOption | null) => {
|
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
||||||
if (value === null) setFocusedNode(null)
|
else if (value.type === 'nodes') useGraphStore.getState().setFocusedNode(value.id)
|
||||||
else if (value.type === 'nodes') setFocusedNode(value.id)
|
}, [])
|
||||||
},
|
|
||||||
[setFocusedNode]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onSelect = useCallback(
|
const onSearchSelect = useCallback((value: GraphSearchOption | null) => {
|
||||||
(value: GraphSearchOption | null) => {
|
if (value === null) {
|
||||||
if (value === null) setSelectedNode(null)
|
useGraphStore.getState().setSelectedNode(null)
|
||||||
else if (value.type === 'nodes') {
|
} else if (value.type === 'nodes') {
|
||||||
setAutoMoveToFocused(true)
|
useGraphStore.getState().setSelectedNode(value.id, true)
|
||||||
setSelectedNode(value.id)
|
|
||||||
setTimeout(() => setAutoMoveToFocused(false), 100)
|
|
||||||
}
|
}
|
||||||
},
|
}, [])
|
||||||
[setSelectedNode, setAutoMoveToFocused]
|
|
||||||
|
const autoFocusedNode = useMemo(() => focusedNode ?? selectedNode, [focusedNode, selectedNode])
|
||||||
|
const searchInitSelectedNode = useMemo(
|
||||||
|
(): OptionItem | null => (selectedNode ? { type: 'nodes', id: selectedNode } : null),
|
||||||
|
[selectedNode]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
|
<SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
|
||||||
<GraphControl
|
<GraphControl />
|
||||||
selectedNode={selectedNode}
|
|
||||||
setSelectedNode={setSelectedNode}
|
|
||||||
focusedNode={focusedNode}
|
|
||||||
setFocusedNode={setFocusedNode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{enableNodeDrag && <GraphEvents />}
|
{enableNodeDrag && <GraphEvents />}
|
||||||
|
|
||||||
<FocusOnNode node={focusedNode ?? selectedNode} move={autoMoveToFocused} />
|
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
||||||
|
|
||||||
<div className="absolute top-2 right-2">
|
<div className="absolute top-2 left-2">
|
||||||
<GraphSearch
|
<GraphSearch
|
||||||
type="nodes"
|
type="nodes"
|
||||||
value={selectedNode ? { type: 'nodes', id: selectedNode } : null}
|
value={searchInitSelectedNode}
|
||||||
onFocus={onFocus}
|
onFocus={onSearchFocus}
|
||||||
onChange={onSelect}
|
onChange={onSearchSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,6 +161,10 @@ export const GraphViewer = () => {
|
|||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<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" />
|
||||||
</div> */}
|
</div> */}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useCamera, useSigma } from '@react-sigma/core'
|
import { useCamera, useSigma } from '@react-sigma/core'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that highlights a node and centers the camera on it.
|
* Component that highlights a node and centers the camera on it.
|
||||||
@@ -14,7 +15,10 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!node) return
|
if (!node) return
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
||||||
if (move) gotoNode(node)
|
if (move) {
|
||||||
|
gotoNode(node)
|
||||||
|
useGraphStore.getState().setMoveToSelectedNode(false)
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
||||||
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
||||||
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
||||||
import useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
import useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
||||||
import useTheme from '@/hooks/useTheme'
|
import useTheme from '@/hooks/useTheme'
|
||||||
import * as Constants from '@/lib/constants'
|
import * as Constants from '@/lib/constants'
|
||||||
import { useSettingsStore } from '@/lib/settings'
|
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
|
|
||||||
const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
||||||
if (ev.type.startsWith('mouse')) {
|
if (ev.type.startsWith('mouse')) {
|
||||||
@@ -18,19 +20,7 @@ const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphControl = ({
|
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
|
||||||
disableHoverEffect,
|
|
||||||
selectedNode,
|
|
||||||
setSelectedNode,
|
|
||||||
focusedNode,
|
|
||||||
setFocusedNode
|
|
||||||
}: {
|
|
||||||
disableHoverEffect?: boolean
|
|
||||||
selectedNode: string | null
|
|
||||||
setSelectedNode: (node: string | null) => void
|
|
||||||
focusedNode: string | null
|
|
||||||
setFocusedNode: (node: string | null) => void
|
|
||||||
}) => {
|
|
||||||
const { lightrageGraph } = useLightragGraph()
|
const { lightrageGraph } = useLightragGraph()
|
||||||
const sigma = useSigma<NodeType, EdgeType>()
|
const sigma = useSigma<NodeType, EdgeType>()
|
||||||
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
||||||
@@ -39,11 +29,13 @@ const GraphControl = ({
|
|||||||
const { assign: assignLayout } = useLayoutForceAtlas2({
|
const { assign: assignLayout } = useLayoutForceAtlas2({
|
||||||
iterations: 20
|
iterations: 20
|
||||||
})
|
})
|
||||||
const [focusedEdge, setfocusedEdge] = useState<string | null>(null)
|
|
||||||
const [selectedEdge, setSelectedEdge] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
||||||
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
|
const selectedEdge = useGraphStore.use.selectedEdge()
|
||||||
|
const focusedEdge = useGraphStore.use.focusedEdge()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount
|
* When component mount
|
||||||
@@ -58,6 +50,9 @@ const GraphControl = ({
|
|||||||
Object.assign(graph, { __force_applied: true })
|
Object.assign(graph, { __force_applied: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
||||||
|
useGraphStore.getState()
|
||||||
|
|
||||||
// Register the events
|
// Register the events
|
||||||
registerEvents({
|
registerEvents({
|
||||||
enterNode: (event) => {
|
enterNode: (event) => {
|
||||||
@@ -80,20 +75,17 @@ const GraphControl = ({
|
|||||||
},
|
},
|
||||||
enterEdge: (event) => {
|
enterEdge: (event) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
setfocusedEdge(event.edge)
|
setFocusedEdge(event.edge)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leaveEdge: (event) => {
|
leaveEdge: (event) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
setfocusedEdge(null)
|
setFocusedEdge(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickStage: () => {
|
clickStage: () => clearSelection()
|
||||||
setSelectedEdge(null)
|
|
||||||
setSelectedNode(null)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}, [assignLayout, loadGraph, registerEvents, lightrageGraph, setFocusedNode, setSelectedNode])
|
}, [assignLayout, loadGraph, registerEvents, lightrageGraph])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount or hovered node change
|
* When component mount or hovered node change
|
||||||
|
@@ -70,7 +70,7 @@ export const GraphSearchInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
className="w-52 rounded-xl border-1 backdrop-blur-lg"
|
className="bg-background/20 w-52 rounded-xl border-1 opacity-60 backdrop-blur-lg transition-opacity hover:opacity-100"
|
||||||
fetcher={loadOptions}
|
fetcher={loadOptions}
|
||||||
renderOption={OptionComponent}
|
renderOption={OptionComponent}
|
||||||
getOptionValue={(item) => item.id}
|
getOptionValue={(item) => item.id}
|
||||||
|
@@ -0,0 +1,231 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
|
||||||
|
import Text from '@/components/ui/Text'
|
||||||
|
import useLightragGraph from '@/hooks/useLightragGraph'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that view properties of elements in graph.
|
||||||
|
*/
|
||||||
|
const PropertiesView = () => {
|
||||||
|
const { getNode, getEdge } = useLightragGraph()
|
||||||
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
|
const selectedEdge = useGraphStore.use.selectedEdge()
|
||||||
|
const focusedEdge = useGraphStore.use.focusedEdge()
|
||||||
|
|
||||||
|
const [currentElement, setCurrentElement] = useState<NodeType | EdgeType | null>(null)
|
||||||
|
const [currentType, setCurrentType] = useState<'node' | 'edge' | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let type: 'node' | 'edge' | null = null
|
||||||
|
let element: RawNodeType | RawEdgeType | null = null
|
||||||
|
if (focusedNode) {
|
||||||
|
type = 'node'
|
||||||
|
element = getNode(focusedNode)
|
||||||
|
} else if (selectedNode) {
|
||||||
|
type = 'node'
|
||||||
|
element = getNode(selectedNode)
|
||||||
|
} else if (focusedEdge) {
|
||||||
|
type = 'edge'
|
||||||
|
element = getEdge(focusedEdge, true)
|
||||||
|
} else if (selectedEdge) {
|
||||||
|
type = 'edge'
|
||||||
|
element = getEdge(selectedEdge, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
if (type == 'node') {
|
||||||
|
setCurrentElement(refineNodeProperties(element as any))
|
||||||
|
} else {
|
||||||
|
setCurrentElement(refineEdgeProperties(element as any))
|
||||||
|
}
|
||||||
|
setCurrentType(type)
|
||||||
|
} else {
|
||||||
|
setCurrentElement(null)
|
||||||
|
setCurrentType(null)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
focusedNode,
|
||||||
|
selectedNode,
|
||||||
|
focusedEdge,
|
||||||
|
selectedEdge,
|
||||||
|
setCurrentElement,
|
||||||
|
setCurrentType,
|
||||||
|
getNode,
|
||||||
|
getEdge
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!currentElement) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="bg-background/20 max-w-sm rounded-xl border-2 p-2 backdrop-blur-lg">
|
||||||
|
{currentType == 'node' ? (
|
||||||
|
<NodePropertiesView node={currentElement as any} />
|
||||||
|
) : (
|
||||||
|
<EdgePropertiesView edge={currentElement as any} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeType = RawNodeType & {
|
||||||
|
relationships: {
|
||||||
|
type: string
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdgeType = RawEdgeType & {
|
||||||
|
sourceNode?: RawNodeType
|
||||||
|
targetNode?: RawNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
const refineNodeProperties = (node: RawNodeType): NodeType => {
|
||||||
|
const state = useGraphStore.getState()
|
||||||
|
const relationships = []
|
||||||
|
|
||||||
|
if (state.sigmaGraph && state.rawGraph) {
|
||||||
|
for (const edgeId of state.sigmaGraph.edges(node.id)) {
|
||||||
|
const edge = state.rawGraph.getEdge(edgeId, true)
|
||||||
|
if (edge) {
|
||||||
|
const isTarget = node.id === edge.source
|
||||||
|
const neighbourId = isTarget ? edge.target : edge.source
|
||||||
|
const neighbour = state.rawGraph.getNode(neighbourId)
|
||||||
|
if (neighbour) {
|
||||||
|
relationships.push({
|
||||||
|
type: isTarget ? 'Target' : 'Source',
|
||||||
|
id: neighbourId,
|
||||||
|
label: neighbour.labels.join(', ')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
relationships
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refineEdgeProperties = (edge: RawEdgeType): EdgeType => {
|
||||||
|
const state = useGraphStore.getState()
|
||||||
|
const sourceNode = state.rawGraph?.getNode(edge.source)
|
||||||
|
const targetNode = state.rawGraph?.getNode(edge.target)
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
sourceNode,
|
||||||
|
targetNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PropertyRow = ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onClick,
|
||||||
|
tooltip
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
value: any
|
||||||
|
onClick?: () => void
|
||||||
|
tooltip?: string
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<label className="text-primary/60 tracking-wide">{name}</label>:
|
||||||
|
<Text
|
||||||
|
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
|
||||||
|
tooltipClassName="max-w-80"
|
||||||
|
text={value}
|
||||||
|
tooltip={tooltip || value}
|
||||||
|
side="left"
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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">
|
||||||
|
<PropertyRow name={'Id'} value={node.id} />
|
||||||
|
<PropertyRow
|
||||||
|
name={'Labels'}
|
||||||
|
value={node.labels.join(', ')}
|
||||||
|
onClick={() => {
|
||||||
|
useGraphStore.getState().setSelectedNode(node.id, true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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">
|
||||||
|
{Object.keys(node.properties)
|
||||||
|
.sort()
|
||||||
|
.map((name) => {
|
||||||
|
return <PropertyRow key={name} name={name} value={node.properties[name]} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{node.relationships.length > 0 && (
|
||||||
|
<>
|
||||||
|
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
|
||||||
|
Relationships
|
||||||
|
</label>
|
||||||
|
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||||
|
{node.relationships.map(({ type, id, label }) => {
|
||||||
|
return (
|
||||||
|
<PropertyRow
|
||||||
|
key={id}
|
||||||
|
name={type}
|
||||||
|
value={label}
|
||||||
|
onClick={() => {
|
||||||
|
useGraphStore.getState().setSelectedNode(id, true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">Relationship</label>
|
||||||
|
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||||
|
<PropertyRow name={'Id'} value={edge.id} />
|
||||||
|
<PropertyRow name={'Type'} value={edge.type} />
|
||||||
|
<PropertyRow
|
||||||
|
name={'Source'}
|
||||||
|
value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
|
||||||
|
onClick={() => {
|
||||||
|
useGraphStore.getState().setSelectedNode(edge.source, true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PropertyRow
|
||||||
|
name={'Target'}
|
||||||
|
value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
|
||||||
|
onClick={() => {
|
||||||
|
useGraphStore.getState().setSelectedNode(edge.target, true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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 p-1">
|
||||||
|
{Object.keys(edge.properties)
|
||||||
|
.sort()
|
||||||
|
.map((name) => {
|
||||||
|
return <PropertyRow key={name} name={name} value={edge.properties[name]} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PropertiesView
|
@@ -3,7 +3,7 @@ import { Checkbox } from '@/components/ui/Checkbox'
|
|||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import { controlButtonVariant } from '@/lib/constants'
|
import { controlButtonVariant } from '@/lib/constants'
|
||||||
import { useSettingsStore } from '@/lib/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
import { SettingsIcon } from 'lucide-react'
|
import { SettingsIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { createContext, useEffect, useState } from 'react'
|
import { createContext, useEffect, useState } from 'react'
|
||||||
import { Theme, useSettingsStore } from '@/lib/settings'
|
import { Theme, useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
type ThemeProviderProps = {
|
type ThemeProviderProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@@ -191,7 +191,7 @@ export function AsyncSelect<T>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CommandList className="max-h-auto" hidden={!open}>
|
<CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
|
||||||
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
||||||
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
|
49
lightrag/api/graph_viewer_webui/src/components/ui/Text.tsx
Normal file
49
lightrag/api/graph_viewer_webui/src/components/ui/Text.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Text = ({
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
tooltipClassName,
|
||||||
|
tooltip,
|
||||||
|
side,
|
||||||
|
onClick
|
||||||
|
}: {
|
||||||
|
text: string
|
||||||
|
className?: string
|
||||||
|
tooltipClassName?: string
|
||||||
|
tooltip?: string
|
||||||
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
|
onClick?: () => void
|
||||||
|
}) => {
|
||||||
|
if (!tooltip) {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={cn(className, onClick !== undefined ? 'cursor-pointer' : undefined)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={200}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<label
|
||||||
|
className={cn(className, onClick !== undefined ? 'cursor-pointer' : undefined)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side={side} className={tooltipClassName}>
|
||||||
|
{tooltip}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Text
|
@@ -2,50 +2,7 @@ import Graph, { DirectedGraph } from 'graphology'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { randomColor } from '@/lib/utils'
|
import { randomColor } from '@/lib/utils'
|
||||||
import * as Constants from '@/lib/constants'
|
import * as Constants from '@/lib/constants'
|
||||||
|
import { useGraphStore, RawGraph } from '@/stores/graph'
|
||||||
type RawNodeType = {
|
|
||||||
id: string
|
|
||||||
labels: string[]
|
|
||||||
properties: Record<string, any>
|
|
||||||
|
|
||||||
size: number
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
color: string
|
|
||||||
|
|
||||||
degree: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type RawEdgeType = {
|
|
||||||
id: string
|
|
||||||
source: string
|
|
||||||
target: string
|
|
||||||
type: string
|
|
||||||
properties: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
class RawGraph {
|
|
||||||
nodes: RawNodeType[] = []
|
|
||||||
edges: RawEdgeType[] = []
|
|
||||||
nodeIdMap: Record<string, number> = {}
|
|
||||||
edgeIdMap: Record<string, number> = {}
|
|
||||||
|
|
||||||
getNode = (nodeId: string) => {
|
|
||||||
const nodeIndex = this.nodeIdMap[nodeId]
|
|
||||||
if (nodeIndex !== undefined) {
|
|
||||||
return this.nodes[nodeIndex]
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdge = (edgeId: string) => {
|
|
||||||
const edgeIndex = this.edgeIdMap[edgeId]
|
|
||||||
if (edgeIndex !== undefined) {
|
|
||||||
return this.edges[edgeIndex]
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateGraph = (graph: RawGraph) => {
|
const validateGraph = (graph: RawGraph) => {
|
||||||
if (!graph) {
|
if (!graph) {
|
||||||
@@ -158,46 +115,12 @@ const fetchGraph = async (label: string) => {
|
|||||||
return rawGraph
|
return rawGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphCache: {
|
const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
||||||
label: string | null
|
|
||||||
rawGraph: RawGraph | null
|
|
||||||
convertedGraph: DirectedGraph | null
|
|
||||||
} = {
|
|
||||||
label: null,
|
|
||||||
rawGraph: null,
|
|
||||||
convertedGraph: null
|
|
||||||
}
|
|
||||||
|
|
||||||
const useLightrangeGraph = () => {
|
|
||||||
const [fetchLabel, setFetchLabel] = useState<string>('*')
|
|
||||||
const [rawGraph, setRawGraph] = useState<RawGraph | null>(graphCache.rawGraph)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (fetchLabel) {
|
|
||||||
if (graphCache.label !== fetchLabel) {
|
|
||||||
fetchGraph(fetchLabel).then((data) => {
|
|
||||||
graphCache.convertedGraph = null
|
|
||||||
graphCache.rawGraph = data
|
|
||||||
graphCache.label = fetchLabel
|
|
||||||
setRawGraph(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setRawGraph(null)
|
|
||||||
}
|
|
||||||
}, [fetchLabel, setRawGraph])
|
|
||||||
|
|
||||||
const lightrageGraph = useCallback(() => {
|
|
||||||
if (graphCache.convertedGraph) {
|
|
||||||
return graphCache.convertedGraph as Graph<NodeType, EdgeType>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the graph
|
|
||||||
const graph = new DirectedGraph()
|
const graph = new DirectedGraph()
|
||||||
|
|
||||||
for (const rawNode of rawGraph?.nodes ?? []) {
|
for (const rawNode of rawGraph?.nodes ?? []) {
|
||||||
graph.addNode(rawNode.id, {
|
graph.addNode(rawNode.id, {
|
||||||
label: rawNode.labels.join(' '),
|
label: rawNode.labels.join(', '),
|
||||||
color: rawNode.color,
|
color: rawNode.color,
|
||||||
x: rawNode.x,
|
x: rawNode.x,
|
||||||
y: rawNode.y,
|
y: rawNode.y,
|
||||||
@@ -209,16 +132,62 @@ const useLightrangeGraph = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const rawEdge of rawGraph?.edges ?? []) {
|
for (const rawEdge of rawGraph?.edges ?? []) {
|
||||||
graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
|
rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
|
||||||
label: rawEdge.type
|
label: rawEdge.type
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
graphCache.convertedGraph = graph
|
return graph
|
||||||
return graph as Graph<NodeType, EdgeType>
|
}
|
||||||
}, [rawGraph])
|
|
||||||
|
|
||||||
return { lightrageGraph, fetchLabel, setFetchLabel }
|
const useLightrangeGraph = () => {
|
||||||
|
const [fetchLabel, setFetchLabel] = useState<string>('*')
|
||||||
|
const rawGraph = useGraphStore.use.rawGraph()
|
||||||
|
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
|
const getNode = useCallback(
|
||||||
|
(nodeId: string) => {
|
||||||
|
return rawGraph?.getNode(nodeId) || null
|
||||||
|
},
|
||||||
|
[rawGraph]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getEdge = useCallback(
|
||||||
|
(edgeId: string, dynamicId: boolean = true) => {
|
||||||
|
return rawGraph?.getEdge(edgeId, dynamicId) || null
|
||||||
|
},
|
||||||
|
[rawGraph]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetchLabel) {
|
||||||
|
const state = useGraphStore.getState()
|
||||||
|
if (state.queryLabel !== fetchLabel) {
|
||||||
|
state.reset()
|
||||||
|
fetchGraph(fetchLabel).then((data) => {
|
||||||
|
state.setQueryLabel(fetchLabel)
|
||||||
|
state.setSigmaGraph(createSigmaGraph(data))
|
||||||
|
data?.buildDynamicMap()
|
||||||
|
state.setRawGraph(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const state = useGraphStore.getState()
|
||||||
|
state.reset()
|
||||||
|
state.setSigmaGraph(new DirectedGraph())
|
||||||
|
}
|
||||||
|
}, [fetchLabel])
|
||||||
|
|
||||||
|
const lightrageGraph = useCallback(() => {
|
||||||
|
if (sigmaGraph) {
|
||||||
|
return sigmaGraph as Graph<NodeType, EdgeType>
|
||||||
|
}
|
||||||
|
const graph = new DirectedGraph()
|
||||||
|
useGraphStore.getState().setSigmaGraph(graph)
|
||||||
|
return graph as Graph<NodeType, EdgeType>
|
||||||
|
}, [sigmaGraph])
|
||||||
|
|
||||||
|
return { lightrageGraph, fetchLabel, setFetchLabel, getNode, getEdge }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useLightrangeGraph
|
export default useLightrangeGraph
|
||||||
|
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useState } from 'react'
|
|||||||
import seedrandom from 'seedrandom'
|
import seedrandom from 'seedrandom'
|
||||||
import { randomColor } from '@/lib/utils'
|
import { randomColor } from '@/lib/utils'
|
||||||
import * as Constants from '@/lib/constants'
|
import * as Constants from '@/lib/constants'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
|
|
||||||
export type NodeType = {
|
export type NodeType = {
|
||||||
x: number
|
x: number
|
||||||
@@ -36,6 +37,8 @@ const useRandomGraph = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const randomGraph = useCallback(() => {
|
const randomGraph = useCallback(() => {
|
||||||
|
useGraphStore.getState().reset()
|
||||||
|
|
||||||
// Create the graph
|
// Create the graph
|
||||||
const graph = erdosRenyi(UndirectedGraph, { order: 100, probability: 0.1 })
|
const graph = erdosRenyi(UndirectedGraph, { order: 100, probability: 0.1 })
|
||||||
graph.nodes().forEach((node: string) => {
|
graph.nodes().forEach((node: string) => {
|
||||||
|
@@ -4,54 +4,114 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme {
|
:root {
|
||||||
--color-background: hsl(var(--background));
|
--background: hsl(0 0% 100%);
|
||||||
--color-foreground: hsl(var(--foreground));
|
--foreground: hsl(240 10% 3.9%);
|
||||||
|
--card: hsl(0 0% 100%);
|
||||||
|
--card-foreground: hsl(240 10% 3.9%);
|
||||||
|
--popover: hsl(0 0% 100%);
|
||||||
|
--popover-foreground: hsl(240 10% 3.9%);
|
||||||
|
--primary: hsl(240 5.9% 10%);
|
||||||
|
--primary-foreground: hsl(0 0% 98%);
|
||||||
|
--secondary: hsl(240 4.8% 95.9%);
|
||||||
|
--secondary-foreground: hsl(240 5.9% 10%);
|
||||||
|
--muted: hsl(240 4.8% 95.9%);
|
||||||
|
--muted-foreground: hsl(240 3.8% 46.1%);
|
||||||
|
--accent: hsl(240 4.8% 95.9%);
|
||||||
|
--accent-foreground: hsl(240 5.9% 10%);
|
||||||
|
--destructive: hsl(0 84.2% 60.2%);
|
||||||
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--border: hsl(240 5.9% 90%);
|
||||||
|
--input: hsl(240 5.9% 90%);
|
||||||
|
--ring: hsl(240 10% 3.9%);
|
||||||
|
--chart-1: hsl(12 76% 61%);
|
||||||
|
--chart-2: hsl(173 58% 39%);
|
||||||
|
--chart-3: hsl(197 37% 24%);
|
||||||
|
--chart-4: hsl(43 74% 66%);
|
||||||
|
--chart-5: hsl(27 87% 67%);
|
||||||
|
--radius: 0.6rem;
|
||||||
|
--sidebar-background: hsl(0 0% 98%);
|
||||||
|
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
||||||
|
--sidebar-primary: hsl(240 5.9% 10%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||||
|
--sidebar-accent: hsl(240 4.8% 95.9%);
|
||||||
|
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
||||||
|
--sidebar-border: hsl(220 13% 91%);
|
||||||
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
|
}
|
||||||
|
|
||||||
--color-card: hsl(var(--card));
|
.dark {
|
||||||
--color-card-foreground: hsl(var(--card-foreground));
|
--background: hsl(240 10% 3.9%);
|
||||||
|
--foreground: hsl(0 0% 98%);
|
||||||
|
--card: hsl(240 10% 3.9%);
|
||||||
|
--card-foreground: hsl(0 0% 98%);
|
||||||
|
--popover: hsl(240 10% 3.9%);
|
||||||
|
--popover-foreground: hsl(0 0% 98%);
|
||||||
|
--primary: hsl(0 0% 98%);
|
||||||
|
--primary-foreground: hsl(240 5.9% 10%);
|
||||||
|
--secondary: hsl(240 3.7% 15.9%);
|
||||||
|
--secondary-foreground: hsl(0 0% 98%);
|
||||||
|
--muted: hsl(240 3.7% 15.9%);
|
||||||
|
--muted-foreground: hsl(240 5% 64.9%);
|
||||||
|
--accent: hsl(240 3.7% 15.9%);
|
||||||
|
--accent-foreground: hsl(0 0% 98%);
|
||||||
|
--destructive: hsl(0 62.8% 30.6%);
|
||||||
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--border: hsl(240 3.7% 15.9%);
|
||||||
|
--input: hsl(240 3.7% 15.9%);
|
||||||
|
--ring: hsl(240 4.9% 83.9%);
|
||||||
|
--chart-1: hsl(220 70% 50%);
|
||||||
|
--chart-2: hsl(160 60% 45%);
|
||||||
|
--chart-3: hsl(30 80% 55%);
|
||||||
|
--chart-4: hsl(280 65% 60%);
|
||||||
|
--chart-5: hsl(340 75% 55%);
|
||||||
|
--sidebar-background: hsl(240 5.9% 10%);
|
||||||
|
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
||||||
|
--sidebar-primary: hsl(224.3 76.3% 48%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||||
|
--sidebar-accent: hsl(240 3.7% 15.9%);
|
||||||
|
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||||
|
--sidebar-border: hsl(240 3.7% 15.9%);
|
||||||
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
|
}
|
||||||
|
|
||||||
--color-popover: hsl(var(--popover));
|
@theme inline {
|
||||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
--color-primary: hsl(var(--primary));
|
--color-card: var(--card);
|
||||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
--color-secondary: hsl(var(--secondary));
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-muted: hsl(var(--muted));
|
--color-secondary: var(--secondary);
|
||||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
--color-accent: hsl(var(--accent));
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--color-destructive: hsl(var(--destructive));
|
--color-destructive: var(--destructive);
|
||||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
--color-border: hsl(var(--border));
|
--color-input: var(--input);
|
||||||
--color-input: hsl(var(--input));
|
--color-ring: var(--ring);
|
||||||
--color-ring: hsl(var(--ring));
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
--color-chart-1: hsl(var(--chart-1));
|
--color-chart-3: var(--chart-3);
|
||||||
--color-chart-2: hsl(var(--chart-2));
|
--color-chart-4: var(--chart-4);
|
||||||
--color-chart-3: hsl(var(--chart-3));
|
--color-chart-5: var(--chart-5);
|
||||||
--color-chart-4: hsl(var(--chart-4));
|
|
||||||
--color-chart-5: hsl(var(--chart-5));
|
|
||||||
|
|
||||||
--color-sidebar: hsl(var(--sidebar-background));
|
|
||||||
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
|
||||||
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
|
||||||
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
|
||||||
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
|
||||||
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
|
||||||
--color-sidebar-border: hsl(var(--sidebar-border));
|
|
||||||
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
|
||||||
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar-background);
|
||||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
@@ -63,6 +123,7 @@
|
|||||||
height: var(--radix-accordion-content-height);
|
height: var(--radix-accordion-content-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes accordion-up {
|
@keyframes accordion-up {
|
||||||
from {
|
from {
|
||||||
height: var(--radix-accordion-content-height);
|
height: var(--radix-accordion-content-height);
|
||||||
@@ -73,105 +134,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
|
||||||
so we've added these compatibility styles to make sure everything still
|
|
||||||
looks the same as it did with Tailwind CSS v3.
|
|
||||||
|
|
||||||
If we ever want to remove these styles, we need to add an explicit border
|
|
||||||
color utility to any element that depends on these defaults.
|
|
||||||
*/
|
|
||||||
@layer base {
|
|
||||||
*,
|
|
||||||
::after,
|
|
||||||
::before,
|
|
||||||
::backdrop,
|
|
||||||
::file-selector-button {
|
|
||||||
border-color: var(--color-gray-200, currentColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
body {
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
:root {
|
|
||||||
--background: 0 0% 100%;
|
|
||||||
--foreground: 0 0% 3.9%;
|
|
||||||
--card: 0 0% 100%;
|
|
||||||
--card-foreground: 0 0% 3.9%;
|
|
||||||
--popover: 0 0% 100%;
|
|
||||||
--popover-foreground: 0 0% 3.9%;
|
|
||||||
--primary: 0 0% 9%;
|
|
||||||
--primary-foreground: 0 0% 98%;
|
|
||||||
--secondary: 0 0% 96.1%;
|
|
||||||
--secondary-foreground: 0 0% 9%;
|
|
||||||
--muted: 0 0% 96.1%;
|
|
||||||
--muted-foreground: 0 0% 45.1%;
|
|
||||||
--accent: 0 0% 96.1%;
|
|
||||||
--accent-foreground: 0 0% 9%;
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
|
||||||
--destructive-foreground: 0 0% 98%;
|
|
||||||
--border: 0 0% 89.8%;
|
|
||||||
--input: 0 0% 89.8%;
|
|
||||||
--ring: 0 0% 3.9%;
|
|
||||||
--chart-1: 12 76% 61%;
|
|
||||||
--chart-2: 173 58% 39%;
|
|
||||||
--chart-3: 197 37% 24%;
|
|
||||||
--chart-4: 43 74% 66%;
|
|
||||||
--chart-5: 27 87% 67%;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--sidebar-background: 0 0% 98%;
|
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
|
||||||
--sidebar-border: 220 13% 91%;
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
|
||||||
.dark {
|
|
||||||
--background: 0 0% 3.9%;
|
|
||||||
--foreground: 0 0% 98%;
|
|
||||||
--card: 0 0% 3.9%;
|
|
||||||
--card-foreground: 0 0% 98%;
|
|
||||||
--popover: 0 0% 3.9%;
|
|
||||||
--popover-foreground: 0 0% 98%;
|
|
||||||
--primary: 0 0% 98%;
|
|
||||||
--primary-foreground: 0 0% 9%;
|
|
||||||
--secondary: 0 0% 14.9%;
|
|
||||||
--secondary-foreground: 0 0% 98%;
|
|
||||||
--muted: 0 0% 14.9%;
|
|
||||||
--muted-foreground: 0 0% 63.9%;
|
|
||||||
--accent: 0 0% 14.9%;
|
|
||||||
--accent-foreground: 0 0% 98%;
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
|
||||||
--destructive-foreground: 0 0% 98%;
|
|
||||||
--border: 0 0% 14.9%;
|
|
||||||
--input: 0 0% 14.9%;
|
|
||||||
--ring: 0 0% 83.1%;
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
--sidebar-background: 240 5.9% 10%;
|
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx'
|
import { clsx, type ClassValue } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
import { StoreApi, UseBoundStore } from 'zustand'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
@@ -13,3 +14,17 @@ export function randomColor() {
|
|||||||
}
|
}
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WithSelectors<S> = S extends { getState: () => infer T }
|
||||||
|
? S & { use: { [K in keyof T]: () => T[K] } }
|
||||||
|
: never
|
||||||
|
|
||||||
|
export const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S) => {
|
||||||
|
const store = _store as WithSelectors<typeof _store>
|
||||||
|
store.use = {}
|
||||||
|
for (const k of Object.keys(store.getState())) {
|
||||||
|
;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
|
||||||
|
}
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
139
lightrag/api/graph_viewer_webui/src/stores/graph.ts
Normal file
139
lightrag/api/graph_viewer_webui/src/stores/graph.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { createSelectors } from '@/lib/utils'
|
||||||
|
import { DirectedGraph } from 'graphology'
|
||||||
|
|
||||||
|
export type RawNodeType = {
|
||||||
|
id: string
|
||||||
|
labels: string[]
|
||||||
|
properties: Record<string, any>
|
||||||
|
|
||||||
|
size: number
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
color: string
|
||||||
|
|
||||||
|
degree: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RawEdgeType = {
|
||||||
|
id: string
|
||||||
|
source: string
|
||||||
|
target: string
|
||||||
|
type: string
|
||||||
|
properties: Record<string, any>
|
||||||
|
|
||||||
|
dynamicId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RawGraph {
|
||||||
|
nodes: RawNodeType[] = []
|
||||||
|
edges: RawEdgeType[] = []
|
||||||
|
nodeIdMap: Record<string, number> = {}
|
||||||
|
edgeIdMap: Record<string, number> = {}
|
||||||
|
edgeDynamicIdMap: Record<string, number> = {}
|
||||||
|
|
||||||
|
getNode = (nodeId: string) => {
|
||||||
|
const nodeIndex = this.nodeIdMap[nodeId]
|
||||||
|
if (nodeIndex !== undefined) {
|
||||||
|
return this.nodes[nodeIndex]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdge = (edgeId: string, dynamicId: boolean = true) => {
|
||||||
|
const edgeIndex = dynamicId ? this.edgeDynamicIdMap[edgeId] : this.edgeIdMap[edgeId]
|
||||||
|
if (edgeIndex !== undefined) {
|
||||||
|
return this.edges[edgeIndex]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDynamicMap = () => {
|
||||||
|
this.edgeDynamicIdMap = {}
|
||||||
|
for (let i = 0; i < this.edges.length; i++) {
|
||||||
|
const edge = this.edges[i]
|
||||||
|
this.edgeDynamicIdMap[edge.dynamicId] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphState {
|
||||||
|
selectedNode: string | null
|
||||||
|
focusedNode: string | null
|
||||||
|
selectedEdge: string | null
|
||||||
|
focusedEdge: string | null
|
||||||
|
|
||||||
|
queryLabel: string | null
|
||||||
|
rawGraph: RawGraph | null
|
||||||
|
sigmaGraph: DirectedGraph | null
|
||||||
|
|
||||||
|
moveToSelectedNode: boolean
|
||||||
|
|
||||||
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
||||||
|
setFocusedNode: (nodeId: string | null) => void
|
||||||
|
setSelectedEdge: (edgeId: string | null) => void
|
||||||
|
setFocusedEdge: (edgeId: string | null) => void
|
||||||
|
clearSelection: () => void
|
||||||
|
reset: () => void
|
||||||
|
|
||||||
|
setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
|
||||||
|
|
||||||
|
setQueryLabel: (queryLabel: string | null) => void
|
||||||
|
setRawGraph: (rawGraph: RawGraph | null) => void
|
||||||
|
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||||
|
selectedNode: null,
|
||||||
|
focusedNode: null,
|
||||||
|
selectedEdge: null,
|
||||||
|
focusedEdge: null,
|
||||||
|
|
||||||
|
moveToSelectedNode: false,
|
||||||
|
|
||||||
|
queryLabel: null,
|
||||||
|
rawGraph: null,
|
||||||
|
sigmaGraph: null,
|
||||||
|
|
||||||
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
||||||
|
set({ selectedNode: nodeId, moveToSelectedNode }),
|
||||||
|
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
||||||
|
setSelectedEdge: (edgeId: string | null) => set({ selectedEdge: edgeId }),
|
||||||
|
setFocusedEdge: (edgeId: string | null) => set({ focusedEdge: edgeId }),
|
||||||
|
clearSelection: () =>
|
||||||
|
set({
|
||||||
|
selectedNode: null,
|
||||||
|
focusedNode: null,
|
||||||
|
selectedEdge: null,
|
||||||
|
focusedEdge: null
|
||||||
|
}),
|
||||||
|
reset: () =>
|
||||||
|
set({
|
||||||
|
selectedNode: null,
|
||||||
|
focusedNode: null,
|
||||||
|
selectedEdge: null,
|
||||||
|
focusedEdge: null,
|
||||||
|
queryLabel: null,
|
||||||
|
rawGraph: null,
|
||||||
|
sigmaGraph: null,
|
||||||
|
moveToSelectedNode: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
setQueryLabel: (queryLabel: string | null) =>
|
||||||
|
set({
|
||||||
|
queryLabel
|
||||||
|
}),
|
||||||
|
|
||||||
|
setRawGraph: (rawGraph: RawGraph | null) =>
|
||||||
|
set({
|
||||||
|
rawGraph
|
||||||
|
}),
|
||||||
|
|
||||||
|
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => set({ sigmaGraph }),
|
||||||
|
|
||||||
|
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
|
||||||
|
}))
|
||||||
|
|
||||||
|
const useGraphStore = createSelectors(useGraphStoreBase)
|
||||||
|
|
||||||
|
export { useGraphStore }
|
@@ -1,5 +1,6 @@
|
|||||||
import { create, StoreApi, UseBoundStore } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||||
|
import { createSelectors } from '@/lib/utils'
|
||||||
|
|
||||||
type Theme = 'dark' | 'light' | 'system'
|
type Theme = 'dark' | 'light' | 'system'
|
||||||
|
|
||||||
@@ -36,20 +37,6 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
type WithSelectors<S> = S extends { getState: () => infer T }
|
|
||||||
? S & { use: { [K in keyof T]: () => T[K] } }
|
|
||||||
: never
|
|
||||||
|
|
||||||
const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S) => {
|
|
||||||
const store = _store as WithSelectors<typeof _store>
|
|
||||||
store.use = {}
|
|
||||||
for (const k of Object.keys(store.getState())) {
|
|
||||||
;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
|
|
||||||
}
|
|
||||||
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
|
|
||||||
const useSettingsStore = createSelectors(useSettingsStoreBase)
|
const useSettingsStore = createSelectors(useSettingsStoreBase)
|
||||||
|
|
||||||
export { useSettingsStore, type Theme }
|
export { useSettingsStore, type Theme }
|
Reference in New Issue
Block a user