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 && ( -
    - - {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 && ( +
    + + {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 请求 - -
- {config.new_flow.enabled && ( -
-
- 桌面通知 - -
-
- 声音提示 - -
-
- )} + /> +
- - {/* 错误通知 */} -
-
- 请求失败 - -
- {config.error_flow.enabled && ( -
-
- 桌面通知 - -
-
- 声音提示 - -
+ /> +
- )} -
- - {/* 阈值警告通知 */} -
-
- 阈值警告 - -
- {config.latency_warning.enabled && ( -
-
- 桌面通知 - +
+
+ )} +
+ + {/* 阈值警告通知 */} +
+
+ 阈值警告 + +
+ {config.latency_warning.enabled && ( +
+
+ 桌面通知 + -
-
- 声音提示 - -
+ /> +
- )} -
- - {/* Token 警告通知 */} -
-
- Token 警告 - -
- {config.token_warning.enabled && ( -
-
- 桌面通知 - -
-
- 声音提示 - -
+ /> +
- )} -
+
+ )}
- {/* 测试按钮 */} -
- + {/* Token 警告通知 */} +
+
+ Token 警告 + +
+ {config.token_warning.enabled && ( +
+
+ 桌面通知 + +
+
+ 声音提示 + +
+
+ )}
+ + {/* 测试按钮 */} +
+ +
-
+ ); } 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 && ( +
+ +
+ + + +
+ {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" /> -
- {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" - /> - -
-

- 当前: {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 */} -
- - + {/* 使用统计(只读) */} +
+ +
+
+ + 使用次数 + + {credential.usage_count} +
+
+ + 错误次数 + + {credential.error_count} +
+
+ + 最后使用 + + + {credential.last_used || "从未"} + +
+
+
+ + {/* Error */} + {error && ( +
+ {error} +
+ )}
-
+ + {/* Footer */} +
+ + +
+ ); } 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 ( - + + +
+ + {/* Provider Selection - 第二行:图标网格选择 */} + {activeCategory === "oauth" && ( +
+ {oauthProviderTypes.map((providerType) => { + const count = getCredentialCount(providerType); + const isActive = activeTab === providerType; + return ( + - ); - })} - {/* 分隔符 */} -
- {/* 配置 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 ( + + ); + })} + {/* Gemini API Key - 配置文件存储但归类到 API Key */} - ))} -
+
+ )} + {activeCategory === "config" && ( +
+ {(["vertex", "amp"] as const).map((tabId) => { + const isActive = activeTab === tabId; + return ( + + ); + })} +
+ )} {/* 配置 Tab 内容 */} {isConfigTab(activeTab) ? ( @@ -432,13 +555,16 @@ export const ProviderPoolPage = forwardRef( }) } /> - + {(config.credential_pool?.gemini_api_keys?.length ?? 0) > + 0 && ( + + )} )} @@ -456,13 +582,16 @@ export const ProviderPoolPage = forwardRef( }) } /> - + {(config.credential_pool?.vertex_api_keys?.length ?? 0) > + 0 && ( + + )} )} @@ -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 && ( -
-
-
-

- - 推荐配置 -

- -
-

- 选择一个预设配置快速设置路由规则和模型别名 -

-
- {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 - }{" "} - 个客户端路由 - - )} -
-
-
- - +
+
+

{preset.name}

+

+ {preset.description} +

+
+ {preset.aliases.length} 个别名 + {preset.rules.length} 条规则 + {preset.endpoint_providers && ( + + { + Object.values(preset.endpoint_providers).filter( + (v) => v, + ).length + }{" "} + 个客户端路由 + + )}
+
+ + +
- ))} -
+
+ ))}
- )} +
    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 @@ + +Google_Antigravity-logo - brandlogos.net \ 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 @@ +Copilot \ 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"),