guantao 10 часов назад
Родитель
Сommit
70b181d6b1

+ 2955 - 0
knowhub/frontend/package-lock.json

@@ -0,0 +1,2955 @@
+{
+  "name": "frontend",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "frontend",
+      "version": "0.0.0",
+      "dependencies": {
+        "axios": "^1.14.0",
+        "clsx": "^2.1.1",
+        "lucide-react": "^1.7.0",
+        "react": "^19.2.4",
+        "react-dom": "^19.2.4",
+        "react-router-dom": "^7.14.0",
+        "react-zoom-pan-pinch": "^4.0.2",
+        "tailwind-merge": "^3.5.0"
+      },
+      "devDependencies": {
+        "@eslint/js": "^9.39.4",
+        "@tailwindcss/vite": "^4.2.2",
+        "@types/node": "^24.12.0",
+        "@types/react": "^19.2.14",
+        "@types/react-dom": "^19.2.3",
+        "@vitejs/plugin-react": "^6.0.1",
+        "eslint": "^9.39.4",
+        "eslint-plugin-react-hooks": "^7.0.1",
+        "eslint-plugin-react-refresh": "^0.5.2",
+        "globals": "^17.4.0",
+        "tailwindcss": "^4.2.2",
+        "typescript": "~5.9.3",
+        "typescript-eslint": "^8.57.0",
+        "vite": "^8.0.1"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+      "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.9.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+      "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+      "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/config-array": {
+      "version": "0.21.2",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+      "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/object-schema": "^2.1.7",
+        "debug": "^4.3.1",
+        "minimatch": "^3.1.5"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/config-helpers": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+      "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^0.17.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+      "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "3.3.5",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+      "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.14.0",
+        "debug": "^4.3.2",
+        "espree": "^10.0.1",
+        "globals": "^14.0.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.1",
+        "minimatch": "^3.1.5",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/globals": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "9.39.4",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+      "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      }
+    },
+    "node_modules/@eslint/object-schema": {
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+      "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+      "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^0.17.0",
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.7",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+      "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.4.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/retry": {
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+      "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.122.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
+      "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
+      "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.7",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
+      "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tailwindcss/node": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
+      "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "enhanced-resolve": "^5.19.0",
+        "jiti": "^2.6.1",
+        "lightningcss": "1.32.0",
+        "magic-string": "^0.30.21",
+        "source-map-js": "^1.2.1",
+        "tailwindcss": "4.2.2"
+      }
+    },
+    "node_modules/@tailwindcss/oxide": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
+      "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20"
+      },
+      "optionalDependencies": {
+        "@tailwindcss/oxide-android-arm64": "4.2.2",
+        "@tailwindcss/oxide-darwin-arm64": "4.2.2",
+        "@tailwindcss/oxide-darwin-x64": "4.2.2",
+        "@tailwindcss/oxide-freebsd-x64": "4.2.2",
+        "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
+        "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
+        "@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
+        "@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
+        "@tailwindcss/oxide-linux-x64-musl": "4.2.2",
+        "@tailwindcss/oxide-wasm32-wasi": "4.2.2",
+        "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
+        "@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
+      "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/vite": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
+      "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@tailwindcss/node": "4.2.2",
+        "@tailwindcss/oxide": "4.2.2",
+        "tailwindcss": "4.2.2"
+      },
+      "peerDependencies": {
+        "vite": "^5.2.0 || ^6 || ^7 || ^8"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.12.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.1.tgz",
+      "integrity": "sha512-v6Ct1W1Fdz7xg5jYCg4FTrbNcIqzds2jv/HL6+5Rs/Cyjf0oljAgW59zvDZXyYG7nt9MLrAFJv9erP/fLjwt+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/@types/react": {
+      "version": "19.2.14",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+      "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "19.2.3",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+      "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^19.2.0"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
+      "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.12.2",
+        "@typescript-eslint/scope-manager": "8.58.0",
+        "@typescript-eslint/type-utils": "8.58.0",
+        "@typescript-eslint/utils": "8.58.0",
+        "@typescript-eslint/visitor-keys": "8.58.0",
+        "ignore": "^7.0.5",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^8.58.0",
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+      "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz",
+      "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "8.58.0",
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/typescript-estree": "8.58.0",
+        "@typescript-eslint/visitor-keys": "8.58.0",
+        "debug": "^4.4.3"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/project-service": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz",
+      "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/tsconfig-utils": "^8.58.0",
+        "@typescript-eslint/types": "^8.58.0",
+        "debug": "^4.4.3"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz",
+      "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/visitor-keys": "8.58.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/tsconfig-utils": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz",
+      "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz",
+      "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/typescript-estree": "8.58.0",
+        "@typescript-eslint/utils": "8.58.0",
+        "debug": "^4.4.3",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
+      "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz",
+      "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/project-service": "8.58.0",
+        "@typescript-eslint/tsconfig-utils": "8.58.0",
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/visitor-keys": "8.58.0",
+        "debug": "^4.4.3",
+        "minimatch": "^10.2.2",
+        "semver": "^7.7.3",
+        "tinyglobby": "^0.2.15",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+      "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz",
+      "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.9.1",
+        "@typescript-eslint/scope-manager": "8.58.0",
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/typescript-estree": "8.58.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz",
+      "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.58.0",
+        "eslint-visitor-keys": "^5.0.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+      "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
+      "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-rc.7"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+        "babel-plugin-react-compiler": "^1.0.0",
+        "vite": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@rolldown/plugin-babel": {
+          "optional": true
+        },
+        "babel-plugin-react-compiler": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.16.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+      "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
+      "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.10.13",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
+      "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+      "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+      "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.10.12",
+        "caniuse-lite": "^1.0.30001782",
+        "electron-to-chromium": "^1.5.328",
+        "node-releases": "^2.0.36",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001784",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz",
+      "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.331",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
+      "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/enhanced-resolve": {
+      "version": "5.20.1",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
+      "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.3.0"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "9.39.4",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+      "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.8.0",
+        "@eslint-community/regexpp": "^4.12.1",
+        "@eslint/config-array": "^0.21.2",
+        "@eslint/config-helpers": "^0.4.2",
+        "@eslint/core": "^0.17.0",
+        "@eslint/eslintrc": "^3.3.5",
+        "@eslint/js": "9.39.4",
+        "@eslint/plugin-kit": "^0.4.1",
+        "@humanfs/node": "^0.16.6",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.4.2",
+        "@types/estree": "^1.0.6",
+        "ajv": "^6.14.0",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^8.4.0",
+        "eslint-visitor-keys": "^4.2.1",
+        "espree": "^10.4.0",
+        "esquery": "^1.5.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.5",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-plugin-react-hooks": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+      "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.24.4",
+        "@babel/parser": "^7.24.4",
+        "hermes-parser": "^0.25.1",
+        "zod": "^3.25.0 || ^4.0.0",
+        "zod-validation-error": "^3.5.0 || ^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-react-refresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+      "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "eslint": "^9 || ^10"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+      "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+      "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.15.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.2.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+      "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+      "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/globals": {
+      "version": "17.4.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
+      "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hermes-estree": {
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+      "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/hermes-parser": {
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+      "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hermes-estree": "0.25.1"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/jiti": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+      "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+      "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+      "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+      "dev": true,
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.32.0",
+        "lightningcss-darwin-arm64": "1.32.0",
+        "lightningcss-darwin-x64": "1.32.0",
+        "lightningcss-freebsd-x64": "1.32.0",
+        "lightningcss-linux-arm-gnueabihf": "1.32.0",
+        "lightningcss-linux-arm64-gnu": "1.32.0",
+        "lightningcss-linux-arm64-musl": "1.32.0",
+        "lightningcss-linux-x64-gnu": "1.32.0",
+        "lightningcss-linux-x64-musl": "1.32.0",
+        "lightningcss-win32-arm64-msvc": "1.32.0",
+        "lightningcss-win32-x64-msvc": "1.32.0"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+      "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/lucide-react": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz",
+      "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==",
+      "license": "ISC",
+      "peerDependencies": {
+        "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.37",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+      "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.8",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+      "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/react": {
+      "version": "19.2.4",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+      "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.2.4",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+      "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+      "license": "MIT",
+      "dependencies": {
+        "scheduler": "^0.27.0"
+      },
+      "peerDependencies": {
+        "react": "^19.2.4"
+      }
+    },
+    "node_modules/react-router": {
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
+      "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "^1.0.1",
+        "set-cookie-parser": "^2.6.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
+      "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
+      "license": "MIT",
+      "dependencies": {
+        "react-router": "7.14.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      }
+    },
+    "node_modules/react-zoom-pan-pinch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-4.0.2.tgz",
+      "integrity": "sha512-PJc40z45IDAaLMYEpXzGrCJo6G+rmALjxtf8yY1XmKILwNQcTOyz5KyPqXyXqKG+04VtAPAl1WKhdHSE/c3gNg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/rolldown": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
+      "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.122.0",
+        "@rolldown/pluginutils": "1.0.0-rc.12"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
+        "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
+        "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
+        "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
+      }
+    },
+    "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
+      "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/scheduler": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+      "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/set-cookie-parser": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+      "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+      "license": "MIT"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tailwind-merge": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
+      "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/dcastil"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
+      "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tapable": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
+      "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+      "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4"
+      }
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/typescript-eslint": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz",
+      "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "8.58.0",
+        "@typescript-eslint/parser": "8.58.0",
+        "@typescript-eslint/typescript-estree": "8.58.0",
+        "@typescript-eslint/utils": "8.58.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
+      "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.32.0",
+        "picomatch": "^4.0.4",
+        "postcss": "^8.5.8",
+        "rolldown": "1.0.0-rc.12",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.1.0",
+        "esbuild": "^0.27.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "@vitejs/devtools": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zod": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+      "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
+    "node_modules/zod-validation-error": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+      "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "peerDependencies": {
+        "zod": "^3.25.0 || ^4.0.0"
+      }
+    }
+  }
+}

+ 1 - 0
knowhub/frontend/package.json

@@ -16,6 +16,7 @@
     "react": "^19.2.4",
     "react-dom": "^19.2.4",
     "react-router-dom": "^7.14.0",
