diff --git a/package-lock.json b/package-lock.json
index 654f31b89..a6fc2cb2e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -50,6 +50,7 @@
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"vite": "^5.4.10",
+ "vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.16"
}
},
@@ -97,6 +98,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -2137,6 +2139,49 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
+ "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/pluginutils/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
@@ -2452,6 +2497,232 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@svgr/babel-plugin-add-jsx-attribute": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
+ "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz",
+ "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz",
+ "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz",
+ "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-svg-dynamic-title": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz",
+ "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-svg-em-dimensions": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz",
+ "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-transform-react-native-svg": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz",
+ "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-transform-svg-component": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz",
+ "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-preset": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz",
+ "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@svgr/babel-plugin-add-jsx-attribute": "8.0.0",
+ "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0",
+ "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0",
+ "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0",
+ "@svgr/babel-plugin-svg-dynamic-title": "8.0.0",
+ "@svgr/babel-plugin-svg-em-dimensions": "8.0.0",
+ "@svgr/babel-plugin-transform-react-native-svg": "8.1.0",
+ "@svgr/babel-plugin-transform-svg-component": "8.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/core": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
+ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/core": "^7.21.3",
+ "@svgr/babel-preset": "8.1.0",
+ "camelcase": "^6.2.0",
+ "cosmiconfig": "^8.1.3",
+ "snake-case": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/hast-util-to-babel-ast": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz",
+ "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.21.3",
+ "entities": "^4.4.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/plugin-jsx": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz",
+ "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.21.3",
+ "@svgr/babel-preset": "8.1.0",
+ "@svgr/hast-util-to-babel-ast": "8.0.0",
+ "svg-parser": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@svgr/core": "*"
+ }
+ },
"node_modules/@tauri-apps/api": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz",
@@ -2780,6 +3051,7 @@
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -2797,6 +3069,7 @@
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -2808,6 +3081,7 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -2847,6 +3121,7 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
@@ -3169,6 +3444,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3387,6 +3663,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3411,6 +3688,19 @@
"node": ">=6"
}
},
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -3572,6 +3862,33 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cosmiconfig": {
+ "version": "8.3.6",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
+ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0",
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3652,6 +3969,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/dot-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
+ "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
@@ -3659,6 +3987,29 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -3734,6 +4085,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4293,6 +4645,13 @@
"node": ">=0.8.19"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -4368,6 +4727,7 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -4411,6 +4771,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -4517,6 +4884,16 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lower-case": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
+ "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -4631,6 +5008,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/no-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
+ "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lower-case": "^2.0.2",
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -4752,6 +5140,25 @@
"node": ">=6"
}
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4779,6 +5186,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -4846,6 +5263,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -5068,6 +5486,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -5080,6 +5499,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -5347,6 +5767,17 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/snake-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
+ "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dot-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
@@ -5443,6 +5874,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/svg-parser": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
+ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
@@ -5572,6 +6010,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -5647,6 +6086,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5759,6 +6199,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -5813,12 +6254,28 @@
}
}
},
+ "node_modules/vite-plugin-svgr": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.5.0.tgz",
+ "integrity": "sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.2.0",
+ "@svgr/core": "^8.1.0",
+ "@svgr/plugin-jsx": "^8.1.0"
+ },
+ "peerDependencies": {
+ "vite": ">=2.6.0"
+ }
+ },
"node_modules/vitest": {
"version": "4.0.16",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "4.0.16",
"@vitest/mocker": "4.0.16",
@@ -6375,6 +6832,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -6388,6 +6846,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
diff --git a/package.json b/package.json
index 9b2618e3f..782ba58d9 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"vite": "^5.4.10",
+ "vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.16"
}
}
diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx
index 0d9e237e9..621dc9396 100644
--- a/src/components/ConfirmDialog.tsx
+++ b/src/components/ConfirmDialog.tsx
@@ -1,4 +1,5 @@
-import { AlertTriangle, X } from "lucide-react";
+import { AlertTriangle } from "lucide-react";
+import { Modal } from "./Modal";
interface ConfirmDialogProps {
isOpen: boolean;
@@ -21,8 +22,6 @@ export function ConfirmDialog({
onConfirm,
onCancel,
}: ConfirmDialogProps) {
- if (!isOpen) return null;
-
const variantStyles = {
danger: {
icon: "text-red-500",
@@ -41,22 +40,19 @@ export function ConfirmDialog({
const styles = variantStyles[variant];
return (
-
-
+
+
-
-
{title}
-
-
+
{title}
{message}
@@ -75,6 +71,6 @@ export function ConfirmDialog({
-
+
);
}
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
new file mode 100644
index 000000000..ccfc4dadb
--- /dev/null
+++ b/src/components/Modal.tsx
@@ -0,0 +1,124 @@
+import { useEffect, type ReactNode, type MouseEvent } from "react";
+import { createPortal } from "react-dom";
+import { X } from "lucide-react";
+
+interface ModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ children: ReactNode;
+ /** Modal 内容区的额外 className */
+ className?: string;
+ /** 是否显示关闭按钮,默认 true */
+ showCloseButton?: boolean;
+ /** 点击遮罩是否关闭,默认 true */
+ closeOnOverlayClick?: boolean;
+ /** 内容区最大宽度,默认 max-w-lg */
+ maxWidth?: string;
+}
+
+export function Modal({
+ isOpen,
+ onClose,
+ children,
+ className = "",
+ showCloseButton = true,
+ closeOnOverlayClick = true,
+ maxWidth = "max-w-lg",
+}: ModalProps) {
+ // ESC 键关闭
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const handleEsc = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ onClose();
+ }
+ };
+
+ document.addEventListener("keydown", handleEsc);
+ return () => document.removeEventListener("keydown", handleEsc);
+ }, [isOpen, onClose]);
+
+ // 阻止背景滚动
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "";
+ }
+ return () => {
+ document.body.style.overflow = "";
+ };
+ }, [isOpen]);
+
+ if (!isOpen) return null;
+
+ const handleOverlayClick = (e: MouseEvent) => {
+ if (closeOnOverlayClick && e.target === e.currentTarget) {
+ onClose();
+ }
+ };
+
+ return createPortal(
+
+
+ {showCloseButton && (
+
+ )}
+ {children}
+
+
,
+ document.body,
+ );
+}
+
+/** Modal 标题区 */
+export function ModalHeader({
+ children,
+ className = "",
+}: {
+ children: ReactNode;
+ className?: string;
+}) {
+ return (
+
+
{children}
+
+ );
+}
+
+/** Modal 内容区 */
+export function ModalBody({
+ children,
+ className = "",
+}: {
+ children: ReactNode;
+ className?: string;
+}) {
+ return {children}
;
+}
+
+/** Modal 底部操作区 */
+export function ModalFooter({
+ children,
+ className = "",
+}: {
+ children: ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/config/ImportExport.tsx b/src/components/config/ImportExport.tsx
index 460628da8..47765a40d 100644
--- a/src/components/config/ImportExport.tsx
+++ b/src/components/config/ImportExport.tsx
@@ -10,6 +10,7 @@ import {
FileText,
Package,
} from "lucide-react";
+import { Modal } from "@/components/Modal";
import { Config, configApi, ImportResult } from "@/lib/api/config";
interface ImportExportProps {
@@ -314,220 +315,226 @@ export function ImportExport({ config, onConfigImported }: ImportExportProps) {
{/* Security Warning Dialog */}
- {showSecurityWarning && (
-
-
-
-
-
安全警告
+
setShowSecurityWarning(false)}
+ maxWidth="max-w-md"
+ showCloseButton={false}
+ >
+
+
-
- 您即将导出未脱敏的凭证数据,导出文件将包含明文 API 密钥和 OAuth
- Token。 请确保:
-
-
- - 不要将此文件分享给他人
- - 不要上传到公共代码仓库
- - 妥善保管导出文件
-
+
+ 您即将导出未脱敏的凭证数据,导出文件将包含明文 API 密钥和 OAuth
+ Token。 请确保:
+
+
+ - 不要将此文件分享给他人
+ - 不要上传到公共代码仓库
+ - 妥善保管导出文件
+
-
-
-
-
+
+
+
- )}
+
{/* Import Dialog */}
- {showImportDialog && (
-
-
-
- {getFileTypeIcon()}
- 导入配置 - {importFileName}
-
+
+
+
+ {getFileTypeIcon()}
+ 导入配置 - {importFileName}
+
- {/* Validation Result */}
- {validationResult && (
-
- {validationResult.valid ? (
-
-
- 文件格式有效
-
- ) : (
-
-
-
-
文件格式无效
-
- {validationResult.errors.map((err, i) => (
- - {err}
- ))}
-
-
-
- )}
-
- {/* Content info */}
- {validationResult.valid && (
-
- {validationResult.version && (
-
- 版本: {validationResult.version}
-
- )}
- {validationResult.has_config && (
-
- 包含配置
-
- )}
- {validationResult.has_credentials && (
-
- 包含凭证
-
- )}
- {validationResult.redacted && (
-
- 已脱敏
-
- )}
-
- )}
-
- {/* Redaction warning */}
- {validationResult.redacted && (
-
-
-
- 此导出包已脱敏,凭证数据(API 密钥、Token)无法恢复。
-
-
- )}
-
- {/* Validation warnings */}
- {validationResult.warnings.length > 0 && (
-
-
- 警告
-
-
- {validationResult.warnings.map((warning, i) => (
- - {warning}
+ {/* Validation Result */}
+ {validationResult && (
+
+ {validationResult.valid ? (
+
+
+ 文件格式有效
+
+ ) : (
+
+
+
+
文件格式无效
+
+ {validationResult.errors.map((err, i) => (
+ - {err}
))}
- )}
-
- )}
+
+ )}
- {/* Preview */}
-
-
-
- {importContent.slice(0, 2000)}
- {importContent.length > 2000 && "\n..."}
-
-
+ {/* Content info */}
+ {validationResult.valid && (
+
+ {validationResult.version && (
+
+ 版本: {validationResult.version}
+
+ )}
+ {validationResult.has_config && (
+
+ 包含配置
+
+ )}
+ {validationResult.has_credentials && (
+
+ 包含凭证
+
+ )}
+ {validationResult.redacted && (
+
+ 已脱敏
+
+ )}
+
+ )}
- {/* Import Options */}
-
+ )}
- {/* Import Result Warnings */}
- {importResult?.warnings && importResult.warnings.length > 0 && (
-
-
- 导入警告
-
-
- {importResult.warnings.map((warning, i) => (
- - {warning}
- ))}
-
-
- )}
-
- {/* Success */}
- {importResult?.success && (
-
-
- 配置导入成功
-
- )}
-
- {/* Error */}
- {error && (
-
- )}
-
- {/* Actions */}
-
-
- {!importResult?.success && validationResult?.valid && (
-
+ {/* Validation warnings */}
+ {validationResult.warnings.length > 0 && (
+
+
+ 警告
+
+
+ {validationResult.warnings.map((warning, i) => (
+ - {warning}
+ ))}
+
+
)}
+ )}
+
+ {/* Preview */}
+
+
+
+ {importContent.slice(0, 2000)}
+ {importContent.length > 2000 && "\n..."}
+
+
+
+ {/* Import Options */}
+
+
+ {/* Import Result Warnings */}
+ {importResult?.warnings && importResult.warnings.length > 0 && (
+
+
+ 导入警告
+
+
+ {importResult.warnings.map((warning, i) => (
+ - {warning}
+ ))}
+
+
+ )}
+
+ {/* Success */}
+ {importResult?.success && (
+
+
+ 配置导入成功
+
+ )}
+
+ {/* Error */}
+ {error && (
+
+ )}
+
+ {/* Actions */}
+
+
+ {!importResult?.success && validationResult?.valid && (
+
+ )}
- )}
+
{/* Global Error */}
{error && !showImportDialog && (
diff --git a/src/components/flow-monitor/NotificationSettings.tsx b/src/components/flow-monitor/NotificationSettings.tsx
index ba025debe..c02a3cdad 100644
--- a/src/components/flow-monitor/NotificationSettings.tsx
+++ b/src/components/flow-monitor/NotificationSettings.tsx
@@ -8,6 +8,7 @@
import { useState, useEffect } from "react";
import { Bell, BellOff, Settings } from "lucide-react";
+import { Modal } from "@/components/Modal";
import {
notificationService,
type NotificationPermission,
@@ -114,22 +115,18 @@ export function NotificationSettings({
});
};
- if (!open) return null;
-
if (loading) {
return (
-
+
{})} maxWidth="max-w-sm">
+ 加载中...
+
);
}
if (error || !config) {
return (
-
-
+
{})} maxWidth="max-w-sm">
+
{error || "配置加载失败"}
@@ -140,412 +137,403 @@ export function NotificationSettings({
关闭
-
+
);
}
return (
-
-
- {/* 标题栏 */}
-
-
-
-
通知设置
+
{})}
+ maxWidth="max-w-sm"
+ className="max-h-[80vh] overflow-y-auto"
+ >
+ {/* 标题栏 */}
+
+
+ {/* 内容 */}
+
+ {/* 权限状态 */}
+
+
+
+
+ {permissionStatus === "granted" && "已授权"}
+ {permissionStatus === "denied" && "已拒绝"}
+ {permissionStatus === "default" && "未设置"}
+
+ {permissionStatus !== "granted" && (
+
+ )}
-
- {/* 内容 */}
-
- {/* 权限状态 */}
-
-
-
-
- {permissionStatus === "granted" && "已授权"}
- {permissionStatus === "denied" && "已拒绝"}
- {permissionStatus === "default" && "未设置"}
-
- {permissionStatus !== "granted" && (
-
+ {/* 基本设置 */}
+
+
+
+ {/* 启用通知 */}
+
+
+ {config.enabled ? (
+
+ ) : (
+
)}
+ 启用通知
-
-
- {/* 基本设置 */}
-
-
-
- {/* 启用通知 */}
-
-
- {config.enabled ? (
-
- ) : (
-
+
+ />
+
+
+
+
+ {/* 通知类型 */}
+
+
+
+ {/* 新 Flow 通知 */}
+
+
+ 新 LLM 请求
+ {config.new_flow.enabled && (
+
+
+ 桌面通知
+
+
+
+ 声音提示
+
+
+
+ )}
- {/* 通知类型 */}
-
-
-
- {/* 新 Flow 通知 */}
-
-
-
新 LLM 请求
-
- {/* 测试按钮 */}
-
-
- 发送测试通知
-
+ {/* Token 警告通知 */}
+
+
+ Token 警告
+
+ updateNotificationSettings("token_warning", {
+ enabled: !config.token_warning.enabled,
+ })
+ }
+ className={cn(
+ "relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
+ config.token_warning.enabled ? "bg-blue-500" : "bg-muted",
+ )}
+ >
+
+
+
+ {config.token_warning.enabled && (
+
+
+ 桌面通知
+
+ updateNotificationSettings("token_warning", {
+ desktop: !config.token_warning.desktop,
+ })
+ }
+ className={cn(
+ "relative inline-flex h-4 w-7 items-center rounded-full transition-colors",
+ config.token_warning.desktop ? "bg-blue-500" : "bg-muted",
+ )}
+ >
+
+
+
+
+ 声音提示
+
+ updateNotificationSettings("token_warning", {
+ sound: !config.token_warning.sound,
+ })
+ }
+ className={cn(
+ "relative inline-flex h-4 w-7 items-center rounded-full transition-colors",
+ config.token_warning.sound ? "bg-blue-500" : "bg-muted",
+ )}
+ >
+
+
+
+
+ )}
+
+ {/* 测试按钮 */}
+
+
+ 发送测试通知
+
+
-
+
);
}
diff --git a/src/components/provider-pool/AddCredentialModal.tsx b/src/components/provider-pool/AddCredentialModal.tsx
index 38d2e380f..c6a51b763 100644
--- a/src/components/provider-pool/AddCredentialModal.tsx
+++ b/src/components/provider-pool/AddCredentialModal.tsx
@@ -4,8 +4,9 @@
*/
import { useState } from "react";
-import { X, Key, FolderOpen } from "lucide-react";
+import { Key, FolderOpen } from "lucide-react";
import { open } from "@tauri-apps/plugin-dialog";
+import { Modal } from "@/components/Modal";
import { providerPoolApi, PoolProviderType } from "@/lib/api/providerPool";
import { AntigravityForm } from "./credential-forms/AntigravityForm";
import { CodexForm } from "./credential-forms/CodexForm";
@@ -514,64 +515,57 @@ export function AddCredentialModal({
};
return (
-
-
- {/* Header */}
-
-
- 添加 {providerLabels[providerType]} 凭证
-
-
-
-
-
-
- {/* Content */}
-
- {/* 名称字段 */}
-
-
- setName(e.target.value)}
- placeholder="给这个凭证起个名字..."
- className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
- />
-
-
- {/* 根据类型渲染不同表单 */}
- {providerType === "antigravity" && antigravityForm.render()}
- {providerType === "codex" && codexForm.render()}
- {providerType === "claude_oauth" && claudeOAuthForm.render()}
- {providerType === "qwen" && qwenForm.render()}
- {providerType === "iflow" && iflowForm.render()}
- {providerType === "gemini" && geminiForm.render()}
- {providerType === "kiro" && kiroForm.render()}
- {isSimpleOAuth.includes(providerType) && renderSimpleOAuthForm()}
- {isApiKey && renderApiKeyForm()}
-
- {/* 错误提示 */}
- {error && (
-
- {error}
-
- )}
-
-
- {/* Footer */}
-
-
- 取消
-
- {renderFooterButton()}
-
+
+ {/* Header */}
+
+
+ 添加 {providerLabels[providerType]} 凭证
+
-
+
+ {/* Content */}
+
+ {/* 名称字段 */}
+
+
+ setName(e.target.value)}
+ placeholder="给这个凭证起个名字..."
+ className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
+ />
+
+
+ {/* 根据类型渲染不同表单 */}
+ {providerType === "antigravity" && antigravityForm.render()}
+ {providerType === "codex" && codexForm.render()}
+ {providerType === "claude_oauth" && claudeOAuthForm.render()}
+ {providerType === "qwen" && qwenForm.render()}
+ {providerType === "iflow" && iflowForm.render()}
+ {providerType === "gemini" && geminiForm.render()}
+ {providerType === "kiro" && kiroForm.render()}
+ {isSimpleOAuth.includes(providerType) && renderSimpleOAuthForm()}
+ {isApiKey && renderApiKeyForm()}
+
+ {/* 错误提示 */}
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {/* Footer */}
+
+
+ 取消
+
+ {renderFooterButton()}
+
+
);
}
diff --git a/src/components/provider-pool/EditCredentialModal.tsx b/src/components/provider-pool/EditCredentialModal.tsx
index 18fa230bf..ccba24792 100644
--- a/src/components/provider-pool/EditCredentialModal.tsx
+++ b/src/components/provider-pool/EditCredentialModal.tsx
@@ -1,6 +1,5 @@
import { useState, useEffect } from "react";
import {
- X,
Eye,
EyeOff,
Settings,
@@ -10,6 +9,7 @@ import {
Globe,
} from "lucide-react";
import { open } from "@tauri-apps/plugin-dialog";
+import { Modal } from "@/components/Modal";
import {
CredentialDisplay,
UpdateCredentialRequest,
@@ -227,318 +227,311 @@ export function EditCredentialModal({
};
return (
-
-
- {/* Header */}
-
-
-
- 编辑凭证
-
-
-
-
-
+
+ {/* Header */}
+
+
+
+ 编辑凭证
+
+
- {/* Content - Scrollable */}
-
-
- {/* 名称 + 健康检查 */}
-
-
-
- setName(e.target.value)}
- placeholder="给这个凭证起个名字..."
- className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
- />
-
-
-
-
-
-
-
- {/* 检查模型名称 */}
+ {/* Content - Scrollable */}
+
+
+ {/* 名称 + 健康检查 */}
+
setCheckModelName(e.target.value)}
- placeholder="用于健康检查的模型名称..."
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ placeholder="给这个凭证起个名字..."
className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
/>
+
+
+
+
+
- {/* OAuth凭据文件路径 */}
- {isOAuth && (
+ {/* 检查模型名称 */}
+
+
+ setCheckModelName(e.target.value)}
+ placeholder="用于健康检查的模型名称..."
+ className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
+ />
+
+
+ {/* OAuth凭据文件路径 */}
+ {isOAuth && (
+
+
+
+
+
+ setShowCredentialDetails(!showCredentialDetails)
+ }
+ className="rounded-lg border p-2 hover:bg-muted"
+ title={showCredentialDetails ? "隐藏" : "显示"}
+ >
+ {showCredentialDetails ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {newCredFilePath && (
+
+
+ 新文件已选择: {newCredFilePath.split("/").pop()}
+
+ )}
+
+ )}
+
+ {/* Gemini Project ID */}
+ {credential.credential_type === "gemini_oauth" && newCredFilePath && (
+
+
+ setNewProjectId(e.target.value)}
+ placeholder="留空保持当前项目ID..."
+ className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
+ />
+
+ )}
+
+ {/* API Key 编辑 */}
+ {isApiKey && (
+ <>
setNewApiKey(e.target.value)}
+ placeholder="留空保持当前 Key,或输入新的 API Key..."
+ className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm"
/>
- setShowCredentialDetails(!showCredentialDetails)
- }
+ onClick={() => setShowApiKey(!showApiKey)}
className="rounded-lg border p-2 hover:bg-muted"
- title={showCredentialDetails ? "隐藏" : "显示"}
+ title={showApiKey ? "隐藏" : "显示"}
>
- {showCredentialDetails ? (
+ {showApiKey ? (
) : (
)}
-
-
-
- {newCredFilePath && (
-
-
- 新文件已选择: {newCredFilePath.split("/").pop()}
-
+
+ 当前: {credential.display_credential}
+
+
+
+
+
setNewBaseUrl(e.target.value)}
+ placeholder={
+ credential.credential_type === "openai_key"
+ ? "https://api.openai.com"
+ : "https://api.anthropic.com"
+ }
+ className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
+ />
+
+ 留空使用默认 URL,或输入自定义代理地址(不要包含 /v1 后缀)
+
+
+ >
+ )}
+
+ {/* 不支持的模型 - Checkbox Grid */}
+
+
+
+
+
+ 选择此提供商不支持的模型,系统会自动排除这些模型
+
+
+
+ {currentProviderModels.map((model) => (
+
+ ))}
+
+
+
+ {/* 高级选项:代理设置 */}
+
+
+
+
+
+ (高级选项)
+
+
+
+
+
+
handleProxyUrlChange(e.target.value)}
+ placeholder="例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080"
+ className={`w-full rounded-lg border bg-background px-3 py-2 text-sm ${
+ proxyError ? "border-red-500" : ""
+ }`}
+ />
+ {proxyError ? (
+
{proxyError}
+ ) : (
+
+ 留空则使用全局代理设置。支持 http://、https://、socks5://
+ 协议
+
)}
- )}
-
- {/* Gemini Project ID */}
- {credential.credential_type === "gemini_oauth" &&
- newCredFilePath && (
-
-
- setNewProjectId(e.target.value)}
- placeholder="留空保持当前项目ID..."
- className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
- />
-
- )}
-
- {/* API Key 编辑 */}
- {isApiKey && (
- <>
-
-
-
- setNewApiKey(e.target.value)}
- placeholder="留空保持当前 Key,或输入新的 API Key..."
- className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm"
- />
- setShowApiKey(!showApiKey)}
- className="rounded-lg border p-2 hover:bg-muted"
- title={showApiKey ? "隐藏" : "显示"}
- >
- {showApiKey ? (
-
- ) : (
-
- )}
-
-
-
- 当前: {credential.display_credential}
-
-
-
-
-
setNewBaseUrl(e.target.value)}
- placeholder={
- credential.credential_type === "openai_key"
- ? "https://api.openai.com"
- : "https://api.anthropic.com"
- }
- className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
- />
-
- 留空使用默认 URL,或输入自定义代理地址(不要包含 /v1 后缀)
-
-
- >
- )}
-
- {/* 不支持的模型 - Checkbox Grid */}
-
-
-
-
-
- 选择此提供商不支持的模型,系统会自动排除这些模型
-
-
-
- {currentProviderModels.map((model) => (
-
- ))}
+
+
+ 代理优先级说明:
+
+
+ - 此凭证代理优先于全局代理
+ - 留空时使用全局代理设置
+ - 全局代理可在「设置 → 通用」中配置
+
-
- {/* 高级选项:代理设置 */}
-
-
-
-
-
- (高级选项)
-
-
-
-
-
-
handleProxyUrlChange(e.target.value)}
- placeholder="例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080"
- className={`w-full rounded-lg border bg-background px-3 py-2 text-sm ${
- proxyError ? "border-red-500" : ""
- }`}
- />
- {proxyError ? (
-
{proxyError}
- ) : (
-
- 留空则使用全局代理设置。支持 http://、https://、socks5://
- 协议
-
- )}
-
-
-
- 代理优先级说明:
-
-
- - 此凭证代理优先于全局代理
- - 留空时使用全局代理设置
- - 全局代理可在「设置 → 通用」中配置
-
-
-
-
-
- {/* 使用统计(只读) */}
-
-
-
-
-
- 使用次数
-
-
- {credential.usage_count}
-
-
-
-
- 错误次数
-
-
- {credential.error_count}
-
-
-
-
- 最后使用
-
-
- {credential.last_used || "从未"}
-
-
-
-
-
- {/* Error */}
- {error && (
-
- {error}
-
- )}
-
- {/* Footer */}
-
-
- 取消
-
-
- {loading ? "保存中..." : "保存更改"}
-
+ {/* 使用统计(只读) */}
+
+
+
+
+
+ 使用次数
+
+ {credential.usage_count}
+
+
+
+ 错误次数
+
+ {credential.error_count}
+
+
+
+ 最后使用
+
+
+ {credential.last_used || "从未"}
+
+
+
+
+
+ {/* Error */}
+ {error && (
+
+ {error}
+
+ )}
-
+
+ {/* Footer */}
+
+
+ 取消
+
+
+ {loading ? "保存中..." : "保存更改"}
+
+
+
);
}
diff --git a/src/components/provider-pool/ProviderPoolPage.tsx b/src/components/provider-pool/ProviderPoolPage.tsx
index 4e984035b..dd491a04c 100644
--- a/src/components/provider-pool/ProviderPoolPage.tsx
+++ b/src/components/provider-pool/ProviderPoolPage.tsx
@@ -30,20 +30,21 @@ export interface ProviderPoolPageRef {
refresh: () => void;
}
-// All provider types (OAuth/API Key 凭证)
-const allProviderTypes: PoolProviderType[] = [
+// OAuth 类型凭证(需要上传凭证文件或登录授权)
+const oauthProviderTypes: PoolProviderType[] = [
"kiro",
"gemini",
"qwen",
"antigravity",
- "openai",
- "claude",
"codex",
"claude_oauth",
"iflow",
];
-// 配置类型 tab(非凭证池)
+// API Key 类型凭证(直接填入 API Key)
+const apiKeyProviderTypes: PoolProviderType[] = ["openai", "claude"];
+
+// 配置类型 tab(非凭证池)- gemini_api 移到 API Key 分类下
type ConfigTabType = "gemini_api" | "vertex" | "amp";
// 所有 tab 类型
@@ -62,7 +63,7 @@ const providerLabels: Record
= {
};
const configTabLabels: Record = {
- gemini_api: "Gemini API Key",
+ gemini_api: "Gemini",
vertex: "Vertex AI",
amp: "Amp CLI",
};
@@ -72,12 +73,16 @@ const isConfigTab = (tab: TabType): tab is ConfigTabType => {
return ["gemini_api", "vertex", "amp"].includes(tab);
};
+// 分类类型
+type CategoryType = "oauth" | "apikey" | "config";
+
export const ProviderPoolPage = forwardRef(
(_props, ref) => {
const [addModalOpen, setAddModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingCredential, setEditingCredential] =
useState(null);
+ const [activeCategory, setActiveCategory] = useState("oauth");
const [activeTab, setActiveTab] = useState("kiro");
const [deletingCredentials, setDeletingCredentials] = useState>(
new Set(),
@@ -136,13 +141,13 @@ export const ProviderPoolPage = forwardRef(
setConfigSaving(false);
};
- // 切换到配置 tab 时加载配置
+ // 切换到配置 tab 或 apikey 分类时加载配置(apikey 分类包含 gemini_api)
useEffect(() => {
- if (isConfigTab(activeTab)) {
+ if (isConfigTab(activeTab) || activeCategory === "apikey") {
loadConfig();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [activeTab]);
+ }, [activeTab, activeCategory]);
useImperativeHandle(ref, () => ({
refresh,
@@ -367,48 +372,166 @@ export const ProviderPoolPage = forwardRef(
)}
- {/* Tabs */}
-
- {/* 凭证池 tabs */}
- {allProviderTypes.map((providerType) => {
- const count = getCredentialCount(providerType);
- return (
-
setActiveTab(providerType)}
- className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px whitespace-nowrap flex items-center gap-2 ${
- activeTab === providerType
- ? "border-primary text-primary"
- : "border-transparent text-muted-foreground hover:text-foreground"
- }`}
- >
-
- {providerLabels[providerType]}
- {count > 0 && (
-
- {count}
+ {/* Category Tabs - 第一行:分类选择 */}
+
+ {
+ setActiveCategory("oauth");
+ setActiveTab(oauthProviderTypes[0]);
+ }}
+ className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
+ activeCategory === "oauth"
+ ? "bg-primary text-primary-foreground"
+ : "bg-muted text-muted-foreground hover:text-foreground hover:bg-muted/80"
+ }`}
+ >
+ OAuth 凭证
+
+ {
+ setActiveCategory("apikey");
+ setActiveTab(apiKeyProviderTypes[0]);
+ }}
+ className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
+ activeCategory === "apikey"
+ ? "bg-primary text-primary-foreground"
+ : "bg-muted text-muted-foreground hover:text-foreground hover:bg-muted/80"
+ }`}
+ >
+ API Key
+
+ {
+ setActiveCategory("config");
+ setActiveTab("vertex");
+ }}
+ className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
+ activeCategory === "config"
+ ? "bg-primary text-primary-foreground"
+ : "bg-muted text-muted-foreground hover:text-foreground hover:bg-muted/80"
+ }`}
+ >
+ 其他配置
+
+
+
+ {/* Provider Selection - 第二行:图标网格选择 */}
+ {activeCategory === "oauth" && (
+
+ {oauthProviderTypes.map((providerType) => {
+ const count = getCredentialCount(providerType);
+ const isActive = activeTab === providerType;
+ return (
+
setActiveTab(providerType)}
+ title={providerLabels[providerType]}
+ className={`group relative flex items-center gap-2 px-3 py-2 rounded-lg border transition-all ${
+ isActive
+ ? "border-primary bg-primary/10 text-primary shadow-sm"
+ : "border-border bg-card hover:border-primary/50 hover:bg-muted text-muted-foreground hover:text-foreground"
+ }`}
+ >
+
+
+ {providerLabels[providerType].split(" ")[0]}
- )}
-
- );
- })}
- {/* 分隔符 */}
-
- {/* 配置 tabs */}
- {(Object.keys(configTabLabels) as ConfigTabType[]).map((tabId) => (
+ {count > 0 && (
+
+ {count}
+
+ )}
+
+ );
+ })}
+
+ )}
+ {activeCategory === "apikey" && (
+
+ {apiKeyProviderTypes.map((providerType) => {
+ const count = getCredentialCount(providerType);
+ const isActive = activeTab === providerType;
+ return (
+
setActiveTab(providerType)}
+ title={providerLabels[providerType]}
+ className={`group relative flex items-center gap-2 px-3 py-2 rounded-lg border transition-all ${
+ isActive
+ ? "border-primary bg-primary/10 text-primary shadow-sm"
+ : "border-border bg-card hover:border-primary/50 hover:bg-muted text-muted-foreground hover:text-foreground"
+ }`}
+ >
+
+
+ {providerLabels[providerType].split(" ")[0]}
+
+ {count > 0 && (
+
+ {count}
+
+ )}
+
+ );
+ })}
+ {/* Gemini API Key - 配置文件存储但归类到 API Key */}
setActiveTab(tabId)}
- className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px whitespace-nowrap ${
- activeTab === tabId
- ? "border-primary text-primary"
- : "border-transparent text-muted-foreground hover:text-foreground"
+ onClick={() => setActiveTab("gemini_api")}
+ title="Gemini API Key"
+ className={`group relative flex items-center gap-2 px-3 py-2 rounded-lg border transition-all ${
+ activeTab === "gemini_api"
+ ? "border-primary bg-primary/10 text-primary shadow-sm"
+ : "border-border bg-card hover:border-primary/50 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
>
- {configTabLabels[tabId]}
+
+ Gemini
+ {(config?.credential_pool?.gemini_api_keys?.length ?? 0) > 0 && (
+
+ {config?.credential_pool?.gemini_api_keys?.length}
+
+ )}
- ))}
-
+
+ )}
+ {activeCategory === "config" && (
+
+ {(["vertex", "amp"] as const).map((tabId) => {
+ const isActive = activeTab === tabId;
+ return (
+ setActiveTab(tabId)}
+ className={`px-3 py-2 rounded-lg border text-sm font-medium transition-all ${
+ isActive
+ ? "border-primary bg-primary/10 text-primary shadow-sm"
+ : "border-border bg-card hover:border-primary/50 hover:bg-muted text-muted-foreground hover:text-foreground"
+ }`}
+ >
+ {configTabLabels[tabId]}
+
+ );
+ })}
+
+ )}
{/* 配置 Tab 内容 */}
{isConfigTab(activeTab) ? (
@@ -432,13 +555,16 @@ export const ProviderPoolPage = forwardRef
(
})
}
/>
-
- {configSaving ? "保存中..." : "保存配置"}
-
+ {(config.credential_pool?.gemini_api_keys?.length ?? 0) >
+ 0 && (
+
+ {configSaving ? "保存中..." : "保存配置"}
+
+ )}
>
)}
@@ -456,13 +582,16 @@ export const ProviderPoolPage = forwardRef(
})
}
/>
-
- {configSaving ? "保存中..." : "保存配置"}
-
+ {(config.credential_pool?.vertex_api_keys?.length ?? 0) >
+ 0 && (
+
+ {configSaving ? "保存中..." : "保存配置"}
+
+ )}
>
)}
@@ -537,15 +666,15 @@ export const ProviderPoolPage = forwardRef(
重置状态
+
+
+ 添加凭证
+
>
)}
-
-
- 添加凭证
-
diff --git a/src/components/routing/RoutingPage.tsx b/src/components/routing/RoutingPage.tsx
index 6f947dbf6..8920d6328 100644
--- a/src/components/routing/RoutingPage.tsx
+++ b/src/components/routing/RoutingPage.tsx
@@ -1,5 +1,6 @@
import { useState, useEffect, forwardRef, useImperativeHandle } from "react";
import { RefreshCw, Route, Sparkles, Check, Trash2 } from "lucide-react";
+import { Modal } from "@/components/Modal";
import { ModelMapping } from "./ModelMapping";
import { RoutingRules } from "./RoutingRules";
import { ExclusionList } from "./ExclusionList";
@@ -293,84 +294,81 @@ export const RoutingPage = forwardRef(
)}
{/* Presets Modal */}
- {showPresets && (
-
-
-
-
-
- 推荐配置
-
-
setShowPresets(false)}
- className="text-muted-foreground hover:text-foreground"
+ setShowPresets(false)}
+ maxWidth="max-w-2xl"
+ className="max-h-[80vh] overflow-y-auto"
+ >
+
+
+
+
+ 推荐配置
+
+
+
+ 选择一个预设配置快速设置路由规则和模型别名
+
+
+ {presets.map((preset) => (
+
- ✕
-
-
-
- 选择一个预设配置快速设置路由规则和模型别名
-
-
- {presets.map((preset) => (
-
-
-
-
{preset.name}
-
- {preset.description}
-
-
- {preset.aliases.length} 个别名
- {preset.rules.length} 条规则
- {preset.endpoint_providers && (
-
- {
- Object.values(preset.endpoint_providers).filter(
- (v) => v,
- ).length
- }{" "}
- 个客户端路由
-
- )}
-
-
-
-
handleApplyPreset(preset.id, true)}
- disabled={applyingPreset !== null}
- className="flex items-center gap-1 rounded px-3 py-1.5 text-sm border hover:bg-muted disabled:opacity-50"
- >
- {applyingPreset === preset.id ? (
-
- ) : (
-
- )}
- 合并
-
-
handleApplyPreset(preset.id, false)}
- disabled={applyingPreset !== null}
- className="flex items-center gap-1 rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
- >
- {applyingPreset === preset.id ? (
-
- ) : (
-
- )}
- 应用
-
+
+
+
{preset.name}
+
+ {preset.description}
+
+
+ {preset.aliases.length} 个别名
+ {preset.rules.length} 条规则
+ {preset.endpoint_providers && (
+
+ {
+ Object.values(preset.endpoint_providers).filter(
+ (v) => v,
+ ).length
+ }{" "}
+ 个客户端路由
+
+ )}
+
+ handleApplyPreset(preset.id, true)}
+ disabled={applyingPreset !== null}
+ className="flex items-center gap-1 rounded px-3 py-1.5 text-sm border hover:bg-muted disabled:opacity-50"
+ >
+ {applyingPreset === preset.id ? (
+
+ ) : (
+
+ )}
+ 合并
+
+ handleApplyPreset(preset.id, false)}
+ disabled={applyingPreset !== null}
+ className="flex items-center gap-1 rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
+ >
+ {applyingPreset === preset.id ? (
+
+ ) : (
+
+ )}
+ 应用
+
+
- ))}
-
+
+ ))}
- )}
+
diff --git a/src/icons/providers/alibaba.svg b/src/icons/providers/alibaba.svg
new file mode 100644
index 000000000..b936dd4e7
--- /dev/null
+++ b/src/icons/providers/alibaba.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/amp.svg b/src/icons/providers/amp.svg
new file mode 100644
index 000000000..369899c8f
--- /dev/null
+++ b/src/icons/providers/amp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/antigravity.svg b/src/icons/providers/antigravity.svg
new file mode 100644
index 000000000..bb78b75c5
--- /dev/null
+++ b/src/icons/providers/antigravity.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/aws.svg b/src/icons/providers/aws.svg
new file mode 100644
index 000000000..6eb331461
--- /dev/null
+++ b/src/icons/providers/aws.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/azure.svg b/src/icons/providers/azure.svg
new file mode 100644
index 000000000..e9caa4367
--- /dev/null
+++ b/src/icons/providers/azure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/claude.svg b/src/icons/providers/claude.svg
new file mode 100644
index 000000000..4b9545ca2
--- /dev/null
+++ b/src/icons/providers/claude.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/copilot.svg b/src/icons/providers/copilot.svg
new file mode 100644
index 000000000..70031f289
--- /dev/null
+++ b/src/icons/providers/copilot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/deepseek.svg b/src/icons/providers/deepseek.svg
new file mode 100644
index 000000000..67fce5dd1
--- /dev/null
+++ b/src/icons/providers/deepseek.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/doubao.svg b/src/icons/providers/doubao.svg
new file mode 100644
index 000000000..4109e2a22
--- /dev/null
+++ b/src/icons/providers/doubao.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/google.svg b/src/icons/providers/google.svg
new file mode 100644
index 000000000..b7c00a0b8
--- /dev/null
+++ b/src/icons/providers/google.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/index.tsx b/src/icons/providers/index.tsx
index e2d7bd51a..6fc760466 100644
--- a/src/icons/providers/index.tsx
+++ b/src/icons/providers/index.tsx
@@ -1,6 +1,48 @@
-import React, { useMemo } from "react";
+import React, { useMemo, ComponentType, SVGProps } from "react";
import { cn } from "@/lib/utils";
-import { providerIcons, providerTypeToIcon } from "./utils";
+import { providerTypeToIcon } from "./utils";
+
+// 使用 SVGR 导入 SVG 组件
+import AwsIcon from "./aws.svg?react";
+import GeminiIcon from "./gemini.svg?react";
+import AnthropicIcon from "./anthropic.svg?react";
+import ClaudeIcon from "./claude.svg?react";
+import QwenIcon from "./qwen.svg?react";
+import GoogleIcon from "./google.svg?react";
+import OpenaiIcon from "./openai.svg?react";
+import AlibabaIcon from "./alibaba.svg?react";
+import CopilotIcon from "./copilot.svg?react";
+import AmpIcon from "./amp.svg?react";
+import KiroIcon from "./kiro.svg?react";
+import DeepseekIcon from "./deepseek.svg?react";
+import ZhipuIcon from "./zhipu.svg?react";
+import KimiIcon from "./kimi.svg?react";
+import MinimaxIcon from "./minimax.svg?react";
+import DoubaoIcon from "./doubao.svg?react";
+import AzureIcon from "./azure.svg?react";
+import AntigravityIcon from "./antigravity.svg?react";
+
+// 图标组件映射
+const iconComponents: Record>> = {
+ aws: AwsIcon,
+ gemini: GeminiIcon,
+ anthropic: AnthropicIcon,
+ claude: ClaudeIcon,
+ qwen: QwenIcon,
+ google: GoogleIcon,
+ openai: OpenaiIcon,
+ alibaba: AlibabaIcon,
+ copilot: CopilotIcon,
+ amp: AmpIcon,
+ kiro: KiroIcon,
+ deepseek: DeepseekIcon,
+ zhipu: ZhipuIcon,
+ kimi: KimiIcon,
+ minimax: MinimaxIcon,
+ doubao: DoubaoIcon,
+ azure: AzureIcon,
+ antigravity: AntigravityIcon,
+};
interface ProviderIconProps {
providerType: string;
@@ -16,7 +58,7 @@ export const ProviderIcon: React.FC = ({
showFallback = true,
}) => {
const iconName = providerTypeToIcon[providerType] || providerType;
- const iconSvg = providerIcons[iconName];
+ const IconComponent = iconComponents[iconName];
const sizeStyle = useMemo(() => {
const sizeValue = typeof size === "number" ? `${size}px` : size;
@@ -28,7 +70,7 @@ export const ProviderIcon: React.FC = ({
};
}, [size]);
- if (iconSvg) {
+ if (IconComponent) {
return (
= ({
className,
)}
style={sizeStyle}
- dangerouslySetInnerHTML={{ __html: iconSvg }}
- />
+ >
+
+
);
}
diff --git a/src/icons/providers/kimi.svg b/src/icons/providers/kimi.svg
new file mode 100644
index 000000000..dfc0f4507
--- /dev/null
+++ b/src/icons/providers/kimi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/kiro.svg b/src/icons/providers/kiro.svg
new file mode 100644
index 000000000..1b16c09b4
--- /dev/null
+++ b/src/icons/providers/kiro.svg
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/minimax.svg b/src/icons/providers/minimax.svg
new file mode 100644
index 000000000..5490590e8
--- /dev/null
+++ b/src/icons/providers/minimax.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/qwen.svg b/src/icons/providers/qwen.svg
new file mode 100644
index 000000000..e08c13e02
--- /dev/null
+++ b/src/icons/providers/qwen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/providers/utils.ts b/src/icons/providers/utils.ts
index 741f724e7..7a3562d05 100644
--- a/src/icons/providers/utils.ts
+++ b/src/icons/providers/utils.ts
@@ -1,11 +1,33 @@
// Provider 图标辅助函数
+// 可用的图标名称列表
+export const availableIcons = [
+ "aws",
+ "gemini",
+ "anthropic",
+ "claude",
+ "qwen",
+ "google",
+ "openai",
+ "alibaba",
+ "copilot",
+ "amp",
+ "kiro",
+ "deepseek",
+ "zhipu",
+ "kimi",
+ "minimax",
+ "doubao",
+ "azure",
+ "antigravity",
+] as const;
+
// Provider 类型到图标名称的映射
-const providerTypeToIcon: Record = {
+export const providerTypeToIcon: Record = {
kiro: "kiro",
gemini: "gemini",
qwen: "qwen",
- antigravity: "gemini",
+ antigravity: "antigravity",
openai: "openai",
claude: "claude",
anthropic: "anthropic",
@@ -22,40 +44,16 @@ const providerTypeToIcon: Record = {
minimax: "minimax",
doubao: "doubao",
azure: "azure",
+ aws: "aws",
};
-// Provider 图标 SVG 内联定义
-export const providerIcons: Record = {
- aws: ``,
- gemini: ``,
- anthropic: ``,
- claude: ``,
- qwen: ``,
- google: ``,
- openai: ``,
- alibaba: ``,
- copilot: ``,
- amp: ``,
- kiro: ``,
- deepseek: ``,
- zhipu: ``,
- kimi: ``,
- minimax: ``,
- doubao: ``,
- azure: ``,
+// 获取规范化的图标名称
+export const getIconName = (providerType: string): string => {
+ return providerTypeToIcon[providerType] || providerType;
};
-// 导出图标名称检查函数
+// 检查是否有对应的图标
export const hasProviderIcon = (providerType: string): boolean => {
- const iconName = providerTypeToIcon[providerType] || providerType;
- return iconName in providerIcons;
+ const iconName = getIconName(providerType);
+ return (availableIcons as readonly string[]).includes(iconName);
};
-
-// 导出获取图标 SVG 函数
-export const getProviderIcon = (providerType: string): string | undefined => {
- const iconName = providerTypeToIcon[providerType] || providerType;
- return providerIcons[iconName];
-};
-
-// 导出 Provider 类型映射
-export { providerTypeToIcon };
diff --git a/src/icons/providers/zhipu.svg b/src/icons/providers/zhipu.svg
new file mode 100644
index 000000000..36564d733
--- /dev/null
+++ b/src/icons/providers/zhipu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index b1280325d..e578d21f7 100644
--- a/src/index.css
+++ b/src/index.css
@@ -51,4 +51,29 @@
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
+
+ /* Custom scrollbar styles */
+ ::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: hsl(var(--muted-foreground) / 0.3);
+ border-radius: 3px;
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: hsl(var(--muted-foreground) / 0.5);
+ }
+
+ /* Firefox scrollbar */
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
+ }
}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index dae00cc80..65f988dbd 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1,4 +1,5 @@
///
+///
// 全局类型声明
declare global {
diff --git a/vite.config.ts b/vite.config.ts
index 1242474e3..b9b54df1e 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,10 +1,11 @@
///
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
+import svgr from "vite-plugin-svgr";
import path from "path";
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), svgr()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),