From 570512549dacdce996ab616615cf14d12a0850ae Mon Sep 17 00:00:00 2001 From: Nezumi-2711 Date: Sat, 15 Aug 2026 10:13:16 +0700 Subject: [PATCH] feat: init the structure ui of the app --- bun.lock | 83 ++++++++++++ package.json | 3 + src/App.tsx | 218 +++++++++++++++--------------- src/components/ConfigPreview.tsx | 112 +++++++++++++++ src/components/ConnectionForm.tsx | 82 +++++++++++ src/components/Header.tsx | 50 +++++++ src/components/ModelList.tsx | 154 +++++++++++++++++++++ src/components/ToolSelector.tsx | 91 +++++++++++++ src/components/ui/Badge.tsx | 39 ++++++ src/components/ui/Button.tsx | 48 +++++++ src/components/ui/Card.tsx | 31 +++++ src/components/ui/CodeBlock.tsx | 56 ++++++++ src/components/ui/Input.tsx | 55 ++++++++ src/data/mockModels.ts | 74 ++++++++++ src/data/tools.ts | 93 +++++++++++++ src/index.css | 137 +++++++------------ src/types.ts | 28 ++++ vite.config.ts | 2 + 18 files changed, 1156 insertions(+), 200 deletions(-) create mode 100644 src/components/ConfigPreview.tsx create mode 100644 src/components/ConnectionForm.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/ModelList.tsx create mode 100644 src/components/ToolSelector.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/CodeBlock.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/data/mockModels.ts create mode 100644 src/data/tools.ts create mode 100644 src/types.ts diff --git a/bun.lock b/bun.lock index 0c2f9da..507bb5d 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], } } diff --git a/package.json b/package.json index d33a403..6b124c6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index a66b5ef..3a7bb63 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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({ + baseUrl: 'http://localhost:20128/v1', + apiKey: '', + }) + + // Default select 2 popular models + const [selectedModelIds, setSelectedModelIds] = useState([ + 'cc/claude-sonnet-4.5', + 'openai/gpt-4o', + ]) + + const [selectedToolId, setSelectedToolId] = useState('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 ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.tsx and save to test HMR -