+    "react-zoom-pan-pinch": "^4.0.2",
     "tailwind-merge": "^3.5.0"
   },
   "devDependencies": {

+ 80 - 11
knowhub/frontend/src/components/dashboard/CategoryTree.tsx

@@ -1,6 +1,6 @@
 import { useState } from 'react';
 import { cn } from '../../lib/utils';
-import { ChevronRight, ChevronDown } from 'lucide-react';
+import { ChevronRight, ChevronDown, ZoomIn, ZoomOut, Maximize } from 'lucide-react';
 
 interface NodeProps {
   node: any;
@@ -13,15 +13,70 @@ function HorizontalTreeNode({ node, onSelect, selectedId, level }: NodeProps) {
   const [expanded, setExpanded] = useState(true); // Default to fully expanded
   const hasChildren = node.children && node.children.length > 0;
 
-  // Determine colors based on level (mimicking horizontal flowchart reference image)
   let bgColor = "bg-white";
   let borderColor = "border-slate-200";
   let textColor = "text-slate-700";
   
-  if (level === 2) {
-    bgColor = "bg-emerald-100"; borderColor = "border-emerald-300"; textColor = "text-emerald-800";
-  } else if (level >= 3) {
-    bgColor = "bg-blue-100"; borderColor = "border-blue-300"; textColor = "text-blue-800";
+  if (hasChildren) {
+    // 1. Parent nodes: no distinct color highlighting
+    bgColor = "bg-white"; borderColor = "border-slate-200"; textColor = "text-slate-700 font-extrabold";
+  } else {
+    if (node.has_requirement) {
+      // 2. Leaf nodes with requirements: color varies by status & intensity varies by posts_count
+      const count = node.total_posts_count || 0;
+      const status = node.node_status || 0; // 0=grey, 1=blue, 2=yellow, 3=green
+      
+      let intensity = 0;
+      if (count < 10) intensity = 0;
+      else if (count < 50) intensity = 1;
+      else if (count < 100) intensity = 2;
+      else if (count < 300) intensity = 3;
+      else if (count < 800) intensity = 4;
+      else intensity = 5;
+
+      const palettes = {
+        0: [
+          { bg: "bg-slate-50", border: "border-slate-200", text: "text-slate-600" },
+          { bg: "bg-slate-100", border: "border-slate-300", text: "text-slate-700" },
+          { bg: "bg-slate-200", border: "border-slate-400", text: "text-slate-800" },
+          { bg: "bg-slate-400", border: "border-slate-500", text: "text-white" },
+          { bg: "bg-slate-500", border: "border-slate-600", text: "text-white" },
+          { bg: "bg-slate-600", border: "border-slate-700", text: "text-white" }
+        ],
+        1: [
+          { bg: "bg-blue-50", border: "border-blue-200", text: "text-blue-700" },
+          { bg: "bg-blue-100", border: "border-blue-300", text: "text-blue-800" },
+          { bg: "bg-blue-200", border: "border-blue-400", text: "text-blue-900" },
+          { bg: "bg-blue-400", border: "border-blue-500", text: "text-white" },
+          { bg: "bg-blue-500", border: "border-blue-600", text: "text-white" },
+          { bg: "bg-blue-600", border: "border-blue-700", text: "text-white" }
+        ],
+        2: [
+          { bg: "bg-green-50", border: "border-green-200", text: "text-green-700" },
+          { bg: "bg-green-100", border: "border-green-300", text: "text-green-800" },
+          { bg: "bg-green-200", border: "border-green-400", text: "text-green-900" },
+          { bg: "bg-green-400", border: "border-green-500", text: "text-white" },
+          { bg: "bg-green-500", border: "border-green-600", text: "text-white" },
+          { bg: "bg-green-600", border: "border-green-700", text: "text-white" }
+        ],
+        3: [
+          { bg: "bg-cyan-50", border: "border-cyan-200", text: "text-cyan-700" },
+          { bg: "bg-cyan-100", border: "border-cyan-300", text: "text-cyan-800" },
+          { bg: "bg-cyan-200", border: "border-cyan-400", text: "text-cyan-900" },
+          { bg: "bg-cyan-400", border: "border-cyan-500", text: "text-white" },
+          { bg: "bg-cyan-500", border: "border-cyan-600", text: "text-white" },
+          { bg: "bg-cyan-600", border: "border-cyan-700", text: "text-white" }
+        ]
+      };
+      
+      const theme = palettes[status as keyof typeof palettes][intensity];
+      bgColor = theme.bg;
+      borderColor = theme.border;
+      textColor = theme.text;
+    } else {
+      // Leaf nodes with no requirements
+      bgColor = "bg-slate-50"; borderColor = "border-slate-200"; textColor = "text-slate-400 opacity-60";
+    }
   }
 
   return (
@@ -87,6 +142,7 @@ function HorizontalTreeNode({ node, onSelect, selectedId, level }: NodeProps) {
 }
 
 export function CategoryTree({ data, onSelect, selectedId }: { data: any, onSelect: (node: any) => void, selectedId: any }) {
+  const [scale, setScale] = useState(1);
   if (!data || !data.children) return (
     <div className="bg-white rounded-3xl border border-slate-100 shadow-sm p-6 flex flex-col items-center justify-center min-h-[400px] text-slate-400">
       <div className="w-8 h-8 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
@@ -94,12 +150,25 @@ export function CategoryTree({ data, onSelect, selectedId }: { data: any, onSele
     </div>
   );
 
-
-  
   return (
-    <div className="bg-white rounded-3xl border border-slate-100 shadow-sm p-0 overflow-hidden flex flex-col min-h-[400px]">
-       <div className="overflow-x-auto overflow-y-auto max-h-[800px] w-full p-4 custom-scrollbar">
-         <div className="flex flex-col gap-8 select-none min-w-max pb-8">
+    <div className="bg-white rounded-3xl border border-slate-100 shadow-sm p-0 overflow-hidden flex flex-col h-[calc(100vh-160px)] min-h-[500px] relative">
+       <div className="absolute top-4 right-4 z-50 flex gap-2 bg-white/90 backdrop-blur p-1.5 rounded-lg shadow-sm border border-slate-200">
+         <button onClick={() => setScale(s => Math.min(s + 0.15, 3))} className="p-1.5 hover:bg-slate-100 rounded text-slate-600 transition-colors" title="放大">
+           <ZoomIn size={18} />
+         </button>
+         <button onClick={() => setScale(s => Math.max(s - 0.15, 0.3))} className="p-1.5 hover:bg-slate-100 rounded text-slate-600 transition-colors" title="缩小">
+           <ZoomOut size={18} />
+         </button>
+         <button onClick={() => setScale(1)} className="p-1.5 hover:bg-slate-100 rounded text-slate-600 transition-colors" title="重置比例">
+           <Maximize size={18} />
+         </button>
+       </div>
+       
+       <div className="flex-1 w-full h-full overflow-x-auto overflow-y-auto bg-slate-50/30 p-8 custom-scrollbar">
+         <div 
+            className="flex flex-col gap-8 select-none min-w-max pb-8 origin-top-left transition-all duration-200"
+            style={{ zoom: scale } as any}
+         >
             {(() => {
                // The exact order the user requested:
                const orderKeyWords = ["形式", "实质", "意图"];

+ 106 - 24
knowhub/frontend/src/pages/Capabilities.tsx

@@ -1,8 +1,9 @@
 import { useEffect, useState } from 'react';
+import type { FormEvent } from 'react';
 import { StatCard } from '../components/common/StatCard';
 import { EntityTag, StatusBadge } from '../components/common/EntityTag';
-import { Cpu, Clock, X, Target, Hammer, CheckCircle2, Wrench, ChevronDown, ChevronRight, FileText } from 'lucide-react';
-import { getCapabilities, getTools, getKnowledge } from '../services/api';
+import { Cpu, Clock, X, Target, Hammer, CheckCircle2, Wrench, ChevronDown, ChevronRight, FileText, Search } from 'lucide-react';
+import { getCapabilities, getTools, getKnowledge, getRequirements, searchCapabilities } from '../services/api';
 import { cn } from '../lib/utils';
 
 function ExpandableTool({ tool }: { tool: any }) {
@@ -33,9 +34,10 @@ function ExpandableTool({ tool }: { tool: any }) {
   );
 }
 
-function CapabilityDetails({ capability, allTools, allKnow, onClose }: { capability: any, allTools: any[], allKnow: any[], onClose: () => void }) {
+function CapabilityDetails({ capability, allTools, allKnow, allReqs, onClose }: { capability: any, allTools: any[], allKnow: any[], allReqs: any[], onClose: () => void }) {
   if (!capability) return null;
   const currentTools = allTools.filter(t => (capability.tools || []).includes(t.id));
+  const currentReqs = allReqs.filter(r => (capability.requirements || []).includes(r.id));
 
   const renderKnowledgeList = (title: string, kIds: string[], defaultColor: string) => {
     if (!kIds || kIds.length === 0) return null;
@@ -59,7 +61,7 @@ function CapabilityDetails({ capability, allTools, allKnow, onClose }: { capabil
   };
 
   return (
-    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto">
+    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto custom-scrollbar">
       <div className="flex justify-between items-start mb-6">
         <div className="flex items-center gap-2 font-bold text-lg text-slate-900 border-b pb-2 w-full">
           <Cpu size={24} className="text-emerald-600"/>{capability.name}
@@ -85,6 +87,28 @@ function CapabilityDetails({ capability, allTools, allKnow, onClose }: { capabil
           <p className="text-emerald-700 text-sm leading-relaxed">{capability.criterion || '暂无详细评估标准'}</p>
         </div>
 
+        {/* Forward Relation: Requirements */}
+        <div className="pt-4 border-t border-slate-100 mt-4">
+          <h3 className="font-bold text-slate-800 mb-4 text-sm flex items-center gap-2">
+            <Target size={16} className="text-rose-600" /> 服务业务需求 ({currentReqs.length})
+          </h3>
+          {currentReqs.length === 0 ? (
+            <p className="text-xs text-slate-400">目前没有任何业务需求激活此能力。</p>
+          ) : (
+            <div className="space-y-2">
+              {currentReqs.map(r => (
+                 <div key={r.id} className="bg-white border border-slate-200 p-3 rounded-xl shadow-sm text-left hover:border-rose-300 transition-colors">
+                     <div className="flex items-center justify-between mb-1">
+                        <span className="text-[10px] text-slate-400 font-mono">ID: {r.id.substring(0,6)}...</span>
+                        <StatusBadge status={r.status} />
+                     </div>
+                     <div className="text-xs text-slate-600 line-clamp-2">{r.description}</div>
+                 </div>
+              ))}
+            </div>
+          )}
+        </div>
+
         <div className="pt-4 border-t border-slate-100 mt-4">
           <h3 className="font-bold text-slate-800 mb-4 text-sm flex items-center gap-2">
             <Wrench size={16} className="text-indigo-600" /> 关联执行工具 ({currentTools.length})
@@ -132,15 +156,20 @@ export function Capabilities() {
   const [capabilities, setCapabilities] = useState<any[]>([]);
   const [allTools, setAllTools] = useState<any[]>([]);
   const [allKnow, setAllKnow] = useState<any[]>([]);
+  const [allReqs, setAllReqs] = useState<any[]>([]);
   const [selectedCap, setSelectedCap] = useState<any>(null);
+  
+  const [searchInput, setSearchInput] = useState("");
+  const [searchQuery, setSearchQuery] = useState("");
+  const [isSearching, setIsSearching] = useState(false);
 
   useEffect(() => {
     Promise.all([
-      getCapabilities(1000),
-      getTools(1000)
-    ]).then(async ([capsRes, toolsRes]) => {
-      setCapabilities(capsRes.results || []);
+      getTools(1000),
+      getRequirements(1000)
+    ]).then(async ([toolsRes, reqsRes]) => {
       setAllTools(toolsRes.results || []);
+      setAllReqs(reqsRes.results || []);
       try {
         const knowRes = await getKnowledge(1, 1000);
         setAllKnow(knowRes.results || []);
@@ -152,11 +181,32 @@ export function Capabilities() {
     });
   }, []);
 
+  useEffect(() => {
+    setIsSearching(true);
+    const fetchAction = searchQuery.trim() !== "" 
+      ? searchCapabilities(searchQuery, 30) 
+      : getCapabilities(1000);
+
+    fetchAction.then(res => {
+      const items = res.candidates || res.results || [];
+      setCapabilities(items);
+      setIsSearching(false);
+    }).catch(e => {
+      console.error(e);
+      setIsSearching(false);
+    });
+  }, [searchQuery]);
+
+  const handleSearch = (e: FormEvent) => {
+    e.preventDefault();
+    setSearchQuery(searchInput);
+  };
+
   const readyCaps = capabilities.filter(c => c.tools?.length > 0);
   const wipCaps = capabilities.filter(c => !c.tools || c.tools.length === 0);
 
   return (
-    <div className="space-y-8 animate-in fade-in duration-500">
+    <div className="space-y-8 animate-in fade-in duration-500 pb-12">
       <div>
         <h1 className="text-2xl font-black text-slate-900 mb-1">制作能力分布分析</h1>
         <p className="text-slate-500 text-sm">原子能力实现深度,基于工具可用性实时探测。</p>
@@ -164,7 +214,7 @@ export function Capabilities() {
 
       <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
         <StatCard 
-          title="能力总数" 
+          title="能力检索库" 
           value={capabilities.length} 
           subtext="已定义核心能力" 
           icon={Target} 
@@ -172,30 +222,50 @@ export function Capabilities() {
           iconColor="text-amber-600" 
         />
         <StatCard 
-          title="工具覆盖" 
+          title="工具覆盖" 
           value={capabilities.length > 0 ? `${Math.round((readyCaps.length / capabilities.length) * 100)}%` : "0%"} 
-          subtext="已接通工具链路" 
+          subtext="已接通工具能力" 
           icon={CheckCircle2} 
           iconBgColor="bg-emerald-50" 
           iconColor="text-emerald-600" 
         />
         <StatCard 
-          title="关联需求数" 
+          title="响应业务矩阵" 
           value={new Set(capabilities.flatMap(c => c.requirements || [])).size} 
-          subtext="覆盖业务场景" 
+          subtext="关联到特定需求" 
           icon={Hammer} 
           iconBgColor="bg-indigo-50" 
           iconColor="text-indigo-600" 
         />
       </div>
 
-      <div className="flex flex-col lg:flex-row gap-6 items-stretch">
-        <div className={cn("transition-all duration-300 ease-in-out", selectedCap ? "w-full lg:w-2/3" : "w-full")}>
+      <div className="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm relative">
+        <form onSubmit={handleSearch} className="relative flex-1 max-w-xl">
+           <Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
+           <input 
+             value={searchInput}
+             onChange={e => setSearchInput(e.target.value)}
+             placeholder="搜索特定能力名称特征或应用语义描述..."
+             className="w-full bg-slate-50 border border-slate-200 text-sm rounded-xl pl-10 pr-24 py-2.5 focus:outline-none focus:ring-2 focus:ring-emerald-500 transition-all font-medium text-slate-700 placeholder:font-normal"
+           />
+           <button type="submit" className="absolute right-2 top-1/2 -translate-y-1/2 bg-emerald-600 text-white text-xs px-3 py-1.5 rounded-lg font-bold hover:bg-emerald-700 transition-colors">
+             探索
+           </button>
+        </form>
+      </div>
+
+      <div className="flex flex-col xl:flex-row gap-6 items-stretch">
+        <div className={cn("transition-all duration-300 ease-in-out relative min-h-[200px]", selectedCap ? "w-full xl:w-2/3" : "w-full")}>
+          {isSearching && (
+             <div className="absolute inset-0 bg-white/60 backdrop-blur-[2px] flex items-center justify-center z-10 rounded-2xl">
+                <div className="w-8 h-8 border-4 border-emerald-200 border-t-emerald-600 rounded-full animate-spin"></div>
+             </div>
+          )}
           <div className={cn("grid gap-8", selectedCap ? "grid-cols-1" : "grid-cols-1 xl:grid-cols-2")}>
             <div>
               <h2 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
                 <span className="w-2 h-2 rounded-full bg-emerald-500"></span>
-                就绪能力 <span className="text-slate-400 text-sm font-normal">Ready to use</span>
+                就绪能力 {readyCaps.length > 0 && <span className="text-emerald-600 text-xs px-2 bg-emerald-100 rounded-full">{readyCaps.length}</span>}
               </h2>
               <div className="grid grid-cols-1 gap-4">
                 {readyCaps.map(cap => (
@@ -208,8 +278,13 @@ export function Capabilities() {
                     onClick={() => setSelectedCap(cap)}
                   >
                     <div className="flex justify-between items-start mb-3">
-                      <h3 className="font-bold text-slate-900 group-hover:text-emerald-700 transition-colors">{cap.name}</h3>
-                      <StatusBadge status="Ready" />
+                      <h3 className="font-bold text-slate-900 group-hover:text-emerald-700 transition-colors">
+                        {cap.name}
+                      </h3>
+                      <div className="flex gap-2">
+                        {cap.score !== undefined && <span className="bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded text-[10px] font-bold self-start">相关度 {cap.score.toFixed(2)}</span>}
+                        <StatusBadge status="Ready" />
+                      </div>
                     </div>
                     <p className="text-sm text-slate-500 line-clamp-2 mb-4">{cap.description}</p>
                     <div className="flex flex-wrap gap-2">
@@ -218,13 +293,14 @@ export function Capabilities() {
                     </div>
                   </div>
                 ))}
+                {readyCaps.length === 0 && <div className="text-xs text-slate-400 font-bold p-4 text-center border-2 border-dashed border-slate-100 rounded-2xl">无相关就绪项</div>}
               </div>
             </div>
 
             <div>
               <h2 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
                 <span className="w-2 h-2 rounded-full bg-amber-500"></span>
-                研发中能力 <span className="text-slate-400 text-sm font-normal">Pending integration</span>
+                研发中能力 {wipCaps.length > 0 && <span className="text-amber-600 text-xs px-2 bg-amber-100 rounded-full">{wipCaps.length}</span>}
               </h2>
               <div className="grid grid-cols-1 gap-4">
                 {wipCaps.map(cap => (
@@ -237,8 +313,13 @@ export function Capabilities() {
                     onClick={() => setSelectedCap(cap)}
                   >
                     <div className="flex justify-between items-start mb-3">
-                      <h3 className="font-bold text-slate-900 group-hover:text-amber-700 transition-colors">{cap.name}</h3>
-                      <StatusBadge status="In Progress" />
+                      <h3 className="font-bold text-slate-900 group-hover:text-amber-700 transition-colors">
+                         {cap.name}
+                      </h3>
+                      <div className="flex gap-2">
+                        {cap.score !== undefined && <span className="bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded text-[10px] font-bold self-start">相关度 {cap.score.toFixed(2)}</span>}
+                        <StatusBadge status="In Progress" />
+                      </div>
                     </div>
                     <p className="text-sm text-slate-500 line-clamp-2 mb-4">{cap.description}</p>
                     <div className="flex items-center justify-between">
@@ -251,14 +332,15 @@ export function Capabilities() {
                     </div>
                   </div>
                 ))}
+                {wipCaps.length === 0 && <div className="text-xs text-slate-400 font-bold p-4 text-center border-2 border-dashed border-slate-100 rounded-2xl">无相关研发中项</div>}
               </div>
             </div>
           </div>
         </div>
 
         {selectedCap && (
-          <div className="w-full lg:w-1/3">
-            <CapabilityDetails capability={selectedCap} allTools={allTools} allKnow={allKnow} onClose={() => setSelectedCap(null)} />
+          <div className="w-full xl:w-1/3">
+            <CapabilityDetails capability={selectedCap} allTools={allTools} allKnow={allKnow} allReqs={allReqs} onClose={() => setSelectedCap(null)} />
           </div>
         )}
       </div>

+ 271 - 118
knowhub/frontend/src/pages/Dashboard.tsx

@@ -22,13 +22,10 @@ function RelationGroup({ title, count, colorClass, borderClass, children, defaul
   );
 }
 
-function ExpandableDetailsItem({ data, type }: { data: any, type: 'req' | 'cap' | 'tool' | 'know' }) {
-  const [open, setOpen] = useState(false);
-
+function CompactListCard({ data, type, onDrillDown }: { data: any, type: 'req' | 'cap' | 'tool' | 'know', onDrillDown: (t: any, d: any) => void }) {
   let Icon: any = Target;
   let iconColor = "text-indigo-500";
   let title = "";
-  let content = "";
   let status = "";
 
   if (type === 'req') {
@@ -38,23 +35,20 @@ function ExpandableDetailsItem({ data, type }: { data: any, type: 'req' | 'cap'
   } else if (type === 'cap') {
     Icon = Brain; iconColor = "text-amber-500";
     title = data.name || data.id;
-    content = data.description || "暂无原子能力详细描述";
   } else if (type === 'tool') {
     Icon = Wrench; iconColor = "text-emerald-500";
     title = data.name || data.id;
-    content = data.introduction || "暂无工具的详细介绍。";
     status = data.status;
   } else if (type === 'know') {
     Icon = FileText; iconColor = "text-violet-500";
     title = data.task || data.content?.substring(0, 40) || data.id;
-    content = data.content || "暂无正文内容源文件";
   }
 
   return (
-    <div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm hover:border-slate-300 transition-colors mb-2 w-full text-left">
+    <div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm hover:border-indigo-300 hover:shadow-md transition-all mb-2 w-full text-left">
       <div 
-        className="flex justify-between items-center p-3 cursor-pointer hover:bg-slate-50 transition-colors"
-        onClick={() => setOpen(!open)}
+        className="flex justify-between items-center p-3 cursor-pointer hover:bg-slate-50 transition-colors group"
+        onClick={() => onDrillDown(type, data)}
       >
         <div className="flex items-center gap-2 font-bold text-sm text-slate-800 flex-1 pr-2">
           <Icon size={14} className={iconColor} />
@@ -67,27 +61,36 @@ function ExpandableDetailsItem({ data, type }: { data: any, type: 'req' | 'cap'
                {status}
              </span>
            )}
-           {open ? <ChevronDown size={14} className="text-slate-400"/> : <ChevronRight size={14} className="text-slate-400"/>}
+           <ChevronRight size={14} className="text-slate-300 group-hover:text-indigo-500 transition-colors"/>
         </div>
       </div>
-      {open && (
-        <div className="p-4 bg-slate-50 border-t border-slate-100 flex flex-col gap-2">
-           <div className="text-[10px] text-slate-400 font-mono">ID: {data.id}</div>
-           {content && <div className="text-xs text-slate-600 leading-relaxed whitespace-pre-wrap max-h-[400px] overflow-y-auto scrollbar-thin">{content}</div>}
-        </div>
-      )}
     </div>
   );
 }
 
 
 export function Dashboard() {
+  type NavItem = { type: 'node' | 'req' | 'cap' | 'tool' | 'know', data: any };
   const [treeData, setTreeData] = useState<any>(null);
   const [selectedNode, setSelectedNode] = useState<any>(null);
+  const [navStack, setNavStack] = useState<NavItem[]>([]);
   const [dbData, setDbData] = useState<{ reqs: any[], caps: any[], tools: any[], know: any[] }>({
     reqs: [], caps: [], tools: [], know: []
   });
 
+  useEffect(() => {
+    if (selectedNode) setNavStack([{ type: 'node', data: selectedNode }]);
+    else setNavStack([]);
+  }, [selectedNode]);
+
+  const handleDrillDown = (type: NavItem['type'], data: any) => {
+    setNavStack(prev => [...prev, { type, data }]);
+  };
+
+  const handleBreadcrumbClick = (idx: number) => {
+    setNavStack(prev => prev.slice(0, idx + 1));
+  };
+
   const [coverageStats, setCoverageStats] = useState({
     totalLeaves: 0,
     reqCoveredNodes: 0,
@@ -167,6 +170,40 @@ export function Dashboard() {
           });
         });
 
+        // Mark has_requirement flag and complex node_status on all leaves
+        leaves.forEach(l => {
+          const attachedReqs = nodeToReqs[l.name];
+          l.has_requirement = !!(attachedReqs && attachedReqs.length > 0);
+          
+          if (!l.has_requirement) {
+             l.node_status = 0; // 灰色 (没有需求)
+          } else {
+             const rIds = new Set(attachedReqs.map(r => r.id));
+             const relCaps = caps.filter((c: any) => (c.requirements || []).some((rid: string) => rIds.has(rid)));
+             
+             // 检查是否所有挂载的需求都有对应的原子能力
+             const reqsWithCaps = attachedReqs.filter((r: any) => caps.some((c: any) => (c.requirements || []).includes(r.id)));
+             if (reqsWithCaps.length < attachedReqs.length) {
+                l.node_status = 1; // 蓝色 (有需求,但没满足,缺能力覆盖)
+             } else {
+                const cIds = new Set(relCaps.map((c: any) => c.id));
+                const relTools = tools.filter((t: any) => (t.capabilities || []).some((cid: string) => cIds.has(cid)));
+                
+                if (relTools.length === 0) {
+                   l.node_status = 2; // 黄色 (有需求且全部被能力满足,但工具未接入/没有工具)
+                } else {
+                   const hasDisconnected = relTools.some((t: any) => t.status !== '已接入' && t.status !== '正常' && t.status !== '已上线' && t.status !== 'active');
+                   if (hasDisconnected) {
+                      l.node_status = 2; // 黄色 (工具状态有未接入)
+                   } else {
+                      l.node_status = 3; // 绿色 (全部满足且工具全接入)
+                   }
+                }
+             }
+          }
+        });
+        setTreeData({...data});
+
         // 4. Calculate metrics based strictly on nodes with Xiaohongshu posts (total_posts_count > 0)
         const activeLeaves = leaves.filter(l => l.total_posts_count && l.total_posts_count > 0);
         const totalLeavesCount = activeLeaves.length;
@@ -252,111 +289,227 @@ export function Dashboard() {
            <CategoryTree data={treeData} onSelect={setSelectedNode} selectedId={selectedNode?.id} />
         </div>
 
-        {/* Right Side: Node Details Sticky Panel */}
-        {selectedNode && (
-          <div className="w-full xl:w-1/3 bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24">
-            <div className="flex items-center gap-2 font-bold text-lg text-slate-900 border-b pb-4 mb-4">
-              <FolderTree size={24} className="text-indigo-600"/>节点详情
+        {/* Right Side: Drill-down Details Panel */}
+        {navStack.length > 0 && (
+          <div className="w-full xl:w-1/3 bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[calc(100vh-100px)] overflow-y-auto custom-scrollbar">
+            {/* Breadcrumbs */}
+            <div className="flex flex-wrap items-center gap-1.5 font-bold text-[13px] text-slate-500 border-b pb-4 mb-4">
+               {navStack.map((item, idx) => {
+                 let Icon: any = FolderTree;
+                 let crumbTitle = "Node";
+                 if (item.type === 'node') { Icon = FolderTree; crumbTitle = item.data.name || "Root"; }
+                 if (item.type === 'req') { Icon = Target; crumbTitle = item.data.id.substring(0, 8); }
+                 if (item.type === 'cap') { Icon = Brain; crumbTitle = item.data.name || item.data.id.substring(0,8); }
+                 if (item.type === 'tool') { Icon = Wrench; crumbTitle = item.data.name || item.data.id.substring(0,8); }
+                 if (item.type === 'know') { Icon = FileText; crumbTitle = item.data.task ? item.data.task.substring(0, 10)+"..." : item.data.id.substring(0,8); }
+                 
+                 const isLast = idx === navStack.length - 1;
+                 
+                 return (
+                   <div key={idx} className="flex items-center gap-1.5">
+                     <div 
+                       className={cn("flex items-center gap-1 transition-colors bg-slate-50 px-2 py-1 rounded-md border border-slate-100", 
+                         isLast ? "text-indigo-700 bg-indigo-50 border-indigo-100 shadow-sm" : "hover:text-indigo-600 cursor-pointer")}
+                       onClick={() => !isLast && handleBreadcrumbClick(idx)}
+                     >
+                       <Icon size={14} />
+                       <span className="max-w-[120px] truncate">{crumbTitle}</span>
+                     </div>
+                     {!isLast && <ChevronRight size={14} className="text-slate-300" />}
+                   </div>
+                 );
+               })}
             </div>
 
-            <div className="space-y-6">
-              <div>
-                <h2 className="text-2xl font-black text-slate-800">{selectedNode.name || "Root"}</h2>
-                <div className="text-xs text-slate-400 font-mono mt-1 break-all bg-slate-50 p-2 rounded">{selectedNode.path || "/"}</div>
-              </div>
-
-              {selectedNode.description && (
-                <div className="bg-indigo-50/50 p-4 rounded-xl border border-indigo-100/50">
-                  <h3 className="font-bold text-indigo-900 mb-2 text-sm">定义与描述</h3>
-                  <p className="text-indigo-700 text-sm leading-relaxed">{selectedNode.description}</p>
-                </div>
-              )}
-
-              <div className="grid grid-cols-2 gap-4">
-                <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
-                  <div className="text-[10px] text-slate-500 mb-1">当前层级 Node ID</div>
-                  <div className="font-bold text-slate-900 text-sm">{selectedNode.id || "N/A"}</div>
-                </div>
-                <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
-                  <div className="text-[10px] text-slate-500 mb-1">子层级结构 (Children)</div>
-                  <div className="font-bold text-slate-900 text-sm">{selectedNode.children?.length || 0} 个分支</div>
-                </div>
-                <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
-                  <div className="text-[10px] text-slate-500 mb-1">节点关联项集总计</div>
-                  <div className="font-bold text-slate-900 text-sm">{selectedNode.total_element_count || 0} 项</div>
-                </div>
-                <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
-                  <div className="text-[10px] text-slate-500 mb-1">小红书关联帖子总计</div>
-                  <div className="font-bold text-indigo-600 text-sm">{selectedNode.total_posts_count || 0} 篇</div>
-                </div>
-              </div>
-
-              {/* Dynamic Relations Panel */}
-              {(() => {
-                const getLeafNames = (nodes: any[]): any[] => {
-                  let leaves: any[] = [];
-                  nodes.forEach(n => {
-                    if (!n.children || n.children.length === 0) leaves.push(n);
-                    else leaves = leaves.concat(getLeafNames(n.children));
+            {/* Content Area */}
+            {(() => {
+               const currentItem = navStack[navStack.length - 1];
+               const d = currentItem.data;
+               
+               let relReqs: any[] = [];
+               let relCaps: any[] = [];
+               let relTools: any[] = [];
+               let relKnow: any[] = [];
+
+               if (currentItem.type === 'node') {
+                  const getLeafNames = (nodes: any[]): any[] => {
+                    let leaves: any[] = [];
+                    nodes.forEach(n => {
+                      if (!n.children || n.children.length === 0) leaves.push(n);
+                      else leaves = leaves.concat(getLeafNames(n.children));
+                    });
+                    return leaves;
+                  };
+                  const leafNames = getLeafNames([d]).map(l => l.name);
+
+                  relReqs = dbData.reqs.filter((r: any) => 
+                    (r.source_nodes || []).some((sn: any) => leafNames.includes(typeof sn === 'object' ? (sn.node_name || sn.name) : sn))
+                  );
+                  const relReqIds = new Set(relReqs.map(r => r.id));
+                  relCaps = dbData.caps.filter((c: any) => (c.requirements || []).some((rid: string) => relReqIds.has(rid)));
+                  const relCapIds = new Set(relCaps.map(c => c.id));
+                  relTools = dbData.tools.filter((t: any) => (t.capabilities || []).some((cid: string) => relCapIds.has(cid)));
+                  const relToolIds = new Set(relTools.map(t => t.id));
+                  relKnow = dbData.know.filter((k: any) => {
+                    const hasCap = (k.support_capability || []).some((cid: string) => relCapIds.has(cid));
+                    const hasTool = (k.tools || []).some((tid: string) => relToolIds.has(tid));
+                    return hasCap || hasTool;
                   });
-                  return leaves;
-                };
-                const leafNames = getLeafNames([selectedNode]).map(l => l.name);
-
-                const relatedReqs = dbData.reqs.filter((r: any) => 
-                  (r.source_nodes || []).some((sn: any) => leafNames.includes(typeof sn === 'object' ? (sn.node_name || sn.name) : sn))
-                );
-
-                const relatedReqIds = new Set(relatedReqs.map((r: any) => r.id));
-                const relatedCaps = dbData.caps.filter((c: any) => 
-                  (c.requirements || []).some((rid: string) => relatedReqIds.has(rid))
-                );
-
-                const relatedCapIds = new Set(relatedCaps.map((c: any) => c.id));
-                const relatedTools = dbData.tools.filter((t: any) => 
-                  (t.capabilities || []).some((cid: string) => relatedCapIds.has(cid))
-                );
-
-                const relatedToolIds = new Set(relatedTools.map((t: any) => t.id));
-                const relatedKnow = dbData.know.filter((k: any) => {
-                  const hasCap = (k.support_capability || []).some((cid: string) => relatedCapIds.has(cid));
-                  const hasTool = (k.tools || []).some((tid: string) => relatedToolIds.has(tid));
-                  return hasCap || hasTool;
-                });
-
-                return (
-                  <div className="pt-2">
-                    <RelationGroup title="关联需求" count={relatedReqs.length} colorClass="text-indigo-600" borderClass="bg-indigo-600">
-                      <div className="space-y-1">
-                        {relatedReqs.map((r: any) => <ExpandableDetailsItem key={r.id} data={r} type="req" />)}
-                        {relatedReqs.length === 0 && <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">未检索到任何需求</div>}
-                      </div>
-                    </RelationGroup>
-
-                    <RelationGroup title="原子能力" count={relatedCaps.length} colorClass="text-amber-700" borderClass="bg-amber-700">
-                      <div className="space-y-1">
-                        {relatedCaps.map((c: any) => <ExpandableDetailsItem key={c.id} data={c} type="cap" />)}
-                        {relatedCaps.length === 0 && <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">能力库为空</div>}
+               } 
+               else if (currentItem.type === 'req') {
+                  relCaps = dbData.caps.filter((c: any) => (c.requirements || []).includes(d.id));
+               }
+               else if (currentItem.type === 'cap') {
+                  relReqs = dbData.reqs.filter((r: any) => (d.requirements || []).includes(r.id));
+                  relTools = dbData.tools.filter((t: any) => (t.capabilities || []).includes(d.id));
+                  relKnow = dbData.know.filter((k: any) => (k.support_capability || []).includes(d.id));
+               }
+               else if (currentItem.type === 'tool') {
+                  relCaps = dbData.caps.filter((c: any) => (d.capabilities || []).includes(c.id));
+                  relKnow = dbData.know.filter((k: any) => (k.tools || []).includes(d.id));
+               }
+               else if (currentItem.type === 'know') {
+                  relCaps = dbData.caps.filter((c: any) => (d.support_capability || []).includes(c.id));
+                  relTools = dbData.tools.filter((t: any) => (d.tools || []).includes(t.id));
+               }
+
+               return (
+                 <div className="space-y-6 pb-8 animate-in fade-in slide-in-from-right-4 duration-300">
+                   {/* Dedicated Detail Views */}
+                   <div>
+                     {currentItem.type === 'node' && (
+                       <>
+                         <h2 className="text-2xl font-black text-slate-800">{d.name || "Root"}</h2>
+                         <div className="text-xs text-slate-400 font-mono mt-1 break-all bg-slate-50 p-2 rounded">{d.path || "/"}</div>
+                         {d.description && (
+                           <div className="bg-indigo-50/50 p-4 rounded-xl border border-indigo-100/50 mt-4">
+                             <h3 className="font-bold text-indigo-900 mb-2 text-sm">定义与描述</h3>
+                             <p className="text-indigo-700 text-sm leading-relaxed">{d.description}</p>
+                           </div>
+                         )}
+                       </>
+                     )}
+                     {currentItem.type === 'req' && (
+                       <>
+                         <h2 className="text-xl font-black text-slate-800 leading-snug">需求定义</h2>
+                         <p className="mt-4 text-indigo-800 text-sm leading-relaxed whitespace-pre-wrap bg-indigo-50/50 p-4 rounded-xl border border-indigo-100/50">{d.description}</p>
+                         <div className="mt-4 bg-slate-50 p-3 rounded-xl border border-slate-100">
+                           <div className="text-[10px] text-slate-500 mb-1">追踪 ID</div>
+                           <div className="font-mono text-slate-700 text-[11px] break-all">{d.id}</div>
+                         </div>
+                       </>
+                     )}
+                     {currentItem.type === 'cap' && (
+                       <>
+                         <h2 className="text-xl font-black text-amber-600">{d.name}</h2>
+                         <div className="bg-amber-50/50 p-4 rounded-xl mt-4 border border-amber-100/50">
+                           <h3 className="font-bold text-amber-900 mb-2 text-sm">能力标准定义</h3>
+                           <p className="text-amber-800 text-sm leading-relaxed">{d.description || "暂无描述"}</p>
+                         </div>
+                         <div className="mt-4 bg-slate-50 p-3 rounded-xl border border-slate-100">
+                           <div className="text-[10px] text-slate-500 mb-1">能力标识 ID</div>
+                           <div className="font-mono text-slate-700 text-[11px] break-all">{d.id}</div>
+                         </div>
+                       </>
+                     )}
+                     {currentItem.type === 'tool' && (
+                       <>
+                         <h2 className="text-xl font-black text-emerald-600">{d.name}</h2>
+                         <div className="bg-emerald-50/50 p-4 rounded-xl mt-4 border border-emerald-100/50">
+                           <h3 className="font-bold text-emerald-900 mb-2 text-sm">工具介绍</h3>
+                           <p className="text-emerald-800 text-sm leading-relaxed">{d.introduction || "暂无介绍"}</p>
+                         </div>
+                         <div className="mt-4 bg-slate-50 p-3 rounded-xl border border-slate-100">
+                           <div className="text-[10px] text-slate-500 mb-1">执行端 ID</div>
+                           <div className="font-mono text-slate-700 text-[11px] break-all">{d.id}</div>
+                         </div>
+                       </>
+                     )}
+                     {currentItem.type === 'know' && (
+                       <>
+                         <h2 className="text-xl font-black text-violet-700 leading-snug">{d.task}</h2>
+                         <div className="bg-violet-50/50 p-4 rounded-xl mt-4 border border-violet-100/50">
+                           <h3 className="font-bold text-violet-900 mb-2 text-sm">知识正文</h3>
+                           <p className="text-violet-800 text-sm whitespace-pre-wrap leading-relaxed">{d.content}</p>
+                         </div>
+                         <div className="mt-4 bg-slate-50 p-3 rounded-xl border border-slate-100">
+                           <div className="text-[10px] text-slate-500 mb-1">知识库 ID</div>
+                           <div className="font-mono text-slate-700 text-[11px] break-all">{d.id}</div>
+                         </div>
+                       </>
+                     )}
+                   </div>
+                   
+                   {currentItem.type === 'node' && (
+                     <div className="grid grid-cols-2 gap-3 mt-6">
+                        <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
+                          <div className="text-[10px] text-slate-500 mb-1">层级分支</div>
+                          <div className="font-bold text-slate-800 text-sm">{d.children?.length || 0} 个</div>
+                        </div>
+                        <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
+                          <div className="text-[10px] text-slate-500 mb-1">小红书热度</div>
+                          <div className="font-bold text-indigo-600 text-sm">{d.total_posts_count || 0} 篇</div>
+                        </div>
                       </div>
-                    </RelationGroup>
-
-                    <RelationGroup title="实现工具" count={relatedTools.length} colorClass="text-emerald-700" borderClass="bg-emerald-700">
-                      <div className="space-y-1">
-                        {relatedTools.map((t: any) => <ExpandableDetailsItem key={t.id} data={t} type="tool" />)}
-                        {relatedTools.length === 0 && <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">未发现支持本能力的执行工具</div>}
-                      </div>
-                    </RelationGroup>
-
-                    <RelationGroup title="支撑知识" count={relatedKnow.length} colorClass="text-violet-700" borderClass="bg-violet-700">
-                      <div className="space-y-1">
-                        {relatedKnow.map((k: any) => <ExpandableDetailsItem key={k.id} data={k} type="know" />)}
-                        {relatedKnow.length === 0 && <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">无相关文档资料</div>}
-                      </div>
-                    </RelationGroup>
-                  </div>
-                );
-              })()}
-            </div>
+                   )}
+
+                   {/* Relations Map */}
+                   <div className="pt-2 mt-6 border-t border-slate-100">
+                      {relReqs.length > 0 && currentItem.type !== 'req' && (
+                        <RelationGroup title="基于此的需求" count={relReqs.length} colorClass="text-indigo-600" borderClass="bg-indigo-600">
+                          <div className="space-y-1">
+                            {relReqs.map((r: any) => <CompactListCard key={r.id} data={r} type="req" onDrillDown={handleDrillDown}/>)}
+                          </div>
+                        </RelationGroup>
+                      )}
+                      {relReqs.length === 0 && currentItem.type === 'node' && (
+                        <RelationGroup title="关联需求" count={0} colorClass="text-indigo-600" borderClass="bg-indigo-600">
+                          <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">未检索到任何需求</div>
+                        </RelationGroup>
+                      )}
+                      
+                      {relCaps.length > 0 && currentItem.type !== 'cap' && (
+                        <RelationGroup title="下属原子能力" count={relCaps.length} colorClass="text-amber-700" borderClass="bg-amber-700">
+                          <div className="space-y-1">
+                            {relCaps.map((c: any) => <CompactListCard key={c.id} data={c} type="cap" onDrillDown={handleDrillDown}/>)}
+                          </div>
+                        </RelationGroup>
+                      )}
+                      {relCaps.length === 0 && currentItem.type === 'node' && (
+                        <RelationGroup title="下属原子能力" count={0} colorClass="text-amber-700" borderClass="bg-amber-700">
+                          <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">能力库为空</div>
+                        </RelationGroup>
+                      )}
+
+                      {relTools.length > 0 && currentItem.type !== 'tool' && (
+                        <RelationGroup title="相关实现工具" count={relTools.length} colorClass="text-emerald-700" borderClass="bg-emerald-700">
+                          <div className="space-y-1">
+                            {relTools.map((t: any) => <CompactListCard key={t.id} data={t} type="tool" onDrillDown={handleDrillDown}/>)}
+                          </div>
+                        </RelationGroup>
+                      )}
+                      {relTools.length === 0 && currentItem.type === 'node' && (
+                        <RelationGroup title="相关实现工具" count={0} colorClass="text-emerald-700" borderClass="bg-emerald-700">
+                          <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">未发现关联的执行工具</div>
+                        </RelationGroup>
+                      )}
+
+                      {relKnow.length > 0 && currentItem.type !== 'know' && (
+                        <RelationGroup title="相关支撑知识" count={relKnow.length} colorClass="text-violet-700" borderClass="bg-violet-700">
+                          <div className="space-y-1">
+                            {relKnow.map((k: any) => <CompactListCard key={k.id} data={k} type="know" onDrillDown={handleDrillDown}/>)}
+                          </div>
+                        </RelationGroup>
+                      )}
+                      {relKnow.length === 0 && currentItem.type === 'node' && (
+                        <RelationGroup title="相关支撑知识" count={0} colorClass="text-violet-700" borderClass="bg-violet-700">
+                          <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">无相关文档资料</div>
+                        </RelationGroup>
+                      )}
+                   </div>
+                 </div>
+               );
+            })()}
           </div>
         )}
       </div>

+ 20 - 11
knowhub/frontend/src/pages/Knowledge.tsx

@@ -1,7 +1,6 @@
 import { useState, useEffect } from 'react';
 import { Search, FileText, Star, X, Database, ChevronDown, ChevronRight, Wrench, Cpu } from 'lucide-react';
-import { getKnowledge, searchKnowledge, getTags, getTools, getCapabilities } from '../services/api';
-import { EntityTag } from '../components/common/EntityTag';
+import { getKnowledge, searchKnowledge, getTags, getTools, getCapabilities, getRequirements } from '../services/api';
 import { cn } from '../lib/utils';
 
 function ExpandableRelatedItem({ type, data }: { type: 'tool' | 'cap', data: any }) {
@@ -37,7 +36,7 @@ function ExpandableRelatedItem({ type, data }: { type: 'tool' | 'cap', data: any
   );
 }
 
-function KnowledgeDetails({ obj, allTools, allCaps, onClose }: { obj: any, allTools: any[], allCaps: any[], onClose: () => void }) {
+function KnowledgeDetails({ obj, allTools, allCaps, allReqs, onClose }: { obj: any, allTools: any[], allCaps: any[], allReqs: any[], onClose: () => void }) {
   if (!obj) return null;
 
   const rawTools = obj.tools || [];
@@ -46,8 +45,11 @@ function KnowledgeDetails({ obj, allTools, allCaps, onClose }: { obj: any, allTo
   const rawCaps = obj.capabilities || obj.support_capability || [];
   const validCaps = rawCaps.map((cid: string) => allCaps.find(c => c.id === cid) || { id: cid, name: cid });
 
+  const rawReqs = obj.tasks || [];
+  const validReqs = rawReqs.map((rid: string) => allReqs.find(r => r.id === rid) || { id: rid, description: rid });
+
   return (
-    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto">
+    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto custom-scrollbar">
       <div className="flex justify-between items-start mb-6">
         <div className="flex items-center gap-2 font-bold text-lg text-slate-900 border-b pb-2 w-full">
           <Search size={24} className="text-rose-600"/>知识详情
@@ -62,7 +64,7 @@ function KnowledgeDetails({ obj, allTools, allCaps, onClose }: { obj: any, allTo
           <span className="text-sm font-mono text-slate-500 bg-slate-100 px-2 py-1 rounded">ID: {obj.id?.substring(0,8)}...</span>
           <div className="flex items-center gap-1 text-amber-500 bg-amber-50 px-2 py-1 rounded-lg">
             <Star size={14} className="fill-current" />
-            <span className="font-bold text-sm">{obj.score || '0.0'}</span>
+            <span className="font-bold text-sm">{obj.score ? obj.score.toFixed(2) : '0.00'}</span>
           </div>
         </div>
         
@@ -90,11 +92,16 @@ function KnowledgeDetails({ obj, allTools, allCaps, onClose }: { obj: any, allTo
             <p className="text-xs text-slate-400">当前知识卡片未关联到具体的执行工具或能力定义。</p>
           )}
 
-          {obj.tasks && obj.tasks.length > 0 && (
-            <div className="mt-4 break-words">
-               <h4 className="text-xs font-bold text-slate-500 uppercase mb-2">应用需求</h4>
-               <div className="flex flex-wrap gap-2">
-                 {obj.tasks.map((tk: string) => <EntityTag key={tk} type="requirement" label={tk} />)}
+          {validReqs.length > 0 && (
+            <div className="mt-6 pt-4 border-t border-slate-100 break-words">
+               <h4 className="text-xs font-bold text-slate-500 uppercase mb-3">关联应用需求 (Requirements)</h4>
+               <div className="space-y-2">
+                 {validReqs.map((r: any) => (
+                    <div key={r.id} className="bg-slate-50 border border-slate-200 p-3 rounded-xl hover:border-slate-300 transition-colors">
+                      <div className="text-xs text-slate-700 font-medium">{r.description || r.id}</div>
+                      {r.description && <div className="text-[10px] text-slate-400 font-mono mt-1">ID: {r.id.substring(0,8)}...</div>}
+                    </div>
+                 ))}
                </div>
             </div>
           )}
@@ -120,6 +127,7 @@ export function Knowledge() {
   const [globalStats, setGlobalStats] = useState<Record<string, number>>({});
   const [allTools, setAllTools] = useState<any[]>([]);
   const [allCaps, setAllCaps] = useState<any[]>([]);
+  const [allReqs, setAllReqs] = useState<any[]>([]);
 
   const loadData = async (typeOverrides?: string[]) => {
     setIsSearching(true);
@@ -174,6 +182,7 @@ export function Knowledge() {
     // Fetch global tools and caps for cross-referencing
     getTools(1000).then(res => setAllTools(res.results || [])).catch(console.error);
     getCapabilities(1000).then(res => setAllCaps(res.results || [])).catch(console.error);
+    getRequirements(1000).then(res => setAllReqs(res.results || [])).catch(console.error);
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
 
@@ -389,7 +398,7 @@ export function Knowledge() {
 
         {selected && (
           <div className="w-full lg:w-1/3">
-            <KnowledgeDetails obj={selected} allTools={allTools} allCaps={allCaps} onClose={() => setSelected(null)} />
+            <KnowledgeDetails obj={selected} allTools={allTools} allCaps={allCaps} allReqs={allReqs} onClose={() => setSelected(null)} />
           </div>
         )}
       </div>

+ 136 - 70
knowhub/frontend/src/pages/Requirements.tsx

@@ -1,17 +1,22 @@
 import { useState, useEffect } from 'react';
-import { Target, X, CheckCircle2, Activity, Crosshair } from 'lucide-react';
-import { getRequirements, getCapabilities } from '../services/api';
+import type { FormEvent } from 'react';
+import { Target, X, CheckCircle2, Activity, Crosshair, Search, Filter, Brain } from 'lucide-react';
+import { getRequirements, getCapabilities, searchRequirements } from '../services/api';
 import { StatCard } from '../components/common/StatCard';
-import { EntityTag, StatusBadge } from '../components/common/EntityTag';
+import { StatusBadge } from '../components/common/EntityTag';
 import { cn } from '../lib/utils';
 
-function RequirementDetails({ req, onClose }: { req: any, onClose: () => void }) {
+function RequirementDetails({ req, allCaps, onClose }: { req: any, allCaps: any[], onClose: () => void }) {
   if (!req) return null;
+  
+  // Find related capabilities
+  const relCaps = allCaps.filter(c => (c.requirements || []).includes(req.id));
+
   return (
-    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24">
+    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[calc(100vh-100px)] overflow-y-auto custom-scrollbar">
       <div className="flex justify-between items-start mb-6">
         <div className="flex items-center gap-2 font-bold text-lg text-slate-900 border-b pb-2 w-full">
-          <Target size={24} className="text-indigo-600"/>需求详情
+          <Target size={24} className="text-indigo-600"/>需求详情 (Requirement)
         </div>
         <button onClick={onClose} className="p-1 hover:bg-slate-100 rounded-lg text-slate-400 absolute right-6 top-6">
           <X size={20} />
@@ -20,18 +25,40 @@ function RequirementDetails({ req, onClose }: { req: any, onClose: () => void })
       <div className="space-y-6">
         <div className="flex gap-2">
           <StatusBadge status={req.status} />
-          <span className="text-sm text-slate-500">ID: {req.id}</span>
+          <span className="text-[11px] text-slate-400 font-mono self-center break-all">ID: {req.id}</span>
         </div>
-        <div className="bg-white p-5 rounded-2xl border border-slate-100">
-          <h3 className="font-bold text-slate-900 mb-2">描述 (Description)</h3>
-          <p className="text-slate-600 text-sm leading-relaxed">{req.description}</p>
+        <div className="bg-indigo-50/50 p-5 rounded-2xl border border-indigo-100/50">
+          <h3 className="font-bold text-indigo-900 mb-2 text-sm">业务描述摘要</h3>
+          <p className="text-indigo-800 text-sm leading-relaxed whitespace-pre-wrap">{req.description}</p>
         </div>
         {req.match_result && (
-          <div className="bg-slate-100 p-5 rounded-2xl">
-            <h3 className="font-bold text-slate-800 mb-2">匹配结果 (Match Result)</h3>
-            <p className="text-slate-600 text-sm">{req.match_result}</p>
+          <div className="bg-slate-50 p-5 rounded-2xl border border-slate-100">
+            <h3 className="font-bold text-slate-700 mb-2 text-sm">规则匹配结果</h3>
+            <p className="text-slate-600 text-sm whitespace-pre-wrap">{req.match_result}</p>
           </div>
         )}
+
+        {/* Forward Relation: Which Capabilities support this requirement? */}
+        <div className="pt-4 border-t border-slate-100">
+          <div className="flex items-center gap-2 font-black text-[13px] tracking-wide mb-4 text-amber-700">
+             <div className="w-6 h-[2px] bg-amber-700"></div>
+             下属原子能力 ({relCaps.length})
+          </div>
+          <div className="space-y-2 pl-1">
+             {relCaps.map(c => (
+               <div key={c.id} className="bg-white border border-slate-200 p-3 rounded-xl shadow-sm text-left hover:border-amber-300 transition-colors">
+                  <div className="flex items-center gap-2 font-bold text-sm text-slate-800 mb-1">
+                    <Brain size={14} className="text-amber-500" />
+                    <span>{c.name}</span>
+                  </div>
+                  <div className="text-xs text-slate-500 line-clamp-2">{c.description || "暂无描述"}</div>
+               </div>
+             ))}
+             {relCaps.length === 0 && (
+               <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">暂无原子能力映射至此需求库。</div>
+             )}
+          </div>
+        </div>
       </div>
     </div>
   );
@@ -39,87 +66,126 @@ function RequirementDetails({ req, onClose }: { req: any, onClose: () => void })
 
 export function Requirements() {
   const [data, setData] = useState<any[]>([]);
+  const [allCaps, setAllCaps] = useState<any[]>([]);
   const [capsTotal, setCapsTotal] = useState<number>(0);
   const [selected, setSelected] = useState<any>(null);
+  
+  // Search & Filter State
+  const [searchInput, setSearchInput] = useState("");
+  const [searchQuery, setSearchQuery] = useState("");
+  const [statusFilter, setStatusFilter] = useState("all");
+  const [isSearching, setIsSearching] = useState(false);
 
   useEffect(() => {
-    getRequirements(1000).then(res => setData(res.results || []));
-    getCapabilities(1000).then(res => setCapsTotal(res.total || res.results?.length || 0));
+    // Indepdenent capability fetch for cross-mapping
+    getCapabilities(1000).then(res => {
+      setAllCaps(res.results || []);
+      setCapsTotal(res.total || res.results?.length || 0);
+    });
   }, []);
 
+  useEffect(() => {
+    setIsSearching(true);
+    const fetchAction = searchQuery.trim() !== "" 
+      ? searchRequirements(searchQuery, 30) 
+      : getRequirements(1000);
+
+    fetchAction.then(res => {
+      // search returns 'candidates', get tool returns 'results'
+      const items = res.candidates || res.results || [];
+      setData(items);
+      setIsSearching(false);
+    }).catch(e => {
+      console.error(e);
+      setIsSearching(false);
+    });
+  }, [searchQuery]);
+
   const uniqueCapsUsed = new Set(data.flatMap(r => r.atomics || [])).size;
   const coveragePerc = capsTotal > 0 ? Math.round((uniqueCapsUsed / capsTotal) * 100) : 0;
 
+  const handleSearch = (e: FormEvent) => {
+    e.preventDefault();
+    setSearchQuery(searchInput);
+  };
+
+  const filteredData = data.filter(req => {
+    if (statusFilter !== "all" && req.status !== statusFilter) return false;
+    return true;
+  });
+
   return (
-    <div className="space-y-8 animate-in fade-in duration-500">
+    <div className="space-y-8 animate-in fade-in duration-500 pb-12">
       <div>
         <h1 className="text-2xl font-black text-slate-900 mb-1">制作需求完备性分析</h1>
         <p className="text-slate-500 text-sm">汇总原子能力与业务需求的映射矩阵。</p>
       </div>
 
+      {/* Meta Stats */}
       <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
-        <StatCard 
-          title="需求总数" 
-          value={data.length} 
-          subtext="活跃制作需求" 
-          icon={Target} 
-          iconBgColor="bg-indigo-50" 
-          iconColor="text-indigo-600" 
-        />
-        <StatCard 
-          title="满足率" 
-          value={data.length > 0 ? `${Math.round((data.filter(r => r.status === '已满足').length / data.length) * 100)}%` : "0%"} 
-          subtext="已闭环占比" 
-          icon={CheckCircle2} 
-          iconBgColor="bg-emerald-50" 
-          iconColor="text-emerald-600" 
-        />
-        <StatCard 
-          title="使用能力数" 
-          value={uniqueCapsUsed} 
-          subtext="被激活原子能力" 
-          icon={Activity} 
-          iconBgColor="bg-amber-50" 
-          iconColor="text-amber-600" 
-        />
-        <StatCard 
-          title="能力覆盖率" 
-          value={`${coveragePerc}%`} 
-          subtext="能力库利用率" 
-          icon={Crosshair} 
-          iconBgColor="bg-rose-50" 
-          iconColor="text-rose-600" 
-        />
+        <StatCard title="需求库规模" value={data.length} subtext="检索返回总量" icon={Target} iconBgColor="bg-indigo-50" iconColor="text-indigo-600" />
+        <StatCard title="已满足数" value={data.filter(r => r.status === '已满足').length} subtext="核心指标" icon={CheckCircle2} iconBgColor="bg-emerald-50" iconColor="text-emerald-600" />
+        <StatCard title="激活能力数" value={uniqueCapsUsed} subtext="涉及的原子能力" icon={Activity} iconBgColor="bg-amber-50" iconColor="text-amber-600" />
+        <StatCard title="系统覆盖率" value={`${coveragePerc}%`} subtext="能力库利用度" icon={Crosshair} iconBgColor="bg-rose-50" iconColor="text-rose-600" />
       </div>
 
-      <div className="flex flex-col lg:flex-row gap-6 items-start">
-        <div className={cn("transition-all duration-300 ease-in-out", selected ? "w-full lg:w-2/3" : "w-full")}>
-          <div className="bg-white rounded-3xl border border-slate-100 shadow-sm overflow-hidden text-sm">
+      {/* Search & Filter Topbar */}
+      <div className="flex flex-col md:flex-row gap-4 justify-between bg-white p-4 rounded-2xl border border-slate-100 shadow-sm">
+        <form onSubmit={handleSearch} className="relative flex-1 max-w-lg">
+           <Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
+           <input 
+             value={searchInput}
+             onChange={e => setSearchInput(e.target.value)}
+             placeholder="输入自然语言进行语义检索..."
+             className="w-full bg-slate-50 border border-slate-200 text-sm rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all font-medium text-slate-700 placeholder:font-normal"
+           />
+           <button type="submit" className="absolute right-2 top-1/2 -translate-y-1/2 bg-indigo-600 text-white text-xs px-3 py-1.5 rounded-lg font-bold hover:bg-indigo-700 transition-colors">
+             检索
+           </button>
+        </form>
+        <div className="flex items-center gap-2 text-sm">
+           <Filter size={16} className="text-slate-400"/>
+           <select 
+              value={statusFilter} 
+              onChange={e => setStatusFilter(e.target.value)}
+              className="bg-slate-50 border border-slate-200 text-slate-700 text-sm rounded-xl px-3 py-2.5 outline-none focus:ring-2 focus:ring-indigo-500 font-bold"
+           >
+             <option value="all">所有状态</option>
+             <option value="已满足">已满足</option>
+             <option value="未满足">未满足</option>
+           </select>
+        </div>
+      </div>
+
+      <div className="flex flex-col xl:flex-row gap-6 items-start">
+        <div className={cn("transition-all duration-300 ease-in-out", selected ? "w-full xl:w-2/3" : "w-full")}>
+          <div className="bg-white rounded-3xl border border-slate-100 shadow-sm overflow-hidden text-sm relative">
+            {isSearching && (
+              <div className="absolute inset-0 bg-white/60 backdrop-blur-[2px] flex items-center justify-center z-10">
+                 <div className="w-8 h-8 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
+              </div>
+            )}
             <table className="w-full text-left">
               <thead className="bg-slate-50/50">
                 <tr>
-                  <th className="px-6 py-4 font-bold text-slate-600">ID</th>
-                  <th className="px-6 py-4 font-bold text-slate-600">描述</th>
-                  <th className="px-6 py-4 font-bold text-slate-600">状态</th>
-                  <th className="px-6 py-4 font-bold text-slate-600">关联能力</th>
+                  <th className="px-6 py-4 font-bold text-slate-600 w-1/4">ID追踪</th>
+                  <th className="px-6 py-4 font-bold text-slate-600 w-2/4">结构化描述</th>
+                  <th className="px-6 py-4 font-bold text-slate-600 w-1/4">状态</th>
                 </tr>
               </thead>
-              <tbody className="divide-y divide-slate-100">
-                {data.map(req => (
-                  <tr key={req.id} className={cn("cursor-pointer transition-colors", selected?.id === req.id ? "bg-indigo-50/50" : "hover:bg-slate-50")} onClick={() => setSelected(req)}>
-                    <td className="px-6 py-4 whitespace-nowrap text-xs text-slate-400">{req.id.substring(0,8)}...</td>
-                    <td className="px-6 py-4 max-w-md truncate text-slate-700 font-medium">{req.description}</td>
-                    <td className="px-6 py-4"><StatusBadge status={req.status} /></td>
-                    <td className="px-6 py-4">
-                      <div className="flex gap-2">
-                        {req.atomics?.slice(0, 2).map((a: string) => <EntityTag key={a} type="capability" label={a} />)}
-                        {req.atomics?.length > 2 && <span className="text-xs text-slate-400 self-center">+{req.atomics.length - 2}</span>}
-                      </div>
+              <tbody className="divide-y divide-slate-50">
+                {filteredData.map(req => (
+                  <tr key={req.id} className={cn("cursor-pointer transition-colors", selected?.id === req.id ? "bg-indigo-50" : "hover:bg-slate-50")} onClick={() => setSelected(req)}>
+                    <td className="px-6 py-4 text-xs font-mono text-slate-400">
+                      {req.id.substring(0,8)}...
+                      {req.score !== undefined && <span className="ml-2 bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded text-[10px] font-bold">相似度: {req.score.toFixed(2)}</span>}
                     </td>
+                    <td className="px-6 py-4 max-w-sm truncate text-slate-700 font-medium">{req.description}</td>
+                    <td className="px-6 py-4"><StatusBadge status={req.status} /></td>
                   </tr>
                 ))}
-                {data.length === 0 && (
-                  <tr><td colSpan={4} className="px-6 py-12 text-center text-slate-400">暂无数据</td></tr>
+                {filteredData.length === 0 && !isSearching && (
+                  <tr><td colSpan={3} className="px-6 py-12 text-center text-slate-400 font-bold">无检索结果 / 暂无该状态需求</td></tr>
                 )}
               </tbody>
             </table>
@@ -127,8 +193,8 @@ export function Requirements() {
         </div>
 
         {selected && (
-          <div className="w-full lg:w-1/3">
-            <RequirementDetails req={selected} onClose={() => setSelected(null)} />
+          <div className="w-full xl:w-1/3">
+            <RequirementDetails req={selected} allCaps={allCaps} onClose={() => setSelected(null)} />
           </div>
         )}
       </div>

+ 121 - 29
knowhub/frontend/src/pages/Tools.tsx

@@ -1,7 +1,8 @@
 import { useState, useEffect } from 'react';
+import type { FormEvent } from 'react';
 import { StatCard } from '../components/common/StatCard';
-import { Wrench, X, Globe, Zap, FileText } from 'lucide-react';
-import { getTools, getKnowledge } from '../services/api';
+import { Wrench, X, Globe, Zap, FileText, Search, Filter, Cpu } from 'lucide-react';
+import { getTools, getKnowledge, getCapabilities, searchTools } from '../services/api';
 import { EntityTag, StatusBadge } from '../components/common/EntityTag';
 import { cn } from '../lib/utils';
 
@@ -25,9 +26,11 @@ function KnowledgeItem({ label, content }: any) {
   );
 }
 
-function ToolDetails({ tool, allKnow, onClose }: { tool: any, allKnow: any[], onClose: () => void }) {
+function ToolDetails({ tool, allKnow, allCaps, onClose }: { tool: any, allKnow: any[], allCaps: any[], onClose: () => void }) {
   if (!tool) return null;
 
+  const currentCaps = allCaps.filter(c => (tool.capabilities || []).includes(c.id) || c.id === tool.capabilities?.[0]);
+
   const renderExpandableKnowledgeList = (title: string, kIds: string[], defaultColor: string) => {
     if (!kIds || kIds.length === 0) return null;
     return (
@@ -46,7 +49,7 @@ function ToolDetails({ tool, allKnow, onClose }: { tool: any, allKnow: any[], on
   };
 
   return (
-    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto">
+    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto custom-scrollbar">
       <div className="flex justify-between items-start mb-6">
         <div className="flex items-center gap-2 font-bold text-lg text-slate-900 border-b pb-2 w-full">
           <Wrench size={24} className="text-amber-600"/>{tool.name}
@@ -57,9 +60,10 @@ function ToolDetails({ tool, allKnow, onClose }: { tool: any, allKnow: any[], on
       </div>
 
       <div className="space-y-6">
-        <div className="flex gap-2">
+        <div className="flex gap-2 items-center">
           <StatusBadge status={tool.status} />
           <span className="text-sm text-slate-500">v{tool.version || '1.0'}</span>
+          <span className="text-[10px] text-slate-400 font-mono ml-auto">ID: {tool.id.substring(0,8)}</span>
         </div>
         
         <div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
@@ -67,14 +71,35 @@ function ToolDetails({ tool, allKnow, onClose }: { tool: any, allKnow: any[], on
           <p className="text-slate-600 text-sm leading-relaxed">{tool.introduction}</p>
         </div>
 
+        {/* Forward Relation: Which Capabilities does it serve? */}
+        <div className="pt-4 border-t border-slate-100 mt-4">
+          <h3 className="font-bold text-slate-800 mb-4 text-sm flex items-center gap-2">
+            <Cpu size={16} className="text-emerald-600" /> 所支持的原子能力 ({currentCaps.length})
+          </h3>
+          {currentCaps.length === 0 ? (
+            <p className="text-xs text-slate-400">尚未绑定到任何原子能力。</p>
+          ) : (
+            <div className="space-y-2">
+              {currentCaps.map(c => (
+                 <div key={c.id} className="bg-white border border-slate-200 p-3 rounded-xl shadow-sm text-left hover:border-emerald-300 transition-colors">
+                     <div className="flex items-center justify-between mb-1">
+                        <span className="text-sm font-bold text-slate-800">{c.name}</span>
+                     </div>
+                     <div className="text-xs text-slate-500 line-clamp-2">{c.description}</div>
+                 </div>
+              ))}
+            </div>
+          )}
+        </div>
+
         <div className="grid grid-cols-1 gap-4">
           <div className="bg-slate-50 p-4 rounded-xl border border-slate-200 border-dashed overflow-hidden">
             <h4 className="text-xs font-bold text-slate-500 uppercase mb-2">Input Schema</h4>
-            <pre className="text-xs overflow-x-auto text-slate-700 whitespace-pre-wrap">{JSON.stringify(tool.input, null, 2)}</pre>
+            <pre className="text-[10px] overflow-x-auto text-slate-700 whitespace-pre-wrap">{JSON.stringify(tool.input, null, 2)}</pre>
           </div>
           <div className="bg-slate-50 p-4 rounded-xl border border-slate-200 border-dashed overflow-hidden">
             <h4 className="text-xs font-bold text-slate-500 uppercase mb-2">Output Schema</h4>
-            <pre className="text-xs overflow-x-auto text-slate-700 whitespace-pre-wrap">{JSON.stringify(tool.output, null, 2)}</pre>
+            <pre className="text-[10px] overflow-x-auto text-slate-700 whitespace-pre-wrap">{JSON.stringify(tool.output, null, 2)}</pre>
           </div>
         </div>
 
@@ -100,37 +125,69 @@ function ToolDetails({ tool, allKnow, onClose }: { tool: any, allKnow: any[], on
 export function Tools() {
   const [data, setData] = useState<any[]>([]);
   const [allKnow, setAllKnow] = useState<any[]>([]);
+  const [allCaps, setAllCaps] = useState<any[]>([]);
   const [selected, setSelected] = useState<any>(null);
 
+  const [searchInput, setSearchInput] = useState("");
+  const [searchQuery, setSearchQuery] = useState("");
+  const [statusFilter, setStatusFilter] = useState("all");
+  const [isSearching, setIsSearching] = useState(false);
+
   useEffect(() => {
-    getTools(1000).then(res => setData(res.results || []));
+    getCapabilities(1000).then(res => setAllCaps(res.results || []));
     getKnowledge(1, 1000).then(res => setAllKnow(res.results || [])).catch(e => console.warn(e));
   }, []);
 
-  const activeTools = data.filter(t => t.status === '已接入' || t.status === '正常' || t.status === '已上线' || t.status === 'active');
-  const toolsAvailability = data.length > 0 ? Math.round((activeTools.length / data.length) * 100) : 0;
+  useEffect(() => {
+    setIsSearching(true);
+    const fetchAction = searchQuery.trim() !== "" 
+      ? searchTools(searchQuery, statusFilter === "all" ? undefined : statusFilter, 30) 
+      : getTools(1000);
+
+    fetchAction.then(res => {
+      const items = res.candidates || res.results || [];
+      setData(items);
+      setIsSearching(false);
+    }).catch(e => {
+      console.error(e);
+      setIsSearching(false);
+    });
+  }, [searchQuery, statusFilter]);
+
+  const handleSearch = (e: FormEvent) => {
+    e.preventDefault();
+    setSearchQuery(searchInput);
+  };
+
+  const filteredData = data.filter(t => {
+    if (searchQuery.trim() === "" && statusFilter !== "all" && t.status !== statusFilter) return false;
+    return true; 
+  });
+
+  const activeTools = filteredData.filter(t => t.status === '已接入' || t.status === '正常' || t.status === '已上线' || t.status === 'active');
+  const toolsAvailability = filteredData.length > 0 ? Math.round((activeTools.length / filteredData.length) * 100) : 0;
   
-  const matureTools = data.filter(t => t.case_knowledge && t.case_knowledge.length > 0);
-  const toolMaturity = data.length > 0 ? Math.round((matureTools.length / data.length) * 100) : 0;
+  const matureTools = filteredData.filter(t => t.case_knowledge && t.case_knowledge.length > 0);
+  const toolMaturity = filteredData.length > 0 ? Math.round((matureTools.length / filteredData.length) * 100) : 0;
 
   return (
-    <div className="space-y-8 animate-in fade-in duration-500">
+    <div className="space-y-8 animate-in fade-in duration-500 pb-12">
       <div>
         <h1 className="text-2xl font-black text-slate-900 mb-1">工具资源完备性分析</h1>
-        <p className="text-slate-500 text-sm">技术执行层资源分布与运行状态。</p>
+        <p className="text-slate-500 text-sm">技术执行层资源分布与语义特征检索。</p>
       </div>
 
       <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
         <StatCard 
-          title="工具总数" 
-          value={data.length} 
-          subtext="活跃工具库总计" 
+          title="工具检索库" 
+          value={filteredData.length} 
+          subtext="过滤后总计" 
           icon={Wrench} 
           iconBgColor="bg-indigo-50" 
           iconColor="text-indigo-600" 
         />
         <StatCard 
-          title="工具可用率" 
+          title="接口可用率" 
           value={`${toolsAvailability}%`} 
           subtext="已接入/总数" 
           icon={Globe} 
@@ -138,20 +195,51 @@ export function Tools() {
           iconColor="text-blue-600" 
         />
         <StatCard 
-          title="工具成熟度" 
+          title="AI工程成熟度" 
           value={`${toolMaturity}%`} 
-          subtext="案例充足占比" 
+          subtext="挂载案例充足评估" 
           icon={Zap} 
           iconBgColor="bg-amber-50" 
           iconColor="text-amber-600" 
         />
       </div>
 
+      <div className="flex flex-col md:flex-row gap-4 justify-between bg-white p-4 rounded-2xl border border-slate-100 shadow-sm">
+        <form onSubmit={handleSearch} className="relative flex-1 max-w-xl">
+           <Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
+           <input 
+             value={searchInput}
+             onChange={e => setSearchInput(e.target.value)}
+             placeholder="搜索特定功能点或参数设计..."
+             className="w-full bg-slate-50 border border-slate-200 text-sm rounded-xl pl-10 pr-24 py-2.5 focus:outline-none focus:ring-2 focus:ring-amber-500 transition-all font-medium text-slate-700 placeholder:font-normal"
+           />
+           <button type="submit" className="absolute right-2 top-1/2 -translate-y-1/2 bg-amber-600 text-white text-xs px-3 py-1.5 rounded-lg font-bold hover:bg-amber-700 transition-colors">
+             检索
+           </button>
+        </form>
+        <div className="flex items-center gap-2 text-sm">
+           <Filter size={16} className="text-slate-400"/>
+           <select 
+              value={statusFilter} 
+              onChange={e => setStatusFilter(e.target.value)}
+              className="bg-slate-50 border border-slate-200 text-slate-700 text-sm rounded-xl px-3 py-2.5 outline-none focus:ring-2 focus:ring-amber-500 font-bold"
+           >
+             <option value="all">所有状态</option>
+             <option value="已接入">已接入</option>
+             <option value="待接入">待接入</option>
+           </select>
+        </div>
+      </div>
 
-      <div className="flex flex-col lg:flex-row gap-6 items-stretch">
-        <div className={cn("transition-all duration-300 ease-in-out", selected ? "w-full lg:w-2/3" : "w-full")}>
+      <div className="flex flex-col xl:flex-row gap-6 items-stretch">
+        <div className={cn("transition-all duration-300 ease-in-out relative min-h-[200px]", selected ? "w-full xl:w-2/3" : "w-full")}>
+          {isSearching && (
+             <div className="absolute inset-0 bg-white/60 backdrop-blur-[2px] flex items-center justify-center z-10 rounded-2xl">
+                <div className="w-8 h-8 border-4 border-amber-200 border-t-amber-600 rounded-full animate-spin"></div>
+             </div>
+          )}
           <div className={cn("grid gap-6", selected ? "grid-cols-1 xl:grid-cols-2" : "grid-cols-1 md:grid-cols-2 lg:grid-cols-3")}>
-            {data.map(tool => (
+            {filteredData.map(tool => (
               <div 
                 key={tool.id} 
                 className={cn(
@@ -162,25 +250,29 @@ export function Tools() {
               >
                 <div className="flex justify-between items-start mb-2">
                   <h3 className="font-bold text-slate-900 group-hover:text-amber-700">{tool.name}</h3>
-                  <StatusBadge status={tool.status} />
+                  <div className="flex gap-2">
+                    {tool.score !== undefined && <span className="bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded text-[10px] font-bold self-start">相似度 {tool.score.toFixed(2)}</span>}
+                    <StatusBadge status={tool.status} />
+                  </div>
                 </div>
-                <p className="text-sm text-slate-500 line-clamp-2 mb-4 flex-1">{tool.introduction || '暂无介绍'}</p>
+                <p className="text-sm text-slate-500 line-clamp-3 mb-4 flex-1">{tool.introduction || '暂无介绍'}</p>
                 <div className="flex flex-wrap gap-2 mt-auto">
                   {(tool.capabilities || []).slice(0, 2).map((c: string) => (
                     <EntityTag key={c} type="capability" label={c} />
                   ))}
+                  {(tool.capabilities || []).length > 2 && <span className="text-xs text-slate-400 self-center">+{tool.capabilities.length - 2}</span>}
                 </div>
               </div>
             ))}
-            {data.length === 0 && (
-              <div className="col-span-full py-12 text-center text-slate-400 bg-white border border-slate-100 rounded-3xl">暂无数据 / 连接断开</div>
+            {filteredData.length === 0 && !isSearching && (
+              <div className="col-span-full py-12 text-center text-slate-400 bg-white border border-slate-100 rounded-3xl font-bold">无符合条件的检索结果</div>
             )}
           </div>
         </div>
 
         {selected && (
-          <div className="w-full lg:w-1/3">
-            <ToolDetails tool={selected} allKnow={allKnow} onClose={() => setSelected(null)} />
+          <div className="w-full xl:w-1/3">
+            <ToolDetails tool={selected} allKnow={allKnow} allCaps={allCaps} onClose={() => setSelected(null)} />
           </div>
         )}
       </div>

+ 14 - 0
knowhub/frontend/src/services/api.ts

@@ -48,6 +48,20 @@ export const searchKnowledge = async (q: string, filters: Record<string, string>
   return fetchWithCache(`/knowledge/search?${params.toString()}`);
 };
 
+export const searchRequirements = async (q: string, top_k = 20) => {
+  return fetchWithCache(`/requirement/search?q=${encodeURIComponent(q)}&top_k=${top_k}`);
+};
+
+export const searchCapabilities = async (q: string, top_k = 20) => {
+  return fetchWithCache(`/capability/search?q=${encodeURIComponent(q)}&top_k=${top_k}`);
+};
+
+export const searchTools = async (q: string, status?: string, top_k = 20) => {
+  const params = new URLSearchParams({ q, top_k: top_k.toString() });
+  if (status) params.append('status', status);
+  return fetchWithCache(`/tool/search?${params.toString()}`);
+};
+
 export const getTags = async () => {
   return fetchWithCache(`/knowledge/meta/tags`);
 };

Разница между файлами не показана из-за своего большого размера
+ 125 - 295
knowhub/frontend/yarn.lock


+ 196 - 0
knowhub/knowhub_db/reclassify_tool_knowledge.py

@@ -0,0 +1,196 @@
+#!/usr/bin/env python3
+"""
+修复脚本:根据知识的 types 标签,将 tool_knowledge 中的知识重新分类到正确的字段
+
+映射规则:
+- types 包含 "plan"    → process_knowledge(工序知识)
+- types 包含 "usecase" → case_knowledge(用例知识)
+- 其他                 → 保留在 tool_knowledge
+"""
+
+import os
+import sys
+import json
+import psycopg2
+from psycopg2.extras import RealDictCursor
+from dotenv import load_dotenv
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+_project_root = os.path.normpath(os.path.join(_script_dir, '..', '..'))
+load_dotenv(os.path.join(_project_root, '.env'))
+
+
+def get_connection():
+    host = os.getenv('KNOWHUB_DB')
+    port = int(os.getenv('KNOWHUB_PORT', 5432))
+    user = os.getenv('KNOWHUB_USER')
+    password = os.getenv('KNOWHUB_PASSWORD')
+    dbname = os.getenv('KNOWHUB_DB_NAME')
+    print(f"连接到 {host}:{port}/{dbname} as {user} ...")
+    conn = psycopg2.connect(
+        host=host, port=port, user=user,
+        password=password, database=dbname, connect_timeout=10
+    )
+    conn.autocommit = False
+    print("连接成功。\n")
+    return conn
+
+
+def parse_jsonb(val):
+    """安全解析 JSONB 字段"""
+    if val is None:
+        return []
+    if isinstance(val, list):
+        return val
+    if isinstance(val, str):
+        try:
+            return json.loads(val)
+        except json.JSONDecodeError:
+            return []
+    return []
+
+
+def main():
+    dry_run = '--dry-run' in sys.argv
+
+    if dry_run:
+        print("=== DRY RUN 模式(不会实际修改数据)===\n")
+
+    conn = get_connection()
+    cursor = conn.cursor(cursor_factory=RealDictCursor)
+
+    # 1. 获取所有有 tool_knowledge 的工具
+    cursor.execute("""
+        SELECT id, tool_knowledge, case_knowledge, process_knowledge
+        FROM tool_table
+        WHERE tool_knowledge IS NOT NULL AND tool_knowledge != '[]'::jsonb
+    """)
+    tools = cursor.fetchall()
+    print(f"找到 {len(tools)} 个有关联知识的工具\n")
+
+    if not tools:
+        print("没有需要处理的工具,退出。")
+        cursor.close()
+        conn.close()
+        return
+
+    # 2. 收集所有涉及的知识 ID
+    all_knowledge_ids = set()
+    for tool in tools:
+        all_knowledge_ids.update(parse_jsonb(tool['tool_knowledge']))
+
+    print(f"涉及 {len(all_knowledge_ids)} 条知识,正在查询类型...\n")
+
+    # 3. 批量查询知识的 types
+    knowledge_types = {}
+    if all_knowledge_ids:
+        id_list = list(all_knowledge_ids)
+        # 分批查询,避免参数过多
+        batch_size = 100
+        for i in range(0, len(id_list), batch_size):
+            batch = id_list[i:i + batch_size]
+            placeholders = ','.join(['%s'] * len(batch))
+            cursor.execute(f"""
+                SELECT id, types FROM knowledge
+                WHERE id IN ({placeholders})
+            """, batch)
+            for row in cursor.fetchall():
+                types_val = row['types']
+                if isinstance(types_val, str):
+                    try:
+                        types_val = json.loads(types_val)
+                    except json.JSONDecodeError:
+                        types_val = []
+                knowledge_types[row['id']] = types_val or []
+
+    print(f"成功查询到 {len(knowledge_types)} 条知识的类型信息\n")
+
+    # 统计
+    plan_count = sum(1 for t in knowledge_types.values() if 'plan' in t)
+    usecase_count = sum(1 for t in knowledge_types.values() if 'usecase' in t)
+    print(f"类型分布:plan={plan_count}, usecase={usecase_count}, "
+          f"其他={len(knowledge_types) - plan_count - usecase_count}\n")
+
+    # 4. 重新分类每个工具的知识
+    updated_count = 0
+    for tool in tools:
+        tool_id = tool['id']
+        old_tool_knowledge = parse_jsonb(tool['tool_knowledge'])
+        old_case_knowledge = parse_jsonb(tool['case_knowledge'])
+        old_process_knowledge = parse_jsonb(tool['process_knowledge'])
+
+        new_tool_knowledge = []
+        new_case_knowledge = list(old_case_knowledge)  # 保留已有的
+        new_process_knowledge = list(old_process_knowledge)  # 保留已有的
+
+        for kid in old_tool_knowledge:
+            types = knowledge_types.get(kid, [])
+            if 'plan' in types:
+                if kid not in new_process_knowledge:
+                    new_process_knowledge.append(kid)
+            elif 'usecase' in types:
+                if kid not in new_case_knowledge:
+                    new_case_knowledge.append(kid)
+            else:
+                new_tool_knowledge.append(kid)
+
+        # 检查是否有变化
+        changed = (
+            set(new_tool_knowledge) != set(old_tool_knowledge) or
+            set(new_case_knowledge) != set(old_case_knowledge) or
+            set(new_process_knowledge) != set(old_process_knowledge)
+        )
+
+        if changed:
+            moved_to_case = [k for k in old_tool_knowledge if k in new_case_knowledge and k not in old_case_knowledge]
+            moved_to_process = [k for k in old_tool_knowledge if k in new_process_knowledge and k not in old_process_knowledge]
+
+            print(f"工具: {tool_id}")
+            if moved_to_process:
+                print(f"  → process_knowledge (plan): +{len(moved_to_process)} 条")
+                for kid in moved_to_process:
+                    print(f"      {kid}")
+            if moved_to_case:
+                print(f"  → case_knowledge (usecase): +{len(moved_to_case)} 条")
+                for kid in moved_to_case:
+                    print(f"      {kid}")
+            print(f"  tool_knowledge: {len(old_tool_knowledge)} → {len(new_tool_knowledge)}")
+            print()
+
+            if not dry_run:
+                cursor.execute("""
+                    UPDATE tool_table
+                    SET tool_knowledge = %s,
+                        case_knowledge = %s,
+                        process_knowledge = %s
+                    WHERE id = %s
+                """, (
+                    json.dumps(new_tool_knowledge),
+                    json.dumps(new_case_knowledge),
+                    json.dumps(new_process_knowledge),
+                    tool_id
+                ))
+            updated_count += 1
+
+    if not dry_run and updated_count > 0:
+        conn.commit()
+
+    print("=" * 60)
+    print(f"处理完成:共 {len(tools)} 个工具,{updated_count} 个需要更新")
+    if dry_run:
+        print("(DRY RUN 模式,未实际修改数据)")
+        print("确认无误后,去掉 --dry-run 参数重新运行即可。")
+    else:
+        print(f"已成功更新 {updated_count} 个工具的知识分类。")
+
+    cursor.close()
+    conn.close()
+
+
+if __name__ == '__main__':
+    print("知识分类修复工具")
+    print("用法:")
+    print("  python reclassify_tool_knowledge.py --dry-run  # 预览变更")
+    print("  python reclassify_tool_knowledge.py            # 执行变更")
+    print()
+    main()

+ 18 - 9
knowhub/server.py

@@ -651,20 +651,28 @@ class KnowledgeProcessor:
         finally:
             cursor.close()
 
-    async def _update_tool_knowledge_index(self, tool_id: str, knowledge_id: str):
-        """更新工具的 tool_knowledge 关联索引(PostgreSQL tool_table)"""
+    async def _update_tool_knowledge_index(self, tool_id: str, knowledge_id: str, knowledge_types: list = None):
+        """根据知识类型更新工具的关联索引(tool_knowledge / case_knowledge / process_knowledge)"""
+        # 确定目标字段
+        if knowledge_types and 'plan' in knowledge_types:
+            target_field = 'process_knowledge'
+        elif knowledge_types and 'usecase' in knowledge_types:
+            target_field = 'case_knowledge'
+        else:
+            target_field = 'tool_knowledge'
+
         now_ts = int(time.time())
         cursor = pg_store._get_cursor()
         try:
-            cursor.execute("SELECT tool_knowledge FROM tool_table WHERE id = %s", (tool_id,))
+            cursor.execute(f"SELECT {target_field} FROM tool_table WHERE id = %s", (tool_id,))
             row = cursor.fetchone()
             if not row:
                 return
-            knowledge_ids = row["tool_knowledge"] if isinstance(row["tool_knowledge"], list) else json.loads(row["tool_knowledge"] or "[]")
+            knowledge_ids = row[target_field] if isinstance(row[target_field], list) else json.loads(row[target_field] or "[]")
             if knowledge_id not in knowledge_ids:
                 knowledge_ids.append(knowledge_id)
                 cursor.execute(
-                    "UPDATE tool_table SET tool_knowledge = %s, updated_time = %s WHERE id = %s",
+                    f"UPDATE tool_table SET {target_field} = %s, updated_time = %s WHERE id = %s",
                     (json.dumps(knowledge_ids), now_ts, tool_id)
                 )
                 pg_store.conn.commit()
@@ -728,8 +736,9 @@ class KnowledgeProcessor:
 
             pg_store.update(kid, updates)
 
+            k_types = knowledge.get("types") or []
             for tool_id in tool_ids:
-                await self._update_tool_knowledge_index(tool_id, kid)
+                await self._update_tool_knowledge_index(tool_id, kid, k_types)
 
             print(f"[Tool Analysis] {kid} 关联了 {len(tool_ids)} 个工具")
 
@@ -2001,7 +2010,7 @@ def list_tools(
 @app.get("/api/tool/search")
 async def search_tools(
     q: str = Query(..., description="查询文本"),
-    top_k: int = Query(5, ge=1, le=20),
+    top_k: int = Query(5, ge=1, le=100),
     status: Optional[str] = None,
 ):
     """向量检索工具"""
@@ -2118,7 +2127,7 @@ def list_capabilities(
 @app.get("/api/capability/search")
 async def search_capabilities(
     q: str = Query(..., description="查询文本"),
-    top_k: int = Query(5, ge=1, le=20),
+    top_k: int = Query(5, ge=1, le=100),
 ):
     """向量检索原子能力"""
     try:
@@ -2231,7 +2240,7 @@ def list_requirements(
 @app.get("/api/requirement/search")
 async def search_requirements(
     q: str = Query(..., description="查询文本"),
-    top_k: int = Query(5, ge=1, le=20),
+    top_k: int = Query(5, ge=1, le=100),
 ):
     """向量检索需求"""
     try:

Некоторые файлы не были показаны из-за большого количества измененных файлов