From 8a9bc868138be6d0f8df41ea6a50694485adb156 Mon Sep 17 00:00:00 2001 From: lim Date: Thu, 31 Oct 2024 07:30:26 +0000 Subject: [PATCH 01/47] ok --- frontend/providers/aiproxy/.eslintrc.json | 48 ++++ frontend/providers/aiproxy/.gitignore | 40 +++ .../providers/aiproxy/.vscode/extensions.json | 12 + .../providers/aiproxy/.vscode/settings.json | 54 ++++ frontend/providers/aiproxy/README.md | 36 +++ .../aiproxy/app/[lng]/(user)/home/page.tsx | 37 +++ .../aiproxy/app/[lng]/(user)/layout.tsx | 24 ++ .../aiproxy/app/[lng]/(user)/logs/page.tsx | 3 + .../providers/aiproxy/app/[lng]/globals.css | 21 ++ .../providers/aiproxy/app/[lng]/layout.tsx | 51 ++++ frontend/providers/aiproxy/app/i18n/client.ts | 66 +++++ .../aiproxy/app/i18n/locales/en/common.json | 18 ++ .../aiproxy/app/i18n/locales/zh/common.json | 18 ++ frontend/providers/aiproxy/app/i18n/server.ts | 39 +++ .../providers/aiproxy/app/i18n/settings.ts | 28 ++ .../aiproxy/components/i18n-switch/Client.tsx | 0 .../aiproxy/components/i18n-switch/Server.tsx | 0 .../aiproxy/components/user/KeyList.tsx | 247 ++++++++++++++++++ .../aiproxy/components/user/Sidebar.tsx | 113 ++++++++ frontend/providers/aiproxy/middleware.ts | 68 +++++ frontend/providers/aiproxy/next.config.ts | 7 + frontend/providers/aiproxy/package.json | 39 +++ frontend/providers/aiproxy/postcss.config.mjs | 8 + .../aiproxy/providers/chakra/providers.tsx | 12 + .../aiproxy/providers/i18n/i18nContext.tsx | 16 ++ frontend/providers/aiproxy/public/file.svg | 1 + frontend/providers/aiproxy/public/globe.svg | 1 + frontend/providers/aiproxy/public/next.svg | 1 + frontend/providers/aiproxy/public/vercel.svg | 1 + frontend/providers/aiproxy/public/window.svg | 1 + frontend/providers/aiproxy/tailwind.config.ts | 19 ++ frontend/providers/aiproxy/tsconfig.json | 42 +++ frontend/providers/aiproxy/types/i18next.d.ts | 12 + frontend/providers/aiproxy/ui/chakraTheme.ts | 18 ++ .../aiproxy/ui/icons/sidebar/HomeIcon.tsx | 28 ++ .../aiproxy/ui/svg/icons/sidebar/home.svg | 6 + .../aiproxy/ui/svg/icons/sidebar/home_a.svg | 6 + .../aiproxy/ui/svg/icons/sidebar/logs.svg | 3 + .../aiproxy/ui/svg/icons/sidebar/logs_a.svg | 3 + 39 files changed, 1147 insertions(+) create mode 100644 frontend/providers/aiproxy/.eslintrc.json create mode 100644 frontend/providers/aiproxy/.gitignore create mode 100644 frontend/providers/aiproxy/.vscode/extensions.json create mode 100644 frontend/providers/aiproxy/.vscode/settings.json create mode 100644 frontend/providers/aiproxy/README.md create mode 100644 frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx create mode 100644 frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx create mode 100644 frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx create mode 100644 frontend/providers/aiproxy/app/[lng]/globals.css create mode 100644 frontend/providers/aiproxy/app/[lng]/layout.tsx create mode 100644 frontend/providers/aiproxy/app/i18n/client.ts create mode 100644 frontend/providers/aiproxy/app/i18n/locales/en/common.json create mode 100644 frontend/providers/aiproxy/app/i18n/locales/zh/common.json create mode 100644 frontend/providers/aiproxy/app/i18n/server.ts create mode 100644 frontend/providers/aiproxy/app/i18n/settings.ts create mode 100644 frontend/providers/aiproxy/components/i18n-switch/Client.tsx create mode 100644 frontend/providers/aiproxy/components/i18n-switch/Server.tsx create mode 100644 frontend/providers/aiproxy/components/user/KeyList.tsx create mode 100644 frontend/providers/aiproxy/components/user/Sidebar.tsx create mode 100644 frontend/providers/aiproxy/middleware.ts create mode 100644 frontend/providers/aiproxy/next.config.ts create mode 100644 frontend/providers/aiproxy/package.json create mode 100644 frontend/providers/aiproxy/postcss.config.mjs create mode 100644 frontend/providers/aiproxy/providers/chakra/providers.tsx create mode 100644 frontend/providers/aiproxy/providers/i18n/i18nContext.tsx create mode 100644 frontend/providers/aiproxy/public/file.svg create mode 100644 frontend/providers/aiproxy/public/globe.svg create mode 100644 frontend/providers/aiproxy/public/next.svg create mode 100644 frontend/providers/aiproxy/public/vercel.svg create mode 100644 frontend/providers/aiproxy/public/window.svg create mode 100644 frontend/providers/aiproxy/tailwind.config.ts create mode 100644 frontend/providers/aiproxy/tsconfig.json create mode 100644 frontend/providers/aiproxy/types/i18next.d.ts create mode 100644 frontend/providers/aiproxy/ui/chakraTheme.ts create mode 100644 frontend/providers/aiproxy/ui/icons/sidebar/HomeIcon.tsx create mode 100644 frontend/providers/aiproxy/ui/svg/icons/sidebar/home.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/sidebar/home_a.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/sidebar/logs.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/sidebar/logs_a.svg diff --git a/frontend/providers/aiproxy/.eslintrc.json b/frontend/providers/aiproxy/.eslintrc.json new file mode 100644 index 00000000000..aee27700638 --- /dev/null +++ b/frontend/providers/aiproxy/.eslintrc.json @@ -0,0 +1,48 @@ +{ + "extends": [ + "next/core-web-vitals", + "next/typescript", + "prettier" + ], + "plugins": [ + "simple-import-sort", + "import" + ], + "rules": { + // Explicitly specify return types for exported functions and classes + "@typescript-eslint/explicit-module-boundary-types": "error", + "simple-import-sort/imports": [ + "error", + { + "groups": [ + [ + "^react", + "^@?\\w" + ], // React 和第三方包 + [ + "^(@|components)(/.*|$)" + ], // 内部别名导入 + [ + "^\\u0000" + ], // Side effect imports + [ + "^\\.\\.(?!/?$)", + "^\\.\\./?$" + ], // 父级目录导入 + [ + "^\\./(?=.*/)(?!/?$)", + "^\\.(?!/?$)", + "^\\./?$" + ], // 同级目录导入 + [ + "^.+\\.?(css)$" + ] // 样式文件 + ] + } + ], + "simple-import-sort/exports": "error", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error" + } +} \ No newline at end of file diff --git a/frontend/providers/aiproxy/.gitignore b/frontend/providers/aiproxy/.gitignore new file mode 100644 index 00000000000..26b002aac1d --- /dev/null +++ b/frontend/providers/aiproxy/.gitignore @@ -0,0 +1,40 @@ +# 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 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..ab270616b52 --- /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/app/[lng]/(user)/home/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx new file mode 100644 index 00000000000..6a4458d3a84 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx @@ -0,0 +1,37 @@ +import { Flex } from '@chakra-ui/react'; + +import KeyList from '@/components/user/KeyList'; + +export default function Home(): JSX.Element { + return ( + + + + + + + ddddxx + + + ); +} 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..25ef271e32a --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -0,0 +1,24 @@ +import { Box, Flex } from '@chakra-ui/react'; + +import SideBar from '@/components/user/Sidebar'; + +export default async function UserLayout({ + children, + params +}: { + children: React.ReactNode; + params: { lng: string }; +}): Promise { + const { lng } = await params; + return ( + + {/* Left Sidebar */} + + + + + {/* 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..45169c1c69e --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -0,0 +1,3 @@ +export default function Home(): React.JSX.Element { + return
logs
; +} diff --git a/frontend/providers/aiproxy/app/[lng]/globals.css b/frontend/providers/aiproxy/app/[lng]/globals.css new file mode 100644 index 00000000000..fae00d6436d --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/globals.css @@ -0,0 +1,21 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #f4f4f7; + --foreground: #111824; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #f4f4f7; + --foreground: #111824; + } +} + +body { + color: var(--foreground); + background: var(--background); + /* font-family: Arial, Helvetica, sans-serif; */ +} diff --git a/frontend/providers/aiproxy/app/[lng]/layout.tsx b/frontend/providers/aiproxy/app/[lng]/layout.tsx new file mode 100644 index 00000000000..fa19c018961 --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/layout.tsx @@ -0,0 +1,51 @@ +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 './globals.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 { + title: t('title'), + description: t('description') + }; +} + +export default async function RootLayout({ + children, + params +}: Readonly<{ + children: React.ReactNode; + params: { + lng: string; + }; +}>): Promise { + const { lng } = await params; + return ( + + + + {children} + + + + ); +} diff --git a/frontend/providers/aiproxy/app/i18n/client.ts b/frontend/providers/aiproxy/app/i18n/client.ts new file mode 100644 index 00000000000..5ec1cd087b2 --- /dev/null +++ b/frontend/providers/aiproxy/app/i18n/client.ts @@ -0,0 +1,66 @@ +'use client'; + +import { useEffect } from 'react'; +import { + FallbackNs, + initReactI18next, + useTranslation as useTranslationOrg, + 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', 'cookie', 'navigator'] + }, + preload: runsOnServerSide ? languages : [] + }); + +export function useTranslationClientSide< + Ns extends FlatNamespace, + KPrefix extends KeyPrefix> = undefined +>( + lng: string, + ns?: Ns, + options?: UseTranslationOptions +): UseTranslationResponse, KPrefix> { + const ret = useTranslationOrg(ns, options); + const { i18n } = ret; + + // server side handle + if (runsOnServerSide) { + if (lng && i18n.resolvedLanguage !== lng) { + i18n.changeLanguage(lng); + } + return ret; + } + + // client side handle + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (!lng || i18n.resolvedLanguage === lng) { + return; + } + i18n.changeLanguage(lng); + localStorage.setItem('userLanguage', lng); + }, [lng, i18n, i18n.resolvedLanguage]); + + 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..e218e743f0f --- /dev/null +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -0,0 +1,18 @@ +{ + "title": "aiproxy", + "description": "ai agent", + "Sidebar": { + "Home": "AI Proxy", + "Logs": "call log" + }, + "keyList": { + "title": "AI Proxy" + }, + "key": { + "key": "API Key", + "name": "name", + "createdAt": "creation time", + "lastUsedAt": "last use time", + "status": "state" + } +} 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..a78bc959748 --- /dev/null +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -0,0 +1,18 @@ +{ + "title": "ai代理", + "description": "ai 代理", + "Sidebar": { + "Home": "AI Proxy", + "Logs": "调用日志" + }, + "keyList": { + "title": "AI Proxy" + }, + "key": { + "key": "API Key", + "name": "名称", + "createdAt": "创建时间", + "lastUsedAt": "最后使用时间", + "status": "状态" + } +} 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/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/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx new file mode 100644 index 00000000000..ce2b4a8f26d --- /dev/null +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -0,0 +1,247 @@ +'use client'; +import { useState } from 'react'; +import { ArrowDownIcon, ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; +import { + Box, + Button, + Flex, + HStack, + Select, + Table, + Tag, + Tbody, + Td, + Text, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import { + Column, + createColumnHelper, + flexRender, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable +} from '@tanstack/react-table'; +import { TFunction } from 'i18next'; + +import { useTranslationClientSide } from '@/app/i18n/client'; +import { useI18n } from '@/providers/i18n/i18nContext'; + +export function KeyList(): JSX.Element { + const { lng } = useI18n(); + const { t } = useTranslationClientSide(lng, 'common'); + return ( + <> + + + {t('keyList.title')} + + + + + + + ); +} + +function KeyItem({ t }: { t: TFunction }): JSX.Element { + return ; +} + +// 1. 定义数据类型 +type KeyItem = { + id: number; + name: string; + key: string; + createdAt: string; + lastUsedAt: string; + status: 'active' | 'inactive'; +}; + +export enum TableHeaderId { + NAME = 'key.name', + KEY = 'key.key', + CREATED_AT = 'key.createdAt', + LAST_USED_AT = 'key.lastUsedAt', + STATUS = 'key.status' +} + +// 2. 自定义表头组件 +const CustomHeader = ({ column, t }: { column: Column; t: TFunction }) => { + console.log(column); + return ( + + {t(column.id)} + {column.getIsSorted() && + { + asc: , + desc: + }[column.getIsSorted() as string]} + + ); +}; + +const TableDemo = ({ t }: { t: TFunction }) => { + const [data] = useState([ + { + id: 1, + name: '1234567890', + key: '1234567890', + createdAt: '2021-01-01', + lastUsedAt: '2021-01-01', + status: 'active' + }, + { + id: 2, + name: '1234567890', + key: '1234567890', + createdAt: '2021-01-01', + lastUsedAt: '2021-01-01', + status: 'inactive' + } + ]); + + const [sorting, setSorting] = useState([]); + + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor((row) => row.name, { + id: TableHeaderId.NAME, + header: (props) => , + cell: (info) => { + return info.getValue(); + } + }), + columnHelper.accessor((row) => row.key, { + id: TableHeaderId.KEY, + header: (props) => , + cell: (info) => info.getValue() + }), + columnHelper.accessor((row) => row.createdAt, { + id: TableHeaderId.CREATED_AT, + header: (props) => , + cell: (info) => info.getValue() + }), + columnHelper.accessor((row) => row.lastUsedAt, { + id: TableHeaderId.LAST_USED_AT, + header: (props) => , + cell: (info) => info.getValue() + }), + columnHelper.accessor((row) => row.status, { + id: TableHeaderId.STATUS, + header: (props) => , + cell: (info) => ( + + {t(`status.${info.getValue()}`)} + + ) + }) + ]; + + const table = useReactTable({ + data, + columns, + state: { + sorting + }, + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getCoreRowModel: getCoreRowModel() + }); + + return ( + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ + + + + + + + + {t('pagination.page')} + + {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} + + + + + +
+ ); +}; + +export default KeyList; diff --git a/frontend/providers/aiproxy/components/user/Sidebar.tsx b/frontend/providers/aiproxy/components/user/Sidebar.tsx new file mode 100644 index 00000000000..f1e0427cf74 --- /dev/null +++ b/frontend/providers/aiproxy/components/user/Sidebar.tsx @@ -0,0 +1,113 @@ +'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'; + +type Menu = { + id: string; + url: string; + value: string; + icon: StaticImageData; + activeIcon: StaticImageData; + display: boolean; +}; + +const SideBar = ({ lng }: { lng: string }): JSX.Element => { + const pathname = usePathname(); + 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 + } + ]; + + return ( + + {menus + .filter((menu) => menu.display) + .map((menu) => { + const fullUrl = `/${lng}${menu.url}`; + const isActive = pathname === fullUrl; + + return ( + + { + const img = e.currentTarget.querySelector('img'); + if (img) { + img.src = menu.activeIcon.src; + } + }} + onMouseLeave={(e) => { + const img = e.currentTarget.querySelector('img'); + if (img && !isActive) { + img.src = menu.icon.src; + } + }} + > + {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..d9dd14fba49 --- /dev/null +++ b/frontend/providers/aiproxy/middleware.ts @@ -0,0 +1,68 @@ +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 (favicon file) + */ + { + source: '/((?!api|_next/static|_next/image|favicon.ico).*)' + }, + + // 排除 assets路径 + { + source: '/((?!assets/).*)' + }, + + // 排除特定文件 + '/((?!sw\\.js).*)', + '/((?!site\\.webmanifest).*)', + + // 排除以特定字符串开头的路径 + '/((?!icon/).*)', + '/((?!chrome/).*)' + ] +}; + +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(); + + let lng: string | undefined | null; + lng = acceptLanguage.get(req.headers.get('Accept-Language')); + if (!lng) lng = fallbackLng; + + if (req.nextUrl.pathname === '/' || req.nextUrl.pathname === '/zh') { + const newUrl = new URL(`/${lng}/home`, req.url); + return NextResponse.redirect(newUrl); + } + + // Redirect if lng in path is not supported + if ( + !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) && + !req.nextUrl.pathname.startsWith('/_next') + ) { + 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.ts b/frontend/providers/aiproxy/next.config.ts new file mode 100644 index 00000000000..e9ffa3083ad --- /dev/null +++ b/frontend/providers/aiproxy/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/frontend/providers/aiproxy/package.json b/frontend/providers/aiproxy/package.json new file mode 100644 index 00000000000..3c8f3baf1b8 --- /dev/null +++ b/frontend/providers/aiproxy/package.json @@ -0,0 +1,39 @@ +{ + "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-table": "^8.10.7", + "accept-language": "^3.0.20", + "axios": "^1.7.7", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", + "i18next-resources-to-backend": "^1.2.1", + "next": "15.0.1", + "react": "19.0.0-rc-69d4b800-20241021", + "react-dom": "19.0.0-rc-69d4b800-20241021", + "sealos-desktop-sdk": "workspace:^", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@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", + "postcss": "^8", + "prettier": "^2.8.8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/frontend/providers/aiproxy/postcss.config.mjs b/frontend/providers/aiproxy/postcss.config.mjs new file mode 100644 index 00000000000..1a69fd2a450 --- /dev/null +++ b/frontend/providers/aiproxy/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; 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/file.svg b/frontend/providers/aiproxy/public/file.svg new file mode 100644 index 00000000000..004145cddf3 --- /dev/null +++ b/frontend/providers/aiproxy/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/public/globe.svg b/frontend/providers/aiproxy/public/globe.svg new file mode 100644 index 00000000000..567f17b0d7c --- /dev/null +++ b/frontend/providers/aiproxy/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/public/next.svg b/frontend/providers/aiproxy/public/next.svg new file mode 100644 index 00000000000..5174b28c565 --- /dev/null +++ b/frontend/providers/aiproxy/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/public/vercel.svg b/frontend/providers/aiproxy/public/vercel.svg new file mode 100644 index 00000000000..77053960334 --- /dev/null +++ b/frontend/providers/aiproxy/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/public/window.svg b/frontend/providers/aiproxy/public/window.svg new file mode 100644 index 00000000000..b2b2a44f6eb --- /dev/null +++ b/frontend/providers/aiproxy/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/aiproxy/tailwind.config.ts b/frontend/providers/aiproxy/tailwind.config.ts new file mode 100644 index 00000000000..d43da912d03 --- /dev/null +++ b/frontend/providers/aiproxy/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/frontend/providers/aiproxy/tsconfig.json b/frontend/providers/aiproxy/tsconfig.json new file mode 100644 index 00000000000..071cf6a7996 --- /dev/null +++ b/frontend/providers/aiproxy/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file 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/ui/chakraTheme.ts b/frontend/providers/aiproxy/ui/chakraTheme.ts new file mode 100644 index 00000000000..cb578698026 --- /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/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/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 From 9ec6cafab47858767ee201cd83c13600230ed049 Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Thu, 31 Oct 2024 17:20:50 +0800 Subject: [PATCH 02/47] update package --- frontend/providers/aiproxy/.eslintrc.json | 47 +-- frontend/providers/aiproxy/.prettierignore | 4 + frontend/providers/aiproxy/.prettierrc.json | 18 ++ .../aiproxy/app/[lng]/(user)/logs/page.tsx | 117 ++++++- .../providers/aiproxy/app/[lng]/globals.css | 169 +++++++++- .../providers/aiproxy/app/[lng]/layout.tsx | 42 +-- .../aiproxy/app/i18n/locales/en/common.json | 3 + .../aiproxy/app/i18n/locales/zh/common.json | 7 + .../components/SelectDateRange/index.tsx | 291 ++++++++++++++++++ frontend/providers/aiproxy/next.config.js | 16 + frontend/providers/aiproxy/next.config.ts | 7 - frontend/providers/aiproxy/package.json | 16 +- frontend/providers/aiproxy/postcss.config.mjs | 8 - frontend/providers/aiproxy/tailwind.config.ts | 19 -- frontend/providers/aiproxy/tsconfig.json | 35 +-- frontend/providers/aiproxy/types/form.d.ts | 8 + frontend/providers/aiproxy/types/log.d.ts | 26 ++ 17 files changed, 689 insertions(+), 144 deletions(-) create mode 100644 frontend/providers/aiproxy/.prettierignore create mode 100644 frontend/providers/aiproxy/.prettierrc.json create mode 100644 frontend/providers/aiproxy/components/SelectDateRange/index.tsx create mode 100644 frontend/providers/aiproxy/next.config.js delete mode 100644 frontend/providers/aiproxy/next.config.ts delete mode 100644 frontend/providers/aiproxy/postcss.config.mjs delete mode 100644 frontend/providers/aiproxy/tailwind.config.ts create mode 100644 frontend/providers/aiproxy/types/form.d.ts create mode 100644 frontend/providers/aiproxy/types/log.d.ts diff --git a/frontend/providers/aiproxy/.eslintrc.json b/frontend/providers/aiproxy/.eslintrc.json index aee27700638..0e81f9b97c8 100644 --- a/frontend/providers/aiproxy/.eslintrc.json +++ b/frontend/providers/aiproxy/.eslintrc.json @@ -1,48 +1,3 @@ { - "extends": [ - "next/core-web-vitals", - "next/typescript", - "prettier" - ], - "plugins": [ - "simple-import-sort", - "import" - ], - "rules": { - // Explicitly specify return types for exported functions and classes - "@typescript-eslint/explicit-module-boundary-types": "error", - "simple-import-sort/imports": [ - "error", - { - "groups": [ - [ - "^react", - "^@?\\w" - ], // React 和第三方包 - [ - "^(@|components)(/.*|$)" - ], // 内部别名导入 - [ - "^\\u0000" - ], // Side effect imports - [ - "^\\.\\.(?!/?$)", - "^\\.\\./?$" - ], // 父级目录导入 - [ - "^\\./(?=.*/)(?!/?$)", - "^\\.(?!/?$)", - "^\\./?$" - ], // 同级目录导入 - [ - "^.+\\.?(css)$" - ] // 样式文件 - ] - } - ], - "simple-import-sort/exports": "error", - "import/first": "error", - "import/newline-after-import": "error", - "import/no-duplicates": "error" - } + "extends": "next/core-web-vitals" } \ 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/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index 45169c1c69e..12528722c0b 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -1,3 +1,118 @@ +'use client' + +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { Box, Flex, Grid, Input, Select } from '@chakra-ui/react' +import { MySelect } from '@sealos/ui' + +import { useTranslationClientSide } from '@/app/i18n/client' +import SelectDateRange from '@/components/SelectDateRange' +import { useI18n } from '@/providers/i18n/i18nContext' +import { LogForm } from '@/types/form' + +const mockModals = ['gpt-3.5-turbo', 'gpt-4o-mini', 'gpt-4'] + +const mockNames = [ + { + id: 1, + group: 'ns-admin', + key: 'ngjLFEFQaEudGOFKA2E6Cc64239644BcA045E57c9eE721F9', + status: 1, + name: 'test token', + quota: 0, + used_amount: 0, + request_count: 0, + models: null, + subnet: '', + created_at: 1729672144913, + accessed_at: -62135596800000, + expired_at: -62135596800000 + } +] + +const mockStatus = ['success', 'failed'] + export default function Home(): React.JSX.Element { - return
logs
; + 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 { getValues, setValue } = useForm({ + defaultValues: { + name: '', + modelName: '', + createdAt: new Date(), + endedAt: new Date(), + page: 1, + pageSize: 10 + } + }) + + return ( + + + + {t('logs.call_log')} + + + + {t('logs.name')} + ({ + value: item.name, + label: item.name + }))} + onchange={(val: string) => setValue('name', val)} + /> + + + + {t('logs.modal')} + ({ + value: item, + label: item + }))} + onchange={(val: string) => setValue('modelName', val)} + /> + + + + {t('logs.status')} + {/* ({ + value: item, + label: item + }))} + onchange={(val: string) => setValue('status', val)} + /> */} + + + {t('logs.time')} + + + + + + ) } diff --git a/frontend/providers/aiproxy/app/[lng]/globals.css b/frontend/providers/aiproxy/app/[lng]/globals.css index fae00d6436d..0c6c75c67c5 100644 --- a/frontend/providers/aiproxy/app/[lng]/globals.css +++ b/frontend/providers/aiproxy/app/[lng]/globals.css @@ -1,21 +1,162 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +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; +} -:root { - --background: #f4f4f7; - --foreground: #111824; +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; } -@media (prefers-color-scheme: dark) { - :root { - --background: #f4f4f7; - --foreground: #111824; +.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; + } } } -body { - color: var(--foreground); - background: var(--background); - /* font-family: Arial, Helvetica, sans-serif; */ +.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; + /* 深色主题颜色 */ + --rdp-accent-color-dark: #3003e1; + --rdp-background-color-dark: #180270; + /* 聚焦元素的轮廓边框 */ + --rdp-outline: 2px solid var(--rdp-accent-color); + /* 聚焦且选中元素的轮廓边框 */ + --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 index fa19c018961..5673c1e2742 100644 --- a/frontend/providers/aiproxy/app/[lng]/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/layout.tsx @@ -1,44 +1,48 @@ -import { dir } from 'i18next'; -import type { Metadata } from 'next'; +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 { 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 './globals.css'; +import './globals.css' +import 'react-day-picker/dist/style.css' + +import { EVENT_NAME } from 'sealos-desktop-sdk' +import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' export async function generateStaticParams(): Promise<{ lng: string }[]> { - return languages.map((lng) => ({ lng })); + return languages.map((lng) => ({ lng })) } export async function generateMetadata({ params }: { params: { - lng: string; - }; + lng: string + } }): Promise { - let { lng } = await params; - if (languages.indexOf(lng) < 0) lng = fallbackLng; + 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'); + const { t } = await useTranslationServerSide(lng, 'common') return { title: t('title'), description: t('description') - }; + } } export default async function RootLayout({ children, params }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode params: { - lng: string; - }; + lng: string + } }>): Promise { - const { lng } = await params; + const { lng } = await params return ( @@ -47,5 +51,5 @@ export default async function RootLayout({ - ); + ) } diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index e218e743f0f..e9aea429475 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -14,5 +14,8 @@ "createdAt": "creation time", "lastUsedAt": "last use time", "status": "state" + }, + "logs": { + "call_log": "call log" } } diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index a78bc959748..20082180b66 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -14,5 +14,12 @@ "createdAt": "创建时间", "lastUsedAt": "最后使用时间", "status": "状态" + }, + "logs": { + "call_log": "调用日志", + "name": "名称", + "modal": "模型", + "status": "状态", + "time": "时间" } } diff --git a/frontend/providers/aiproxy/components/SelectDateRange/index.tsx b/frontend/providers/aiproxy/components/SelectDateRange/index.tsx new file mode 100644 index 00000000000..ecaed02150d --- /dev/null +++ b/frontend/providers/aiproxy/components/SelectDateRange/index.tsx @@ -0,0 +1,291 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +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) => { + console.log(range, selectedRange); + 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/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/next.config.ts b/frontend/providers/aiproxy/next.config.ts deleted file mode 100644 index e9ffa3083ad..00000000000 --- a/frontend/providers/aiproxy/next.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - /* config options here */ -}; - -export default nextConfig; diff --git a/frontend/providers/aiproxy/package.json b/frontend/providers/aiproxy/package.json index 3c8f3baf1b8..74077e0cde9 100644 --- a/frontend/providers/aiproxy/package.json +++ b/frontend/providers/aiproxy/package.json @@ -13,15 +13,20 @@ "@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", - "next": "15.0.1", - "react": "19.0.0-rc-69d4b800-20241021", - "react-dom": "19.0.0-rc-69d4b800-20241021", + "next": "14.2.5", + "react": "^18", + "jsonwebtoken": "^9.0.2", + "react-dom": "^18", + "react-day-picker": "^8.8.2", + "react-hook-form": "^7.46.2", "sealos-desktop-sdk": "workspace:^", - "zustand": "^5.0.0" + "immer": "^10.1.1", + "zustand": "^4.5.4" }, "devDependencies": { "@types/node": "^20", @@ -31,9 +36,8 @@ "eslint-config-next": "15.0.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-simple-import-sort": "^12.1.1", - "postcss": "^8", "prettier": "^2.8.8", "tailwindcss": "^3.4.1", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/frontend/providers/aiproxy/postcss.config.mjs b/frontend/providers/aiproxy/postcss.config.mjs deleted file mode 100644 index 1a69fd2a450..00000000000 --- a/frontend/providers/aiproxy/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; diff --git a/frontend/providers/aiproxy/tailwind.config.ts b/frontend/providers/aiproxy/tailwind.config.ts deleted file mode 100644 index d43da912d03..00000000000 --- a/frontend/providers/aiproxy/tailwind.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Config } from "tailwindcss"; - -const config: Config = { - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, - }, - plugins: [], -}; -export default config; diff --git a/frontend/providers/aiproxy/tsconfig.json b/frontend/providers/aiproxy/tsconfig.json index 071cf6a7996..a3cd4f2ebcf 100644 --- a/frontend/providers/aiproxy/tsconfig.json +++ b/frontend/providers/aiproxy/tsconfig.json @@ -1,42 +1,29 @@ { "compilerOptions": { - "target": "ES2017", + "baseUrl": ".", "lib": [ "dom", "dom.iterable", "esnext" ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "baseUrl": ".", + "target": "ES2020", + "forceConsistentCasingInFileNames": false, "paths": { "@/*": [ "./*" ] - } + }, + "plugins": [ + { + "name": "next" + } + ] }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts", - "types/**/*.ts" + ".next/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "extends": "../../tsconfig.web.json" } \ No newline at end of file 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/log.d.ts b/frontend/providers/aiproxy/types/log.d.ts new file mode 100644 index 00000000000..521a81cb0db --- /dev/null +++ b/frontend/providers/aiproxy/types/log.d.ts @@ -0,0 +1,26 @@ +export interface LogItem { + id: number; + 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; +} From 4003294016eab7b6659a12b9af1791b36df392da Mon Sep 17 00:00:00 2001 From: lim Date: Thu, 31 Oct 2024 11:31:25 +0000 Subject: [PATCH 03/47] ok --- .../aiproxy/app/api/user/get-keys.ts | 127 ++++++++++ .../aiproxy/app/api/user/get-logs.ts | 164 +++++++++++++ .../aiproxy/app/api/user/get-modes.ts | 61 +++++ .../aiproxy/app/api/user/init-app-config.ts | 65 +++++ .../aiproxy/components/user/KeyList.tsx | 228 ++++++++++-------- .../providers/aiproxy/mock/config.local.yaml | 5 + frontend/providers/aiproxy/types/api.d.ts | 9 + .../providers/aiproxy/types/appConfig.d.ts | 14 ++ frontend/providers/aiproxy/utils/auth.ts | 36 +++ frontend/providers/aiproxy/utils/request.ts | 65 +++++ 10 files changed, 668 insertions(+), 106 deletions(-) create mode 100644 frontend/providers/aiproxy/app/api/user/get-keys.ts create mode 100644 frontend/providers/aiproxy/app/api/user/get-logs.ts create mode 100644 frontend/providers/aiproxy/app/api/user/get-modes.ts create mode 100644 frontend/providers/aiproxy/app/api/user/init-app-config.ts create mode 100644 frontend/providers/aiproxy/mock/config.local.yaml create mode 100644 frontend/providers/aiproxy/types/api.d.ts create mode 100644 frontend/providers/aiproxy/types/appConfig.d.ts create mode 100644 frontend/providers/aiproxy/utils/auth.ts create mode 100644 frontend/providers/aiproxy/utils/request.ts diff --git a/frontend/providers/aiproxy/app/api/user/get-keys.ts b/frontend/providers/aiproxy/app/api/user/get-keys.ts new file mode 100644 index 00000000000..b2297b22b58 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/get-keys.ts @@ -0,0 +1,127 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { parseJwtToken } from '@/utils/auth'; + +interface 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; +} + +interface SearchResponse { + data: { + tokens: TokenInfo[]; + total: number; + }; + message: string; + success: boolean; +} + +function validateParams(group: string, page: number, perPage: number): string | null { + if (!group) { + return 'Group parameter is required'; + } + + 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.aiproxy); + url.searchParams.append('p', page.toString()); + url.searchParams.append('per_page', perPage.toString()); + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + 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 { + tokens: result.data.tokens, + 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('p') || '1', 10); + const perPage = parseInt(searchParams.get('per_page') || '10', 10); + + const validationError = validateParams(group, 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/user/get-logs.ts b/frontend/providers/aiproxy/app/api/user/get-logs.ts new file mode 100644 index 00000000000..6ef872c093d --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/get-logs.ts @@ -0,0 +1,164 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { parseJwtToken } from '@/utils/auth'; + +interface LogInfo { + token_name: string; + endpoint: string; + content: string; + group: string; + model: string; + price: number; + id: number; + completion_price: number; + token_id: number; + used_amount: number; + prompt_tokens: number; + completion_tokens: number; + channel: number; + code: number; + created_at: number; +} + +interface SearchResponse { + data: { + logs: LogInfo[]; + total: number; + }; + message: string; + success: boolean; +} + +interface QueryParams { + token_name?: string; + model_name?: string; + code?: string; + start_timestamp?: string; + end_timestamp?: string; + page: number; + perPage: number; +} + +function validateParams(group: string, params: QueryParams): string | null { + if (!group) { + return 'Group parameter is required'; + } + 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( + group: string, + params: QueryParams +): Promise<{ logs: LogInfo[]; total: number }> { + try { + const url = new URL(`/api/logs/${group}/search`, 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 response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + 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); + return { + logs: [], + total: 0 + }; + } +} + +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('p') || '1', 10), + perPage: parseInt(searchParams.get('per_page') || '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(group, queryParams); + if (validationError) { + return NextResponse.json( + { + code: 400, + message: validationError, + error: validationError + }, + { status: 400 } + ); + } + + const { logs, total } = await fetchLogs(group, queryParams); + + 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/user/get-modes.ts b/frontend/providers/aiproxy/app/api/user/get-modes.ts new file mode 100644 index 00000000000..44752a9b643 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/get-modes.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { parseJwtToken } from '@/utils/auth'; + +interface SearchResponse { + data: string[]; + message: string; + success: boolean; +} + +async function fetchModels(): Promise { + try { + const url = new URL(`/api/models/enabled`, global.AppConfig?.backend.aiproxy); + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + 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; + } 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/user/init-app-config.ts b/frontend/providers/aiproxy/app/api/user/init-app-config.ts new file mode 100644 index 00000000000..2783f17f24e --- /dev/null +++ b/frontend/providers/aiproxy/app/api/user/init-app-config.ts @@ -0,0 +1,65 @@ +import { NextResponse } from 'next/server'; + +import type { AppConfigType } from '@/types/appConfig'; + +function getAppConfig(appConfig: AppConfigType): AppConfigType { + if (process.env.appTokenJwtKey) { + appConfig.auth.appTokenJwtKey = process.env.appTokenJwtKey; + } + if (process.env.aiProxyBackendKey) { + appConfig.auth.aiProxyBackendKey = process.env.aiProxyBackendKey; + } + if (process.env.aiproxy) { + appConfig.backend.aiproxy = process.env.aiproxy; + } + return appConfig; +} + +export function initAppConfig(): AppConfigType { + // default config + const DefaultAppConfig: AppConfigType = { + auth: { + appTokenJwtKey: '', + aiProxyBackendKey: '' + }, + backend: { + aiproxy: 'http://localhost:8080' + } + }; + 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/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index ce2b4a8f26d..1cfb02b055d 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -1,13 +1,18 @@ 'use client'; import { useState } from 'react'; -import { ArrowDownIcon, ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; +import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; import { Box, Button, Flex, HStack, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, Select, Table, + TableContainer, Tag, Tbody, Td, @@ -22,8 +27,6 @@ import { flexRender, getCoreRowModel, getPaginationRowModel, - getSortedRowModel, - SortingState, useReactTable } from '@tanstack/react-table'; import { TFunction } from 'i18next'; @@ -60,7 +63,6 @@ function KeyItem({ t }: { t: TFunction }): JSX.Element { return ; } -// 1. 定义数据类型 type KeyItem = { id: number; name: string; @@ -75,22 +77,12 @@ export enum TableHeaderId { KEY = 'key.key', CREATED_AT = 'key.createdAt', LAST_USED_AT = 'key.lastUsedAt', - STATUS = 'key.status' + STATUS = 'key.status', + ACTIONS = 'key.actions' } -// 2. 自定义表头组件 const CustomHeader = ({ column, t }: { column: Column; t: TFunction }) => { - console.log(column); - return ( - - {t(column.id)} - {column.getIsSorted() && - { - asc: , - desc: - }[column.getIsSorted() as string]} - - ); + return {t(column.id as TableHeaderId)}; }; const TableDemo = ({ t }: { t: TFunction }) => { @@ -113,17 +105,13 @@ const TableDemo = ({ t }: { t: TFunction }) => { } ]); - const [sorting, setSorting] = useState([]); - const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor((row) => row.name, { id: TableHeaderId.NAME, header: (props) => , - cell: (info) => { - return info.getValue(); - } + cell: (info) => info.getValue() }), columnHelper.accessor((row) => row.key, { id: TableHeaderId.KEY, @@ -143,10 +131,19 @@ const TableDemo = ({ t }: { t: TFunction }) => { columnHelper.accessor((row) => row.status, { id: TableHeaderId.STATUS, header: (props) => , + cell: (info) => info.getValue() + }), + + columnHelper.display({ + id: TableHeaderId.ACTIONS, + header: (props) => , cell: (info) => ( - - {t(`status.${info.getValue()}`)} - + Actions + // handleStatusChange(info.row.original.id)} + // onDelete={() => handleDelete(info.row.original.id)} + // /> ) }) ]; @@ -154,94 +151,113 @@ const TableDemo = ({ t }: { t: TFunction }) => { const table = useReactTable({ data, columns, - state: { - sorting - }, - onSortingChange: setSorting, - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), getCoreRowModel: getCoreRowModel() }); return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {flexRender(header.column.columnDef.header, header.getContext())} -
{flexRender(cell.column.columnDef.cell, cell.getContext())}
- - - - - - - - - {t('pagination.page')} - - {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} - - - - - + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
); }; +function SwitchPage({ table, t }: { table: Table; t: TFunction }) { + return ( + + + + + + + + {t('pagination.page')} + + {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} + + + + + + ); +} + export default KeyList; diff --git a/frontend/providers/aiproxy/mock/config.local.yaml b/frontend/providers/aiproxy/mock/config.local.yaml new file mode 100644 index 00000000000..448bfd4abbc --- /dev/null +++ b/frontend/providers/aiproxy/mock/config.local.yaml @@ -0,0 +1,5 @@ +auth: + appTokenJwtKey: 1234567890 + aiProxyBackendKey: 1234567890 +backend: + aiproxy: http://localhost:8080 diff --git a/frontend/providers/aiproxy/types/api.d.ts b/frontend/providers/aiproxy/types/api.d.ts new file mode 100644 index 00000000000..ebfd734b6f1 --- /dev/null +++ b/frontend/providers/aiproxy/types/api.d.ts @@ -0,0 +1,9 @@ +export type ApiResp = { + code?: number; + message?: string; + data?: Tdata; + error?: unknown; +}; + +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..6e01ba593ac --- /dev/null +++ b/frontend/providers/aiproxy/types/appConfig.d.ts @@ -0,0 +1,14 @@ +export type AppConfigType = { + auth: { + appTokenJwtKey: string; + aiProxyBackendKey: string; + }; + backend: { + aiproxy: string; + }; +}; + +declare global { + // eslint-disable-next-line no-var + var AppConfig: AppConfigType | undefined; +} diff --git a/frontend/providers/aiproxy/utils/auth.ts b/frontend/providers/aiproxy/utils/auth.ts new file mode 100644 index 00000000000..ae1a760f87b --- /dev/null +++ b/frontend/providers/aiproxy/utils/auth.ts @@ -0,0 +1,36 @@ +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..dd4eb6fe8ad --- /dev/null +++ b/frontend/providers/aiproxy/utils/request.ts @@ -0,0 +1,65 @@ +// http.ts +import axios, { AxiosRequestConfig, AxiosResponse, RawAxiosRequestHeaders } from 'axios'; + +import { ApiResp } from '@/types/api'; + +const request = axios.create({ + baseURL: '/', + withCredentials: true, + timeout: 40000 +}); + +// request interceptor +request.interceptors.request.use( + (config: AxiosRequestConfig) => { + // auto append service prefix + const _headers: RawAxiosRequestHeaders = config.headers || {}; + const session = useSessionStore.getState().session; + if (config.url && config.url?.startsWith('/api/')) { + _headers['Authorization'] = encodeURIComponent(session?.kubeconfig || ''); + } + + if (!config.headers || config.headers['Content-Type'] === '') { + _headers['Content-Type'] = 'application/json'; + } + + config.headers = _headers; + config.data = { ...config.data, internalToken: session.token }; + // nprogress.start(); + return config; + }, + (error) => { + error.data = {}; + error.data.message = 'error'; + // nprogress.done(); + return Promise.resolve(error); + } +); + +// response interceptor +request.interceptors.response.use( + (response: AxiosResponse) => { + const data = response.data as ApiResp; + + if (!data.code || data.code < 200 || data.code > 300) { + return Promise.reject(response.data); + } + // nprogress.done(); + return response.data; + }, + (error) => { + if (!error) { + return Promise.reject({ message: '未知错误' }); + } + if (axios.isCancel(error)) { + console.log('repeated request: ' + error.message); + } else { + error.data = {}; + error.data.message = 'error'; + } + // nprogress.done(); + return Promise.reject(error); + } +); + +export default request; From ba64e1de922be3407561583116b80e0503ecaeaf Mon Sep 17 00:00:00 2001 From: lim Date: Thu, 31 Oct 2024 11:49:20 +0000 Subject: [PATCH 04/47] ok --- .../aiproxy/app/api/user/get-logs.ts | 129 ++++++++---------- 1 file changed, 59 insertions(+), 70 deletions(-) diff --git a/frontend/providers/aiproxy/app/api/user/get-logs.ts b/frontend/providers/aiproxy/app/api/user/get-logs.ts index 6ef872c093d..149c72009b2 100644 --- a/frontend/providers/aiproxy/app/api/user/get-logs.ts +++ b/frontend/providers/aiproxy/app/api/user/get-logs.ts @@ -1,88 +1,80 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server' -import { parseJwtToken } from '@/utils/auth'; +import { parseJwtToken } from '@/utils/auth' interface LogInfo { - token_name: string; - endpoint: string; - content: string; - group: string; - model: string; - price: number; - id: number; - completion_price: number; - token_id: number; - used_amount: number; - prompt_tokens: number; - completion_tokens: number; - channel: number; - code: number; - created_at: number; + token_name: string + endpoint: string + content: string + group: string + model: string + price: number + id: number + completion_price: number + token_id: number + used_amount: number + prompt_tokens: number + completion_tokens: number + channel: number + code: number + created_at: number } interface SearchResponse { data: { - logs: LogInfo[]; - total: number; - }; - message: string; - success: boolean; + logs: LogInfo[] + total: number + } + message: string + success: boolean } interface QueryParams { - token_name?: string; - model_name?: string; - code?: string; - start_timestamp?: string; - end_timestamp?: string; - page: number; - perPage: number; + token_name?: string + model_name?: string + code?: string + start_timestamp?: string + end_timestamp?: string + page: number + perPage: number } -function validateParams(group: string, params: QueryParams): string | null { - if (!group) { - return 'Group parameter is required'; - } +function validateParams(params: QueryParams): string | null { if (params.page < 1) { - return 'Page number must be greater than 0'; + return 'Page number must be greater than 0' } if (params.perPage < 1 || params.perPage > 100) { - return 'Per page must be between 1 and 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 'Start timestamp cannot be greater than end timestamp' } } - return null; + return null } -async function fetchLogs( - group: string, - params: QueryParams -): Promise<{ logs: LogInfo[]; total: number }> { +async function fetchLogs(params: QueryParams): Promise<{ logs: LogInfo[]; total: number }> { try { - const url = new URL(`/api/logs/${group}/search`, global.AppConfig?.backend.aiproxy); + const url = new URL(`/api/logs/search`, global.AppConfig?.backend.aiproxy) - // 添加基础分页参数 - url.searchParams.append('p', params.page.toString()); - url.searchParams.append('per_page', params.perPage.toString()); + 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); + url.searchParams.append('token_name', params.token_name) } if (params.model_name) { - url.searchParams.append('model_name', params.model_name); + url.searchParams.append('model_name', params.model_name) } if (params.code) { - url.searchParams.append('code', params.code); + url.searchParams.append('code', params.code) } if (params.start_timestamp) { - url.searchParams.append('start_timestamp', params.start_timestamp); + url.searchParams.append('start_timestamp', params.start_timestamp) } if (params.end_timestamp) { - url.searchParams.append('end_timestamp', params.end_timestamp); + url.searchParams.append('end_timestamp', params.end_timestamp) } const response = await fetch(url.toString(), { @@ -90,34 +82,31 @@ async function fetchLogs( headers: { 'Content-Type': 'application/json' } - }); + }) if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status}`) } - const result: SearchResponse = await response.json(); + const result: SearchResponse = await response.json() if (!result.success) { - throw new Error(result.message || 'API request failed'); + 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); - return { - logs: [], - total: 0 - }; + 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; + await parseJwtToken(request.headers) + const searchParams = request.nextUrl.searchParams const queryParams: QueryParams = { page: parseInt(searchParams.get('p') || '1', 10), @@ -127,9 +116,9 @@ export async function GET(request: NextRequest): Promise { code: searchParams.get('code') || undefined, start_timestamp: searchParams.get('start_timestamp') || undefined, end_timestamp: searchParams.get('end_timestamp') || undefined - }; + } - const validationError = validateParams(group, queryParams); + const validationError = validateParams(queryParams) if (validationError) { return NextResponse.json( { @@ -138,10 +127,10 @@ export async function GET(request: NextRequest): Promise { error: validationError }, { status: 400 } - ); + ) } - const { logs, total } = await fetchLogs(group, queryParams); + const { logs, total } = await fetchLogs(queryParams) return NextResponse.json({ code: 200, @@ -149,9 +138,9 @@ export async function GET(request: NextRequest): Promise { logs, total } - }); + }) } catch (error) { - console.error('Logs search error:', error); + console.error('Logs search error:', error) return NextResponse.json( { code: 500, @@ -159,6 +148,6 @@ export async function GET(request: NextRequest): Promise { error: error instanceof Error ? error.message : 'Internal server error' }, { status: 500 } - ); + ) } } From 0bcc57cc86ed7787b6c574edf10218a1c54ad4ce Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Thu, 31 Oct 2024 19:50:46 +0800 Subject: [PATCH 05/47] add @tanstack/react-query --- frontend/pnpm-lock.yaml | 1156 ++++++++++++++++- .../aiproxy/app/[lng]/(user)/home/page.tsx | 14 +- .../aiproxy/app/[lng]/(user)/logs/page.tsx | 341 ++++- .../aiproxy/app/i18n/locales/en/common.json | 13 +- .../aiproxy/app/i18n/locales/zh/common.json | 10 +- .../components/SelectDateRange/index.tsx | 156 ++- .../aiproxy/components/SwitchPage.tsx | 117 ++ .../aiproxy/components/table/baseTable.tsx | 66 + .../aiproxy/components/user/KeyList.tsx | 103 +- frontend/providers/aiproxy/package.json | 1 + frontend/providers/aiproxy/types/log.d.ts | 39 +- frontend/providers/aiproxy/ui/chakraTheme.ts | 6 +- frontend/providers/aiproxy/ui/icons/index.tsx | 35 + 13 files changed, 1835 insertions(+), 222 deletions(-) create mode 100644 frontend/providers/aiproxy/components/SwitchPage.tsx create mode 100644 frontend/providers/aiproxy/components/table/baseTable.tsx create mode 100644 frontend/providers/aiproxy/ui/icons/index.tsx diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index db919018e26..e73a917cc70 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -480,6 +480,97 @@ importers: specifier: ^1.68.0 version: 1.69.5 + providers/aiproxy: + dependencies: + '@sealos/ui': + specifier: workspace:^ + version: link:../../packages/ui + '@tanstack/react-query': + specifier: ^4.35.3 + version: 4.36.1(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-table': + specifier: ^8.10.7 + version: 8.10.7(react-dom@18.2.0)(react@18.2.0) + accept-language: + specifier: ^3.0.20 + version: 3.0.20 + axios: + specifier: ^1.7.7 + version: 1.7.7 + date-fns: + specifier: ^2.30.0 + version: 2.30.0 + i18next: + specifier: ^23.11.5 + version: 23.12.1 + i18next-browser-languagedetector: + specifier: ^8.0.0 + version: 8.0.0 + i18next-http-backend: + specifier: ^2.6.2 + version: 2.6.2 + i18next-resources-to-backend: + specifier: ^1.2.1 + version: 1.2.1 + immer: + specifier: ^10.1.1 + version: 10.1.1 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + next: + specifier: 14.2.5 + version: 14.2.5(@babel/core@7.23.5)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) + react: + specifier: ^18 + version: 18.2.0 + react-day-picker: + specifier: ^8.8.2 + version: 8.9.1(date-fns@2.30.0)(react@18.2.0) + react-dom: + specifier: ^18 + version: 18.2.0(react@18.2.0) + react-hook-form: + specifier: ^7.46.2 + version: 7.48.2(react@18.2.0) + sealos-desktop-sdk: + specifier: workspace:^ + version: link:../../packages/client-sdk + zustand: + specifier: ^4.5.4 + version: 4.5.4(@types/react@18.2.37)(immer@10.1.1)(react@18.2.0) + devDependencies: + '@types/node': + specifier: ^20 + version: 20.10.0 + '@types/react': + specifier: ^18 + version: 18.2.37 + '@types/react-dom': + specifier: ^18 + version: 18.0.11 + eslint: + specifier: ^8 + version: 8.57.0 + eslint-config-next: + specifier: 15.0.1 + version: 15.0.1(eslint@8.57.0)(typescript@5.2.2) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.0) + eslint-plugin-simple-import-sort: + specifier: ^12.1.1 + version: 12.1.1(eslint@8.57.0) + prettier: + specifier: ^2.8.8 + version: 2.8.8 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.1 + typescript: + specifier: ^5 + version: 5.2.2 + providers/applaunchpad: dependencies: '@chakra-ui/anatomy': @@ -4280,7 +4371,7 @@ packages: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.6 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4354,7 +4445,7 @@ packages: '@babel/core': 7.23.5 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4 + debug: 4.3.6 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -5431,7 +5522,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.5 '@babel/types': 7.23.5 - debug: 4.3.4 + debug: 4.3.6 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7128,7 +7219,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=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 @@ -7228,7 +7319,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 @@ -7482,7 +7573,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 @@ -7848,6 +7939,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 @@ -9117,6 +9214,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==} @@ -10413,6 +10518,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} @@ -10549,6 +10681,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} @@ -10558,6 +10717,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} @@ -10569,10 +10733,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: @@ -10589,16 +10753,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} @@ -10614,6 +10816,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 @@ -10772,6 +10982,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'} @@ -10825,7 +11041,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 @@ -11041,6 +11257,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'} @@ -11052,6 +11273,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'} @@ -11062,6 +11291,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 @@ -11082,6 +11323,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'} @@ -11092,6 +11345,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'} @@ -11119,6 +11384,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'} @@ -11131,6 +11407,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 @@ -11214,6 +11504,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==} @@ -11224,6 +11521,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'} @@ -11268,11 +11570,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 @@ -11443,6 +11760,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: @@ -11545,7 +11867,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==} @@ -11642,6 +11963,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 @@ -12209,6 +12541,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'} @@ -12454,6 +12794,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'} @@ -12539,7 +12906,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} @@ -12641,6 +13007,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'} @@ -13071,13 +13446,77 @@ 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 - get-intrinsic: 1.2.2 - has-symbols: 1.0.3 - is-arguments: 1.1.1 + 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 is-map: 2.0.2 is-set: 2.0.2 is-string: 1.0.7 @@ -13103,10 +13542,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'} @@ -13115,6 +13581,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: @@ -13370,6 +13845,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: @@ -13516,6 +14026,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'} @@ -13690,7 +14253,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 @@ -13869,6 +14432,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'} @@ -14037,6 +14661,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'} @@ -14161,6 +14794,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'} @@ -14398,6 +15066,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) @@ -14411,7 +15080,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 @@ -14648,6 +15317,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'} @@ -14744,7 +15424,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==} @@ -15066,6 +15745,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'} @@ -15096,6 +15786,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: @@ -15152,7 +15851,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: @@ -15212,6 +15911,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'} @@ -15335,10 +16042,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'} @@ -15349,6 +16067,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'} @@ -15360,6 +16085,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: @@ -15529,7 +16261,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 @@ -15555,7 +16287,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 @@ -15579,10 +16311,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: @@ -15613,6 +16365,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 @@ -15701,6 +16458,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'} @@ -15771,6 +16537,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==} @@ -15827,6 +16601,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'} @@ -15905,6 +16693,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'} @@ -15994,6 +16787,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'} @@ -16021,6 +16821,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==} @@ -16101,7 +16908,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 @@ -16119,7 +16926,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: @@ -16143,6 +16950,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'} @@ -16286,7 +17104,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 @@ -16326,7 +17144,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 @@ -16417,7 +17235,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 @@ -16589,7 +17407,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 @@ -17057,11 +17875,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'} @@ -17280,7 +18093,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: @@ -17774,7 +18587,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 @@ -17798,7 +18611,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: @@ -17869,6 +18682,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==} @@ -18653,7 +19473,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: @@ -18794,6 +19614,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'} @@ -18802,6 +19632,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'} @@ -18810,6 +19649,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: @@ -18818,6 +19667,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: @@ -18832,6 +19690,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'} @@ -19138,6 +20005,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'} @@ -19184,7 +20056,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 @@ -20488,6 +21360,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'} @@ -20889,6 +21771,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 @@ -20903,6 +21795,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==} @@ -20970,6 +21871,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'} @@ -21017,6 +21923,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'} @@ -21025,6 +21943,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'} @@ -21090,6 +22018,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==} @@ -21490,6 +22428,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: @@ -21503,6 +22450,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'} @@ -21511,6 +22483,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: @@ -21518,6 +22500,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: @@ -21525,6 +22515,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: @@ -21751,7 +22750,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 @@ -22100,6 +23099,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 @@ -22116,6 +23124,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 @@ -22209,6 +23226,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'} @@ -22218,6 +23244,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'} @@ -22228,6 +23265,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: @@ -22235,6 +23284,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 @@ -22872,6 +23933,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/app/[lng]/(user)/home/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx index 6a4458d3a84..7d57476d297 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx @@ -1,10 +1,10 @@ -import { Flex } from '@chakra-ui/react'; +import { Flex } from '@chakra-ui/react' -import KeyList from '@/components/user/KeyList'; +import KeyList from '@/components/user/KeyList' export default function Home(): JSX.Element { return ( - + + borderRadius="12px"> @@ -28,10 +27,9 @@ export default function Home(): JSX.Element { flexDirection="column" justifyContent="flex-end" alignItems="flex-start" - gap="22px" - > + gap="22px"> ddddxx - ); + ) } diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index 12528722c0b..208243fa434 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { Box, Flex, Grid, Input, Select } from '@chakra-ui/react' import { MySelect } from '@sealos/ui' @@ -9,6 +9,10 @@ import { useTranslationClientSide } from '@/app/i18n/client' import SelectDateRange from '@/components/SelectDateRange' import { useI18n } from '@/providers/i18n/i18nContext' import { LogForm } from '@/types/form' +import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import { LogItem } from '@/types/log' +import { BaseTable } from '@/components/table/baseTable' +import SwitchPage from '@/components/SwitchPage' const mockModals = ['gpt-3.5-turbo', 'gpt-4o-mini', 'gpt-4'] @@ -32,6 +36,245 @@ const mockNames = [ const mockStatus = ['success', 'failed'] +const mockLogItems: LogItem[] = [ + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + completion_price: 0.0035, + token_id: 1, + used_amount: 0.0002415, + prompt_tokens: 18, + completion_tokens: 51, + channel: 1, + code: 200, + created_at: 1730354956491 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + + completion_price: 0.0035, + token_id: 1, + used_amount: 0.000315, + prompt_tokens: 18, + completion_tokens: 72, + channel: 1, + code: 200, + created_at: 1730354922071 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + + completion_price: 0.0035, + token_id: 1, + used_amount: 0.000294, + prompt_tokens: 18, + completion_tokens: 66, + channel: 1, + code: 200, + created_at: 1730354860678 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + + completion_price: 0.0035, + token_id: 1, + used_amount: 0.0004375, + prompt_tokens: 18, + completion_tokens: 107, + channel: 1, + code: 200, + created_at: 1730354800680 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + completion_price: 0.0035, + token_id: 1, + used_amount: 0.0003605, + prompt_tokens: 18, + completion_tokens: 85, + channel: 1, + code: 200, + created_at: 1730088079991 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + + completion_price: 0.0035, + token_id: 1, + used_amount: 0.000329, + prompt_tokens: 18, + completion_tokens: 76, + channel: 1, + code: 200, + created_at: 1730087556437 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + id: 5, + completion_price: 0.0105, + token_id: 1, + used_amount: 0.0005775000000000001, + prompt_tokens: 18, + completion_tokens: 49, + channel: 1, + code: 200, + created_at: 1729847798155 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + id: 4, + completion_price: 0.0105, + token_id: 1, + used_amount: 0.000672, + prompt_tokens: 18, + completion_tokens: 58, + channel: 1, + code: 200, + created_at: 1729840920384 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + id: 3, + completion_price: 0.0105, + token_id: 1, + used_amount: 0.00034749225, + prompt_tokens: 99, + completion_tokens: 27, + channel: 1, + code: 200, + created_at: 1729840549119 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + id: 2, + completion_price: 0.0105, + token_id: 1, + used_amount: 0.00006638100000000001, + prompt_tokens: 18, + completion_tokens: 92, + channel: 1, + code: 200, + created_at: 1729840027914 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + id: 5, + completion_price: 0.0105, + token_id: 1, + used_amount: 0.0005775000000000001, + prompt_tokens: 18, + completion_tokens: 49, + channel: 1, + code: 200, + created_at: 1729847798155 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + id: 4, + completion_price: 0.0105, + token_id: 1, + used_amount: 0.000672, + prompt_tokens: 18, + completion_tokens: 58, + channel: 1, + code: 200, + created_at: 1729840920384 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + id: 3, + completion_price: 0.0105, + token_id: 1, + used_amount: 0.00034749225, + prompt_tokens: 99, + completion_tokens: 27, + channel: 1, + code: 200, + created_at: 1729840549119 + }, + { + token_name: 'test token', + endpoint: '/v1/chat/completions', + content: '', + group: 'ns-admin', + model: 'gpt-3.5-turbo', + price: 0.0035, + id: 2, + completion_price: 0.0105, + token_id: 1, + used_amount: 0.00006638100000000001, + prompt_tokens: 18, + completion_tokens: 92, + channel: 1, + code: 200, + created_at: 1729840027914 + } +] + export default function Home(): React.JSX.Element { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') @@ -54,17 +297,62 @@ export default function Home(): React.JSX.Element { } }) + 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 ? 'success' : 'failed'), + id: 'status' + }, + { + header: t('logs.time'), + accessorFn: (row) => new Date(row.created_at).toLocaleString(), + id: 'created_at' + }, + { + header: t('logs.price'), + accessorFn: (row) => `${row.price}/${row.completion_price}`, + id: 'price' + } + ] + }, []) + + const table = useReactTable({ + data: mockLogItems, + columns, + getCoreRowModel: getCoreRowModel() + }) + return ( - + {t('logs.call_log')} - - - {t('logs.name')} + + + + {t('logs.name')} + ({ @@ -75,10 +363,12 @@ export default function Home(): React.JSX.Element { /> - - {t('logs.modal')} + + + {t('logs.modal')} + ({ @@ -89,21 +379,25 @@ export default function Home(): React.JSX.Element { /> - - {t('logs.status')} - {/* + + {t('logs.status')} + + ({ value: item, label: item }))} - onchange={(val: string) => setValue('status', val)} - /> */} + onchange={(val: string) => setValue('modelName', val)} + /> - - {t('logs.time')} + + + {t('logs.time')} + + + + setValue('page', idx)} + /> + ) diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index e9aea429475..1efef50c7af 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -16,6 +16,15 @@ "status": "state" }, "logs": { - "call_log": "call log" - } + "call_log": "call log", + "name": "name", + "status": "state", + "time": "time", + "modal": "Model", + "prompt_tokens": "enter", + "completion_tokens": "output", + "price": "price" + }, + "Page": "Page", + "Total": "total" } diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index 20082180b66..da5f84d310f 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -20,6 +20,12 @@ "name": "名称", "modal": "模型", "status": "状态", - "time": "时间" - } + "time": "时间", + "model": "模型", + "prompt_tokens": "输入", + "completion_tokens": "输出", + "price": "价格" + }, + "Page": "页", + "Total": "总数" } diff --git a/frontend/providers/aiproxy/components/SelectDateRange/index.tsx b/frontend/providers/aiproxy/components/SelectDateRange/index.tsx index ecaed02150d..df5d222bff8 100644 --- a/frontend/providers/aiproxy/components/SelectDateRange/index.tsx +++ b/frontend/providers/aiproxy/components/SelectDateRange/index.tsx @@ -1,7 +1,5 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions */ - -import { ChangeEventHandler, Dispatch, SetStateAction, useState } from 'react'; -import { DateRange, DayPicker, SelectRangeEventHandler } from 'react-day-picker'; +import { ChangeEventHandler, Dispatch, SetStateAction, useState } from 'react' +import { DateRange, DayPicker, SelectRangeEventHandler } from 'react-day-picker' import { Box, Button, @@ -11,16 +9,16 @@ import { Popover, PopoverContent, PopoverTrigger -} from '@chakra-ui/react'; -import { endOfDay, format, isAfter, isBefore, isValid, parse, startOfDay } from 'date-fns'; +} 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>; -}; + isDisabled?: boolean + startTime: Date + setStartTime: Dispatch> + endTime: Date + setEndTime: Dispatch> +} export default function SelectDateRange({ isDisabled, @@ -29,161 +27,159 @@ export default function SelectDateRange({ endTime, setEndTime }: SelectDateRangeProps): JSX.Element { - const initState = { from: startTime, to: endTime }; + 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 [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)); - }; + 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()); + 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 }); + return setSelectedRange({ from: undefined, to: selectedRange?.to }) } if (selectedRange?.to) { if (isAfter(date, selectedRange.to)) { - setSelectedRange({ from: selectedRange.to, to: date }); + setSelectedRange({ from: selectedRange.to, to: date }) } else { - setSelectedRange({ from: date, to: selectedRange?.to }); + setSelectedRange({ from: date, to: selectedRange?.to }) } } else { - setSelectedRange({ from: date, to: date }); + setSelectedRange({ from: date, to: date }) } - }; + } const handleToChange: ChangeEventHandler = (e) => { - setToValue(e.target.value); - const date = parse(e.target.value, 'y-MM-dd', new Date()); + 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 }); + return setSelectedRange({ from: selectedRange?.from, to: undefined }) } if (selectedRange?.from) { if (isBefore(date, selectedRange.from)) { - setSelectedRange({ from: date, to: selectedRange.from }); + setSelectedRange({ from: date, to: selectedRange.from }) } else { - setSelectedRange({ from: selectedRange?.from, to: date }); + setSelectedRange({ from: selectedRange?.from, to: date }) } } else { - setSelectedRange({ from: date, to: date }); + setSelectedRange({ from: date, to: date }) } - }; + } const handleRangeSelect: SelectRangeEventHandler = (range: DateRange | undefined) => { if (range) { - let { from, to } = range; + let { from, to } = range if (inputState === 0) { if (from === selectedRange?.from) { - from = to; + from = to } else { - to = from; + to = from } - setInputState(1); + setInputState(1) } else { - setInputState(0); + setInputState(0) } setSelectedRange({ from, to - }); + }) if (from) { - setFromValue(format(from, 'y-MM-dd')); + setFromValue(format(from, 'y-MM-dd')) } else { - setFromValue(''); + setFromValue('') } if (to) { - setToValue(format(to, 'y-MM-dd')); + setToValue(format(to, 'y-MM-dd')) } else { - setToValue(from ? format(from, 'y-MM-dd') : ''); + setToValue(from ? format(from, 'y-MM-dd') : '') } } else { if (fromValue && selectedRange?.from) { - setToValue(fromValue); + setToValue(fromValue) setSelectedRange({ ...selectedRange, to: selectedRange.from - }); - setInputState(1); + }) + setInputState(1) } } - }; + } const handleRangeSelectFrom: SelectRangeEventHandler = (range: DateRange | undefined) => { if (range) { - let { from, to } = range; + let { from, to } = range if (selectedRange?.to) { if (from) { if (!to) { - to = from; + to = from } else if (from === selectedRange?.from) { - from = to; - to = selectedRange.to; + from = to + to = selectedRange.to } if (isBefore(from, selectedRange.to)) { setSelectedRange({ ...selectedRange, from - }); - setFromValue(format(from, 'y-MM-dd')); + }) + setFromValue(format(from, 'y-MM-dd')) } } } } - }; + } const handleRangeSelectTo: SelectRangeEventHandler = (range: DateRange | undefined) => { - console.log(range, selectedRange); if (range) { - let { from, to } = range; + let { from, to } = range if (selectedRange?.from) { if (to) { if (!from) { - from = to; + from = to } else if (to === selectedRange?.to) { - to = from; - from = selectedRange.from; + to = from + from = selectedRange.from } if (isAfter(to, selectedRange.from)) { setSelectedRange({ ...selectedRange, to - }); - setToValue(format(to, 'y-MM-dd')); + }) + setToValue(format(to, 'y-MM-dd')) } } } } else { if (fromValue && selectedRange?.from) { - setToValue(fromValue); + setToValue(fromValue) setSelectedRange({ ...selectedRange, to: selectedRange.from - }); - setInputState(1); + }) + setInputState(1) } } - }; + } return ( + borderRadius="6px"> @@ -229,8 +225,8 @@ export default function SelectDateRange({ minW="90px" onChange={handleToChange} onBlur={() => { - selectedRange?.to && setEndTime(endOfDay(selectedRange.to)); - console.log(selectedRange?.to); + selectedRange?.to && setEndTime(endOfDay(selectedRange.to)) + console.log(selectedRange?.to) }} /> @@ -252,10 +248,9 @@ export default function SelectDateRange({ { - setInputState(0); - onClose(); - }} - > + setInputState(0) + onClose() + }}> + + {currentPage} + / + {totalPage} + + + + + {pageSize} + + + /{t('Page')} + + + ) +} diff --git a/frontend/providers/aiproxy/components/table/baseTable.tsx b/frontend/providers/aiproxy/components/table/baseTable.tsx new file mode 100644 index 00000000000..4539be71d2b --- /dev/null +++ b/frontend/providers/aiproxy/components/table/baseTable.tsx @@ -0,0 +1,66 @@ +import { + 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 +}: { table: ReactTable } & TableContainerProps) { + return ( + + + + {table.getHeaderGroups().map((headers) => { + return ( + + {headers.headers.map((header, i) => { + return ( + + ) + })} + + ) + })} + + + {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 index 1cfb02b055d..058e9817694 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -1,11 +1,12 @@ -'use client'; -import { useState } from 'react'; -import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; +'use client' +import { useState } from 'react' +import { ChevronLeftIcon, ChevronRightIcon, PlusSquareIcon } from '@chakra-ui/icons' import { Box, Button, Flex, HStack, + Icon, Popover, PopoverBody, PopoverContent, @@ -20,7 +21,7 @@ import { Th, Thead, Tr -} from '@chakra-ui/react'; +} from '@chakra-ui/react' import { Column, createColumnHelper, @@ -28,15 +29,15 @@ import { getCoreRowModel, getPaginationRowModel, useReactTable -} from '@tanstack/react-table'; -import { TFunction } from 'i18next'; +} from '@tanstack/react-table' +import { TFunction } from 'i18next' -import { useTranslationClientSide } from '@/app/i18n/client'; -import { useI18n } from '@/providers/i18n/i18nContext'; +import { useTranslationClientSide } from '@/app/i18n/client' +import { useI18n } from '@/providers/i18n/i18nContext' export function KeyList(): JSX.Element { - const { lng } = useI18n(); - const { t } = useTranslationClientSide(lng, 'common'); + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') return ( <> @@ -47,30 +48,47 @@ export function KeyList(): JSX.Element { fontStyle="normal" fontWeight={500} lineHeight="26px" - letterSpacing="0.15px" - > + letterSpacing="0.15px"> {t('keyList.title')} + + + + + API Endpoint: + https://www.aiproxy.com + + - ); + ) } function KeyItem({ t }: { t: TFunction }): JSX.Element { - return ; + return } type KeyItem = { - id: number; - name: string; - key: string; - createdAt: string; - lastUsedAt: string; - status: 'active' | 'inactive'; -}; + id: number + name: string + key: string + createdAt: string + lastUsedAt: string + status: 'active' | 'inactive' +} export enum TableHeaderId { NAME = 'key.name', @@ -82,8 +100,8 @@ export enum TableHeaderId { } const CustomHeader = ({ column, t }: { column: Column; t: TFunction }) => { - return {t(column.id as TableHeaderId)}; -}; + return {t(column.id as TableHeaderId)} +} const TableDemo = ({ t }: { t: TFunction }) => { const [data] = useState([ @@ -103,9 +121,9 @@ const TableDemo = ({ t }: { t: TFunction }) => { lastUsedAt: '2021-01-01', status: 'inactive' } - ]); + ]) - const columnHelper = createColumnHelper(); + const columnHelper = createColumnHelper() const columns = [ columnHelper.accessor((row) => row.name, { @@ -146,13 +164,13 @@ const TableDemo = ({ t }: { t: TFunction }) => { // /> ) }) - ]; + ] const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() - }); + }) return ( @@ -166,16 +184,14 @@ const TableDemo = ({ t }: { t: TFunction }) => { borderRadius="6px" bg="grayModern.100" display="flex" - alignItems="center" - > + alignItems="center"> {headerGroup.headers.map((header) => ( + px={4}> {flexRender(header.column.columnDef.header, header.getContext())} ))} @@ -190,15 +206,13 @@ const TableDemo = ({ t }: { t: TFunction }) => { alignItems="center" height="48px" borderBottom="1px solid" - borderColor="grayModern.150" - > + borderColor="grayModern.150"> {row.getVisibleCells().map((cell) => ( + px={4}> {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} @@ -208,8 +222,8 @@ const TableDemo = ({ t }: { t: TFunction }) => { - ); -}; + ) +} function SwitchPage({ table, t }: { table: Table; t: TFunction }) { return ( @@ -217,8 +231,7 @@ function SwitchPage({ table, t }: { table: Table; t: TFunction }) { @@ -247,9 +259,8 @@ function SwitchPage({ table, t }: { table: Table; t: TFunction }) { w="auto" value={table.getState().pagination.pageSize} onChange={(e) => { - table.setPageSize(Number(e.target.value)); - }} - > + table.setPageSize(Number(e.target.value)) + }}> {[10, 20, 30, 40, 50].map((pageSize) => (
diff --git a/frontend/providers/aiproxy/app/[lng]/layout.tsx b/frontend/providers/aiproxy/app/[lng]/layout.tsx index 5673c1e2742..af2a59ffe31 100644 --- a/frontend/providers/aiproxy/app/[lng]/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/layout.tsx @@ -11,6 +11,7 @@ import 'react-day-picker/dist/style.css' import { EVENT_NAME } from 'sealos-desktop-sdk' import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' +import QueryProvider from '@/providers/chakra/QueryProvider' export async function generateStaticParams(): Promise<{ lng: string }[]> { return languages.map((lng) => ({ lng })) @@ -47,7 +48,9 @@ export default async function RootLayout({ - {children} + + {children} + diff --git a/frontend/providers/aiproxy/app/api/user/get-modes.ts b/frontend/providers/aiproxy/app/api/user/get-models.ts similarity index 60% rename from frontend/providers/aiproxy/app/api/user/get-modes.ts rename to frontend/providers/aiproxy/app/api/user/get-models.ts index 44752a9b643..b9b50d8dd82 100644 --- a/frontend/providers/aiproxy/app/api/user/get-modes.ts +++ b/frontend/providers/aiproxy/app/api/user/get-models.ts @@ -1,53 +1,53 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server' -import { parseJwtToken } from '@/utils/auth'; +import { parseJwtToken } from '@/utils/auth' interface SearchResponse { - data: string[]; - message: string; - success: boolean; + data: string[] + message: string + success: boolean } async function fetchModels(): Promise { try { - const url = new URL(`/api/models/enabled`, global.AppConfig?.backend.aiproxy); + const url = new URL(`/api/models/enabled`, global.AppConfig?.backend.aiproxy) const response = await fetch(url.toString(), { method: 'GET', headers: { 'Content-Type': 'application/json' } - }); + }) if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status}`) } - const result: SearchResponse = await response.json(); + const result: SearchResponse = await response.json() if (!result.success) { - throw new Error(result.message || 'get models API request failed'); + throw new Error(result.message || 'get models API request failed') } - return result.data; + return result.data } catch (error) { - console.error('Error fetching models:', error); - return Promise.reject(error); + console.error('Error fetching models:', error) + return Promise.reject(error) } } export async function GET(request: NextRequest): Promise { try { - await parseJwtToken(request.headers); + // await parseJwtToken(request.headers) - const models = await fetchModels(); + const models = await fetchModels() return NextResponse.json({ code: 200, data: models - }); + }) } catch (error) { - console.error('get models error:', error); + console.error('get models error:', error) return NextResponse.json( { @@ -56,6 +56,6 @@ export async function GET(request: NextRequest): Promise { error: error instanceof Error ? error.message : 'Internal server error' }, { status: 500 } - ); + ) } } 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/types/api.d.ts b/frontend/providers/aiproxy/types/api.d.ts index ebfd734b6f1..71452bae229 100644 --- a/frontend/providers/aiproxy/types/api.d.ts +++ b/frontend/providers/aiproxy/types/api.d.ts @@ -1,9 +1,8 @@ -export type ApiResp = { - code?: number; - message?: string; - data?: Tdata; - error?: unknown; -}; +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'; + typeof x.code === 'number' && typeof x.message === 'string' diff --git a/frontend/providers/aiproxy/utils/request.ts b/frontend/providers/aiproxy/utils/request.ts index dd4eb6fe8ad..aec3111f784 100644 --- a/frontend/providers/aiproxy/utils/request.ts +++ b/frontend/providers/aiproxy/utils/request.ts @@ -1,65 +1,95 @@ -// http.ts -import axios, { AxiosRequestConfig, AxiosResponse, RawAxiosRequestHeaders } from 'axios'; - -import { ApiResp } from '@/types/api'; +import { ApiResp } from '@/types/api' +import axios, { + InternalAxiosRequestConfig, + AxiosHeaders, + AxiosResponse, + AxiosRequestConfig +} from 'axios' const request = axios.create({ baseURL: '/', withCredentials: true, - timeout: 40000 -}); + timeout: 60000 +}) // request interceptor request.interceptors.request.use( - (config: AxiosRequestConfig) => { + (config: InternalAxiosRequestConfig) => { // auto append service prefix - const _headers: RawAxiosRequestHeaders = config.headers || {}; - const session = useSessionStore.getState().session; - if (config.url && config.url?.startsWith('/api/')) { - _headers['Authorization'] = encodeURIComponent(session?.kubeconfig || ''); + if (config.url && !config.url?.startsWith('/api/')) { + config.url = '' + config.url } + let _headers: AxiosHeaders = config.headers + + //获取token,并将其添加至请求头中 + _headers['Authorization'] = config.headers.Authorization if (!config.headers || config.headers['Content-Type'] === '') { - _headers['Content-Type'] = 'application/json'; + _headers['Content-Type'] = 'application/json' } - config.headers = _headers; - config.data = { ...config.data, internalToken: session.token }; - // nprogress.start(); - return config; + config.headers = _headers + return config }, - (error) => { - error.data = {}; - error.data.message = 'error'; - // nprogress.done(); - return Promise.resolve(error); + (error: any) => { + error.data = {} + error.data.msg = '服务器异常,请联系管理员!' + return Promise.resolve(error) } -); +) // response interceptor request.interceptors.response.use( (response: AxiosResponse) => { - const data = response.data as ApiResp; + const { status, data } = response + if (status < 200 || status >= 300) { + return Promise.reject(status + ', ' + typeof data === 'string' ? data : String(data)) + } - if (!data.code || data.code < 200 || data.code > 300) { - return Promise.reject(response.data); + const apiResp = data as ApiResp + if (apiResp.code < 200 || apiResp.code >= 400) { + return Promise.reject(apiResp.code + ':' + apiResp.message) } - // nprogress.done(); - return response.data; + + response.data = apiResp.data + return response.data }, - (error) => { - if (!error) { - return Promise.reject({ message: '未知错误' }); - } + (error: any) => { if (axios.isCancel(error)) { - console.log('repeated request: ' + error.message); + return Promise.reject('cancel request' + String(error)) } else { - error.data = {}; - error.data.message = 'error'; + error.errMessage = '请求超时或服务器异常,请检查网络或联系管理员!' } - // nprogress.done(); - return Promise.reject(error); + 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 default request; +export function DELETE( + url: string, + data?: { [key: string]: any }, + config?: AxiosRequestConfig +): Promise { + return request.delete(url, { + params: data, + ...config + }) +} From 28f8aa0f36f1016182abb37608e3c76e51376074 Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Thu, 31 Oct 2024 21:33:16 +0800 Subject: [PATCH 09/47] update api --- .../providers/aiproxy/.vscode/settings.json | 100 +++++++++--------- frontend/providers/aiproxy/api/platform.ts | 9 +- .../aiproxy/app/[lng]/(user)/layout.tsx | 69 +++++++++++- .../providers/aiproxy/app/[lng]/layout.tsx | 5 +- .../{user/get-keys.ts => get-keys/route.ts} | 93 ++++++++-------- .../{user/get-logs.ts => get-logs/route.ts} | 5 +- .../get-models.ts => get-models/route.ts} | 10 +- .../route.ts} | 0 frontend/providers/aiproxy/middleware.ts | 38 ++++--- frontend/providers/aiproxy/package.json | 13 +-- frontend/providers/aiproxy/utils/request.ts | 3 +- frontend/providers/aiproxy/utils/user.ts | 14 +++ 12 files changed, 224 insertions(+), 135 deletions(-) rename frontend/providers/aiproxy/app/api/{user/get-keys.ts => get-keys/route.ts} (54%) rename frontend/providers/aiproxy/app/api/{user/get-logs.ts => get-logs/route.ts} (96%) rename frontend/providers/aiproxy/app/api/{user/get-models.ts => get-models/route.ts} (84%) rename frontend/providers/aiproxy/app/api/{user/init-app-config.ts => init-app-config/route.ts} (100%) create mode 100644 frontend/providers/aiproxy/utils/user.ts diff --git a/frontend/providers/aiproxy/.vscode/settings.json b/frontend/providers/aiproxy/.vscode/settings.json index ab270616b52..61caaefac34 100644 --- a/frontend/providers/aiproxy/.vscode/settings.json +++ b/frontend/providers/aiproxy/.vscode/settings.json @@ -1,54 +1,54 @@ { - // editor + // 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.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" + "editor.insertSpaces": true, + "editor.detectIndentation": false + }, + "typescript.tsdk": "node_modules/typescript/lib" } \ No newline at end of file diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts index 5c4f3d3385f..d9884155df9 100644 --- a/frontend/providers/aiproxy/api/platform.ts +++ b/frontend/providers/aiproxy/api/platform.ts @@ -1,10 +1,5 @@ import { GET, POST } from '@/utils/request' -export const getAppEnv = () => GET('/api/getEnv') +export const initAppConfig = () => GET('/api/init-app-config') -export const getRuntime = () => GET('/api/platform/getRuntime') - -export const postAuthCname = (data: { publicDomain: string; customDomain: string }) => - POST('/api/platform/authCname', data) - -export const getModels = () => GET('/api/user/get-models') +export const getModels = () => GET('/api/get-models') diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index 2ce36be8c62..4bc3dc5b474 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -3,13 +3,76 @@ import { Box, Flex } from '@chakra-ui/react' import SideBar from '@/components/user/Sidebar' -export default function UserLayout({ children }: { children: React.ReactNode }) { +import { EVENT_NAME } from 'sealos-desktop-sdk' +import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' +import { useEffect } from 'react' +import { initAppConfig } from '@/api/platform' + +export default function UserLayout({ + children, + params +}: { + children: React.ReactNode + params: { lng: string } +}) { + // init session + useEffect(() => { + const response = 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('devbox: app init success') + } catch (err) { + console.log('devbox: app is not running in desktop') + if (!process.env.NEXT_PUBLIC_MOCK_USER) { + localStorage.removeItem('session') + } + } + })() + return response + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + initAppConfig() + + // const changeI18n = async (data: any) => { + // const lastLang = getcl() + // const newLang = data.currentLanguage + // if (lastLang !== newLang) { + // router.push(pathname, { locale: newLang }) + // setLangStore(newLang) + // setRefresh((state) => !state) + // } + // } + + // ;(async () => { + // try { + // const lang = await sealosApp.getLanguage() + // changeI18n({ + // currentLanguage: lang.lng + // }) + // } catch (error) { + // changeI18n({ + // currentLanguage: 'zh' + // }) + // } + // })() + + // return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return ( - + - {/* Main Content */} {children} diff --git a/frontend/providers/aiproxy/app/[lng]/layout.tsx b/frontend/providers/aiproxy/app/[lng]/layout.tsx index af2a59ffe31..4ece0aca6da 100644 --- a/frontend/providers/aiproxy/app/[lng]/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/layout.tsx @@ -5,14 +5,11 @@ 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' -import { EVENT_NAME } from 'sealos-desktop-sdk' -import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' -import QueryProvider from '@/providers/chakra/QueryProvider' - export async function generateStaticParams(): Promise<{ lng: string }[]> { return languages.map((lng) => ({ lng })) } diff --git a/frontend/providers/aiproxy/app/api/user/get-keys.ts b/frontend/providers/aiproxy/app/api/get-keys/route.ts similarity index 54% rename from frontend/providers/aiproxy/app/api/user/get-keys.ts rename to frontend/providers/aiproxy/app/api/get-keys/route.ts index b2297b22b58..ba44bbf7ca6 100644 --- a/frontend/providers/aiproxy/app/api/user/get-keys.ts +++ b/frontend/providers/aiproxy/app/api/get-keys/route.ts @@ -1,46 +1,46 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server' -import { parseJwtToken } from '@/utils/auth'; +import { parseJwtToken } from '@/utils/auth' interface 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; + 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 } interface SearchResponse { data: { - tokens: TokenInfo[]; - total: number; - }; - message: string; - success: boolean; + tokens: TokenInfo[] + total: number + } + message: string + success: boolean } function validateParams(group: string, page: number, perPage: number): string | null { if (!group) { - return 'Group parameter is required'; + return 'Group parameter is required' } if (page < 1) { - return 'Page number must be greater than 0'; + return 'Page number must be greater than 0' } if (perPage < 1 || perPage > 100) { - return 'Per page must be between 1 and 100'; + return 'Per page must be between 1 and 100' } - return null; + return null } async function fetchTokens( @@ -49,49 +49,52 @@ async function fetchTokens( group: string ): Promise<{ tokens: TokenInfo[]; total: number }> { try { - const url = new URL(`/api/token/${group}/search`, global.AppConfig?.backend.aiproxy); - url.searchParams.append('p', page.toString()); - url.searchParams.append('per_page', perPage.toString()); + const url = new URL(`/api/token/${group}/search`, 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' + 'Content-Type': 'application/json', + Authorization: `${token}` } - }); + }) if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status}`) } - const result: SearchResponse = await response.json(); + const result: SearchResponse = await response.json() if (!result.success) { - throw new Error(result.message || 'API request failed'); + throw new Error(result.message || 'API request failed') } return { tokens: result.data.tokens, total: result.data.total - }; + } } catch (error) { - console.error('Error fetching tokens:', 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 group = await parseJwtToken(request.headers) - const searchParams = request.nextUrl.searchParams; - const page = parseInt(searchParams.get('p') || '1', 10); - const perPage = parseInt(searchParams.get('per_page') || '10', 10); + const searchParams = request.nextUrl.searchParams + const page = parseInt(searchParams.get('p') || '1', 10) + const perPage = parseInt(searchParams.get('per_page') || '10', 10) - const validationError = validateParams(group, page, perPage); + const validationError = validateParams(group, page, perPage) if (validationError) { return NextResponse.json( { @@ -100,10 +103,10 @@ export async function GET(request: NextRequest): Promise { error: validationError }, { status: 400 } - ); + ) } - const { tokens, total } = await fetchTokens(page, perPage, group); + const { tokens, total } = await fetchTokens(page, perPage, group) return NextResponse.json({ code: 200, @@ -111,9 +114,9 @@ export async function GET(request: NextRequest): Promise { tokens, total } - }); + }) } catch (error) { - console.error('Token search error:', error); + console.error('Token search error:', error) return NextResponse.json( { @@ -122,6 +125,6 @@ export async function GET(request: NextRequest): Promise { error: error instanceof Error ? error.message : 'Internal server error' }, { status: 500 } - ); + ) } } diff --git a/frontend/providers/aiproxy/app/api/user/get-logs.ts b/frontend/providers/aiproxy/app/api/get-logs/route.ts similarity index 96% rename from frontend/providers/aiproxy/app/api/user/get-logs.ts rename to frontend/providers/aiproxy/app/api/get-logs/route.ts index 44304588712..920800766f9 100644 --- a/frontend/providers/aiproxy/app/api/user/get-logs.ts +++ b/frontend/providers/aiproxy/app/api/get-logs/route.ts @@ -76,10 +76,13 @@ async function fetchLogs(params: QueryParams): Promise<{ logs: LogInfo[]; total: 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' + 'Content-Type': 'application/json', + Authorization: `${token}` } }) diff --git a/frontend/providers/aiproxy/app/api/user/get-models.ts b/frontend/providers/aiproxy/app/api/get-models/route.ts similarity index 84% rename from frontend/providers/aiproxy/app/api/user/get-models.ts rename to frontend/providers/aiproxy/app/api/get-models/route.ts index b9b50d8dd82..752fbcd236c 100644 --- a/frontend/providers/aiproxy/app/api/user/get-models.ts +++ b/frontend/providers/aiproxy/app/api/get-models/route.ts @@ -11,11 +11,13 @@ interface SearchResponse { async function fetchModels(): Promise { try { const url = new URL(`/api/models/enabled`, global.AppConfig?.backend.aiproxy) + const token = global.AppConfig?.auth.aiProxyBackendKey const response = await fetch(url.toString(), { method: 'GET', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: `${token}` } }) @@ -38,7 +40,11 @@ async function fetchModels(): Promise { export async function GET(request: NextRequest): Promise { try { - // await parseJwtToken(request.headers) + console.log(global.AppConfig) + + console.log(request.headers) + + await parseJwtToken(request.headers) const models = await fetchModels() diff --git a/frontend/providers/aiproxy/app/api/user/init-app-config.ts b/frontend/providers/aiproxy/app/api/init-app-config/route.ts similarity index 100% rename from frontend/providers/aiproxy/app/api/user/init-app-config.ts rename to frontend/providers/aiproxy/app/api/init-app-config/route.ts diff --git a/frontend/providers/aiproxy/middleware.ts b/frontend/providers/aiproxy/middleware.ts index d9dd14fba49..5292ec3bbc0 100644 --- a/frontend/providers/aiproxy/middleware.ts +++ b/frontend/providers/aiproxy/middleware.ts @@ -1,9 +1,9 @@ -import acceptLanguage from 'accept-language'; -import { NextRequest, NextResponse } from 'next/server'; +import acceptLanguage from 'accept-language' +import { NextRequest, NextResponse } from 'next/server' -import { fallbackLng, languages } from '@/app/i18n/settings'; +import { fallbackLng, languages } from '@/app/i18n/settings' -acceptLanguage.languages(languages); +acceptLanguage.languages(languages) export const config = { matcher: [ @@ -31,38 +31,44 @@ export const config = { '/((?!icon/).*)', '/((?!chrome/).*)' ] -}; +} export function middleware(req: NextRequest): NextResponse { + // 如果是 API 请求,直接放行,不添加语言前缀 + if (req.nextUrl.pathname.includes('/api/')) { + return NextResponse.next() + } + // static file /public/xxx.svg if ( req.nextUrl.pathname.endsWith('.svg') || req.nextUrl.pathname.endsWith('.png') || req.nextUrl.pathname.endsWith('.ico') ) { - return NextResponse.next(); + return NextResponse.next() } if (req.nextUrl.pathname.indexOf('icon') > -1 || req.nextUrl.pathname.indexOf('chrome') > -1) - return NextResponse.next(); + return NextResponse.next() - let lng: string | undefined | null; - lng = acceptLanguage.get(req.headers.get('Accept-Language')); - if (!lng) lng = fallbackLng; + let lng: string | undefined | null + lng = acceptLanguage.get(req.headers.get('Accept-Language')) + if (!lng) lng = fallbackLng if (req.nextUrl.pathname === '/' || req.nextUrl.pathname === '/zh') { - const newUrl = new URL(`/${lng}/home`, req.url); - return NextResponse.redirect(newUrl); + const newUrl = new URL(`/${lng}/home`, req.url) + return NextResponse.redirect(newUrl) } // Redirect if lng in path is not supported if ( !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) && - !req.nextUrl.pathname.startsWith('/_next') + !req.nextUrl.pathname.startsWith('/_next') && + !req.nextUrl.pathname.startsWith('/api/') // 添加这个条件,确保 API 路由不会被重定向 ) { - const newUrl = new URL(`/${lng}${req.nextUrl.pathname}${req.nextUrl.search}`, req.url); - return NextResponse.redirect(newUrl); + const newUrl = new URL(`/${lng}${req.nextUrl.pathname}${req.nextUrl.search}`, req.url) + return NextResponse.redirect(newUrl) } - return NextResponse.next(); + return NextResponse.next() } diff --git a/frontend/providers/aiproxy/package.json b/frontend/providers/aiproxy/package.json index 274431b98ae..d4e321884ec 100644 --- a/frontend/providers/aiproxy/package.json +++ b/frontend/providers/aiproxy/package.json @@ -9,7 +9,8 @@ "lint": "next lint" }, "dependencies": { - "@sealos/ui": "workspace:^", + "@sealos/ui": "workspace:*", + "@tanstack/react-query": "^4.35.3", "@tanstack/react-table": "^8.10.7", "accept-language": "^3.0.20", "axios": "^1.7.7", @@ -18,18 +19,18 @@ "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", - "jsonwebtoken": "^9.0.2", - "react-dom": "^18", "react-day-picker": "^8.8.2", + "react-dom": "^18", "react-hook-form": "^7.46.2", - "sealos-desktop-sdk": "workspace:^", - "immer": "^10.1.1", - "@tanstack/react-query": "^4.35.3", + "sealos-desktop-sdk": "workspace:*", "zustand": "^4.5.4" }, "devDependencies": { + "@types/jsonwebtoken": "^9.0.3", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/frontend/providers/aiproxy/utils/request.ts b/frontend/providers/aiproxy/utils/request.ts index aec3111f784..f559b7e801b 100644 --- a/frontend/providers/aiproxy/utils/request.ts +++ b/frontend/providers/aiproxy/utils/request.ts @@ -5,6 +5,7 @@ import axios, { AxiosResponse, AxiosRequestConfig } from 'axios' +import { getUserSession } from './user' const request = axios.create({ baseURL: '/', @@ -22,7 +23,7 @@ request.interceptors.request.use( let _headers: AxiosHeaders = config.headers //获取token,并将其添加至请求头中 - _headers['Authorization'] = config.headers.Authorization + _headers['Authorization'] = getUserSession() if (!config.headers || config.headers['Content-Type'] === '') { _headers['Content-Type'] = 'application/json' 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 +} From 436053de5d41dbdd68072bfbc625be31b754cf27 Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Thu, 31 Oct 2024 21:33:53 +0800 Subject: [PATCH 10/47] update api --- frontend/pnpm-lock.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index e73a917cc70..111a3422b8b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -483,7 +483,7 @@ importers: providers/aiproxy: dependencies: '@sealos/ui': - specifier: workspace:^ + specifier: workspace:* version: link:../../packages/ui '@tanstack/react-query': specifier: ^4.35.3 @@ -534,12 +534,15 @@ importers: specifier: ^7.46.2 version: 7.48.2(react@18.2.0) sealos-desktop-sdk: - specifier: workspace:^ + specifier: workspace:* version: link:../../packages/client-sdk zustand: specifier: ^4.5.4 version: 4.5.4(@types/react@18.2.37)(immer@10.1.1)(react@18.2.0) devDependencies: + '@types/jsonwebtoken': + specifier: ^9.0.3 + version: 9.0.5 '@types/node': specifier: ^20 version: 20.10.0 From 82039f6a5d61911d0a110e7aa2dc335abcc8aa19 Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Thu, 31 Oct 2024 22:29:39 +0800 Subject: [PATCH 11/47] done --- frontend/providers/aiproxy/api/platform.ts | 5 + .../aiproxy/app/[lng]/(user)/logs/page.tsx | 320 +++--------------- .../aiproxy/app/api/get-logs/route.ts | 35 +- .../aiproxy/app/api/get-models/route.ts | 4 - .../aiproxy/app/i18n/locales/en/common.json | 5 +- .../aiproxy/app/i18n/locales/zh/common.json | 4 +- 6 files changed, 63 insertions(+), 310 deletions(-) diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts index d9884155df9..6e5adcf0499 100644 --- a/frontend/providers/aiproxy/api/platform.ts +++ b/frontend/providers/aiproxy/api/platform.ts @@ -1,5 +1,10 @@ +import { QueryParams, SearchResponse } from '@/app/api/get-logs/route' import { GET, POST } from '@/utils/request' export const initAppConfig = () => GET('/api/init-app-config') export const getModels = () => GET('/api/get-models') + +export const getLogs = (params: QueryParams) => GET('/api/get-logs', params) + +export const getKeys = () => GET('/api/get-keys') diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index 5206fa091b5..7f953d17f98 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -1,280 +1,20 @@ 'use client' -import { useMemo, useState } from 'react' -import { Box, Flex, Grid, Input, Select } from '@chakra-ui/react' +import { Box, Flex } from '@chakra-ui/react' import { MySelect } 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 { LogForm } from '@/types/form' -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' import { LogItem } from '@/types/log' -import { BaseTable } from '@/components/table/baseTable' -import SwitchPage from '@/components/SwitchPage' import { useQuery } from '@tanstack/react-query' -import { getModels } from '@/api/platform' - -const mockModals = ['gpt-3.5-turbo', 'gpt-4o-mini', 'gpt-4'] - -const mockNames = [ - { - id: 1, - group: 'ns-admin', - key: 'ngjLFEFQaEudGOFKA2E6Cc64239644BcA045E57c9eE721F9', - status: 1, - name: 'test token', - quota: 0, - used_amount: 0, - request_count: 0, - models: null, - subnet: '', - created_at: 1729672144913, - accessed_at: -62135596800000, - expired_at: -62135596800000 - } -] - -const mockStatus = ['success', 'failed'] +import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table' -const mockLogItems: LogItem[] = [ - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - completion_price: 0.0035, - token_id: 1, - used_amount: 0.0002415, - prompt_tokens: 18, - completion_tokens: 51, - channel: 1, - code: 200, - created_at: 1730354956491 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - - completion_price: 0.0035, - token_id: 1, - used_amount: 0.000315, - prompt_tokens: 18, - completion_tokens: 72, - channel: 1, - code: 200, - created_at: 1730354922071 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - - completion_price: 0.0035, - token_id: 1, - used_amount: 0.000294, - prompt_tokens: 18, - completion_tokens: 66, - channel: 1, - code: 200, - created_at: 1730354860678 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - - completion_price: 0.0035, - token_id: 1, - used_amount: 0.0004375, - prompt_tokens: 18, - completion_tokens: 107, - channel: 1, - code: 200, - created_at: 1730354800680 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - completion_price: 0.0035, - token_id: 1, - used_amount: 0.0003605, - prompt_tokens: 18, - completion_tokens: 85, - channel: 1, - code: 200, - created_at: 1730088079991 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - - completion_price: 0.0035, - token_id: 1, - used_amount: 0.000329, - prompt_tokens: 18, - completion_tokens: 76, - channel: 1, - code: 200, - created_at: 1730087556437 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - id: 5, - completion_price: 0.0105, - token_id: 1, - used_amount: 0.0005775000000000001, - prompt_tokens: 18, - completion_tokens: 49, - channel: 1, - code: 200, - created_at: 1729847798155 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - id: 4, - completion_price: 0.0105, - token_id: 1, - used_amount: 0.000672, - prompt_tokens: 18, - completion_tokens: 58, - channel: 1, - code: 200, - created_at: 1729840920384 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - id: 3, - completion_price: 0.0105, - token_id: 1, - used_amount: 0.00034749225, - prompt_tokens: 99, - completion_tokens: 27, - channel: 1, - code: 200, - created_at: 1729840549119 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - id: 2, - completion_price: 0.0105, - token_id: 1, - used_amount: 0.00006638100000000001, - prompt_tokens: 18, - completion_tokens: 92, - channel: 1, - code: 200, - created_at: 1729840027914 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - id: 5, - completion_price: 0.0105, - token_id: 1, - used_amount: 0.0005775000000000001, - prompt_tokens: 18, - completion_tokens: 49, - channel: 1, - code: 200, - created_at: 1729847798155 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - id: 4, - completion_price: 0.0105, - token_id: 1, - used_amount: 0.000672, - prompt_tokens: 18, - completion_tokens: 58, - channel: 1, - code: 200, - created_at: 1729840920384 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - id: 3, - completion_price: 0.0105, - token_id: 1, - used_amount: 0.00034749225, - prompt_tokens: 99, - completion_tokens: 27, - channel: 1, - code: 200, - created_at: 1729840549119 - }, - { - token_name: 'test token', - endpoint: '/v1/chat/completions', - content: '', - group: 'ns-admin', - model: 'gpt-3.5-turbo', - price: 0.0035, - id: 2, - completion_price: 0.0105, - token_id: 1, - used_amount: 0.00006638100000000001, - prompt_tokens: 18, - completion_tokens: 92, - channel: 1, - code: 200, - created_at: 1729840027914 - } -] +const mockStatus = ['all', 'success', 'failed'] export default function Home(): React.JSX.Element { const { lng } = useI18n() @@ -286,16 +26,34 @@ export default function Home(): React.JSX.Element { return currentDate }) const [endTime, setEndTime] = useState(new Date()) - const [name, setName] = useState('') const [modelName, setModelName] = useState('') - const [createdAt, setCreatedAt] = useState(new Date()) - const [endedAt, setEndedAt] = useState(new Date()) const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(10) + const [pageSize, setPageSize] = useState(1) + const [logData, setLogData] = useState([]) + const [total, setTotal] = useState(0) + + const { data: models = [] } = useQuery(['getModels'], () => getModels()) + const { data: modelNames = [] } = useQuery(['getKeys'], () => getKeys()) + + useQuery( + ['getLogs', page, pageSize, name, modelName], + () => getLogs({ page, perPage: pageSize, token_name: name, model_name: modelName }), + { + onSuccess: (data) => { + console.log(data, 'data') + if (!data.logs) { + setLogData([]) + setTotal(0) + return + } + setLogData(data.logs) + setTotal(data.total) + } + } + ) - const { data: models } = useQuery(['getModels'], () => getModels()) - console.log(models) + console.log(models, logData, modelNames) const columns = useMemo[]>(() => { return [ @@ -335,7 +93,7 @@ export default function Home(): React.JSX.Element { }, []) const table = useReactTable({ - data: mockLogItems, + data: logData, columns, getCoreRowModel: getCoreRowModel() }) @@ -352,12 +110,13 @@ export default function Home(): React.JSX.Element { {t('logs.name')} ({ - value: item.name, - label: item.name + list={models.map((item) => ({ + value: item, + label: item }))} onchange={(val: string) => setName(val)} /> @@ -368,10 +127,11 @@ export default function Home(): React.JSX.Element { {t('logs.modal')} ({ + list={models.map((item) => ({ value: item, label: item }))} @@ -411,8 +171,8 @@ export default function Home(): React.JSX.Element { setPage(idx)} /> diff --git a/frontend/providers/aiproxy/app/api/get-logs/route.ts b/frontend/providers/aiproxy/app/api/get-logs/route.ts index 920800766f9..9595b6cfa71 100644 --- a/frontend/providers/aiproxy/app/api/get-logs/route.ts +++ b/frontend/providers/aiproxy/app/api/get-logs/route.ts @@ -1,34 +1,17 @@ +import { LogItem } from '@/types/log' import { parseJwtToken } from '@/utils/auth' import { NextRequest, NextResponse } from 'next/server' -interface LogInfo { - token_name: string - endpoint: string - content: string - group: string - model: string - price: number - id: number - completion_price: number - token_id: number - used_amount: number - prompt_tokens: number - completion_tokens: number - channel: number - code: number - created_at: number -} - -interface SearchResponse { +export interface SearchResponse { data: { - logs: LogInfo[] + logs: LogItem[] total: number } message: string success: boolean } -interface QueryParams { +export interface QueryParams { token_name?: string model_name?: string code?: string @@ -53,7 +36,7 @@ function validateParams(params: QueryParams): string | null { return null } -async function fetchLogs(params: QueryParams): Promise<{ logs: LogInfo[]; total: number }> { +async function fetchLogs(params: QueryParams): Promise<{ logs: LogItem[]; total: number }> { try { const url = new URL(`/api/logs/search`, global.AppConfig?.backend.aiproxy) @@ -78,6 +61,8 @@ async function fetchLogs(params: QueryParams): Promise<{ logs: LogInfo[]; total: const token = global.AppConfig?.auth.aiProxyBackendKey + // console.log(url) + const response = await fetch(url.toString(), { method: 'GET', headers: { @@ -105,14 +90,16 @@ async function fetchLogs(params: QueryParams): Promise<{ logs: LogInfo[]; total: } } +export const dynamic = 'force-dynamic' + export async function GET(request: NextRequest): Promise { try { await parseJwtToken(request.headers) const searchParams = request.nextUrl.searchParams const queryParams: QueryParams = { - page: parseInt(searchParams.get('p') || '1', 10), - perPage: parseInt(searchParams.get('per_page') || '10', 10), + 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, diff --git a/frontend/providers/aiproxy/app/api/get-models/route.ts b/frontend/providers/aiproxy/app/api/get-models/route.ts index 752fbcd236c..df97d8bc2c4 100644 --- a/frontend/providers/aiproxy/app/api/get-models/route.ts +++ b/frontend/providers/aiproxy/app/api/get-models/route.ts @@ -40,10 +40,6 @@ async function fetchModels(): Promise { export async function GET(request: NextRequest): Promise { try { - console.log(global.AppConfig) - - console.log(request.headers) - await parseJwtToken(request.headers) const models = await fetchModels() diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index 1efef50c7af..654613a4901 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -23,7 +23,10 @@ "modal": "Model", "prompt_tokens": "enter", "completion_tokens": "output", - "price": "price" + "price": "price", + "select_modal": "Please select a model", + "select_token_name": "Please select a name", + "model": "Model" }, "Page": "Page", "Total": "total" diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index da5f84d310f..cd9f1dcc9c1 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -24,7 +24,9 @@ "model": "模型", "prompt_tokens": "输入", "completion_tokens": "输出", - "price": "价格" + "price": "价格", + "select_modal": "请选择模型", + "select_token_name": "请选择名称" }, "Page": "页", "Total": "总数" From c38d2fdaf117babafee6fb79b99e7db021cb276c Mon Sep 17 00:00:00 2001 From: lim Date: Thu, 31 Oct 2024 16:45:47 +0000 Subject: [PATCH 12/47] ok --- .../aiproxy/app/[lng]/(user)/home/page.tsx | 7 +- .../aiproxy/app/api/create-key/route.ts | 107 +++++++ .../aiproxy/app/i18n/locales/en/common.json | 5 +- .../aiproxy/app/i18n/locales/zh/common.json | 5 +- .../aiproxy/components/user/KeyList.tsx | 291 ++++++++++++------ .../aiproxy/components/user/ModelList.tsx | 92 ++++++ .../providers/aiproxy/ui/icons/home/Icons.tsx | 44 +++ .../aiproxy/ui/svg/icons/modelist/openai.svg | 3 + 8 files changed, 449 insertions(+), 105 deletions(-) create mode 100644 frontend/providers/aiproxy/app/api/create-key/route.ts create mode 100644 frontend/providers/aiproxy/components/user/ModelList.tsx create mode 100644 frontend/providers/aiproxy/ui/icons/home/Icons.tsx create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/openai.svg diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx index 7d57476d297..2cd43a36c4c 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx @@ -1,10 +1,11 @@ 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 ( - + - ddddxx + ) 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..aa20935fe30 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/create-key/route.ts @@ -0,0 +1,107 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/auth' + +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.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({ + 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/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index 654613a4901..db03a45d9f4 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -29,5 +29,8 @@ "model": "Model" }, "Page": "Page", - "Total": "total" + "Total": "total", + "modelList": { + "title": "Supported models" + } } diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index cd9f1dcc9c1..07844f9450f 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -29,5 +29,8 @@ "select_token_name": "请选择名称" }, "Page": "页", - "Total": "总数" + "Total": "总数", + "modelList": { + "title": "可支持的模型" + } } diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index 058e9817694..e19ab9492a5 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -20,7 +20,8 @@ import { Text, Th, Thead, - Tr + Tr, + Tooltip } from '@chakra-ui/react' import { Column, @@ -34,13 +35,14 @@ import { TFunction } from 'i18next' import { useTranslationClientSide } from '@/app/i18n/client' import { useI18n } from '@/providers/i18n/i18nContext' +import { ChainIcon } from '@/ui/icons/home/Icons' export function KeyList(): JSX.Element { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') return ( <> - + - - - - - API Endpoint: - https://www.aiproxy.com - - - + + + + + + + API Endpoint: + + + + https://www.aiproxy.com + + + + + @@ -100,7 +126,17 @@ export enum TableHeaderId { } const CustomHeader = ({ column, t }: { column: Column; t: TFunction }) => { - return {t(column.id as TableHeaderId)} + return ( + + {t(column.id as TableHeaderId)} + + ) } const TableDemo = ({ t }: { t: TFunction }) => { @@ -129,39 +165,140 @@ const TableDemo = ({ t }: { t: TFunction }) => { columnHelper.accessor((row) => row.name, { id: TableHeaderId.NAME, header: (props) => , - cell: (info) => info.getValue() + cell: (info) => ( + + {info.getValue()} + + ) }), columnHelper.accessor((row) => row.key, { id: TableHeaderId.KEY, header: (props) => , - cell: (info) => info.getValue() + cell: (info) => ( + + {info.getValue()} + + ) }), columnHelper.accessor((row) => row.createdAt, { id: TableHeaderId.CREATED_AT, header: (props) => , - cell: (info) => info.getValue() + cell: (info) => ( + + {info.getValue()} + + ) }), columnHelper.accessor((row) => row.lastUsedAt, { id: TableHeaderId.LAST_USED_AT, header: (props) => , - cell: (info) => info.getValue() + cell: (info) => ( + + {info.getValue()} + + ) }), columnHelper.accessor((row) => row.status, { id: TableHeaderId.STATUS, header: (props) => , - cell: (info) => info.getValue() + cell: (info) => ( + + {info.getValue()} + + ) }), columnHelper.display({ id: TableHeaderId.ACTIONS, header: (props) => , cell: (info) => ( - Actions - // handleStatusChange(info.row.original.id)} - // onDelete={() => handleDelete(info.row.original.id)} - // /> + + + + + + + + + + + + + + + + + ) }) ] @@ -173,25 +310,32 @@ const TableDemo = ({ t }: { t: TFunction }) => { }) return ( - - - + + +
{table.getHeaderGroups().map((headerGroup) => ( + sx={{ + // 移除表头的下边线 + th: { + borderBottom: 'none' // 移除所有表头单元格的下边线 + }, + 'th:first-of-type': { + borderTopLeftRadius: '6px', + borderBottomLeftRadius: '6px' + }, + 'th:last-of-type': { + borderTopRightRadius: '6px', + borderBottomRightRadius: '6px' + } + }}> {headerGroup.headers.map((header) => ( - ))} @@ -202,19 +346,12 @@ const TableDemo = ({ t }: { t: TFunction }) => { {table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( - + ))} ))} @@ -225,50 +362,4 @@ const TableDemo = ({ t }: { t: TFunction }) => { ) } -function SwitchPage({ table, t }: { table: Table; t: TFunction }) { - return ( - - - - - - - - {t('pagination.page')} - - {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} - - - - - - ) -} - 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..a76ce4ec27e --- /dev/null +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -0,0 +1,92 @@ +'use client' +import { Badge, Box, Flex, 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 OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg' +import Image, { StaticImageData } from 'next/image' + +const IconList: Record = { + OpenAI: OpenAIIcon +} + +const modes = { + OpenAI: { + render: () => { + return ( + + + + OpenAI + + + ) + } + } +} + +const ModelList: React.FC = () => { + const { lng } = useI18n() + const { t } = useTranslationClientSide(lng, 'common') + return ( + <> + + + + + {t('modelList.title')} + + + + 23 + + + + + + {/* 第二个 Flex 容器用于标题和描述 */} + + + + + OpenAI + + + + + ) +} + +export default ModelList 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/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 From 7526107378c0182e9a0ef2b5cb5a02e8a9466d1c Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Fri, 1 Nov 2024 10:30:17 +0800 Subject: [PATCH 13/47] done --- frontend/providers/aiproxy/api/platform.ts | 3 +- .../aiproxy/app/[lng]/(user)/logs/page.tsx | 186 +++++++++++------- .../aiproxy/app/api/get-keys/route.ts | 6 +- .../aiproxy/app/i18n/locales/en/common.json | 4 +- .../aiproxy/app/i18n/locales/zh/common.json | 4 +- .../components/SelectDateRange/index.tsx | 5 +- 6 files changed, 133 insertions(+), 75 deletions(-) diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts index 6e5adcf0499..5ccbb24007c 100644 --- a/frontend/providers/aiproxy/api/platform.ts +++ b/frontend/providers/aiproxy/api/platform.ts @@ -1,3 +1,4 @@ +import { KeysSearchResponse } from '@/app/api/get-keys/route' import { QueryParams, SearchResponse } from '@/app/api/get-logs/route' import { GET, POST } from '@/utils/request' @@ -7,4 +8,4 @@ export const getModels = () => GET('/api/get-models') export const getLogs = (params: QueryParams) => GET('/api/get-logs', params) -export const getKeys = () => GET('/api/get-keys') +export const getKeys = () => GET('/api/get-keys') diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index 7f953d17f98..77e95785896 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -1,7 +1,7 @@ 'use client' -import { Box, Flex } from '@chakra-ui/react' -import { MySelect } from '@sealos/ui' +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' @@ -34,7 +34,7 @@ export default function Home(): React.JSX.Element { const [total, setTotal] = useState(0) const { data: models = [] } = useQuery(['getModels'], () => getModels()) - const { data: modelNames = [] } = useQuery(['getKeys'], () => getKeys()) + const { data: modelNameData } = useQuery(['getKeys'], () => getKeys()) useQuery( ['getLogs', page, pageSize, name, modelName], @@ -53,7 +53,7 @@ export default function Home(): React.JSX.Element { } ) - console.log(models, logData, modelNames) + console.log(models, logData, modelNameData?.tokens) const columns = useMemo[]>(() => { return [ @@ -85,9 +85,20 @@ export default function Home(): React.JSX.Element { id: 'created_at' }, { - header: t('logs.price'), - accessorFn: (row) => `${row.price}/${row.completion_price}`, - id: 'price' + accessorKey: 'completion_price', + id: 'price', + header: () => { + return ( + + + + {t('logs.total_price')} + + + + + ) + } } ] }, []) @@ -101,71 +112,112 @@ export default function Home(): React.JSX.Element { return ( - - {t('logs.call_log')} - - - - - {t('logs.name')} - - ({ - value: item, - label: item - }))} - onchange={(val: string) => setName(val)} - /> - + + {t('logs.call_log')} + + - - - {t('logs.modal')} - - ({ - value: item, - label: item - }))} - onchange={(val: string) => setModelName(val)} - /> - + + + + + {t('logs.name')} + + ({ + value: item.name, + label: item.name + })) || [] + } + onchange={(val: string) => setName(val)} + /> + - - - {t('logs.status')} - - ({ - value: item, - label: item - }))} - onchange={(val: string) => setModelName(val)} - /> + + + {t('logs.modal')} + + ({ + value: item, + label: item + })) || [] + } + onchange={(val: string) => setModelName(val)} + /> + - - - {t('logs.time')} - - + + + + + {t('logs.status')} + + ({ + value: item, + label: item + }))} + onchange={(val: string) => setModelName(val)} + /> + + + + {t('logs.time')} + + + + From 471f6dbc854ce67c873c21aada645a939d60261b Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Fri, 1 Nov 2024 10:53:36 +0800 Subject: [PATCH 14/47] logs done --- .../aiproxy/app/[lng]/(user)/logs/page.tsx | 31 ++++++++---- .../aiproxy/app/api/get-logs/route.ts | 13 ++--- .../aiproxy/components/table/baseTable.tsx | 48 +++++++++++-------- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index 77e95785896..aac37dd22de 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -29,19 +29,26 @@ export default function Home(): React.JSX.Element { const [name, setName] = useState('') const [modelName, setModelName] = useState('') const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(1) + const [pageSize, setPageSize] = useState(10) const [logData, setLogData] = useState([]) const [total, setTotal] = useState(0) const { data: models = [] } = useQuery(['getModels'], () => getModels()) const { data: modelNameData } = useQuery(['getKeys'], () => getKeys()) - useQuery( - ['getLogs', page, pageSize, name, modelName], - () => getLogs({ page, perPage: pageSize, token_name: name, model_name: modelName }), + 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) => { - console.log(data, 'data') if (!data.logs) { setLogData([]) setTotal(0) @@ -53,7 +60,7 @@ export default function Home(): React.JSX.Element { } ) - console.log(models, logData, modelNameData?.tokens) + console.log(logData, models, modelNameData) const columns = useMemo[]>(() => { return [ @@ -174,12 +181,18 @@ export default function Home(): React.JSX.Element { height="32px" value={modelName} list={ - models.map((item) => ({ + ['all', ...models].map((item) => ({ value: item, label: item })) || [] } - onchange={(val: string) => setModelName(val)} + onchange={(val: string) => { + if (val === 'all') { + setModelName('') + } else { + setModelName(val) + } + }} /> @@ -219,7 +232,7 @@ export default function Home(): React.JSX.Element { - + { +async function fetchLogs( + params: QueryParams, + group: string +): Promise<{ logs: LogItem[]; total: number }> { try { - const url = new URL(`/api/logs/search`, global.AppConfig?.backend.aiproxy) + const url = new URL(`/api/log/${group}/search`, global.AppConfig?.backend.aiproxy) url.searchParams.append('p', params.page.toString()) url.searchParams.append('per_page', params.perPage.toString()) @@ -90,11 +93,9 @@ async function fetchLogs(params: QueryParams): Promise<{ logs: LogItem[]; total: } } -export const dynamic = 'force-dynamic' - export async function GET(request: NextRequest): Promise { try { - await parseJwtToken(request.headers) + const group = await parseJwtToken(request.headers) const searchParams = request.nextUrl.searchParams const queryParams: QueryParams = { @@ -119,7 +120,7 @@ export async function GET(request: NextRequest): Promise { ) } - const { logs, total } = await fetchLogs(queryParams) + const { logs, total } = await fetchLogs(queryParams, group) return NextResponse.json({ code: 200, diff --git a/frontend/providers/aiproxy/components/table/baseTable.tsx b/frontend/providers/aiproxy/components/table/baseTable.tsx index 4539be71d2b..64a85dd0b16 100644 --- a/frontend/providers/aiproxy/components/table/baseTable.tsx +++ b/frontend/providers/aiproxy/components/table/baseTable.tsx @@ -1,4 +1,5 @@ import { + Spinner, Table, TableContainer, TableContainerProps, @@ -11,8 +12,9 @@ import { import { Table as ReactTable, flexRender } from '@tanstack/react-table' export function BaseTable({ - table -}: { table: ReactTable } & TableContainerProps) { + table, + isLoading +}: { table: ReactTable; isLoading: boolean } & TableContainerProps) { return (
+ {flexRender(header.column.columnDef.header, header.getContext())}
- {flexRender(cell.column.columnDef.cell, cell.getContext())} - {flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -42,23 +44,31 @@ export function BaseTable({ })} - {table.getRowModel().rows.map((item) => { - return ( - - {item.getAllCells().map((cell, i) => { - return ( - - ) - })} - - ) - })} + {isLoading ? ( + + + + ) : ( + table.getRowModel().rows.map((item) => { + return ( + + {item.getAllCells().map((cell, i) => { + return ( + + ) + })} + + ) + }) + )}
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
From 6e764f3b4ff571067ae539b1676fc2f1ba5a9f5b Mon Sep 17 00:00:00 2001 From: lim Date: Fri, 1 Nov 2024 01:40:18 +0000 Subject: [PATCH 15/47] ok --- .../aiproxy/app/i18n/locales/en/common.json | 16 +- .../aiproxy/app/i18n/locales/zh/common.json | 16 +- .../aiproxy/components/user/KeyList.tsx | 228 +++++++++++++++++- 3 files changed, 250 insertions(+), 10 deletions(-) diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index 8ef377ed163..0f7872be4fd 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -13,7 +13,13 @@ "name": "name", "createdAt": "creation time", "lastUsedAt": "last use time", - "status": "state" + "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" }, "logs": { "call_log": "call log", @@ -34,5 +40,11 @@ "Total": "total", "modelList": { "title": "Supported models" - } + }, + "copy": "copy", + "createKey": "New", + "Key": { + "create": "Create a new AI Proxy" + }, + "confirm": "confirm" } diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index d0662fe1924..015a975ce19 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -13,7 +13,13 @@ "name": "名称", "createdAt": "创建时间", "lastUsedAt": "最后使用时间", - "status": "状态" + "status": "状态", + "namePlaceholder": "请输入名称", + "nameRequired": "请输入 key 名字", + "nameMaxLength": "key 长度不合法", + "nameOnlyLettersAndNumbers": "key 名字包含特殊字符", + "createSuccess": "创建成功", + "createFailed": "创建失败" }, "logs": { "call_log": "调用日志", @@ -34,5 +40,11 @@ "Total": "总数", "modelList": { "title": "可支持的模型" - } + }, + "copy": "复制", + "createKey": "新建", + "Key": { + "create": "新建 AI Proxy" + }, + "confirm": "确认" } diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index e19ab9492a5..c7f25fae1aa 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -1,6 +1,5 @@ 'use client' -import { useState } from 'react' -import { ChevronLeftIcon, ChevronRightIcon, PlusSquareIcon } from '@chakra-ui/icons' +import React, { useState } from 'react' import { Box, Button, @@ -21,7 +20,17 @@ import { Th, Thead, Tr, - Tooltip + Tooltip, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + FormControl, + Input, + FormErrorMessage, + useDisclosure } from '@chakra-ui/react' import { Column, @@ -31,15 +40,20 @@ import { getPaginationRowModel, useReactTable } from '@tanstack/react-table' +import { ApiResp } from '@/types/api' import { TFunction } from 'i18next' import { useTranslationClientSide } from '@/app/i18n/client' import { useI18n } from '@/providers/i18n/i18nContext' import { ChainIcon } from '@/ui/icons/home/Icons' +import { useMutation } from '@tanstack/react-query' +import { useMessage } from '@sealos/ui' +import { useQueryClient } from '@tanstack/react-query' export function KeyList(): JSX.Element { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') + const { isOpen, onOpen, onClose } = useDisclosure() return ( <> @@ -56,6 +70,7 @@ export function KeyList(): JSX.Element { + {/* header */} @@ -78,7 +93,7 @@ export function KeyList(): JSX.Element { textDecoration="none" _hover={{ textDecoration: 'underline' }} cursor="pointer"> - + https://www.aiproxy.com @@ -93,11 +108,15 @@ export function KeyList(): JSX.Element { bg="grayModern.900" color="white" boxShadow="0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)" - _hover={{ bg: 'grayModern.800' }}> - 新建 + _hover={{ bg: 'grayModern.800' }} + onClick={onOpen}> + {t('createKey')} + {/* table */} + {/* modal */} + ) @@ -362,4 +381,201 @@ const TableDemo = ({ t }: { t: TFunction }) => { ) } +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) => + request.post>('api/create-key', { + name + }), + { + onSuccess(data) { + createKeyMutation.reset() + setName('') + queryClient.invalidateQueries(['getAccount']) // Invalidate the cache + 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?.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.name')} + + + + {error && {error}} + + + {/* button */} + + + + + + + + ) +} + export default KeyList From 8e2348189cf81e2a095bda33ec4995e2df104311 Mon Sep 17 00:00:00 2001 From: lim Date: Fri, 1 Nov 2024 01:45:59 +0000 Subject: [PATCH 16/47] ok --- frontend/providers/aiproxy/api/platform.ts | 2 + .../aiproxy/components/user/KeyList.tsx | 53 +++++++++---------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts index 5ccbb24007c..c890f938754 100644 --- a/frontend/providers/aiproxy/api/platform.ts +++ b/frontend/providers/aiproxy/api/platform.ts @@ -9,3 +9,5 @@ export const getModels = () => GET('/api/get-models') export const getLogs = (params: QueryParams) => GET('/api/get-logs', params) export const getKeys = () => GET('/api/get-keys') + +export const createKey = (name: string) => POST('/api/create-key', { name }) diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index c7f25fae1aa..751cba4f7fe 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -42,6 +42,7 @@ import { } from '@tanstack/react-table' import { ApiResp } from '@/types/api' import { TFunction } from 'i18next' +import { createKey } from '@/api/platform' import { useTranslationClientSide } from '@/app/i18n/client' import { useI18n } from '@/providers/i18n/i18nContext' @@ -404,36 +405,30 @@ function CreateKeyModal({ successIconFill: 'white' }) - const createKeyMutation = useMutation( - (name: string) => - request.post>('api/create-key', { - name - }), - { - onSuccess(data) { - createKeyMutation.reset() - setName('') - queryClient.invalidateQueries(['getAccount']) // Invalidate the cache - 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?.message || t('key.createFailed'), - isClosable: true, - position: 'top' - }) - } + const createKeyMutation = useMutation((name: string) => createKey(name), { + onSuccess(data) { + createKeyMutation.reset() + setName('') + queryClient.invalidateQueries(['getAccount']) // Invalidate the cache + 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?.message || t('key.createFailed'), + isClosable: true, + position: 'top' + }) } - ) + }) const validateName = (value: string) => { if (!value) { From 7cd2c4cda0f9f2c5d597f83526f3917ec56aef00 Mon Sep 17 00:00:00 2001 From: lim Date: Fri, 1 Nov 2024 03:00:22 +0000 Subject: [PATCH 17/47] ok --- frontend/providers/aiproxy/api/platform.ts | 1 + .../aiproxy/app/[lng]/(user)/home/page.tsx | 8 +- .../aiproxy/app/api/get-keys/route.ts | 28 ++----- .../aiproxy/components/user/KeyList.tsx | 84 +++++++++---------- frontend/providers/aiproxy/types/getKeys.d.ts | 15 ++++ 5 files changed, 67 insertions(+), 69 deletions(-) create mode 100644 frontend/providers/aiproxy/types/getKeys.d.ts diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts index c890f938754..c923dc150b5 100644 --- a/frontend/providers/aiproxy/api/platform.ts +++ b/frontend/providers/aiproxy/api/platform.ts @@ -1,5 +1,6 @@ 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 } from '@/utils/request' export const initAppConfig = () => GET('/api/init-app-config') diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx index 2cd43a36c4c..ecba3e02b6d 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx @@ -5,7 +5,7 @@ import ModelList from '@/components/user/ModelList' export default function Home(): JSX.Element { return ( - + @@ -28,7 +30,9 @@ export default function Home(): JSX.Element { flexDirection="column" justifyContent="flex-start" alignItems="flex-start" - gap="22px"> + gap="22px" + h="full" + w="full"> diff --git a/frontend/providers/aiproxy/app/api/get-keys/route.ts b/frontend/providers/aiproxy/app/api/get-keys/route.ts index 8490aa59f82..c79db9f10cd 100644 --- a/frontend/providers/aiproxy/app/api/get-keys/route.ts +++ b/frontend/providers/aiproxy/app/api/get-keys/route.ts @@ -1,23 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' +import { TokenInfo } from '@/types/getKeys' import { parseJwtToken } from '@/utils/auth' -export interface 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 -} - export interface KeysSearchResponse { data: { tokens: TokenInfo[] @@ -27,11 +12,12 @@ export interface KeysSearchResponse { success: boolean } -function validateParams(group: string, page: number, perPage: number): string | null { - if (!group) { - return 'Group parameter is required' - } +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' } @@ -94,7 +80,7 @@ export async function GET(request: NextRequest): Promise { const page = parseInt(searchParams.get('p') || '1', 10) const perPage = parseInt(searchParams.get('per_page') || '10', 10) - const validationError = validateParams(group, page, perPage) + const validationError = validateParams(page, perPage) if (validationError) { return NextResponse.json( { diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index 751cba4f7fe..71f38add833 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -4,16 +4,13 @@ import { Box, Button, Flex, - HStack, Icon, Popover, PopoverBody, PopoverContent, PopoverTrigger, - Select, Table, TableContainer, - Tag, Tbody, Td, Text, @@ -37,12 +34,11 @@ import { createColumnHelper, flexRender, getCoreRowModel, - getPaginationRowModel, useReactTable } from '@tanstack/react-table' -import { ApiResp } from '@/types/api' +import { useQuery } from '@tanstack/react-query' import { TFunction } from 'i18next' -import { createKey } from '@/api/platform' +import { createKey, getKeys } from '@/api/platform' import { useTranslationClientSide } from '@/app/i18n/client' import { useI18n } from '@/providers/i18n/i18nContext' @@ -50,6 +46,8 @@ import { ChainIcon } from '@/ui/icons/home/Icons' import { useMutation } from '@tanstack/react-query' import { useMessage } from '@sealos/ui' import { useQueryClient } from '@tanstack/react-query' +import { TokenInfo } from '@/types/getKeys' +import SwitchPage from '@/components/SwitchPage' export function KeyList(): JSX.Element { const { lng } = useI18n() @@ -115,7 +113,7 @@ export function KeyList(): JSX.Element { {/* table */} - + {/* modal */}
@@ -123,19 +121,6 @@ export function KeyList(): JSX.Element { ) } -function KeyItem({ t }: { t: TFunction }): JSX.Element { - return -} - -type KeyItem = { - id: number - name: string - key: string - createdAt: string - lastUsedAt: string - status: 'active' | 'inactive' -} - export enum TableHeaderId { NAME = 'key.name', KEY = 'key.key', @@ -145,7 +130,7 @@ export enum TableHeaderId { ACTIONS = 'key.actions' } -const CustomHeader = ({ column, t }: { column: Column; t: TFunction }) => { +const CustomHeader = ({ column, t }: { column: Column; t: TFunction }) => { return ( ; t: TFunction }) ) } -const TableDemo = ({ t }: { t: TFunction }) => { - const [data] = useState([ - { - id: 1, - name: '1234567890', - key: '1234567890', - createdAt: '2021-01-01', - lastUsedAt: '2021-01-01', - status: 'active' - }, - { - id: 2, - name: '1234567890', - key: '1234567890', - createdAt: '2021-01-01', - lastUsedAt: '2021-01-01', - status: 'inactive' +const ModelKeyTable = ({ t }: { t: TFunction }) => { + const [keys, setKeys] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + + useQuery(['getKeys', page, pageSize], () => getKeys({ page, perPage: pageSize }), { + onSuccess: (data) => { + console.log(data, 'data') + if (!data.tokens) { + setKeys([]) + setTotal(0) + return + } + setKeys(data.tokens) + setTotal(data.total) } - ]) + }) - const columnHelper = createColumnHelper() + const columnHelper = createColumnHelper() const columns = [ columnHelper.accessor((row) => row.name, { @@ -212,7 +196,7 @@ const TableDemo = ({ t }: { t: TFunction }) => { ) }), - columnHelper.accessor((row) => row.createdAt, { + columnHelper.accessor((row) => row.created_at, { id: TableHeaderId.CREATED_AT, header: (props) => , cell: (info) => ( @@ -223,11 +207,11 @@ const TableDemo = ({ t }: { t: TFunction }) => { fontWeight={500} lineHeight="16px" letterSpacing="0.5px"> - {info.getValue()} + {new Date(info.getValue()).toLocaleString()} ) }), - columnHelper.accessor((row) => row.lastUsedAt, { + columnHelper.accessor((row) => row.accessed_at, { id: TableHeaderId.LAST_USED_AT, header: (props) => , cell: (info) => ( @@ -238,7 +222,7 @@ const TableDemo = ({ t }: { t: TFunction }) => { fontWeight={500} lineHeight="16px" letterSpacing="0.5px"> - {info.getValue()} + {info.getValue() ? new Date(info.getValue()).toLocaleString() : '-'} ) }), @@ -324,13 +308,13 @@ const TableDemo = ({ t }: { t: TFunction }) => { ] const table = useReactTable({ - data, + data: keys, columns, getCoreRowModel: getCoreRowModel() }) return ( - + @@ -378,6 +362,14 @@ const TableDemo = ({ t }: { t: TFunction }) => {
+ setPage(idx)} + />
) } 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 +} From d871ec9327cf24d33fed93926eb26fbaa77b8f7d Mon Sep 17 00:00:00 2001 From: lim Date: Fri, 1 Nov 2024 06:53:20 +0000 Subject: [PATCH 18/47] ok --- frontend/providers/aiproxy/api/platform.ts | 11 +- .../aiproxy/app/[lng]/(user)/layout.tsx | 21 +- .../aiproxy/app/api/delete-key/[id]/route.ts | 75 +++ .../aiproxy/app/api/get-keys/route.ts | 6 +- .../aiproxy/app/api/update-key/[id]/route.ts | 94 ++++ .../aiproxy/app/i18n/locales/en/common.json | 22 +- .../aiproxy/app/i18n/locales/zh/common.json | 22 +- .../aiproxy/components/user/KeyList.tsx | 490 ++++++++++++++---- .../aiproxy/components/user/ModelList.tsx | 79 +-- 9 files changed, 665 insertions(+), 155 deletions(-) create mode 100644 frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts create mode 100644 frontend/providers/aiproxy/app/api/update-key/[id]/route.ts diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts index c923dc150b5..48b4dfae425 100644 --- a/frontend/providers/aiproxy/api/platform.ts +++ b/frontend/providers/aiproxy/api/platform.ts @@ -1,14 +1,19 @@ 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 } from '@/utils/request' +import { GET, POST, DELETE } from '@/utils/request' -export const initAppConfig = () => GET('/api/init-app-config') +export const initAppConfig = () => GET<{ aiproxyBackend: string }>('/api/init-app-config') export const getModels = () => GET('/api/get-models') export const getLogs = (params: QueryParams) => GET('/api/get-logs', params) -export const getKeys = () => GET('/api/get-keys') +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]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index 4bc3dc5b474..451c1606779 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -7,14 +7,10 @@ import { EVENT_NAME } from 'sealos-desktop-sdk' import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' import { useEffect } from 'react' import { initAppConfig } from '@/api/platform' +import { useI18n } from '@/providers/i18n/i18nContext' -export default function UserLayout({ - children, - params -}: { - children: React.ReactNode - params: { lng: string } -}) { +export default function UserLayout({ children }: { children: React.ReactNode }) { + const { lng } = useI18n() // init session useEffect(() => { const response = createSealosApp() @@ -39,7 +35,14 @@ export default function UserLayout({ }, []) useEffect(() => { - initAppConfig() + const initConfig = async () => { + const { aiproxyBackend } = await initAppConfig() + // 删除已存在的 aiproxyBackend,然后重新存储 + localStorage.removeItem('aiproxyBackend') + localStorage.setItem('aiproxyBackend', aiproxyBackend) + } + + initConfig() // const changeI18n = async (data: any) => { // const lastLang = getcl() @@ -71,7 +74,7 @@ export default function UserLayout({ return ( - + {/* Main Content */} {children} 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..7229ce34269 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts @@ -0,0 +1,75 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/auth' + +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.aiproxy) + const token = global.AppConfig?.auth.aiProxyBackendKey + console.log(url.toString()) + + const response = await fetch(url.toString(), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + } + }) + + 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 index c79db9f10cd..fa07bf9c149 100644 --- a/frontend/providers/aiproxy/app/api/get-keys/route.ts +++ b/frontend/providers/aiproxy/app/api/get-keys/route.ts @@ -36,6 +36,8 @@ async function fetchTokens( ): Promise<{ tokens: TokenInfo[]; total: number }> { try { const url = new URL(`/api/token/${group}/search`, global.AppConfig?.backend.aiproxy) + console.log(perPage) + console.log(page) url.searchParams.append('p', page.toString()) url.searchParams.append('per_page', perPage.toString()) @@ -77,8 +79,8 @@ export async function GET(request: NextRequest): Promise { const group = await parseJwtToken(request.headers) const searchParams = request.nextUrl.searchParams - const page = parseInt(searchParams.get('p') || '1', 10) - const perPage = parseInt(searchParams.get('per_page') || '10', 10) + const page = parseInt(searchParams.get('page') || '1', 10) + const perPage = parseInt(searchParams.get('perPage') || '10', 10) const validationError = validateParams(page, perPage) if (validationError) { 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..a113ba7a8e8 --- /dev/null +++ b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts @@ -0,0 +1,94 @@ +import { NextRequest, NextResponse } from 'next/server' +import { parseJwtToken } from '@/utils/auth' + +interface UpdateTokenResponse { + message: string + success: boolean +} + +interface UpdateTokenBody { + status: number +} + +async function updateToken(group: string, id: string, status: number): Promise { + try { + const url = new URL(`/api/token/${group}/${id}/status`, 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 }) + }) + + 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/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index 0f7872be4fd..97f913bb832 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -19,7 +19,12 @@ "nameMaxLength": "Key length is illegal", "nameOnlyLettersAndNumbers": "key name contains special characters", "createSuccess": "Created successfully", - "createFailed": "Creation failed" + "createFailed": "Creation failed", + "actions": "operate", + "deleteSuccess": "Delete key successfully", + "deleteFailed": "Failed to delete key", + "updateSuccess": "Update status successful", + "updateFailed": "Update status failed" }, "logs": { "call_log": "call log", @@ -46,5 +51,18 @@ "Key": { "create": "Create a new AI Proxy" }, - "confirm": "confirm" + "confirm": "confirm", + "keystatus": { + "enabled": "enable", + "disabled": "Disable", + "expired": "Expired", + "exhausted": "exhausted", + "unknown": "unknown" + }, + "delete": "delete", + "disable": "Disable", + "enable": "enable", + "copySuccess": "Copied successfully", + "copyFailed": "Copy failed", + "noData": "You don’t have an AI Proxy yet" } diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index 015a975ce19..02e94b478cc 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -19,7 +19,12 @@ "nameMaxLength": "key 长度不合法", "nameOnlyLettersAndNumbers": "key 名字包含特殊字符", "createSuccess": "创建成功", - "createFailed": "创建失败" + "createFailed": "创建失败", + "actions": "操作", + "deleteSuccess": "删除成功", + "deleteFailed": "删除失败", + "updateSuccess": "状态更新成功", + "updateFailed": "状态更新失败" }, "logs": { "call_log": "调用日志", @@ -46,5 +51,18 @@ "Key": { "create": "新建 AI Proxy" }, - "confirm": "确认" + "confirm": "确认", + "keystatus": { + "enabled": "启用", + "disabled": "禁用", + "expired": "过期", + "exhausted": "耗尽", + "unknown": "未知" + }, + "delete": "删除", + "disable": "禁用", + "enable": "启用", + "copySuccess": "复制成功", + "copyFailed": "复制失败", + "noData": "你还没有 AI Proxy" } diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index 71f38add833..6851ebda024 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -4,7 +4,6 @@ import { Box, Button, Flex, - Icon, Popover, PopoverBody, PopoverContent, @@ -27,7 +26,8 @@ import { FormControl, Input, FormErrorMessage, - useDisclosure + useDisclosure, + Center } from '@chakra-ui/react' import { Column, @@ -36,16 +36,14 @@ import { getCoreRowModel, useReactTable } from '@tanstack/react-table' -import { useQuery } from '@tanstack/react-query' import { TFunction } from 'i18next' -import { createKey, getKeys } from '@/api/platform' +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 { useMutation } from '@tanstack/react-query' import { useMessage } from '@sealos/ui' -import { useQueryClient } from '@tanstack/react-query' +import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query' import { TokenInfo } from '@/types/getKeys' import SwitchPage from '@/components/SwitchPage' @@ -53,6 +51,14 @@ export function KeyList(): JSX.Element { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') const { isOpen, onOpen, onClose } = useDisclosure() + 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 ( <> @@ -68,7 +74,7 @@ export function KeyList(): JSX.Element { - + {/* header */} @@ -91,9 +97,32 @@ export function KeyList(): JSX.Element { letterSpacing="0.1px" textDecoration="none" _hover={{ textDecoration: 'underline' }} - cursor="pointer"> + cursor="pointer" + onClick={() => { + const endpoint = localStorage.getItem('aiproxyBackend') || 'https://www.aiproxy.com' + 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' + }) + } + ) + }}> - https://www.aiproxy.com + {localStorage.getItem('aiproxyBackend') || 'https://www.aiproxy.com'} @@ -113,7 +142,7 @@ export function KeyList(): JSX.Element { {/* table */} - + {/* modal */} @@ -129,6 +158,12 @@ export enum TableHeaderId { 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 ( @@ -144,15 +179,81 @@ const CustomHeader = ({ column, t }: { column: Column; t: TFunction } ) } -const ModelKeyTable = ({ t }: { t: TFunction }) => { +const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { const [keys, setKeys] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) + 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) + } + useQuery(['getKeys', page, pageSize], () => getKeys({ page, perPage: pageSize }), { onSuccess: (data) => { - console.log(data, 'data') if (!data.tokens) { setKeys([]) setTotal(0) @@ -229,17 +330,45 @@ const ModelKeyTable = ({ t }: { t: TFunction }) => { columnHelper.accessor((row) => row.status, { id: TableHeaderId.STATUS, header: (props) => , - cell: (info) => ( - - {info.getValue()} - - ) + 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({ @@ -248,7 +377,13 @@ const ModelKeyTable = ({ t }: { t: TFunction }) => { cell: (info) => ( - + { - - - - + + + + {info.row.original.status === KeyStatus.DISABLED ? ( + + ) : ( + + )} @@ -315,61 +538,122 @@ const ModelKeyTable = ({ t }: { t: TFunction }) => { return ( - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + {keys.length === 0 ? ( + +
+ + + + + + + + + + {t('noData')} + + + + + +
+
+ ) : ( + <> + +
- {flexRender(header.column.columnDef.header, header.getContext())} -
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + ))} - - ))} - -
+ {flexRender(header.column.columnDef.header, header.getContext())} +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
-
- setPage(idx)} - /> + + + + setPage(idx)} + /> + + )}
) } @@ -401,7 +685,7 @@ function CreateKeyModal({ onSuccess(data) { createKeyMutation.reset() setName('') - queryClient.invalidateQueries(['getAccount']) // Invalidate the cache + queryClient.invalidateQueries(['getKeys']) message({ status: 'success', title: t('key.createSuccess'), diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx index a76ce4ec27e..b104d7190d5 100644 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -1,39 +1,51 @@ 'use client' -import { Badge, Box, Flex, Text } from '@chakra-ui/react' +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 OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg' import Image, { StaticImageData } from 'next/image' +import { useQuery } from '@tanstack/react-query' +import { getModels } from '@/api/platform' const IconList: Record = { OpenAI: OpenAIIcon } -const modes = { - OpenAI: { - render: () => { - return ( - - OpenAI - - OpenAI - - - ) - } - } +type ModelKey = 'gpt-4o-mini' | 'gpt-4' | 'gpt-3.5-turbo' + +const createModelRender = (modelName: string) => { + const ModelComponent = () => ( + + OpenAI + + {modelName} + + + ) + ModelComponent.displayName = `Model_${modelName}` + return ModelComponent +} + +const modes: Record JSX.Element }> = { + 'gpt-4o-mini': { render: createModelRender('gpt-4o-mini') }, + 'gpt-4': { render: createModelRender('gpt-4') }, + 'gpt-3.5-turbo': { render: createModelRender('gpt-3.5-turbo') } } const ModelList: React.FC = () => { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') + + const { isLoading, data } = useQuery(['getModels'], () => getModels()) + console.log(data) + return ( <> @@ -64,26 +76,25 @@ const ModelList: React.FC = () => { fontWeight={500} lineHeight="16px" letterSpacing="0.5px"> - 23 + {data?.length || 0}
- {/* 第二个 Flex 容器用于标题和描述 */} - - OpenAI - - OpenAI - - + {isLoading ? ( +
+ +
+ ) : ( + data?.map((model) => { + const ModelComponent = Object.keys(modes).includes(model) + ? modes[model as ModelKey].render + : undefined + return ModelComponent ? : null + }) + )}
) From e815a077adf7c51a3bb702957978409f915dfbc8 Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Fri, 1 Nov 2024 14:58:30 +0800 Subject: [PATCH 19/47] update ui --- frontend/packages/ui/src/components/Select/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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} > Date: Fri, 1 Nov 2024 08:31:38 +0000 Subject: [PATCH 20/47] ok --- .../aiproxy/app/[lng]/(user)/layout.tsx | 3 ++ .../aiproxy/app/[lng]/(user)/logs/page.tsx | 28 ++++++++++++------- .../providers/aiproxy/app/[lng]/layout.tsx | 3 ++ .../aiproxy/app/api/create-key/route.ts | 2 +- .../aiproxy/app/api/delete-key/[id]/route.ts | 6 ++-- .../aiproxy/app/api/get-keys/route.ts | 7 +++-- .../aiproxy/app/api/get-logs/route.ts | 7 +++-- .../aiproxy/app/api/get-models/route.ts | 5 +++- .../aiproxy/app/api/init-app-config/route.ts | 6 +++- .../aiproxy/app/api/update-key/[id]/route.ts | 8 +++++- .../components/SelectDateRange/index.tsx | 2 -- .../aiproxy/components/user/KeyList.tsx | 9 ++++-- .../aiproxy/components/user/ModelList.tsx | 1 - frontend/providers/aiproxy/public/favicon.svg | 9 ++++++ frontend/providers/aiproxy/public/file.svg | 1 - frontend/providers/aiproxy/public/globe.svg | 1 - frontend/providers/aiproxy/public/next.svg | 1 - frontend/providers/aiproxy/public/vercel.svg | 1 - frontend/providers/aiproxy/public/window.svg | 1 - frontend/providers/aiproxy/store/backend.ts | 19 +++++++++++++ .../providers/aiproxy/types/appConfig.d.ts | 15 +++++----- 21 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 frontend/providers/aiproxy/public/favicon.svg delete mode 100644 frontend/providers/aiproxy/public/file.svg delete mode 100644 frontend/providers/aiproxy/public/globe.svg delete mode 100644 frontend/providers/aiproxy/public/next.svg delete mode 100644 frontend/providers/aiproxy/public/vercel.svg delete mode 100644 frontend/providers/aiproxy/public/window.svg create mode 100644 frontend/providers/aiproxy/store/backend.ts diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index 451c1606779..61f28af9de9 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -8,9 +8,11 @@ import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' import { useEffect } from 'react' import { initAppConfig } from '@/api/platform' import { useI18n } from '@/providers/i18n/i18nContext' +import { useBackendStore } from '@/store/backend' export default function UserLayout({ children }: { children: React.ReactNode }) { const { lng } = useI18n() + const { setAiproxyBackend } = useBackendStore() // init session useEffect(() => { const response = createSealosApp() @@ -37,6 +39,7 @@ export default function UserLayout({ children }: { children: React.ReactNode }) useEffect(() => { const initConfig = async () => { const { aiproxyBackend } = await initAppConfig() + setAiproxyBackend(aiproxyBackend) // 删除已存在的 aiproxyBackend,然后重新存储 localStorage.removeItem('aiproxyBackend') localStorage.setItem('aiproxyBackend', aiproxyBackend) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index aac37dd22de..c6ab5d7412b 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -34,7 +34,7 @@ export default function Home(): React.JSX.Element { const [total, setTotal] = useState(0) const { data: models = [] } = useQuery(['getModels'], () => getModels()) - const { data: modelNameData } = useQuery(['getKeys'], () => getKeys()) + const { data: tokenData } = useQuery(['getKeys'], () => getKeys({ page: 1, perPage: 100 })) const { isLoading } = useQuery( ['getLogs', page, pageSize, name, modelName, startTime, endTime], @@ -60,8 +60,6 @@ export default function Home(): React.JSX.Element { } ) - console.log(logData, models, modelNameData) - const columns = useMemo[]>(() => { return [ { @@ -157,13 +155,23 @@ export default function Home(): React.JSX.Element { placeholder={t('logs.select_token_name')} height="32px" value={name} - list={ - modelNameData?.tokens?.map((item) => ({ + list={[ + { + value: 'all', + label: 'all' + }, + ...(tokenData?.tokens?.map((item) => ({ value: item.name, label: item.name - })) || [] - } - onchange={(val: string) => setName(val)} + })) || []) + ]} + onchange={(val: string) => { + if (val === 'all') { + setName('') + } else { + setName(val) + } + }} />
@@ -198,7 +206,7 @@ export default function Home(): React.JSX.Element {
- + {/* {t('logs.status')} @@ -216,7 +224,7 @@ export default function Home(): React.JSX.Element { }))} onchange={(val: string) => setModelName(val)} /> - + */} {t('logs.time')} diff --git a/frontend/providers/aiproxy/app/[lng]/layout.tsx b/frontend/providers/aiproxy/app/[lng]/layout.tsx index 4ece0aca6da..68535b1c5dd 100644 --- a/frontend/providers/aiproxy/app/[lng]/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/layout.tsx @@ -26,6 +26,9 @@ export async function generateMetadata({ // 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') } diff --git a/frontend/providers/aiproxy/app/api/create-key/route.ts b/frontend/providers/aiproxy/app/api/create-key/route.ts index aa20935fe30..7feb5506406 100644 --- a/frontend/providers/aiproxy/app/api/create-key/route.ts +++ b/frontend/providers/aiproxy/app/api/create-key/route.ts @@ -38,7 +38,7 @@ async function createToken(name: string, group: string): Promise { try { const url = new URL( `/api/token/${group}?auto_create_group=true`, - global.AppConfig?.backend.aiproxy + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy ) const token = global.AppConfig?.auth.aiProxyBackendKey const response = await fetch(url.toString(), { diff --git a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts index 7229ce34269..b03fb60143c 100644 --- a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts +++ b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts @@ -8,9 +8,11 @@ interface DeleteTokenResponse { async function deleteToken(group: string, id: string): Promise { try { - const url = new URL(`/api/token/${group}/${id}`, global.AppConfig?.backend.aiproxy) + const url = new URL( + `/api/token/${group}/${id}`, + global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy + ) const token = global.AppConfig?.auth.aiProxyBackendKey - console.log(url.toString()) const response = await fetch(url.toString(), { method: 'DELETE', diff --git a/frontend/providers/aiproxy/app/api/get-keys/route.ts b/frontend/providers/aiproxy/app/api/get-keys/route.ts index fa07bf9c149..6f2cfc559e9 100644 --- a/frontend/providers/aiproxy/app/api/get-keys/route.ts +++ b/frontend/providers/aiproxy/app/api/get-keys/route.ts @@ -35,9 +35,10 @@ async function fetchTokens( group: string ): Promise<{ tokens: TokenInfo[]; total: number }> { try { - const url = new URL(`/api/token/${group}/search`, global.AppConfig?.backend.aiproxy) - console.log(perPage) - console.log(page) + 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()) diff --git a/frontend/providers/aiproxy/app/api/get-logs/route.ts b/frontend/providers/aiproxy/app/api/get-logs/route.ts index 6c1992b5e2d..b288fb387b4 100644 --- a/frontend/providers/aiproxy/app/api/get-logs/route.ts +++ b/frontend/providers/aiproxy/app/api/get-logs/route.ts @@ -41,7 +41,10 @@ async function fetchLogs( group: string ): Promise<{ logs: LogItem[]; total: number }> { try { - const url = new URL(`/api/log/${group}/search`, global.AppConfig?.backend.aiproxy) + 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()) @@ -64,8 +67,6 @@ async function fetchLogs( const token = global.AppConfig?.auth.aiProxyBackendKey - // console.log(url) - const response = await fetch(url.toString(), { method: 'GET', headers: { diff --git a/frontend/providers/aiproxy/app/api/get-models/route.ts b/frontend/providers/aiproxy/app/api/get-models/route.ts index df97d8bc2c4..ae14a0e57b8 100644 --- a/frontend/providers/aiproxy/app/api/get-models/route.ts +++ b/frontend/providers/aiproxy/app/api/get-models/route.ts @@ -10,7 +10,10 @@ interface SearchResponse { async function fetchModels(): Promise { try { - const url = new URL(`/api/models/enabled`, global.AppConfig?.backend.aiproxy) + 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(), { diff --git a/frontend/providers/aiproxy/app/api/init-app-config/route.ts b/frontend/providers/aiproxy/app/api/init-app-config/route.ts index 313ca9d8cad..f1559365d17 100644 --- a/frontend/providers/aiproxy/app/api/init-app-config/route.ts +++ b/frontend/providers/aiproxy/app/api/init-app-config/route.ts @@ -12,6 +12,9 @@ function getAppConfig(appConfig: AppConfigType): AppConfigType { 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 } @@ -23,7 +26,8 @@ export function initAppConfig(): AppConfigType { aiProxyBackendKey: '' }, backend: { - aiproxy: 'http://localhost:8080' + aiproxy: 'http://localhost:8080', + aiproxyInternal: 'http://localhost:8080' } } if (!global.AppConfig) { diff --git a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts index a113ba7a8e8..a7e1393f3de 100644 --- a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts +++ b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts @@ -12,7 +12,13 @@ interface UpdateTokenBody { async function updateToken(group: string, id: string, status: number): Promise { try { - const url = new URL(`/api/token/${group}/${id}/status`, global.AppConfig?.backend.aiproxy) + 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(), { diff --git a/frontend/providers/aiproxy/components/SelectDateRange/index.tsx b/frontend/providers/aiproxy/components/SelectDateRange/index.tsx index e56ae50abe0..37ea5cde25e 100644 --- a/frontend/providers/aiproxy/components/SelectDateRange/index.tsx +++ b/frontend/providers/aiproxy/components/SelectDateRange/index.tsx @@ -193,7 +193,6 @@ export default function SelectDateRange({ onChange={handleFromChange} onBlur={() => { selectedRange?.from && setStartTime(startOfDay(selectedRange.from)) - console.log(selectedRange?.from) }} /> @@ -227,7 +226,6 @@ export default function SelectDateRange({ onChange={handleToChange} onBlur={() => { selectedRange?.to && setEndTime(endOfDay(selectedRange.to)) - console.log(selectedRange?.to) }} /> diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index 6851ebda024..896e761ac78 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -46,6 +46,7 @@ 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' export function KeyList(): JSX.Element { const { lng } = useI18n() @@ -59,6 +60,8 @@ export function KeyList(): JSX.Element { successIconBg: 'var(--Green-600, #039855)', successIconFill: 'white' }) + const aiproxyBackend = useBackendStore((state) => state.aiproxyBackend) + return ( <> @@ -99,7 +102,7 @@ export function KeyList(): JSX.Element { _hover={{ textDecoration: 'underline' }} cursor="pointer" onClick={() => { - const endpoint = localStorage.getItem('aiproxyBackend') || 'https://www.aiproxy.com' + const endpoint = aiproxyBackend navigator.clipboard.writeText(endpoint).then( () => { message({ @@ -122,7 +125,7 @@ export function KeyList(): JSX.Element { ) }}> - {localStorage.getItem('aiproxyBackend') || 'https://www.aiproxy.com'} + {aiproxyBackend} @@ -699,7 +702,7 @@ function CreateKeyModal({ message({ status: 'warning', title: t('key.createFailed'), - description: err?.message || t('key.createFailed'), + description: err?.response?.data?.message || t('key.createFailed'), isClosable: true, position: 'top' }) diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx index b104d7190d5..e09c428bec3 100644 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -44,7 +44,6 @@ const ModelList: React.FC = () => { const { t } = useTranslationClientSide(lng, 'common') const { isLoading, data } = useQuery(['getModels'], () => getModels()) - console.log(data) return ( <> 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/public/file.svg b/frontend/providers/aiproxy/public/file.svg deleted file mode 100644 index 004145cddf3..00000000000 --- a/frontend/providers/aiproxy/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/providers/aiproxy/public/globe.svg b/frontend/providers/aiproxy/public/globe.svg deleted file mode 100644 index 567f17b0d7c..00000000000 --- a/frontend/providers/aiproxy/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/providers/aiproxy/public/next.svg b/frontend/providers/aiproxy/public/next.svg deleted file mode 100644 index 5174b28c565..00000000000 --- a/frontend/providers/aiproxy/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/providers/aiproxy/public/vercel.svg b/frontend/providers/aiproxy/public/vercel.svg deleted file mode 100644 index 77053960334..00000000000 --- a/frontend/providers/aiproxy/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/providers/aiproxy/public/window.svg b/frontend/providers/aiproxy/public/window.svg deleted file mode 100644 index b2b2a44f6eb..00000000000 --- a/frontend/providers/aiproxy/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ 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/types/appConfig.d.ts b/frontend/providers/aiproxy/types/appConfig.d.ts index 6e01ba593ac..f96828a80a0 100644 --- a/frontend/providers/aiproxy/types/appConfig.d.ts +++ b/frontend/providers/aiproxy/types/appConfig.d.ts @@ -1,14 +1,15 @@ export type AppConfigType = { auth: { - appTokenJwtKey: string; - aiProxyBackendKey: string; - }; + appTokenJwtKey: string + aiProxyBackendKey: string + } backend: { - aiproxy: string; - }; -}; + aiproxy: string + aiproxyInternal: string + } +} declare global { // eslint-disable-next-line no-var - var AppConfig: AppConfigType | undefined; + var AppConfig: AppConfigType | undefined } From 64b820e569a721c56f06a26bf24b8ea57027a307 Mon Sep 17 00:00:00 2001 From: lim Date: Fri, 1 Nov 2024 08:57:05 +0000 Subject: [PATCH 21/47] ok --- .../aiproxy/app/api/init-app-config/route.ts | 2 +- frontend/providers/aiproxy/utils/auth.ts | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/frontend/providers/aiproxy/app/api/init-app-config/route.ts b/frontend/providers/aiproxy/app/api/init-app-config/route.ts index f1559365d17..f9c6175b2d2 100644 --- a/frontend/providers/aiproxy/app/api/init-app-config/route.ts +++ b/frontend/providers/aiproxy/app/api/init-app-config/route.ts @@ -18,7 +18,7 @@ function getAppConfig(appConfig: AppConfigType): AppConfigType { return appConfig } -export function initAppConfig(): AppConfigType { +function initAppConfig(): AppConfigType { // default config const DefaultAppConfig: AppConfigType = { auth: { diff --git a/frontend/providers/aiproxy/utils/auth.ts b/frontend/providers/aiproxy/utils/auth.ts index ae1a760f87b..59e9e2c0584 100644 --- a/frontend/providers/aiproxy/utils/auth.ts +++ b/frontend/providers/aiproxy/utils/auth.ts @@ -1,36 +1,39 @@ -import jwt from 'jsonwebtoken'; +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; + 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'); + const token = headers.get('authorization') if (!token) { - return Promise.reject('Token is missing'); + return Promise.reject('Token is missing') } - const decoded = jwt.verify(token, global.AppConfig?.auth.appTokenJwtKey) as AppTokenPayload; - const now = Math.floor(Date.now() / 1000); + 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'); + return Promise.reject('Token expired') } if (!decoded.workspaceId) { - return Promise.reject('Invalid token'); + return Promise.reject('Invalid token') } - return decoded.workspaceId; + return decoded.workspaceId } catch (error) { - console.error('Token parsing error:', error); - return Promise.reject('Invalid token'); + console.error('Token parsing error:', error) + return Promise.reject('Invalid token') } } From fcef335bb6c2f65d21409c958bc9d3598b330c81 Mon Sep 17 00:00:00 2001 From: lim Date: Fri, 1 Nov 2024 09:37:36 +0000 Subject: [PATCH 22/47] ok --- frontend/providers/aiproxy/app/api/init-app-config/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/providers/aiproxy/app/api/init-app-config/route.ts b/frontend/providers/aiproxy/app/api/init-app-config/route.ts index f9c6175b2d2..7d21c5251b4 100644 --- a/frontend/providers/aiproxy/app/api/init-app-config/route.ts +++ b/frontend/providers/aiproxy/app/api/init-app-config/route.ts @@ -26,8 +26,8 @@ function initAppConfig(): AppConfigType { aiProxyBackendKey: '' }, backend: { - aiproxy: 'http://localhost:8080', - aiproxyInternal: 'http://localhost:8080' + aiproxy: '', + aiproxyInternal: '' } } if (!global.AppConfig) { From 885d2971fcfd461b07a0297bf5666db5730e85d7 Mon Sep 17 00:00:00 2001 From: lim Date: Fri, 1 Nov 2024 09:40:14 +0000 Subject: [PATCH 23/47] ok --- frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index 61f28af9de9..e34170c0526 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -40,9 +40,6 @@ export default function UserLayout({ children }: { children: React.ReactNode }) const initConfig = async () => { const { aiproxyBackend } = await initAppConfig() setAiproxyBackend(aiproxyBackend) - // 删除已存在的 aiproxyBackend,然后重新存储 - localStorage.removeItem('aiproxyBackend') - localStorage.setItem('aiproxyBackend', aiproxyBackend) } initConfig() From d31ad2c40e8b1a3032c0b9ae13c6509c6fbb82ce Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Fri, 1 Nov 2024 18:11:57 +0800 Subject: [PATCH 24/47] update api --- frontend/providers/aiproxy/app/api/create-key/route.ts | 2 ++ frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts | 1 + frontend/providers/aiproxy/app/api/get-keys/route.ts | 1 + frontend/providers/aiproxy/app/api/get-logs/route.ts | 1 + frontend/providers/aiproxy/app/api/get-models/route.ts | 2 ++ frontend/providers/aiproxy/app/api/init-app-config/route.ts | 2 ++ frontend/providers/aiproxy/app/api/update-key/[id]/route.ts | 1 + 7 files changed, 10 insertions(+) diff --git a/frontend/providers/aiproxy/app/api/create-key/route.ts b/frontend/providers/aiproxy/app/api/create-key/route.ts index 7feb5506406..c3c9179c425 100644 --- a/frontend/providers/aiproxy/app/api/create-key/route.ts +++ b/frontend/providers/aiproxy/app/api/create-key/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' import { parseJwtToken } from '@/utils/auth' +export const dynamic = 'force-dynamic' + interface CreateTokenRequest { name: string } diff --git a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts index b03fb60143c..81a526f0db5 100644 --- a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts +++ b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { parseJwtToken } from '@/utils/auth' +export const dynamic = 'force-dynamic' interface DeleteTokenResponse { message: string success: boolean diff --git a/frontend/providers/aiproxy/app/api/get-keys/route.ts b/frontend/providers/aiproxy/app/api/get-keys/route.ts index 6f2cfc559e9..e22469c905e 100644 --- a/frontend/providers/aiproxy/app/api/get-keys/route.ts +++ b/frontend/providers/aiproxy/app/api/get-keys/route.ts @@ -3,6 +3,7 @@ import { TokenInfo } from '@/types/getKeys' import { parseJwtToken } from '@/utils/auth' +export const dynamic = 'force-dynamic' export interface KeysSearchResponse { data: { tokens: TokenInfo[] diff --git a/frontend/providers/aiproxy/app/api/get-logs/route.ts b/frontend/providers/aiproxy/app/api/get-logs/route.ts index b288fb387b4..2e076ddd2b9 100644 --- a/frontend/providers/aiproxy/app/api/get-logs/route.ts +++ b/frontend/providers/aiproxy/app/api/get-logs/route.ts @@ -2,6 +2,7 @@ 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[] diff --git a/frontend/providers/aiproxy/app/api/get-models/route.ts b/frontend/providers/aiproxy/app/api/get-models/route.ts index ae14a0e57b8..199ede851d6 100644 --- a/frontend/providers/aiproxy/app/api/get-models/route.ts +++ b/frontend/providers/aiproxy/app/api/get-models/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server' import { parseJwtToken } from '@/utils/auth' +export const dynamic = 'force-dynamic' + interface SearchResponse { data: string[] message: string diff --git a/frontend/providers/aiproxy/app/api/init-app-config/route.ts b/frontend/providers/aiproxy/app/api/init-app-config/route.ts index 7d21c5251b4..2e37a2e3a33 100644 --- a/frontend/providers/aiproxy/app/api/init-app-config/route.ts +++ b/frontend/providers/aiproxy/app/api/init-app-config/route.ts @@ -2,6 +2,8 @@ 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 diff --git a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts index a7e1393f3de..898ec5ddf8d 100644 --- a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts +++ b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { parseJwtToken } from '@/utils/auth' +export const dynamic = 'force-dynamic' interface UpdateTokenResponse { message: string success: boolean From 6a99a1ca2ce69e6d5dddff79dfacc74355b687fe Mon Sep 17 00:00:00 2001 From: lim Date: Fri, 1 Nov 2024 11:15:18 +0000 Subject: [PATCH 25/47] no cache --- frontend/providers/aiproxy/app/api/create-key/route.ts | 1 + frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts | 3 ++- frontend/providers/aiproxy/app/api/get-keys/route.ts | 3 ++- frontend/providers/aiproxy/app/api/get-logs/route.ts | 3 ++- frontend/providers/aiproxy/app/api/get-models/route.ts | 3 ++- frontend/providers/aiproxy/app/api/update-key/[id]/route.ts | 3 ++- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/providers/aiproxy/app/api/create-key/route.ts b/frontend/providers/aiproxy/app/api/create-key/route.ts index c3c9179c425..89161f4457e 100644 --- a/frontend/providers/aiproxy/app/api/create-key/route.ts +++ b/frontend/providers/aiproxy/app/api/create-key/route.ts @@ -49,6 +49,7 @@ async function createToken(name: string, group: string): Promise { 'Content-Type': 'application/json', Authorization: `${token}` }, + cache: 'no-store', body: JSON.stringify({ name }) diff --git a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts index 81a526f0db5..26ed34f6ff2 100644 --- a/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts +++ b/frontend/providers/aiproxy/app/api/delete-key/[id]/route.ts @@ -20,7 +20,8 @@ async function deleteToken(group: string, id: string): Promise { headers: { 'Content-Type': 'application/json', Authorization: `${token}` - } + }, + cache: 'no-store' }) if (!response.ok) { diff --git a/frontend/providers/aiproxy/app/api/get-keys/route.ts b/frontend/providers/aiproxy/app/api/get-keys/route.ts index e22469c905e..c37296da5d9 100644 --- a/frontend/providers/aiproxy/app/api/get-keys/route.ts +++ b/frontend/providers/aiproxy/app/api/get-keys/route.ts @@ -50,7 +50,8 @@ async function fetchTokens( headers: { 'Content-Type': 'application/json', Authorization: `${token}` - } + }, + cache: 'no-store' }) if (!response.ok) { diff --git a/frontend/providers/aiproxy/app/api/get-logs/route.ts b/frontend/providers/aiproxy/app/api/get-logs/route.ts index 2e076ddd2b9..5f9fd0a93be 100644 --- a/frontend/providers/aiproxy/app/api/get-logs/route.ts +++ b/frontend/providers/aiproxy/app/api/get-logs/route.ts @@ -73,7 +73,8 @@ async function fetchLogs( headers: { 'Content-Type': 'application/json', Authorization: `${token}` - } + }, + cache: 'no-store' }) if (!response.ok) { diff --git a/frontend/providers/aiproxy/app/api/get-models/route.ts b/frontend/providers/aiproxy/app/api/get-models/route.ts index 199ede851d6..05906728b7f 100644 --- a/frontend/providers/aiproxy/app/api/get-models/route.ts +++ b/frontend/providers/aiproxy/app/api/get-models/route.ts @@ -23,7 +23,8 @@ async function fetchModels(): Promise { headers: { 'Content-Type': 'application/json', Authorization: `${token}` - } + }, + cache: 'no-store' }) if (!response.ok) { diff --git a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts index 898ec5ddf8d..c0665abd3ff 100644 --- a/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts +++ b/frontend/providers/aiproxy/app/api/update-key/[id]/route.ts @@ -28,7 +28,8 @@ async function updateToken(group: string, id: string, status: number): Promise Date: Mon, 4 Nov 2024 10:27:17 +0000 Subject: [PATCH 26/47] ui --- .../app/[lng]/(admin)/dashboard/page.tsx | 0 .../aiproxy/app/[lng]/(admin)/layout.tsx | 17 ++++ .../aiproxy/app/[lng]/(user)/layout.tsx | 2 +- .../aiproxy/app/[lng]/(user)/logs/page.tsx | 22 +++++- .../aiproxy/app/i18n/locales/en/common.json | 7 +- .../aiproxy/app/i18n/locales/zh/common.json | 7 +- .../aiproxy/components/user/KeyList.tsx | 79 +++++++++++++++---- .../aiproxy/components/user/ModelList.tsx | 2 + .../aiproxy/components/user/test.json | 41 ++++++++++ 9 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx create mode 100644 frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx create mode 100644 frontend/providers/aiproxy/components/user/test.json 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..e69de29bb2d 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..139590268a6 --- /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)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index e34170c0526..ddf9f511686 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -72,7 +72,7 @@ export default function UserLayout({ children }: { children: React.ReactNode }) }, []) return ( - + diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index c6ab5d7412b..b7b5caa2a7c 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -81,7 +81,25 @@ export default function Home(): React.JSX.Element { { header: t('logs.status'), - accessorFn: (row) => (row.code === 200 ? 'success' : 'failed'), + accessorFn: (row) => (row.code === 200 ? t('logs.success') : t('logs.failed')), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + + {value} + + ) + }, id: 'status' }, { @@ -91,7 +109,7 @@ export default function Home(): React.JSX.Element { }, { accessorKey: 'completion_price', - id: 'price', + id: 'used_amount', header: () => { return ( diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index 97f913bb832..35b09464b97 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -24,7 +24,8 @@ "deleteSuccess": "Delete key successfully", "deleteFailed": "Failed to delete key", "updateSuccess": "Update status successful", - "updateFailed": "Update status failed" + "updateFailed": "Update status failed", + "unused": "not use" }, "logs": { "call_log": "call log", @@ -39,7 +40,9 @@ "select_token_name": "Please select a name", "model": "Model", "total_price": "lump sum", - "total_price_tip": "Enter the number of tokens x enter the fee Enter the number of tokens x enter the fee" + "total_price_tip": "Enter the number of tokens x enter the fee Enter the number of tokens x enter the fee", + "success": "success", + "failed": "failed" }, "Page": "Page", "Total": "total", diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index 02e94b478cc..166451936fa 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -24,7 +24,8 @@ "deleteSuccess": "删除成功", "deleteFailed": "删除失败", "updateSuccess": "状态更新成功", - "updateFailed": "状态更新失败" + "updateFailed": "状态更新失败", + "unused": "未使用" }, "logs": { "call_log": "调用日志", @@ -39,7 +40,9 @@ "select_modal": "请选择模型", "select_token_name": "请选择名称", "total_price": "总金额", - "total_price_tip": "输入 token 数 x 输入费用 + 输入 token 数 x 输入费用" + "total_price_tip": "输入 token 数 x 输入费用 + 输入 token 数 x 输入费用", + "success": "成功", + "failed": "失败" }, "Page": "页", "Total": "总数", diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index 896e761ac78..c7afee0cbd6 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -124,7 +124,16 @@ export function KeyList(): JSX.Element { } ) }}> - + {aiproxyBackend} @@ -295,8 +304,43 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { fontSize="12px" fontWeight={500} lineHeight="16px" - letterSpacing="0.5px"> - {info.getValue()} + letterSpacing="0.5px" + cursor="pointer" + onClick={() => { + 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()} + ) }), @@ -318,17 +362,23 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { columnHelper.accessor((row) => row.accessed_at, { id: TableHeaderId.LAST_USED_AT, header: (props) => , - cell: (info) => ( - - {info.getValue() ? new Date(info.getValue()).toLocaleString() : '-'} - - ) + cell: (info) => { + const timestamp = info.getValue() + const displayValue = + timestamp && timestamp > 0 ? new Date(timestamp).toLocaleString() : t('key.unused') + + return ( + + {displayValue} + + ) + } }), columnHelper.accessor((row) => row.status, { id: TableHeaderId.STATUS, @@ -612,6 +662,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { th: { borderBottom: 'none' // 移除所有表头单元格的下边线 }, + // 第一个和最后一个表头单元格的圆角 'th:first-of-type': { borderTopLeftRadius: '6px', borderBottomLeftRadius: '6px' diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx index e09c428bec3..cb46727a416 100644 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -32,6 +32,7 @@ const createModelRender = (modelName: string) => { ModelComponent.displayName = `Model_${modelName}` return ModelComponent } +// qwen chatglm deepseek moonshot SparkDesk Doubao glm moonshot abab5 ERNIE embedding abab6 cogview const modes: Record JSX.Element }> = { 'gpt-4o-mini': { render: createModelRender('gpt-4o-mini') }, @@ -44,6 +45,7 @@ const ModelList: React.FC = () => { const { t } = useTranslationClientSide(lng, 'common') const { isLoading, data } = useQuery(['getModels'], () => getModels()) + console.log(data) return ( <> diff --git a/frontend/providers/aiproxy/components/user/test.json b/frontend/providers/aiproxy/components/user/test.json new file mode 100644 index 00000000000..e24f35fec13 --- /dev/null +++ b/frontend/providers/aiproxy/components/user/test.json @@ -0,0 +1,41 @@ +[ + "qwen-long", + "chatglm_lite", + "deepseek-chat", + "moonshot-v1-128k", + "SparkDesk-v4.0", + "qwen-plus", + "Doubao-embedding", + "SparkDesk", + "Doubao-pro-32k", + "SparkDesk-v3.1-128K", + "Doubao-lite-128k", + "glm-4", + "moonshot-v1-8k", + "abab5.5-chat", + "abab5.5s-chat", + "Doubao-lite-32k", + "chatglm_std", + "moonshot-v1-32k", + "ERNIE-4.0-8K", + "glm-3-turbo", + "embedding-2", + "deepseek-coder", + "abab6.5-chat", + "SparkDesk-v3.5", + "chatglm_turbo", + "abab6.5s-chat", + "abab6-chat", + "SparkDesk-v2.1", + "qwen-max", + "Doubao-pro-128k", + "Doubao-pro-4k", + "Doubao-lite-4k", + "qwen-turbo", + "chatglm_pro", + "glm-4v", + "cogview-3", + "ERNIE-3.5-8K", + "SparkDesk-v3.1", + "SparkDesk-v1.1" +] From e9872b7643f9b49d85c4f0d9a37aeacb47f6afcb Mon Sep 17 00:00:00 2001 From: lim Date: Mon, 4 Nov 2024 10:49:18 +0000 Subject: [PATCH 27/47] ok --- .../aiproxy/app/[lng]/(user)/home/page.tsx | 5 +- .../aiproxy/components/user/ModelList.tsx | 136 ++++++++++++++---- .../aiproxy/ui/svg/icons/modelist/chatglm.svg | 4 + .../ui/svg/icons/modelist/deepseek.svg | 10 ++ .../ui/svg/icons/modelist/moonshot.svg | 19 +++ .../aiproxy/ui/svg/icons/modelist/qianwen.svg | 3 + .../ui/svg/icons/modelist/sparkdesk.svg | 20 +++ 7 files changed, 165 insertions(+), 32 deletions(-) create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/chatglm.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/deepseek.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/moonshot.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/qianwen.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/sparkdesk.svg diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx index ecba3e02b6d..2bd8c810e21 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx @@ -26,13 +26,14 @@ export default function Home(): JSX.Element { bg="white" borderRadius="12px" display="inline-flex" - p="19px 59px 0px 23px" + p="19px 59px 23px 23px" flexDirection="column" justifyContent="flex-start" alignItems="flex-start" gap="22px" h="full" - w="full"> + w="full" + overflow="auto"> diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx index cb46727a416..9743ddd7f65 100644 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -3,49 +3,128 @@ 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 OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg' import Image, { StaticImageData } from 'next/image' import { useQuery } from '@tanstack/react-query' import { getModels } from '@/api/platform' +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 DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg' + +// 图标导入 +// 图标映射 const IconList: Record = { - OpenAI: OpenAIIcon + OpenAI: OpenAIIcon, + qwen: QwenIcon, + chatglm: ChatglmIcon, + deepseek: DeepseekIcon, + moonshot: MoonshotIcon, + sparkdesk: SparkdeskIcon, + doubao: OpenAIIcon, + glm: OpenAIIcon, + abab: OpenAIIcon, + ernie: OpenAIIcon, + cogview: OpenAIIcon } -type ModelKey = 'gpt-4o-mini' | 'gpt-4' | 'gpt-3.5-turbo' +// 获取模型主标识符 +const getModelIcon = (modelName: string): StaticImageData => { + const mainIdentifier = modelName.toLowerCase().split(/[-._]/)[0] + if (mainIdentifier === 'gpt') return IconList.OpenAI + return IconList[mainIdentifier] || IconList.OpenAI +} +// 创建模型渲染器 const createModelRender = (modelName: string) => { - const ModelComponent = () => ( - - OpenAI - - {modelName} - - - ) + const ModelComponent = () => { + const iconSrc = getModelIcon(modelName) + + return ( + + {modelName} + + {modelName} + + + ) + } ModelComponent.displayName = `Model_${modelName}` return ModelComponent } -// qwen chatglm deepseek moonshot SparkDesk Doubao glm moonshot abab5 ERNIE embedding abab6 cogview -const modes: Record JSX.Element }> = { - 'gpt-4o-mini': { render: createModelRender('gpt-4o-mini') }, - 'gpt-4': { render: createModelRender('gpt-4') }, - 'gpt-3.5-turbo': { render: createModelRender('gpt-3.5-turbo') } -} +// 生成动态的模型类型 +const modelList = [ + 'qwen-long', + 'chatglm_lite', + 'deepseek-chat', + 'moonshot-v1-128k', + 'SparkDesk-v4.0', + 'qwen-plus', + 'Doubao-embedding', + 'SparkDesk', + 'Doubao-pro-32k', + 'SparkDesk-v3.1-128K', + 'Doubao-lite-128k', + 'glm-4', + 'moonshot-v1-8k', + 'abab5.5-chat', + 'abab5.5s-chat', + 'Doubao-lite-32k', + 'chatglm_std', + 'moonshot-v1-32k', + 'ERNIE-4.0-8K', + 'glm-3-turbo', + 'embedding-2', + 'deepseek-coder', + 'abab6.5-chat', + 'SparkDesk-v3.5', + 'chatglm_turbo', + 'abab6.5s-chat', + 'abab6-chat', + 'SparkDesk-v2.1', + 'qwen-max', + 'Doubao-pro-128k', + 'Doubao-pro-4k', + 'Doubao-lite-4k', + 'qwen-turbo', + 'chatglm_pro', + 'glm-4v', + 'cogview-3', + 'ERNIE-3.5-8K', + 'SparkDesk-v3.1', + 'SparkDesk-v1.1', + // 添加原有的 GPT 模型 + 'gpt-4o-mini', + 'gpt-4', + 'gpt-3.5-turbo' +] as const + +type ModelKey = (typeof modelList)[number] +// 创建模型渲染映射 +const modes: Record JSX.Element }> = modelList.reduce( + (acc, modelName) => ({ + ...acc, + [modelName]: { render: createModelRender(modelName) } + }), + {} +) + +// 模型列表组件 const ModelList: React.FC = () => { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') - const { isLoading, data } = useQuery(['getModels'], () => getModels()) - console.log(data) return ( <> @@ -82,7 +161,6 @@ const ModelList: React.FC = () => { - {isLoading ? (
@@ -90,10 +168,8 @@ const ModelList: React.FC = () => {
) : ( data?.map((model) => { - const ModelComponent = Object.keys(modes).includes(model) - ? modes[model as ModelKey].render - : undefined - return ModelComponent ? : null + const ModelComponent = modes[model]?.render + return ModelComponent ? : null }) )}
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/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/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 From 7dd62e7cdabe7e8634f88fe4dc16dafe0b32d514 Mon Sep 17 00:00:00 2001 From: lim Date: Mon, 4 Nov 2024 10:55:55 +0000 Subject: [PATCH 28/47] ok --- .../providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx index e69de29bb2d..ad1248c3d56 100644 --- a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx @@ -0,0 +1,6 @@ +'use client' +import { Flex } from '@chakra-ui/react' + +export default function DashboardPage() { + return Dashboard +} From 0b78f9ad04f506722b3b06bc9136836defcc72c1 Mon Sep 17 00:00:00 2001 From: lim Date: Tue, 5 Nov 2024 07:21:33 +0000 Subject: [PATCH 29/47] ok --- .../aiproxy/app/[lng]/(user)/logs/page.tsx | 2 +- .../aiproxy/app/i18n/locales/en/common.json | 4 +- .../aiproxy/app/i18n/locales/zh/common.json | 4 +- .../aiproxy/components/user/KeyList.tsx | 59 +++--- .../aiproxy/components/user/ModelList.tsx | 180 ++++++++---------- .../aiproxy/components/user/test.json | 41 ---- .../aiproxy/ui/svg/icons/modelist/doubao.svg | 9 + .../aiproxy/ui/svg/icons/modelist/ernie.svg | 5 + .../aiproxy/ui/svg/icons/modelist/glm.svg | 54 ++++++ .../aiproxy/ui/svg/icons/modelist/minimax.svg | 4 + 10 files changed, 185 insertions(+), 177 deletions(-) delete mode 100644 frontend/providers/aiproxy/components/user/test.json create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/doubao.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/ernie.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/glm.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/modelist/minimax.svg diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index b7b5caa2a7c..c3af404213c 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -108,7 +108,7 @@ export default function Home(): React.JSX.Element { id: 'created_at' }, { - accessorKey: 'completion_price', + accessorKey: 'used_amount', id: 'used_amount', header: () => { return ( diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index 35b09464b97..2bf2dafafa7 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -1,6 +1,6 @@ { - "title": "aiproxy", - "description": "ai agent", + "title": "Ai proxy", + "description": "Ai agent", "Sidebar": { "Home": "AI Proxy", "Logs": "call log" diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index 166451936fa..60798e20536 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -1,6 +1,6 @@ { - "title": "ai代理", - "description": "ai 代理", + "title": "Ai 代理", + "description": "Ai 代理", "Sidebar": { "Home": "AI Proxy", "Logs": "调用日志" diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index c7afee0cbd6..c38d10fa75d 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -196,6 +196,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { 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)', @@ -428,7 +429,10 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { id: TableHeaderId.ACTIONS, header: (props) => , cell: (info) => ( - + setOpenPopoverId(null)}> void }) => { p="var(--xs, 10px)" alignItems="center" gap="var(--sm, 6px)" - borderRadius="var(--sm, 6px)"> + borderRadius="var(--sm, 6px)" + onClick={() => setOpenPopoverId(info.row.original.id)}> void }) => { /> } - onClick={() => + onClick={() => { handleStatusUpdate(info.row.original.id, info.row.original.status) - }> + setOpenPopoverId(null) + }}> {t('enable')} ) : ( @@ -535,9 +541,10 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { /> } - onClick={() => + onClick={() => { + setOpenPopoverId(null) handleStatusUpdate(info.row.original.id, info.row.original.status) - }> + }}> {t('disable')} )} @@ -572,7 +579,10 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { /> } - onClick={() => handleDelete(info.row.original.id)}> + onClick={() => { + handleDelete(info.row.original.id) + setOpenPopoverId(null) + }}> {t('delete')}
@@ -652,28 +662,19 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + {headerGroup.headers.map((header, i) => ( + ))} diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx index 9743ddd7f65..1332c099678 100644 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -12,113 +12,89 @@ 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 DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg' - -// 图标导入 - -// 图标映射 -const IconList: Record = { - OpenAI: OpenAIIcon, - qwen: QwenIcon, - chatglm: ChatglmIcon, - deepseek: DeepseekIcon, - moonshot: MoonshotIcon, - sparkdesk: SparkdeskIcon, - doubao: OpenAIIcon, - glm: OpenAIIcon, - abab: OpenAIIcon, - ernie: OpenAIIcon, - cogview: OpenAIIcon +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 GlmIcon from '@/ui/svg/icons/modelist/glm.svg' +// 图标映射和标识符关系 +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 getModelIcon = (modelName: string): StaticImageData => { - const mainIdentifier = modelName.toLowerCase().split(/[-._]/)[0] - if (mainIdentifier === 'gpt') return IconList.OpenAI - return IconList[mainIdentifier] || IconList.OpenAI + const identifier = modelName.toLowerCase().split(/[-._\d]/)[0] + const group = Object.values(modelGroups).find((group) => group.identifiers.includes(identifier)) + return group?.icon || OpenAIIcon } -// 创建模型渲染器 -const createModelRender = (modelName: string) => { - const ModelComponent = () => { - const iconSrc = getModelIcon(modelName) +// 按图标分组模型 +const sortModelsByIcon = (models: string[]): string[] => { + const groupedModels = new Map() - return ( - - - - {modelName} - - - ) - } - ModelComponent.displayName = `Model_${modelName}` - return ModelComponent -} + // 按图标分组 + models.forEach((model) => { + const icon = getModelIcon(model) + if (!groupedModels.has(icon)) { + groupedModels.set(icon, []) + } + groupedModels.get(icon)?.push(model) + }) -// 生成动态的模型类型 -const modelList = [ - 'qwen-long', - 'chatglm_lite', - 'deepseek-chat', - 'moonshot-v1-128k', - 'SparkDesk-v4.0', - 'qwen-plus', - 'Doubao-embedding', - 'SparkDesk', - 'Doubao-pro-32k', - 'SparkDesk-v3.1-128K', - 'Doubao-lite-128k', - 'glm-4', - 'moonshot-v1-8k', - 'abab5.5-chat', - 'abab5.5s-chat', - 'Doubao-lite-32k', - 'chatglm_std', - 'moonshot-v1-32k', - 'ERNIE-4.0-8K', - 'glm-3-turbo', - 'embedding-2', - 'deepseek-coder', - 'abab6.5-chat', - 'SparkDesk-v3.5', - 'chatglm_turbo', - 'abab6.5s-chat', - 'abab6-chat', - 'SparkDesk-v2.1', - 'qwen-max', - 'Doubao-pro-128k', - 'Doubao-pro-4k', - 'Doubao-lite-4k', - 'qwen-turbo', - 'chatglm_pro', - 'glm-4v', - 'cogview-3', - 'ERNIE-3.5-8K', - 'SparkDesk-v3.1', - 'SparkDesk-v1.1', - // 添加原有的 GPT 模型 - 'gpt-4o-mini', - 'gpt-4', - 'gpt-3.5-turbo' -] as const + // 将分组后的模型展平为数组 + return Array.from(groupedModels.values()).flat() +} -type ModelKey = (typeof modelList)[number] +// 模型组件 +const ModelComponent = ({ modelName }: { modelName: string }) => { + const iconSrc = getModelIcon(modelName) -// 创建模型渲染映射 -const modes: Record JSX.Element }> = modelList.reduce( - (acc, modelName) => ({ - ...acc, - [modelName]: { render: createModelRender(modelName) } - }), - {} -) + return ( + + + + {modelName} + + + ) +} // 模型列表组件 const ModelList: React.FC = () => { @@ -126,6 +102,9 @@ const ModelList: React.FC = () => { const { t } = useTranslationClientSide(lng, 'common') const { isLoading, data } = useQuery(['getModels'], () => getModels()) + // 对模型进行排序 + const sortedModels = data ? sortModelsByIcon(data) : [] + return ( <> @@ -167,10 +146,7 @@ const ModelList: React.FC = () => { ) : ( - data?.map((model) => { - const ModelComponent = modes[model]?.render - return ModelComponent ? : null - }) + sortedModels.map((model) => ) )} diff --git a/frontend/providers/aiproxy/components/user/test.json b/frontend/providers/aiproxy/components/user/test.json deleted file mode 100644 index e24f35fec13..00000000000 --- a/frontend/providers/aiproxy/components/user/test.json +++ /dev/null @@ -1,41 +0,0 @@ -[ - "qwen-long", - "chatglm_lite", - "deepseek-chat", - "moonshot-v1-128k", - "SparkDesk-v4.0", - "qwen-plus", - "Doubao-embedding", - "SparkDesk", - "Doubao-pro-32k", - "SparkDesk-v3.1-128K", - "Doubao-lite-128k", - "glm-4", - "moonshot-v1-8k", - "abab5.5-chat", - "abab5.5s-chat", - "Doubao-lite-32k", - "chatglm_std", - "moonshot-v1-32k", - "ERNIE-4.0-8K", - "glm-3-turbo", - "embedding-2", - "deepseek-coder", - "abab6.5-chat", - "SparkDesk-v3.5", - "chatglm_turbo", - "abab6.5s-chat", - "abab6-chat", - "SparkDesk-v2.1", - "qwen-max", - "Doubao-pro-128k", - "Doubao-pro-4k", - "Doubao-lite-4k", - "qwen-turbo", - "chatglm_pro", - "glm-4v", - "cogview-3", - "ERNIE-3.5-8K", - "SparkDesk-v3.1", - "SparkDesk-v1.1" -] 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 From db9c83622f1e6f73ce130d888153dda3ee7e6232 Mon Sep 17 00:00:00 2001 From: lim Date: Wed, 6 Nov 2024 06:56:05 +0000 Subject: [PATCH 30/47] ok --- frontend/providers/aiproxy/api/platform.ts | 3 + .../app/[lng]/(admin)/dashboard/page.tsx | 13 +- .../aiproxy/app/[lng]/(admin)/layout.tsx | 2 +- .../aiproxy/app/[lng]/(user)/layout.tsx | 42 +-- .../aiproxy/app/[lng]/(user)/price/page.tsx | 308 ++++++++++++++++++ .../aiproxy/app/api/get-mode-price/route.ts | 83 +++++ .../aiproxy/app/i18n/locales/en/common.json | 13 +- .../aiproxy/app/i18n/locales/zh/common.json | 13 +- .../aiproxy/components/MyTooltip/index.tsx | 27 ++ .../aiproxy/components/table/baseTable.tsx | 31 +- .../aiproxy/components/user/KeyList.tsx | 89 +++-- .../aiproxy/components/user/ModelList.tsx | 95 ++++-- .../aiproxy/components/user/Sidebar.tsx | 79 +++-- frontend/providers/aiproxy/types/backend.d.ts | 5 + frontend/providers/aiproxy/types/yaml | 0 .../aiproxy/ui/svg/icons/sidebar/price.svg | 4 + .../aiproxy/ui/svg/icons/sidebar/price_a.svg | 4 + 17 files changed, 654 insertions(+), 157 deletions(-) create mode 100644 frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx create mode 100644 frontend/providers/aiproxy/app/api/get-mode-price/route.ts create mode 100644 frontend/providers/aiproxy/components/MyTooltip/index.tsx create mode 100644 frontend/providers/aiproxy/types/backend.d.ts create mode 100644 frontend/providers/aiproxy/types/yaml create mode 100644 frontend/providers/aiproxy/ui/svg/icons/sidebar/price.svg create mode 100644 frontend/providers/aiproxy/ui/svg/icons/sidebar/price_a.svg diff --git a/frontend/providers/aiproxy/api/platform.ts b/frontend/providers/aiproxy/api/platform.ts index 48b4dfae425..012096a2f53 100644 --- a/frontend/providers/aiproxy/api/platform.ts +++ b/frontend/providers/aiproxy/api/platform.ts @@ -2,11 +2,14 @@ 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) => diff --git a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx index ad1248c3d56..d84c050af4e 100644 --- a/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/dashboard/page.tsx @@ -2,5 +2,16 @@ import { Flex } from '@chakra-ui/react' export default function DashboardPage() { - return Dashboard + 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 index 139590268a6..6454303cbdd 100644 --- a/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(admin)/layout.tsx @@ -9,7 +9,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) justify="center" align="center" bg="white"> - + {children} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index ddf9f511686..9b1a73f737c 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -9,9 +9,11 @@ import { 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' export default function UserLayout({ children }: { children: React.ReactNode }) { const { lng } = useI18n() + const { i18n } = useTranslationClientSide(lng) const { setAiproxyBackend } = useBackendStore() // init session useEffect(() => { @@ -44,40 +46,28 @@ export default function UserLayout({ children }: { children: React.ReactNode }) initConfig() - // const changeI18n = async (data: any) => { - // const lastLang = getcl() - // const newLang = data.currentLanguage - // if (lastLang !== newLang) { - // router.push(pathname, { locale: newLang }) - // setLangStore(newLang) - // setRefresh((state) => !state) - // } - // } - - // ;(async () => { - // try { - // const lang = await sealosApp.getLanguage() - // changeI18n({ - // currentLanguage: lang.lng - // }) - // } catch (error) { - // changeI18n({ - // currentLanguage: 'zh' - // }) - // } - // })() + const changeI18n = async (data: any) => { + try { + const { lng } = await sealosApp.getLanguage() + i18n.changeLanguage(lng) + } catch (error) { + i18n.changeLanguage('zh') + } + } - // return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n) + return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( - - + + {/* Main Content */} - {children} + + {children} + ) } 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..e96bfe9270d --- /dev/null +++ b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx @@ -0,0 +1,308 @@ +'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, useState } from 'react' +import { + Column, + createColumnHelper, + getCoreRowModel, + useReactTable, + flexRender +} from '@tanstack/react-table' +import { TFunction } from 'i18next' +import { SealosCoin } 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 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 iconSrc = getModelIcon(modelName) + + return ( + + + + {modelName} + + + ) + } + + const getIdentifier = (modelName: string): string => { + return modelName.toLowerCase().split(/[-._\d]/)[0] + } + + const sortModelsByIdentifier = (models: ModelPrice[]): ModelPrice[] => { + const groupedModels = new Map() + + models.forEach((model) => { + const identifier = getIdentifier(model.name) + if (!groupedModels.has(identifier)) { + groupedModels.set(identifier, []) + } + groupedModels.get(identifier)!.push(model) + }) + + const sortedEntries = Array.from(groupedModels.entries()).sort((a, b) => + a[0].localeCompare(b[0]) + ) + + 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]) + + console.log('sortedData', sortedData) + const table = useReactTable({ + data: sortedData, + columns, + getCoreRowModel: getCoreRowModel() + }) + + return isLoading ? ( +
+ +
+ ) : ( + +
+
{flexRender(header.column.columnDef.header, header.getContext())}
+ + {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/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/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index 2bf2dafafa7..b28c43e4a65 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -3,7 +3,8 @@ "description": "Ai agent", "Sidebar": { "Home": "AI Proxy", - "Logs": "call log" + "Logs": "call log", + "Price": "model price" }, "keyList": { "title": "AI Proxy" @@ -25,7 +26,9 @@ "deleteFailed": "Failed to delete key", "updateSuccess": "Update status successful", "updateFailed": "Update status failed", - "unused": "not use" + "unused": "not use", + "inputPrice": "Enter unit price", + "outputPrice": "Output unit price" }, "logs": { "call_log": "call log", @@ -67,5 +70,9 @@ "enable": "enable", "copySuccess": "Copied successfully", "copyFailed": "Copy failed", - "noData": "You don’t have an AI Proxy yet" + "noData": "You don’t have an AI Proxy yet", + "price": { + "title": "mode price", + "per1kTokens": "1k tokens" + } } diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index 60798e20536..fb63c5b0497 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -3,7 +3,8 @@ "description": "Ai 代理", "Sidebar": { "Home": "AI Proxy", - "Logs": "调用日志" + "Logs": "调用日志", + "Price": "模型价格" }, "keyList": { "title": "AI Proxy" @@ -25,7 +26,9 @@ "deleteFailed": "删除失败", "updateSuccess": "状态更新成功", "updateFailed": "状态更新失败", - "unused": "未使用" + "unused": "未使用", + "inputPrice": "输入单价", + "outputPrice": "输出单价" }, "logs": { "call_log": "调用日志", @@ -67,5 +70,9 @@ "enable": "启用", "copySuccess": "复制成功", "copyFailed": "复制失败", - "noData": "你还没有 AI Proxy" + "noData": "你还没有 AI Proxy", + "price": { + "title": "模型价格", + "per1kTokens": "1k tokens" + } } 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/table/baseTable.tsx b/frontend/providers/aiproxy/components/table/baseTable.tsx index 64a85dd0b16..ef1529e92d3 100644 --- a/frontend/providers/aiproxy/components/table/baseTable.tsx +++ b/frontend/providers/aiproxy/components/table/baseTable.tsx @@ -16,21 +16,20 @@ export function BaseTable({ isLoading }: { table: ReactTable; isLoading: boolean } & TableContainerProps) { return ( - - + +
{table.getHeaderGroups().map((headers) => { return ( - + {headers.headers.map((header, i) => { return ( {isLoading ? ( - - + @@ -55,12 +59,13 @@ export function BaseTable({ return ( + alignSelf="stretch" + borderBottom="1px solid" + borderColor="grayModern.150" + fontSize="12px"> {item.getAllCells().map((cell, i) => { return ( - ) diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index c38d10fa75d..2f5f1b431b9 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import { Box, Button, @@ -47,6 +47,7 @@ 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() @@ -124,18 +125,7 @@ export function KeyList(): JSX.Element { } ) }}> - - {aiproxyBackend} - + {aiproxyBackend}
({
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ +
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx index 1332c099678..5548fb5581a 100644 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -1,11 +1,13 @@ 'use client' -import { Badge, Center, Flex, Spinner, Text } from '@chakra-ui/react' +import { Badge, Center, Flex, Spinner, Text, Tooltip } 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' @@ -15,7 +17,8 @@ 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 GlmIcon from '@/ui/svg/icons/modelist/glm.svg' +import { useMemo } from 'react' +import { MyTooltip } from '@/components/MyTooltip' // 图标映射和标识符关系 const modelGroups = { ernie: { @@ -52,46 +55,89 @@ const modelGroups = { } } +const getIdentifier = (modelName: string): string => { + return modelName.toLowerCase().split(/[-._\d]/)[0] +} + // 获取模型图标 const getModelIcon = (modelName: string): StaticImageData => { - const identifier = modelName.toLowerCase().split(/[-._\d]/)[0] + const identifier = getIdentifier(modelName) const group = Object.values(modelGroups).find((group) => group.identifiers.includes(identifier)) return group?.icon || OpenAIIcon } -// 按图标分组模型 -const sortModelsByIcon = (models: string[]): string[] => { - const groupedModels = new Map() +const sortModels = (models: string[]): string[] => { + // 使用 Map 进行分组 + const groupMap = new Map() - // 按图标分组 + // 分组 models.forEach((model) => { - const icon = getModelIcon(model) - if (!groupedModels.has(icon)) { - groupedModels.set(icon, []) + const identifier = getIdentifier(model) + if (!groupMap.has(identifier)) { + groupMap.set(identifier, []) } - groupedModels.get(icon)?.push(model) + groupMap.get(identifier)?.push(model) }) - // 将分组后的模型展平为数组 - return Array.from(groupedModels.values()).flat() + // 按照 identifier 排序并扁平化结果 + return Array.from(groupMap.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) // 按 identifier 排序 + .flatMap(([_, models]) => models.sort()) // 扁平化并保持每组内的排序 } // 模型组件 const ModelComponent = ({ modelName }: { modelName: string }) => { + 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} + + ) } @@ -102,8 +148,7 @@ const ModelList: React.FC = () => { const { t } = useTranslationClientSide(lng, 'common') const { isLoading, data } = useQuery(['getModels'], () => getModels()) - // 对模型进行排序 - const sortedModels = data ? sortModelsByIcon(data) : [] + const sortedData = useMemo(() => sortModels(data || []), [data]) return ( <> @@ -146,7 +191,7 @@ const ModelList: React.FC = () => { ) : ( - sortedModels.map((model) => ) + sortedData.map((model) => ) )} diff --git a/frontend/providers/aiproxy/components/user/Sidebar.tsx b/frontend/providers/aiproxy/components/user/Sidebar.tsx index f1e0427cf74..6ab69c08082 100644 --- a/frontend/providers/aiproxy/components/user/Sidebar.tsx +++ b/frontend/providers/aiproxy/components/user/Sidebar.tsx @@ -1,27 +1,29 @@ -'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'; +'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 { 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' type Menu = { - id: string; - url: string; - value: string; - icon: StaticImageData; - activeIcon: StaticImageData; - display: boolean; -}; + id: string + url: string + value: string + icon: StaticImageData + activeIcon: StaticImageData + display: boolean +} const SideBar = ({ lng }: { lng: string }): JSX.Element => { - const pathname = usePathname(); - const { t } = useTranslationClientSide(lng, 'common'); + const pathname = usePathname() + const { t } = useTranslationClientSide(lng, 'common') const menus: Menu[] = [ { @@ -39,8 +41,16 @@ const SideBar = ({ lng }: { lng: string }): JSX.Element => { icon: logsIcon, activeIcon: logsIcon_a, display: true + }, + { + id: 'price', + url: '/price', + value: t('Sidebar.Price'), + icon: priceIcon, + activeIcon: priceIcon_a, + display: true } - ]; + ] return ( { px="12px" gap="var(--md, 8px)" alignContent="center" - flexShrink={0} - > + flexShrink={0}> {menus .filter((menu) => menu.display) .map((menu) => { - const fullUrl = `/${lng}${menu.url}`; - const isActive = pathname === fullUrl; + const fullUrl = `/${lng}${menu.url}` + const isActive = pathname === fullUrl return ( @@ -73,18 +82,17 @@ const SideBar = ({ lng }: { lng: string }): JSX.Element => { role="group" _hover={{ backgroundColor: '#9699B426' }} onMouseEnter={(e) => { - const img = e.currentTarget.querySelector('img'); + const img = e.currentTarget.querySelector('img') if (img) { - img.src = menu.activeIcon.src; + img.src = menu.activeIcon.src } }} onMouseLeave={(e) => { - const img = e.currentTarget.querySelector('img'); + const img = e.currentTarget.querySelector('img') if (img && !isActive) { - img.src = menu.icon.src; + img.src = menu.icon.src } - }} - > + }}> { fontWeight={500} lineHeight="16px" letterSpacing="0.5px" - _groupHover={{ color: 'grayModern.900' }} - > + _groupHover={{ color: 'grayModern.900' }}> {menu.value} - ); + ) })} - ); -}; + ) +} -export default SideBar; +export default SideBar 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/yaml b/frontend/providers/aiproxy/types/yaml new file mode 100644 index 00000000000..e69de29bb2d 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 From 32bcf935848575bd6a43e88ca22d1778f3f75a13 Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Wed, 6 Nov 2024 15:23:15 +0800 Subject: [PATCH 31/47] update logs page --- .../aiproxy/app/[lng]/(user)/logs/page.tsx | 37 +++++++++++-------- .../aiproxy/components/SwitchPage.tsx | 2 +- .../aiproxy/components/table/baseTable.tsx | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index c3af404213c..fe23fb5bfcf 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -133,8 +133,15 @@ export default function Home(): React.JSX.Element { }) return ( - - + + {t('logs.call_log')}
{table.getHeaderGroups().map((headers) => { From fc009036f9ec7366194e99fb712d7350bc97870d Mon Sep 17 00:00:00 2001 From: lim Date: Wed, 6 Nov 2024 07:26:08 +0000 Subject: [PATCH 32/47] ok --- frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx | 3 ++- frontend/providers/aiproxy/components/user/ModelList.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx index 2bd8c810e21..e43a476d36a 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/home/page.tsx @@ -31,8 +31,9 @@ export default function Home(): JSX.Element { justifyContent="flex-start" alignItems="flex-start" gap="22px" - h="full" + minW="260px" w="full" + h="full" overflow="auto"> diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx index 5548fb5581a..d8f251ae4a4 100644 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -1,5 +1,5 @@ 'use client' -import { Badge, Center, Flex, Spinner, Text, Tooltip } from '@chakra-ui/react' +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' From f69bfebd550944d09eecb5385852f737ff8c7389 Mon Sep 17 00:00:00 2001 From: lim Date: Wed, 6 Nov 2024 07:41:38 +0000 Subject: [PATCH 33/47] ok --- .../providers/aiproxy/app/[lng]/(user)/price/page.tsx | 1 - frontend/providers/aiproxy/app/api/get-keys/route.ts | 2 +- frontend/providers/aiproxy/app/api/get-models/route.ts | 2 +- frontend/providers/aiproxy/components/user/KeyList.tsx | 8 +++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx index e96bfe9270d..c9349716cfd 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx @@ -255,7 +255,6 @@ function PriceTable() { const sortedData = useMemo(() => sortModelsByIdentifier(data || []), [data]) - console.log('sortedData', sortedData) const table = useReactTable({ data: sortedData, columns, diff --git a/frontend/providers/aiproxy/app/api/get-keys/route.ts b/frontend/providers/aiproxy/app/api/get-keys/route.ts index c37296da5d9..13d3ff9c136 100644 --- a/frontend/providers/aiproxy/app/api/get-keys/route.ts +++ b/frontend/providers/aiproxy/app/api/get-keys/route.ts @@ -65,7 +65,7 @@ async function fetchTokens( } return { - tokens: result.data.tokens, + tokens: result.data.tokens.sort((a, b) => a.name.localeCompare(b.name)), total: result.data.total } } catch (error) { diff --git a/frontend/providers/aiproxy/app/api/get-models/route.ts b/frontend/providers/aiproxy/app/api/get-models/route.ts index 05906728b7f..7f2d205fb48 100644 --- a/frontend/providers/aiproxy/app/api/get-models/route.ts +++ b/frontend/providers/aiproxy/app/api/get-models/route.ts @@ -37,7 +37,7 @@ async function fetchModels(): Promise { throw new Error(result.message || 'get models API request failed') } - return result.data + return result.data.sort((a, b) => a.localeCompare(b)) } catch (error) { console.error('Error fetching models:', error) return Promise.reject(error) diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index 2f5f1b431b9..b2d25dec178 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -257,7 +257,10 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { const { data, isLoading } = useQuery({ queryKey: ['getKeys', page, pageSize], queryFn: () => getKeys({ page, perPage: pageSize }), - refetchOnReconnect: true + refetchOnReconnect: true, + onSuccess(data) { + setTotal(data.total) + } }) const columnHelper = createColumnHelper() @@ -588,7 +591,6 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { columns, getCoreRowModel: getCoreRowModel() }) - console.log(data) return ( @@ -649,7 +651,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { ) : ( <> - +
{table.getHeaderGroups().map((headerGroup) => ( From 3590b12f376bf2774c012a70cd315194beea34ff Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Wed, 6 Nov 2024 16:18:49 +0800 Subject: [PATCH 34/47] update input --- .../aiproxy/app/[lng]/(user)/logs/page.tsx | 29 ++++--------------- .../components/SelectDateRange/index.tsx | 2 +- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index fe23fb5bfcf..34c7e60385f 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -230,30 +230,11 @@ export default function Home(): React.JSX.Element { - - {/* - - {t('logs.status')} - - ({ - value: item, - label: item - }))} - onchange={(val: string) => setModelName(val)} - /> - */} - - - {t('logs.time')} - + + + {t('logs.time')} + + { selectedRange?.from && setStartTime(startOfDay(selectedRange.from)) From 1d4b5c608a6491d8214453fd2e02e8ee4eaf008a Mon Sep 17 00:00:00 2001 From: zjy <3161362058@qq.com> Date: Wed, 6 Nov 2024 16:46:49 +0800 Subject: [PATCH 35/47] update log page --- .../aiproxy/app/[lng]/(user)/logs/page.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx index 34c7e60385f..c3ca7c7907b 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/logs/page.tsx @@ -230,18 +230,16 @@ export default function Home(): React.JSX.Element { - + {t('logs.time')} - - - + From 796b0ca6e126f705043d2f24a18302e3da8daab9 Mon Sep 17 00:00:00 2001 From: lim Date: Wed, 6 Nov 2024 09:32:00 +0000 Subject: [PATCH 36/47] ok --- .../aiproxy/app/[lng]/(user)/price/page.tsx | 79 +++++++++---- .../aiproxy/app/i18n/locales/en/common.json | 19 +++- .../aiproxy/app/i18n/locales/zh/common.json | 19 +++- .../aiproxy/components/user/KeyList.tsx | 6 +- .../aiproxy/components/user/ModelList.tsx | 104 ++++++++++-------- frontend/providers/aiproxy/types/front.d.ts | 12 ++ 6 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 frontend/providers/aiproxy/types/front.d.ts diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx index c9349716cfd..45eb13fca01 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx @@ -18,16 +18,17 @@ import { useI18n } from '@/providers/i18n/i18nContext' import { useQuery } from '@tanstack/react-query' import { ModelPrice } from '@/types/backend' import { getModelPrices } from '@/api/platform' -import { useMemo, useState } from 'react' +import { useMemo } from 'react' import { - Column, createColumnHelper, getCoreRowModel, useReactTable, flexRender } from '@tanstack/react-table' -import { TFunction } from 'i18next' 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' @@ -115,6 +116,10 @@ function PriceTable() { } } + 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)) @@ -122,31 +127,61 @@ function PriceTable() { } 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 getIdentifier = (modelName: string): string => { - return modelName.toLowerCase().split(/[-._\d]/)[0] - } - 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)) { @@ -155,9 +190,15 @@ function PriceTable() { groupedModels.get(identifier)!.push(model) }) - const sortedEntries = Array.from(groupedModels.entries()).sort((a, b) => - a[0].localeCompare(b[0]) - ) + // 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) } diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index b28c43e4a65..d1692109a98 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -1,6 +1,6 @@ { - "title": "Ai proxy", - "description": "Ai agent", + "title": "AI proxy", + "description": "AI agent", "Sidebar": { "Home": "AI Proxy", "Logs": "call log", @@ -55,7 +55,7 @@ "copy": "copy", "createKey": "New", "Key": { - "create": "Create a new AI Proxy" + "create": "Create a new API Key" }, "confirm": "confirm", "keystatus": { @@ -74,5 +74,16 @@ "price": { "title": "mode price", "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" } diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index fb63c5b0497..74fb1f1ce0a 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -1,6 +1,6 @@ { - "title": "Ai 代理", - "description": "Ai 代理", + "title": "AI 代理", + "description": "AI 代理", "Sidebar": { "Home": "AI Proxy", "Logs": "调用日志", @@ -55,7 +55,7 @@ "copy": "复制", "createKey": "新建", "Key": { - "create": "新建 AI Proxy" + "create": "新建 API Key" }, "confirm": "确认", "keystatus": { @@ -74,5 +74,16 @@ "price": { "title": "模型价格", "per1kTokens": "1k tokens" - } + }, + "ernie": "百度文心", + "qwen": "阿里千问", + "chatglm": "智谱", + "deepseek": "deepseek", + "moonshot": "月之暗面", + "sparkdesk": "讯飞星火", + "abab": "minimax", + "doubao": "字节豆包", + "glm": "智谱", + "o": "OpenAI", + "gpt": "OpenAI" } diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index b2d25dec178..fb7ac99e742 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -316,7 +316,11 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { } ) }}> - {'sk-' + '*'.repeat(info.getValue().length)} + + {'sk-' + + info.getValue().substring(0, 8) + + '*'.repeat(Math.max(0, info.getValue().length - 8))} + ) }), diff --git a/frontend/providers/aiproxy/components/user/ModelList.tsx b/frontend/providers/aiproxy/components/user/ModelList.tsx index d8f251ae4a4..ac833669c8f 100644 --- a/frontend/providers/aiproxy/components/user/ModelList.tsx +++ b/frontend/providers/aiproxy/components/user/ModelList.tsx @@ -19,51 +19,10 @@ 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' -// 图标映射和标识符关系 -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'] - } -} +import { ModelIdentifier } from '@/types/front' -const getIdentifier = (modelName: string): string => { - return modelName.toLowerCase().split(/[-._\d]/)[0] -} - -// 获取模型图标 -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 getIdentifier = (modelName: string): ModelIdentifier => { + return modelName.toLowerCase().split(/[-._\d]/)[0] as ModelIdentifier } const sortModels = (models: string[]): string[] => { @@ -73,10 +32,12 @@ const sortModels = (models: string[]): string[] => { // 分组 models.forEach((model) => { const identifier = getIdentifier(model) - if (!groupMap.has(identifier)) { - groupMap.set(identifier, []) + // 特殊处理 gpt 和 o1,将它们归为同一组 'openai' + const groupKey = identifier === 'gpt' || identifier === 'o' ? 'openai' : identifier + if (!groupMap.has(groupKey)) { + groupMap.set(groupKey, []) } - groupMap.get(identifier)?.push(model) + groupMap.get(groupKey)?.push(model) }) // 按照 identifier 排序并扁平化结果 @@ -87,6 +48,53 @@ const sortModels = (models: string[]): string[] => { // 模型组件 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'] + } + } + + // 获取模型图标 + 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) @@ -102,7 +110,7 @@ const ModelComponent = ({ modelName }: { modelName: string }) => { return ( - + Date: Wed, 6 Nov 2024 12:30:18 +0000 Subject: [PATCH 37/47] ok --- .../aiproxy/app/[lng]/(user)/layout.tsx | 9 ++++ frontend/providers/aiproxy/app/i18n/client.ts | 44 +++++++++---------- .../aiproxy/app/i18n/locales/en/common.json | 29 ++++++------ .../aiproxy/app/i18n/locales/zh/common.json | 11 ++--- .../aiproxy/components/user/KeyList.tsx | 4 +- .../aiproxy/components/user/Sidebar.tsx | 2 + 6 files changed, 56 insertions(+), 43 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index 9b1a73f737c..561c10b7b54 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -55,6 +55,15 @@ export default function UserLayout({ children }: { children: React.ReactNode }) } } + ;(async () => { + try { + const lang = await sealosApp.getLanguage() + i18n.changeLanguage(lang.lng) + } catch (error) { + i18n.changeLanguage('zh') + } + })() + return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/frontend/providers/aiproxy/app/i18n/client.ts b/frontend/providers/aiproxy/app/i18n/client.ts index 5ec1cd087b2..b19ee779ff7 100644 --- a/frontend/providers/aiproxy/app/i18n/client.ts +++ b/frontend/providers/aiproxy/app/i18n/client.ts @@ -1,20 +1,20 @@ -'use client'; +'use client' -import { useEffect } from 'react'; +import { useEffect } from 'react' import { FallbackNs, initReactI18next, useTranslation as useTranslationOrg, UseTranslationOptions, UseTranslationResponse -} from 'react-i18next'; -import i18next, { FlatNamespace, KeyPrefix } from 'i18next'; -import LanguageDetector from 'i18next-browser-languagedetector'; -import resourcesToBackend from 'i18next-resources-to-backend'; +} 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'; +import { getOptions, languages } from './settings' -const runsOnServerSide = typeof window === 'undefined'; +const runsOnServerSide = typeof window === 'undefined' i18next .use(initReactI18next) @@ -31,7 +31,7 @@ i18next order: ['path', 'htmlTag', 'cookie', 'navigator'] }, preload: runsOnServerSide ? languages : [] - }); + }) export function useTranslationClientSide< Ns extends FlatNamespace, @@ -41,26 +41,26 @@ export function useTranslationClientSide< ns?: Ns, options?: UseTranslationOptions ): UseTranslationResponse, KPrefix> { - const ret = useTranslationOrg(ns, options); - const { i18n } = ret; + const ret = useTranslationOrg(ns, options) + const { i18n } = ret // server side handle - if (runsOnServerSide) { - if (lng && i18n.resolvedLanguage !== lng) { - i18n.changeLanguage(lng); - } - return ret; - } + // if (runsOnServerSide) { + // if (lng && i18n.resolvedLanguage !== lng) { + // i18n.changeLanguage(lng); + // } + // return ret; + // } // client side handle // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { if (!lng || i18n.resolvedLanguage === lng) { - return; + return } - i18n.changeLanguage(lng); - localStorage.setItem('userLanguage', lng); - }, [lng, i18n, i18n.resolvedLanguage]); + i18n.changeLanguage(lng) + localStorage.setItem('userLanguage', lng) + }, [lng, i18n, i18n.resolvedLanguage]) - return ret; + return ret } diff --git a/frontend/providers/aiproxy/app/i18n/locales/en/common.json b/frontend/providers/aiproxy/app/i18n/locales/en/common.json index d1692109a98..5b5be85b2bd 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -2,12 +2,12 @@ "title": "AI proxy", "description": "AI agent", "Sidebar": { - "Home": "AI Proxy", - "Logs": "call log", - "Price": "model price" + "Home": "API Keys", + "Logs": "Logs", + "Price": "Pricing" }, "keyList": { - "title": "AI Proxy" + "title": "API Keys" }, "key": { "key": "API Key", @@ -27,16 +27,17 @@ "updateSuccess": "Update status successful", "updateFailed": "Update status failed", "unused": "not use", - "inputPrice": "Enter unit price", - "outputPrice": "Output unit price" + "inputPrice": "Input price", + "outputPrice": "Output price", + "createName": "Name" }, "logs": { - "call_log": "call log", - "name": "name", + "call_log": "Logs", + "name": "Name", "status": "state", - "time": "time", + "time": "Time", "modal": "Model", - "prompt_tokens": "enter", + "prompt_tokens": "input", "completion_tokens": "output", "price": "price", "select_modal": "Please select a model", @@ -55,9 +56,9 @@ "copy": "copy", "createKey": "New", "Key": { - "create": "Create a new API Key" + "create": "Create API Key" }, - "confirm": "confirm", + "confirm": "Confirm", "keystatus": { "enabled": "enable", "disabled": "Disable", @@ -70,9 +71,9 @@ "enable": "enable", "copySuccess": "Copied successfully", "copyFailed": "Copy failed", - "noData": "You don’t have an AI Proxy yet", + "noData": "You don’t have an API Key yet", "price": { - "title": "mode price", + "title": "Pricing", "per1kTokens": "1k tokens" }, "ernie": "baidu-ernie", diff --git a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json index 74fb1f1ce0a..882e14d0787 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -2,12 +2,12 @@ "title": "AI 代理", "description": "AI 代理", "Sidebar": { - "Home": "AI Proxy", + "Home": "API Keys", "Logs": "调用日志", "Price": "模型价格" }, "keyList": { - "title": "AI Proxy" + "title": "API Keys" }, "key": { "key": "API Key", @@ -28,7 +28,8 @@ "updateFailed": "状态更新失败", "unused": "未使用", "inputPrice": "输入单价", - "outputPrice": "输出单价" + "outputPrice": "输出单价", + "createName": "名称" }, "logs": { "call_log": "调用日志", @@ -50,7 +51,7 @@ "Page": "页", "Total": "总数", "modelList": { - "title": "可支持的模型" + "title": "支持的模型" }, "copy": "复制", "createKey": "新建", @@ -70,7 +71,7 @@ "enable": "启用", "copySuccess": "复制成功", "copyFailed": "复制失败", - "noData": "你还没有 AI Proxy", + "noData": "你还没有 API Key", "price": { "title": "模型价格", "per1kTokens": "1k tokens" diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index fb7ac99e742..6c5ca254ff2 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -806,7 +806,7 @@ function CreateKeyModal({ background="grayModern.25" w="full"> - + {t('Key.create')} - {t('key.name')} + {t('key.createName')} { fontWeight={500} lineHeight="16px" letterSpacing="0.5px" + textAlign="center" + whiteSpace="nowrap" _groupHover={{ color: 'grayModern.900' }}> {menu.value} From 6eafdde0332a03dd4a8d29674216c4206848b8c3 Mon Sep 17 00:00:00 2001 From: lim Date: Thu, 7 Nov 2024 10:20:42 +0000 Subject: [PATCH 38/47] ok --- .../aiproxy/app/[lng]/(user)/layout.tsx | 72 +++- .../aiproxy/app/[lng]/(user)/price/page.tsx | 12 +- .../providers/aiproxy/app/[lng]/layout.tsx | 6 +- frontend/providers/aiproxy/app/i18n/client.ts | 28 +- .../aiproxy/app/i18n/locales/en/common.json | 69 ++-- .../aiproxy/app/i18n/locales/zh/common.json | 13 +- .../aiproxy/components/user/KeyList.tsx | 379 ++++++++++-------- .../aiproxy/components/user/Sidebar.tsx | 23 +- frontend/providers/aiproxy/middleware.ts | 51 +-- 9 files changed, 340 insertions(+), 313 deletions(-) diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx index 561c10b7b54..1de240c7627 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/layout.tsx @@ -5,19 +5,38 @@ import SideBar from '@/components/user/Sidebar' import { EVENT_NAME } from 'sealos-desktop-sdk' import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' -import { useEffect } from 'react' +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 // 这里会获取最新的 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 response = createSealosApp() + const cleanup = createSealosApp() ;(async () => { try { const newSession = JSON.stringify(await sealosApp.getSession()) @@ -26,18 +45,24 @@ export default function UserLayout({ children }: { children: React.ReactNode }) localStorage.setItem('session', newSession) window.location.reload() } - console.log('devbox: app init success') + console.log('aiproxy: app init success') } catch (err) { - console.log('devbox: app is not running in desktop') + console.log('aiproxy: app is not running in desktop') if (!process.env.NEXT_PUBLIC_MOCK_USER) { localStorage.removeItem('session') } } })() - return response + 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() @@ -46,32 +71,41 @@ export default function UserLayout({ children }: { children: React.ReactNode }) initConfig() - const changeI18n = async (data: any) => { + const initLanguage = async () => { + const pathLng = pathname.split('/')[1] try { - const { lng } = await sealosApp.getLanguage() - i18n.changeLanguage(lng) + 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) { - i18n.changeLanguage('zh') + if (error instanceof Error) { + console.debug('Language initialization error:', error.message) + } else { + console.debug('Unknown language initialization error:', error) + } } } - ;(async () => { - try { - const lang = await sealosApp.getLanguage() - i18n.changeLanguage(lang.lng) - } catch (error) { - i18n.changeLanguage('zh') - } - })() + initLanguage() + + const cleanup = sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, handleI18nChange) - return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n) + return () => { + if (cleanup && typeof cleanup === 'function') { + cleanup() + } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( - + {/* Main Content */} diff --git a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx index 45eb13fca01..e8545672698 100644 --- a/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx +++ b/frontend/providers/aiproxy/app/[lng]/(user)/price/page.tsx @@ -207,7 +207,17 @@ function PriceTable() { const columns = [ columnHelper.accessor((row) => row.name, { id: 'name', - header: () => t('key.name'), + header: () => ( + + {t('key.name')} + + ), cell: (info) => }), columnHelper.accessor((row) => row.prompt, { diff --git a/frontend/providers/aiproxy/app/[lng]/layout.tsx b/frontend/providers/aiproxy/app/[lng]/layout.tsx index 68535b1c5dd..5b861c6a6b4 100644 --- a/frontend/providers/aiproxy/app/[lng]/layout.tsx +++ b/frontend/providers/aiproxy/app/[lng]/layout.tsx @@ -39,11 +39,9 @@ export default async function RootLayout({ params }: Readonly<{ children: React.ReactNode - params: { - lng: string - } + params: Promise<{ lng: string }> }>): Promise { - const { lng } = await params + const lng = (await params).lng return ( diff --git a/frontend/providers/aiproxy/app/i18n/client.ts b/frontend/providers/aiproxy/app/i18n/client.ts index b19ee779ff7..29461c7f8c5 100644 --- a/frontend/providers/aiproxy/app/i18n/client.ts +++ b/frontend/providers/aiproxy/app/i18n/client.ts @@ -1,10 +1,9 @@ 'use client' -import { useEffect } from 'react' import { FallbackNs, initReactI18next, - useTranslation as useTranslationOrg, + useTranslation, UseTranslationOptions, UseTranslationResponse } from 'react-i18next' @@ -28,7 +27,7 @@ i18next ...getOptions(), lng: undefined, // let detect the language on client side detection: { - order: ['path', 'htmlTag', 'cookie', 'navigator'] + order: ['path', 'htmlTag', 'navigator'] }, preload: runsOnServerSide ? languages : [] }) @@ -41,26 +40,11 @@ export function useTranslationClientSide< ns?: Ns, options?: UseTranslationOptions ): UseTranslationResponse, KPrefix> { - const ret = useTranslationOrg(ns, options) - const { i18n } = ret + const ret = useTranslation(ns, options) - // server side handle - // if (runsOnServerSide) { - // if (lng && i18n.resolvedLanguage !== lng) { - // i18n.changeLanguage(lng); - // } - // return ret; - // } - - // client side handle - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (!lng || i18n.resolvedLanguage === lng) { - return - } - i18n.changeLanguage(lng) - localStorage.setItem('userLanguage', lng) - }, [lng, i18n, i18n.resolvedLanguage]) + 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 index 5b5be85b2bd..347c319059a 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/en/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/en/common.json @@ -1,6 +1,6 @@ { "title": "AI proxy", - "description": "AI agent", + "description": "AI proxy", "Sidebar": { "Home": "API Keys", "Logs": "Logs", @@ -11,22 +11,22 @@ }, "key": { "key": "API Key", - "name": "name", - "createdAt": "creation time", - "lastUsedAt": "last use time", - "status": "state", + "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": "operate", + "actions": "Operation", "deleteSuccess": "Delete key successfully", "deleteFailed": "Failed to delete key", "updateSuccess": "Update status successful", "updateFailed": "Update status failed", - "unused": "not use", + "unused": "Not use", "inputPrice": "Input price", "outputPrice": "Output price", "createName": "Name" @@ -34,57 +34,58 @@ "logs": { "call_log": "Logs", "name": "Name", - "status": "state", + "status": "State", "time": "Time", "modal": "Model", - "prompt_tokens": "input", - "completion_tokens": "output", - "price": "price", - "select_modal": "Please select a model", - "select_token_name": "Please select a name", + "prompt_tokens": "Input", + "completion_tokens": "Output", + "price": "Price", + "select_modal": "Please select model", + "select_token_name": "Please select name", "model": "Model", - "total_price": "lump sum", - "total_price_tip": "Enter the number of tokens x enter the fee Enter the number of tokens x enter the fee", - "success": "success", - "failed": "failed" + "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", + "Total": "Total", "modelList": { "title": "Supported models" }, - "copy": "copy", + "copy": "Copy", "createKey": "New", "Key": { "create": "Create API Key" }, "confirm": "Confirm", "keystatus": { - "enabled": "enable", + "enabled": "Enable", "disabled": "Disable", "expired": "Expired", - "exhausted": "exhausted", - "unknown": "unknown" + "exhausted": "Exhausted", + "unknown": "Unknown" }, - "delete": "delete", + "delete": "Delete", "disable": "Disable", - "enable": "enable", - "copySuccess": "Copied successfully", + "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", + "ernie": "Baidu-Ernie", + "qwen": "Alibaba-Qwen", + "chatglm": "BigModel-Chatglm", + "deepseek": "Deepseek", + "moonshot": "Moonshot", + "sparkdesk": "Sparkdesk", + "abab": "Minimax", "doubao": "ByteDance-Doubao", - "glm": "glm", + "glm": "Glm", "o": "OpenAI", - "gpt": "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 index 882e14d0787..5a6226556e4 100644 --- a/frontend/providers/aiproxy/app/i18n/locales/zh/common.json +++ b/frontend/providers/aiproxy/app/i18n/locales/zh/common.json @@ -16,9 +16,9 @@ "lastUsedAt": "最后使用时间", "status": "状态", "namePlaceholder": "请输入名称", - "nameRequired": "请输入 key 名字", + "nameRequired": "请输入 Key 名字", "nameMaxLength": "key 长度不合法", - "nameOnlyLettersAndNumbers": "key 名字包含特殊字符", + "nameOnlyLettersAndNumbers": "Key 名字包含特殊字符", "createSuccess": "创建成功", "createFailed": "创建失败", "actions": "操作", @@ -44,7 +44,7 @@ "select_modal": "请选择模型", "select_token_name": "请选择名称", "total_price": "总金额", - "total_price_tip": "输入 token 数 x 输入费用 + 输入 token 数 x 输入费用", + "total_price_tip": "(输入 token 数×输入价格)+(输出 token 数 × 输出价格)", "success": "成功", "failed": "失败" }, @@ -79,12 +79,13 @@ "ernie": "百度文心", "qwen": "阿里千问", "chatglm": "智谱", - "deepseek": "deepseek", + "deepseek": "Deepseek", "moonshot": "月之暗面", "sparkdesk": "讯飞星火", - "abab": "minimax", + "abab": "Minimax", "doubao": "字节豆包", "glm": "智谱", "o": "OpenAI", - "gpt": "OpenAI" + "gpt": "OpenAI", + "createKey2": "新建 Key" } diff --git a/frontend/providers/aiproxy/components/user/KeyList.tsx b/frontend/providers/aiproxy/components/user/KeyList.tsx index 6c5ca254ff2..b082d643805 100644 --- a/frontend/providers/aiproxy/components/user/KeyList.tsx +++ b/frontend/providers/aiproxy/components/user/KeyList.tsx @@ -53,15 +53,6 @@ export function KeyList(): JSX.Element { const { lng } = useI18n() const { t } = useTranslationClientSide(lng, 'common') const { isOpen, onOpen, onClose } = useDisclosure() - 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 aiproxyBackend = useBackendStore((state) => state.aiproxyBackend) return ( <> @@ -79,70 +70,6 @@ export function KeyList(): JSX.Element { - {/* header */} - - - - - 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 */} {/* modal */} @@ -182,6 +109,7 @@ const CustomHeader = ({ column, t }: { column: Column; t: TFunction } } 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) @@ -597,116 +525,214 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => { }) return ( - - {!isLoading && data?.tokens.length === 0 ? ( - -
- - - + {isLoading || data?.tokens.length === 0 ? ( + + +
+ + + + + + + + + + {t('noData')} + + + + - - - -
+
+
) : ( <> - -
- - {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)} - /> + + + + + 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)} + /> +
)} - + ) } @@ -879,8 +905,7 @@ function CreateKeyModal({ gap="12px">