diff --git a/frontend/packages/ui/src/components/Select/index.tsx b/frontend/packages/ui/src/components/Select/index.tsx index 6443f93c5b1..f75268a28e9 100644 --- a/frontend/packages/ui/src/components/Select/index.tsx +++ b/frontend/packages/ui/src/components/Select/index.tsx @@ -12,7 +12,7 @@ import { MenuButton, Flex } from '@chakra-ui/react'; -import type { ButtonProps } from '@chakra-ui/react'; +import type { BoxProps, ButtonProps } from '@chakra-ui/react'; import { ChevronDownIcon } from '@chakra-ui/icons'; interface Props extends ButtonProps { @@ -26,6 +26,7 @@ interface Props extends ButtonProps { }[]; onchange?: (val: string) => void; isInvalid?: boolean; + boxStyle?: BoxProps; } const MySelect = ( @@ -37,6 +38,7 @@ const MySelect = ( list, onchange, isInvalid, + boxStyle, ...props }: Props, selectRef: any @@ -62,6 +64,7 @@ const MySelect = ( onClick={() => { isOpen ? onClose() : onOpen(); }} + {...boxStyle} > =16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.6 espree: 9.6.1 globals: 13.23.0 ignore: 5.3.0 @@ -7240,7 +7334,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4 + debug: 4.3.6 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7494,7 +7588,7 @@ packages: jest-haste-map: 29.7.0 jest-regex-util: 29.6.3 jest-util: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.7 pirates: 4.0.6 slash: 3.0.0 write-file-atomic: 4.0.2 @@ -7860,6 +7954,12 @@ packages: glob: 10.3.10 dev: true + /@next/eslint-plugin-next@15.0.1: + resolution: {integrity: sha512-bKWsMaGPbiFAaGqrDJvbE8b4Z0uKicGVcgOI77YM2ui3UfjHMr4emFPrZTLeZVchi7fT1mooG2LxREfUUClIKw==} + dependencies: + fast-glob: 3.3.1 + dev: true + /@next/font@13.1.6: resolution: {integrity: sha512-AITjmeb1RgX1HKMCiA39ztx2mxeAyxl4ljv2UoSBUGAbFFMg8MO7YAvjHCgFhD39hL7YTbFjol04e/BPBH5RzQ==} dev: false @@ -9129,6 +9229,14 @@ packages: rollup: 2.79.1 dev: true + /@rtsao/scc@1.1.0: + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + dev: true + + /@rushstack/eslint-patch@1.10.4: + resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + dev: true + /@rushstack/eslint-patch@1.6.0: resolution: {integrity: sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==} @@ -10446,6 +10554,33 @@ packages: '@types/yargs-parser': 21.0.3 dev: true + /@typescript-eslint/eslint-plugin@8.12.2(@typescript-eslint/parser@6.13.1)(eslint@8.57.0)(typescript@5.2.2): + resolution: {integrity: sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.13.1(eslint@8.57.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 8.12.2 + '@typescript-eslint/type-utils': 8.12.2(eslint@8.57.0)(typescript@5.2.2) + '@typescript-eslint/utils': 8.12.2(eslint@8.57.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 8.12.2 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@5.62.0(eslint@8.33.0)(typescript@5.2.2): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10582,6 +10717,33 @@ packages: '@typescript-eslint/visitor-keys': 6.13.1 dev: true + /@typescript-eslint/scope-manager@8.12.2: + resolution: {integrity: sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/visitor-keys': 8.12.2 + dev: true + + /@typescript-eslint/type-utils@8.12.2(eslint@8.57.0)(typescript@5.2.2): + resolution: {integrity: sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.2.2) + '@typescript-eslint/utils': 8.12.2(eslint@8.57.0)(typescript@5.2.2) + debug: 4.3.6 + ts-api-utils: 1.4.0(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - eslint + - supports-color + dev: true + /@typescript-eslint/types@5.62.0: resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10591,6 +10753,11 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@8.12.2: + resolution: {integrity: sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.2.2): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10602,10 +10769,10 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.4 + debug: 4.3.6 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -10622,16 +10789,54 @@ packages: dependencies: '@typescript-eslint/types': 6.13.1 '@typescript-eslint/visitor-keys': 6.13.1 - debug: 4.3.4 + debug: 4.3.6 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true + /@typescript-eslint/typescript-estree@8.12.2(typescript@5.2.2): + resolution: {integrity: sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/visitor-keys': 8.12.2 + debug: 4.3.6 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@8.12.2(eslint@8.57.0)(typescript@5.2.2): + resolution: {integrity: sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 8.12.2 + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.2.2) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@5.62.0: resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10647,6 +10852,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@8.12.2: + resolution: {integrity: sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.12.2 + eslint-visitor-keys: 3.4.3 + dev: true + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true @@ -10805,6 +11018,12 @@ packages: deprecated: Use your platform's native atob() and btoa() methods instead dev: true + /accept-language@3.0.20: + resolution: {integrity: sha512-xklPzRma4aoDEPk0ZfMjeuxB2FP4JBYlAR25OFUqCoOYDjYo6wGwAs49SnTN/MoB5VpnNX9tENfZ+vEIFmHQMQ==} + dependencies: + bcp47: 1.1.2 + dev: false + /accepts@1.0.7: resolution: {integrity: sha512-iq8ew2zitUlNcUca0wye3fYwQ6sSPItDo38oC0R+XA5KTzeXRN+GF7NjOXs3dVItj4J+gQVdpq4/qbnMb1hMHw==} engines: {node: '>= 0.8.0'} @@ -10858,7 +11077,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4 + debug: 4.3.6 transitivePeerDependencies: - supports-color @@ -11074,6 +11293,11 @@ packages: dependencies: dequal: 2.0.3 + /aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + dev: true + /arr-union@3.1.0: resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} engines: {node: '>=0.10.0'} @@ -11085,6 +11309,14 @@ packages: call-bind: 1.0.5 is-array-buffer: 3.0.2 + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + dev: true + /array-includes@3.1.7: resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} engines: {node: '>= 0.4'} @@ -11095,6 +11327,18 @@ packages: get-intrinsic: 1.2.2 is-string: 1.0.7 + /array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + dev: true + /array-tree-filter@2.1.0: resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==} dev: false @@ -11115,6 +11359,18 @@ packages: engines: {node: '>=0.10.0'} dev: false + /array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + dev: true + /array.prototype.findlastindex@1.2.3: resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} engines: {node: '>= 0.4'} @@ -11125,6 +11381,18 @@ packages: es-shim-unscopables: 1.0.2 get-intrinsic: 1.2.2 + /array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + dev: true + /array.prototype.flat@1.3.2: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} @@ -11152,6 +11420,17 @@ packages: es-shim-unscopables: 1.0.2 get-intrinsic: 1.2.2 + /array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-shim-unscopables: 1.0.2 + dev: true + /arraybuffer.prototype.slice@1.0.2: resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} engines: {node: '>= 0.4'} @@ -11164,6 +11443,20 @@ packages: is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + dev: true + /arraybuffer.slice@0.0.6: resolution: {integrity: sha512-6ZjfQaBSy6CuIH0+B0NrxMfDE5VIOCP/5gOqSpEIsaAZx9/giszzrXg6PZ7G51U/n88UmlAgYLNQ9wAnII7PJA==} dev: false @@ -11247,6 +11540,13 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + /aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} @@ -11257,6 +11557,11 @@ packages: /aws4@1.12.0: resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} + /axe-core@4.10.2: + resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} + engines: {node: '>=4'} + dev: true + /axe-core@4.7.0: resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} engines: {node: '>=4'} @@ -11301,11 +11606,26 @@ packages: - debug dev: false + /axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} dependencies: dequal: 2.0.3 + /axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + dev: true + /b4a@1.6.4: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} dev: false @@ -11476,6 +11796,11 @@ packages: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} dev: false + /bcp47@1.1.2: + resolution: {integrity: sha512-JnkkL4GUpOvvanH9AZPX38CxhiLsXMBicBY2IAtqiVN8YulGDQybUydWA4W6yAMtw6iShtw+8HEF6cfrTHU+UQ==} + engines: {node: '>=0.10'} + dev: false + /bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} dependencies: @@ -11578,7 +11903,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.1.1 - dev: true /browser-or-node@2.1.1: resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} @@ -11675,6 +11999,17 @@ packages: get-intrinsic: 1.2.2 set-function-length: 1.1.1 + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: true + /callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} dev: false @@ -12242,6 +12577,14 @@ packages: hasBin: true dev: false + /cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -12487,6 +12830,33 @@ packages: whatwg-url: 11.0.0 dev: true + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + /date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -12572,7 +12942,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} @@ -12674,6 +13043,15 @@ packages: gopd: 1.0.1 has-property-descriptors: 1.0.1 + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: true + /define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -13104,10 +13482,74 @@ packages: unbox-primitive: 1.0.2 which-typed-array: 1.1.13 - /es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.5 get-intrinsic: 1.2.2 has-symbols: 1.0.3 is-arguments: 1.1.1 @@ -13136,10 +13578,37 @@ packages: iterator.prototype: 1.1.2 safe-array-concat: 1.0.1 + /es-iterator-helpers@1.1.0: + resolution: {integrity: sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + globalthis: 1.0.4 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + iterator.prototype: 1.1.3 + safe-array-concat: 1.1.2 + dev: true + /es-module-lexer@1.4.1: resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} dev: false + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true + /es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} engines: {node: '>= 0.4'} @@ -13148,6 +13617,15 @@ packages: has-tostringtag: 1.0.0 hasown: 2.0.0 + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + /es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} dependencies: @@ -13403,6 +13881,41 @@ packages: - supports-color dev: true + /eslint-config-next@15.0.1(eslint@8.57.0)(typescript@5.2.2): + resolution: {integrity: sha512-3cYCrgbH6GS/ufApza7XCKz92vtq4dAdYhx++rMFNlH2cAV+/GsAKkrr4+bohYOACmzG2nAOR+uWprKC1Uld6A==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@next/eslint-plugin-next': 15.0.1 + '@rushstack/eslint-patch': 1.10.4 + '@typescript-eslint/eslint-plugin': 8.12.2(@typescript-eslint/parser@6.13.1)(eslint@8.57.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.13.1(eslint@8.57.0)(typescript@5.2.2) + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0) + eslint-plugin-react: 7.37.2(eslint@8.57.0) + eslint-plugin-react-hooks: 5.0.0(eslint@8.57.0) + typescript: 5.2.2 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-config-prettier@9.1.0(eslint@8.57.0): + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.57.0 + dev: true + /eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} dependencies: @@ -13549,6 +14062,59 @@ packages: - supports-color dev: true + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0): + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + dependencies: + debug: 4.3.4 + enhanced-resolve: 5.15.0 + eslint: 8.57.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + fast-glob: 3.3.2 + get-tsconfig: 4.7.2 + is-core-module: 2.13.1 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-module-utils@2.12.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.13.1(eslint@8.57.0)(typescript@5.2.2) + debug: 3.2.7 + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0) + transitivePeerDependencies: + - supports-color + dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.33.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -13723,7 +14289,7 @@ packages: debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color dev: true @@ -13902,6 +14468,67 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@rtsao/scc': 1.1.0 + '@typescript-eslint/parser': 6.13.1(eslint@8.57.0)(typescript@5.2.2) + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + hasown: 2.0.2 + is-core-module: 2.15.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0): + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.2 + ast-types-flow: 0.0.8 + axe-core: 4.10.2 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.57.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.0.3 + string.prototype.includes: 2.0.1 + dev: true + /eslint-plugin-jsx-a11y@6.8.0(eslint@8.33.0): resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} engines: {node: '>=4.0'} @@ -14070,6 +14697,15 @@ packages: eslint: 8.57.0 dev: true + /eslint-plugin-react-hooks@5.0.0(eslint@8.57.0): + resolution: {integrity: sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + dependencies: + eslint: 8.57.0 + dev: true + /eslint-plugin-react@7.33.2(eslint@8.33.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} @@ -14194,6 +14830,41 @@ packages: string.prototype.matchall: 4.0.10 dev: true + /eslint-plugin-react@7.37.2(eslint@8.57.0): + resolution: {integrity: sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.1.0 + eslint: 8.57.0 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.values: 1.2.0 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + string.prototype.repeat: 1.0.0 + dev: true + + /eslint-plugin-simple-import-sort@12.1.1(eslint@8.57.0): + resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} + peerDependencies: + eslint: '>=5.0.0' + dependencies: + eslint: 8.57.0 + dev: true + /eslint-plugin-xss@0.1.12: resolution: {integrity: sha512-L5oYaD//ZE7fKNtWUfVgYTRW19jrZlvaHe2swyFLxXQ5pwVQLivi5m92rtXd/ww8yqg4Drasqyi0hlBmhf9YQg==} engines: {node: '>=0.10.0'} @@ -14431,6 +15102,7 @@ packages: /eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) @@ -14444,7 +15116,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.6 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -14681,6 +15353,17 @@ packages: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} dev: false + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + dev: true + /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -14777,7 +15460,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /filter-obj@1.1.0: resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} @@ -15099,6 +15781,17 @@ packages: has-symbols: 1.0.3 hasown: 2.0.0 + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: true + /get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -15129,6 +15822,15 @@ packages: call-bind: 1.0.5 get-intrinsic: 1.2.2 + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + dev: true + /get-tsconfig@4.7.2: resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} dependencies: @@ -15185,7 +15887,7 @@ packages: foreground-child: 3.1.1 jackspeak: 2.3.6 minimatch: 9.0.3 - minipass: 5.0.0 + minipass: 7.1.2 path-scurry: 1.10.1 /glob@7.1.7: @@ -15245,6 +15947,14 @@ packages: dependencies: define-properties: 1.2.1 + /globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + dev: true + /globby@10.0.1: resolution: {integrity: sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==} engines: {node: '>=8'} @@ -15368,10 +16078,21 @@ packages: dependencies: get-intrinsic: 1.2.2 + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: true + /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: true + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -15382,6 +16103,13 @@ packages: dependencies: has-symbols: 1.0.3 + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /has@1.0.4: resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} engines: {node: '>= 0.4.0'} @@ -15393,6 +16121,13 @@ packages: dependencies: function-bind: 1.1.2 + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + /hast-util-from-parse5@7.1.2: resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} dependencies: @@ -15562,7 +16297,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.6 transitivePeerDependencies: - supports-color dev: true @@ -15588,7 +16323,7 @@ packages: resolution: {integrity: sha512-l5rcAoKP8A9XOIlcIA87Wt9A7AX2fgOslHOF4WB5Q24y/1+aeH8b7c7NKfm+Bcf+h0u4FHNtLCriC4mAFmCYgg==} dependencies: '@types/node': 20.10.0 - debug: 4.3.4 + debug: 4.3.6 transitivePeerDependencies: - supports-color dev: false @@ -15612,10 +16347,30 @@ packages: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} dev: false + /i18next-browser-languagedetector@8.0.0: + resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==} + dependencies: + '@babel/runtime': 7.24.0 + dev: false + /i18next-fs-backend@2.3.1: resolution: {integrity: sha512-tvfXskmG/9o+TJ5Fxu54sSO5OkY6d+uMn+K6JiUGLJrwxAVfer+8V3nU8jq3ts9Pe5lXJv4b1N7foIjJ8Iy2Gg==} dev: false + /i18next-http-backend@2.6.2: + resolution: {integrity: sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==} + dependencies: + cross-fetch: 4.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /i18next-resources-to-backend@1.2.1: + resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} + dependencies: + '@babel/runtime': 7.24.0 + dev: false + /i18next@23.12.1: resolution: {integrity: sha512-l4y291ZGRgUhKuqVSiqyuU2DDzxKStlIWSaoNBR4grYmh0X+pRYbFpTMs3CnJ5ECKbOI8sQcJ3PbTUfLgPRaMA==} dependencies: @@ -15646,6 +16401,11 @@ packages: resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} engines: {node: '>= 4'} + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + dev: true + /immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} dev: false @@ -15734,6 +16494,15 @@ packages: hasown: 2.0.0 side-channel: 1.0.4 + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.4 + dev: true + /interpret@1.4.0: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} @@ -15804,6 +16573,14 @@ packages: get-intrinsic: 1.2.2 is-typed-array: 1.1.12 + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -15860,6 +16637,20 @@ packages: dependencies: hasown: 2.0.0 + /is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + dev: true + + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: true + /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} @@ -15938,6 +16729,11 @@ packages: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: true + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -16027,6 +16823,13 @@ packages: dependencies: call-bind: 1.0.5 + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -16054,6 +16857,13 @@ packages: dependencies: which-typed-array: 1.1.13 + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: true + /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -16134,7 +16944,7 @@ packages: '@babel/parser': 7.23.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: true @@ -16152,7 +16962,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4 + debug: 4.3.6 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -16176,6 +16986,17 @@ packages: reflect.getprototypeof: 1.0.4 set-function-name: 2.0.1 + /iterator.prototype@1.1.3: + resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + reflect.getprototypeof: 1.0.4 + set-function-name: 2.0.1 + dev: true + /jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -16319,7 +17140,7 @@ packages: jest-runner: 29.7.0 jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.7 parse-json: 5.2.0 pretty-format: 29.7.0 slash: 3.0.0 @@ -16359,7 +17180,7 @@ packages: jest-runner: 29.7.0 jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.7 parse-json: 5.2.0 pretty-format: 29.7.0 slash: 3.0.0 @@ -16450,7 +17271,7 @@ packages: jest-regex-util: 29.6.3 jest-util: 29.7.0 jest-worker: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.7 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 @@ -16622,7 +17443,7 @@ packages: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: true @@ -17090,11 +17911,6 @@ packages: engines: {node: '>=10'} dev: true - /lilconfig@3.1.1: - resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} - engines: {node: '>=14'} - dev: true - /lilconfig@3.1.2: resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} @@ -17313,7 +18129,7 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: - semver: 7.5.4 + semver: 7.6.3 dev: true /makeerror@1.0.12: @@ -17807,7 +18623,7 @@ packages: resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} dependencies: '@types/debug': 4.1.12 - debug: 4.3.4 + debug: 4.3.6 decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -17831,7 +18647,7 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 /micromatch@4.0.7: @@ -17902,6 +18718,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -18686,7 +19509,7 @@ packages: resolution: {integrity: sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA==} engines: {node: '>=10'} dependencies: - semver: 7.5.4 + semver: 7.6.3 dev: false /node-addon-api@6.1.0: @@ -18827,6 +19650,16 @@ packages: has-symbols: 1.0.3 object-keys: 1.1.1 + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + /object.entries@1.1.7: resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} engines: {node: '>= 0.4'} @@ -18835,6 +19668,15 @@ packages: define-properties: 1.2.1 es-abstract: 1.22.3 + /object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + /object.fromentries@2.0.7: resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} engines: {node: '>= 0.4'} @@ -18843,6 +19685,16 @@ packages: define-properties: 1.2.1 es-abstract: 1.22.3 + /object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + /object.groupby@1.0.1: resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} dependencies: @@ -18851,6 +19703,15 @@ packages: es-abstract: 1.22.3 get-intrinsic: 1.2.2 + /object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + dev: true + /object.hasown@1.1.3: resolution: {integrity: sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==} dependencies: @@ -18865,6 +19726,15 @@ packages: define-properties: 1.2.1 es-abstract: 1.22.3 + /object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + /octokit@3.1.2: resolution: {integrity: sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==} engines: {node: '>= 18'} @@ -19175,6 +20045,11 @@ packages: polyline-miter-util: 1.0.1 dev: false + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + /postcss-import@15.1.0(postcss@8.4.31): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -19221,7 +20096,7 @@ packages: ts-node: optional: true dependencies: - lilconfig: 3.1.1 + lilconfig: 3.1.2 postcss: 8.4.31 yaml: 2.4.0 dev: true @@ -20525,6 +21400,16 @@ packages: define-properties: 1.2.1 set-function-name: 2.0.1 + /regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + dev: true + /regexpp@3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} @@ -20926,6 +21811,16 @@ packages: has-symbols: 1.0.3 isarray: 2.0.5 + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} dev: false @@ -20940,6 +21835,15 @@ packages: get-intrinsic: 1.2.2 is-regex: 1.1.4 + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + dev: true + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -21007,6 +21911,11 @@ packages: dependencies: lru-cache: 6.0.0 + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + /send@0.6.0: resolution: {integrity: sha512-A3EwHmDwcPcmLxIRNjr2YbXiYWq6M9JyUq4303pLKVFs4m5oeME0a9Cpcu9N22fED5XVepldjPYGo9eJifb7Yg==} engines: {node: '>= 0.8.0'} @@ -21054,6 +21963,18 @@ packages: gopd: 1.0.1 has-property-descriptors: 1.0.1 + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: true + /set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} engines: {node: '>= 0.4'} @@ -21062,6 +21983,16 @@ packages: functions-have-names: 1.2.3 has-property-descriptors: 1.0.1 + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + /set-harmonic-interval@1.0.1: resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} engines: {node: '>=6.9'} @@ -21127,6 +22058,16 @@ packages: get-intrinsic: 1.2.2 object-inspect: 1.13.1 + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -21527,6 +22468,15 @@ packages: strip-ansi: 7.1.0 dev: true + /string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + dev: true + /string.prototype.matchall@4.0.10: resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} dependencies: @@ -21540,6 +22490,31 @@ packages: set-function-name: 2.0.1 side-channel: 1.0.4 + /string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.3 + set-function-name: 2.0.2 + side-channel: 1.0.6 + dev: true + + /string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + dependencies: + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + /string.prototype.trim@1.2.8: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} @@ -21548,6 +22523,16 @@ packages: define-properties: 1.2.1 es-abstract: 1.22.3 + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + /string.prototype.trimend@1.0.7: resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} dependencies: @@ -21555,6 +22540,14 @@ packages: define-properties: 1.2.1 es-abstract: 1.22.3 + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + /string.prototype.trimstart@1.0.7: resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} dependencies: @@ -21562,6 +22555,15 @@ packages: define-properties: 1.2.1 es-abstract: 1.22.3 + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: @@ -21788,7 +22790,7 @@ packages: is-glob: 4.0.3 jiti: 1.21.0 lilconfig: 2.1.0 - micromatch: 4.0.5 + micromatch: 4.0.7 normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 @@ -22137,6 +23139,15 @@ packages: typescript: 5.2.2 dev: true + /ts-api-utils@1.4.0(typescript@5.2.2): + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.2.2 + dev: true + /ts-easing@0.2.0: resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} dev: false @@ -22153,6 +23164,15 @@ packages: minimist: 1.2.8 strip-bom: 3.0.0 + /tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + /tslib@1.13.0: resolution: {integrity: sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==} dev: false @@ -22246,6 +23266,15 @@ packages: get-intrinsic: 1.2.2 is-typed-array: 1.1.12 + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: true + /typed-array-byte-length@1.0.0: resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} engines: {node: '>= 0.4'} @@ -22255,6 +23284,17 @@ packages: has-proto: 1.0.1 is-typed-array: 1.1.12 + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + /typed-array-byte-offset@1.0.0: resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} engines: {node: '>= 0.4'} @@ -22265,6 +23305,18 @@ packages: has-proto: 1.0.1 is-typed-array: 1.1.12 + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: @@ -22272,6 +23324,18 @@ packages: for-each: 0.3.3 is-typed-array: 1.1.12 + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: true + /typed-regex@0.0.8: resolution: {integrity: sha512-1XkGm1T/rUngbFROIOw9wPnMAKeMsRoc+c9O6GwOHz6aH/FrJFtcyd2sHASbT0OXeGLot5N1shPNpwHGTv9RdQ==} dev: false @@ -22909,6 +23973,17 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true diff --git a/frontend/providers/aiproxy/.eslintrc.json b/frontend/providers/aiproxy/.eslintrc.json new file mode 100644 index 00000000000..0e81f9b97c8 --- /dev/null +++ b/frontend/providers/aiproxy/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} \ No newline at end of file diff --git a/frontend/providers/aiproxy/.gitignore b/frontend/providers/aiproxy/.gitignore new file mode 100644 index 00000000000..19d5f1802cc --- /dev/null +++ b/frontend/providers/aiproxy/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for commiting if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# mock +mock/config.local.yaml \ No newline at end of file diff --git a/frontend/providers/aiproxy/.prettierignore b/frontend/providers/aiproxy/.prettierignore new file mode 100644 index 00000000000..ab9a493f2ce --- /dev/null +++ b/frontend/providers/aiproxy/.prettierignore @@ -0,0 +1,4 @@ +dist +.vscode +**/.DS_Store +node_modules diff --git a/frontend/providers/aiproxy/.prettierrc.json b/frontend/providers/aiproxy/.prettierrc.json new file mode 100644 index 00000000000..c3bc434d271 --- /dev/null +++ b/frontend/providers/aiproxy/.prettierrc.json @@ -0,0 +1,18 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "none", + "bracketSpacing": true, + "jsxBracketSameLine": true, + "bracketSameLine": true, + "arrowParens": "always", + "requirePragma": false, + "insertPragma": false, + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "ignore", + "endOfLine": "auto" +} diff --git a/frontend/providers/aiproxy/.vscode/extensions.json b/frontend/providers/aiproxy/.vscode/extensions.json new file mode 100644 index 00000000000..11e06c85a21 --- /dev/null +++ b/frontend/providers/aiproxy/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "christian-kohler.npm-intellisense", + "streetsidesoftware.code-spell-checker", + "pomdtr.excalidraw-editor", + "lokalise.i18n-ally", + "stylelint.vscode-stylelint", + "EditorConfig.EditorConfig" + ] +} \ No newline at end of file diff --git a/frontend/providers/aiproxy/.vscode/settings.json b/frontend/providers/aiproxy/.vscode/settings.json new file mode 100644 index 00000000000..61caaefac34 --- /dev/null +++ b/frontend/providers/aiproxy/.vscode/settings.json @@ -0,0 +1,54 @@ +{ + // editor + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.tabSize": 2, + "editor.suggestSelection": "first", + "editor.renderControlCharacters": true, + "editor.quickSuggestions": { + "strings": true + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + // eslint + "eslint.format.enable": true, + "eslint.run": "onSave", + "eslint.codeActionsOnSave.mode": "all", + // i18n + "i18n-ally.localesPaths": [ + "app/i18n/locales" + ], + "i18n-ally.enabledParsers": [ + "json" + ], + "i18n-ally.enabledFrameworks": [ + "react", + "i18next", + "general" + ], + "i18n-ally.sourceLanguage": "zh", + "i18n-ally.displayLanguage": "zh,en", + "i18n-ally.keystyle": "nested", + // format and language sepciic + // "[typescriptreact]": { + // "editor.defaultFormatter": "dbaeumer.vscode-eslint" + // }, + // "[typescript]": { + // "editor.defaultFormatter": "dbaeumer.vscode-eslint" + // }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.detectIndentation": false + }, + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/frontend/providers/aiproxy/README.md b/frontend/providers/aiproxy/README.md new file mode 100644 index 00000000000..e215bc4ccf1 --- /dev/null +++ b/frontend/providers/aiproxy/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts new file mode 100644 index 00000000000..012096a2f53 --- /dev/null +++ b/frontend/providers/aiproxy/api/platform.ts @@ -0,0 +1,22 @@ +import { KeysSearchResponse } from '@/app/api/get-keys/route' +import { QueryParams, SearchResponse } from '@/app/api/get-logs/route' +import { QueryParams as KeysQueryParams } from '@/app/api/get-keys/route' +import { GET, POST, DELETE } from '@/utils/request' +import { ModelPrice } from '@/types/backend' + +export const initAppConfig = () => GET<{ aiproxyBackend: string }>('/api/init-app-config') + +export const getModels = () => GET('/api/get-models') + +export const getModelPrices = () => GET('/api/get-mode-price') + +export const getLogs = (params: QueryParams) => GET('/api/get-logs', params) + +export const getKeys = (params: KeysQueryParams) => + GET('/api/get-keys', params) + +export const createKey = (name: string) => POST('/api/create-key', { name }) + +export const deleteKey = (id: number) => DELETE(`/api/delete-key/${id}`) + +export const updateKey = (id: number, status: number) => POST(`/api/update-key/${id}`, { status }) diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx new file mode 100644 index 00000000000..d84c050af4e --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx @@ -0,0 +1,17 @@ +'use client' +import { Flex } from '@chakra-ui/react' + +export default function DashboardPage() { + return ( + <> + + Dashboard + + + + ) +} + +function ChannelList() { + return ChannelList +} diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx new file mode 100644 index 00000000000..6454303cbdd --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx @@ -0,0 +1,17 @@ +import { Flex } from '@chakra-ui/react' + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx new file mode 100644 index 00000000000..e43a476d36a --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx @@ -0,0 +1,42 @@ +import { Flex } from '@chakra-ui/react' + +import KeyList from '@/components/user/KeyList' +import ModelList from '@/components/user/ModelList' + +export default function Home(): JSX.Element { + return ( + + + + + + + + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx new file mode 100644 index 00000000000..11fcb755d05 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -0,0 +1,116 @@ +'use client' +import { Box, Flex } from '@chakra-ui/react' + +import SideBar from '@/components/user/Sidebar' + +import { EVENT_NAME } from 'sealos-desktop-sdk' +import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' +import { useCallback, useEffect } from 'react' +import { initAppConfig } from '@/api/platform' +import { useI18n } from '@/providers/i18n/i18nContext' +import { useBackendStore } from '@/store/backend' +import { useTranslationClientSide } from '@/app/i18n/client' +import { usePathname } from 'next/navigation' +import { useRouter } from 'next/navigation' + +export default function UserLayout({ children }: { children: React.ReactNode }) { + const router = useRouter() + const pathname = usePathname() + const { lng } = useI18n() + const { i18n } = useTranslationClientSide(lng) + const { setAiproxyBackend } = useBackendStore() + + const handleI18nChange = useCallback( + (data: { currentLanguage: string }) => { + const currentLng = i18n.resolvedLanguage // get the latest resolvedLanguage + const newLng = data.currentLanguage + + if (currentLng !== newLng) { + const currentPath = window.location.pathname + const pathWithoutLang = currentPath.split('/').slice(2).join('/') + router.push(`/${newLng}/${pathWithoutLang}`) + } + }, + [i18n.resolvedLanguage] + ) + + // init session + useEffect(() => { + const cleanup = createSealosApp() + ;(async () => { + try { + const newSession = JSON.stringify(await sealosApp.getSession()) + const oldSession = localStorage.getItem('session') + if (newSession && newSession !== oldSession) { + localStorage.setItem('session', newSession) + window.location.reload() + } + console.log('aiproxy: app init success') + } catch (err) { + console.log('aiproxy: app is not running in desktop') + if (!process.env.NEXT_PUBLIC_MOCK_USER) { + localStorage.removeItem('session') + } + } + })() + return () => { + if (cleanup && typeof cleanup === 'function') { + cleanup() + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // init config and language + useEffect(() => { + const initConfig = async () => { + const { aiproxyBackend } = await initAppConfig() + setAiproxyBackend(aiproxyBackend) + } + + initConfig() + + const initLanguage = async () => { + const pathLng = pathname.split('/')[1] + try { + const lang = await sealosApp.getLanguage() + if (pathLng !== lang.lng) { + const pathParts = pathname.split('/') + pathParts[1] = lang.lng + router.push(pathParts.join('/')) + router.refresh() + } + } catch (error) { + if (error instanceof Error) { + console.debug('Language initialization error:', error.message) + } else { + console.debug('Unknown language initialization error:', error) + } + } + } + + initLanguage() + + const cleanup = sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, handleI18nChange) + + return () => { + if (cleanup && typeof cleanup === 'function') { + cleanup() + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + + + + + {/* Main Content */} + + {children} + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx new file mode 100644 index 00000000000..c3ca7c7907b --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -0,0 +1,258 @@ +'use client' + +import { Box, Flex, Text, Button, Icon } from '@chakra-ui/react' +import { MySelect, MyTooltip, SealosCoin } from '@sealos/ui' +import { useMemo, useState } from 'react' + +import { getKeys, getLogs, getModels } from '@/api/platform' +import { useTranslationClientSide } from '@/app/i18n/client' +import SelectDateRange from '@/components/SelectDateRange' +import SwitchPage from '@/components/SwitchPage' +import { BaseTable } from '@/components/table/baseTable' +import { useI18n } from '@/providers/i18n/i18nContext' +import { LogItem } from '@/types/log' +import { useQuery } from '@tanstack/react-query' +import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table' + +const mockStatus = ['all', 'success', 'failed'] + +export default function Home(): React.JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + + const [startTime, setStartTime] = useState(() => { + const currentDate = new Date() + currentDate.setMonth(currentDate.getMonth() - 1) + return currentDate + }) + const [endTime, setEndTime] = useState(new Date()) + const [name, setName] = useState('') + const [modelName, setModelName] = useState('') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [logData, setLogData] = useState([]) + const [total, setTotal] = useState(0) + + const { data: models = [] } = useQuery(['getModels'], () => getModels()) + const { data: tokenData } = useQuery(['getKeys'], () => getKeys({ page: 1, perPage: 100 })) + + const { isLoading } = useQuery( + ['getLogs', page, pageSize, name, modelName, startTime, endTime], + () => + getLogs({ + page, + perPage: pageSize, + token_name: name, + model_name: modelName, + start_timestamp: startTime.getTime().toString(), + end_timestamp: endTime.getTime().toString() + }), + { + onSuccess: (data) => { + if (!data.logs) { + setLogData([]) + setTotal(0) + return + } + setLogData(data.logs) + setTotal(data.total) + } + } + ) + + const columns = useMemo[]>(() => { + return [ + { + header: t('logs.name'), + accessorKey: 'token_name' + }, + { + header: t('logs.model'), + accessorKey: 'model' + }, + { + header: t('logs.prompt_tokens'), + accessorKey: 'prompt_tokens' + }, + { + header: t('logs.completion_tokens'), + accessorKey: 'completion_tokens' + }, + + { + header: t('logs.status'), + accessorFn: (row) => (row.code === 200 ? t('logs.success') : t('logs.failed')), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + + {value} + + ) + }, + id: 'status' + }, + { + header: t('logs.time'), + accessorFn: (row) => new Date(row.created_at).toLocaleString(), + id: 'created_at' + }, + { + accessorKey: 'used_amount', + id: 'used_amount', + header: () => { + return ( + + + + {t('logs.total_price')} + + + + + ) + } + } + ] + }, []) + + const table = useReactTable({ + data: logData, + columns, + getCoreRowModel: getCoreRowModel() + }) + + return ( + + + + {t('logs.call_log')} + + + + + + + + {t('logs.name')} + + ({ + value: item.name, + label: item.name + })) || []) + ]} + onchange={(val: string) => { + if (val === 'all') { + setName('') + } else { + setName(val) + } + }} + /> + + + + + {t('logs.modal')} + + ({ + value: item, + label: item + })) || [] + } + onchange={(val: string) => { + if (val === 'all') { + setModelName('') + } else { + setModelName(val) + } + }} + /> + + + + + + {t('logs.time')} + + + + + + + setPage(idx)} + /> + + + ) +} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx new file mode 100644 index 00000000000..e8545672698 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx @@ -0,0 +1,358 @@ +'use client' +import { + Box, + Flex, + Text, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + Center, + Spinner +} from '@chakra-ui/react' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { useQuery } from '@tanstack/react-query' +import { ModelPrice } from '@/types/backend' +import { getModelPrices } from '@/api/platform' +import { useMemo } from 'react' +import { + createColumnHelper, + getCoreRowModel, + useReactTable, + flexRender +} from '@tanstack/react-table' +import { SealosCoin } from '@sealos/ui' +import { ModelIdentifier } from '@/types/front' +import { MyTooltip } from '@/components/MyTooltip' +import { useMessage } from '@sealos/ui' +// icons +import OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg' +import QwenIcon from '@/ui/svg/icons/modelist/qianwen.svg' +import ChatglmIcon from '@/ui/svg/icons/modelist/chatglm.svg' +import DeepseekIcon from '@/ui/svg/icons/modelist/deepseek.svg' +import MoonshotIcon from '@/ui/svg/icons/modelist/moonshot.svg' +import SparkdeskIcon from '@/ui/svg/icons/modelist/sparkdesk.svg' +import AbabIcon from '@/ui/svg/icons/modelist/minimax.svg' +import DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg' +import ErnieIcon from '@/ui/svg/icons/modelist/ernie.svg' +import Image, { StaticImageData } from 'next/image' + +function Price() { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + return ( + + + + + {t('price.title')} + + + + + + + + ) +} + +function PriceTable() { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { isLoading, data } = useQuery({ + queryKey: ['getModelPrices'], + queryFn: () => getModelPrices(), + refetchOnReconnect: true + }) + + const modelGroups = { + ernie: { + icon: ErnieIcon, + identifiers: ['ernie'] + }, + qwen: { + icon: QwenIcon, + identifiers: ['qwen'] + }, + chatglm: { + icon: ChatglmIcon, + identifiers: ['chatglm', 'glm'] + }, + deepseek: { + icon: DeepseekIcon, + identifiers: ['deepseek'] + }, + moonshot: { + icon: MoonshotIcon, + identifiers: ['moonshot'] + }, + sparkdesk: { + icon: SparkdeskIcon, + identifiers: ['sparkdesk'] + }, + abab: { + icon: AbabIcon, + identifiers: ['abab'] + }, + doubao: { + icon: DoubaoIcon, + identifiers: ['doubao'] + } + } + + const getIdentifier = (modelName: string): ModelIdentifier => { + return modelName.toLowerCase().split(/[-._\d]/)[0] as ModelIdentifier + } + + const getModelIcon = (modelName: string): StaticImageData => { + const identifier = getIdentifier(modelName) + const group = Object.values(modelGroups).find((group) => group.identifiers.includes(identifier)) + return group?.icon || OpenAIIcon + } + + const ModelComponent = ({ modelName }: { modelName: string }) => { + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + const iconSrc = getModelIcon(modelName) + + return ( + + {modelName} + + + navigator.clipboard.writeText(modelName).then( + () => { + message({ + status: 'success', + title: t('copySuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + (err) => { + message({ + status: 'warning', + title: t('copyFailed'), + description: err?.message || t('copyFailed'), + isClosable: true, + position: 'top' + }) + } + ) + } + cursor="pointer"> + {modelName} + + + + ) + } + + const sortModelsByIdentifier = (models: ModelPrice[]): ModelPrice[] => { + const groupedModels = new Map() + + // Group models by identifier + models.forEach((model) => { + const identifier = getIdentifier(model.name) + if (!groupedModels.has(identifier)) { + groupedModels.set(identifier, []) + } + groupedModels.get(identifier)!.push(model) + }) + + // Define order based on modelGroups + const orderMap = new Map(Object.keys(modelGroups).map((key, index) => [key, index])) + + // Sort based on modelGroups order, unknown models go to the end + const sortedEntries = Array.from(groupedModels.entries()).sort((a, b) => { + const orderA = orderMap.has(a[0]) ? orderMap.get(a[0])! : Number.MAX_VALUE + const orderB = orderMap.has(b[0]) ? orderMap.get(b[0])! : Number.MAX_VALUE + return orderA - orderB + }) + + return sortedEntries.flatMap(([_, models]) => models) + } + + const columnHelper = createColumnHelper() + const columns = [ + columnHelper.accessor((row) => row.name, { + id: 'name', + header: () => ( + + {t('key.name')} + + ), + cell: (info) => + }), + columnHelper.accessor((row) => row.prompt, { + id: 'inputPrice', + header: () => { + return ( + + + + {t('key.inputPrice')} + + + + {t('price.per1kTokens').toLowerCase()} + + + + ) + }, + cell: (info) => ( + + {info.getValue()} + + ) + }), + columnHelper.accessor((row) => row.completion, { + id: 'outputPrice', + header: () => ( + + + + {t('key.outputPrice')} + + + + {t('price.per1kTokens').toLowerCase()} + + + + ), + cell: (info) => ( + + {info.getValue()} + + ) + }) + ] + + const sortedData = useMemo(() => sortModelsByIdentifier(data || []), [data]) + + const table = useReactTable({ + data: sortedData, + columns, + getCoreRowModel: getCoreRowModel() + }) + + return isLoading ? ( +
+ +
+ ) : ( + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header, i) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ) +} + +export default Price diff --git a/frontend/providers/aiproxy/app/[lng]/globals.css b/frontend/providers/aiproxy/app/[lng]/globals.css new file mode 100644 index 00000000000..e6e10f493dd --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/globals.css @@ -0,0 +1,162 @@ +body, +h1, +h2, +h3, +h4, +hr, +p, +blockquote, +dl, +dt, +dd, +ul, +ol, +li, +pre, +form, +fieldset, +legend, +button, +input, +textarea, +th, +td { + margin: 0; +} +body, +button, +input, +select, +textarea { + font: 12px/1.5tahoma, arial, \5b8b\4f53; +} + +address, +cite, +dfn, +em, +var { + font-style: normal; +} +body { + background-color: #f4f4f7 !important; +} +small { + font-size: 12px; +} +ul, +ol { + list-style: none; + padding: 0; +} +a { + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +sup { + vertical-align: text-top; +} +sub { + vertical-align: text-bottom; +} +legend { + color: #000; +} +fieldset, +img { + border: 0; +} +button, +input, +select, +textarea { + font-size: 100%; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +* { + box-sizing: border-box; +} + +.icon { + width: 1em; + height: 1em; + vertical-align: -0.15em; + fill: currentColor; + overflow: hidden; +} + +input::placeholder, +textarea::placeholder { + color: var(--chakra-colors-blackAlpha-400); +} + +#__next { + height: 100%; +} + +::-webkit-scrollbar, +::-webkit-scrollbar { + width: 8px; + height: 8px; + border-radius: 8px; +} +::-webkit-scrollbar-track, +::-webkit-scrollbar-track { + background: transparent !important; + border-radius: 2px; +} +::-webkit-scrollbar-thumb, +::-webkit-scrollbar-thumb { + background: rgba(189, 193, 197, 1) !important; + border-radius: 2px; +} +::-webkit-scrollbar-thumb:hover, +::-webkit-scrollbar-thumb:hover { + background: rgba(189, 193, 197, 1) !important; +} + +div { + &::-webkit-scrollbar-thumb, + &::-webkit-scrollbar-thumb { + background: transparent !important; + border-radius: 2px; + transition: 1s; + } + &:hover { + &::-webkit-scrollbar-thumb, + &::-webkit-scrollbar-thumb { + background: rgba(189, 193, 197, 0.5) !important; + } + &::-webkit-scrollbar-thumb:hover, + &::-webkit-scrollbar-thumb:hover { + background: rgba(189, 193, 197, 1) !important; + } + } +} + +.hover-button { + display: none; +} + +.hover-container:hover .hover-button { + display: block; +} + +div.rdp { + --rdp-cell-size: 40px; + --rdp-accent-color: black; + --rdp-background-color: #f4f6f8; + /* black theme color */ + --rdp-accent-color-dark: #3003e1; + --rdp-background-color-dark: #180270; + /* focus element outline border */ + --rdp-outline: 2px solid var(--rdp-accent-color); + /* focus and selected element outline border */ + --rdp-outline-selected: 2px solid rgba(0, 0, 0, 0.75); +} diff --git a/frontend/providers/aiproxy/app/[lng]/layout.tsx b/frontend/providers/aiproxy/app/[lng]/layout.tsx new file mode 100644 index 00000000000..5b861c6a6b4 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/layout.tsx @@ -0,0 +1,56 @@ +import { dir } from 'i18next' +import type { Metadata } from 'next' + +import { useTranslationServerSide } from '@/app/i18n/server' +import { fallbackLng, languages } from '@/app/i18n/settings' +import ChakraProviders from '@/providers/chakra/providers' +import { I18nProvider } from '@/providers/i18n/i18nContext' +import QueryProvider from '@/providers/chakra/QueryProvider' + +import './globals.css' +import 'react-day-picker/dist/style.css' + +export async function generateStaticParams(): Promise<{ lng: string }[]> { + return languages.map((lng) => ({ lng })) +} + +export async function generateMetadata({ + params +}: { + params: { + lng: string + } +}): Promise { + let { lng } = await params + if (languages.indexOf(lng) < 0) lng = fallbackLng + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = await useTranslationServerSide(lng, 'common') + return { + icons: { + icon: '/favicon.svg' + }, + title: t('title'), + description: t('description') + } +} + +export default async function RootLayout({ + children, + params +}: Readonly<{ + children: React.ReactNode + params: Promise<{ lng: string }> +}>): Promise { + const lng = (await params).lng + return ( + + + + + {children} + + + + + ) +} diff --git a/frontend/providers/aiproxy/app/api/create-key/route.ts b/frontend/providers/aiproxy/app/api/create-key/route.ts new file mode 100644 index 00000000000..89161f4457e --- /dev/null +++ b/frontend/providers/aiproxy/app/api/create-key/route.ts @@ -0,0 +1,110 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/auth' + +export const dynamic = 'force-dynamic' + +interface CreateTokenRequest { + name: string +} + +interface TokenInfo { + id: number + group: string + key: string + status: number + name: string + quota: number + used_amount: number + request_count: number + models: string[] | null + subnet: string + created_at: number + accessed_at: number + expired_at: number +} + +interface CreateTokenResponse { + data: TokenInfo + message: string + success: boolean +} + +function validateCreateParams(body: CreateTokenRequest): string | null { + if (!body.name) { + return 'Name parameter is required' + } + return null +} + +async function createToken(name: string, group: string): Promise { + try { + const url = new URL( + `/api/token/${group}?auto_create_group=true`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store', + body: JSON.stringify({ + name + }) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: CreateTokenResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to create token') + } + + return result.data + } catch (error) { + console.error('Error creating token:', error) + throw error + } +} + +export async function POST(request: NextRequest): Promise { + try { + const group = await parseJwtToken(request.headers) + const body: CreateTokenRequest = await request.json() + + const validationError = validateCreateParams(body) + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ) + } + + // 创建Token + const newToken = await createToken(body.name, group) + + return NextResponse.json({ + code: 200, + data: newToken, + message: 'Token created successfully' + }) + } catch (error) { + console.error('Token creation error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts new file mode 100644 index 00000000000..26ed34f6ff2 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/auth' + +export const dynamic = 'force-dynamic' +interface DeleteTokenResponse { + message: string + success: boolean +} + +async function deleteToken(group: string, id: string): Promise { + try { + const url = new URL( + `/api/token/${group}/${id}`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: DeleteTokenResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to delete token') + } + } catch (error) { + console.error('Error deleting token:', error) + throw error + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +): Promise { + try { + // 验证用户权限 + const userGroup = await parseJwtToken(request.headers) + + // 验证 ID 参数 + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Token ID is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + // 删除 Token + await deleteToken(userGroup, params.id) + + return NextResponse.json({ + code: 200, + message: 'Token deleted successfully' + }) + } catch (error) { + console.error('Token deletion error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/get-keys/route.ts b/frontend/providers/aiproxy/app/api/get-keys/route.ts new file mode 100644 index 00000000000..13d3ff9c136 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/get-keys/route.ts @@ -0,0 +1,121 @@ +import { NextRequest, NextResponse } from 'next/server' +import { TokenInfo } from '@/types/getKeys' + +import { parseJwtToken } from '@/utils/auth' + +export const dynamic = 'force-dynamic' +export interface KeysSearchResponse { + data: { + tokens: TokenInfo[] + total: number + } + message: string + success: boolean +} + +export interface QueryParams { + page: number + perPage: number +} + +function validateParams(page: number, perPage: number): string | null { + if (page < 1) { + return 'Page number must be greater than 0' + } + + if (perPage < 1 || perPage > 100) { + return 'Per page must be between 1 and 100' + } + + return null +} + +async function fetchTokens( + page: number, + perPage: number, + group: string +): Promise<{ tokens: TokenInfo[]; total: number }> { + try { + const url = new URL( + `/api/token/${group}/search`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + url.searchParams.append('p', page.toString()) + url.searchParams.append('per_page', perPage.toString()) + + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: KeysSearchResponse = await response.json() + + if (!result.success) { + throw new Error(result.message || 'API request failed') + } + + return { + tokens: result.data.tokens.sort((a, b) => a.name.localeCompare(b.name)), + total: result.data.total + } + } catch (error) { + console.error('Error fetching tokens:', error) + return { + tokens: [], + total: 0 + } + } +} + +export async function GET(request: NextRequest): Promise { + try { + const group = await parseJwtToken(request.headers) + + const searchParams = request.nextUrl.searchParams + const page = parseInt(searchParams.get('page') || '1', 10) + const perPage = parseInt(searchParams.get('perPage') || '10', 10) + + const validationError = validateParams(page, perPage) + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ) + } + + const { tokens, total } = await fetchTokens(page, perPage, group) + + return NextResponse.json({ + code: 200, + data: { + tokens, + total + } + }) + } catch (error) { + console.error('Token search error:', error) + + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/get-logs/route.ts b/frontend/providers/aiproxy/app/api/get-logs/route.ts new file mode 100644 index 00000000000..5f9fd0a93be --- /dev/null +++ b/frontend/providers/aiproxy/app/api/get-logs/route.ts @@ -0,0 +1,146 @@ +import { LogItem } from '@/types/log' +import { parseJwtToken } from '@/utils/auth' +import { NextRequest, NextResponse } from 'next/server' + +export const dynamic = 'force-dynamic' +export interface SearchResponse { + data: { + logs: LogItem[] + total: number + } + message: string + success: boolean +} + +export interface QueryParams { + token_name?: string + model_name?: string + code?: string + start_timestamp?: string + end_timestamp?: string + page: number + perPage: number +} + +function validateParams(params: QueryParams): string | null { + if (params.page < 1) { + return 'Page number must be greater than 0' + } + if (params.perPage < 1 || params.perPage > 100) { + return 'Per page must be between 1 and 100' + } + if (params.start_timestamp && params.end_timestamp) { + if (parseInt(params.start_timestamp) > parseInt(params.end_timestamp)) { + return 'Start timestamp cannot be greater than end timestamp' + } + } + return null +} + +async function fetchLogs( + params: QueryParams, + group: string +): Promise<{ logs: LogItem[]; total: number }> { + try { + const url = new URL( + `/api/log/${group}/search`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + + url.searchParams.append('p', params.page.toString()) + url.searchParams.append('per_page', params.perPage.toString()) + + if (params.token_name) { + url.searchParams.append('token_name', params.token_name) + } + if (params.model_name) { + url.searchParams.append('model_name', params.model_name) + } + if (params.code) { + url.searchParams.append('code', params.code) + } + if (params.start_timestamp) { + url.searchParams.append('start_timestamp', params.start_timestamp) + } + if (params.end_timestamp) { + url.searchParams.append('end_timestamp', params.end_timestamp) + } + + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: SearchResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'API request failed') + } + + return { + logs: result.data.logs, + total: result.data.total + } + } catch (error) { + console.error('Error fetching logs:', error) + throw error + } +} + +export async function GET(request: NextRequest): Promise { + try { + const group = await parseJwtToken(request.headers) + const searchParams = request.nextUrl.searchParams + + const queryParams: QueryParams = { + page: parseInt(searchParams.get('page') || '1', 10), + perPage: parseInt(searchParams.get('perPage') || '10', 10), + token_name: searchParams.get('token_name') || undefined, + model_name: searchParams.get('model_name') || undefined, + code: searchParams.get('code') || undefined, + start_timestamp: searchParams.get('start_timestamp') || undefined, + end_timestamp: searchParams.get('end_timestamp') || undefined + } + + const validationError = validateParams(queryParams) + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ) + } + + const { logs, total } = await fetchLogs(queryParams, group) + + return NextResponse.json({ + code: 200, + data: { + logs, + total + } + }) + } catch (error) { + console.error('Logs search error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/get-mode-price/route.ts b/frontend/providers/aiproxy/app/api/get-mode-price/route.ts new file mode 100644 index 00000000000..f9b6e0680e5 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/get-mode-price/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/auth' +import { ModelPrice } from '@/types/backend' + +export const dynamic = 'force-dynamic' + +interface PriceResponse { + data: Record< + string, + { + prompt: number + completion: number + } + > + message: string + success: boolean +} + +function transformToList( + data: Record +): ModelPrice[] { + return Object.entries(data).map(([name, prices]) => ({ + name, + prompt: prices.prompt, + completion: prices.completion + })) +} + +async function fetchModelPrices(): Promise { + try { + const url = new URL( + `/api/models/enabled/price`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: PriceResponse = await response.json() + + if (!result.success) { + throw new Error(result.message || 'get model prices API request failed') + } + + return transformToList(result.data) + } catch (error) { + console.error('Error fetching model prices:', error) + return Promise.reject(error) + } +} + +export async function GET(request: NextRequest): Promise { + try { + await parseJwtToken(request.headers) + const modelPrices = await fetchModelPrices() + + return NextResponse.json({ + code: 200, + data: modelPrices + }) + } catch (error) { + console.error('get model prices error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/get-models/route.ts b/frontend/providers/aiproxy/app/api/get-models/route.ts new file mode 100644 index 00000000000..7f2d205fb48 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/get-models/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server' + +import { parseJwtToken } from '@/utils/auth' + +export const dynamic = 'force-dynamic' + +interface SearchResponse { + data: string[] + message: string + success: boolean +} + +async function fetchModels(): Promise { + try { + const url = new URL( + `/api/models/enabled`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: SearchResponse = await response.json() + + if (!result.success) { + throw new Error(result.message || 'get models API request failed') + } + + return result.data.sort((a, b) => a.localeCompare(b)) + } catch (error) { + console.error('Error fetching models:', error) + return Promise.reject(error) + } +} + +export async function GET(request: NextRequest): Promise { + try { + await parseJwtToken(request.headers) + + const models = await fetchModels() + + return NextResponse.json({ + code: 200, + data: models + }) + } catch (error) { + console.error('get models error:', error) + + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/init-app-config/route.ts b/frontend/providers/aiproxy/app/api/init-app-config/route.ts new file mode 100644 index 00000000000..2e37a2e3a33 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/init-app-config/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server' + +import type { AppConfigType } from '@/types/appConfig' + +export const dynamic = 'force-dynamic' + +function getAppConfig(appConfig: AppConfigType): AppConfigType { + if (process.env.APP_TOKEN_JWT_KEY) { + appConfig.auth.appTokenJwtKey = process.env.APP_TOKEN_JWT_KEY + } + if (process.env.AI_PROXY_BACKEND_KEY) { + appConfig.auth.aiProxyBackendKey = process.env.AI_PROXY_BACKEND_KEY + } + if (process.env.AI_PROXY_BACKEND) { + appConfig.backend.aiproxy = process.env.AI_PROXY_BACKEND + } + if (process.env.AI_PROXY_BACKEND_INTERNAL) { + appConfig.backend.aiproxyInternal = process.env.AI_PROXY_BACKEND_INTERNAL + } + return appConfig +} + +function initAppConfig(): AppConfigType { + // default config + const DefaultAppConfig: AppConfigType = { + auth: { + appTokenJwtKey: '', + aiProxyBackendKey: '' + }, + backend: { + aiproxy: '', + aiproxyInternal: '' + } + } + if (!global.AppConfig) { + try { + global.AppConfig = getAppConfig(DefaultAppConfig) + } catch (error) { + console.error('Config initialization error:', error) + global.AppConfig = DefaultAppConfig + } + } + + return global.AppConfig +} + +export async function GET(): Promise { + try { + const config = initAppConfig() + + return NextResponse.json({ + code: 200, + message: 'Success', + data: { + aiproxyBackend: config.backend.aiproxy + } + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + console.error('Config API error:', errorMessage) + + return NextResponse.json( + { + code: 500, + message: 'Failed to load configuration', + error: errorMessage + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts new file mode 100644 index 00000000000..c0665abd3ff --- /dev/null +++ b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts @@ -0,0 +1,102 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/auth' + +export const dynamic = 'force-dynamic' +interface UpdateTokenResponse { + message: string + success: boolean +} + +interface UpdateTokenBody { + status: number +} + +async function updateToken(group: string, id: string, status: number): Promise { + try { + if (status !== 1 && status !== 2) { + throw new Error('Invalid status') + } + const url = new URL( + `/api/token/${group}/${id}/status`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) + const token = global.AppConfig?.auth.aiProxyBackendKey + + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify({ status }), + cache: 'no-store' + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: UpdateTokenResponse = await response.json() + if (!result.success) { + throw new Error(result.message || 'Failed to update token') + } + } catch (error) { + console.error('Error updating token:', error) + throw error + } +} + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +): Promise { + try { + // 验证用户权限 + const userGroup = await parseJwtToken(request.headers) + + // 验证 ID 参数 + if (!params.id) { + return NextResponse.json( + { + code: 400, + message: 'Token ID is required', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + // 获取请求体 + const body: UpdateTokenBody = await request.json() + + // 验证状态参数 + if (typeof body.status !== 'number') { + return NextResponse.json( + { + code: 400, + message: 'Status must be a number', + error: 'Bad Request' + }, + { status: 400 } + ) + } + + // 更新 Token + await updateToken(userGroup, params.id, body.status) + + return NextResponse.json({ + code: 200, + message: 'Token updated successfully' + }) + } catch (error) { + console.error('Token update error:', error) + return NextResponse.json( + { + code: 500, + message: error instanceof Error ? error.message : 'Internal server error', + error: error instanceof Error ? error.message : 'Internal server error' + }, + { status: 500 } + ) + } +} diff --git a/frontend/providers/aiproxy/app/i18n/client.ts b/frontend/providers/aiproxy/app/i18n/client.ts new file mode 100644 index 00000000000..29461c7f8c5 --- /dev/null +++ b/frontend/providers/aiproxy/app/i18n/client.ts @@ -0,0 +1,50 @@ +'use client' + +import { + FallbackNs, + initReactI18next, + useTranslation, + UseTranslationOptions, + UseTranslationResponse +} from 'react-i18next' +import i18next, { FlatNamespace, KeyPrefix } from 'i18next' +import LanguageDetector from 'i18next-browser-languagedetector' +import resourcesToBackend from 'i18next-resources-to-backend' + +import { getOptions, languages } from './settings' + +const runsOnServerSide = typeof window === 'undefined' + +i18next + .use(initReactI18next) + .use(LanguageDetector) + .use( + resourcesToBackend( + (language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`) + ) + ) + .init({ + ...getOptions(), + lng: undefined, // let detect the language on client side + detection: { + order: ['path', 'htmlTag', 'navigator'] + }, + preload: runsOnServerSide ? languages : [] + }) + +export function useTranslationClientSide< + Ns extends FlatNamespace, + KPrefix extends KeyPrefix> = undefined +>( + lng: string, + ns?: Ns, + options?: UseTranslationOptions +): UseTranslationResponse, KPrefix> { + const ret = useTranslation(ns, options) + + if (lng && lng !== i18next.resolvedLanguage) { + i18next.changeLanguage(lng) + } + + return ret +} diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json new file mode 100644 index 00000000000..e5d2271905a --- /dev/null +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -0,0 +1,91 @@ +{ + "title": "AI proxy", + "description": "AI proxy", + "Sidebar": { + "Home": "API Keys", + "Logs": "Logs", + "Price": "Pricing" + }, + "keyList": { + "title": "API Keys" + }, + "key": { + "key": "API Key", + "name": "Name", + "createdAt": "Creation time", + "lastUsedAt": "Last use time", + "status": "State", + "namePlaceholder": "Please enter name", + "nameRequired": "Please enter key name", + "nameMaxLength": "Key length is illegal", + "nameOnlyLettersAndNumbers": "key name contains special characters", + "createSuccess": "Created successfully", + "createFailed": "Creation failed", + "actions": "Operation", + "deleteSuccess": "Delete key successfully", + "deleteFailed": "Failed to delete key", + "updateSuccess": "Update status successful", + "updateFailed": "Update status failed", + "unused": "Not use", + "inputPrice": "Input price", + "outputPrice": "Output price", + "createName": "Name" + }, + "logs": { + "call_log": "Logs", + "name": "Name", + "status": "State", + "time": "Time", + "modal": "Model", + "prompt_tokens": "Input", + "completion_tokens": "Output", + "price": "Price", + "select_modal": "Please select model", + "select_token_name": "Please select name", + "model": "Model", + "total_price": "Total amount", + "total_price_tip": "(Number of input tokens × Input price) + (Number of output tokens × Output price)", + "success": "Success", + "failed": "Failed" + }, + "Page": "Page", + "Total": "Total", + "modelList": { + "title": "Supported models" + }, + "copy": "Copy", + "createKey": "New", + "Key": { + "create": "Create API Key" + }, + "confirm": "Confirm", + "keystatus": { + "enabled": "Enabled", + "disabled": "Disabled", + "expired": "Expired", + "exhausted": "Exhausted", + "unknown": "Unknown" + }, + "delete": "Delete", + "disable": "Disable", + "enable": "Enable", + "copySuccess": "Copied", + "copyFailed": "Copy failed", + "noData": "You don’t have an API Key yet", + "price": { + "title": "Pricing", + "per1kTokens": "1k tokens" + }, + "ernie": "Baidu-Ernie", + "qwen": "Alibaba-Qwen", + "chatglm": "BigModel-Chatglm", + "deepseek": "Deepseek", + "moonshot": "Moonshot", + "sparkdesk": "Sparkdesk", + "abab": "Minimax", + "doubao": "ByteDance-Doubao", + "glm": "Glm", + "o": "OpenAI", + "gpt": "OpenAI", + "createKey2": "Create Key" +} diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json new file mode 100644 index 00000000000..5a6226556e4 --- /dev/null +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -0,0 +1,91 @@ +{ + "title": "AI 代理", + "description": "AI 代理", + "Sidebar": { + "Home": "API Keys", + "Logs": "调用日志", + "Price": "模型价格" + }, + "keyList": { + "title": "API Keys" + }, + "key": { + "key": "API Key", + "name": "名称", + "createdAt": "创建时间", + "lastUsedAt": "最后使用时间", + "status": "状态", + "namePlaceholder": "请输入名称", + "nameRequired": "请输入 Key 名字", + "nameMaxLength": "key 长度不合法", + "nameOnlyLettersAndNumbers": "Key 名字包含特殊字符", + "createSuccess": "创建成功", + "createFailed": "创建失败", + "actions": "操作", + "deleteSuccess": "删除成功", + "deleteFailed": "删除失败", + "updateSuccess": "状态更新成功", + "updateFailed": "状态更新失败", + "unused": "未使用", + "inputPrice": "输入单价", + "outputPrice": "输出单价", + "createName": "名称" + }, + "logs": { + "call_log": "调用日志", + "name": "名称", + "modal": "模型", + "status": "状态", + "time": "时间", + "model": "模型", + "prompt_tokens": "输入", + "completion_tokens": "输出", + "price": "价格", + "select_modal": "请选择模型", + "select_token_name": "请选择名称", + "total_price": "总金额", + "total_price_tip": "(输入 token 数×输入价格)+(输出 token 数 × 输出价格)", + "success": "成功", + "failed": "失败" + }, + "Page": "页", + "Total": "总数", + "modelList": { + "title": "支持的模型" + }, + "copy": "复制", + "createKey": "新建", + "Key": { + "create": "新建 API Key" + }, + "confirm": "确认", + "keystatus": { + "enabled": "启用", + "disabled": "禁用", + "expired": "过期", + "exhausted": "耗尽", + "unknown": "未知" + }, + "delete": "删除", + "disable": "禁用", + "enable": "启用", + "copySuccess": "复制成功", + "copyFailed": "复制失败", + "noData": "你还没有 API Key", + "price": { + "title": "模型价格", + "per1kTokens": "1k tokens" + }, + "ernie": "百度文心", + "qwen": "阿里千问", + "chatglm": "智谱", + "deepseek": "Deepseek", + "moonshot": "月之暗面", + "sparkdesk": "讯飞星火", + "abab": "Minimax", + "doubao": "字节豆包", + "glm": "智谱", + "o": "OpenAI", + "gpt": "OpenAI", + "createKey2": "新建 Key" +} diff --git a/frontend/providers/aiproxy/app/i18n/server.ts b/frontend/providers/aiproxy/app/i18n/server.ts new file mode 100644 index 00000000000..9f501d0e9fe --- /dev/null +++ b/frontend/providers/aiproxy/app/i18n/server.ts @@ -0,0 +1,39 @@ +import { FallbackNs } from 'react-i18next'; +import { initReactI18next } from 'react-i18next/initReactI18next'; +import { createInstance, FlatNamespace, i18n, KeyPrefix, TFunction } from 'i18next'; +import resourcesToBackend from 'i18next-resources-to-backend'; + +import { getOptions } from './settings'; + +const initI18next = async (lng: string, ns: string | string[]): Promise => { + // on server side we create a new instance for each render, because during compilation everything seems to be executed in parallel + const i18nInstance = createInstance(); + await i18nInstance + .use(initReactI18next) + .use( + resourcesToBackend( + (language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`) + ) + ) + .init(getOptions(lng, ns)); + return i18nInstance; +}; + +export async function useTranslationServerSide< + Ns extends FlatNamespace, + KPrefix extends KeyPrefix> = undefined +>( + lng: string, + ns?: Ns, + options: { keyPrefix?: KPrefix } = {} +): Promise<{ t: TFunction; i18n: i18n }> { + const i18nextInstance = await initI18next( + lng, + Array.isArray(ns) ? ns : [ns as string].filter(Boolean) + ); + return { + // ns default is translation.json + t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix), + i18n: i18nextInstance + }; +} diff --git a/frontend/providers/aiproxy/app/i18n/settings.ts b/frontend/providers/aiproxy/app/i18n/settings.ts new file mode 100644 index 00000000000..1540970f6e3 --- /dev/null +++ b/frontend/providers/aiproxy/app/i18n/settings.ts @@ -0,0 +1,28 @@ +export const fallbackLng = 'zh'; +export const languages = [fallbackLng, 'en']; +export const defaultNS = 'common'; + +interface I18nextOptions { + supportedLngs: string[]; + fallbackLng: string; + lng: string; + fallbackNS: string; + defaultNS: string; + ns: string | string[]; +} + +export function getOptions(lng = fallbackLng, ns: string | string[] = defaultNS): I18nextOptions { + return { + // debug: true, + supportedLngs: languages, + // preload: languages, + fallbackLng, + lng, + fallbackNS: defaultNS, + defaultNS, + ns + // backend: { + // projectId: '01b2e5e8-6243-47d1-b36f-963dbb8bcae3' + // } + }; +} diff --git a/frontend/providers/aiproxy/components/MyTooltip/index.tsx b/frontend/providers/aiproxy/components/MyTooltip/index.tsx new file mode 100644 index 00000000000..dd5aefadf64 --- /dev/null +++ b/frontend/providers/aiproxy/components/MyTooltip/index.tsx @@ -0,0 +1,27 @@ +import { Tooltip, TooltipProps } from '@chakra-ui/react' + +export const MyTooltip = ({ children, ...tooltipProps }: TooltipProps) => { + return ( + + {children} + + ) +} diff --git a/frontend/providers/aiproxy/components/SelectDateRange/index.tsx b/frontend/providers/aiproxy/components/SelectDateRange/index.tsx new file mode 100644 index 00000000000..67862c57cf2 --- /dev/null +++ b/frontend/providers/aiproxy/components/SelectDateRange/index.tsx @@ -0,0 +1,284 @@ +import { ChangeEventHandler, Dispatch, SetStateAction, useState } from 'react' +import { DateRange, DayPicker, SelectRangeEventHandler } from 'react-day-picker' +import { + Box, + Button, + Flex, + Icon, + Input, + Popover, + PopoverContent, + PopoverTrigger +} from '@chakra-ui/react' +import { endOfDay, format, isAfter, isBefore, isValid, parse, startOfDay } from 'date-fns' + +type SelectDateRangeProps = { + isDisabled?: boolean + startTime: Date + setStartTime: Dispatch> + endTime: Date + setEndTime: Dispatch> +} + +export default function SelectDateRange({ + isDisabled, + startTime, + setStartTime, + endTime, + setEndTime +}: SelectDateRangeProps): JSX.Element { + const initState = { from: startTime, to: endTime } + + const [selectedRange, setSelectedRange] = useState(initState) + const [fromValue, setFromValue] = useState(format(initState.from, 'y-MM-dd')) + const [toValue, setToValue] = useState(format(initState.to, 'y-MM-dd')) + const [inputState, setInputState] = useState<0 | 1>(0) + const onClose = () => { + selectedRange?.from && setStartTime(startOfDay(selectedRange.from)) + selectedRange?.to && setEndTime(endOfDay(selectedRange.to)) + } + + const handleFromChange: ChangeEventHandler = (e) => { + setFromValue(e.target.value) + const date = parse(e.target.value, 'y-MM-dd', new Date()) + if (!isValid(date)) { + return setSelectedRange({ from: undefined, to: selectedRange?.to }) + } + + if (selectedRange?.to) { + if (isAfter(date, selectedRange.to)) { + setSelectedRange({ from: selectedRange.to, to: date }) + } else { + setSelectedRange({ from: date, to: selectedRange?.to }) + } + } else { + setSelectedRange({ from: date, to: date }) + } + } + + const handleToChange: ChangeEventHandler = (e) => { + setToValue(e.target.value) + const date = parse(e.target.value, 'y-MM-dd', new Date()) + + if (!isValid(date)) { + return setSelectedRange({ from: selectedRange?.from, to: undefined }) + } + if (selectedRange?.from) { + if (isBefore(date, selectedRange.from)) { + setSelectedRange({ from: date, to: selectedRange.from }) + } else { + setSelectedRange({ from: selectedRange?.from, to: date }) + } + } else { + setSelectedRange({ from: date, to: date }) + } + } + + const handleRangeSelect: SelectRangeEventHandler = (range: DateRange | undefined) => { + if (range) { + let { from, to } = range + if (inputState === 0) { + if (from === selectedRange?.from) { + from = to + } else { + to = from + } + setInputState(1) + } else { + setInputState(0) + } + setSelectedRange({ + from, + to + }) + if (from) { + setFromValue(format(from, 'y-MM-dd')) + } else { + setFromValue('') + } + if (to) { + setToValue(format(to, 'y-MM-dd')) + } else { + setToValue(from ? format(from, 'y-MM-dd') : '') + } + } else { + if (fromValue && selectedRange?.from) { + setToValue(fromValue) + setSelectedRange({ + ...selectedRange, + to: selectedRange.from + }) + setInputState(1) + } + } + } + + const handleRangeSelectFrom: SelectRangeEventHandler = (range: DateRange | undefined) => { + if (range) { + let { from, to } = range + if (selectedRange?.to) { + if (from) { + if (!to) { + to = from + } else if (from === selectedRange?.from) { + from = to + to = selectedRange.to + } + if (isBefore(from, selectedRange.to)) { + setSelectedRange({ + ...selectedRange, + from + }) + setFromValue(format(from, 'y-MM-dd')) + } + } + } + } + } + + const handleRangeSelectTo: SelectRangeEventHandler = (range: DateRange | undefined) => { + if (range) { + let { from, to } = range + if (selectedRange?.from) { + if (to) { + if (!from) { + from = to + } else if (to === selectedRange?.to) { + to = from + from = selectedRange.from + } + if (isAfter(to, selectedRange.from)) { + setSelectedRange({ + ...selectedRange, + to + }) + setToValue(format(to, 'y-MM-dd')) + } + } + } + } else { + if (fromValue && selectedRange?.from) { + setToValue(fromValue) + setSelectedRange({ + ...selectedRange, + to: selectedRange.from + }) + setInputState(1) + } + } + } + + return ( + + + + + + + + + + - + + + + + + + + + + { + setInputState(0) + onClose() + }}> + + + + + + + + + ) +} diff --git a/frontend/providers/aiproxy/components/SwitchPage.tsx b/frontend/providers/aiproxy/components/SwitchPage.tsx new file mode 100644 index 00000000000..cb5855afb7b --- /dev/null +++ b/frontend/providers/aiproxy/components/SwitchPage.tsx @@ -0,0 +1,117 @@ +import { RightFirstIcon, ToLeftIcon } from '@/ui/icons' +import { Button, ButtonProps, Flex, FlexProps, Text } from '@chakra-ui/react' +import { useTranslation } from 'next-i18next' + +export default function SwitchPage({ + totalPage, + totalItem, + pageSize, + currentPage, + setCurrentPage, + isPreviousData, + ...props +}: { + currentPage: number + totalPage: number + totalItem: number + pageSize: number + isPreviousData?: boolean + setCurrentPage: (idx: number) => void +} & FlexProps) { + const { t } = useTranslation() + const switchStyle: ButtonProps = { + width: '24px', + height: '24px', + minW: '0', + background: 'grayModern.250', + flexGrow: '0', + borderRadius: 'full', + // variant:'unstyled', + _hover: { + background: 'grayModern.150', + minW: '0' + }, + _disabled: { + borderRadius: 'full', + background: 'grayModern.150', + cursor: 'not-allowed', + minW: '0' + } + } + return ( + + + {t('Total')}: + + + {totalItem} + + + + + {currentPage} + / + {totalPage} + + + + + {pageSize} + + + /{t('Page')} + + + ) +} diff --git a/frontend/providers/aiproxy/components/i18n-switch/Client.tsx b/frontend/providers/aiproxy/components/i18n-switch/Client.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frontend/providers/aiproxy/components/i18n-switch/Server.tsx b/frontend/providers/aiproxy/components/i18n-switch/Server.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frontend/providers/aiproxy/components/table/baseTable.tsx b/frontend/providers/aiproxy/components/table/baseTable.tsx new file mode 100644 index 00000000000..21f569a6d95 --- /dev/null +++ b/frontend/providers/aiproxy/components/table/baseTable.tsx @@ -0,0 +1,81 @@ +import { + Spinner, + Table, + TableContainer, + TableContainerProps, + Tbody, + Td, + Th, + Thead, + Tr +} from '@chakra-ui/react' +import { Table as ReactTable, flexRender } from '@tanstack/react-table' + +export function BaseTable({ + table, + isLoading +}: { table: ReactTable; isLoading: boolean } & TableContainerProps) { + return ( + + + + {table.getHeaderGroups().map((headers) => { + return ( + + {headers.headers.map((header, i) => { + return ( + + ) + })} + + ) + })} + + + {isLoading ? ( + + + + ) : ( + table.getRowModel().rows.map((item) => { + return ( + + {item.getAllCells().map((cell, i) => { + return ( + + ) + })} + + ) + }) + )} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ) +} diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx new file mode 100644 index 00000000000..09b7b5e9c7d --- /dev/null +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -0,0 +1,954 @@ +'use client' +import React, { useMemo, useState } from 'react' +import { + Box, + Button, + Flex, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Table, + TableContainer, + Tbody, + Td, + Text, + Th, + Thead, + Tr, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + FormControl, + Input, + FormErrorMessage, + useDisclosure, + Center +} from '@chakra-ui/react' +import { + Column, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable +} from '@tanstack/react-table' +import { TFunction } from 'i18next' +import { createKey, deleteKey, getKeys, updateKey } from '@/api/platform' + +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import { ChainIcon } from '@/ui/icons/home/Icons' +import { useMessage } from '@sealos/ui' +import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query' +import { TokenInfo } from '@/types/getKeys' +import SwitchPage from '@/components/SwitchPage' +import { useBackendStore } from '@/store/backend' +import { MyTooltip } from '@/components/MyTooltip' + +export function KeyList(): JSX.Element { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { isOpen, onOpen, onClose } = useDisclosure() + + return ( + <> + + + {t('keyList.title')} + + + + + {/* table */} + + {/* modal */} + + + + ) +} + +export enum TableHeaderId { + NAME = 'key.name', + KEY = 'key.key', + CREATED_AT = 'key.createdAt', + LAST_USED_AT = 'key.lastUsedAt', + STATUS = 'key.status', + ACTIONS = 'key.actions' +} +enum KeyStatus { + ENABLED = 1, + DISABLED = 2, + EXPIRED = 3, + EXHAUSTED = 4 +} + +const CustomHeader = ({ column, t }: { column: Column; t: TFunction }) => { + return ( + + {t(column.id as TableHeaderId)} + + ) +} + +const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { + const aiproxyBackend = useBackendStore((state) => state.aiproxyBackend) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [openPopoverId, setOpenPopoverId] = useState(null) + + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + const queryClient = useQueryClient() + + const deleteKeyMutation = useMutation((id: number) => deleteKey(id), { + onSuccess() { + queryClient.invalidateQueries(['getKeys']) + message({ + status: 'success', + title: t('key.deleteSuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + onError(err: any) { + message({ + status: 'warning', + title: t('key.deleteFailed'), + description: err?.message || t('key.deleteFailed'), + isClosable: true, + position: 'top' + }) + } + }) + + const updateKeyMutation = useMutation( + ({ id, status }: { id: number; status: number }) => updateKey(id, status), + { + onSuccess() { + queryClient.invalidateQueries(['getKeys']) + message({ + status: 'success', + title: t('key.updateSuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + onError(err: any) { + message({ + status: 'warning', + title: t('key.updateFailed'), + description: err?.message || t('key.updateFailed'), + isClosable: true, + position: 'top' + }) + } + } + ) + + // Update the button click handlers in the table actions column: + const handleStatusUpdate = (id: number, currentStatus: number) => { + const newStatus = currentStatus === KeyStatus.DISABLED ? KeyStatus.ENABLED : KeyStatus.DISABLED + updateKeyMutation.mutate({ id, status: newStatus }) + } + + const handleDelete = (id: number) => { + deleteKeyMutation.mutate(id) + } + + const { data, isLoading } = useQuery({ + queryKey: ['getKeys', page, pageSize], + queryFn: () => getKeys({ page, perPage: pageSize }), + refetchOnReconnect: true, + onSuccess(data) { + setTotal(data.total) + } + }) + + const columnHelper = createColumnHelper() + + const columns = [ + columnHelper.accessor((row) => row.name, { + id: TableHeaderId.NAME, + header: (props) => , + cell: (info) => ( + + {info.getValue()} + + ) + }), + columnHelper.accessor((row) => row.key, { + id: TableHeaderId.KEY, + header: (props) => , + cell: (info) => ( + { + const key = 'sk-' + info.getValue() + navigator.clipboard.writeText(key).then( + () => { + message({ + status: 'success', + title: t('copySuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + (err) => { + message({ + status: 'warning', + title: t('copyFailed'), + description: err?.message || t('copyFailed'), + isClosable: true, + position: 'top' + }) + } + ) + }}> + + {'sk-' + + info.getValue().substring(0, 8) + + '*'.repeat(Math.max(0, info.getValue().length - 8))} + + + ) + }), + columnHelper.accessor((row) => row.created_at, { + id: TableHeaderId.CREATED_AT, + header: (props) => , + cell: (info) => ( + + {new Date(info.getValue()).toLocaleString()} + + ) + }), + columnHelper.accessor((row) => row.accessed_at, { + id: TableHeaderId.LAST_USED_AT, + header: (props) => , + cell: (info) => { + const timestamp = info.getValue() + + if (timestamp && timestamp < 0) { + return ( + + {t('key.unused')} + + ) + } + + return ( + + {new Date(timestamp).toLocaleString()} + + ) + } + }), + columnHelper.accessor((row) => row.status, { + id: TableHeaderId.STATUS, + header: (props) => , + cell: (info) => { + const status = info.getValue() + let statusText = '' + let statusColor = '' + + switch (status) { + case KeyStatus.ENABLED: + statusText = t('keystatus.enabled') + statusColor = 'green.600' + break + case KeyStatus.DISABLED: + statusText = t('keystatus.disabled') + statusColor = 'red.600' + break + case KeyStatus.EXPIRED: + statusText = t('keystatus.expired') + statusColor = 'orange.500' + break + case KeyStatus.EXHAUSTED: + statusText = t('keystatus.exhausted') + statusColor = 'gray.500' + break + default: + statusText = t('keystatus.unknown') + statusColor = 'gray.500' + } + + return ( + + {statusText} + + ) + } + }), + + columnHelper.display({ + id: TableHeaderId.ACTIONS, + header: (props) => , + cell: (info) => ( + setOpenPopoverId(null)}> + + setOpenPopoverId(info.row.original.id)}> + + + + + + + + + {info.row.original.status === KeyStatus.DISABLED ? ( + + ) : ( + + )} + + + + + + ) + }) + ] + + const sortTableData = (data: TokenInfo[]) => { + return data.sort((a, b) => b.created_at - a.created_at) + } + + const sortedData = useMemo(() => sortTableData(data?.tokens || []), [data]) + + const table = useReactTable({ + data: sortedData, + columns, + getCoreRowModel: getCoreRowModel() + }) + + return ( + <> + {isLoading || data?.tokens.length === 0 ? ( + + +
+ + + + + + + + + + + + + + + + + {t('noData')} + + + + + +
+
+
+ ) : ( + <> + + + + + API Endpoint: + + { + const endpoint = aiproxyBackend + navigator.clipboard.writeText(endpoint).then( + () => { + message({ + status: 'success', + title: t('copySuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + (err) => { + message({ + status: 'warning', + title: t('copyFailed'), + description: err?.message || t('copyFailed'), + isClosable: true, + position: 'top' + }) + } + ) + }}> + {aiproxyBackend} + + + + + + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header, i) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ setPage(idx)} + /> +
+ + )} + + ) +} + +function CreateKeyModal({ + isOpen, + onClose, + t +}: { + isOpen: boolean + onClose: () => void + t: TFunction +}) { + const initialRef = React.useRef(null) + const [name, setName] = useState('') + const [error, setError] = useState('') + const queryClient = useQueryClient() + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + const createKeyMutation = useMutation((name: string) => createKey(name), { + onSuccess(data) { + createKeyMutation.reset() + setName('') + queryClient.invalidateQueries(['getKeys']) + message({ + status: 'success', + title: t('key.createSuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + onClose() + }, + onError(err: any) { + message({ + status: 'warning', + title: t('key.createFailed'), + description: err?.response?.data?.message || t('key.createFailed'), + isClosable: true, + position: 'top' + }) + } + }) + + const validateName = (value: string) => { + if (!value) { + setError(t('key.nameRequired')) + } else if (value.length >= 32) { + setError(t('key.nameMaxLength')) + } else if (!/^[A-Za-z0-9-]+$/.test(value)) { + setError(t('key.nameOnlyLettersAndNumbers')) + } else { + setError('') + } + } + + const handleNameChange = (e: React.ChangeEvent) => { + const newName = e.target.value + validateName(newName) + setName(newName) + } + + const handleConfirm = () => { + if (error === '' && name !== '') { + createKeyMutation.mutate(name) + return + } + } + + return ( + + + + {/* header */} + + + + {t('Key.create')} + + + + + {/* body */} + + + + + {t('key.createName')} + + + + {error && {error}} + + + {/* button */} + + + + + + + + ) +} + +export default KeyList diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx new file mode 100644 index 00000000000..e58eca70739 --- /dev/null +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -0,0 +1,206 @@ +'use client' +import { Badge, Center, Flex, Spinner, Text } from '@chakra-ui/react' +import { ListIcon } from '@/ui/icons/home/Icons' +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' +import Image, { StaticImageData } from 'next/image' +import { useQuery } from '@tanstack/react-query' +import { getModels } from '@/api/platform' +import { useMessage } from '@sealos/ui' +// icons +import OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg' +import QwenIcon from '@/ui/svg/icons/modelist/qianwen.svg' +import ChatglmIcon from '@/ui/svg/icons/modelist/chatglm.svg' +import DeepseekIcon from '@/ui/svg/icons/modelist/deepseek.svg' +import MoonshotIcon from '@/ui/svg/icons/modelist/moonshot.svg' +import SparkdeskIcon from '@/ui/svg/icons/modelist/sparkdesk.svg' +import AbabIcon from '@/ui/svg/icons/modelist/minimax.svg' +import DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg' +import ErnieIcon from '@/ui/svg/icons/modelist/ernie.svg' +import { useMemo } from 'react' +import { MyTooltip } from '@/components/MyTooltip' +import { ModelIdentifier } from '@/types/front' + +const getIdentifier = (modelName: string): ModelIdentifier => { + return modelName.toLowerCase().split(/[-._\d]/)[0] as ModelIdentifier +} + +const sortModels = (models: string[]): string[] => { + // group by identifier + const groupMap = new Map() + + // group by identifier + models.forEach((model) => { + const identifier = getIdentifier(model) + // special handle gpt and o1, group them as 'openai' + const groupKey = identifier === 'gpt' || identifier === 'o' ? 'openai' : identifier + if (!groupMap.has(groupKey)) { + groupMap.set(groupKey, []) + } + groupMap.get(groupKey)?.push(model) + }) + + // sort by identifier and flatten the result + return Array.from(groupMap.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) // sort by identifier + .flatMap(([_, models]) => models.sort()) // flatten and keep the order in each group +} + +const ModelComponent = ({ modelName }: { modelName: string }) => { + const modelGroups = { + openai: { + icon: OpenAIIcon, + identifiers: ['gpt', 'o1'] + }, + ernie: { + icon: ErnieIcon, + identifiers: ['ernie'] + }, + qwen: { + icon: QwenIcon, + identifiers: ['qwen'] + }, + chatglm: { + icon: ChatglmIcon, + identifiers: ['chatglm', 'glm'] + }, + deepseek: { + icon: DeepseekIcon, + identifiers: ['deepseek'] + }, + moonshot: { + icon: MoonshotIcon, + identifiers: ['moonshot'] + }, + sparkdesk: { + icon: SparkdeskIcon, + identifiers: ['sparkdesk'] + }, + abab: { + icon: AbabIcon, + identifiers: ['abab'] + }, + doubao: { + icon: DoubaoIcon, + identifiers: ['doubao'] + } + } + + // get model icon + const getModelIcon = (modelName: string): StaticImageData => { + const identifier = getIdentifier(modelName) + const group = Object.values(modelGroups).find((group) => group.identifiers.includes(identifier)) + return group?.icon || OpenAIIcon + } + + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const iconSrc = getModelIcon(modelName) + const { message } = useMessage({ + warningBoxBg: 'var(--Yellow-50, #FFFAEB)', + warningIconBg: 'var(--Yellow-500, #F79009)', + warningIconFill: 'white', + successBoxBg: 'var(--Green-50, #EDFBF3)', + successIconBg: 'var(--Green-600, #039855)', + successIconFill: 'white' + }) + + return ( + + {modelName} + + + navigator.clipboard.writeText(modelName).then( + () => { + message({ + status: 'success', + title: t('copySuccess'), + isClosable: true, + duration: 2000, + position: 'top' + }) + }, + (err) => { + message({ + status: 'warning', + title: t('copyFailed'), + description: err?.message || t('copyFailed'), + isClosable: true, + position: 'top' + }) + } + ) + } + cursor="pointer" + _hover={{ color: 'blue.500' }} + transition="color 0.2s ease"> + {modelName} + + + + ) +} + +const ModelList: React.FC = () => { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + const { isLoading, data } = useQuery(['getModels'], () => getModels()) + + const sortedData = useMemo(() => sortModels(data || []), [data]) + + return ( + <> + + + + + {t('modelList.title')} + + + + {data?.length || 0} + + + + + + {isLoading ? ( +
+ +
+ ) : ( + sortedData.map((model) => ) + )} +
+ + ) +} + +export default ModelList diff --git a/frontend/providers/aiproxy/components/user/Sidebar.tsx b/frontend/providers/aiproxy/components/user/Sidebar.tsx new file mode 100644 index 00000000000..8b2b211db30 --- /dev/null +++ b/frontend/providers/aiproxy/components/user/Sidebar.tsx @@ -0,0 +1,111 @@ +'use client' +import { Flex, Text } from '@chakra-ui/react' +import Image, { StaticImageData } from 'next/image' +import Link from 'next/link' +import { usePathname } from 'next/navigation' + +import { useTranslationClientSide } from '@/app/i18n/client' +import homeIcon from '@/ui/svg/icons/sidebar/home.svg' +import homeIcon_a from '@/ui/svg/icons/sidebar/home_a.svg' +import logsIcon from '@/ui/svg/icons/sidebar/logs.svg' +import logsIcon_a from '@/ui/svg/icons/sidebar/logs_a.svg' +import priceIcon from '@/ui/svg/icons/sidebar/price.svg' +import priceIcon_a from '@/ui/svg/icons/sidebar/price_a.svg' +import { useI18n } from '@/providers/i18n/i18nContext' + +type Menu = { + id: string + url: string + value: string + icon: StaticImageData + activeIcon: StaticImageData + display: boolean +} + +const SideBar = (): JSX.Element => { + const pathname = usePathname() + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + + const menus: Menu[] = [ + { + id: 'home', + url: '/home', + value: t('Sidebar.Home'), + icon: homeIcon, + activeIcon: homeIcon_a, + display: true + }, + { + id: 'logs', + url: '/logs', + value: t('Sidebar.Logs'), + icon: logsIcon, + activeIcon: logsIcon_a, + display: true + }, + { + id: 'price', + url: '/price', + value: t('Sidebar.Price'), + icon: priceIcon, + activeIcon: priceIcon_a, + display: true + } + ] + + return ( + + {menus + .filter((menu) => menu.display) + .map((menu) => { + const fullUrl = `/${lng}${menu.url}` + const isActive = pathname === fullUrl + + return ( + + + {menu.value} + + {menu.value} + + + + ) + })} + + ) +} + +export default SideBar diff --git a/frontend/providers/aiproxy/middleware.ts b/frontend/providers/aiproxy/middleware.ts new file mode 100644 index 00000000000..97bae24a108 --- /dev/null +++ b/frontend/providers/aiproxy/middleware.ts @@ -0,0 +1,59 @@ +import acceptLanguage from 'accept-language' +import { NextRequest, NextResponse } from 'next/server' + +import { fallbackLng, languages } from '@/app/i18n/settings' + +acceptLanguage.languages(languages) + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico, sitemap.xml, robots.txt (metadata files) + */ + '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)' + ] +} + +export function middleware(req: NextRequest): NextResponse { + // static file /public/xxx.svg + if ( + req.nextUrl.pathname.endsWith('.svg') || + req.nextUrl.pathname.endsWith('.png') || + req.nextUrl.pathname.endsWith('.ico') + ) { + return NextResponse.next() + } + + if (req.nextUrl.pathname.indexOf('icon') > -1 || req.nextUrl.pathname.indexOf('chrome') > -1) { + return NextResponse.next() + } + + // get language from pathname + let lng = req.nextUrl.pathname.split('/')[1] + + // if pathname does not contain valid language code, use fallback language + if (!languages.includes(lng)) { + lng = fallbackLng + } + + // handle root path and language path redirect + if ( + req.nextUrl.pathname === '/' || + languages.some((lang) => req.nextUrl.pathname === `/${lang}`) + ) { + const newUrl = new URL(`/${lng}/home`, req.url) + return NextResponse.redirect(newUrl) + } + + // handle other paths that need to add language prefix + if (!languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`))) { + const newUrl = new URL(`/${lng}${req.nextUrl.pathname}${req.nextUrl.search}`, req.url) + return NextResponse.redirect(newUrl) + } + + return NextResponse.next() +} diff --git a/frontend/providers/aiproxy/next.config.js b/frontend/providers/aiproxy/next.config.js new file mode 100644 index 00000000000..2b1342bc9ba --- /dev/null +++ b/frontend/providers/aiproxy/next.config.js @@ -0,0 +1,16 @@ +/** @type {import('next').NextConfig} */ + +const path = require('path') + +const nextConfig = { + output: 'standalone', + reactStrictMode: false, + compress: true, + transpilePackages: ['@sealos/ui', 'sealos-desktop-sdk', '@sealos/driver'], + experimental: { + // this includes files from the monorepo base two directories up + outputFileTracingRoot: path.join(__dirname, '../../') + } +} + +module.exports = nextConfig diff --git a/frontend/providers/aiproxy/package.json b/frontend/providers/aiproxy/package.json new file mode 100644 index 00000000000..d4e321884ec --- /dev/null +++ b/frontend/providers/aiproxy/package.json @@ -0,0 +1,45 @@ +{ + "name": "aiproxy", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@sealos/ui": "workspace:*", + "@tanstack/react-query": "^4.35.3", + "@tanstack/react-table": "^8.10.7", + "accept-language": "^3.0.20", + "axios": "^1.7.7", + "date-fns": "^2.30.0", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", + "i18next-resources-to-backend": "^1.2.1", + "immer": "^10.1.1", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-day-picker": "^8.8.2", + "react-dom": "^18", + "react-hook-form": "^7.46.2", + "sealos-desktop-sdk": "workspace:*", + "zustand": "^4.5.4" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.3", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "15.0.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-simple-import-sort": "^12.1.1", + "prettier": "^2.8.8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/frontend/providers/aiproxy/providers/chakra/QueryProvider.tsx b/frontend/providers/aiproxy/providers/chakra/QueryProvider.tsx new file mode 100644 index 00000000000..26e9fae9e23 --- /dev/null +++ b/frontend/providers/aiproxy/providers/chakra/QueryProvider.tsx @@ -0,0 +1,23 @@ +'use client' + +import { useState } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +const QueryProvider = ({ children }: { children: React.ReactNode }) => { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: false, + cacheTime: 0 + } + } + }) + ) + + return {children} +} + +export default QueryProvider diff --git a/frontend/providers/aiproxy/providers/chakra/providers.tsx b/frontend/providers/aiproxy/providers/chakra/providers.tsx new file mode 100644 index 00000000000..87b54a622cf --- /dev/null +++ b/frontend/providers/aiproxy/providers/chakra/providers.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { ReactNode } from 'react'; +import { ChakraProvider } from '@chakra-ui/react'; + +import { theme } from '@/ui/chakraTheme'; + +export function ChakraProviders({ children }: { children: ReactNode }): JSX.Element { + return {children}; +} + +export default ChakraProviders; diff --git a/frontend/providers/aiproxy/providers/i18n/i18nContext.tsx b/frontend/providers/aiproxy/providers/i18n/i18nContext.tsx new file mode 100644 index 00000000000..aea040b0ca7 --- /dev/null +++ b/frontend/providers/aiproxy/providers/i18n/i18nContext.tsx @@ -0,0 +1,16 @@ +'use client'; +import { createContext, useContext } from 'react'; + +const I18nContext = createContext<{ lng: string }>({ lng: 'en' }); + +export const useI18n = (): { lng: string } => useContext(I18nContext); + +export function I18nProvider({ + children, + lng +}: { + children: React.ReactNode; + lng: string; +}): JSX.Element { + return {children}; +} diff --git a/frontend/providers/aiproxy/public/favicon.svg b/frontend/providers/aiproxy/public/favicon.svg new file mode 100644 index 00000000000..0dd4ba6e5ad --- /dev/null +++ b/frontend/providers/aiproxy/public/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/store/backend.ts b/frontend/providers/aiproxy/store/backend.ts new file mode 100644 index 00000000000..a15d4b7e7a4 --- /dev/null +++ b/frontend/providers/aiproxy/store/backend.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface BackendState { + aiproxyBackend: string + setAiproxyBackend: (backend: string) => void +} + +export const useBackendStore = create()( + persist( + (set) => ({ + aiproxyBackend: '', + setAiproxyBackend: (backend) => set({ aiproxyBackend: backend }) + }), + { + name: 'aiproxy-backend-storage' + } + ) +) diff --git a/frontend/providers/aiproxy/tsconfig.json b/frontend/providers/aiproxy/tsconfig.json new file mode 100644 index 00000000000..a3cd4f2ebcf --- /dev/null +++ b/frontend/providers/aiproxy/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "target": "ES2020", + "forceConsistentCasingInFileNames": false, + "paths": { + "@/*": [ + "./*" + ] + }, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "extends": "../../tsconfig.web.json" +} \ No newline at end of file diff --git a/frontend/providers/aiproxy/types/api.d.ts b/frontend/providers/aiproxy/types/api.d.ts new file mode 100644 index 00000000000..71452bae229 --- /dev/null +++ b/frontend/providers/aiproxy/types/api.d.ts @@ -0,0 +1,8 @@ +export interface ApiResp { + code: number + message: string + data?: T +} + +export const isApiResp = (x: unknown): x is ApiResp => + typeof x.code === 'number' && typeof x.message === 'string' diff --git a/frontend/providers/aiproxy/types/appConfig.d.ts b/frontend/providers/aiproxy/types/appConfig.d.ts new file mode 100644 index 00000000000..f96828a80a0 --- /dev/null +++ b/frontend/providers/aiproxy/types/appConfig.d.ts @@ -0,0 +1,15 @@ +export type AppConfigType = { + auth: { + appTokenJwtKey: string + aiProxyBackendKey: string + } + backend: { + aiproxy: string + aiproxyInternal: string + } +} + +declare global { + // eslint-disable-next-line no-var + var AppConfig: AppConfigType | undefined +} diff --git a/frontend/providers/aiproxy/types/backend.d.ts b/frontend/providers/aiproxy/types/backend.d.ts new file mode 100644 index 00000000000..d572c684e7c --- /dev/null +++ b/frontend/providers/aiproxy/types/backend.d.ts @@ -0,0 +1,5 @@ +export interface ModelPrice { + name: string + prompt: number + completion: number +} diff --git a/frontend/providers/aiproxy/types/form.d.ts b/frontend/providers/aiproxy/types/form.d.ts new file mode 100644 index 00000000000..3f5d605bbb5 --- /dev/null +++ b/frontend/providers/aiproxy/types/form.d.ts @@ -0,0 +1,8 @@ +export interface LogForm { + name: string + modelName: string + createdAt: Date + endedAt: Date + page: number + pageSize: number +} diff --git a/frontend/providers/aiproxy/types/front.d.ts b/frontend/providers/aiproxy/types/front.d.ts new file mode 100644 index 00000000000..de83dc1fd21 --- /dev/null +++ b/frontend/providers/aiproxy/types/front.d.ts @@ -0,0 +1,12 @@ +export type ModelIdentifier = + | 'ernie' + | 'qwen' + | 'chatglm' + | 'gpt' // Add these + | 'o' // Add these + | 'deepseek' + | 'moonshot' + | 'sparkdesk' + | 'abab' + | 'glm' + | 'doubao' diff --git a/frontend/providers/aiproxy/types/getKeys.d.ts b/frontend/providers/aiproxy/types/getKeys.d.ts new file mode 100644 index 00000000000..de84e756d30 --- /dev/null +++ b/frontend/providers/aiproxy/types/getKeys.d.ts @@ -0,0 +1,15 @@ +export type TokenInfo = { + key: string + name: string + group: string + subnet: string + models: string[] | null + status: number + id: number + quota: number + used_amount: number + request_count: number + created_at: number + accessed_at: number + expired_at: number +} diff --git a/frontend/providers/aiproxy/types/i18next.d.ts b/frontend/providers/aiproxy/types/i18next.d.ts new file mode 100644 index 00000000000..a1f4c137d25 --- /dev/null +++ b/frontend/providers/aiproxy/types/i18next.d.ts @@ -0,0 +1,12 @@ +import 'i18next'; + +import type common from '../app/i18n/locales/en/common.json'; + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'common'; + resources: { + common: typeof common; + }; + } +} diff --git a/frontend/providers/aiproxy/types/log.d.ts b/frontend/providers/aiproxy/types/log.d.ts new file mode 100644 index 00000000000..35e53134b77 --- /dev/null +++ b/frontend/providers/aiproxy/types/log.d.ts @@ -0,0 +1,25 @@ +export interface LogItem { + code: number + content: string + group: string + model: string + used_amount: number + price: number + completion_price: number + token_id: number + token_name: string + prompt_tokens: number + completion_tokens: number + channel: number + endpoint: string + created_at: number +} + +export interface LogResponse { + data: { + logs: LogItem[] + total: number + } + message: string + success: boolean +} diff --git a/frontend/providers/aiproxy/ui/chakraTheme.ts b/frontend/providers/aiproxy/ui/chakraTheme.ts new file mode 100644 index 00000000000..253954d7925 --- /dev/null +++ b/frontend/providers/aiproxy/ui/chakraTheme.ts @@ -0,0 +1,18 @@ +import { extendTheme } from '@chakra-ui/react' +import { theme as SealosTheme } from '@sealos/ui' + +export const theme = extendTheme(SealosTheme, { + styles: { + global: { + 'html, body': { + color: 'var(--foreground)', + background: 'var(--background)', + fontSize: 'md', + height: '100%', + overflowY: 'auto', + fontWeight: 400, + minWidth: '700px' + } + } + } +}) diff --git a/frontend/providers/aiproxy/ui/icons/home/Icons.tsx b/frontend/providers/aiproxy/ui/icons/home/Icons.tsx new file mode 100644 index 00000000000..54c78739c00 --- /dev/null +++ b/frontend/providers/aiproxy/ui/icons/home/Icons.tsx @@ -0,0 +1,44 @@ +import { Icon, IconProps } from '@chakra-ui/react' + +export const ChainIcon = (props: IconProps) => ( + + + +) + +export const ListIcon = (props: IconProps) => ( + + + + + + + + +) + +// 使用示例: +// diff --git a/frontend/providers/aiproxy/ui/icons/index.tsx b/frontend/providers/aiproxy/ui/icons/index.tsx new file mode 100644 index 00000000000..14c182dd619 --- /dev/null +++ b/frontend/providers/aiproxy/ui/icons/index.tsx @@ -0,0 +1,35 @@ +import { Icon, IconProps } from '@chakra-ui/react' + +export function ToLeftIcon(props: IconProps) { + return ( + + + + ) +} + +export function RightFirstIcon(props: IconProps) { + return ( + + + + ) +} diff --git a/frontend/providers/aiproxy/ui/icons/sidebar/HomeIcon.tsx b/frontend/providers/aiproxy/ui/icons/sidebar/HomeIcon.tsx new file mode 100644 index 00000000000..a2ddb485ae1 --- /dev/null +++ b/frontend/providers/aiproxy/ui/icons/sidebar/HomeIcon.tsx @@ -0,0 +1,28 @@ +import { createIcon } from '@chakra-ui/react'; + +export const ConsoleIcon = createIcon({ + displayName: 'ConsoleIcon', + viewBox: '0 0 24 24', + path: ( + <> + + + + + + ) +}); diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/chatglm.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/chatglm.svg new file mode 100644 index 00000000000..cd5d3cc5bd6 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/chatglm.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/deepseek.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/deepseek.svg new file mode 100644 index 00000000000..b409c0c1373 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/deepseek.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/doubao.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/doubao.svg new file mode 100644 index 00000000000..dfc8f4665a4 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/doubao.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/ernie.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/ernie.svg new file mode 100644 index 00000000000..c64f120991a --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/ernie.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/glm.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/glm.svg new file mode 100644 index 00000000000..1d6e7f5e2fb --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/glm.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/minimax.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/minimax.svg new file mode 100644 index 00000000000..b54567d93e0 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/minimax.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/moonshot.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/moonshot.svg new file mode 100644 index 00000000000..c7cf5679468 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/moonshot.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/openai.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/openai.svg new file mode 100644 index 00000000000..daa761bea0b --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/openai.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/qianwen.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/qianwen.svg new file mode 100644 index 00000000000..e12862d8761 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/qianwen.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/modelist/sparkdesk.svg b/frontend/providers/aiproxy/ui/svg/icons/modelist/sparkdesk.svg new file mode 100644 index 00000000000..5f4036f28fd --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/modelist/sparkdesk.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/home.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/home.svg new file mode 100644 index 00000000000..9fe618a35f7 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/home.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/home_a.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/home_a.svg new file mode 100644 index 00000000000..3ba408e7786 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/home_a.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/logs.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/logs.svg new file mode 100644 index 00000000000..d40349cbd0a --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/logs.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/logs_a.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/logs_a.svg new file mode 100644 index 00000000000..69879256e10 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/logs_a.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/price.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/price.svg new file mode 100644 index 00000000000..4a014d509e7 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/price.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/ui/svg/icons/sidebar/price_a.svg b/frontend/providers/aiproxy/ui/svg/icons/sidebar/price_a.svg new file mode 100644 index 00000000000..439cb2df3f9 --- /dev/null +++ b/frontend/providers/aiproxy/ui/svg/icons/sidebar/price_a.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/aiproxy/utils/auth.ts b/frontend/providers/aiproxy/utils/auth.ts new file mode 100644 index 00000000000..59e9e2c0584 --- /dev/null +++ b/frontend/providers/aiproxy/utils/auth.ts @@ -0,0 +1,39 @@ +import jwt from 'jsonwebtoken' + +// Token payload 类型定义 +interface AppTokenPayload { + workspaceUid: string + workspaceId: string + regionUid: string + userCrUid: string + userCrName: string + userId: string + userUid: string + iat: number + exp: number +} + +export async function parseJwtToken(headers: Headers): Promise { + try { + const token = headers.get('authorization') + if (!token) { + return Promise.reject('Token is missing') + } + + const decoded = jwt.verify( + token, + global.AppConfig?.auth.appTokenJwtKey || '' + ) as AppTokenPayload + const now = Math.floor(Date.now() / 1000) + if (decoded.exp && decoded.exp < now) { + return Promise.reject('Token expired') + } + if (!decoded.workspaceId) { + return Promise.reject('Invalid token') + } + return decoded.workspaceId + } catch (error) { + console.error('Token parsing error:', error) + return Promise.reject('Invalid token') + } +} diff --git a/frontend/providers/aiproxy/utils/request.ts b/frontend/providers/aiproxy/utils/request.ts new file mode 100644 index 00000000000..f559b7e801b --- /dev/null +++ b/frontend/providers/aiproxy/utils/request.ts @@ -0,0 +1,96 @@ +import { ApiResp } from '@/types/api' +import axios, { + InternalAxiosRequestConfig, + AxiosHeaders, + AxiosResponse, + AxiosRequestConfig +} from 'axios' +import { getUserSession } from './user' + +const request = axios.create({ + baseURL: '/', + withCredentials: true, + timeout: 60000 +}) + +// request interceptor +request.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // auto append service prefix + if (config.url && !config.url?.startsWith('/api/')) { + config.url = '' + config.url + } + let _headers: AxiosHeaders = config.headers + + //获取token,并将其添加至请求头中 + _headers['Authorization'] = getUserSession() + + if (!config.headers || config.headers['Content-Type'] === '') { + _headers['Content-Type'] = 'application/json' + } + + config.headers = _headers + return config + }, + (error: any) => { + error.data = {} + error.data.msg = '服务器异常,请联系管理员!' + return Promise.resolve(error) + } +) + +// response interceptor +request.interceptors.response.use( + (response: AxiosResponse) => { + const { status, data } = response + if (status < 200 || status >= 300) { + return Promise.reject(status + ', ' + typeof data === 'string' ? data : String(data)) + } + + const apiResp = data as ApiResp + if (apiResp.code < 200 || apiResp.code >= 400) { + return Promise.reject(apiResp.code + ':' + apiResp.message) + } + + response.data = apiResp.data + return response.data + }, + (error: any) => { + if (axios.isCancel(error)) { + return Promise.reject('cancel request' + String(error)) + } else { + error.errMessage = '请求超时或服务器异常,请检查网络或联系管理员!' + } + return Promise.reject(error) + } +) + +export function GET( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.get(url, { + params: data, + ...config + }) +} + +export function POST( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.post(url, data, config) +} + +export function DELETE( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.delete(url, { + params: data, + ...config + }) +} diff --git a/frontend/providers/aiproxy/utils/user.ts b/frontend/providers/aiproxy/utils/user.ts new file mode 100644 index 00000000000..c60c02883dc --- /dev/null +++ b/frontend/providers/aiproxy/utils/user.ts @@ -0,0 +1,14 @@ +export const getUserSession = () => { + let token: string = + process.env.NODE_ENV === 'development' ? process.env.NEXT_PUBLIC_MOCK_USER || '' : '' + + try { + const store = localStorage.getItem('session') + if (!token && store) { + token = JSON.parse(store)?.token + } + } catch (err) { + err + } + return token +}