mirror of
https://github.com/Nezumi-2711/9router-config-generate.git
synced 2026-09-22 05:31:47 +00:00
feat: init the structure ui of the app
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"@babel/core": "^7.29.7",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@rolldown/plugin-babel": "^0.2.3",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/react": "^19.2.17",
|
||||
@@ -22,6 +23,8 @@
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"globals": "^17.7.0",
|
||||
"lucide-react": "^1.31.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^8.2.0",
|
||||
@@ -266,6 +269,36 @@
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.24", "", {}, "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
@@ -344,6 +377,8 @@
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.406", "", {}, "sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
@@ -396,6 +431,8 @@
|
||||
|
||||
"globals": ["globals@17.11.0", "", {}, "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
||||
|
||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||
@@ -410,6 +447,8 @@
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
@@ -456,6 +495,10 @@
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@1.31.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"miniflare": ["miniflare@5.20260811.1-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260811.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-DtOG0BeanIxs2sH0smFvExZD89cBQwGckbHiFkRJrrNAUu3NGClZkUxqu+zy7HYfKBAgq935EMY49vIPm3JVdA=="],
|
||||
|
||||
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||
@@ -512,6 +555,10 @@
|
||||
|
||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
@@ -562,10 +609,46 @@
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@babel/core": "^7.29.7",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@rolldown/plugin-babel": "^0.2.3",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/react": "^19.2.17",
|
||||
@@ -29,6 +30,8 @@
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"globals": "^17.7.0",
|
||||
"lucide-react": "^1.31.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^8.2.0",
|
||||
|
||||
+106
-112
@@ -1,121 +1,115 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from './assets/vite.svg'
|
||||
import heroImg from './assets/hero.png'
|
||||
import './App.css'
|
||||
import { useState, useMemo } from 'react'
|
||||
import type { ToolId, ConnectionConfig } from './types'
|
||||
import { mockModels } from './data/mockModels'
|
||||
import { tools, toolList } from './data/tools'
|
||||
import { Header } from './components/Header'
|
||||
import { ConnectionForm } from './components/ConnectionForm'
|
||||
import { ModelList } from './components/ModelList'
|
||||
import { ToolSelector } from './components/ToolSelector'
|
||||
import { ConfigPreview } from './components/ConfigPreview'
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
export function App() {
|
||||
const [connection, setConnection] = useState<ConnectionConfig>({
|
||||
baseUrl: 'http://localhost:20128/v1',
|
||||
apiKey: '',
|
||||
})
|
||||
|
||||
// Default select 2 popular models
|
||||
const [selectedModelIds, setSelectedModelIds] = useState<string[]>([
|
||||
'cc/claude-sonnet-4.5',
|
||||
'openai/gpt-4o',
|
||||
])
|
||||
|
||||
const [selectedToolId, setSelectedToolId] = useState<ToolId>('copilot')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
const handleToggleModel = (id: string) => {
|
||||
setSelectedModelIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((mId) => mId !== id) : [...prev, id]
|
||||
)
|
||||
}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
const q = searchQuery.toLowerCase().trim()
|
||||
const visibleIds = mockModels
|
||||
.filter(
|
||||
(m) =>
|
||||
!q ||
|
||||
m.id.toLowerCase().includes(q) ||
|
||||
m.name.toLowerCase().includes(q) ||
|
||||
(m.provider && m.provider.toLowerCase().includes(q)) ||
|
||||
(m.description && m.description.toLowerCase().includes(q))
|
||||
)
|
||||
.map((m) => m.id)
|
||||
|
||||
setSelectedModelIds((prev) => Array.from(new Set([...prev, ...visibleIds])))
|
||||
}
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
const q = searchQuery.toLowerCase().trim()
|
||||
const visibleIds = new Set(
|
||||
mockModels
|
||||
.filter(
|
||||
(m) =>
|
||||
!q ||
|
||||
m.id.toLowerCase().includes(q) ||
|
||||
m.name.toLowerCase().includes(q) ||
|
||||
(m.provider && m.provider.toLowerCase().includes(q)) ||
|
||||
(m.description && m.description.toLowerCase().includes(q))
|
||||
)
|
||||
.map((m) => m.id)
|
||||
)
|
||||
|
||||
setSelectedModelIds((prev) => prev.filter((id) => !visibleIds.has(id)))
|
||||
}
|
||||
|
||||
const selectedModels = useMemo(() => {
|
||||
return mockModels.filter((m) => selectedModelIds.includes(m.id))
|
||||
}, [selectedModelIds])
|
||||
|
||||
const activeTool = tools[selectedToolId] || tools.copilot
|
||||
|
||||
return (
|
||||
<>
|
||||
<section id="center">
|
||||
<div className="hero">
|
||||
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
||||
<img src={reactLogo} className="framework" alt="React logo" />
|
||||
<img src={viteLogo} className="vite" alt="Vite logo" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>Get started</h1>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="counter"
|
||||
onClick={() => setCount((count) => count + 1)}
|
||||
>
|
||||
Count is {count}
|
||||
</button>
|
||||
</section>
|
||||
<div className="min-h-screen flex flex-col bg-[var(--app-bg)] text-[var(--app-fg)]">
|
||||
<Header />
|
||||
|
||||
<div className="ticks"></div>
|
||||
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_minmax(0,540px)] gap-6 items-start">
|
||||
{/* Left Controls Pane */}
|
||||
<div className="flex flex-col gap-6 min-w-0">
|
||||
<ConnectionForm
|
||||
connection={connection}
|
||||
onChange={setConnection}
|
||||
/>
|
||||
|
||||
<section id="next-steps">
|
||||
<div id="docs">
|
||||
<svg className="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#documentation-icon"></use>
|
||||
</svg>
|
||||
<h2>Documentation</h2>
|
||||
<p>Your questions, answered</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://vite.dev/" target="_blank">
|
||||
<img className="logo" src={viteLogo} alt="" />
|
||||
Explore Vite
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://react.dev/" target="_blank">
|
||||
<img className="button-icon" src={reactLogo} alt="" />
|
||||
Learn more
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="social">
|
||||
<svg className="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#social-icon"></use>
|
||||
</svg>
|
||||
<h2>Connect with us</h2>
|
||||
<p>Join the Vite community</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
||||
<svg
|
||||
className="button-icon"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/icons.svg#github-icon"></use>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chat.vite.dev/" target="_blank">
|
||||
<svg
|
||||
className="button-icon"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/icons.svg#discord-icon"></use>
|
||||
</svg>
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/vite_js" target="_blank">
|
||||
<svg
|
||||
className="button-icon"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/icons.svg#x-icon"></use>
|
||||
</svg>
|
||||
X.com
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
||||
<svg
|
||||
className="button-icon"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/icons.svg#bluesky-icon"></use>
|
||||
</svg>
|
||||
Bluesky
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<ModelList
|
||||
models={mockModels}
|
||||
selectedIds={selectedModelIds}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onToggleModel={handleToggleModel}
|
||||
onSelectAll={handleSelectAll}
|
||||
onDeselectAll={handleDeselectAll}
|
||||
/>
|
||||
|
||||
<div className="ticks"></div>
|
||||
<section id="spacer"></section>
|
||||
</>
|
||||
<ToolSelector
|
||||
tools={toolList}
|
||||
selectedTool={selectedToolId}
|
||||
onSelectTool={setSelectedToolId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Sticky Preview Pane */}
|
||||
<div>
|
||||
<ConfigPreview
|
||||
tool={activeTool}
|
||||
connection={connection}
|
||||
selectedModels={selectedModels}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { useState } from 'react'
|
||||
import type { ToolMeta, ConnectionConfig, Model } from '../types'
|
||||
import { Card } from './ui/Card'
|
||||
import { Button } from './ui/Button'
|
||||
import { Badge } from './ui/Badge'
|
||||
import { CodeBlock } from './ui/CodeBlock'
|
||||
import { Download, Copy, Check, FileCode, Info } from 'lucide-react'
|
||||
|
||||
interface ConfigPreviewProps {
|
||||
tool: ToolMeta
|
||||
connection: ConnectionConfig
|
||||
selectedModels: Model[]
|
||||
}
|
||||
|
||||
export const ConfigPreview: React.FC<ConfigPreviewProps> = ({
|
||||
tool,
|
||||
connection,
|
||||
selectedModels,
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const configContent = tool.sampleTemplate(connection, selectedModels)
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(configContent)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
const blob = new Blob([configContent], { type: 'text/plain;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = tool.targetFilename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lg:sticky lg:top-20 flex flex-col gap-4 min-w-0 w-full">
|
||||
<Card className="flex flex-col gap-3.5 shadow-md min-w-0">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2.5 border-b border-zinc-200 dark:border-[#22232c] pb-3">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="w-7 h-7 shrink-0 rounded-md bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center">
|
||||
<FileCode className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-mono text-xs sm:text-sm font-bold text-zinc-900 dark:text-zinc-100 truncate">
|
||||
{tool.targetFilename}
|
||||
</span>
|
||||
<Badge variant="brand" size="sm">
|
||||
{tool.language.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400 block text-left truncate">
|
||||
Target for {tool.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0 ml-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
icon={copied ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleDownload}
|
||||
icon={<Download className="w-3.5 h-3.5" />}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 p-2.5 rounded-lg bg-zinc-100/60 dark:bg-zinc-800/40 text-xs text-zinc-600 dark:text-zinc-400 text-left">
|
||||
<Info className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<span className="font-semibold text-zinc-800 dark:text-zinc-200">
|
||||
Save Location:{' '}
|
||||
</span>
|
||||
<span>{tool.locationDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeBlock
|
||||
code={configContent}
|
||||
language={tool.language}
|
||||
className="max-h-[500px]"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400 pt-1">
|
||||
<span>{selectedModels.length} model(s) included</span>
|
||||
<span>Live preview interpolated</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react'
|
||||
import type { ConnectionConfig } from '../types'
|
||||
import { Input } from './ui/Input'
|
||||
import { Button } from './ui/Button'
|
||||
import { Card } from './ui/Card'
|
||||
import { Badge } from './ui/Badge'
|
||||
import { Link2, Key, RefreshCw, Eye, EyeOff } from 'lucide-react'
|
||||
|
||||
interface ConnectionFormProps {
|
||||
connection: ConnectionConfig
|
||||
onChange: (connection: ConnectionConfig) => void
|
||||
}
|
||||
|
||||
export const ConnectionForm: React.FC<ConnectionFormProps> = ({
|
||||
connection,
|
||||
onChange,
|
||||
}) => {
|
||||
const [showKey, setShowKey] = useState(false)
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 m-0">
|
||||
1. Gateway Connection
|
||||
</h2>
|
||||
<Badge variant="brand" size="sm">
|
||||
Local Router
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">OpenAI compatible</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
|
||||
<Input
|
||||
label="Base URL"
|
||||
placeholder="http://localhost:20128/v1"
|
||||
value={connection.baseUrl}
|
||||
onChange={(e) => onChange({ ...connection, baseUrl: e.target.value })}
|
||||
leftIcon={<Link2 className="w-4 h-4" />}
|
||||
helperText="Default 9router endpoint is http://localhost:20128/v1"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="API Key (Optional)"
|
||||
type={showKey ? 'text' : 'password'}
|
||||
placeholder="sk-..."
|
||||
value={connection.apiKey}
|
||||
onChange={(e) => onChange({ ...connection, apiKey: e.target.value })}
|
||||
leftIcon={<Key className="w-4 h-4" />}
|
||||
helperText="Passed as authorization header if required"
|
||||
rightElement={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKey(!showKey)}
|
||||
className="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors p-1"
|
||||
title={showKey ? 'Hide key' : 'Show key'}
|
||||
>
|
||||
{showKey ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<div className="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span>Local mock catalog loaded</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
icon={<RefreshCw className="w-3.5 h-3.5" />}
|
||||
title="Live endpoint fetching will be available in next release"
|
||||
>
|
||||
Fetch Models (Live)
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import { Sparkles, Terminal, Globe } from 'lucide-react'
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
return (
|
||||
<header className="border-b border-zinc-200 dark:border-[#22232c] bg-white/70 dark:bg-[#121319]/70 backdrop-blur-md sticky top-0 z-30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 flex items-center justify-center text-white shadow-sm shadow-amber-500/25">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-base font-bold tracking-tight text-zinc-900 dark:text-white m-0 font-sans">
|
||||
9router Config Generator
|
||||
</h1>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20">
|
||||
v1.0
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 m-0">
|
||||
Generate ready-to-paste AI coding tool configs for your local gateway
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 px-3 py-1.5 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<Terminal className="w-3.5 h-3.5" />
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
<a
|
||||
href="http://localhost:20128"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hidden sm:flex text-xs font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 px-3 py-1.5 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors items-center gap-1.5"
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5 text-amber-500" />
|
||||
<span>9router Local</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { Model } from '../types'
|
||||
import { Card } from './ui/Card'
|
||||
import { Badge } from './ui/Badge'
|
||||
import { Input } from './ui/Input'
|
||||
import { Search, CheckSquare, Square, Eye, Wrench, Layers } from 'lucide-react'
|
||||
|
||||
interface ModelListProps {
|
||||
models: Model[]
|
||||
selectedIds: string[]
|
||||
searchQuery: string
|
||||
onSearchChange: (q: string) => void
|
||||
onToggleModel: (id: string) => void
|
||||
onSelectAll: () => void
|
||||
onDeselectAll: () => void
|
||||
}
|
||||
|
||||
export const ModelList: React.FC<ModelListProps> = ({
|
||||
models,
|
||||
selectedIds,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
onToggleModel,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
}) => {
|
||||
const filteredModels = useMemo(() => {
|
||||
const q = searchQuery.toLowerCase().trim()
|
||||
if (!q) return models
|
||||
return models.filter(
|
||||
(m) =>
|
||||
m.id.toLowerCase().includes(q) ||
|
||||
m.name.toLowerCase().includes(q) ||
|
||||
(m.provider && m.provider.toLowerCase().includes(q)) ||
|
||||
(m.description && m.description.toLowerCase().includes(q))
|
||||
)
|
||||
}, [models, searchQuery])
|
||||
|
||||
const allFilteredSelected =
|
||||
filteredModels.length > 0 &&
|
||||
filteredModels.every((m) => selectedIds.includes(m.id))
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col gap-3.5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 m-0">
|
||||
2. Select Models
|
||||
</h2>
|
||||
<Badge variant="brand" size="sm">
|
||||
{selectedIds.length} selected
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={allFilteredSelected ? onDeselectAll : onSelectAll}
|
||||
className="text-xs font-medium text-amber-600 hover:text-amber-700 dark:text-amber-400 transition-colors"
|
||||
>
|
||||
{allFilteredSelected ? 'Deselect visible' : 'Select all visible'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
placeholder="Filter models by id, provider, name..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
leftIcon={<Search className="w-4 h-4" />}
|
||||
className="text-xs"
|
||||
/>
|
||||
|
||||
<div className="max-h-72 overflow-y-auto pr-1 flex flex-col gap-2">
|
||||
{filteredModels.length === 0 ? (
|
||||
<div className="p-6 text-center text-xs text-zinc-500 dark:text-zinc-400 border border-dashed border-zinc-200 dark:border-zinc-800 rounded-lg">
|
||||
No models matching "{searchQuery}"
|
||||
</div>
|
||||
) : (
|
||||
filteredModels.map((model) => {
|
||||
const isSelected = selectedIds.includes(model.id)
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
onClick={() => onToggleModel(model.id)}
|
||||
className={`flex items-start gap-3 p-2.5 rounded-lg border transition-all cursor-pointer select-none text-left ${
|
||||
isSelected
|
||||
? 'bg-amber-500/5 dark:bg-amber-500/10 border-amber-500/40 dark:border-amber-500/30'
|
||||
: 'bg-zinc-50/50 dark:bg-[#121319]/50 border-zinc-200/80 dark:border-zinc-800/80 hover:bg-zinc-100 dark:hover:bg-zinc-800/60'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isSelected ? `Deselect ${model.name}` : `Select ${model.name}`}
|
||||
className="mt-0.5 text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300"
|
||||
>
|
||||
{isSelected ? (
|
||||
<CheckSquare className="w-4 h-4 text-amber-500" />
|
||||
) : (
|
||||
<Square className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-1.5 mb-0.5">
|
||||
<span className="font-mono text-xs font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{model.id}
|
||||
</span>
|
||||
{model.provider && (
|
||||
<span className="text-[10px] px-1.5 py-0.2 rounded bg-zinc-200/70 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 font-medium">
|
||||
{model.provider}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{model.name && model.name !== model.id && (
|
||||
<p className="text-xs text-zinc-700 dark:text-zinc-300 truncate">
|
||||
{model.name}
|
||||
</p>
|
||||
)}
|
||||
{model.description && (
|
||||
<p className="text-[11px] text-zinc-500 dark:text-zinc-400 mt-0.5 line-clamp-1">
|
||||
{model.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-1 shrink-0 text-[10px] text-zinc-400">
|
||||
{model.contextWindow && (
|
||||
<span className="flex items-center gap-0.5 font-mono" title="Context window">
|
||||
<Layers className="w-3 h-3 text-zinc-400" />
|
||||
{(model.contextWindow / 1000).toFixed(0)}k
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
{model.vision && (
|
||||
<span title="Supports Vision" className="flex items-center">
|
||||
<Eye className="w-3 h-3 text-zinc-400" />
|
||||
</span>
|
||||
)}
|
||||
{model.toolCalling && (
|
||||
<span title="Supports Function Calling" className="flex items-center">
|
||||
<Wrench className="w-3 h-3 text-zinc-400" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react'
|
||||
import type { ToolId, ToolMeta } from '../types'
|
||||
import { Card } from './ui/Card'
|
||||
import { Badge } from './ui/Badge'
|
||||
import { Bot, Terminal, Cpu, Check } from 'lucide-react'
|
||||
|
||||
interface ToolSelectorProps {
|
||||
tools: ToolMeta[]
|
||||
selectedTool: ToolId
|
||||
onSelectTool: (id: ToolId) => void
|
||||
}
|
||||
|
||||
export const ToolSelector: React.FC<ToolSelectorProps> = ({
|
||||
tools,
|
||||
selectedTool,
|
||||
onSelectTool,
|
||||
}) => {
|
||||
const getIcon = (iconName: ToolMeta['iconName']) => {
|
||||
switch (iconName) {
|
||||
case 'bot':
|
||||
return <Bot className="w-5 h-5" />
|
||||
case 'terminal':
|
||||
return <Terminal className="w-5 h-5" />
|
||||
case 'cpu':
|
||||
return <Cpu className="w-5 h-5" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col gap-3.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 m-0">
|
||||
3. Choose Target Tool
|
||||
</h2>
|
||||
<Badge variant="outline" size="sm">
|
||||
{tools.length} available
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">Single select</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{tools.map((tool) => {
|
||||
const isSelected = selectedTool === tool.id
|
||||
return (
|
||||
<div
|
||||
key={tool.id}
|
||||
onClick={() => onSelectTool(tool.id)}
|
||||
className={`relative flex flex-col justify-between p-3.5 rounded-xl border transition-all cursor-pointer text-left ${
|
||||
isSelected
|
||||
? 'bg-amber-500/5 dark:bg-amber-500/10 border-amber-500 shadow-sm shadow-amber-500/10 ring-1 ring-amber-500'
|
||||
: 'bg-zinc-50/50 dark:bg-[#121319]/50 border-zinc-200 dark:border-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-800/60'
|
||||
}`}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="absolute top-2.5 right-2.5 w-4 h-4 rounded-full bg-amber-500 text-white flex items-center justify-center">
|
||||
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-lg flex items-center justify-center mb-2.5 ${
|
||||
isSelected
|
||||
? 'bg-amber-500 text-white'
|
||||
: 'bg-zinc-200 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
{getIcon(tool.iconName)}
|
||||
</div>
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-zinc-100 mb-0.5">
|
||||
{tool.name}
|
||||
</h3>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mb-2 leading-snug">
|
||||
{tool.tagline}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-zinc-200/60 dark:border-zinc-800/60">
|
||||
<span className="font-mono text-[11px] text-zinc-600 dark:text-zinc-400 truncate block">
|
||||
{tool.targetFilename}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode
|
||||
variant?: 'default' | 'brand' | 'outline' | 'success'
|
||||
size?: 'sm' | 'md'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Badge: React.FC<BadgeProps> = ({
|
||||
children,
|
||||
variant = 'default',
|
||||
size = 'sm',
|
||||
className = '',
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
sm: 'px-2 py-0.5 text-[11px]',
|
||||
md: 'px-2.5 py-1 text-xs',
|
||||
}[size]
|
||||
|
||||
const variantClasses = {
|
||||
default:
|
||||
'bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 font-medium',
|
||||
brand:
|
||||
'bg-amber-500/10 text-amber-600 dark:text-amber-400 font-semibold border border-amber-500/20',
|
||||
outline:
|
||||
'border border-zinc-200 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400 font-medium',
|
||||
success:
|
||||
'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 font-medium border border-emerald-500/20',
|
||||
}[variant]
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center justify-center rounded-md whitespace-nowrap leading-none transition-colors ${sizeClasses} ${variantClasses} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
icon,
|
||||
className = '',
|
||||
disabled,
|
||||
...props
|
||||
}) => {
|
||||
const baseClasses =
|
||||
'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed select-none'
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-2.5 py-1 text-xs gap-1.5',
|
||||
md: 'px-3.5 py-2 text-sm gap-2',
|
||||
lg: 'px-4.5 py-2.5 text-base gap-2.5',
|
||||
}[size]
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
'bg-amber-500 hover:bg-amber-600 text-white shadow-sm shadow-amber-500/20 active:translate-y-px',
|
||||
secondary:
|
||||
'bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-zinc-800 dark:text-zinc-200 border border-zinc-200 dark:border-zinc-700',
|
||||
outline:
|
||||
'bg-transparent hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-300 dark:border-zinc-700',
|
||||
ghost:
|
||||
'bg-transparent hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-600 dark:text-zinc-400',
|
||||
}[variant]
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${baseClasses} ${sizeClasses} ${variantClasses} ${className}`}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span className="inline-flex shrink-0">{icon}</span>}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
|
||||
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
selected?: boolean
|
||||
interactive?: boolean
|
||||
}
|
||||
|
||||
export const Card: React.FC<CardProps> = ({
|
||||
children,
|
||||
selected = false,
|
||||
interactive = false,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-xl border transition-all duration-150 p-4 ${
|
||||
selected
|
||||
? 'bg-white dark:bg-[#161720] border-amber-500 dark:border-amber-500 shadow-sm shadow-amber-500/10 ring-1 ring-amber-500'
|
||||
: 'bg-white dark:bg-[#14151c] border-zinc-200 dark:border-[#262732]'
|
||||
} ${
|
||||
interactive
|
||||
? 'cursor-pointer hover:border-zinc-300 dark:hover:border-zinc-700 hover:shadow-xs'
|
||||
: ''
|
||||
} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Copy, Check } from 'lucide-react'
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string
|
||||
language?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CodeBlock: React.FC<CodeBlockProps> = ({
|
||||
code,
|
||||
language,
|
||||
className = '',
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
// fallback if clipboard api fails
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative group rounded-lg overflow-hidden border border-zinc-200 dark:border-[#262732] bg-[#f8f8fa] dark:bg-[#0c0d12] ${className}`}>
|
||||
<div className="flex items-center justify-between px-3.5 py-2 border-b border-zinc-200 dark:border-[#22232c] bg-zinc-100/70 dark:bg-[#12131a]/80 text-xs text-zinc-500 font-mono">
|
||||
<span>{language ? language.toUpperCase() : 'CODE'}</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 text-xs text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors p-1 rounded hover:bg-zinc-200/50 dark:hover:bg-zinc-800"
|
||||
title="Copy snippet"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span className="text-emerald-500 font-medium">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto p-3.5 font-mono text-[13px] leading-relaxed text-zinc-800 dark:text-zinc-200 selection:bg-amber-500/20">
|
||||
<pre className="m-0 tab-4 whitespace-pre">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react'
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string
|
||||
helperText?: string
|
||||
leftIcon?: React.ReactNode
|
||||
rightElement?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Input: React.FC<InputProps> = ({
|
||||
label,
|
||||
helperText,
|
||||
leftIcon,
|
||||
rightElement,
|
||||
className = '',
|
||||
id,
|
||||
...props
|
||||
}) => {
|
||||
const inputId = id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 w-full text-left">
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="text-xs font-semibold uppercase tracking-wider text-zinc-600 dark:text-zinc-400"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative flex items-center w-full">
|
||||
{leftIcon && (
|
||||
<div className="absolute left-3 flex items-center pointer-events-none text-zinc-400 dark:text-zinc-500">
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
id={inputId}
|
||||
className={`w-full bg-white dark:bg-[#12131a] text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-600 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3.5 py-2 text-sm transition-all focus:outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500 ${
|
||||
leftIcon ? 'pl-9' : ''
|
||||
} ${rightElement ? 'pr-10' : ''} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{rightElement && (
|
||||
<div className="absolute right-2.5 flex items-center">
|
||||
{rightElement}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{helperText && (
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">{helperText}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Model } from '../types'
|
||||
|
||||
export const mockModels: Model[] = [
|
||||
{
|
||||
id: 'cc/claude-opus-4-7',
|
||||
name: 'Claude Opus 4.7 (Anthropic via 9router)',
|
||||
family: 'claude-opus-4-7',
|
||||
provider: 'Anthropic',
|
||||
contextWindow: 200000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'Most powerful reasoning model for complex architecture and codebases',
|
||||
},
|
||||
{
|
||||
id: 'cc/claude-sonnet-4.5',
|
||||
name: 'Claude Sonnet 4.5',
|
||||
family: 'claude-sonnet-4-5',
|
||||
provider: 'Anthropic',
|
||||
contextWindow: 200000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'High-speed intelligent coding powerhouse with strong agentic skills',
|
||||
},
|
||||
{
|
||||
id: 'kr/claude-sonnet-4.5',
|
||||
name: 'Claude Sonnet 4.5 (KR Route)',
|
||||
family: 'claude-sonnet-4-5',
|
||||
provider: 'Anthropic',
|
||||
contextWindow: 200000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'Alternative regional routing for low latency',
|
||||
},
|
||||
{
|
||||
id: 'openai/gpt-4o',
|
||||
name: 'GPT-4o',
|
||||
family: 'gpt-4o',
|
||||
provider: 'OpenAI',
|
||||
contextWindow: 128000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'Flagship multimodal omni model with rapid reasoning and code output',
|
||||
},
|
||||
{
|
||||
id: 'openai/o3-mini',
|
||||
name: 'o3-mini (High)',
|
||||
family: 'o3-mini',
|
||||
provider: 'OpenAI',
|
||||
contextWindow: 200000,
|
||||
vision: false,
|
||||
toolCalling: true,
|
||||
description: 'Deep mathematical and algorithmic reasoning specialist',
|
||||
},
|
||||
{
|
||||
id: 'vertex/gemini-3-flash-preview',
|
||||
name: 'Gemini 3 Flash Preview',
|
||||
family: 'gemini-3-flash',
|
||||
provider: 'Google Vertex',
|
||||
contextWindow: 1000000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'Ultra high-speed large 1M token context model for massive file ingestion',
|
||||
},
|
||||
{
|
||||
id: 'deepseek/deepseek-r1',
|
||||
name: 'DeepSeek R1',
|
||||
family: 'deepseek-r1',
|
||||
provider: 'DeepSeek',
|
||||
contextWindow: 64000,
|
||||
vision: false,
|
||||
toolCalling: true,
|
||||
description: 'Open-weights reasoning model with high performance on coding tasks',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { ToolId, ToolMeta, ConnectionConfig, Model } from '../types'
|
||||
|
||||
export const tools: Record<ToolId, ToolMeta> = {
|
||||
copilot: {
|
||||
id: 'copilot',
|
||||
name: 'GitHub Copilot',
|
||||
tagline: 'VS Code chatLanguageModels.json configuration',
|
||||
targetFilename: 'chatLanguageModels.json',
|
||||
locationDescription: 'VS Code User settings or global profile directory',
|
||||
language: 'json',
|
||||
iconName: 'bot',
|
||||
sampleTemplate: (connection: ConnectionConfig, selectedModels: Model[]) => {
|
||||
const activeModels = selectedModels.length > 0 ? selectedModels : [
|
||||
{
|
||||
id: 'cc/claude-sonnet-4.5',
|
||||
name: 'Claude Sonnet 4.5',
|
||||
family: 'claude-sonnet-4-5',
|
||||
contextWindow: 200000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
}
|
||||
]
|
||||
|
||||
const entries = activeModels.map((m) => ({
|
||||
id: `9router-${m.id.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
|
||||
name: `${m.name || m.id} (9router)`,
|
||||
vendor: 'customoai',
|
||||
family: m.family || m.id.split('/').pop() || 'custom',
|
||||
version: '1.0.0',
|
||||
maxInputTokens: m.contextWindow || 128000,
|
||||
maxOutputTokens: 8192,
|
||||
endpoint: connection.baseUrl.replace(/\/+$/, '') + '/chat/completions',
|
||||
apiKey: connection.apiKey ? connection.apiKey : '${input:9router-api-key}',
|
||||
model: m.id,
|
||||
toolCalling: m.toolCalling ?? true,
|
||||
vision: m.vision ?? true,
|
||||
}))
|
||||
|
||||
return JSON.stringify(entries, null, 2)
|
||||
},
|
||||
},
|
||||
'claude-code': {
|
||||
id: 'claude-code',
|
||||
name: 'Claude Code',
|
||||
tagline: 'Anthropic CLI ~/.claude/config.json',
|
||||
targetFilename: 'config.json',
|
||||
locationDescription: '~/.claude/config.json or environment variables',
|
||||
language: 'json',
|
||||
iconName: 'terminal',
|
||||
sampleTemplate: (connection: ConnectionConfig, selectedModels: Model[]) => {
|
||||
const primaryModel = selectedModels[0]?.id || 'cc/claude-sonnet-4.5'
|
||||
const configObj = {
|
||||
anthropic_api_base: connection.baseUrl.replace(/\/+$/, ''),
|
||||
anthropic_api_key: connection.apiKey || 'sk-9router-local-key',
|
||||
model: primaryModel,
|
||||
verbose: false,
|
||||
}
|
||||
|
||||
const envNotice = `// Target: ~/.claude/config.json\n// Alternative environment variables:\n// export ANTHROPIC_BASE_URL="${connection.baseUrl}"\n// export ANTHROPIC_AUTH_TOKEN="${connection.apiKey || 'sk-9router-local-key'}"\n// export ANTHROPIC_MODEL="${primaryModel}"\n\n`
|
||||
return envNotice + JSON.stringify(configObj, null, 2)
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
id: 'codex',
|
||||
name: 'Codex CLI',
|
||||
tagline: 'Codex CLI ~/.codex/config.toml configuration',
|
||||
targetFilename: 'config.toml',
|
||||
locationDescription: '~/.codex/config.toml',
|
||||
language: 'toml',
|
||||
iconName: 'cpu',
|
||||
sampleTemplate: (connection: ConnectionConfig, selectedModels: Model[]) => {
|
||||
const primaryModel = selectedModels[0]?.id || 'openai/gpt-4o'
|
||||
const apiKeyVal = connection.apiKey || '9ROUTER_API_KEY'
|
||||
return `# ~/.codex/config.toml
|
||||
model_provider = "9router"
|
||||
model = "${primaryModel}"
|
||||
temperature = 0.2
|
||||
|
||||
[model_providers.9router]
|
||||
name = "9router Local Gateway"
|
||||
base_url = "${connection.baseUrl.replace(/\/+$/, '')}"
|
||||
env_key = "${apiKeyVal}"
|
||||
wire_format = "openai"
|
||||
`
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const toolList: ToolMeta[] = [
|
||||
tools.copilot,
|
||||
tools['claude-code'],
|
||||
tools.codex,
|
||||
]
|
||||
+49
-88
@@ -1,111 +1,72 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand: #f59e0b;
|
||||
--color-brand-hover: #d97706;
|
||||
--color-brand-subtle: rgba(245, 158, 11, 0.1);
|
||||
--color-brand-border: rgba(245, 158, 11, 0.35);
|
||||
|
||||
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
--app-bg: #fcfcfd;
|
||||
--app-fg: #09090b;
|
||||
--app-muted: #71717a;
|
||||
--app-card-bg: #ffffff;
|
||||
--app-card-border: #e4e4e7;
|
||||
--app-code-bg: #f4f4f5;
|
||||
--app-code-border: #e4e4e7;
|
||||
--app-hover-bg: #f4f4f5;
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
--app-bg: #0d0e12;
|
||||
--app-fg: #f4f4f5;
|
||||
--app-muted: #9ca3af;
|
||||
--app-card-bg: #15161d;
|
||||
--app-card-border: #262732;
|
||||
--app-code-bg: #101117;
|
||||
--app-code-border: #22232c;
|
||||
--app-hover-bg: #1c1e28;
|
||||
}
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--app-bg);
|
||||
color: var(--app-fg);
|
||||
font-family: var(--font-sans);
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(156, 163, 175, 0.3);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(156, 163, 175, 0.5);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export type ToolId = 'copilot' | 'claude-code' | 'codex'
|
||||
|
||||
export interface Model {
|
||||
id: string
|
||||
name: string
|
||||
contextWindow?: number
|
||||
provider?: string
|
||||
description?: string
|
||||
family?: string
|
||||
vision?: boolean
|
||||
toolCalling?: boolean
|
||||
}
|
||||
|
||||
export interface ConnectionConfig {
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface ToolMeta {
|
||||
id: ToolId
|
||||
name: string
|
||||
tagline: string
|
||||
targetFilename: string
|
||||
locationDescription: string
|
||||
language: 'json' | 'toml' | 'bash'
|
||||
iconName: 'bot' | 'terminal' | 'cpu'
|
||||
sampleTemplate: (connection: ConnectionConfig, selectedModels: Model[]) => string
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
|
||||
import babel from '@rolldown/plugin-babel'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
react(),
|
||||
babel({ presets: [reactCompilerPreset()] })
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user