diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index 3a7151ab..0fbccad4 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { AiSettingProvider } from './contexts/AiSettingContext' import OnlineModelList from './OnlineModelList' +import LocalModelList from './LocalModelList' const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const @@ -47,7 +48,9 @@ const AiSettingContent = () => { { key: 'config', label: $t('本地模型'), - children:
+ children:
+ +
} ]} /> diff --git a/frontend/packages/core/src/pages/aiSetting/LocalModelList.tsx b/frontend/packages/core/src/pages/aiSetting/LocalModelList.tsx new file mode 100644 index 00000000..40196bfa --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/LocalModelList.tsx @@ -0,0 +1,347 @@ +import { ActionType } from '@ant-design/pro-components' +import PageList, { PageProColumns } from '@common/components/aoplatform/PageList' +import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { App, Divider, Form, Space, Switch, Tag } from 'antd' +import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react' +import { ModelListData } from './types' +import LocalAiDeploy, { LocalAiDeployHandle } from '../guide/LocalAiDeploy' +import { ServiceDeployment } from '../system/serviceDeployment/ServiceDeployment' +import { LogsFooter } from '../system/serviceDeployment/ServiceDeployMentFooter' +import WithPermission from '@common/components/aoplatform/WithPermission' +type EditLocalModelModalHandle = { + save: () => Promise +} +type EditLocalModelModalProps = { + enable: boolean + modelID?: string +} +const EditLocalModelModal = forwardRef((props: EditLocalModelModalProps, ref) => { + const { enable, modelID } = props + const { fetchData } = useFetch() + const { message } = App.useApp() + const [form] = Form.useForm() + const [currentStatus, setCurrentStatus] = useState(enable) + + useEffect(() => { + form.setFieldsValue({ enable }) + }, []) + /** + * 保存 + * @returns + */ + const save: () => Promise = () => { + return new Promise((resolve, reject) => { + try { + form + .validateFields() + .then((value) => { + const finalValue = { + disable: !value.enable + } + + fetchData>('model/local/info', { + method: 'PUT', + eoParams: { model: modelID }, + eoBody: finalValue, + }) + .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)) + }) + .catch((errorInfo) => reject(errorInfo)) + } catch (error) { + reject(error) + } + }) + } + useImperativeHandle(ref, () => ({ + save + })) + + return ( + +
+ +
+
+ {$t('当前调用状态:')} + {currentStatus && {$t('正常')}} + {!currentStatus && {$t('停用')}} +
+ + { + form.setFieldsValue({ enable: checked }) + setCurrentStatus(checked) + }} + /> + +
+
+
+
+ ) +}) + +const LocalModelList: React.FC = () => { + const pageListRef = useRef(null) + const { message, modal } = App.useApp() + const { fetchData } = useFetch() + const [searchWord, setSearchWord] = useState('') + const localAiDeployRef = useRef() + const EditLocalModelModalRef = useRef() + + const handleEdit = (record: ModelListData) => { + modal.confirm({ + title: $t('部署 AI 模型'), + content: , + onOk: () => { + return EditLocalModelModalRef.current?.save().then((res) => { + if (res === true) { + pageListRef.current?.reload() + } + }) + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + const handleAdd = () => { + const modalInstance = modal.confirm({ + title: $t('部署 AI 模型'), + content: ( + { + modalInstance.destroy() + pageListRef.current?.reload() + }} + > + ), + onOk: () => { + return localAiDeployRef.current?.deployLocalAIServer().then((res) => { + if (res === true) { + pageListRef.current?.reload() + } + }) + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + const handleDelete = async (id: string, apiCount: number) => { + modal.confirm({ + title: $t('停止部署'), + content: `${$t('有')} ${apiCount} ${$t('个API使用当前模型,删除当前的模型配置后,该模型相关的API将会切换为使用负载均衡中优先级最高的可用模型。并且当前模型下的所有API KEY和相关数据将会被清空,是否确认删除当前模型?')}`, + onOk: () => { + return new Promise((resolve, reject) => { + try { + fetchData>('ai/provider', { + method: 'DELETE', + eoParams: { + provider: id + } + }) + .then((response) => { + if (response.code === STATUS_CODE.SUCCESS) { + message.success($t('删除成功')) + pageListRef.current?.reload() + } else { + message.error(response.msg || RESPONSE_TIPS.error) + } + resolve(true) + }) + .catch((error) => { + message.error(RESPONSE_TIPS.error) + resolve(true) + }) + } catch (error) { + message.error(RESPONSE_TIPS.error) + resolve(true) + } + }) + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + const requestList = async (params: any) => { + try { + const response = await fetchData>('model/local/list', { + method: 'GET', + eoParams: { + page_size: params.pageSize, + keyword: searchWord, + page: params.current + }, + eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/', + eoTransformKeys: ['can_delete', 'api_count'] + }) + + if (response.code === STATUS_CODE.SUCCESS) { + return { + data: response.data.models, + success: true, + total: response.data.total + } + } else { + message.error(response.msg || $t(RESPONSE_TIPS.error)) + return { + data: [], + success: false, + total: response.data.total + } + } + } catch (error) { + return { + data: [], + success: false, + total: 0 + } + } + } + + const operation: PageProColumns[] = [ + { + title: '', + key: 'option', + btnNums: 4, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: ModelListData) => [ + handleEdit(entity)} + btnTitle={$t('设置')} + />, + , + handleDelete(entity.id as string, entity?.apiCount)} + btnTitle={$t('删除')} + /> + ] + } + ] + + const openLogsModal = (record: any) => { + const modalInstance = modal.confirm({ + title: $t('部署过程'), + content: , + footer: () => { + return + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + const columns: PageProColumns[] = [ + { + title: $t('名称'), + dataIndex: 'name', + render: (dom: React.ReactNode, entity: ModelListData) => {entity.name} + }, + { + title: $t('状态'), + width: 140, + dataIndex: 'state', + ellipsis: true, + render: (dom: React.ReactNode, entity: ModelListData) => ( + .ant-typography]:text-[#2196f3] cursor-pointer' : entity?.state === 'error' ? '[&>.ant-typography]:text-[#ff4d4f] cursor-pointer' : ''}`} + onClick={(e) => { + if (['deploying', 'error'].includes(entity?.state as string)) { + e?.stopPropagation() + openLogsModal(entity) + } + }} + > + {dom} + + ) + }, + { + title: $t('Apis'), + dataIndex: 'apiCount', + render: (dom: React.ReactNode, record: ModelListData) => ( + + + {record.apiCount || '0'} + + + ) + }, + ...operation + ] + + return ( + { + setSearchWord(e.target.value) + pageListRef.current?.reload() + }} + showPagination={true} + searchPlaceholder={$t('请输入名称搜索')} + columns={columns} + addNewBtnTitle={$t('部署模型')} + onAddNewBtnClick={handleAdd} + /> + ) +} + +export default LocalModelList diff --git a/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx b/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx index b59e6541..436e4e07 100644 --- a/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx @@ -11,39 +11,62 @@ import { AiSettingListItem, ModelListData } from './types' const OnlineModelList: React.FC = () => { const pageListRef = useRef(null) - const { message } = App.useApp() + const { message, modal } = App.useApp() const { fetchData } = useFetch() const [searchWord, setSearchWord] = useState('') const [total, setTotal] = useState(0) const { openConfigModal } = useAiSetting() const handleEdit = (record: ModelListData) => { - openConfigModal({ id: record.id, defaultLlm: record.defaultLlm } as AiSettingListItem) + openConfigModal({ id: record.id, defaultLlm: record.defaultLlm } as AiSettingListItem, () => { + pageListRef.current?.reload() + }) } const handleAdd = () => { - openConfigModal() + openConfigModal(undefined, () => { + pageListRef.current?.reload() + }) } - const handleDelete = async (id: string) => { - try { - const response = await fetchData>('ai/provider', { - method: 'DELETE', - eoParams: { - provider: id - } - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - }) + const handleDelete = async (id: string, apiCount: number) => { + modal.confirm({ + title: $t('停止部署'), + content: `${$t('有')} ${apiCount} ${$t('个API使用当前模型,删除当前的模型配置后,该模型相关的API将会切换为使用负载均衡中优先级最高的可用模型。并且当前模型下的所有API KEY和相关数据将会被清空,是否确认删除当前模型?')}`, + onOk: () => { + return new Promise((resolve, reject) => { + try { + fetchData>('ai/provider', { + method: 'DELETE', + eoParams: { + provider: id + } + // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' + }).then((response) => { + if (response.code === STATUS_CODE.SUCCESS) { + message.success($t('删除成功')) + pageListRef.current?.reload() + } else { + message.error(response.msg || RESPONSE_TIPS.error) + } + resolve(true) + }).catch((error) => { + message.error(RESPONSE_TIPS.error) + resolve(true) + }) + } catch (error) { + message.error(RESPONSE_TIPS.error) + resolve(true) + } + }) + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) - 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 requestList = async (params: any) => { @@ -55,7 +78,7 @@ const OnlineModelList: React.FC = () => { keyword: searchWord, page: params.current }, - eoTransformKeys: ['default_llm'] + eoTransformKeys: ['default_llm', 'api_count', 'key_count', 'can_delete'] // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' }) @@ -107,8 +130,10 @@ const OnlineModelList: React.FC = () => { handleDelete(entity.id as string)} + onClick={() => handleDelete(entity.id as string, entity.apiCount)} btnTitle={$t('删除')} /> ] @@ -137,11 +162,45 @@ const OnlineModelList: React.FC = () => { }, { title: $t('Apis'), - dataIndex: 'api_count' + dataIndex: 'apiCount', + render: (dom: React.ReactNode, record: ModelListData) => ( + + + {record.apiCount || '0'} + + + ) }, { title: $t('Keys'), - dataIndex: 'key_count' + dataIndex: 'keyCount', + render: (dom: React.ReactNode, record: ModelListData) => ( + + + {record.keyCount || '0'} + + + ) }, ...operation ] diff --git a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx index 1fb60bba..ce71f5a7 100644 --- a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx +++ b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx @@ -8,7 +8,7 @@ import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSetting import { AiSettingListItem } from '../types' interface AiSettingContextType { - openConfigModal: (entity?: AiSettingListItem) => Promise + openConfigModal: (entity?: AiSettingListItem, callback?: () => void) => Promise } const AiSettingContext = createContext(undefined) @@ -19,7 +19,7 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi const modalRef = useRef() const entityData = useRef(null) - const openConfigModal = async (entity?: AiSettingListItem) => { + const openConfigModal = async (entity?: AiSettingListItem, callback?: () => void) => { // 更新弹窗 const updateEntityData = (data: any) => { entityData.current = data @@ -41,6 +41,7 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi return modalRef.current?.save().then((res) => { if (res === true) { setAiConfigFlushed(!aiConfigFlushed) + callback?.() } }) }, diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index 4dcc6f05..c9930b55 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -1,5 +1,6 @@ export type ModelStatus = 'enabled' | 'abnormal' | 'disabled' export type KeyStatus = 'normal' | 'abnormal' | 'disabled' +export type ModelDeployStatus = 'normal' | 'disabled' | 'deploying' | 'error' | undefined export interface KeyData { id: string @@ -14,9 +15,12 @@ export interface ModelListData { defaultLlm: string | undefined modelMode?: string status: ModelStatus - api_count: number - key_count: number + state?: ModelDeployStatus + apiCount: number + keyCount: number + isDisabled?: boolean keys: KeyData[] + canDelete: boolean } export interface AISettingEntityItem { diff --git a/frontend/packages/core/src/pages/loadBalancing/index.tsx b/frontend/packages/core/src/pages/loadBalancing/index.tsx index 3441f5fd..6c2fbfd5 100644 --- a/frontend/packages/core/src/pages/loadBalancing/index.tsx +++ b/frontend/packages/core/src/pages/loadBalancing/index.tsx @@ -208,7 +208,7 @@ const LoadBalancingPage = () => { ellipsis: true, width: 100, key: 'provider', - render: (text: string, record: LoadBalancingItems) => ( + render: (dom: React.ReactNode, record: LoadBalancingItems) => ( {record.provider?.name} / {record.model?.name} @@ -220,7 +220,7 @@ const LoadBalancingPage = () => { width: 100, ellipsis: true, key: 'type', - render: (text: string, record: LoadBalancingItems) => ( + render: (dom: React.ReactNode, record: LoadBalancingItems) => ( {record.type === 'online' ? $t('线上模型') : $t('本地模型')} ) }, @@ -230,15 +230,15 @@ const LoadBalancingPage = () => { width: 120, ellipsis: true, key: 'state', - render: (text: string, record: LoadBalancingItems) => {statusEnum[record.state]?.text || '-'} + render: (dom: React.ReactNode, record: LoadBalancingItems) => {statusEnum[record.state]?.text || '-'} }, { - title: $t('API 数量'), + title: $t('Apis'), dataIndex: 'api_count', ellipsis: true, width: 80, key: 'api_count', - render: (text: string, record: LoadBalancingItems) => ( + render: (dom: React.ReactNode, record: LoadBalancingItems) => ( { textDecoration: 'none' }} > - {record.api_count || '-'} + {record.api_count || '0'} ) }, { - title: $t('KEY 数量'), + title: $t('Keys'), dataIndex: 'key_count', ellipsis: true, width: 80, key: 'key_count', - render: (text: string, record: LoadBalancingItems) => ( + render: (dom: React.ReactNode, record: LoadBalancingItems) => ( { textDecoration: 'none' }} > - {record.key_count || '-'} + {record.key_count || '0'} ) diff --git a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx index 485a12cb..26f6e023 100644 --- a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx +++ b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx @@ -65,7 +65,10 @@ export const ServiceDeployment = (props: { record: SystemTableListItem }) => { { method: 'POST', eoBody: { recordId: record.id }, - custom: true, + eoApiPrefix: '', + headers: { + 'Content-Type': 'event-stream' + }, isStream: true, handleStream: (chunk) => { const parsedChunk = JSON.parse(chunk)