-
- -
+
+
-
+
+
+ {/* Left Controls Pane */} +
+ -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
+ -
-
- + +
+ + {/* Right Sticky Preview Pane */} +
+ +
+
+
+
) } diff --git a/src/components/ConfigPreview.tsx b/src/components/ConfigPreview.tsx new file mode 100644 index 0000000..60d68f4 --- /dev/null +++ b/src/components/ConfigPreview.tsx @@ -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 = ({ + 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 ( +
+ +
+
+
+ +
+
+
+ + {tool.targetFilename} + + + {tool.language.toUpperCase()} + +
+ + Target for {tool.name} + +
+
+ +
+ + +
+
+ +
+ +
+ + Save Location:{' '} + + {tool.locationDescription} +
+
+ + + +
+ {selectedModels.length} model(s) included + Live preview interpolated +
+
+
+ ) +} diff --git a/src/components/ConnectionForm.tsx b/src/components/ConnectionForm.tsx new file mode 100644 index 0000000..79f81cb --- /dev/null +++ b/src/components/ConnectionForm.tsx @@ -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 = ({ + connection, + onChange, +}) => { + const [showKey, setShowKey] = useState(false) + + return ( + +
+
+

+ 1. Gateway Connection +

+ + Local Router + +
+ OpenAI compatible +
+ +
+ onChange({ ...connection, baseUrl: e.target.value })} + leftIcon={} + helperText="Default 9router endpoint is http://localhost:20128/v1" + /> + + onChange({ ...connection, apiKey: e.target.value })} + leftIcon={} + helperText="Passed as authorization header if required" + rightElement={ + + } + /> +
+ +
+
+
+ Local mock catalog loaded +
+ +
+ + ) +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..206c224 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { Sparkles, Terminal, Globe } from 'lucide-react' + +export const Header: React.FC = () => { + return ( +
+
+
+
+ +
+
+
+

+ 9router Config Generator +

+ + v1.0 + +
+

+ Generate ready-to-paste AI coding tool configs for your local gateway +

+
+
+ + +
+
+ ) +} diff --git a/src/components/ModelList.tsx b/src/components/ModelList.tsx new file mode 100644 index 0000000..3179a4e --- /dev/null +++ b/src/components/ModelList.tsx @@ -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 = ({ + 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 ( + +
+
+

+ 2. Select Models +

+ + {selectedIds.length} selected + +
+ +
+ +
+
+ + onSearchChange(e.target.value)} + leftIcon={} + className="text-xs" + /> + +
+ {filteredModels.length === 0 ? ( +
+ No models matching "{searchQuery}" +
+ ) : ( + filteredModels.map((model) => { + const isSelected = selectedIds.includes(model.id) + return ( +
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' + }`} + > + + +
+
+ + {model.id} + + {model.provider && ( + + {model.provider} + + )} +
+ {model.name && model.name !== model.id && ( +

+ {model.name} +

+ )} + {model.description && ( +

+ {model.description} +

+ )} +
+ +
+ {model.contextWindow && ( + + + {(model.contextWindow / 1000).toFixed(0)}k + + )} +
+ {model.vision && ( + + + + )} + {model.toolCalling && ( + + + + )} +
+
+
+ ) + }) + )} +
+
+ ) +} diff --git a/src/components/ToolSelector.tsx b/src/components/ToolSelector.tsx new file mode 100644 index 0000000..aefad28 --- /dev/null +++ b/src/components/ToolSelector.tsx @@ -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 = ({ + tools, + selectedTool, + onSelectTool, +}) => { + const getIcon = (iconName: ToolMeta['iconName']) => { + switch (iconName) { + case 'bot': + return + case 'terminal': + return + case 'cpu': + return + } + } + + return ( + +
+
+

+ 3. Choose Target Tool +

+ + {tools.length} available + +
+ Single select +
+ +
+ {tools.map((tool) => { + const isSelected = selectedTool === tool.id + return ( +
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 && ( +
+ +
+ )} + +
+
+ {getIcon(tool.iconName)} +
+

+ {tool.name} +

+

+ {tool.tagline} +

+
+ +
+ + {tool.targetFilename} + +
+
+ ) + })} +
+
+ ) +} diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..ac294d7 --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -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 = ({ + 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 ( + + {children} + + ) +} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..a432c96 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,48 @@ +import React from 'react' + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'outline' | 'ghost' + size?: 'sm' | 'md' | 'lg' + icon?: React.ReactNode +} + +export const Button: React.FC = ({ + 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 ( + + ) +} diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx new file mode 100644 index 0000000..8bea72e --- /dev/null +++ b/src/components/ui/Card.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +interface CardProps extends React.HTMLAttributes { + selected?: boolean + interactive?: boolean +} + +export const Card: React.FC = ({ + children, + selected = false, + interactive = false, + className = '', + ...props +}) => { + return ( +
+ {children} +
+ ) +} diff --git a/src/components/ui/CodeBlock.tsx b/src/components/ui/CodeBlock.tsx new file mode 100644 index 0000000..39252f5 --- /dev/null +++ b/src/components/ui/CodeBlock.tsx @@ -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 = ({ + 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 ( +
+
+ {language ? language.toUpperCase() : 'CODE'} + +
+
+
+          {code}
+        
+
+
+ ) +} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..0dba9e0 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,55 @@ +import React from 'react' + +interface InputProps extends React.InputHTMLAttributes { + label?: string + helperText?: string + leftIcon?: React.ReactNode + rightElement?: React.ReactNode +} + +export const Input: React.FC = ({ + label, + helperText, + leftIcon, + rightElement, + className = '', + id, + ...props +}) => { + const inputId = id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined) + + return ( +
+ {label && ( + + )} +
+ {leftIcon && ( +
+ {leftIcon} +
+ )} + + {rightElement && ( +
+ {rightElement} +
+ )} +
+ {helperText && ( + {helperText} + )} +
+ ) +} diff --git a/src/data/mockModels.ts b/src/data/mockModels.ts new file mode 100644 index 0000000..b7997db --- /dev/null +++ b/src/data/mockModels.ts @@ -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', + }, +] diff --git a/src/data/tools.ts b/src/data/tools.ts new file mode 100644 index 0000000..0ade750 --- /dev/null +++ b/src/data/tools.ts @@ -0,0 +1,93 @@ +import type { ToolId, ToolMeta, ConnectionConfig, Model } from '../types' + +export const tools: Record = { + 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, +] diff --git a/src/index.css b/src/index.css index 5fb3313..ca572a9 100644 --- a/src/index.css +++ b/src/index.css @@ -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); } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b15f9b9 --- /dev/null +++ b/src/types.ts @@ -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 +} diff --git a/vite.config.ts b/vite.config.ts index d1203cd..1ba605f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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()] }) ],