From 8d660ec7c0f08021351b313aaf9e68e2ac07c717 Mon Sep 17 00:00:00 2001 From: scarqin Date: Thu, 26 Dec 2024 16:05:56 +0800 Subject: [PATCH 01/17] feat: ai model config --- .../src/pages/aiSetting/AIUnconfigure.tsx | 139 ++++++++++++++++++ .../src/pages/aiSetting/AiSettingList.tsx | 131 +---------------- 2 files changed, 143 insertions(+), 127 deletions(-) create mode 100644 frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx new file mode 100644 index 00000000..8634e890 --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx @@ -0,0 +1,139 @@ +import Icon, { LoadingOutlined } from '@ant-design/icons' +import WithPermission from '@common/components/aoplatform/WithPermission' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { App, Button, Card, Divider, Empty, Spin, Tag } from 'antd' +import { FC, memo, useEffect, useState } from 'react' +import { AiSettingListItem } from './AiSettingList' + +const CardBox = memo( + ({ provider, openModal }: { provider: AiSettingListItem; openModal: (provider: AiSettingListItem) => void }) => { + return ( + +
+ + {provider.name} +
+ + {provider.configured ? $t('已配置') : $t('未配置')} + + + } + className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " + classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} + > +
+
+ {provider.configured && ( + <> + + {provider.defaultLlm} + + )} +
+ + + +
+
+ ) + } +) +const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { + return ( + <> + {modelList.length > 0 ? ( +
+ {modelList.map((provider: AiSettingListItem) => ( + + ))} +
+ ) : ( + + )} + + ) +} +const AIUnconfigure: FC = () => { + const { message } = App.useApp() + const { fetchData } = useFetch() + const [aiSettingList, setAiSettingList] = useState([]) + const [loading, setLoading] = useState(false) + + const getAiSettingList = () => { + setLoading(true) + return fetchData[] }>>( + `ai/providers/unconfigured`, + { method: 'GET', eoTransformKeys: ['default_llm', 'default_llm_logo'] } + ) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setAiSettingList( + data.providers?.map((x: AiSettingListItem) => ({ + ...x, + name: $t(x.name), + llmListStatus: 'unload', + availableLlms: [] + })) + ) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .finally(() => setLoading(false)) + } + + useEffect(() => { + getAiSettingList() + }, []) + + return ( + } + spinning={loading} + > + {aiSettingList && aiSettingList.length > 0 ? ( +
+ {aiSettingList.filter((item) => !item.configured).length > 0 && ( + <> + +

{$t('未配置')}

+ !item.configured) || []} /> + + )} +
+ ) : ( + + )} +
+ ) +} + +export default AIUnconfigure diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index 5d2af89a..9505364f 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -1,16 +1,15 @@ -import { LoadingOutlined } from '@ant-design/icons' import InsidePage from '@common/components/aoplatform/InsidePage' -import WithPermission from '@common/components/aoplatform/WithPermission' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' import { checkAccess } from '@common/utils/permission' import { Icon } from '@iconify/react/dist/iconify.js' -import { App, Button, Card, Divider, Empty, Spin, Tag } from 'antd' -import { memo, useEffect, useRef, useState } from 'react' +import { App } from 'antd' +import { useRef } from 'react' import AIFlowChart from './AIFlowChart' import AiSettingModalContent, { AiSettingModalContentHandle } from './AiSettingModal' +import AIUnconfigure from './AIUnconfigure' export type AiSettingListItem = { name: string @@ -47,36 +46,9 @@ export type AiProviderConfig = { const AiSettingList = () => { const { modal, message } = App.useApp() const { fetchData } = useFetch() - const [aiSettingList, setAiSettingList] = useState([]) - const [loading, setLoading] = useState(false) const modalRef = useRef() const { setAiConfigFlushed, accessData } = useGlobalContext() - const getAiSettingList = () => { - setLoading(true) - return fetchData[] }>>( - `ai/providers/unconfigured`, - { method: 'GET', eoTransformKeys: ['default_llm', 'default_llm_logo'] } - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - ) - .then((response) => { - const { code, data, msg } = response - if (code === STATUS_CODE.SUCCESS) { - setAiSettingList( - data.providers?.map((x: AiSettingListItem) => ({ - ...x, - name: $t(x.name), - llmListStatus: 'unload', - availableLlms: [] - })) - ) - } else { - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }) - .finally(() => setLoading(false)) - } - const openModal = async (entity: AiSettingListItem) => { message.loading($t(RESPONSE_TIPS.loading)) const { code, data, msg } = await fetchData>('ai/provider/config', { @@ -101,7 +73,6 @@ const AiSettingList = () => { onOk: () => { return modalRef.current?.save().then((res) => { if (res === true) setAiConfigFlushed(true) - getAiSettingList() }) }, width: 600, @@ -131,81 +102,6 @@ const AiSettingList = () => { }) } - useEffect(() => { - getAiSettingList() - }, []) - - const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => { - return ( - -
- - {provider.name} -
- - {provider.configured ? $t('已配置') : $t('未配置')} - - - } - className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " - classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} - > -
-
- {provider.configured && ( - <> - - {provider.defaultLlm} - - )} -
- - - -
-
- ) - }) - - const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { - return ( - <> - {modelList.length > 0 ? ( -
- {modelList.map((provider: AiSettingListItem) => ( - - ))} -
- ) : ( - - )} - - ) - } - return ( <> { scrollPage={false} > - } - spinning={loading} - > - {aiSettingList && aiSettingList.length > 0 ? ( -
- {aiSettingList.filter((item) => !item.configured).length > 0 && ( - <> - -

{$t('未配置')}

- !item.configured) || []} /> - - )} -
- ) : ( - - )} -
+
) From 58231eb19c6dcff1dfbd729d719b084afc31570b Mon Sep 17 00:00:00 2001 From: scarqin Date: Fri, 27 Dec 2024 09:56:15 +0800 Subject: [PATCH 02/17] feat: api tab --- .../src/pages/aiSetting/AIUnconfigure.tsx | 30 ++++++++--- .../src/pages/aiSetting/AiSettingList.tsx | 54 +++++++------------ .../core/src/pages/aiSetting/styles.css | 16 ++++++ .../core/src/pages/aiSetting/types.ts | 33 ++++++++++++ 4 files changed, 91 insertions(+), 42 deletions(-) diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx index 8634e890..2f94679f 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx @@ -3,12 +3,18 @@ import WithPermission from '@common/components/aoplatform/WithPermission' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' -import { App, Button, Card, Divider, Empty, Spin, Tag } from 'antd' +import { App, Button, Card, Empty, Spin, Tag } from 'antd' import { FC, memo, useEffect, useState } from 'react' import { AiSettingListItem } from './AiSettingList' const CardBox = memo( - ({ provider, openModal }: { provider: AiSettingListItem; openModal: (provider: AiSettingListItem) => void }) => { + ({ + provider, + openModal + }: { + provider: AiSettingListItem + openModal: (provider: AiSettingListItem) => Promise + }) => { return ( { +const ModelCardArea = ({ + modelList, + className, + openModal +}: { + modelList: AiSettingListItem[] + className?: string + openModal?: (provider: AiSettingListItem) => Promise +}) => { return ( <> {modelList.length > 0 ? ( @@ -69,7 +83,7 @@ const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[ }} > {modelList.map((provider: AiSettingListItem) => ( - + ))} ) : ( @@ -78,7 +92,11 @@ const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[ ) } -const AIUnconfigure: FC = () => { +interface AIUnconfigureProps { + openModal: (entity: AiSettingListItem) => Promise +} + +const AIUnconfigure: FC = ({ openModal }) => { const { message } = App.useApp() const { fetchData } = useFetch() const [aiSettingList, setAiSettingList] = useState([]) @@ -123,8 +141,6 @@ const AIUnconfigure: FC = () => {
{aiSettingList.filter((item) => !item.configured).length > 0 && ( <> - -

{$t('未配置')}

!item.configured) || []} /> )} diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index 9505364f..fb66cb74 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -5,44 +5,13 @@ import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' import { checkAccess } from '@common/utils/permission' import { Icon } from '@iconify/react/dist/iconify.js' -import { App } from 'antd' +import { App, Tabs } from 'antd' import { useRef } from 'react' import AIFlowChart from './AIFlowChart' import AiSettingModalContent, { AiSettingModalContentHandle } from './AiSettingModal' import AIUnconfigure from './AIUnconfigure' +import { AiProviderConfig, AiSettingListItem } from './types' -export type AiSettingListItem = { - name: string - id: string - logo: string - defaultLlm: string - defaultLlmLogo: string - enable: boolean - configured: boolean -} - -export type AiProviderLlmsItems = { - id: string - logo: string - scopes: ('chat' | 'completions')[] - config: string -} - -export type AiProviderDefaultConfig = { - id: string - provider: string - name: string - logo: string - defaultLlm: string - scopes: string[] -} - -export type AiProviderConfig = { - id: string - name: string - config: string - getApikeyUrl: string -} const AiSettingList = () => { const { modal, message } = App.useApp() const { fetchData } = useFetch() @@ -111,8 +80,23 @@ const AiSettingList = () => { showBorder={false} scrollPage={false} > - - +
+ + }, + { + key: 'config', + label: $t('未设置'), + children:
{}
+ } + ]} + /> +
) diff --git a/frontend/packages/core/src/pages/aiSetting/styles.css b/frontend/packages/core/src/pages/aiSetting/styles.css index e12ed580..13d5b62b 100644 --- a/frontend/packages/core/src/pages/aiSetting/styles.css +++ b/frontend/packages/core/src/pages/aiSetting/styles.css @@ -1,4 +1,20 @@ /* Flow Chart Styles */ +.react-flow { + width: 100%; + height: 100%; + min-height: 500px; +} + +.react-flow__container { + width: 100%; + height: 100%; +} + +.react-flow__renderer { + width: 100%; + height: 100%; +} + .react-flow__node { padding: 0; border-radius: 8px; diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index 4935473e..43d46536 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -18,3 +18,36 @@ export interface ModelData { keys: KeyData[] priority?: number } + +export type AiSettingListItem = { + name: string + id: string + logo: string + defaultLlm: string + defaultLlmLogo: string + enable: boolean + configured: boolean +} + +export type AiProviderLlmsItems = { + id: string + logo: string + scopes: ('chat' | 'completions')[] + config: string +} + +export type AiProviderDefaultConfig = { + id: string + provider: string + name: string + logo: string + defaultLlm: string + scopes: string[] +} + +export type AiProviderConfig = { + id: string + name: string + config: string + getApikeyUrl: string +} From a090bb7caac93f5a1f17c46dd056defa5225ad44 Mon Sep 17 00:00:00 2001 From: scarqin Date: Fri, 27 Dec 2024 21:28:35 +0800 Subject: [PATCH 03/17] feat: ai apis --- .../pages/aiApis/components/ApiKeyContent.tsx | 111 ++++++ .../pages/aiApis/components/StatusFilter.tsx | 40 ++ .../packages/core/src/pages/aiApis/index.tsx | 354 +++++++++++++++++- .../packages/core/src/pages/aiApis/types.ts | 16 + 4 files changed, 514 insertions(+), 7 deletions(-) create mode 100644 frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx create mode 100644 frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx create mode 100644 frontend/packages/core/src/pages/aiApis/types.ts diff --git a/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx b/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx new file mode 100644 index 00000000..0ce3c4a4 --- /dev/null +++ b/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx @@ -0,0 +1,111 @@ +import { Codebox } from '@common/components/postcat/api/Codebox' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { AIProvider } from '@core/components/AIProviderSelect' +import { App, DatePicker, Form, Input, Switch } from 'antd' +import dayjs from 'dayjs' +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react' +import { EditAPIKey } from '../types' + +interface ApiKeyContentProps { + provider?: AIProvider + entity: EditAPIKey +} + +const ApiKeyContent: React.FC = forwardRef(({ provider, entity }, ref) => { + const [form] = Form.useForm() + const [neverExpire, setNeverExpire] = useState(true) + const { fetchData } = useFetch() + const { message } = App.useApp() + + useEffect(() => { + try { + const isNeverExpire = entity.expire_time === 0 + setNeverExpire(isNeverExpire) + form.setFieldsValue({ + name: entity.name, + expire_time: isNeverExpire ? undefined : dayjs(entity.expire_time), + config: entity.config + }) + } catch (e) { + form.setFieldsValue({ + name: entity.name, + expire_time: undefined, + config: '' + }) + } + }, []) + + const handleOk = async () => { + try { + const values = await form.validateFields() + const { expire_time, ...restValues } = values + const expireTime = neverExpire ? 0 : expire_time.valueOf() + + const response = await fetchData>('ai/resource/key', { + method: entity.id ? 'PUT' : 'POST', + eoParams: { provider: provider?.id, id: entity.id }, + eoBody: { ...restValues, expire_time: expireTime }, + eoTransformKeys: ['config'] + }) + + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || $t(RESPONSE_TIPS.success)) + return true + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return false + } + } catch (error) { + console.error('Validation failed:', error) + } + } + useImperativeHandle(ref, () => ({ + handleOk + })) + const handleNeverExpireChange = (checked: boolean) => { + setNeverExpire(checked) + if (!checked) { + form.setFieldsValue({ + expire_time: dayjs().add(7, 'days') + }) + } + } + + return ( +
+ + + + + + + +
+ + {neverExpire ? $t('永不过期') : $t('设置过期时间')} +
+
+ {!neverExpire && ( + + + + )} +
+ ) +}) + +export default ApiKeyContent diff --git a/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx b/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx new file mode 100644 index 00000000..64cc18e9 --- /dev/null +++ b/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx @@ -0,0 +1,40 @@ +import { $t } from '@common/locales' +import { Select, Space, theme } from 'antd' +import React from 'react' + +interface StatusFilterProps { + value: string[] + onChange: (value: string[]) => void +} + +const StatusFilter: React.FC = ({ value, onChange }) => { + const { token } = theme.useToken() + + const options = [ + { label: $t('Normal'), value: 'normal', color: token.colorSuccess }, + { label: $t('Exceeded'), value: 'exceeded', color: token.colorError }, + { label: $t('Expired'), value: 'expired', color: token.colorWarning }, + { label: $t('Disabled'), value: 'disabled', color: token.colorTextDisabled }, + { label: $t('Error'), value: 'error', color: token.colorError } + ] + + return ( + + {$t('Status')}: + + + label={$t('优先级')} + name="priority" + rules={[ + { required: true }, + { + validator: async (_, value) => { + if (value <= 0) { + throw new Error($t('优先级必须大于0')) + } + return Promise.resolve() + } + } + ]} + initialValue={1} + > + + + label={$t('参数')} name="config"> Promise +} + +const AiSettingContext = createContext(undefined) + +export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { modal, message } = App.useApp() + const { fetchData } = useFetch() + const { setAiConfigFlushed, accessData } = useGlobalContext() + const modalRef = useRef() + + const openConfigModal = async (entity: AiSettingListItem) => { + message.loading($t(RESPONSE_TIPS.loading)) + const { code, data, msg } = await fetchData>('ai/provider/config', { + method: 'GET', + eoParams: { provider: entity!.id }, + eoTransformKeys: ['get_apikey_url'] + }) + message.destroy() + if (code !== STATUS_CODE.SUCCESS) { + message.error(msg || $t(RESPONSE_TIPS.error)) + return + } + + modal.confirm({ + title: $t('模型配置'), + content: ( + + ), + onOk: () => { + return modalRef.current?.save().then((res) => { + if (res === true) { + setAiConfigFlushed(true) + } + }) + }, + width: 600, + okText: $t('确认'), + footer: (_, { OkBtn, CancelBtn }) => { + return ( +
+ + {$t('从 (0) 获取 API KEY', [data.provider.name])} + + +
+ + {checkAccess('system.devops.ai_provider.edit', accessData) ? : null} +
+
+ ) + }, + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + return {children} +} + +export const useAiSetting = () => { + const context = useContext(AiSettingContext) + if (!context) { + throw new Error('useAiSetting must be used within an AiSettingProvider') + } + return context +} diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index 43d46536..eebcc66a 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -27,6 +27,7 @@ export type AiSettingListItem = { defaultLlmLogo: string enable: boolean configured: boolean + priority?: number } export type AiProviderLlmsItems = { @@ -48,6 +49,7 @@ export type AiProviderDefaultConfig = { export type AiProviderConfig = { id: string name: string - config: string + config?: string getApikeyUrl: string + priority?: number } From e0896864c2cb00750ee95f70168180323b008cd5 Mon Sep 17 00:00:00 2001 From: scarqin Date: Mon, 30 Dec 2024 17:00:09 +0800 Subject: [PATCH 06/17] feat: update list after edit modal --- .../core/src/pages/aiSetting/AIFlowChart.tsx | 11 +- .../src/pages/aiSetting/AIUnconfigure.tsx | 132 ++++++++---------- .../src/pages/aiSetting/AiSettingModal.tsx | 1 + .../aiSetting/components/ModelCardNode.tsx | 10 +- 4 files changed, 75 insertions(+), 79 deletions(-) diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx index 92a0063d..6c824284 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx @@ -1,6 +1,7 @@ 'use client' import { BasicResponse } from '@common/const/const' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { useFetch } from '@common/hooks/http' import { CoordinateExtent, @@ -20,7 +21,6 @@ import { KeyStatusNode } from './components/KeyStatusNode' import { ModelCardNode } from './components/ModelCardNode' import { ServiceCardNode } from './components/NodeComponents' import { LAYOUT } from './constants' -import { useAiSetting } from './contexts/AiSettingContext' import './styles.css' import { ModelData } from './types' @@ -67,7 +67,7 @@ const AIFlowChart = () => { const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const { fetchData } = useFetch() - const { openConfigModal } = useAiSetting() + const { aiConfigFlushed } = useGlobalContext() useEffect(() => { fetchData('ai/providers/configured', { @@ -76,7 +76,7 @@ const AIFlowChart = () => { const mockApiResponse: ApiResponse = response as ApiResponse setModelData(mockApiResponse.data.providers) }) - }, []) + }, [aiConfigFlushed]) useEffect(() => { if (!modelData.length) return @@ -104,8 +104,7 @@ const AIFlowChart = () => { status: model.status, defaultLlm: model.default_llm, logo: model.logo, - id: model.id, - openModal: openConfigModal + id: model.id } })), ...modelData.map((model) => ({ @@ -220,7 +219,7 @@ const AIFlowChart = () => { ) return ( -
+
Promise - }) => { - return ( - -
- - {provider.name} -
- - {provider.configured ? $t('已配置') : $t('未配置')} - -
- } - className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " - classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} - > -
-
- {provider.configured && ( - <> - - {provider.defaultLlm} - - )} -
- - - -
- - ) +const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => { + const { openConfigModal } = useAiSetting() + const { aiConfigFlushed, setAiConfigFlushed } = useGlobalContext() + + const handleOpenModal = async (provider: AiSettingListItem) => { + await openConfigModal(provider) + setAiConfigFlushed(!aiConfigFlushed) } -) -const ModelCardArea = ({ - modelList, - className, - openModal -}: { - modelList: AiSettingListItem[] - className?: string - openModal?: (provider: AiSettingListItem) => Promise -}) => { + + return ( + +
+ + {provider.name} +
+ + {provider.configured ? $t('已配置') : $t('未配置')} + +
+ } + className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " + classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} + > +
+
+ {provider.configured && ( + <> + + {provider.defaultLlm} + + )} +
+ + + +
+ + ) +}) +const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { return ( <> {modelList.length > 0 ? ( @@ -84,7 +77,7 @@ const ModelCardArea = ({ }} > {modelList.map((provider: AiSettingListItem) => ( - + ))}
) : ( @@ -97,8 +90,8 @@ const ModelCardArea = ({ const AIUnConfigure = () => { const [modelData, setModelData] = useState([]) const { fetchData } = useFetch() - const { openConfigModal } = useAiSetting() const [loading, setLoading] = useState(false) + const { aiConfigFlushed } = useGlobalContext() useEffect(() => { setLoading(true) @@ -123,23 +116,20 @@ const AIUnConfigure = () => { } }) .finally(() => setLoading(false)) - }, []) + }, [aiConfigFlushed]) return ( } - spinning={modelData.length === 0} + spinning={loading} > {modelData && modelData.length > 0 ? (
{modelData.filter((item) => !item.configured).length > 0 && ( <> - !item.configured) || []} - /> + !item.configured) || []} /> )}
diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index b9e8486c..74def9cf 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -80,6 +80,7 @@ const AiSettingModalContent = forwardRef { const { code, msg } = response diff --git a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx index b32ce854..ca684709 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx @@ -1,7 +1,9 @@ +import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { Icon } from '@iconify/react' import { Handle, Position } from '@xyflow/react' import { t } from 'i18next' import React from 'react' +import { useAiSetting } from '../contexts/AiSettingContext' import { AiSettingListItem, ModelStatus } from '../types' interface ModelCardData { @@ -14,11 +16,12 @@ interface ModelCardData { type ModelCardNodeData = ModelCardData & { id: string position: { x: number; y: number } - openModal?: (entity: AiSettingListItem) => Promise } export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => { const { title, status, defaultLlm, logo } = data + const { openConfigModal } = useAiSetting() + const { aiConfigFlushed, setAiConfigFlushed } = useGlobalContext() return (
= ({ data }) = data.openModal?.({ id: data.id, defaultLlm: defaultLlm })} + onClick={() => { + openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem) + setAiConfigFlushed(!aiConfigFlushed) + }} />
From b4b9469284eba5e28976fbc53d95597320842eae Mon Sep 17 00:00:00 2001 From: scarqin Date: Mon, 30 Dec 2024 17:03:36 +0800 Subject: [PATCH 07/17] refacor: delete useless code --- .../core/src/pages/aiSetting/AIUnconfigure.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx index 4f7f14f9..f8d8e61d 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx @@ -95,21 +95,14 @@ const AIUnConfigure = () => { useEffect(() => { setLoading(true) - fetchData[] }>>( - `ai/providers/unconfigured`, - { method: 'GET', eoTransformKeys: ['default_llm', 'default_llm_logo'] } - ) + fetchData[] }>>(`ai/providers/unconfigured`, { + method: 'GET', + eoTransformKeys: ['default_llm', 'default_llm_logo'] + }) .then((response) => { const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) { - setModelData( - data.providers?.map((x: AiSettingListItem) => ({ - ...x, - name: $t(x.name), - llmListStatus: 'unload', - availableLlms: [] - })) - ) + setModelData(data.providers) } else { const { message } = App.useApp() message.error(msg || $t(RESPONSE_TIPS.error)) From 54f76d65768ee5c6fe69b80f685cb0b02e95eb23 Mon Sep 17 00:00:00 2001 From: scarqin Date: Tue, 31 Dec 2024 10:27:50 +0800 Subject: [PATCH 08/17] feat: add priority --- .../core/src/pages/aiSetting/AIFlowChart.tsx | 98 ++++++++----------- .../src/pages/aiSetting/AiSettingList.tsx | 14 ++- .../src/pages/aiSetting/AiSettingModal.tsx | 14 ++- 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx index 6c824284..0a68684a 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx @@ -140,7 +140,6 @@ const AIFlowChart = () => { animated: true })) ] - setNodes(newNodes) setEdges(newEdges) }, [modelData]) @@ -156,78 +155,65 @@ const AIFlowChart = () => { ] as CoordinateExtent }, [modelData.length]) - const onNodeDrag: any = useCallback( - (_: MouseEvent, node: Node) => { - if (node.type !== 'modelCard') return + const updateProviderOrder = async (sortedProviderIds: string[]) => { + await fetchData('ai/provider/sort', { + method: 'PUT', + body: JSON.stringify({ + providers: sortedProviderIds + }) + }) + } - setNodes((nds) => { - return nds.map((n) => { - if (n.type === 'keyCard' && n.id === `${node.id}-keys`) { + const onNodeDragStop: any = useCallback((_: any, node: Node) => { + if (node.type !== 'modelCard') return + + setNodes((nds) => { + const modelNodes = nds.filter((n) => n.type === 'modelCard') + const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y) + const sortedProviderIds = sortedNodes.map((node) => node.id) + + // Update provider order outside of setNodes callback + updateProviderOrder(sortedProviderIds) + // Update all node positions in a single pass + return nds.map((n) => { + if (n.type === 'modelCard') { + const index = sortedNodes.findIndex((sn) => sn.id === n.id) + return { + ...n, + position: { + x: LAYOUT.MODEL_NODE_X, + y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + } + } + } + if (n.type === 'keyCard') { + const modelId = n.id.replace('-keys', '') + const modelNode = sortedNodes.find((mn) => mn.id === modelId) + if (modelNode) { + const index = sortedNodes.findIndex((sn) => sn.id === modelId) return { ...n, position: { x: LAYOUT.KEY_NODE_X, - y: node.position.y + y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + 16 } } } - return n - }) + } + return n }) - }, - [setNodes] - ) - - const onNodeDragStop: any = useCallback( - (_: any, node: Node) => { - if (node.type !== 'modelCard') return - - setNodes((nds) => { - const modelNodes = nds.filter((n) => n.type === 'modelCard') - const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y) - - return nds.map((n) => { - if (n.type === 'modelCard') { - const index = sortedNodes.findIndex((sn) => sn.id === n.id) - return { - ...n, - position: { - x: LAYOUT.MODEL_NODE_X, - y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP - } - } - } - if (n.type === 'keyCard') { - const modelId = n.id.replace('-keys', '') - const modelNode = sortedNodes.find((mn) => mn.id === modelId) - if (modelNode) { - const index = sortedNodes.findIndex((sn) => sn.id === modelId) - return { - ...n, - position: { - x: LAYOUT.KEY_NODE_X, - y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + 16 - } - } - } - } - return n - }) - }) - }, - [setNodes] - ) + }) + }, []) return ( -
+
{ return ( { >
+ children: ( +
+ +
+ ) }, { key: 'config', label: $t('未设置'), children: ( -
+
) diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index 74def9cf..f5d3e206 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -1,8 +1,9 @@ +import { QuestionCircleOutlined } from '@ant-design/icons' import { Codebox } from '@common/components/postcat/api/Codebox' import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' -import { App, Form, InputNumber, Select, Tag } from 'antd' +import { App, Form, InputNumber, Select, Tag, Tooltip } from 'antd' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' import { AiProviderConfig, AiProviderLlmsItems } from './AiSettingList' @@ -130,7 +131,16 @@ const AiSettingModalContent = forwardRef - label={$t('优先级')} + label={ + + {$t('负载优先级')} + + + + + } name="priority" rules={[ { required: true }, From 9777859f427dc32853039fe6bb7354cbc7cd6760 Mon Sep 17 00:00:00 2001 From: scarqin Date: Tue, 31 Dec 2024 12:01:42 +0800 Subject: [PATCH 09/17] feat: add llm status manage --- .../core/src/pages/aiSetting/AIFlowChart.tsx | 16 ++--- .../src/pages/aiSetting/AiSettingModal.tsx | 60 ++++++++++++++----- .../aiSetting/components/ModelCardNode.tsx | 40 +++++++------ .../core/src/pages/aiSetting/types.ts | 19 ++++-- 4 files changed, 88 insertions(+), 47 deletions(-) diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx index 0a68684a..a2799f5d 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx @@ -22,17 +22,17 @@ import { ModelCardNode } from './components/ModelCardNode' import { ServiceCardNode } from './components/NodeComponents' import { LAYOUT } from './constants' import './styles.css' -import { ModelData } from './types' +import { ModelListData } from './types' export type ApiResponse = BasicResponse<{ backup: { id: string name: string } - providers: ModelData[] + providers: ModelListData[] }> -const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => { +const calculateNodePositions = (models: ModelListData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => { return models.reduce( (acc, model, index) => { const y = startY + index * gap @@ -63,7 +63,7 @@ const edgeTypes: EdgeTypes = { } const AIFlowChart = () => { - const [modelData, setModelData] = useState([]) + const [modelData, setModelData] = useState([]) const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const { fetchData } = useFetch() @@ -71,7 +71,8 @@ const AIFlowChart = () => { useEffect(() => { fetchData('ai/providers/configured', { - method: 'GET' + method: 'GET', + eoTransformKeys: ['default_llm'] }).then((response) => { const mockApiResponse: ApiResponse = response as ApiResponse setModelData(mockApiResponse.data.providers) @@ -84,7 +85,6 @@ const AIFlowChart = () => { const positions = calculateNodePositions(modelData) // subtract 5 to make sure the service node is aligned with the top model node const serviceY = positions[modelData[0].id].y - 5 - const newNodes = [ { id: 'apiService', @@ -100,9 +100,9 @@ const AIFlowChart = () => { type: 'modelCard', position: positions[model.id], data: { - title: model.name, + name: model.name, status: model.status, - defaultLlm: model.default_llm, + defaultLlm: model.defaultLlm, logo: model.logo, id: model.id } diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index f5d3e206..71ad949a 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -3,9 +3,9 @@ import { Codebox } from '@common/components/postcat/api/Codebox' import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' -import { App, Form, InputNumber, Select, Tag, Tooltip } from 'antd' +import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' -import { AiProviderConfig, AiProviderLlmsItems } from './AiSettingList' +import { AiProviderConfig, AiProviderLlmsItems, ModelDetailData } from './types' export type AiSettingModalContentProps = { entity: AiProviderConfig & { defaultLlm: string } @@ -16,12 +16,6 @@ export type AiSettingModalContentHandle = { save: () => Promise } -type AiSettingModalContentField = { - config: string - defaultLlm: string - priority: number -} - const AiSettingModalContent = forwardRef((props, ref) => { const [form] = Form.useForm() const { message } = App.useApp() @@ -55,13 +49,15 @@ const AiSettingModalContent = forwardRef { + if (!isChecked) { + return '保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。' + } + return '保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。' + } + useImperativeHandle(ref, () => ({ save })) @@ -112,8 +115,9 @@ const AiSettingModalContent = forwardRef - label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}> + label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}> - + label={ {$t('负载优先级')} @@ -147,7 +151,7 @@ const AiSettingModalContent = forwardRef { if (value <= 0) { - throw new Error($t('优先级必须大于0')) + throw new Error($t('优先级必须大于 0')) } return Promise.resolve() } @@ -158,16 +162,42 @@ const AiSettingModalContent = forwardRef - label={$t('参数')} name="config"> + label={$t('API Key(默认 Key)')} name="config"> + + {entity.id && ( + +
+
+ 当前调用状态: + {entity.status === 'enabled' && {$t('正常')}} + {entity.status === 'disabled' && {$t('停用')}} + {entity.status === 'abnormal' && {$t('异常')}} +
+ + { + form.setFieldsValue({ enable: checked }) + }} + /> + +
+ {(entity.status === 'enabled' && !form.getFieldValue('enable')) || + (entity.status !== 'enabled' && form.getFieldValue('enable')) ? ( +
* {getTooltipText(form.getFieldValue('enable'))}
+ ) : null} +
+ )} ) }) diff --git a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx index ca684709..1b381485 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx @@ -4,27 +4,34 @@ import { Handle, Position } from '@xyflow/react' import { t } from 'i18next' import React from 'react' import { useAiSetting } from '../contexts/AiSettingContext' -import { AiSettingListItem, ModelStatus } from '../types' +import { AiSettingListItem, ModelDetailData, ModelStatus } from '../types' -interface ModelCardData { - title: string - status: ModelStatus - logo: string - defaultLlm: string -} - -type ModelCardNodeData = ModelCardData & { +type ModelCardNodeData = ModelDetailData & { id: string position: { x: number; y: number } } export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => { - const { title, status, defaultLlm, logo } = data + const { name, status, defaultLlm, logo } = data const { openConfigModal } = useAiSetting() const { aiConfigFlushed, setAiConfigFlushed } = useGlobalContext() + + const getStatusIcon = (status: ModelStatus) => { + switch (status) { + case 'enabled': + return { icon: 'mdi:check-circle', color: 'text-green-500' } + case 'disabled': + return { icon: 'mdi:pause-circle', color: 'text-gray-400' } + case 'abnormal': + return { icon: 'mdi:alert-circle', color: 'text-red-500' } + } + } + + const statusConfig = getStatusIcon(status) + return (
@@ -32,17 +39,14 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
-
+
- {title} - + {name} +
{/* Action buttons */} diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index eebcc66a..814346c9 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -1,4 +1,4 @@ -export type ModelStatus = 'enable' | 'abnormal'|'disabled' +export type ModelStatus = 'enabled' | 'abnormal'|'disabled' export type KeyStatus ='normal' | 'abnormal'|'disabled' export interface KeyData { @@ -7,18 +7,23 @@ export interface KeyData { status: KeyStatus, } -export interface ModelData { +export interface ModelListData { id: string name: string logo: string - default_llm: string + defaultLlm: string status: ModelStatus api_count: number key_count: number keys: KeyData[] +} +export interface ModelDetailData extends ModelListData{ + enable:boolean + config: string, priority?: number } + export type AiSettingListItem = { name: string id: string @@ -46,10 +51,12 @@ export type AiProviderDefaultConfig = { scopes: string[] } -export type AiProviderConfig = { +export interface AiProviderConfig { id: string name: string - config?: string + config: string getApikeyUrl: string - priority?: number + priority: number + enable: boolean + status: ModelStatus } From 3ebafcbc031756f31d94d7077b03e1fa93dc0724 Mon Sep 17 00:00:00 2001 From: scarqin Date: Tue, 31 Dec 2024 14:24:15 +0800 Subject: [PATCH 10/17] feat: stopaable tips --- .../core/src/pages/aiSetting/AiSettingModal.tsx | 10 +++++----- .../src/pages/aiSetting/components/ModelCardNode.tsx | 3 --- .../src/pages/aiSetting/contexts/AiSettingContext.tsx | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index 71ad949a..fca85750 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -23,7 +23,7 @@ const AiSettingModalContent = forwardRef() const [loading, setLoading] = useState(false) - + const [enableState, setEnableState] = useState(entity.status === 'enabled') const getLlmList = () => { setLoading(true) fetchData>(`ai/provider/llms`, { @@ -108,10 +108,10 @@ const AiSettingModalContent = forwardRef { form.setFieldsValue({ enable: checked }) + setEnableState(checked) }} />
- {(entity.status === 'enabled' && !form.getFieldValue('enable')) || - (entity.status !== 'enabled' && form.getFieldValue('enable')) ? ( -
* {getTooltipText(form.getFieldValue('enable'))}
+ {(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? ( +
* {getTooltipText(enableState)}
) : null} )} diff --git a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx index 1b381485..bbabc26d 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx @@ -1,4 +1,3 @@ -import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { Icon } from '@iconify/react' import { Handle, Position } from '@xyflow/react' import { t } from 'i18next' @@ -14,7 +13,6 @@ type ModelCardNodeData = ModelDetailData & { export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => { const { name, status, defaultLlm, logo } = data const { openConfigModal } = useAiSetting() - const { aiConfigFlushed, setAiConfigFlushed } = useGlobalContext() const getStatusIcon = (status: ModelStatus) => { switch (status) { @@ -56,7 +54,6 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) = className="text-xl text-gray-400 cursor-pointer hover:text-[--primary-color]" onClick={() => { openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem) - setAiConfigFlushed(!aiConfigFlushed) }} />
diff --git a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx index 41b2800b..502c3bf3 100644 --- a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx +++ b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx @@ -18,7 +18,7 @@ const AiSettingContext = createContext(undefin export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { modal, message } = App.useApp() const { fetchData } = useFetch() - const { setAiConfigFlushed, accessData } = useGlobalContext() + const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext() const modalRef = useRef() const openConfigModal = async (entity: AiSettingListItem) => { @@ -46,7 +46,7 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi onOk: () => { return modalRef.current?.save().then((res) => { if (res === true) { - setAiConfigFlushed(true) + setAiConfigFlushed(!aiConfigFlushed) } }) }, From 9b6d07dc4c4a831313ba209cdb32dedb72e42705 Mon Sep 17 00:00:00 2001 From: scarqin Date: Tue, 31 Dec 2024 15:07:43 +0800 Subject: [PATCH 11/17] feat: apikey --- .../core/src/pages/aiSetting/AIFlowChart.tsx | 53 ++++++++++++------- .../src/pages/aiSetting/AIUnconfigure.tsx | 5 +- .../src/pages/aiSetting/AiSettingList.tsx | 15 ++++++ .../src/pages/aiSetting/AiSettingModal.tsx | 6 +-- .../aiSetting/components/ModelCardNode.tsx | 2 +- .../aiSetting/contexts/AiSettingContext.tsx | 4 +- .../core/src/pages/aiSetting/types.ts | 13 ++--- 7 files changed, 61 insertions(+), 37 deletions(-) diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx index a2799f5d..9f7173bf 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx @@ -15,7 +15,9 @@ import { useNodesState } from '@xyflow/react' import '@xyflow/react/dist/style.css' +import { Button, Space } from 'antd' import { useCallback, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' import CustomEdge from './components/CustomEdge' import { KeyStatusNode } from './components/KeyStatusNode' import { ModelCardNode } from './components/ModelCardNode' @@ -68,11 +70,13 @@ const AIFlowChart = () => { const [edges, setEdges, onEdgesChange] = useEdgesState([]) const { fetchData } = useFetch() const { aiConfigFlushed } = useGlobalContext() + const navigate = useNavigate() useEffect(() => { fetchData('ai/providers/configured', { method: 'GET', eoTransformKeys: ['default_llm'] + // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' }).then((response) => { const mockApiResponse: ApiResponse = response as ApiResponse setModelData(mockApiResponse.data.providers) @@ -207,26 +211,35 @@ const AIFlowChart = () => { return (
- + {modelData.length === 0 ? ( + +
No AI model configured
+ +
+ ) : ( + + )}
) } diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx index f8d8e61d..ba1ae334 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx @@ -6,16 +6,17 @@ import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' import { App, Button, Card, Empty, Spin, Tag } from 'antd' import { memo, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' import { useAiSetting } from './contexts/AiSettingContext' import { AiSettingListItem } from './types' const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => { const { openConfigModal } = useAiSetting() - const { aiConfigFlushed, setAiConfigFlushed } = useGlobalContext() + const navigate = useNavigate() const handleOpenModal = async (provider: AiSettingListItem) => { await openConfigModal(provider) - setAiConfigFlushed(!aiConfigFlushed) + navigate('/aisetting?status=configure') } return ( diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index 353f5bf5..30694d5a 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -1,6 +1,8 @@ import InsidePage from '@common/components/aoplatform/InsidePage' import { $t } from '@common/locales' import { Tabs } from 'antd' +import { useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' import AIFlowChart from './AIFlowChart' import AIUnConfigure from './AIUnconfigure' import { AiSettingProvider } from './contexts/AiSettingContext' @@ -8,6 +10,14 @@ import { AiSettingProvider } from './contexts/AiSettingContext' const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const const AiSettingContent = () => { + const [searchParams, setSearchParams] = useSearchParams() + const [activeKey, setActiveKey] = useState(searchParams.get('status') === 'unconfigure' ? 'config' : 'flow') + + useEffect(() => { + const newActiveKey = searchParams.get('status') === 'unconfigure' ? 'config' : 'flow' + setActiveKey(newActiveKey) + }, [searchParams]) + return ( { >
{ + setActiveKey(key) + setSearchParams({ status: key === 'config' ? 'unconfigure' : 'configure' }) + }} className="sticky top-0 flex-shrink-0" items={[ { diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index fca85750..9527d009 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -5,10 +5,10 @@ import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' -import { AiProviderConfig, AiProviderLlmsItems, ModelDetailData } from './types' +import { AiProviderLlmsItems, ModelDetailData } from './types' export type AiSettingModalContentProps = { - entity: AiProviderConfig & { defaultLlm: string } + entity: ModelDetailData & { defaultLlm: string } readOnly: boolean } @@ -173,7 +173,7 @@ const AiSettingModalContent = forwardRef - {entity.id && ( + {entity.configured && (
diff --git a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx index bbabc26d..99efbe2b 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx @@ -44,7 +44,7 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) = >
{name} - +
{/* Action buttons */} diff --git a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx index 502c3bf3..f8e078bd 100644 --- a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx +++ b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx @@ -7,7 +7,7 @@ import { checkAccess } from '@common/utils/permission' import { App } from 'antd' import { createContext, useContext, useRef } from 'react' import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSettingModal' -import { AiProviderConfig, AiSettingListItem } from '../types' +import { AiSettingListItem, ModelDetailData } from '../types' interface AiSettingContextType { openConfigModal: (entity: AiSettingListItem) => Promise @@ -23,7 +23,7 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi const openConfigModal = async (entity: AiSettingListItem) => { message.loading($t(RESPONSE_TIPS.loading)) - const { code, data, msg } = await fetchData>('ai/provider/config', { + const { code, data, msg } = await fetchData>('ai/provider/config', { method: 'GET', eoParams: { provider: entity!.id }, eoTransformKeys: ['get_apikey_url'] diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index 814346c9..6ba2ea7a 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -21,6 +21,9 @@ export interface ModelDetailData extends ModelListData{ enable:boolean config: string, priority?: number + getApikeyUrl: string + status: ModelStatus + configured: boolean } @@ -51,12 +54,4 @@ export type AiProviderDefaultConfig = { scopes: string[] } -export interface AiProviderConfig { - id: string - name: string - config: string - getApikeyUrl: string - priority: number - enable: boolean - status: ModelStatus -} + From 26dbce9dbf426b088f8dc34cbfc6a50083b7719d Mon Sep 17 00:00:00 2001 From: scarqin Date: Tue, 31 Dec 2024 15:17:36 +0800 Subject: [PATCH 12/17] feat: ai column add disable --- .../core/src/const/ai-service/const.tsx | 239 +++--- .../api/AiServiceInsideRouterCreate.tsx | 724 ++++++++++-------- .../api/AiServiceInsideRouterList.tsx | 380 +++++---- 3 files changed, 730 insertions(+), 613 deletions(-) diff --git a/frontend/packages/core/src/const/ai-service/const.tsx b/frontend/packages/core/src/const/ai-service/const.tsx index bd101041..ba4a08f8 100644 --- a/frontend/packages/core/src/const/ai-service/const.tsx +++ b/frontend/packages/core/src/const/ai-service/const.tsx @@ -1,127 +1,134 @@ -import { AiServiceRouterTableListItem, VariableItems } from "./type"; -import { TabsProps } from "antd"; -import { frontendTimeSorter } from "@common/utils/dataTransfer"; -import { COLUMNS_TITLE, PLACEHOLDER } from "@common/const/const"; - -import { PageProColumns } from "@common/components/aoplatform/PageList"; - +import { COLUMNS_TITLE, PLACEHOLDER } from '@common/const/const' +import { frontendTimeSorter } from '@common/utils/dataTransfer' +import { TabsProps } from 'antd' +import { AiServiceRouterTableListItem, VariableItems } from './type' +import { PageProColumns } from '@common/components/aoplatform/PageList' export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns[] = [ - { - title:('URL'), - dataIndex: 'requestPath', - ellipsis:true - }, - { - title:('名称'), - dataIndex: 'name', - ellipsis:true, - }, - { - title:('模型'), - dataIndex: ['model','logo'], - ellipsis:true, - render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) =>
{entity.model.id}
- }, - { - title:('描述'), - dataIndex: 'description', - ellipsis:true - }, - { - title:('创建者'), - dataIndex: ['creator','name'], - ellipsis: true, - filters: true, - onFilter: true, - valueType: 'select', - filterSearch: true, - }, - { - title:('更新时间'), - dataIndex: 'updateTime', - ellipsis:true, - hideInSearch: true, - width:182, - sorter: (a,b)=>frontendTimeSorter(a,b,'updateTime') - }, -]; + { + title: 'URL', + dataIndex: 'requestPath', + ellipsis: true + }, + { + title: '名称', + dataIndex: 'name', + ellipsis: true + }, + { + title: '模型', + dataIndex: ['model', 'logo'], + ellipsis: true, + render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => ( +
+ {entity.model.id} +
+ ) + }, + { + title: '是否放行', + dataIndex: 'disable', + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select' + }, + { + title: '描述', + dataIndex: 'description', + ellipsis: true + }, + { + title: '创建者', + dataIndex: ['creator', 'name'], + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true + }, + { + title: '更新时间', + dataIndex: 'updateTime', + ellipsis: true, + hideInSearch: true, + width: 182, + sorter: (a, b) => frontendTimeSorter(a, b, 'updateTime') + } +] - -export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns[] = [ - { - title:('Key'), - dataIndex: 'key', - key:'key', - width: '30%', - formItemProps: { - className:'p-0 bg-transparent border-none', - rules: [ - { - required: true, - whitespace: true - }, - { - pattern:/^[a-zA-Z][a-zA-Z0-9-_]*$/, - message: PLACEHOLDER.onlyAlphabet - } - ], - }, - ellipsis:true, - fieldProps:{ - allowClear:false - } - }, - { - title:('描述'), - dataIndex: 'description', - key:'description', - formItemProps: { - className:'p-0 bg-transparent border-none' +export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns[] = [ + { + title: 'Key', + dataIndex: 'key', + key: 'key', + width: '30%', + formItemProps: { + className: 'p-0 bg-transparent border-none', + rules: [ + { + required: true, + whitespace: true }, - fieldProps:{ - allowClear:false + { + pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/, + message: PLACEHOLDER.onlyAlphabet } + ] }, - { - title:('必填'), - dataIndex: 'require', - key:'require', - valueType:'switch', - width:64, - formItemProps: { - className:'p-0 bg-transparent border-none'} - }, - { - title: COLUMNS_TITLE.operate, - valueType: 'option', - width:34, - render: ()=>null - }, - ]; - - -export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [ - { - key: '0', - label:('待审核'), - }, - { - key: '1', - label: ('已审核'), + ellipsis: true, + fieldProps: { + allowClear: false } -]; - + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + formItemProps: { + className: 'p-0 bg-transparent border-none' + }, + fieldProps: { + allowClear: false + } + }, + { + title: '必填', + dataIndex: 'require', + key: 'require', + valueType: 'switch', + width: 64, + formItemProps: { + className: 'p-0 bg-transparent border-none' + } + }, + { + title: COLUMNS_TITLE.operate, + valueType: 'option', + width: 34, + render: () => null + } +] +export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [ + { + key: '0', + label: '待审核' + }, + { + key: '1', + label: '已审核' + } +] export const AiService_PUBLISH_TAB_ITEMS: TabsProps['items'] = [ - { - key: '0', - label: ('发布版本'), - }, - { - key: '1', - label: ('发布申请记录'), - } -]; + { + key: '0', + label: '发布版本' + }, + { + key: '1', + label: '发布申请记录' + } +] diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx index e983f90d..caf9078b 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx @@ -1,354 +1,422 @@ -import {App, Button, Form, Input, InputNumber, Row, Select, Space, Spin, Tag} from "antd"; -import { MutableRefObject, useEffect, useMemo, useRef, useState} from "react"; -import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; -import {useFetch} from "@common/hooks/http.ts"; -import { $t } from "@common/locales/index.ts"; -import { LoadingOutlined } from "@ant-design/icons"; -import InsidePage from "@common/components/aoplatform/InsidePage.tsx"; -import { Icon } from "@iconify/react/dist/iconify.js"; -import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; -import { useNavigate, useParams } from "react-router-dom"; -import { useAiServiceContext } from "@core/contexts/AiServiceContext.tsx"; -import EditableTableNotAutoGen from "@common/components/aoplatform/EditableTableNotAutoGen.tsx"; -import { AI_SERVICE_VARIABLES_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx"; -import { VariableItems } from "@core/const/ai-service/type.ts"; -import PromptEditorResizable from '@common/components/aoplatform/prompt-editor/PromptEditorResizable.tsx'; -import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter"; -import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from "./AiServiceInsideRouterModelConfig"; -import { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList"; -import { EditableFormInstance } from "@ant-design/pro-components"; -import { validateUrlSlash } from "@common/utils/validate"; -import { API_PATH_MATCH_RULES } from "@core/const/system/const"; -import { useGlobalContext } from "@common/contexts/GlobalStateContext"; +import { LoadingOutlined } from '@ant-design/icons' +import { EditableFormInstance } from '@ant-design/pro-components' +import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter' +import EditableTableNotAutoGen from '@common/components/aoplatform/EditableTableNotAutoGen.tsx' +import InsidePage from '@common/components/aoplatform/InsidePage.tsx' +import PromptEditorResizable from '@common/components/aoplatform/prompt-editor/PromptEditorResizable.tsx' +import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { useFetch } from '@common/hooks/http.ts' +import { $t } from '@common/locales/index.ts' +import { validateUrlSlash } from '@common/utils/validate' +import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx' +import { AI_SERVICE_VARIABLES_TABLE_COLUMNS } from '@core/const/ai-service/const.tsx' +import { VariableItems } from '@core/const/ai-service/type.ts' +import { API_PATH_MATCH_RULES } from '@core/const/system/const' +import { useAiServiceContext } from '@core/contexts/AiServiceContext.tsx' +import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList' +import { Icon } from '@iconify/react/dist/iconify.js' +import { App, Button, Form, Input, InputNumber, Row, Select, Space, Spin, Switch, Tag } from 'antd' +import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from './AiServiceInsideRouterModelConfig' type AiServiceRouterField = { - name:string - path:string - prompt:string - variables:Array<{key:string, description:string, require:true}> - description:string - timeout:number - retry:number + name: string + path: string + prompt: string + variables: Array<{ key: string; description: string; require: true }> + description: string + timeout: number + retry: number + disable: boolean } type AiServiceRouterConfig = { - name:string - path:string - aiPrompt:{ - prompt:string - variables:Array<{key:string, description:string, require:true}> - } - aiModel:{ - id:string - config:string - } - description:string - timeout:number - retry:number + name: string + path: string + aiPrompt: { + prompt: string + variables: Array<{ key: string; description: string; require: true }> + } + aiModel: { + id: string + config: string + } + description: string + timeout: number + retry: number } const AiServiceInsideRouterCreate = () => { - const navigator = useNavigate() - const { message } = App.useApp() - const {serviceId, teamId,routeId} = useParams() - const [form] = Form.useForm(); - const {fetchData} = useFetch() - const [loading, setLoading] = useState(false) - const {apiPrefix,prefixForce ,aiServiceInfo} = useAiServiceContext() - const [variablesTable,setVariablesTable] = useState([]) - const [drawerType,setDrawerType]= useState<'edit'|undefined>() - const [open, setOpen] = useState(false); - const drawerAddFormRef = useRef(null) - const [defaultLlm, setDefaultLlm] = useState() - const [llmList, setLlmList] = useState([]) - const [variablesTableRef, setVariablesTableRef] = useState | undefined>>() - const {state} = useGlobalContext() - - const onFinish = ()=>{ - return variablesTableRef?.current?.validateFields().then(()=>{ - return form.validateFields().then((formValue)=>{ - const {name, path, description, variables, prompt, timeout, retry,pathMatch} = formValue - const body = { - name, - path: `${prefixForce ? apiPrefix + '/' : ''}${path.trim()}${pathMatch === 'prefix' ? '/*' : ''}`, - description,timeout, retry,aiPrompt:{variables:variables, prompt:prompt},aiModel:{id:defaultLlm?.id, provider:defaultLlm?.provider, config:defaultLlm?.config}} - return fetchData>('service/ai-router',{method: routeId ? 'PUT' : 'POST',eoBody:(body), eoParams: {service:serviceId,team:teamId, ...(routeId ? {router:routeId}: {})},eoTransformKeys:['aiPrompt','aiModel']}).then(response=>{ - const {code,msg} = response - if(code === STATUS_CODE.SUCCESS){ - message.success(msg || $t(RESPONSE_TIPS.success)) - navigator(`/service/${teamId}/aiInside/${serviceId}/route`) - return Promise.resolve(true) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - return Promise.reject(msg || $t(RESPONSE_TIPS.error)) - } - }).catch(errInfo=>Promise.reject(errInfo)) - }) - }) - .catch(errInfo=>Promise.reject(errInfo)) - } + const navigator = useNavigate() + const { message } = App.useApp() + const { serviceId, teamId, routeId } = useParams() + const [form] = Form.useForm() + const { fetchData } = useFetch() + const [loading, setLoading] = useState(false) + const { apiPrefix, prefixForce, aiServiceInfo } = useAiServiceContext() + const [variablesTable, setVariablesTable] = useState([]) + const [drawerType, setDrawerType] = useState<'edit' | undefined>() + const [open, setOpen] = useState(false) + const drawerAddFormRef = useRef(null) + const [defaultLlm, setDefaultLlm] = useState() + const [llmList, setLlmList] = useState([]) + const [variablesTableRef, setVariablesTableRef] = useState | undefined>>() + const { state } = useGlobalContext() - const openDrawer = (type:'edit')=>{ - setDrawerType(type) - } - - useEffect(()=>{drawerType !== undefined ? setOpen(true):setOpen(false)},[drawerType]) - - const getRouterConfig = ()=>{ - setLoading(true) - fetchData>('service/ai-router',{method:'GET',eoParams:{service:serviceId,team:teamId, router:routeId}, eoTransformKeys:['ai_model', 'ai_prompt']}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - const {path, aiPrompt,aiModel} = data.api - let newPath = path - let pathMatch = 'full' - if(prefixForce && path?.startsWith(apiPrefix + '/')){ - newPath = path.slice((apiPrefix?.length || 0) + 1) - } - if(newPath.endsWith('/*')){ - newPath = newPath.slice(0,-2) - pathMatch = 'prefix' - } - form.setFieldsValue({ - ...data.api, - ...aiPrompt, - path:newPath, - pathMatch}) - setVariablesTable(aiPrompt.variables as VariableItems[]) - setDefaultLlm(prev => ({...prev, provider: aiModel?.provider, id:aiModel?.id, config:aiModel.config}) as (AiProviderDefaultConfig & { config: string; })) - getDefaultModelConfig(aiModel?.provider) - }else{ + const onFinish = () => { + return variablesTableRef?.current + ?.validateFields() + .then(() => { + return form.validateFields().then((formValue) => { + const { name, path, description, variables, prompt, timeout, retry, pathMatch, disable } = formValue + const body = { + name, + path: `${prefixForce ? apiPrefix + '/' : ''}${path.trim()}${pathMatch === 'prefix' ? '/*' : ''}`, + description, + timeout, + retry, + aiPrompt: { variables: variables, prompt: prompt }, + aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config }, + disable + } + return fetchData>('service/ai-router', { + method: routeId ? 'PUT' : 'POST', + eoBody: body, + eoParams: { service: serviceId, team: teamId, ...(routeId ? { router: routeId } : {}) }, + eoTransformKeys: ['aiPrompt', 'aiModel'] + }) + .then((response) => { + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || $t(RESPONSE_TIPS.success)) + navigator(`/service/${teamId}/aiInside/${serviceId}/route`) + return Promise.resolve(true) + } else { message.error(msg || $t(RESPONSE_TIPS.error)) - } - }).catch((errorInfo)=> console.error(errorInfo)) - .finally(()=>setLoading(false)) - } - - const getDefaultModelConfig = (provider?:string)=>{ - fetchData>('ai/provider/llms',{method:'GET',eoParams:{provider:provider ?? aiServiceInfo?.provider?.id}, eoTransformKeys:['default_llm']}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - setLlmList(data.llms) - setDefaultLlm(prev => { - const llmSetting = data.llms?.find((x:AiProviderLlmsItems)=>x.id ===( prev?.id ?? data.provider.defaultLlm)) - return {...prev, - defaultLlm:data.provider.defaultLlm, - provider:data.provider.id, - name:data.provider.name, - config:llmSetting?.config || '', - ...(llmSetting ?? {}) - } as (AiProviderDefaultConfig & { config: string; }) - }) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }).catch((errorInfo)=> console.error(errorInfo)) - } - - - useEffect(()=>{ - !routeId && aiServiceInfo?.provider && getDefaultModelConfig() - },[ - aiServiceInfo - ]) - - - useEffect(() => { - if(routeId){ - getRouterConfig() - }else{ - form.setFieldsValue({ - prefix:apiPrefix, - variables:[{key:'Query',value:'',require:true}], - prompt:'{{Query}}', - retry:0, - timeout:300000, - pathMatch:'prefix' + return Promise.reject(msg || $t(RESPONSE_TIPS.error)) + } }) - } - return (form.setFieldsValue({})) - }, []); - - const addVariable = ()=>{ - form.setFieldsValue({ - variables:[...form.getFieldValue('variables'),{key:'',value:'',require:true}] + .catch((errInfo) => Promise.reject(errInfo)) }) - } + }) + .catch((errInfo) => Promise.reject(errInfo)) + } - const handleVariablesChange = (newKeys:string[])=>{ - const variables = form.getFieldValue('variables') || [] - const variablesKeys = variables?.map(({key}:{key:string})=>(key)) - for(const key of newKeys){ - if(!variablesKeys ||variablesKeys.indexOf(key) === -1){ - variables.push({key, value:'',require:true}) - } + const openDrawer = (type: 'edit') => { + setDrawerType(type) + } + + useEffect(() => { + drawerType !== undefined ? setOpen(true) : setOpen(false) + }, [drawerType]) + + const getRouterConfig = () => { + setLoading(true) + fetchData>('service/ai-router', { + method: 'GET', + eoParams: { service: serviceId, team: teamId, router: routeId }, + eoTransformKeys: ['ai_model', 'ai_prompt'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + const { path, aiPrompt, aiModel } = data.api + let newPath = path + let pathMatch = 'full' + if (prefixForce && path?.startsWith(apiPrefix + '/')) { + newPath = path.slice((apiPrefix?.length || 0) + 1) + } + if (newPath.endsWith('/*')) { + newPath = newPath.slice(0, -2) + pathMatch = 'prefix' + } + form.setFieldsValue({ + ...data.api, + ...aiPrompt, + path: newPath, + pathMatch + }) + setVariablesTable(aiPrompt.variables as VariableItems[]) + setDefaultLlm( + (prev) => + ({ + ...prev, + provider: aiModel?.provider, + id: aiModel?.id, + config: aiModel.config + }) as AiProviderDefaultConfig & { config: string } + ) + getDefaultModelConfig(aiModel?.provider) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) } - form.setFieldsValue({ - variables:[...variables] - }) - setVariablesTable(variables as VariableItems[]) - } + }) + .catch((errorInfo) => console.error(errorInfo)) + .finally(() => setLoading(false)) + } - - const handleValuesChange = (changedValues:Record) => { - if(changedValues.variables){ - setVariablesTable(changedValues.variables as VariableItems[]) + const getDefaultModelConfig = (provider?: string) => { + fetchData>('ai/provider/llms', { + method: 'GET', + eoParams: { provider: provider ?? aiServiceInfo?.provider?.id }, + eoTransformKeys: ['default_llm'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setLlmList(data.llms) + setDefaultLlm((prev) => { + const llmSetting = data.llms?.find( + (x: AiProviderLlmsItems) => x.id === (prev?.id ?? data.provider.defaultLlm) + ) + return { + ...prev, + defaultLlm: data.provider.defaultLlm, + provider: data.provider.id, + name: data.provider.name, + config: llmSetting?.config || '', + ...(llmSetting ?? {}) + } as AiProviderDefaultConfig & { config: string } + }) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) } - }; + }) + .catch((errorInfo) => console.error(errorInfo)) + } - - const handlerSubmit:() => Promise|undefined= ()=>{ - return drawerAddFormRef.current?.save()?.then((res:{id:string, config:string})=>{ - setDefaultLlm(prev => ({...prev, provider:res.provider, id:res.id, config:res.config, logo:llmList?.find((x:AiProviderLlmsItems)=>x.id === res.id)?.logo}) as (AiProviderDefaultConfig & { config: string; })) - return true}) + useEffect(() => { + !routeId && aiServiceInfo?.provider && getDefaultModelConfig() + }, [aiServiceInfo]) + + useEffect(() => { + if (routeId) { + getRouterConfig() + } else { + form.setFieldsValue({ + prefix: apiPrefix, + variables: [{ key: 'Query', value: '', require: true }], + prompt: '{{Query}}', + retry: 0, + timeout: 300000, + pathMatch: 'prefix' + }) } + return form.setFieldsValue({}) + }, []) - const onClose = () => { - setDrawerType(undefined); - }; + const addVariable = () => { + form.setFieldsValue({ + variables: [...form.getFieldValue('variables'), { key: '', value: '', require: true }] + }) + } - const apiPathMatchRulesOptions = useMemo(()=>API_PATH_MATCH_RULES.map( - x=>({label:$t(x.label), value:x.value})),[state.language]) + const handleVariablesChange = (newKeys: string[]) => { + const variables = form.getFieldValue('variables') || [] + const variablesKeys = variables?.map(({ key }: { key: string }) => key) + for (const key of newKeys) { + if (!variablesKeys || variablesKeys.indexOf(key) === -1) { + variables.push({ key, value: '', require: true }) + } + } + form.setFieldsValue({ + variables: [...variables] + }) + setVariablesTable(variables as VariableItems[]) + } - return ( - - - - - + const handleValuesChange = (changedValues: Record) => { + if (changedValues.variables) { + setVariablesTable(changedValues.variables as VariableItems[]) + } + } + + const handlerSubmit: () => Promise | undefined = () => { + return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string }) => { + setDefaultLlm( + (prev) => + ({ + ...prev, + provider: res.provider, + id: res.id, + config: res.config, + logo: llmList?.find((x: AiProviderLlmsItems) => x.id === res.id)?.logo + }) as AiProviderDefaultConfig & { config: string } + ) + return true + }) + } + + const onClose = () => { + setDrawerType(undefined) + } + + const apiPathMatchRulesOptions = useMemo( + () => API_PATH_MATCH_RULES.map((x) => ({ label: $t(x.label), value: x.value })), + [state.language] + ) + + return ( + + + + +
+ } + > + } + spinning={loading} + wrapperClassName=" pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X" + > +
+
+ + + className="flex-1" + label={$t('路由名称')} + name="name" + rules={[{ required: true, whitespace: true }]} + > + + + + + + + { + if ((e.target.value as string).endsWith('/*')) { + form.setFieldValue('path', e.target.value.slice(0, -2)) + form.setFieldValue('pathMatch', 'prefix') + } + }} + /> + + + + + + label={$t('提示词')} name="prompt"> + + + + + label={ +
+ {$t('变量')} + + + New +
- }> - } spinning={loading} wrapperClassName=' pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X'> - -
- - - className="flex-1" - label={$t("路由名称")} - name="name" - rules={[{ required: true,whitespace:true }]} - > - - - - - - - - { - if((e.target.value as string).endsWith('/*')){ - form.setFieldValue('path',e.target.value.slice(0,-2)) - form.setFieldValue('pathMatch','prefix') - } - }}/> - - - + } + name="variables" + className="[&>.ant-row>.ant-col>label]:w-full" + > + + getFromRef={setVariablesTableRef} + configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS} + /> + - + label={$t('描述')} name="description"> + + - - label={$t("提示词")} - name="prompt" - > - - - - - label={
{$t("变量")}New
} - name="variables" - className="[&>.ant-row>.ant-col>label]:w-full" - > - - getFromRef={setVariablesTableRef} - configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS} - /> - - - - label={$t("描述")} - name="description" - > - - - - - - className="flex-1" - label={$t("请求超时时间")} - name={'timeout'} - rules={[{required: true}]} - > - - - - className="flex-1" - label={$t("重试次数")} - name={'retry'} - rules={[{required: true}]} - > - - - - - -
- -
- handlerSubmit()} - > - - - - ) + + + className="flex-1" + label={$t('请求超时时间')} + name={'timeout'} + rules={[{ required: true }]} + > + + + + className="flex-1" + label={$t('重试次数')} + name={'retry'} + rules={[{ required: true }]} + > + + + + + label={$t('拦截接口')} + name="disable" + extra={$t('开启拦截后,网关会拦截所有该路径的请求。')} + > + + +
+ +
+ handlerSubmit()}> + + +
+ ) } export default AiServiceInsideRouterCreate - - - \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx index 88a09f72..150d22cc 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx @@ -1,182 +1,224 @@ -import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx" -import {ActionType} from "@ant-design/pro-components"; -import {FC, useEffect, useMemo, useRef, useState} from "react"; -import {Link, useNavigate, useParams} from "react-router-dom"; -import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; -import {App, Divider} from "antd"; -import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; -import { SimpleMemberItem} from '@common/const/type.ts' -import {useFetch} from "@common/hooks/http.ts"; -import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; -import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; -import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; -import { checkAccess } from "@common/utils/permission.ts"; -import { $t } from "@common/locales/index.ts"; -import { AiServiceRouterTableListItem } from "@core/const/ai-service/type.ts"; -import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx"; +import { ActionType } from '@ant-design/pro-components' +import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx' +import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx' +import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx' +import { SimpleMemberItem } from '@common/const/type.ts' +import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx' +import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx' +import { useFetch } from '@common/hooks/http.ts' +import { $t } from '@common/locales/index.ts' +import { checkAccess } from '@common/utils/permission.ts' +import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx' +import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from '@core/const/ai-service/const.tsx' +import { AiServiceRouterTableListItem } from '@core/const/ai-service/type.ts' +import { App, Divider, Typography } from 'antd' +import { FC, useEffect, useMemo, useRef, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' -const AiServiceInsideRouterList:FC = ()=>{ - const [searchWord, setSearchWord] = useState('') - const { setBreadcrumb } = useBreadcrumb() - const { modal,message } = App.useApp() - const [tableListDataSource, setTableListDataSource] = useState([]); - const [tableHttpReload, setTableHttpReload] = useState(true); - const {fetchData} = useFetch() - const pageListRef = useRef(null); - const [memberValueEnum, setMemberValueEnum] = useState([]) - const {accessData,state} = useGlobalContext() - const {serviceId, teamId} = useParams() - const navigator = useNavigate() +const AiServiceInsideRouterList: FC = () => { + const [searchWord, setSearchWord] = useState('') + const { setBreadcrumb } = useBreadcrumb() + const { modal, message } = App.useApp() + const [tableListDataSource, setTableListDataSource] = useState([]) + const [tableHttpReload, setTableHttpReload] = useState(true) + const { fetchData } = useFetch() + const pageListRef = useRef(null) + const [memberValueEnum, setMemberValueEnum] = useState([]) + const { accessData, state } = useGlobalContext() + const { serviceId, teamId } = useParams() + const navigator = useNavigate() - const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[], success: boolean }>=> { - if(!tableHttpReload){ - setTableHttpReload(true) - return Promise.resolve({ - data: tableListDataSource, - success: true, - }); - } - - return fetchData>('service/ai-routers',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','disable']}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - setTableListDataSource(data.apis) - setTableHttpReload(false) - return {data:data.apis, success: true} - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - return {data:[], success:false} - } - }).catch(() => { - return {data:[], success:false} - }) + const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[]; success: boolean }> => { + if (!tableHttpReload) { + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true + }) } - const deleteRoute = (entity:AiServiceRouterTableListItem)=>{ - return new Promise((resolve, reject)=>{ - fetchData>('service/ai-router',{method:'DELETE',eoParams:{service:serviceId,team:teamId, router:entity!.id}}).then(response=>{ - const {code,msg} = response - if(code === STATUS_CODE.SUCCESS){ - message.success(msg || $t(RESPONSE_TIPS.success)) - resolve(true) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - reject(msg || $t(RESPONSE_TIPS.error)) - } - }).catch((errorInfo)=> reject(errorInfo)) - }) - } - - const openModal = async (type: 'delete',entity:AiServiceRouterTableListItem) =>{ - let title:string = '' - let content:string|React.ReactNode = '' - switch (type){ - case 'delete': - title=$t('删除') - content=$t(DELETE_TIPS.default) - break; + return fetchData>('service/ai-routers', { + method: 'GET', + eoParams: { service: serviceId, team: teamId, keyword: searchWord }, + eoTransformKeys: ['request_path', 'create_time', 'update_time', 'disable'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setTableListDataSource(data.apis) + setTableHttpReload(false) + return { data: data.apis, success: true } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return { data: [], success: false } } + }) + .catch(() => { + return { data: [], success: false } + }) + } - modal.confirm({ - title, - content, - onOk:()=> { - switch (type){ - case 'delete': - return deleteRoute(entity).then((res)=>{if(res === true) manualReloadTable()}) - } - }, - width:600, - okText:$t('确认'), - okButtonProps:{ - disabled : !checkAccess( `team.service.router.${type}`, accessData ) - }, - cancelText:$t('取消'), - closable:true, - icon:<>, - }) - } - - const operation:PageProColumns[] =[ - { - title: COLUMNS_TITLE.operate, - key: 'option', - btnNums:2, - fixed:'right', - valueType: 'option', - render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [ - {navigator(`/service/${teamId}/aiInside/${serviceId}/route/${entity.id}`)}} btnTitle="编辑"/>, - , - {openModal('delete',entity)}} btnTitle="删除"/>, - ], - } - ] - - const manualReloadTable = () => { - setTableHttpReload(true); // 表格数据需要从后端接口获取 - pageListRef.current?.reload() - }; - - const getMemberList = async ()=>{ - setMemberValueEnum([]) - const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) - if(code === STATUS_CODE.SUCCESS){ - setMemberValueEnum(data.members) - }else{ + const deleteRoute = (entity: AiServiceRouterTableListItem) => { + return new Promise((resolve, reject) => { + fetchData>('service/ai-router', { + method: 'DELETE', + eoParams: { service: serviceId, team: teamId, router: entity!.id } + }) + .then((response) => { + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || $t(RESPONSE_TIPS.success)) + resolve(true) + } else { message.error(msg || $t(RESPONSE_TIPS.error)) - } + reject(msg || $t(RESPONSE_TIPS.error)) + } + }) + .catch((errorInfo) => reject(errorInfo)) + }) + } + + const openModal = async (type: 'delete', entity: AiServiceRouterTableListItem) => { + let title: string = '' + let content: string | React.ReactNode = '' + switch (type) { + case 'delete': + title = $t('删除') + content = $t(DELETE_TIPS.default) + break } - useEffect(() => { - setBreadcrumb([ - { - title:{$t('服务')} - }, - { - title:$t('路由') - } - ]) - getMemberList() - manualReloadTable() - }, [serviceId]); - - const columns = useMemo(()=>{ - return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map(x=>{ - if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){ - const tmpValueEnum:{[k:string]:{text:string}} = {} - memberValueEnum?.forEach((x:SimpleMemberItem)=>{ - tmpValueEnum[x.name] = {text:x.name} - }) - x.valueEnum = tmpValueEnum - } - - return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}}) - },[memberValueEnum,state.language]) + modal.confirm({ + title, + content, + onOk: () => { + switch (type) { + case 'delete': + return deleteRoute(entity).then((res) => { + if (res === true) manualReloadTable() + }) + } + }, + width: 600, + okText: $t('确认'), + okButtonProps: { + disabled: !checkAccess(`team.service.router.${type}`, accessData) + }, + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + const operation: PageProColumns[] = [ + { + title: COLUMNS_TITLE.operate, + key: 'option', + btnNums: 2, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [ + { + navigator(`/service/${teamId}/aiInside/${serviceId}/route/${entity.id}`) + }} + btnTitle="编辑" + />, + , + { + openModal('delete', entity) + }} + btnTitle="删除" + /> + ] + } + ] - return ( - <> - getRoutesList()} - dataSource={tableListDataSource} - addNewBtnTitle={$t('添加路由')} - searchPlaceholder={$t('输入 URL 查找路由')} - onAddNewBtnClick={()=>{navigator(`/service/${teamId}/aiInside/${serviceId}/route/create`)}} - addNewBtnAccess="team.service.router.add" - tableClickAccess="team.service.router.view" - manualReloadTable={manualReloadTable} - onSearchWordChange={(e)=>{setSearchWord(e.target.value)}} - onChange={() => { - setTableHttpReload(false) - }} - onRowClick={(row:AiServiceRouterTableListItem)=>navigator(`/service/${teamId}/aiInside/${serviceId}/route/${row.id}`)} - tableClass="mr-PAGE_INSIDE_X " - /> - - ) + const manualReloadTable = () => { + setTableHttpReload(true) // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + } + const getMemberList = async () => { + setMemberValueEnum([]) + const { code, data, msg } = await fetchData>('simple/member', { + method: 'GET' + }) + if (code === STATUS_CODE.SUCCESS) { + setMemberValueEnum(data.members) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + } + + useEffect(() => { + setBreadcrumb([ + { + title: {$t('服务')} + }, + { + title: $t('路由') + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]) + + const columns = useMemo(() => { + return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map((x) => { + if (x.filters && (x.dataIndex as string[])?.indexOf('creator') !== -1) { + const tmpValueEnum: { [k: string]: { text: string } } = {} + memberValueEnum?.forEach((x: SimpleMemberItem) => { + tmpValueEnum[x.name] = { text: x.name } + }) + x.valueEnum = tmpValueEnum + } + if (x.filters && (x.dataIndex as string[])?.indexOf('disable') !== -1) { + x.valueEnum = { + true: { text: {$t('拦截')} }, + false: { text: {$t('放行')} } + } + } + + return { ...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title } + }) + }, [memberValueEnum, state.language]) + + return ( + <> + getRoutesList()} + dataSource={tableListDataSource} + addNewBtnTitle={$t('添加路由')} + searchPlaceholder={$t('输入 URL 查找路由')} + onAddNewBtnClick={() => { + navigator(`/service/${teamId}/aiInside/${serviceId}/route/create`) + }} + addNewBtnAccess="team.service.router.add" + tableClickAccess="team.service.router.view" + manualReloadTable={manualReloadTable} + onSearchWordChange={(e) => { + setSearchWord(e.target.value) + }} + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row: AiServiceRouterTableListItem) => + navigator(`/service/${teamId}/aiInside/${serviceId}/route/${row.id}`) + } + tableClass="mr-PAGE_INSIDE_X " + /> + + ) } -export default AiServiceInsideRouterList \ No newline at end of file +export default AiServiceInsideRouterList From 82fa1b5b1c5d51068a6e114b2efcbc2e4475492f Mon Sep 17 00:00:00 2001 From: scarqin Date: Tue, 31 Dec 2024 16:17:53 +0800 Subject: [PATCH 13/17] chore: change disable to disabled --- frontend/packages/core/src/const/ai-service/const.tsx | 2 +- frontend/packages/core/src/const/system/const.tsx | 2 +- .../pages/aiService/api/AiServiceInsideRouterCreate.tsx | 8 ++++---- .../src/pages/aiService/api/AiServiceInsideRouterList.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/packages/core/src/const/ai-service/const.tsx b/frontend/packages/core/src/const/ai-service/const.tsx index ba4a08f8..91112b48 100644 --- a/frontend/packages/core/src/const/ai-service/const.tsx +++ b/frontend/packages/core/src/const/ai-service/const.tsx @@ -28,7 +28,7 @@ export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns[] }, { title: '是否放行', - dataIndex: 'disable', + dataIndex: 'disabled', ellipsis: true, filters: true, onFilter: true, diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx index caf9078b..3ed0819c 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx @@ -29,7 +29,7 @@ type AiServiceRouterField = { description: string timeout: number retry: number - disable: boolean + disabled: boolean } type AiServiceRouterConfig = { @@ -70,7 +70,7 @@ const AiServiceInsideRouterCreate = () => { ?.validateFields() .then(() => { return form.validateFields().then((formValue) => { - const { name, path, description, variables, prompt, timeout, retry, pathMatch, disable } = formValue + const { name, path, description, variables, prompt, timeout, retry, pathMatch, disabled } = formValue const body = { name, path: `${prefixForce ? apiPrefix + '/' : ''}${path.trim()}${pathMatch === 'prefix' ? '/*' : ''}`, @@ -79,7 +79,7 @@ const AiServiceInsideRouterCreate = () => { retry, aiPrompt: { variables: variables, prompt: prompt }, aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config }, - disable + disabled } return fetchData>('service/ai-router', { method: routeId ? 'PUT' : 'POST', @@ -405,7 +405,7 @@ const AiServiceInsideRouterCreate = () => { label={$t('拦截接口')} - name="disable" + name="disabled" extra={$t('开启拦截后,网关会拦截所有该路径的请求。')} > diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx index 150d22cc..007f64f6 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx @@ -180,7 +180,7 @@ const AiServiceInsideRouterList: FC = () => { }) x.valueEnum = tmpValueEnum } - if (x.filters && (x.dataIndex as string[])?.indexOf('disable') !== -1) { + if (x.filters && (x.dataIndex as string[])?.indexOf('disabled') !== -1) { x.valueEnum = { true: { text: {$t('拦截')} }, false: { text: {$t('放行')} } From 1c536df3c8a1ec47dec9f20eb385934c932f8fa5 Mon Sep 17 00:00:00 2001 From: scarqin Date: Tue, 31 Dec 2024 17:21:56 +0800 Subject: [PATCH 14/17] feat: apilist --- .../packages/core/src/pages/aiApis/index.tsx | 240 ++++++------------ .../core/src/pages/keySettings/index.tsx | 2 + 2 files changed, 85 insertions(+), 157 deletions(-) diff --git a/frontend/packages/core/src/pages/aiApis/index.tsx b/frontend/packages/core/src/pages/aiApis/index.tsx index bf485cca..50291fc2 100644 --- a/frontend/packages/core/src/pages/aiApis/index.tsx +++ b/frontend/packages/core/src/pages/aiApis/index.tsx @@ -2,126 +2,57 @@ import { ActionType } from '@ant-design/pro-components' import InsidePage from '@common/components/aoplatform/InsidePage' import PageList, { PageProColumns } from '@common/components/aoplatform/PageList' import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission' +import TimeRangeSelector, { TimeRangeButton } from '@common/components/aoplatform/TimeRangeSelector' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' -import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' import AIProviderSelect, { AIProvider } from '@core/components/AIProviderSelect' -import { App, Divider, Space, Typography } from 'antd' +import { App, Button, Typography } from 'antd' import dayjs from 'dayjs' import React, { useEffect, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { APIKey } from './types' -const KeySettings: React.FC = () => { +const ApiSettings: React.FC = () => { const pageListRef = useRef(null) const { modal, message } = App.useApp() const [searchParams] = useSearchParams() const [selectedProvider, setSelectedProvider] = useState(searchParams.get('modelId') || '') const [provider, setProvider] = useState() - const [apiKeys, setApiKeys] = useState([]) const { fetchData } = useFetch() const [searchWord, setSearchWord] = useState('') const [total, setTotal] = useState(0) - const modalRef = useRef() - const { accessData } = useGlobalContext() + const [timeButton, setTimeButton] = useState('day') + const [timeRange, setTimeRange] = useState<{ start: number | null; end: number | null }>({ + start: null, + end: null + }) + const [queryBtnLoading, setQueryBtnLoading] = useState(false) useEffect(() => { pageListRef.current?.reload() }, [selectedProvider]) - const handleEdit = (record: APIKey) => {} - - const handleAdd = () => {} - - const handleDelete = async (id: string) => { - try { - const response = await fetchData>('ai/resource/key', { - method: 'DELETE', - eoParams: { - provider: selectedProvider, - id: id, - branchID: 0 - } - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - }) - - if (response.code === STATUS_CODE.SUCCESS) { - message.success($t('删除成功')) - pageListRef.current?.reload() - } else { - message.error(response.msg || RESPONSE_TIPS.error) - } - } catch (error) { - message.error(RESPONSE_TIPS.error) - } - } - - const handleToggleStatus = async (id: string, currentStatus: string) => { - try { - const newStatus = currentStatus === 'normal' ? 'disable' : 'enable' - const response = await fetchData>(`ai/resource/key/${newStatus}`, { - method: 'PUT', - eoParams: { - provider: selectedProvider, - id: id - } - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - }) - - if (response.code === STATUS_CODE.SUCCESS) { - message.success(newStatus === 'disable' ? $t('停用成功') : $t('启用成功')) - pageListRef.current?.reload() - } else { - message.error(response.msg || RESPONSE_TIPS.error) - } - } catch (error) { - message.error(RESPONSE_TIPS.error) - } - } - - const handleDragSortEnd = async (beforeIndex: number, afterIndex: number, newDataSource: APIKey[]) => { - console.log(beforeIndex, afterIndex, newDataSource) - try { - const response = await fetchData>('ai/resource/key/sort', { - method: 'PUT', - eoParams: { - origin: newDataSource[beforeIndex].id, - target: newDataSource[afterIndex].id, - sort: afterIndex > beforeIndex ? 'before' : 'after' - } - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - }) - - if (response.code === STATUS_CODE.SUCCESS) { - message.success($t('排序成功')) - pageListRef.current?.reload() - } else { - message.error(response.msg || RESPONSE_TIPS.error) - } - } catch (error) { - message.error(RESPONSE_TIPS.error) - } - } - - const requestApiKeys = async (params: any) => { + const requestApis = async (params: any) => { if (!selectedProvider) return + setQueryBtnLoading(true) try { - const response = await fetchData>('ai/resource/keys', { + const response = await fetchData>('ai/apis', { method: 'GET', eoParams: { provider: selectedProvider, page_size: params.pageSize, keyword: searchWord, - page: params.current + page: params.current, + start: timeRange.start, + end: timeRange.end } - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' }) - + setQueryBtnLoading(false) if (response.code === STATUS_CODE.SUCCESS) { setTotal(response.data.total) return { - data: response.data.keys, + data: response.data.apis, success: true, total: response.data.total } @@ -141,13 +72,6 @@ const KeySettings: React.FC = () => { } } } - const statusEnum = { - normal: { text: {$t('正常')} }, - exceeded: { text: {$t('超额')} }, - expired: { text: {$t('过期')} }, - disabled: { text: {$t('停用')} }, - error: { text: {$t('错误')} } - } const operation: PageProColumns[] = [ { @@ -161,85 +85,66 @@ const KeySettings: React.FC = () => { access="system.settings.ai_key_resource.manager" key="edit" btnType="edit" - onClick={() => handleEdit(entity)} + // onClick={() => handleEdit(entity)} btnTitle={$t('编辑')} - />, - , - entity.status !== 'expired' && entity.status !== 'error' && ( - <> - handleToggleStatus(entity.id, entity.status)} - btnTitle={entity.status === 'normal' ? $t('停用') : $t('启用')} - /> - - - ), - entity.can_delete !== false && ( - handleDelete(entity.id as string)} - btnTitle={$t('删除')} - /> - ) + /> ] } ] const columns: PageProColumns[] = [ { - title: '', - dataIndex: 'drag', - width: '40px' - }, - { - title: $t('调用优先级'), - dataIndex: 'priority', - width: '100px' - }, - { - title: $t('名称'), + title: 'AI 服务(name)', dataIndex: 'name', - render: (dom: React.ReactNode, entity: APIKey) => {entity.name} + key: 'name', + width: 180 }, { - title: $t('状态'), - dataIndex: 'status', - ellipsis: true, - valueType: 'select', - filters: true, - onFilter: true, - valueEnum: statusEnum, - render: (dom: React.ReactNode, entity: APIKey) => statusEnum[entity.status]?.text || entity.status + title: 'API URL', + dataIndex: 'request_path', + key: 'request_path', + width: 200, + ellipsis: true }, { - title: $t('已用 Token'), + title: '模型', + dataIndex: 'model', + key: 'model', + width: 150 + }, + { + title: '已用 Token', dataIndex: 'use_token', - render: (dom: React.ReactNode, entity: APIKey) => { - const value = entity.use_token - return value.toLocaleString() - } + key: 'use_token', + width: 120 }, { - title: $t('编辑时间'), - dataIndex: 'update_time' + title: '是否放行', + dataIndex: 'disabled', + key: 'disabled', + width: 100, + render: (disabled: boolean) => {disabled ? '禁用' : '启用'} }, { - title: $t('过期时间'), - dataIndex: 'expire_time', - render: (dom: React.ReactNode, entity: APIKey) => { - return entity.expire_time === 0 - ? $t('永不过期') - : dayjs(Number(entity.expire_time)).format('YYYY-MM-DD HH:mm:ss') - } + title: '编辑时间', + dataIndex: 'update_time', + key: 'update_time', + width: 160, + render: (time: string) => {dayjs(time).format('YYYY-MM-DD HH:mm:ss')} }, ...operation ] + const resetQuery = () => { + setTimeButton('day') + setTimeRange({ start: null, end: null }) + setSearchWord('') + } + + const getData = () => { + pageListRef.current?.reload() + } + return ( { showBorder={false} scrollPage={false} > +
+ { + setTimeRange($event) + }} + /> +
+ + +
+
{ setSearchWord(e.target.value) }} showPagination={true} searchPlaceholder={$t('请输入 APIURL 搜索')} columns={columns} - dragSortKey="drag" - onDragSortEnd={handleDragSortEnd} - addNewBtnTitle={$t('添加 APIKey')} - onAddNewBtnClick={handleAdd} />
) } -export default KeySettings +export default ApiSettings diff --git a/frontend/packages/core/src/pages/keySettings/index.tsx b/frontend/packages/core/src/pages/keySettings/index.tsx index e5a3d8a7..5c9ed154 100644 --- a/frontend/packages/core/src/pages/keySettings/index.tsx +++ b/frontend/packages/core/src/pages/keySettings/index.tsx @@ -187,6 +187,8 @@ const KeySettings: React.FC = () => { page_size: params.pageSize, keyword: searchWord, page: params.current + //TODO API 筛选 + // statuses: params.statuses || [] } // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' }) From fa0a211db931c3d7689098c8e0dfe99b502fcc9c Mon Sep 17 00:00:00 2001 From: scarqin Date: Thu, 2 Jan 2025 08:21:33 +0800 Subject: [PATCH 15/17] feat: add banner --- .../packages/core/src/pages/aiApis/index.tsx | 130 +++++++++++------- .../core/src/pages/aiSetting/AIFlowChart.tsx | 25 +++- .../core/src/pages/keySettings/index.tsx | 2 +- .../src/component/MonitorApiPage.tsx | 31 +++-- 4 files changed, 119 insertions(+), 69 deletions(-) diff --git a/frontend/packages/core/src/pages/aiApis/index.tsx b/frontend/packages/core/src/pages/aiApis/index.tsx index 50291fc2..c4f87ddf 100644 --- a/frontend/packages/core/src/pages/aiApis/index.tsx +++ b/frontend/packages/core/src/pages/aiApis/index.tsx @@ -7,7 +7,8 @@ import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' import AIProviderSelect, { AIProvider } from '@core/components/AIProviderSelect' -import { App, Button, Typography } from 'antd' +import { getTime } from '@dashboard/utils/dashboard' +import { Alert, App, Button, Typography } from 'antd' import dayjs from 'dayjs' import React, { useEffect, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' @@ -37,16 +38,22 @@ const ApiSettings: React.FC = () => { if (!selectedProvider) return setQueryBtnLoading(true) try { + const eoParams = { + provider: selectedProvider, + page_size: params.pageSize, + keyword: searchWord, + page: params.current, + start: timeRange.start, + end: timeRange.end + } + if (!timeRange || !timeRange.start) { + const { startTime, endTime } = getTime(timeButton, []) + eoParams.start = startTime + eoParams.end = endTime + } const response = await fetchData>('ai/apis', { method: 'GET', - eoParams: { - provider: selectedProvider, - page_size: params.pageSize, - keyword: searchWord, - page: params.current, - start: timeRange.start, - end: timeRange.end - } + eoParams }) setQueryBtnLoading(false) if (response.code === STATUS_CODE.SUCCESS) { @@ -94,7 +101,7 @@ const ApiSettings: React.FC = () => { const columns: PageProColumns[] = [ { - title: 'AI 服务(name)', + title: 'AI 服务', dataIndex: 'name', key: 'name', width: 180 @@ -108,28 +115,38 @@ const ApiSettings: React.FC = () => { }, { title: '模型', - dataIndex: 'model', + dataIndex: ['model', 'name'], key: 'model', - width: 150 + width: 150, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum: {} }, { title: '已用 Token', dataIndex: 'use_token', key: 'use_token', - width: 120 + width: 120, + sorter: true }, { title: '是否放行', dataIndex: 'disabled', - key: 'disabled', - width: 100, - render: (disabled: boolean) => {disabled ? '禁用' : '启用'} + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum: { + true: { text: {$t('拦截')} }, + false: { text: {$t('放行')} } + } }, { title: '编辑时间', dataIndex: 'update_time', key: 'update_time', - width: 160, + width: 200, render: (time: string) => {dayjs(time).format('YYYY-MM-DD HH:mm:ss')} }, ...operation @@ -145,56 +162,77 @@ const ApiSettings: React.FC = () => { pageListRef.current?.reload() } + const renderProviderBanner = () => { + if (provider?.disabled) { + return ( + window.open('/details')}> + {$t('查看详情')} + + } + /> + ) + } + return null + } + return ( - {$t('支持查看调用某个 AI 供应商的所有 AI 服务 API 清单')} -
+
{ + onChange={(value, option) => { setSelectedProvider(value) - setProvider(provider) + setProvider(option) }} />
+ {renderProviderBanner()} } showBorder={false} scrollPage={false} > -
- { - setTimeRange($event) - }} - /> -
- - -
-
+ { + setTimeRange($event) + }} + /> +
+ + +
+
+ } request={requestApis} onSearchWordChange={(e) => { setSearchWord(e.target.value) diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx index 9f7173bf..79611d80 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx @@ -3,6 +3,7 @@ import { BasicResponse } from '@common/const/const' import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' import { CoordinateExtent, Edge, @@ -15,7 +16,7 @@ import { useNodesState } from '@xyflow/react' import '@xyflow/react/dist/style.css' -import { Button, Space } from 'antd' +import { Button, Space, Spin } from 'antd' import { useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import CustomEdge from './components/CustomEdge' @@ -66,6 +67,7 @@ const edgeTypes: EdgeTypes = { const AIFlowChart = () => { const [modelData, setModelData] = useState([]) + const [loading, setLoading] = useState(false) const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const { fetchData } = useFetch() @@ -73,14 +75,19 @@ const AIFlowChart = () => { const navigate = useNavigate() useEffect(() => { + setLoading(true) fetchData('ai/providers/configured', { method: 'GET', eoTransformKeys: ['default_llm'] // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - }).then((response) => { - const mockApiResponse: ApiResponse = response as ApiResponse - setModelData(mockApiResponse.data.providers) }) + .then((response) => { + const mockApiResponse: ApiResponse = response as ApiResponse + setModelData(mockApiResponse.data.providers) + }) + .finally(() => { + setLoading(false) + }) }, [aiConfigFlushed]) useEffect(() => { @@ -211,11 +218,15 @@ const AIFlowChart = () => { return (
- {modelData.length === 0 ? ( + {loading ? ( +
+ +
+ ) : modelData.length === 0 ? ( -
No AI model configured
+
{$t('未配置 AI 模型')}
) : ( diff --git a/frontend/packages/core/src/pages/keySettings/index.tsx b/frontend/packages/core/src/pages/keySettings/index.tsx index 5c9ed154..6ea97505 100644 --- a/frontend/packages/core/src/pages/keySettings/index.tsx +++ b/frontend/packages/core/src/pages/keySettings/index.tsx @@ -309,7 +309,7 @@ const KeySettings: React.FC = () => { render: (dom: React.ReactNode, entity: APIKey) => { return entity.expire_time === 0 ? $t('永不过期') - : dayjs(Number(entity.expire_time)).format('YYYY-MM-DD HH:mm:ss') + : dayjs(Number(entity.expire_time) * 1000).format('YYYY-MM-DD HH:mm:ss') } }, ...operation diff --git a/frontend/packages/dashboard/src/component/MonitorApiPage.tsx b/frontend/packages/dashboard/src/component/MonitorApiPage.tsx index 3a824368..5bb5d2d4 100644 --- a/frontend/packages/dashboard/src/component/MonitorApiPage.tsx +++ b/frontend/packages/dashboard/src/component/MonitorApiPage.tsx @@ -1,23 +1,23 @@ import { CloseOutlined, ExpandOutlined, SearchOutlined } from '@ant-design/icons' -import { Select, Input, Button, App, Drawer } from 'antd' -import { debounce } from 'lodash-es' -import { useState, useEffect, useRef } from 'react' -import { MonitorApiData, SearchBody } from '@dashboard/const/type' -import { getTime } from '../utils/dashboard' import ScrollableSection from '@common/components/aoplatform/ScrollableSection' import TimeRangeSelector, { RangeValue, TimeRange, TimeRangeButton } from '@common/components/aoplatform/TimeRangeSelector' -import MonitorTable, { MonitorTableHandler } from './MonitorTable' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' -import { DefaultOptionType } from 'antd/es/select' -import { useExcelExport } from '@common/hooks/excel' -import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from '@dashboard/const/const' -import { useFetch } from '@common/hooks/http' import { EntityItem } from '@common/const/type' +import { useExcelExport } from '@common/hooks/excel' +import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' +import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from '@dashboard/const/const' +import { MonitorApiData, SearchBody } from '@dashboard/const/type' +import { App, Button, Drawer, Input, Select } from 'antd' +import { DefaultOptionType } from 'antd/es/select' +import { debounce } from 'lodash-es' +import { useEffect, useRef, useState } from 'react' +import { getTime } from '../utils/dashboard' +import MonitorTable, { MonitorTableHandler } from './MonitorTable' export type MonitorApiPageProps = { fetchTableData: (body: SearchBody) => Promise> detailDrawerContent: React.ReactNode @@ -100,6 +100,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) { const getMonitorData = () => { let query = queryData if (!queryData || queryData.start === undefined) { + console.log(timeButton, datePickerValue) const { startTime, endTime } = getTime(timeButton, datePickerValue || []) query = { ...query, start: startTime, end: endTime } } @@ -186,7 +187,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) { } return ( -
+
-
- +
+ ({ ...(prevData || {}), apis: value })) }} /> - +
{/* setQueryData({ ...queryData, path: '' })} /> */} Date: Thu, 2 Jan 2025 09:29:03 +0800 Subject: [PATCH 16/17] fix: alert api list tips --- frontend/packages/core/src/App.tsx | 258 +++++++++--------- .../src/components/AIProviderSelect/index.tsx | 23 +- .../packages/core/src/pages/aiApis/index.tsx | 10 +- 3 files changed, 149 insertions(+), 142 deletions(-) diff --git a/frontend/packages/core/src/App.tsx b/frontend/packages/core/src/App.tsx index de83bc0f..aa5b4c40 100644 --- a/frontend/packages/core/src/App.tsx +++ b/frontend/packages/core/src/App.tsx @@ -1,47 +1,45 @@ - +import { StyleProvider } from '@ant-design/cssinjs' +import { BreadcrumbProvider } from '@common/contexts/BreadcrumbContext.tsx' +import { GlobalProvider } from '@common/contexts/GlobalStateContext' +import { useLocaleContext } from '@common/contexts/LocaleContext' +import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext' +import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext' +import useInitializeMonaco from '@common/hooks/useInitializeMonaco' +import { $t } from '@common/locales' +import RenderRoutes from '@core/components/aoplatform/RenderRoutes' +import { App as AppAntd, ConfigProvider } from 'antd' +import { useMemo } from 'react' import './App.css' -import { ConfigProvider, App as AppAntd } from 'antd'; -import RenderRoutes from '@core/components/aoplatform/RenderRoutes'; -import {BreadcrumbProvider} from "@common/contexts/BreadcrumbContext.tsx"; -import useInitializeMonaco from "@common/hooks/useInitializeMonaco"; -import { useMemo } from 'react'; -import { GlobalProvider } from '@common/contexts/GlobalStateContext'; -import { $t } from '@common/locales'; -import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'; -import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext'; -import { useLocaleContext } from '@common/contexts/LocaleContext'; -import { StyleProvider } from '@ant-design/cssinjs'; - const antdComponentThemeToken = { token: { // Seed Token,影响范围大 colorPrimary: '#3D46F2', - colorLink:'#3D46F2', - colorBorder:'#ededed', - colorText:'#333', + colorLink: '#3D46F2', + colorBorder: '#ededed', + colorText: '#333', borderRadius: 4, // 派生变量,影响范围小 colorBgContainer: '#fff', - colorPrimaryBg:'#EBEEF2', - colorTextQuaternary:'#BBB', - colorTextTertiary:'#999' + colorPrimaryBg: '#EBEEF2', + colorTextQuaternary: '#BBB', + colorTextTertiary: '#999' }, - components:{ + components: { // 派生变量,影响范围小 - Input:{ - activeShadow:'none' + Input: { + activeShadow: 'none' }, - Select:{ - activeShadow:'none' + Select: { + activeShadow: 'none' }, - Checkbox:{ - activeShadow:'none' + Checkbox: { + activeShadow: 'none' }, - Cascader:{ - activeShadow:'none', - optionSelectedBg:'#EBEEF2', - optionHoverBg:'#EBEEF2' + Cascader: { + activeShadow: 'none', + optionSelectedBg: '#EBEEF2', + optionHoverBg: '#EBEEF2' }, Layout: { bodyBg: '#fff', @@ -50,122 +48,122 @@ const antdComponentThemeToken = { headerHeight: 50, headerPadding: '10 20px', lightSiderBg: '#fff', - siderBg: '#fff', + siderBg: '#fff' }, - Breadcrumb:{ - itemColor:'#666', - linkColor:'#666', - lastItemColor:'#333', + Breadcrumb: { + itemColor: '#666', + linkColor: '#666', + lastItemColor: '#333' }, - Table:{ - headerBorderRadius:0, - headerSplitColor:'#ededed', - borderColor:'#ededed', - cellPaddingBlockMD:'15px', - cellPaddingInlineMD:'12px', - cellPaddingBlockSM:'8px', - cellPaddingInlineSM:'12px', - headerFilterHoverBg:'#EBEEF2', - headerSortActiveBg:'#F7F8FA', - headerSortHoverBg:'#F7F8FA', - fixedHeaderSortActiveBg:'#F7F8FA', - headerBg:'#FAFAFA', - rowHoverBg:'#EBEEF2' - + Table: { + headerBorderRadius: 0, + headerSplitColor: '#ededed', + borderColor: '#ededed', + cellPaddingBlockMD: '15px', + cellPaddingInlineMD: '12px', + cellPaddingBlockSM: '8px', + cellPaddingInlineSM: '12px', + headerFilterHoverBg: '#EBEEF2', + headerSortActiveBg: '#F7F8FA', + headerSortHoverBg: '#F7F8FA', + fixedHeaderSortActiveBg: '#F7F8FA', + headerBg: '#FAFAFA', + rowHoverBg: '#EBEEF2' }, - Segmented:{ - itemColor:'#333', - itemSelectedColor:'#333', - trackBg:'#f7f8fa', - trackPadding:0, - // itemHoverColor:'#EBEEF2', - itemActiveBg:'#EBEEF2', - itemHoverBg:'#EBEEF2', - itemSelectedBg:'#EBEEF2', + Segmented: { + itemColor: '#333', + itemSelectedColor: '#333', + trackBg: '#f7f8fa', + trackPadding: 0, + // itemHoverColor:'#EBEEF2', + itemActiveBg: '#EBEEF2', + itemHoverBg: '#EBEEF2', + itemSelectedBg: '#EBEEF2' }, - Tree:{ - // titleHeight:30, - // fontSize:12, - directoryNodeSelectedBg:'#EBEEF2', - directoryNodeSelectedColor:'#333', - nodeSelectedBg:'#EBEEF2', - nodeHoverBg:'#EBEEF2' - }, - Collapse:{ - headerBg:'#f7f8fa', - headerPadding:"12px", - contentPadding:"0 10px 12px 10px" - }, - Button:{ - // paddingInline:8, - dangerShadow:'none', - defaultShadow:'none', - primaryShadow:'none' - }, - Tabs:{ - cardBg:'#EBEEF2', - cardHeight:42, - horizontalItemGutter:8, - horizontalItemPaddingSM:'12px 8px 8px 8px', - horizontalItemPadding:'12px 8px 8px 8px', - }, - Menu:{ - // itemBg:'#F7F8FA', - // subMenuItemBg:'#F7F8FA', - // itemMarginBlock:0, - // activeBarBorderWidth:0, - // itemSelectedColor:'#333', - // itemSelectedBg:'#EBEEF2', - // itemHoverBg:'#EBEEF2' - }, - List:{ - itemPadding:'8px 0' - }, - Form:{ - itemMarginBottom:10, - - }, - Alert:{ - defaultPadding:'12px 16px' - }, - Tag:{ - defaultBg:"#f7f8fa" - }, + Tree: { + // titleHeight:30, + // fontSize:12, + directoryNodeSelectedBg: '#EBEEF2', + directoryNodeSelectedColor: '#333', + nodeSelectedBg: '#EBEEF2', + nodeHoverBg: '#EBEEF2' + }, + Collapse: { + headerBg: '#f7f8fa', + headerPadding: '12px', + contentPadding: '0 10px 12px 10px' + }, + Button: { + // paddingInline:8, + dangerShadow: 'none', + defaultShadow: 'none', + primaryShadow: 'none' + }, + Tabs: { + cardBg: '#EBEEF2', + cardHeight: 42, + horizontalItemGutter: 8, + horizontalItemPaddingSM: '12px 8px 8px 8px', + horizontalItemPadding: '12px 8px 8px 8px' + }, + Menu: { + // itemBg:'#F7F8FA', + // subMenuItemBg:'#F7F8FA', + // itemMarginBlock:0, + // activeBarBorderWidth:0, + // itemSelectedColor:'#333', + // itemSelectedBg:'#EBEEF2', + // itemHoverBg:'#EBEEF2' + }, + List: { + itemPadding: '8px 0' + }, + Form: { + itemMarginBottom: 10 + }, + Alert: { + defaultPadding: '8px 12px' + }, + Tag: { + defaultBg: '#f7f8fa' + } } } - function App() { - const { locale } = useLocaleContext(); + const { locale } = useLocaleContext() useInitializeMonaco() - - - const validateMessages = useMemo(()=>({ - required: $t('必填项'), - email:$t('不是有效邮箱地址')} - ),[locale]) - + + const validateMessages = useMemo( + () => ({ + required: $t('必填项'), + email: $t('不是有效邮箱地址') + }), + [locale] + ) + return ( - - + - - - - - - - - - + form={{ validateMessages }} + > + + + + + + + + + - ); + ) } export default App diff --git a/frontend/packages/core/src/components/AIProviderSelect/index.tsx b/frontend/packages/core/src/components/AIProviderSelect/index.tsx index cb1bd4cf..cd28fc64 100644 --- a/frontend/packages/core/src/components/AIProviderSelect/index.tsx +++ b/frontend/packages/core/src/components/AIProviderSelect/index.tsx @@ -1,17 +1,14 @@ import { STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' +import { ModelDetailData } from '@core/pages/aiSetting/types' import { Select, Space, message } from 'antd' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -export interface AIProvider { - id: string - name: string - logo: string - configured: boolean - getApikeyUrl: string - status: string +export interface AIProvider extends ModelDetailData { default_config: string + backupName: string + backupModel: string } interface AIProviderResponse { @@ -41,10 +38,18 @@ const AIProviderSelect: React.FC = ({ value, onChange, st const fetchProviders = async () => { if (isMounted) setLoading(true) try { - const response = await fetchData('simple/ai/providers/configured', { method: 'GET' }) + const endpoint = 'simple/ai/providers/configured' + const response = await fetchData(endpoint, { method: 'GET' }) const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) { - isMounted && setProviders(data.providers) + isMounted && + setProviders( + data.providers.map((val) => ({ + ...val, + backupName: data.backup?.name, + backupModel: data.backup?.model?.name + })) + ) if (!data.providers?.length) return const selectedProvider: AIProvider = value ? data.providers.find((p) => p.id === value) : data.providers[0] onChange?.(selectedProvider.id, selectedProvider) diff --git a/frontend/packages/core/src/pages/aiApis/index.tsx b/frontend/packages/core/src/pages/aiApis/index.tsx index c4f87ddf..e4573c10 100644 --- a/frontend/packages/core/src/pages/aiApis/index.tsx +++ b/frontend/packages/core/src/pages/aiApis/index.tsx @@ -163,13 +163,17 @@ const ApiSettings: React.FC = () => { } const renderProviderBanner = () => { - if (provider?.disabled) { + console.log(provider) + if (provider?.status === 'disabled') { return ( window.open('/details')}> {$t('查看详情')} From 6574f36c73203bd8ee83542698e2eedd905db1d0 Mon Sep 17 00:00:00 2001 From: scarqin Date: Thu, 2 Jan 2025 09:32:19 +0800 Subject: [PATCH 17/17] fix: provider tips --- .../src/components/AIProviderSelect/index.tsx | 16 +++++++--------- .../packages/core/src/pages/aiApis/index.tsx | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/frontend/packages/core/src/components/AIProviderSelect/index.tsx b/frontend/packages/core/src/components/AIProviderSelect/index.tsx index cd28fc64..273d9117 100644 --- a/frontend/packages/core/src/components/AIProviderSelect/index.tsx +++ b/frontend/packages/core/src/components/AIProviderSelect/index.tsx @@ -42,16 +42,14 @@ const AIProviderSelect: React.FC = ({ value, onChange, st const response = await fetchData(endpoint, { method: 'GET' }) const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) { - isMounted && - setProviders( - data.providers.map((val) => ({ - ...val, - backupName: data.backup?.name, - backupModel: data.backup?.model?.name - })) - ) + const providers = data.providers.map((val) => ({ + ...val, + backupName: data.backup?.name, + backupModel: data.backup?.model?.name + })) + isMounted && setProviders(providers) if (!data.providers?.length) return - const selectedProvider: AIProvider = value ? data.providers.find((p) => p.id === value) : data.providers[0] + const selectedProvider: AIProvider = value ? providers.find((p) => p.id === value) : providers[0] onChange?.(selectedProvider.id, selectedProvider) } else { message.error(msg || t('Failed to fetch AI providers')) diff --git a/frontend/packages/core/src/pages/aiApis/index.tsx b/frontend/packages/core/src/pages/aiApis/index.tsx index e4573c10..0a12ec3c 100644 --- a/frontend/packages/core/src/pages/aiApis/index.tsx +++ b/frontend/packages/core/src/pages/aiApis/index.tsx @@ -11,7 +11,7 @@ import { getTime } from '@dashboard/utils/dashboard' import { Alert, App, Button, Typography } from 'antd' import dayjs from 'dayjs' import React, { useEffect, useRef, useState } from 'react' -import { useSearchParams } from 'react-router-dom' +import { useNavigate, useSearchParams } from 'react-router-dom' import { APIKey } from './types' const ApiSettings: React.FC = () => { @@ -24,6 +24,7 @@ const ApiSettings: React.FC = () => { const [searchWord, setSearchWord] = useState('') const [total, setTotal] = useState(0) const [timeButton, setTimeButton] = useState('day') + const navigate = useNavigate() const [timeRange, setTimeRange] = useState<{ start: number | null; end: number | null }>({ start: null, end: null @@ -163,19 +164,23 @@ const ApiSettings: React.FC = () => { } const renderProviderBanner = () => { + if (!provider) return null + console.log(provider) - if (provider?.status === 'disabled') { + if (provider.status === 'disabled' || provider.status === 'abnormal') { + const message = + provider.status === 'disabled' + ? $t(`当前供应商异常,以下API均临时调用 ${provider.backupName} 下的 ${provider.backupModel} 模型能力。`) + : $t(`当前供应商异常,以下API均临时调用 ${provider.backupName} 下的 ${provider.backupModel} 模型能力。`) + return ( window.open('/details')}> + }