From fe96afc8c36a1f828ce3c71573b57fcc950235c0 Mon Sep 17 00:00:00 2001 From: jeak Date: Fri, 17 Jan 2025 14:57:31 +0800 Subject: [PATCH 01/38] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c52c63a..4ab6e887 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ APIPark uses the Apache 2.0 License. For more details, please refer to the LICEN For enterprise-level features and professional technical support, contact our pre-sales experts for personalized demos, customized solutions, and pricing. - Website: https://apipark.com -- Email: dev@apipark.com +- Email: contact@apipark.com
From 2f81e087e762c67672110d70c4fbc128496416b4 Mon Sep 17 00:00:00 2001 From: jeak Date: Fri, 17 Jan 2025 14:58:19 +0800 Subject: [PATCH 02/38] Update readme-zh-cn.md --- readme/readme-zh-cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme/readme-zh-cn.md b/readme/readme-zh-cn.md index 9384a30a..ae7c9e8d 100644 --- a/readme/readme-zh-cn.md +++ b/readme/readme-zh-cn.md @@ -215,7 +215,7 @@ APIPark 使用 Apache 2.0 许可证。更多详情请查看 LICENSE 文件。 对于企业级功能和专业技术支持,请联系售前专家进行个性化演示、定制方案和获取报价。 - 网站: https://apipark.com -- 电子邮件: dev@apipark.com +- 电子邮件: contact@apipark.com
From d1aeb621d4b7620585d0b0d361a887d40d9f4874 Mon Sep 17 00:00:00 2001 From: jeak Date: Fri, 17 Jan 2025 14:58:46 +0800 Subject: [PATCH 03/38] Update readme-zh-tw.md --- readme/readme-zh-tw.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme/readme-zh-tw.md b/readme/readme-zh-tw.md index c6ddc025..ab4dd255 100644 --- a/readme/readme-zh-tw.md +++ b/readme/readme-zh-tw.md @@ -212,7 +212,7 @@ APIPark 使用 Apache 2.0 授權條款。更多詳情請參閱 LICENSE 文件。 如需企業級功能與專業技術支援,請聯絡我們的售前專家,獲取個性化演示、定制方案和報價。 - 網站: https://apipark.com -- 電子郵件: dev@apipark.com +- 電子郵件: contact@apipark.com
From 34446fae37c4c47d1415f58c8fe29c7c313edfe3 Mon Sep 17 00:00:00 2001 From: jeak Date: Fri, 17 Jan 2025 14:59:10 +0800 Subject: [PATCH 04/38] Update readme-jp.md --- readme/readme-jp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme/readme-jp.md b/readme/readme-jp.md index 6977a313..7f1602d0 100644 --- a/readme/readme-jp.md +++ b/readme/readme-jp.md @@ -211,7 +211,7 @@ APIParkはApache 2.0ライセンスの下で提供されています。詳細に エンタープライズ機能や専門的な技術サポートについては、プリセールスの専門家に連絡し、個別デモ、カスタムソリューション、価格情報を入手してください。 - ウェブサイト: https://apipark.com -- メール: dev@apipark.com +- メール: contact@apipark.com
From 6b1224f9f80b99031be865aecea11ea26fc58ace Mon Sep 17 00:00:00 2001 From: Liujian <824010343@qq.com> Date: Sat, 8 Feb 2025 14:58:36 +0800 Subject: [PATCH 05/38] Fix: AI provider's default key synchronization to Apinto failed issue --- module/ai/iml.go | 29 ++++++++++--------------- module/application-authorization/iml.go | 6 +++-- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/module/ai/iml.go b/module/ai/iml.go index 201ff398..ba4ee255 100644 --- a/module/ai/iml.go +++ b/module/ai/iml.go @@ -535,15 +535,15 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string, Priority: input.Priority, Status: &status, } - _, err = i.aiKeyService.DefaultKey(ctx, id) + _, err = i.aiKeyService.DefaultKey(txCtx, id) if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return err } - err = i.aiKeyService.Create(ctx, &ai_key.Create{ + err = i.aiKeyService.Create(txCtx, &ai_key.Create{ ID: id, Name: info.Name, - Config: info.Config, + Config: input.Config, Provider: id, Status: 1, ExpireTime: 0, @@ -551,28 +551,21 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string, Priority: 1, }) } else { - err = i.aiKeyService.Save(ctx, id, &ai_key.Edit{ - Config: &info.Config, + err = i.aiKeyService.Save(txCtx, id, &ai_key.Edit{ + Config: &input.Config, + Status: &status, }) } if err != nil { return err } - - //if input.Enable != nil { - // status = 0 - // if *input.Enable { - // status = 1 - // } - // pInfo.Status = &status - //} - err = i.providerService.Save(ctx, id, pInfo) + err = i.providerService.Save(txCtx, id, pInfo) if err != nil { return err } if *pInfo.Status == 0 { - return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{ + return i.syncGateway(txCtx, cluster.DefaultClusterID, []*gateway.DynamicRelease{ { BasicItem: &gateway.BasicItem{ ID: id, @@ -581,8 +574,8 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string, }, }, false) } - // 获取当前供应商所有Key信息 - defaultKey, err := i.aiKeyService.DefaultKey(ctx, id) + // 获取当前供应商默认Key信息 + defaultKey, err := i.aiKeyService.DefaultKey(txCtx, id) if err != nil { return err } @@ -592,7 +585,7 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string, cfg["model_config"] = model.DefaultConfig() cfg["priority"] = info.Priority cfg["base"] = fmt.Sprintf("%s://%s", p.URI().Scheme(), p.URI().Host()) - return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{ + return i.syncGateway(txCtx, cluster.DefaultClusterID, []*gateway.DynamicRelease{ { BasicItem: &gateway.BasicItem{ ID: id, diff --git a/module/application-authorization/iml.go b/module/application-authorization/iml.go index 9a42ec07..2dae5943 100644 --- a/module/application-authorization/iml.go +++ b/module/application-authorization/iml.go @@ -100,7 +100,8 @@ func (i *imlAuthorizationModule) getApplications(ctx context.Context, appIds []s Config: authCfg, HideCredential: a.HideCredential, Label: map[string]string{ - "authorization": a.UUID, + "authorization": a.UUID, + "authorization_name": a.Name, }, } }), @@ -157,7 +158,8 @@ func (i *imlAuthorizationModule) online(ctx context.Context, s *service.Service) Config: authCfg, HideCredential: a.HideCredential, Label: map[string]string{ - "authorization": a.UUID, + "authorization": a.UUID, + "authorization_name": a.Name, }, } }), From 18a3283cb4f82b5df177675febd0665cc5ee982c Mon Sep 17 00:00:00 2001 From: Liujian <824010343@qq.com> Date: Sat, 8 Feb 2025 15:57:36 +0800 Subject: [PATCH 06/38] update workflows actions/upload-artifact to v4 --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 49b80caa..2dc95396 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,7 +25,7 @@ jobs: echo "Build frontend..." cd ./frontend && pnpm run build - name: upload frontend release - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: frontend-package path: frontend/dist @@ -41,7 +41,7 @@ jobs: - name: Checkout #Checkout代码 uses: actions/checkout@v3 - name: download frontend release - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: frontend-package path: frontend/dist From 637044e99d1c33d7b740347e6af07153cc2d4fa0 Mon Sep 17 00:00:00 2001 From: Liujian <824010343@qq.com> Date: Sat, 8 Feb 2025 16:14:01 +0800 Subject: [PATCH 07/38] update workflows actions/download-artifact to v4 --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2dc95396..aa645b89 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@v3 - name: download frontend release - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: frontend-package path: frontend/dist From 0b2928eb3c455bbca11cd6664400436a939f3ae7 Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Sat, 8 Feb 2025 18:47:08 +0800 Subject: [PATCH 08/38] feat: Home Page AI Service Deployment --- .../packages/common/src/assets/localAI.svg | 3 + .../packages/common/src/assets/onlineAI.svg | 3 + .../packages/common/src/assets/restAPI.svg | 14 + .../src/components/aoplatform/BasicLayout.tsx | 2 +- frontend/packages/common/src/const/type.ts | 6 + .../packages/core/src/const/system/const.tsx | 7 + .../src/pages/aiSetting/AiSettingModal.tsx | 276 ++++++++-- .../core/src/pages/aiSetting/types.ts | 1 + .../core/src/pages/guide/AIModelGuide.tsx | 427 ++++++++++++++ .../packages/core/src/pages/guide/Guide.tsx | 519 ++++++++++-------- .../core/src/pages/system/SystemConfig.tsx | 1 - .../core/src/pages/system/SystemList.tsx | 33 +- .../serviceDeployment/ServiceDeployment.tsx | 44 ++ 13 files changed, 1071 insertions(+), 265 deletions(-) create mode 100644 frontend/packages/common/src/assets/localAI.svg create mode 100644 frontend/packages/common/src/assets/onlineAI.svg create mode 100644 frontend/packages/common/src/assets/restAPI.svg create mode 100644 frontend/packages/core/src/pages/guide/AIModelGuide.tsx create mode 100644 frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx diff --git a/frontend/packages/common/src/assets/localAI.svg b/frontend/packages/common/src/assets/localAI.svg new file mode 100644 index 00000000..ea2ac931 --- /dev/null +++ b/frontend/packages/common/src/assets/localAI.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/packages/common/src/assets/onlineAI.svg b/frontend/packages/common/src/assets/onlineAI.svg new file mode 100644 index 00000000..008a606f --- /dev/null +++ b/frontend/packages/common/src/assets/onlineAI.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/packages/common/src/assets/restAPI.svg b/frontend/packages/common/src/assets/restAPI.svg new file mode 100644 index 00000000..680b98f3 --- /dev/null +++ b/frontend/packages/common/src/assets/restAPI.svg @@ -0,0 +1,14 @@ + + + + + + REST + diff --git a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx index 931bf7f0..471d78b5 100644 --- a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx +++ b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx @@ -182,7 +182,7 @@ function BasicLayout({ project = 'core' }: { project: string }) { , ...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[]) || []) ] - }, [pluginSlotHub.getSlot('basicLayoutAfterBtns')]) + }, [state.language, pluginSlotHub.getSlot('basicLayoutAfterBtns')]) return (
[] = [ dataIndex: ['team', 'name'], ellipsis: true }, + { + title: '状态', + width: 140, + dataIndex: 'update_time', + // dataIndex: 'state', + ellipsis: true + }, { title: 'API 数量', dataIndex: 'apiNum', diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index a81a4c7d..f0df517d 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -4,31 +4,50 @@ import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/ 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 { AiProviderLlmsItems, ModelDetailData } from './types' +import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react' +import { AiProviderLlmsItems, ModelDetailData, AiSettingListItem } from './types' +import { MemberItem, SimpleTeamItem } from '@common/const/type' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' export type AiSettingModalContentProps = { entity: ModelDetailData & { defaultLlm: string } readOnly: boolean + modelMode?: 'auto' | 'manual' + /** 如果是手动选择 AI 模型,那么需要更新 footer 底部的内容,所以需要这个方法去更新外部的 footer */ + updateEntityData: (entity: ModelDetailData & { defaultLlm: string }) => void } export type AiSettingModalContentHandle = { save: () => Promise + deployAIServer: () => Promise } const AiSettingModalContent = forwardRef((props, ref) => { const [form] = Form.useForm() const { message } = App.useApp() - const { entity, readOnly } = props + const { entity, readOnly, modelMode = 'auto', updateEntityData } = props const { fetchData } = useFetch() const [llmList, setLlmList] = useState() const [loading, setLoading] = useState(false) - const [enableState, setEnableState] = useState(entity.status === 'enabled') - const getLlmList = () => { + // AI 模型配置 + const [localEntity, setLocalEntity] = useState(entity) + const [teamList, setTeamList] = useState([]) + // AI 模型提供商列表 + const modelProviderListRef = useRef([]) + // 模型模式加载 + const [modelModeLoading, setModelModeLoading] = useState(false) + const [enableState, setEnableState] = useState(localEntity?.status === 'enabled') + const { checkPermission } = useGlobalContext() + + /** + * 获取 llm 列表 + * @param id + */ + const getLlmList = (id?: string) => { setLoading(true) fetchData>(`ai/provider/llms`, { method: 'GET', - eoParams: { provider: entity.id } + eoParams: { provider: id || localEntity.id } }) .then((response) => { const { code, data, msg } = response @@ -43,25 +62,159 @@ const AiSettingModalContent = forwardRef { - getLlmList() + /** + * 获取团队选项列表 + * @returns + */ + const getTeamOptionList = async (): any[] => { + const response = await fetchData>( + !checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams', + { method: 'GET', eoTransformKeys: [] } + ) + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + const teamOptionList = data.teams?.map((x: MemberItem) => { + return { ...x, label: x.name, value: x.id } + }) + setTeamList(teamOptionList) + if (form.getFieldValue('team') === undefined && data.teams?.length) { + form.setFieldValue('team', data.teams[0].id) + } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return [] + } + } + + /** + * 获取未配置模型提供者列表 + */ + const getModelProviderList = () => { + setModelModeLoading(true) + fetchData>(`ai/providers/unconfigured`, { + method: 'GET', + eoTransformKeys: ['default_llm', 'default_llm_logo'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + const providers = data.providers || [] + modelProviderListRef.current = providers + if (providers.length) { + const id = providers[0].id + form.setFieldValue('modelMode', id) + getModelConfig(id) + } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .finally(() => { + setModelModeLoading(false) + }) + } + + /** + * 获取模型配置 + * @param id + */ + const getModelConfig = (id: string) => { + getLlmList(id) + fetchData>(`ai/provider/config`, { + method: 'GET', + eoParams: { provider: id }, + eoTransformKeys: ['get_apikey_url'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + const modelEntity = { + ...data.provider, + defaultLlm: modelProviderListRef.current.find((x) => x.id === id)?.defaultLlm + } + setLocalEntity(modelEntity) + setFormFieldsValue(modelEntity) + updateEntityData(modelEntity) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .finally(() => { + setModelModeLoading(false) + }) + } + + /** + * 设置表单字段值 + * @param fieldsValue + */ + const setFormFieldsValue = (fieldsValue: any) => { try { form.setFieldsValue({ - defaultLlm: entity.defaultLlm, - config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : '', - priority: entity.priority || 1, - enable: entity.status === 'enabled' + defaultLlm: fieldsValue.defaultLlm, + config: fieldsValue!.config ? JSON.stringify(JSON.parse(fieldsValue!.config), null, 2) : '', + priority: fieldsValue.priority || 1, + enable: fieldsValue.status === 'enabled' }) } catch (e) { form.setFieldsValue({ - defaultLlm: entity.defaultLlm, + defaultLlm: localEntity.defaultLlm, config: '', priority: 1, enable: true }) } + } + useEffect(() => { + // 如果是直接在 AI 模型配置,则获取默认模型列表和团队列表 + if (modelMode === 'auto') { + getLlmList() + setFormFieldsValue(localEntity) + } else { + getModelProviderList() + getTeamOptionList() + } }, []) + /** + * 部署 AI 服务 + */ + const deployAIServer: () => Promise = () => { + return new Promise((resolve, reject) => { + form + .validateFields() + .then((value) => { + const finalValue = { + config: value.config, + model: value.defaultLlm, + team: value.team, + provider: localEntity?.id + } + console.log(finalValue) + fetchData>('quick/service/ai', { + method: 'POST', + 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)) + }) + } + + /** + * 保存 + * @returns + */ const save: () => Promise = () => { return new Promise((resolve, reject) => { form @@ -74,7 +227,7 @@ const AiSettingModalContent = forwardRef>('ai/provider/config', { method: 'PUT', - eoParams: { provider: entity?.id }, + eoParams: { provider: localEntity?.id }, eoBody: finalValue, eoTransformKeys: ['defaultLlm'] // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' @@ -103,7 +256,8 @@ const AiSettingModalContent = forwardRef ({ - save + save, + deployAIServer })) return ( @@ -117,6 +271,26 @@ const AiSettingModalContent = forwardRef + {modelMode === 'manual' && ( + label={$t('模型来源')} name="modelMode" rules={[{ required: true }]}> + + + )} label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}> - - - label={ - - {$t('负载优先级')} - - - - - } - name="priority" - rules={[ - { required: true }, - { - validator: async (_, value) => { - if (value <= 0) { - throw new Error($t('优先级必须大于 0')) - } - return Promise.resolve() - } + {modelMode === 'auto' && ( + + label={ + + {$t('负载优先级')} + + + + } - ]} - initialValue={1} - > - - + name="priority" + rules={[ + { required: true }, + { + validator: async (_, value) => { + if (value <= 0) { + throw new Error($t('优先级必须大于 0')) + } + return Promise.resolve() + } + } + ]} + initialValue={1} + > + + + )} + {modelMode === 'manual' && ( + + + + )} label={$t('API Key(默认 Key)')} name="config"> - {entity.configured && ( + {localEntity?.configured && (
{$t('当前调用状态:')} - {entity.status === 'enabled' && {$t('正常')}} - {entity.status === 'disabled' && {$t('停用')}} - {entity.status === 'abnormal' && {$t('异常')}} + {localEntity.status === 'enabled' && {$t('正常')}} + {localEntity.status === 'disabled' && {$t('停用')}} + {localEntity.status === 'abnormal' && {$t('异常')}}
- {(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? ( + {(localEntity.status === 'enabled' && !enableState) || (localEntity.status !== 'enabled' && enableState) ? (
* {getTooltipText(enableState)}
) : null}
diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index 6ba2ea7a..6a8a4598 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -12,6 +12,7 @@ export interface ModelListData { name: string logo: string defaultLlm: string + modelMode?: string status: ModelStatus api_count: number key_count: number diff --git a/frontend/packages/core/src/pages/guide/AIModelGuide.tsx b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx new file mode 100644 index 00000000..53c2288b --- /dev/null +++ b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx @@ -0,0 +1,427 @@ +import restAPIPic from '@common/assets/restAPI.svg' +import onlineAIPic from '@common/assets/onlineAI.svg' +import localAIPic from '@common/assets/localAI.svg' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { $t } from '@common/locales' +import { Icon } from '@iconify/react/dist/iconify.js' +import { App, Upload, UploadProps, Form, message, Select } from 'antd' +import { Card } from 'antd' +import { useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import WithPermission from '@common/components/aoplatform/WithPermission' +import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' +import { LocalModelItem, MemberItem, SimpleTeamItem } from '@common/const/type' +import AiSettingModalContent, { AiSettingModalContentHandle } from '../aiSetting/AiSettingModal' +import { checkAccess } from '@common/utils/permission' +const { Dragger } = Upload +export const AIModelGuide = () => { + const { modal } = App.useApp() + const [, forceUpdate] = useState(null) + const [form] = Form.useForm() + const entityData = useRef(null) + const { fetchData } = useFetch() + const navigateTo = useNavigate() + const { checkPermission, accessData } = useGlobalContext() + const modalRef = useRef() + + /** + * 获取 team 选项列表 + * @returns + */ + const getTeamOptionList = async (): any[] => { + const response = await fetchData>( + !checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams', + { method: 'GET', eoTransformKeys: [] } + ) + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + const teamOptionList = data.teams?.map((x: MemberItem) => { + return { ...x, label: x.name, value: x.id } + }) + if (form.getFieldValue('team') === undefined && data.teams?.length) { + form.setFieldValue('team', data.teams[0].id) + } + return teamOptionList + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return [] + } + } + + /** + * 部署 rest 服务 + * @param file + * @returns + */ + const deployRestServer = async (file: File) => { + return new Promise((resolve, reject) => { + const formData = new FormData() + formData.append('file', file) + formData.append('type', file.type) + formData.append('team', form.getFieldValue('team')) + fetchData>('quick/service/rest', { + method: 'POST', + body: formData + }).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(false) + } + }) + }) + } + + const deployPopularModel = async (id: string, modalInstance: any) => { + await deployLocalModel({ + modelID: id, + team: form.getFieldValue('team') + }) + modalInstance.destroy() + navigateTo(`/service/list`) + } + + /** + * 部署本地模型 + * @param value + * @returns + */ + const deployLocalModel = (value: { modelID: string; team?: number }) => { + return new Promise((resolve, reject) => { + const finalValue = { + model: value.modelID, + team: value?.team + } + console.log(finalValue) + fetchData>('model/local/deploy', { + method: 'POST', + 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(false) + } + }) + .catch((errorInfo) => reject(errorInfo)) + }) + } + + /** + * 获取本地模型列表 + * @returns 本地模型列表 + */ + const getLocalModelList = async (): any[] => { + const response = await fetchData>( + 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/model/local/can_deploy', + // 'model/local/can_deploy' + { method: 'GET', custom: true, eoTransformKeys: ['is_popular'] } + ) + // TODO_数据模拟 + if (response.ok) { + const datas = await response.json() + const { code, data, msg } = datas + if (code === STATUS_CODE.SUCCESS) { + const modelList = data.models?.map((x: LocalModelItem) => { + return { ...x, label: x.name, value: x.id } + }) + return modelList + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return [] + } + } else { + console.error('HTTP error', response.status) + } + // const { code, data, msg } = response + // if (code === STATUS_CODE.SUCCESS) { + // const modelList = data.models?.map((x: LocalModelItem) => { + // return { ...x, label: x.name, value: x.id } + // }) + // console.log('modelList===', modelList); + + // return modelList + // } else { + // message.error(msg || $t(RESPONSE_TIPS.error)) + // return [] + // } + } + + /** + * rest 服务卡片点击事件 + */ + const restCardClick = async () => { + form.resetFields() + const teamList = await getTeamOptionList() + const props: UploadProps = { + name: 'file', + multiple: false, + maxCount: 1, + beforeUpload: (file) => { + form.setFieldsValue({ key: file }) + forceUpdate({}) + return false + } + } + + modal.confirm({ + title: $t('添加 Rest 服务'), + content: ( + +
+ + +

+ +

+

{$t('选择 OpenAPI 文件 (.json / .yaml)')}

+
+
+ + + +
+
+ ), + onOk: () => { + return new Promise((resolve, reject) => { + form + .validateFields() + .then(async (value) => { + await deployRestServer(value.key.file) + resolve(true) + navigateTo(`/service/list`) + }) + .catch((errorInfo) => reject(errorInfo)) + }) + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + /** + * AI 模型配置弹窗 + */ + const aiCardClick = () => { + // 更新弹窗 + const updateEntityData = (data: any) => { + entityData.current = data + // 更新弹窗 + modalInstance.update({}) + } + const modalInstance = modal.confirm({ + title: $t('模型配置'), + content: ( + + ), + onOk: () => { + return modalRef.current?.deployAIServer().then((res) => { + if (res === true) { + navigateTo(`/service/list`) + } + }) + }, + width: 600, + okText: $t('确认'), + footer: (_, { OkBtn, CancelBtn }) => { + return ( +
+ + {$t('从 (0) 获取 API KEY', [entityData.current?.name])} + + +
+ + {checkAccess('system.devops.ai_provider.edit', accessData) ? : null} +
+
+ ) + }, + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + /** + * 本地部署 AI 并生成 API + */ + const localModelCardClick = async () => { + form.resetFields() + const teamList = await getTeamOptionList() + const modelList = await getLocalModelList() + const modalInstance = modal.confirm({ + title: $t('部署 AI 模型'), + content: ( + +
+ + +
+ + {$t('热点模型')} +
+
+ {modelList.length && + modelList + .filter((item) => item.is_popular) + .map((item) => ( + { + deployPopularModel(item.id, modalInstance) + }} + > + {item.name} + + ))} +
+
+ + + +
+
+ ), + onOk: () => { + return new Promise((resolve, reject) => { + form + .validateFields() + .then(async (value) => { + await deployLocalModel(value) + resolve(true) + navigateTo(`/service/list`) + }) + .catch((errorInfo) => reject(errorInfo)) + }) + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + const deployDeepSeek = (e: any) => { + e.stopPropagation() + deployLocalModel({ + modelID: 'deepseek-r1' + }) + } + + const cardList = [ + { + imgSrc: restAPIPic, + title: $t('添加 Rest 服务'), + description: $t('支持批量添加现有 API 文档以实现统一的外部访问。'), + click: restCardClick + }, + { + imgSrc: onlineAIPic, + title: $t('添加在线 AI API'), + description: $t('快速调用 AI 模型的云服务 API,方便管理提示词和统一计费。'), + click: aiCardClick + }, + { + imgSrc: localAIPic, + title: $t('本地部署 AI 并生成 API'), + description: $t('快速在本地部署开源模型并自动生成 API。'), + click: localModelCardClick, + bottomRender: ( + + + {$t('部署')} Deepseek-R1 + + ) + } + ] + return ( +
+ {cardList.map((item, itemIndex) => ( + + +

{item.title}

+

{item.description}

+ {item.bottomRender ? item.bottomRender : null} +
+ ))} +
+ ) +} diff --git a/frontend/packages/core/src/pages/guide/Guide.tsx b/frontend/packages/core/src/pages/guide/Guide.tsx index 9e79f2e8..ed8a085c 100644 --- a/frontend/packages/core/src/pages/guide/Guide.tsx +++ b/frontend/packages/core/src/pages/guide/Guide.tsx @@ -1,232 +1,321 @@ -import InsidePage from "@common/components/aoplatform/InsidePage" -import { useGlobalContext } from "@common/contexts/GlobalStateContext" -import { $t } from "@common/locales" -import { Icon } from "@iconify/react/dist/iconify.js" -import { Button, Card, Collapse } from "antd" -import { Dispatch, SetStateAction, useEffect, useState } from "react" -import { useLocation, useNavigate } from "react-router-dom" +import InsidePage from '@common/components/aoplatform/InsidePage' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { $t } from '@common/locales' +import { Icon } from '@iconify/react/dist/iconify.js' +import { Button, Card, Collapse } from 'antd' +import { Dispatch, SetStateAction, useEffect, useState } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import { AIModelGuide } from './AIModelGuide' -export default function Guide(){ - const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false' ) - const [showAdvancedGuide, setShowAdvancedGuide] = useState(localStorage.getItem('showAdvancedGuide') !== 'false' ) - const [, forceUpdate] = useState(null); - const {state} = useGlobalContext() - const location = useLocation() - const currentUrl = location.pathname - const navigator = useNavigate() - const guideSections = [ +export default function Guide() { + const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false') + const [showAdvancedGuide, setShowAdvancedGuide] = useState(localStorage.getItem('showAdvancedGuide') !== 'false') + const [, forceUpdate] = useState(null) + const { state } = useGlobalContext() + const location = useLocation() + const currentUrl = location.pathname + const navigator = useNavigate() + const guideSections = [ + { + title: $t('快速接入 AI'), + items: [ { - title: $t('快速接入 AI'), - items: [ - { - title: $t("配置你的 AI 模型"), - description: $t('通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。'), - link: 'https://docs.apipark.com/docs/system_setting/ai_model_providers' - }, - { - title: $t("创建 AI 服务和 API"), - description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'), - link: 'https://docs.apipark.com/docs/services/ai_services' - }, - { - title: $t("创建调用 Token"), - description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'), - link: 'https://docs.apipark.com/docs/consumers' - }, - { - title: $t("调用"), - description: $t('现在你可以通过 Token 来调用这些 API。'), - link: 'https://docs.apipark.com/docs/call_api' - } - ] + title: $t('配置你的 AI 模型'), + description: $t('通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。'), + link: 'https://docs.apipark.com/docs/system_setting/ai_model_providers' }, { - title: $t('快速接入 REST API'), - items: [ - { - title: $t("创建 REST 服务和 API"), - description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'), - link: 'https://docs.apipark.com/docs/services/rest_services' - }, - { - title: $t("创建调用 Token"), - description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'), - link: 'https://docs.apipark.com/docs/consumers' - }, - { - title: $t("调用"), - description: $t('现在你可以通过 Token 来调用这些 API。'), - link: 'https://docs.apipark.com/docs/call_api' - } - ] + title: $t('创建 AI 服务和 API'), + description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'), + link: 'https://docs.apipark.com/docs/services/ai_services' }, { - title: $t('仪表盘'), - items: [ - { - title: $t("统计 API 调用情况"), - description: $t('仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。'), - link: 'https://docs.apipark.com/docs/analysis' - } - ] + title: $t('创建调用 Token'), + description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'), + link: 'https://docs.apipark.com/docs/consumers' + }, + { + title: $t('调用'), + description: $t('现在你可以通过 Token 来调用这些 API。'), + link: 'https://docs.apipark.com/docs/call_api' } - ]; - const advanceGuideSections = [ + ] + }, + { + title: $t('快速接入 REST API'), + items: [ { - title: $t('核心功能'), - items: [ - { - title: $t("账号与角色"), - description: $t('邀请你的团队成员加入 APIPark,共同管理和调用 API。'), - link: 'https://docs.apipark.com/docs/system_setting/account_role' - }, - { - title: $t("团队"), - description: $t('团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。'), - link: 'https://docs.apipark.com/docs/teams' - }, - { - title: $t("服务"), - description: $t('服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。'), - link: 'https://docs.apipark.com/docs/category/-%E6%9C%8D%E5%8A%A1' - } - ] + title: $t('创建 REST 服务和 API'), + description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'), + link: 'https://docs.apipark.com/docs/services/rest_services' }, { - title: $t('权限管理'), - items: [ - { - title: $t("订阅服务"), - description: $t('如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。'), - link: 'https://docs.apipark.com/docs/developer_portal' - }, - { - title: $t("审核订阅申请"), - description: $t('提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。'), - link: 'https://docs.apipark.com/docs/services/review_consumers' - } - ] + title: $t('创建调用 Token'), + description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'), + link: 'https://docs.apipark.com/docs/consumers' }, { - title: $t('集成'), - items: [ - { - title: $t("日志"), - description: $t('APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。'), - link: 'https://docs.apipark.com/docs/system_setting/log/' - } - ] + title: $t('调用'), + description: $t('现在你可以通过 Token 来调用这些 API。'), + link: 'https://docs.apipark.com/docs/call_api' } - ]; - useEffect(()=>{ - localStorage.setItem('showGuide', showGuide.toString()) - },[showGuide]) - useEffect(()=>{ - localStorage.setItem('showAdvancedGuide', showAdvancedGuide.toString()) - },[showAdvancedGuide]) - - useEffect(()=>{ - if(currentUrl === '/guide'){ - setTimeout(()=>{ - navigator('/guide/page') - },0) + ] + }, + { + title: $t('仪表盘'), + items: [ + { + title: $t('统计 API 调用情况'), + description: $t('仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。'), + link: 'https://docs.apipark.com/docs/analysis' } - },[]) - useEffect(()=>{forceUpdate({})},[state.language]) - return ( - - 👋 - {$t('Hello!欢迎使用 APIPark')} - -
} - description={
-

{$t("你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。")}

-

{$t("如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。")}

-
} - showBorder={false} - scrollPage={false} - contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B" - > -
- {showGuide && - -

- 🚀{`${$t('快速入门')}`}

-

{$t("我们提供了一些任务来帮你快速了解 APIPark")}

, - children: }]} - />} - {showAdvancedGuide && - -

- 🏍️{`${$t('进阶教程')}`}

-

{$t("了解 APIPark 如何更好地管理 API 和 AI")}

, - children: }]} - />} - - - ) + ] + } + ] + const advanceGuideSections = [ + { + title: $t('核心功能'), + items: [ + { + title: $t('账号与角色'), + description: $t('邀请你的团队成员加入 APIPark,共同管理和调用 API。'), + link: 'https://docs.apipark.com/docs/system_setting/account_role' + }, + { + title: $t('团队'), + description: $t( + '团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。' + ), + link: 'https://docs.apipark.com/docs/teams' + }, + { + title: $t('服务'), + description: $t('服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。'), + link: 'https://docs.apipark.com/docs/category/-%E6%9C%8D%E5%8A%A1' + } + ] + }, + { + title: $t('权限管理'), + items: [ + { + title: $t('订阅服务'), + description: $t( + '如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。' + ), + link: 'https://docs.apipark.com/docs/developer_portal' + }, + { + title: $t('审核订阅申请'), + description: $t('提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。'), + link: 'https://docs.apipark.com/docs/services/review_consumers' + } + ] + }, + { + title: $t('集成'), + items: [ + { + title: $t('日志'), + description: $t('APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。'), + link: 'https://docs.apipark.com/docs/system_setting/log/' + } + ] + } + ] + useEffect(() => { + localStorage.setItem('showGuide', showGuide.toString()) + }, [showGuide]) + useEffect(() => { + localStorage.setItem('showAdvancedGuide', showAdvancedGuide.toString()) + }, [showAdvancedGuide]) + + useEffect(() => { + if (currentUrl === '/guide') { + setTimeout(() => { + navigator('/guide/page') + }, 0) + } + }, []) + useEffect(() => { + forceUpdate({}) + }, [state.language]) + return ( + + 👋 + {$t('Hello!欢迎使用 APIPark')} + + } + description={ +
+

+ {$t( + '你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。' + )} +

+

+ {$t('如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。')} + {$t('点击这里')} + +   + +   + + + + + +   + +   + + {$t('点击')} +   + + + + Star +

+
+ } + showBorder={false} + scrollPage={false} + contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B" + > + +
+ {showGuide && ( + +

+ 🚀 + {`${$t('快速入门')}`}{' '} +

+

{$t('我们提供了一些任务来帮你快速了解 APIPark')}

+
+ ), + children: + } + ]} + /> + )} + {showAdvancedGuide && ( + +

+ 🏍️ + {`${$t('进阶教程')}`}{' '} +

+

{$t('了解 APIPark 如何更好地管理 API 和 AI')}

+ + ), + children: ( + + ) + } + ]} + /> + )} + +
+ ) } -const QuickGuideContent = ({changeGuideShow,guideSections}:{changeGuideShow:Dispatch>,guideSections: { - title: string; +const QuickGuideContent = ({ + changeGuideShow, + guideSections +}: { + changeGuideShow: Dispatch> + guideSections: { + title: string items: { - title: string; - description: string; - link: string; - }[]; -}[]})=>{ - - - return (<> -
- {guideSections.map((section, index) => ( -
-

- - {section.title} -

-
-
- {section.items.map((item, itemIndex) => ( - { window.open(item.link, '_blank') }} - > - {item.description} - - ))} -
-
-
- ))} -

- -

- - -
-

-
- ) -} \ No newline at end of file + title: string + description: string + link: string + }[] + }[] +}) => { + return ( + <> +
+ {guideSections.map((section, index) => ( +
+

+ + {section.title} +

+
+
+ {section.items.map((item, itemIndex) => ( + { + window.open(item.link, '_blank') + }} + > + {item.description} + + ))} +
+
+
+ ))} +
+ +
+ + +
+
+
+ + ) +} diff --git a/frontend/packages/core/src/pages/system/SystemConfig.tsx b/frontend/packages/core/src/pages/system/SystemConfig.tsx index 55a833da..b449c81c 100644 --- a/frontend/packages/core/src/pages/system/SystemConfig.tsx +++ b/frontend/packages/core/src/pages/system/SystemConfig.tsx @@ -440,7 +440,6 @@ const SystemConfig = forwardRef((_, ref) => { diff --git a/frontend/packages/core/src/pages/system/SystemList.tsx b/frontend/packages/core/src/pages/system/SystemList.tsx index e397ab58..5c08f906 100644 --- a/frontend/packages/core/src/pages/system/SystemList.tsx +++ b/frontend/packages/core/src/pages/system/SystemList.tsx @@ -14,6 +14,7 @@ import { useNavigate } from 'react-router-dom' import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/const.tsx' import { SystemConfigHandle, SystemTableListItem } from '../../const/system/type.ts' import SystemConfig from './SystemConfig.tsx' +import { ServiceDeployment } from './serviceDeployment/ServiceDeployment.tsx' const SystemList: FC = () => { const navigate = useNavigate() @@ -23,7 +24,7 @@ const SystemList: FC = () => { const { fetchData } = useFetch() const [tableListDataSource, setTableListDataSource] = useState([]) const [tableHttpReload, setTableHttpReload] = useState(true) - const { message } = App.useApp() + const { message, modal } = App.useApp() const pageListRef = useRef(null) const [memberValueEnum, setMemberValueEnum] = useState<{ [k: string]: { text: string } }>({}) const [open, setOpen] = useState(false) @@ -128,7 +129,22 @@ const SystemList: FC = () => { const onClose = () => { setOpen(false) } + const openLogsModal = (record: any) => { + console.log('record', record) + modal.confirm({ + title: $t('部署过程'), + content: , + onOk: () => { + console.log('ok') + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } const columns = useMemo(() => { const res = SYSTEM_TABLE_COLUMNS.map((x) => { const dataIndex = x.dataIndex as string[] @@ -145,6 +161,21 @@ const SystemList: FC = () => { ;(x.valueEnum as any)[option.value] = { text: $t(option.label) } }) } + if ((x.dataIndex as string) === 'update_time') { + x.render = (text: any, record: any) => ( + .ant-typography]:text-[#2196f3]' : ''}`} + onClick={(e) => { + if (record.can_delete) { + e?.stopPropagation(); + openLogsModal(record) + } + }} + > + {text} + + ) + } return { ...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title } }) diff --git a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx new file mode 100644 index 00000000..7f75a6e9 --- /dev/null +++ b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx @@ -0,0 +1,44 @@ +import { SystemTableListItem } from '@core/const/system/type' +import type { StepsProps } from 'antd' +import { Popover, Steps } from 'antd' +import { CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons' + +const customDot: StepsProps['progressDot'] = (dot, { status, index }) => ( + + step {index} status: {status} + + } + > + {dot} + +) + +export const ServiceDeployment = (props: { record: SystemTableListItem }) => { + const { record } = props + console.log('record', record) + + const items = [ + { + title: 'Download', + description: '4.7 GB / 4.7 GB' + }, + { + title: 'Deploy', + }, + { + title: 'Initializing', + } + ] + return ( +
+ {/* */} + +
+ ) +} From 8ce65cbe3d951baea82a55f6c1940637dab1e51d Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Mon, 10 Feb 2025 17:03:28 +0800 Subject: [PATCH 09/38] feat: Deployment Progress Popup Development --- .../components/postcat/api/Codebox/index.tsx | 11 +- frontend/packages/common/src/hooks/http.ts | 40 ++++- .../packages/core/src/const/system/const.tsx | 3 +- frontend/packages/core/src/index.css | 6 + .../core/src/pages/guide/AIModelGuide.tsx | 12 +- .../core/src/pages/system/SystemList.tsx | 17 +- .../ServiceDeployMentFooter.tsx | 93 ++++++++++ .../serviceDeployment/ServiceDeployment.tsx | 167 +++++++++++++++--- 8 files changed, 295 insertions(+), 54 deletions(-) create mode 100644 frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployMentFooter.tsx diff --git a/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx b/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx index 7cf4ec9c..73e2b399 100644 --- a/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx +++ b/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx @@ -26,7 +26,8 @@ interface CodeboxProps { language?: codeBoxLanguagesType extraContent?: React.ReactNode sx?: Record - editorTheme?: 'vs' | 'vs-dark' | 'hc-black' + editorTheme?: 'vs' | 'vs-dark' | 'hc-black', + autoScrollToEnd?: boolean } export const Codebox = memo((props: CodeboxProps) => { @@ -41,7 +42,8 @@ export const Codebox = memo((props: CodeboxProps) => { readOnly = false, language = 'plaintext', extraContent, - editorTheme = 'vs' + editorTheme = 'vs', + autoScrollToEnd = false } = props const [code, setCode] = useState(``) @@ -120,6 +122,11 @@ export const Codebox = memo((props: CodeboxProps) => { const editorDidMount = (editor: MonacoEditor.IStandaloneCodeEditor): void => { editorRef.current = editor + autoScrollToEnd && editor.onDidChangeModelContent(() => { + const model = editor.getModel() + const lineCount = model.getLineCount() + editor.revealLine(lineCount) + }) } const formatCode = async (): Promise => { diff --git a/frontend/packages/common/src/hooks/http.ts b/frontend/packages/common/src/hooks/http.ts index 6688fdad..b02b4e10 100644 --- a/frontend/packages/common/src/hooks/http.ts +++ b/frontend/packages/common/src/hooks/http.ts @@ -129,11 +129,13 @@ const DEFAULT_HEADERS = { namespace: 'default' } -type EoRequest = RequestInit & { +export type EoRequest = RequestInit & { eoParams?: { [k: string]: unknown } eoTransformKeys?: string[] eoApiPrefix?: string eoBody?: { [k: string]: unknown } | Array | string + isStream?: boolean + handleStream?: (line: any) => void } type EoHeaders = Headers | { [k: string]: string } @@ -186,14 +188,36 @@ export function useFetch() { throw new Error(`HTTP error! status: ${response.status}`) } - // 如果响应体为JSON且指定了转换键,则转换响应数据 - if (options?.eoApiPrefix||isJsonHttp(response.headers)) { - const data = await response.json() - const newData = (await pluginEventHub.emit('httpResponse', { data, continue: true })) as Response - return shouldTransformKeys ? (keysToCamel(newData, options.eoTransformKeys as string[]) as T) : data - } + if (options?.isStream) { + const reader = response.body?.getReader() + const decoder = new TextDecoder('utf-8') + let buffer = '' + if (reader) { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() || '' + for (const line of lines) { + options?.handleStream?.(line) + } + } - return response + if (buffer) { + options?.handleStream?.(buffer) + } + } + } else { + // 如果响应体为JSON且指定了转换键,则转换响应数据 + if (options?.eoApiPrefix || isJsonHttp(response.headers)) { + const data = await response.json() + const newData = (await pluginEventHub.emit('httpResponse', { data, continue: true })) as Response + return shouldTransformKeys ? (keysToCamel(newData, options.eoTransformKeys as string[]) as T) : data + } + + return response + } }) .catch((error) => { // 全局错误处理 diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index a3090477..bfd95cf1 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -111,8 +111,7 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns[] = [ { title: '状态', width: 140, - dataIndex: 'update_time', - // dataIndex: 'state', + dataIndex: 'state', ellipsis: true }, { diff --git a/frontend/packages/core/src/index.css b/frontend/packages/core/src/index.css index 38b3f917..fe0145b5 100644 --- a/frontend/packages/core/src/index.css +++ b/frontend/packages/core/src/index.css @@ -746,6 +746,12 @@ p{ padding:16px 20px !important } } +.custom-steps .ant-steps-icon span { + width: auto !important; +} +.custom-steps .ant-steps-item-content { + margin-top: 0 !important; +} .ant-modal-body .pr-PAGE_INSIDE_X{ diff --git a/frontend/packages/core/src/pages/guide/AIModelGuide.tsx b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx index 53c2288b..7e88505f 100644 --- a/frontend/packages/core/src/pages/guide/AIModelGuide.tsx +++ b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx @@ -92,14 +92,12 @@ export const AIModelGuide = () => { */ const deployLocalModel = (value: { modelID: string; team?: number }) => { return new Promise((resolve, reject) => { - const finalValue = { - model: value.modelID, - team: value?.team - } - console.log(finalValue) - fetchData>('model/local/deploy', { + fetchData>('model/local/deploy/start', { method: 'POST', - eoBody: finalValue + eoBody: { + model: value.modelID, + team: value?.team + } }) .then((response) => { const { code, msg } = response diff --git a/frontend/packages/core/src/pages/system/SystemList.tsx b/frontend/packages/core/src/pages/system/SystemList.tsx index 5c08f906..683b16d0 100644 --- a/frontend/packages/core/src/pages/system/SystemList.tsx +++ b/frontend/packages/core/src/pages/system/SystemList.tsx @@ -15,6 +15,7 @@ import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/c import { SystemConfigHandle, SystemTableListItem } from '../../const/system/type.ts' import SystemConfig from './SystemConfig.tsx' import { ServiceDeployment } from './serviceDeployment/ServiceDeployment.tsx' +import { LogsFooter } from './serviceDeployment/ServiceDeployMentFooter.tsx' const SystemList: FC = () => { const navigate = useNavigate() @@ -130,13 +131,11 @@ const SystemList: FC = () => { setOpen(false) } const openLogsModal = (record: any) => { - console.log('record', record) - - modal.confirm({ + const modalInstance = modal.confirm({ title: $t('部署过程'), content: , - onOk: () => { - console.log('ok') + footer: () => { + return }, width: 600, okText: $t('确认'), @@ -161,13 +160,13 @@ const SystemList: FC = () => { ;(x.valueEnum as any)[option.value] = { text: $t(option.label) } }) } - if ((x.dataIndex as string) === 'update_time') { + if ((x.dataIndex as string) === 'state') { x.render = (text: any, record: any) => ( .ant-typography]:text-[#2196f3]' : ''}`} + className={`text-[13px] ${record.state === 'deploying' ? '[&>.ant-typography]:text-[#2196f3]' : record.state === 'error' ? '[&>.ant-typography]:text-[#ff4d4f]' : ''}`} onClick={(e) => { - if (record.can_delete) { - e?.stopPropagation(); + if (['deploying', 'error'].includes(record.state)) { + e?.stopPropagation() openLogsModal(record) } }} diff --git a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployMentFooter.tsx b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployMentFooter.tsx new file mode 100644 index 00000000..b121329b --- /dev/null +++ b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployMentFooter.tsx @@ -0,0 +1,93 @@ +import { App, Button } from 'antd' +import { $t } from '@common/locales/index.ts' +import { useFetch } from '@common/hooks/http.ts' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' + +export const LogsFooter = (props: any) => { + const { record, modalInstance } = props + const { message, modal } = App.useApp() + const { fetchData } = useFetch() + const stopDeploy = () => { + modal.confirm({ + title: $t('停止部署'), + content: $t('确定停止部署吗?'), + onOk: () => { + return new Promise((resolve, reject) => { + fetchData>('model/local/cancel_deploy', { + method: 'POST', + eoBody: { recordId: record.id } + }) + .then((response) => { + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + resolve(true) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + reject(false) + } + }) + .finally(() => { + resolve(true) + modalInstance.destroy() + }) + }) + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + const deleteService = () => { + modal.confirm({ + title: $t('删除服务'), + content: $t('确定删除服务吗?'), + onOk: () => { + return new Promise((resolve, reject) => { + fetchData>('model/local', { + method: 'DELETE', + eoBody: { recordId: record.id } + }) + .then((response: BasicResponse) => { + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + resolve(true) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + reject(false) + } + }) + .finally(() => { + resolve(true) + modalInstance.destroy() + }) + }) + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + return ( + <> + {record.state === 'error' ? ( +
+ + +
+ ) : ( +
+ + +
+ )} + + ) +} diff --git a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx index 7f75a6e9..485a12cb 100644 --- a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx +++ b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx @@ -1,44 +1,159 @@ import { SystemTableListItem } from '@core/const/system/type' -import type { StepsProps } from 'antd' -import { Popover, Steps } from 'antd' -import { CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons' +import { Steps } from 'antd' +import { CheckCircleOutlined, LoadingOutlined, ClockCircleOutlined, CloseCircleOutlined } from '@ant-design/icons' +import { Codebox } from '@common/components/postcat/api/Codebox' +import { Collapse } from 'antd' +import { useEffect, useState } from 'react' +import { useFetch } from '@common/hooks/http' -const customDot: StepsProps['progressDot'] = (dot, { status, index }) => ( - - step {index} status: {status} -
- } - > - {dot} - -) +const getIcon = (status: string) => { + switch (status) { + case 'completed': + return + case 'inProgress': + return + case 'pending': + return + case 'error': + return + default: + return null + } +} export const ServiceDeployment = (props: { record: SystemTableListItem }) => { const { record } = props - console.log('record', record) - const items = [ + const [stepItem, setStepItem] = useState< + { + title: string + description?: string + status?: string + }[] + >([ { title: 'Download', - description: '4.7 GB / 4.7 GB' + status: 'pending' }, { title: 'Deploy', + status: 'pending' }, { title: 'Initializing', + status: 'pending' } - ] + ]) + + const [scriptStr, setScriptStr] = useState('') + const [step, setStep] = useState(0) + const [collapseText] = useState('Progress log') + const { fetchData } = useFetch() + + useEffect(() => { + setStepItem((prevItems) => + prevItems.map((item, index) => { + return { ...item, status: index < step ? 'completed' : item.status } + }) + ) + }, [step]) + + useEffect(() => { + fetchData( + 'http://localhost:3000/stream', + // 'model/local/deploy', + { + method: 'POST', + eoBody: { recordId: record.id }, + custom: true, + isStream: true, + handleStream: (chunk) => { + const parsedChunk = JSON.parse(chunk) + // 下载中 + if (parsedChunk?.data?.state.includes('download')) { + setStepItem((prevItems) => + prevItems.map((item) => { + return item.title === 'Download' + ? { + ...item, + description: `${parsedChunk?.data?.info?.current} / ${parsedChunk?.data?.info?.total}`, + status: 'inProgress' + } + : item + }) + ) + setStep(0) + // 部署中 + } else if (parsedChunk?.data?.state.includes('deploy')) { + setStepItem((prevItems) => + prevItems.map((item) => { + return { ...item, status: item.title === 'Deploy' ? 'inProgress' : item.status } + }) + ) + setStep(1) + // 初始化中 + } else if (parsedChunk?.data?.state.includes('initializing')) { + setStepItem((prevItems) => + prevItems.map((item) => { + return { ...item, status: item.title === 'Initializing' ? 'inProgress' : item.status } + }) + ) + setStep(2) + // 完成 + } else if (parsedChunk?.data?.state.includes('finish')) { + setStepItem((prevItems) => + prevItems.map((item) => { + return { ...item, status: item.title === 'Initializing' ? 'completed' : item.status } + }) + ) + setStep(4) + } + setScriptStr(parsedChunk?.data?.message || '') + } + } + ) + }, []) + return ( -
- {/* */} - -
+ <> +
+ + {stepItem.map((item, index) => ( + + ))} + +
+ + ) + } + ]} + > + ) } From 628cd98fd08e2c16ccc999cbbc463869d39605dd Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Tue, 11 Feb 2025 10:36:47 +0800 Subject: [PATCH 10/38] feat: feature/1.5 Extract home page navigation component --- .../core/src/pages/guide/AIModelGuide.tsx | 294 ++---------------- .../core/src/pages/guide/LocalAiDeploy.tsx | 145 +++++++++ .../core/src/pages/guide/RestAIDeploy.tsx | 125 ++++++++ .../core/src/pages/guide/deployModelUtil.ts | 57 ++++ 4 files changed, 354 insertions(+), 267 deletions(-) create mode 100644 frontend/packages/core/src/pages/guide/LocalAiDeploy.tsx create mode 100644 frontend/packages/core/src/pages/guide/RestAIDeploy.tsx create mode 100644 frontend/packages/core/src/pages/guide/deployModelUtil.ts diff --git a/frontend/packages/core/src/pages/guide/AIModelGuide.tsx b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx index 7e88505f..8563a318 100644 --- a/frontend/packages/core/src/pages/guide/AIModelGuide.tsx +++ b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx @@ -4,220 +4,43 @@ import localAIPic from '@common/assets/localAI.svg' import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { $t } from '@common/locales' import { Icon } from '@iconify/react/dist/iconify.js' -import { App, Upload, UploadProps, Form, message, Select } from 'antd' +import { App } from 'antd' import { Card } from 'antd' -import { useRef, useState } from 'react' +import { useRef } from 'react' import { useNavigate } from 'react-router-dom' -import WithPermission from '@common/components/aoplatform/WithPermission' -import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' -import { useFetch } from '@common/hooks/http' -import { LocalModelItem, MemberItem, SimpleTeamItem } from '@common/const/type' + import AiSettingModalContent, { AiSettingModalContentHandle } from '../aiSetting/AiSettingModal' import { checkAccess } from '@common/utils/permission' -const { Dragger } = Upload +import LocalAiDeploy, { LocalAiDeployHandle } from './LocalAiDeploy' +import useDeployLocalModel from './deployModelUtil' +import RestAIDeploy, { RestAIDeployHandle } from './RestAIDeploy' + export const AIModelGuide = () => { const { modal } = App.useApp() - const [, forceUpdate] = useState(null) - const [form] = Form.useForm() const entityData = useRef(null) - const { fetchData } = useFetch() const navigateTo = useNavigate() - const { checkPermission, accessData } = useGlobalContext() + const { accessData } = useGlobalContext() const modalRef = useRef() + const localAiDeployRef = useRef() + const restAiDeployRef = useRef() + const { deployLocalModel } = useDeployLocalModel() - /** - * 获取 team 选项列表 - * @returns - */ - const getTeamOptionList = async (): any[] => { - const response = await fetchData>( - !checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams', - { method: 'GET', eoTransformKeys: [] } - ) - const { code, data, msg } = response - if (code === STATUS_CODE.SUCCESS) { - const teamOptionList = data.teams?.map((x: MemberItem) => { - return { ...x, label: x.name, value: x.id } - }) - if (form.getFieldValue('team') === undefined && data.teams?.length) { - form.setFieldValue('team', data.teams[0].id) - } - return teamOptionList - } else { - message.error(msg || $t(RESPONSE_TIPS.error)) - return [] - } - } - - /** - * 部署 rest 服务 - * @param file - * @returns - */ - const deployRestServer = async (file: File) => { - return new Promise((resolve, reject) => { - const formData = new FormData() - formData.append('file', file) - formData.append('type', file.type) - formData.append('team', form.getFieldValue('team')) - fetchData>('quick/service/rest', { - method: 'POST', - body: formData - }).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(false) - } - }) - }) - } - - const deployPopularModel = async (id: string, modalInstance: any) => { - await deployLocalModel({ - modelID: id, - team: form.getFieldValue('team') - }) - modalInstance.destroy() - navigateTo(`/service/list`) - } - - /** - * 部署本地模型 - * @param value - * @returns - */ - const deployLocalModel = (value: { modelID: string; team?: number }) => { - return new Promise((resolve, reject) => { - fetchData>('model/local/deploy/start', { - method: 'POST', - eoBody: { - model: value.modelID, - team: value?.team - } - }) - .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(false) - } - }) - .catch((errorInfo) => reject(errorInfo)) - }) - } - - /** - * 获取本地模型列表 - * @returns 本地模型列表 - */ - const getLocalModelList = async (): any[] => { - const response = await fetchData>( - 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/model/local/can_deploy', - // 'model/local/can_deploy' - { method: 'GET', custom: true, eoTransformKeys: ['is_popular'] } - ) - // TODO_数据模拟 - if (response.ok) { - const datas = await response.json() - const { code, data, msg } = datas - if (code === STATUS_CODE.SUCCESS) { - const modelList = data.models?.map((x: LocalModelItem) => { - return { ...x, label: x.name, value: x.id } - }) - return modelList - } else { - message.error(msg || $t(RESPONSE_TIPS.error)) - return [] - } - } else { - console.error('HTTP error', response.status) - } - // const { code, data, msg } = response - // if (code === STATUS_CODE.SUCCESS) { - // const modelList = data.models?.map((x: LocalModelItem) => { - // return { ...x, label: x.name, value: x.id } - // }) - // console.log('modelList===', modelList); - - // return modelList - // } else { - // message.error(msg || $t(RESPONSE_TIPS.error)) - // return [] - // } + const dumpServerPage = () => { + navigateTo('/service/list') } /** * rest 服务卡片点击事件 */ const restCardClick = async () => { - form.resetFields() - const teamList = await getTeamOptionList() - const props: UploadProps = { - name: 'file', - multiple: false, - maxCount: 1, - beforeUpload: (file) => { - form.setFieldsValue({ key: file }) - forceUpdate({}) - return false - } - } - modal.confirm({ title: $t('添加 Rest 服务'), - content: ( - -
- - -

- -

-

{$t('选择 OpenAPI 文件 (.json / .yaml)')}

-
-
- - - -
-
- ), + content: , onOk: () => { - return new Promise((resolve, reject) => { - form - .validateFields() - .then(async (value) => { - await deployRestServer(value.key.file) - resolve(true) - navigateTo(`/service/list`) - }) - .catch((errorInfo) => reject(errorInfo)) + return restAiDeployRef.current?.deployRestAIServer().then((res) => { + if (res === true) { + dumpServerPage() + } }) }, width: 600, @@ -251,7 +74,7 @@ export const AIModelGuide = () => { onOk: () => { return modalRef.current?.deployAIServer().then((res) => { if (res === true) { - navigateTo(`/service/list`) + dumpServerPage() } }) }, @@ -286,80 +109,17 @@ export const AIModelGuide = () => { * 本地部署 AI 并生成 API */ const localModelCardClick = async () => { - form.resetFields() - const teamList = await getTeamOptionList() - const modelList = await getLocalModelList() const modalInstance = modal.confirm({ title: $t('部署 AI 模型'), - content: ( - -
- - -
- - {$t('热点模型')} -
-
- {modelList.length && - modelList - .filter((item) => item.is_popular) - .map((item) => ( - { - deployPopularModel(item.id, modalInstance) - }} - > - {item.name} - - ))} -
-
- - - -
-
- ), + content: { + modalInstance.destroy() + dumpServerPage() + }}>, onOk: () => { - return new Promise((resolve, reject) => { - form - .validateFields() - .then(async (value) => { - await deployLocalModel(value) - resolve(true) - navigateTo(`/service/list`) - }) - .catch((errorInfo) => reject(errorInfo)) + return localAiDeployRef.current?.deployLocalAIServer().then((res) => { + if (res === true) { + dumpServerPage() + } }) }, width: 600, diff --git a/frontend/packages/core/src/pages/guide/LocalAiDeploy.tsx b/frontend/packages/core/src/pages/guide/LocalAiDeploy.tsx new file mode 100644 index 00000000..639254ad --- /dev/null +++ b/frontend/packages/core/src/pages/guide/LocalAiDeploy.tsx @@ -0,0 +1,145 @@ +import { Icon } from '@iconify/react/dist/iconify.js' +import WithPermission from '@common/components/aoplatform/WithPermission' +import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { Form, message, Select } from 'antd' +import { $t } from '@common/locales' +import { LocalModelItem, SimpleTeamItem } from '@common/const/type' +import { useFetch } from '@common/hooks/http' +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' +import useDeployLocalModel from './deployModelUtil' +export type LocalAiDeployHandle = { + deployLocalAIServer: () => Promise +} +const LocalAiDeploy = forwardRef((props: any, ref: any) => { + const { onClose } = props + const [form] = Form.useForm() + const { fetchData } = useFetch() + const [modelList, setModelList] = useState([]) + const [teamList, setTeamList] = useState([]) + const { deployLocalModel, getTeamOptionList } = useDeployLocalModel() + + /** + * 获取本地模型列表 + * @returns 本地模型列表 + */ + const getLocalModelList = async () => { + const response = await fetchData>( + 'model/local/can_deploy', + { method: 'GET', eoTransformKeys: ['is_popular'] } + ) + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + const modelList = data.models?.map((x: LocalModelItem) => { + return { ...x, label: x.name, value: x.id } + }) + setModelList(modelList) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return [] + } + } + + /** + * 部署热门模型 + * @param id 模型ID + * @returns + */ + const deployPopularModel = async (id: string) => { + await deployLocalModel({ + modelID: id, + team: form.getFieldValue('team') + }) + onClose?.() + } + + const getTeamList = async () => { + const teamOptionList = await getTeamOptionList() + setTeamList(teamOptionList) + if (form.getFieldValue('team') === undefined && teamOptionList.length) { + form.setFieldValue('team', teamOptionList[0].value) + } + } + useEffect(() => { + getLocalModelList() + getTeamList() + }, []) + + /** + * 部署本地AI + * @returns + */ + const deployLocalAIServer = () => { + return new Promise((resolve, reject) => { + form + .validateFields() + .then(async (value) => { + await deployLocalModel(value) + resolve(true) + }) + .catch((errorInfo) => reject(errorInfo)) + }) + } + + useImperativeHandle(ref, () => ({ + deployLocalAIServer + })) + return ( + +
+ + +
+ + {$t('热点模型')} +
+
+ {modelList.length ? + modelList + .filter((item) => item.is_popular) + .map((item) => ( + { + deployPopularModel(item.id) + }} + > + {item.name} + + )) : null} +
+
+ + + +
+
+ ) +}) + +export default LocalAiDeploy diff --git a/frontend/packages/core/src/pages/guide/RestAIDeploy.tsx b/frontend/packages/core/src/pages/guide/RestAIDeploy.tsx new file mode 100644 index 00000000..01ac1c0c --- /dev/null +++ b/frontend/packages/core/src/pages/guide/RestAIDeploy.tsx @@ -0,0 +1,125 @@ +import { Icon } from '@iconify/react/dist/iconify.js' +import WithPermission from '@common/components/aoplatform/WithPermission' +import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { Upload, UploadProps, Form, message, Select } from 'antd' +import { $t } from '@common/locales' +import { SimpleTeamItem } from '@common/const/type' +import { useFetch } from '@common/hooks/http' +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' +import useDeployLocalModel from './deployModelUtil' + +const { Dragger } = Upload +export type RestAIDeployHandle = { + deployRestAIServer: () => Promise +} +const RestAIDeploy = forwardRef((props: any, ref: any) => { + const [form] = Form.useForm() + const { fetchData } = useFetch() + const [teamList, setTeamList] = useState([]) + const { getTeamOptionList } = useDeployLocalModel() + + const uploadProps: UploadProps = { + name: 'file', + multiple: false, + maxCount: 1, + beforeUpload: (file) => { + form.setFieldsValue({ key: file }) + return false + } + } + const getTeamList = async () => { + const teamOptionList = await getTeamOptionList() + setTeamList(teamOptionList) + if (form.getFieldValue('team') === undefined && teamOptionList.length) { + form.setFieldValue('team', teamOptionList[0].value) + } + } + useEffect(() => { + getTeamList() + }, []) + + /** + * 部署 rest 服务 + * @param file + * @returns + */ + const deployRestServer = async (file: File) => { + return new Promise((resolve, reject) => { + const formData = new FormData() + formData.append('file', file) + formData.append('type', file.type) + formData.append('team', form.getFieldValue('team')) + fetchData>('quick/service/rest', { + method: 'POST', + body: formData + }).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(false) + } + }) + }) + } + + /** + * 部署本地AI + * @returns + */ + const deployRestAIServer = () => { + return new Promise((resolve, reject) => { + form + .validateFields() + .then(async (value) => { + await deployRestServer(value.key.file) + resolve(true) + }) + .catch((errorInfo) => reject(errorInfo)) + }) + } + + useImperativeHandle(ref, () => ({ + deployRestAIServer + })) + return ( + +
+ + +

+ +

+

{$t('选择 OpenAPI 文件 (.json / .yaml)')}

+
+
+ + + +
+
+ ) +}) + +export default RestAIDeploy diff --git a/frontend/packages/core/src/pages/guide/deployModelUtil.ts b/frontend/packages/core/src/pages/guide/deployModelUtil.ts new file mode 100644 index 00000000..f442a7ad --- /dev/null +++ b/frontend/packages/core/src/pages/guide/deployModelUtil.ts @@ -0,0 +1,57 @@ +// deployModelUtil.ts +import { useFetch } from '@common/hooks/http' +import { message } from 'antd' +import { STATUS_CODE, RESPONSE_TIPS, BasicResponse } from '@common/const/const' +import { $t } from '@common/locales' +import { MemberItem, SimpleTeamItem } from '@common/const/type' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' + +const useDeployLocalModel = () => { + const { fetchData } = useFetch() + const { checkPermission } = useGlobalContext() + const deployLocalModel = (value: { modelID: string; team?: number }) => { + return new Promise((resolve, reject) => { + fetchData>('model/local/deploy/start', { + method: 'POST', + eoBody: { + model: value.modelID, + team: value?.team + } + }) + .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(false) + } + }) + .catch((errorInfo) => reject(errorInfo)) + }) + } + /** + * 获取 team 选项列表 + * @returns + */ + const getTeamOptionList = async (): any[] => { + const response = await fetchData>( + !checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams', + { method: 'GET', eoTransformKeys: [] } + ) + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + const teamOptionList = data.teams?.map((x: MemberItem) => { + return { ...x, label: x.name, value: x.id } + }) + return teamOptionList + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return [] + } + } + return { deployLocalModel, getTeamOptionList } +} + +export default useDeployLocalModel From 5cc01d7aab8a787731a50686b1d6c8648fe91c68 Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Tue, 11 Feb 2025 18:27:14 +0800 Subject: [PATCH 11/38] feat: load banancing list --- .../src/contexts/GlobalStateContext.tsx | 7 + .../packages/common/src/hooks/pluginLoader.ts | 10 + frontend/packages/core/src/const/const.tsx | 15 + .../api/AiServiceInsideRouterCreate.tsx | 5 +- .../api/AiServiceInsideRouterModelConfig.tsx | 311 ++++++++++------ .../src/pages/aiSetting/AiSettingModal.tsx | 2 +- .../core/src/pages/loadBalancing/AddModel.tsx | 227 ++++++++++++ .../core/src/pages/loadBalancing/index.tsx | 349 ++++++++++++++++++ .../loadBalancing/loadBalancingLayout.tsx | 16 + .../core/src/pages/loadBalancing/type.ts | 31 ++ 10 files changed, 857 insertions(+), 116 deletions(-) create mode 100644 frontend/packages/core/src/pages/loadBalancing/AddModel.tsx create mode 100644 frontend/packages/core/src/pages/loadBalancing/index.tsx create mode 100644 frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx create mode 100644 frontend/packages/core/src/pages/loadBalancing/type.ts diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index 31fbb1d4..1a3eb1ff 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -160,6 +160,13 @@ const mockData = [ path: '/aiApis', icon: 'ic:baseline-api', access: 'system.settings.ai_api.view' + }, + { + name: '负载均衡', + key: 'loadBalancing', + path: '/loadBalancing', + icon: 'ph:network-x', + access: 'system.settings.data_source.view' } ] }, diff --git a/frontend/packages/common/src/hooks/pluginLoader.ts b/frontend/packages/common/src/hooks/pluginLoader.ts index a33eb8e4..84f06bc4 100644 --- a/frontend/packages/common/src/hooks/pluginLoader.ts +++ b/frontend/packages/common/src/hooks/pluginLoader.ts @@ -219,6 +219,16 @@ const mockData = { type: 'normal' } ] + }, + { + driver: 'apipark.builtIn.component', + name: 'loadBalancing', + router: [ + { + path: 'loadBalancing', + type: 'normal' + } + ] } // { // "driver": "apipark.remote.normal", diff --git a/frontend/packages/core/src/const/const.tsx b/frontend/packages/core/src/const/const.tsx index 79b6452d..1ee3f57b 100644 --- a/frontend/packages/core/src/const/const.tsx +++ b/frontend/packages/core/src/const/const.tsx @@ -799,5 +799,20 @@ export const routerMap: Map = new Map([ } ] } + ], + [ + 'loadBalancing', + { + type: 'module', + lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/loadBalancingLayout.tsx')), + key: 'loadBalancing', + children: [ + { + path: 'list', + lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/index.tsx')), + key: 'loadBalancingList' + } + ] + } ] ]) diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx index 489ed981..7fbf127a 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx @@ -79,7 +79,7 @@ const AiServiceInsideRouterCreate = () => { timeout, retry, aiPrompt: { variables: variables, prompt: prompt }, - aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config }, + aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config, type: defaultLlm?.type }, disabled } return fetchData>('service/ai-router', { @@ -237,13 +237,14 @@ const AiServiceInsideRouterCreate = () => { } const handlerSubmit: () => Promise | undefined = () => { - return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string }) => { + return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string, type: string, provider: string }) => { setDefaultLlm( (prev) => ({ ...prev, provider: res.provider, id: res.id, + type: res.type, config: res.config, logo: llmList?.find((x: AiProviderLlmsItems) => x.id === res.id)?.logo }) as AiProviderDefaultConfig & { config: string } diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx index 415a99f6..628b44df 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx @@ -1,132 +1,217 @@ -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 { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList" -import { SimpleAiProviderItem } from "@core/pages/system/SystemConfig" -import { Form, message, Select, Tag } from "antd" -import { DefaultOptionType } from "antd/es/select" -import { forwardRef, useEffect, useImperativeHandle, useState } from "react" +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 { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList' +import { LocalLlmType } from '@core/pages/loadBalancing/type' +import { SimpleAiProviderItem } from '@core/pages/system/SystemConfig' +import { Form, message, Select, Tag } from 'antd' +import { DefaultOptionType } from 'antd/es/select' +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' export type AiServiceRouterModelConfigHandle = { - save:()=>Promise<{id:string, config:string}> + save: () => Promise<{ id: string; config: string, type: string, provider: string }> } export type AiServiceRouterModelConfigProps = { - entity:AiServiceRouterModelConfigField - llmList:AiProviderLlmsItems[] + entity: AiServiceRouterModelConfigField + llmList: AiProviderLlmsItems[] } type AiServiceRouterModelConfigField = { - provider:string - id:string - config:string + provider: string + id: string + config: string + type: string } -const AiServiceRouterModelConfig = forwardRef((props, ref)=>{ - const [form] = Form.useForm(); - const {entity} = props - const [providerList, setProviderList]= useState([]) - const [llmList, setLlmList]= useState([]) - const {fetchData} = useFetch() - useImperativeHandle(ref, ()=>({ - save:form.validateFields - }) - ) +const AiServiceRouterModelConfig = forwardRef( + (props, ref) => { + const [form] = Form.useForm() + const { entity } = props + const [providerList, setProviderList] = useState([]) + const [llmList, setLlmList] = useState([]) + const [modelType, setModelType] = useState<'online' | 'local'>('online') + const { fetchData } = useFetch() + useImperativeHandle(ref, () => ({ + save: form.validateFields + })) + const [modelTypeList] = useState([ + { + label: $t('线上模型'), + value: 'online' + }, + { + label: $t('本地模型'), + value: 'local' + } + ]) - useEffect(()=>{ + /** + * 获取本地模型列表 + * @param setDefaultValue + */ + const getLocalLlmList = (setDefaultValue?: boolean) => { + fetchData('simple/ai/models/local/configured', { + method: 'GET', + eoTransformKeys: ['default_config'] + }).then((response) => { + const models = response.data.models || [] + setLlmList(models) + if (setDefaultValue && models.length) { + const id = models[0].id + form.setFieldsValue({ + id, + config: models.find((x) => x.id === id)?.defaultConfig + }) + } + }) + } + + /** + * 切换模型类型 + * @param e + */ + const modelTypeChange = (e: string) => { + setModelType(e as 'online' | 'local') + setLlmList([]) + form.setFieldsValue({ + provider: '', + id: '', + config: '', + type: e + }) + if (e === 'online') { + getProviderList(true) + } else { + getLocalLlmList(true) + } + } + + useEffect(() => { + if (entity.type === 'online') { getProviderList() - form.setFieldsValue(entity) - },[]) - - const getProviderList = ()=>{ - setProviderList([]) - fetchData>('simple/ai/providers',{method:'GET',eoTransformKeys:[]}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - setProviderList(data.providers?.filter(x=>x.configured)?.map((x:SimpleAiProviderItem)=>{return {...x, - label: x.name, value:x.id - }})) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }) - } - - const getLlmList = (provider:string)=>{ - fetchData>('ai/provider/llms',{method:'GET',eoParams:{provider}, eoTransformKeys:['default_llm']}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - setLlmList(data.llms) - form.setFieldsValue({ - id:data.provider.defaultLlm, - config:data.llms.find(x=>x.id===data.provider.defaultLlm)?.config}) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }).catch((errorInfo)=> console.error(errorInfo)) - } - - const handleChangeProvider = (provider:string)=>{ - getLlmList(provider) - } - - useEffect(()=>{ getLlmList(entity.provider) - },[]) + } else { + getLocalLlmList() + } + form.setFieldsValue(entity) + }, []) + + const getProviderList = (setDefaultValue?: boolean) => { + setProviderList([]) + fetchData>('simple/ai/providers/configured', { + method: 'GET', + eoTransformKeys: [] + }).then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setProviderList( + data.providers + ?.map((x: SimpleAiProviderItem) => { + return { ...x, label: x.name, value: x.id } + }) + ) + if (setDefaultValue && data.providers.length) { + const id = data.providers[0].id + form.setFieldValue('provider', id) + getLlmList(id) + } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + const getLlmList = (provider: string) => { + fetchData>('ai/provider/llms', { + method: 'GET', + eoParams: { provider }, + eoTransformKeys: ['default_llm'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setLlmList(data.llms) + form.setFieldsValue({ + id: data.provider.defaultLlm, + config: data.llms.find((x) => x.id === data.provider.defaultLlm)?.config + }) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .catch((errorInfo) => console.error(errorInfo)) + } + + const handleChangeProvider = (provider: string) => { + getLlmList(provider) + } return ( -
- - label={$t("模型供应商")} - name="provider" - rules={[{ required: true }]} - > - - + + label={$t('模型类型')} name="type" rules={[{ required: true }]}> + + + {modelType === 'online' && ( + + label={$t('模型供应商')} + name="provider" + rules={[{ required: true }]} + > + + + )} - - label={$t("模型")} - name="id" - rules={[{ required: true }]} - > - - + label={$t('模型')} name="id" rules={[{ required: true }]}> + + - - label={$t("参数")} - name="config" - > - - - + label={$t('参数')} name="config"> + + + ) -}) + } +) -export default AiServiceRouterModelConfig \ No newline at end of file +export default AiServiceRouterModelConfig diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index f0df517d..83e197e1 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -272,7 +272,7 @@ const AiSettingModalContent = forwardRef {modelMode === 'manual' && ( - label={$t('模型来源')} name="modelMode" rules={[{ required: true }]}> + label={$t('模型供应商')} name="modelMode" rules={[{ required: true }]}> { + modelTypeChange(e) + }} + > + + {modelType === 'online' && ( + label={$t('模型供应商')} name="provider" rules={[{ required: true }]}> + + + )} + + + + + ) +}) + +export default AddLoadBalancingModel diff --git a/frontend/packages/core/src/pages/loadBalancing/index.tsx b/frontend/packages/core/src/pages/loadBalancing/index.tsx new file mode 100644 index 00000000..3441f5fd --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/index.tsx @@ -0,0 +1,349 @@ +import { ActionType } from '@ant-design/pro-components' +import InsidePage from '@common/components/aoplatform/InsidePage' +import PageList, { PageProColumns } from '@common/components/aoplatform/PageList' +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/index.ts' +import { App, Button, Typography } from 'antd' +import { useEffect, useRef, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { LoadBalancingHandle, LoadBalancingItems } from './type' +import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission' +import AddLoadBalancingModel from './AddModel' + + + +const LoadBalancingPage = () => { + const pageListRef = useRef(null) + const [searchParams] = useSearchParams() + const serviceId = searchParams.get('serviceId') + const [searchWord, setSearchWord] = useState('') + const [columns, setColumns] = useState[]>([]) + const { modal, message } = App.useApp() + const [apiKeys, setApiKeys] = useState([]) + const addModelRef = useRef() + const statusEnum: Record = { + normal: { text: {$t('正常')} }, + abnormal: { text: {$t('异常')} } + } + + /** + * 请求数据 + */ + const { fetchData } = useFetch() + const addModel = () => { + modal.confirm({ + title: $t('添加负载均衡'), + content: , + width: 600, + closable: true, + onOk: () => { + return addModelRef.current?.save().then((res) => { + if (res === true) { + pageListRef.current?.reload() + } + }) + }, + wrapClassName: 'ant-modal-without-footer', + okText: $t('确认'), + cancelText: $t('取消'), + icon: <> + }) + } + + /** + * 获取列表数据 + * @param dataType + * @returns + */ + const requestApis = ( + params: LoadBalancingItems & { + pageSize: number + current: number + }, + sort: Record, + filter: Record + ) => { + let filters + if (filter) { + filters = [] + if (filter.isStop) { + if (filter.isStop.indexOf('true') !== -1) { + filters.push('enable') + } + if (filter.isStop.indexOf('false') !== -1) { + filters.push('disable') + } + if (filter.publishStatus?.length > 0) { + filters = [...filters, ...filter.publishStatus] + } + } + } + + return fetchData>( + `strategy/${serviceId === undefined ? 'global' : 'service'}/data-masking/list`, + { + method: 'GET', + eoParams: { + order: Object.keys(sort)?.[0], + sort: Object.keys(sort)?.length > 0 ? (Object.values(sort)?.[0] === 'descend' ? 'desc' : 'asc') : undefined, + filters: JSON.stringify(filters), + keyword: searchWord, + service: serviceId + }, + eoTransformKeys: ['is_stop', 'is_delete', 'update_time', 'publish_status', 'processed_total'] + } + ) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setApiKeys(response.data.list) + // 保存数据 + return { + data: data.list, + total: data.total, + success: true + } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return { data: [], success: false } + } + }) + .catch(() => { + return { data: [], success: false } + }) + } + + /** + * 排序 + * @param beforeIndex + * @param afterIndex + * @param newDataSource + */ + const handleDragSortEnd = async (beforeIndex: number, afterIndex: number, newDataSource: LoadBalancingItems[]) => { + try { + let targetId + let sortDirection + + // Check if there's an item before afterIndex + if (afterIndex > 0) { + targetId = newDataSource[afterIndex - 1].id + sortDirection = 'after' + } else if (afterIndex < newDataSource.length - 1) { + // If no item before, use the item after + targetId = newDataSource[afterIndex + 1].id + sortDirection = 'before' + } + + const response = await fetchData>('ai/balance/sort', { + method: 'PUT', + eoBody: { + origin: apiKeys[beforeIndex].id, + target: targetId, + sort: sortDirection + } + }) + + if (response.code === STATUS_CODE.SUCCESS) { + message.success($t('排序成功')) + pageListRef.current?.reload() + } else { + message.error(response.msg || RESPONSE_TIPS.error) + // Revert the UI if API call fails + pageListRef.current?.reload() + } + } catch (error) { + message.error(RESPONSE_TIPS.error) + // Revert the UI if API call fails + pageListRef.current?.reload() + } + } + + /** + * 删除 + * @param id + */ + const handleDelete = (id: string) => { + fetchData>('ai/balance', { + method: 'DELETE', + eoBody: { + id + } + }) + .then((response) => { + const { code } = response + if (code === STATUS_CODE.SUCCESS) { + message.success($t('删除成功')) + pageListRef.current?.reload() + } else { + message.error(RESPONSE_TIPS.error) + } + }) + .catch((error) => { + message.error(RESPONSE_TIPS.error) + }) + } + + /** + * 设置表格列 + */ + const setTableColumns = () => { + setColumns([ + { + title: '', + dataIndex: 'drag', + width: '40px' + }, + { + title: $t('优先级'), + dataIndex: 'priority', + width: 80, + ellipsis: true, + key: 'priority' + }, + { + title: $t('模型'), + dataIndex: ['provider', 'name'], + ellipsis: true, + width: 100, + key: 'provider', + render: (text: string, record: LoadBalancingItems) => ( + + {record.provider?.name} / {record.model?.name} + + ) + }, + { + title: $t('类型'), + dataIndex: 'type', + width: 100, + ellipsis: true, + key: 'type', + render: (text: string, record: LoadBalancingItems) => ( + {record.type === 'online' ? $t('线上模型') : $t('本地模型')} + ) + }, + { + title: $t('状态'), + dataIndex: 'state', + width: 120, + ellipsis: true, + key: 'state', + render: (text: string, record: LoadBalancingItems) => {statusEnum[record.state]?.text || '-'} + }, + { + title: $t('API 数量'), + dataIndex: 'api_count', + ellipsis: true, + width: 80, + key: 'api_count', + render: (text: string, record: LoadBalancingItems) => ( + + + {record.api_count || '-'} + + + ) + }, + { + title: $t('KEY 数量'), + dataIndex: 'key_count', + ellipsis: true, + width: 80, + key: 'key_count', + render: (text: string, record: LoadBalancingItems) => ( + + + {record.key_count || '-'} + + + ) + }, + { + title: '', + key: 'option', + btnNums: 1, + width: 80, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: any) => [ + handleDelete(entity.id as string)} + btnTitle={$t('删除')} + /> + ] + } + ]) + } + useEffect(() => { + setTableColumns() + }, []) + + return ( + <> + +
+ + + + ]} + request={async ( + params: any & { + pageSize: number + current: number + }, + sort: Record, + filter: Record + ) => requestApis(params, sort, filter)} + onSearchWordChange={(e) => { + setSearchWord(e.target.value) + }} + showPagination={true} + dragSortKey="drag" + onDragSortEnd={handleDragSortEnd} + searchPlaceholder={$t('请输入...')} + columns={columns} + /> +
+
+ + ) +} +export default LoadBalancingPage diff --git a/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx b/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx new file mode 100644 index 00000000..196fc934 --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { Outlet, useLocation, useNavigate } from 'react-router-dom' + +export default function LoadBalancingLayout() { + const location = useLocation() + const pathName = location.pathname + const navigator = useNavigate() + + useEffect(() => { + if (pathName === '/loadBalancing') { + const queryParams = new URLSearchParams(location.search).toString() + navigator(`/loadBalancing/list${queryParams ? `?${queryParams}` : ''}`) + } + }, [pathName]) + return +} diff --git a/frontend/packages/core/src/pages/loadBalancing/type.ts b/frontend/packages/core/src/pages/loadBalancing/type.ts new file mode 100644 index 00000000..d7ff34ed --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/type.ts @@ -0,0 +1,31 @@ +export interface LoadBalancingItems { + id: string + priority: string + provider: { + id: string + name: string + } + model: { + id: string + name: string + } + type: string + state: string + api_count: string + key_count: string +} + +export interface LoadModelDetailData { + type: string + provider: string + model: string +} +export interface LocalLlmType { + id: string + name: string + defaultConfig: string +} + +export type LoadBalancingHandle = { + save: () => Promise +} From 6491de3064a26e298b81a6b51af0387b1d7d977f Mon Sep 17 00:00:00 2001 From: scarqin Date: Tue, 11 Feb 2025 19:01:15 +0800 Subject: [PATCH 12/38] feat: online model settings --- .../core/src/pages/aiSetting/AIFlowChart.tsx | 269 ------------------ .../src/pages/aiSetting/AIUnconfigure.tsx | 136 --------- .../src/pages/aiSetting/AiSettingList.tsx | 15 +- .../src/pages/aiSetting/OnlineModelList.tsx | 174 +++++++++++ .../pages/aiSetting/components/CustomEdge.tsx | 72 ----- .../aiSetting/components/KeyStatusNode.tsx | 53 ---- .../aiSetting/components/ModelCardNode.tsx | 76 ----- .../aiSetting/components/NodeComponents.tsx | 3 - .../aiSetting/components/ServiceCardNode.tsx | 18 -- .../core/src/pages/playground/index.tsx | 4 +- 10 files changed, 180 insertions(+), 640 deletions(-) delete mode 100644 frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx delete mode 100644 frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx create mode 100644 frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx delete mode 100644 frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx delete mode 100644 frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx delete mode 100644 frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx delete mode 100644 frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx delete mode 100644 frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx deleted file mode 100644 index dfd90d5c..00000000 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ /dev/null @@ -1,269 +0,0 @@ -'use client' - -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, - EdgeTypes, - Node, - NodeTypes, - PanOnScrollMode, - ReactFlow, - useEdgesState, - useNodesState -} from '@xyflow/react' -import '@xyflow/react/dist/style.css' -import { Button, Space, Spin } 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' -import { ServiceCardNode } from './components/NodeComponents' -import { LAYOUT } from './constants' -import './styles.css' -import { ModelListData } from './types' - -export type ApiResponse = BasicResponse<{ - backup: { - id: string - name: string - } - providers: ModelListData[] -}> - -const calculateNodePositions = (models: ModelListData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => { - return models.reduce( - (acc, model, index) => { - const y = startY + index * gap - return { - ...acc, - [model.id]: { - x: LAYOUT.MODEL_NODE_X, - y - }, - [`${model.id}-keys`]: { - x: LAYOUT.KEY_NODE_X, - y: y + 16 - } - } - }, - {} as Record - ) -} - -const nodeTypes: NodeTypes = { - modelCard: ModelCardNode, - keyCard: KeyStatusNode, - serviceCard: ServiceCardNode -} as const - -const edgeTypes: EdgeTypes = { - custom: CustomEdge -} - -const AIFlowChart = () => { - const [modelData, setModelData] = useState([]) - const [loading, setLoading] = useState(false) - const [nodes, setNodes, onNodesChange] = useNodesState([]) - const [edges, setEdges, onEdgesChange] = useEdgesState([]) - const { fetchData } = useFetch() - const { aiConfigFlushed } = useGlobalContext() - 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) - }) - .finally(() => { - setLoading(false) - }) - }, [aiConfigFlushed]) - - useEffect(() => { - if (!modelData.length) return - - const positions = calculateNodePositions(modelData) - const firstSuccessModel = modelData.find((model) => model.status === 'enabled') - console.log(firstSuccessModel) - - // 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', - type: 'serviceCard', - position: { x: LAYOUT.SERVICE_NODE_X, y: serviceY }, - draggable: false, - data: { - title: 'API Services', - count: modelData.length - } - }, - ...modelData.map((model) => ({ - id: model.id, - type: 'modelCard', - position: positions[model.id], - data: { - name: model.name, - status: model.status, - defaultLlm: model.defaultLlm, - logo: model.logo, - id: model.id, - alternativeModel: firstSuccessModel - } - })), - ...modelData.map((model) => ({ - id: `${model.id}-keys`, - type: 'keyCard', - position: positions[`${model.id}-keys`], - data: { - title: '', - keys: (model.keys || []).map((key, index) => ({ - id: key.id, - status: key.status, - priority: index + 1 - })) - } - })) - ] - - const newEdges: any = [ - ...modelData.map((model) => ({ - id: `service-${model.id}`, - source: 'apiService', - target: model.id, - label: `${model.api_count} apis`, - data: { - id: model.id, - status: model.status - }, - animated: true, - style: { stroke: model.status === 'enabled' ? '#52c41a' : '#ff4d4f' } - })), - ...modelData.map((model) => ({ - id: `${model.id}-keys-edge`, - source: model.id, - target: `${model.id}-keys`, - label: `${model.key_count} keys`, - data: { id: model.id }, - animated: true - })) - ] - setNodes(newNodes) - setEdges(newEdges) - }, [modelData]) - - const calculateExtent = useCallback(() => { - const left = LAYOUT.SERVICE_NODE_X - const right = LAYOUT.KEY_NODE_X - const top = 0 // Allow slight negative scroll to reduce top padding - const bottom = LAYOUT.NODE_START_Y + modelData.length * LAYOUT.NODE_GAP - return [ - [left, top], - [right, bottom < 100 ? 5000 : bottom] - ] as CoordinateExtent - }, [modelData.length]) - - const updateProviderOrder = async (sortedProviderIds: string[]) => { - await fetchData('ai/provider/sort', { - method: 'PUT', - body: JSON.stringify({ - providers: sortedProviderIds - }) - }) - } - - 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: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + 16 - } - } - } - } - return n - }) - }) - }, []) - - return ( -
- {loading ? ( -
- -
- ) : modelData.length === 0 ? ( - -
{$t('未配置 AI 模型')}
- -
- ) : ( - - )} -
- ) -} - -export default AIFlowChart diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx deleted file mode 100644 index ba1ae334..00000000 --- a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx +++ /dev/null @@ -1,136 +0,0 @@ -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 { useGlobalContext } from '@common/contexts/GlobalStateContext' -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 navigate = useNavigate() - - const handleOpenModal = async (provider: AiSettingListItem) => { - await openConfigModal(provider) - navigate('/aisetting?status=configure') - } - - 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 = () => { - const [modelData, setModelData] = useState([]) - const { fetchData } = useFetch() - const [loading, setLoading] = useState(false) - const { aiConfigFlushed } = useGlobalContext() - - useEffect(() => { - setLoading(true) - 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) - } else { - const { message } = App.useApp() - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }) - .finally(() => setLoading(false)) - }, [aiConfigFlushed]) - - return ( - } - spinning={loading} - > - {modelData && modelData.length > 0 ? ( -
- {modelData.filter((item) => !item.configured).length > 0 && ( - <> - !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 aa7034d2..3a7151ab 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -3,9 +3,8 @@ import { useI18n } 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' +import OnlineModelList from './OnlineModelList' const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const @@ -38,21 +37,17 @@ const AiSettingContent = () => { items={[ { key: 'flow', - label: $t('已设置'), + label: $t('在线模型'), children: (
- +
) }, { key: 'config', - label: $t('未设置'), - children: ( -
- -
- ) + label: $t('本地模型'), + children:
} ]} /> diff --git a/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx b/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx new file mode 100644 index 00000000..3170911b --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx @@ -0,0 +1,174 @@ +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 { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { AIProvider } from '@core/components/AIProviderSelect' +import { App, Divider, Space, Typography } from 'antd' +import React, { useRef, useState } from 'react' +import { useAiSetting } from './contexts/AiSettingContext' +import { AiSettingListItem, ModelListData } from './types' + +const OnlineModelList: React.FC = () => { + const pageListRef = useRef(null) + const { modal, message } = App.useApp() + const [provider, setProvider] = useState() + const { fetchData } = useFetch() + const [searchWord, setSearchWord] = useState('') + const [total, setTotal] = useState(0) + const modalRef = useRef() + const { accessData } = useGlobalContext() + const { openConfigModal } = useAiSetting() + + const handleEdit = (record: ModelListData) => { + openConfigModal({ id: record.id, defaultLlm: record.defaultLlm } as AiSettingListItem) + } + + const handleAdd = () => { + // openConfigModal() + } + + const handleDelete = async (id: string) => { + try { + const response = await fetchData>('ai/resource/key', { + method: 'DELETE', + eoParams: { + 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 requestList = async (params: any) => { + try { + const response = await fetchData>('ai/providers/configured', { + method: 'GET', + eoParams: { + page_size: params.pageSize, + keyword: searchWord, + page: params.current + } + // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' + }) + + if (response.code === STATUS_CODE.SUCCESS) { + console.log(response) + setTotal(response.data.total) + return { + data: response.data.providers, + 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 statusEnum = { + enabled: { text: {$t('正常')} }, + disabled: { text: {$t('停用')} }, + abnormal: { text: {$t('异常')} } + } + + 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)} + btnTitle={$t('删除')} + /> + ] + } + ] + + const columns: PageProColumns[] = [ + { + title: $t('名称'), + dataIndex: 'name', + render: (dom: React.ReactNode, entity: ModelListData) => {entity.name} + }, + { + title: $t('状态'), + dataIndex: 'status', + ellipsis: true, + valueType: 'select', + filters: true, + onFilter: true, + valueEnum: statusEnum, + render: (dom: React.ReactNode, entity: ModelListData) => statusEnum[entity.status]?.text || entity.status + }, + { + title: $t('默认模型'), + dataIndex: 'defaultLlm' + }, + { + title: $t('Apis'), + dataIndex: 'api_count' + }, + { + title: $t('Keys'), + dataIndex: 'key_count' + }, + ...operation + ] + + return ( + { + setSearchWord(e.target.value) + pageListRef.current?.reload() + }} + showPagination={true} + searchPlaceholder={$t('请输入名称搜索')} + columns={columns} + dragSortKey="drag" + addNewBtnTitle={$t('添加模型')} + onAddNewBtnClick={handleAdd} + /> + ) +} + +export default OnlineModelList diff --git a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx b/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx deleted file mode 100644 index 35d585f5..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useStore } from '@xyflow/react' - -export default function CustomEdge({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - style = {}, - markerEnd, - label, - data, - source, - target -}: EdgeProps) { - // Get all edges to check for duplicates - const edges = useStore((state) => state.edges) - - // Find duplicate edges between the same source and target - const duplicateEdges = edges.filter((edge) => edge.source === source && edge.target === target) - const edgeIndex = duplicateEdges.findIndex((edge) => edge.id === id) - - // Adjust the path if this is a duplicate edge - const offset = edgeIndex * 20 // 20px offset for each duplicate edge - - const [edgePath] = getSmoothStepPath({ - sourceX, - sourceY: sourceY, - sourcePosition, - targetX, - targetY: targetY + offset, - targetPosition, - borderRadius: 16 - }) - - const modelId = data?.id - - return ( - <> - - {label && ( - - - {label} - - - )} - - ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx deleted file mode 100644 index ef24aee6..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Handle, Position } from '@xyflow/react' -import React from 'react' -import { KeyData } from '../types' - -interface KeyStatusNodeData { - id: string - title: string - keys: KeyData[] -} - -const KEY_SIZE = '1.25rem' // 20px -const KEY_GAP = '0.25rem' // 4px -const MAX_KEYS = 10 - -export const KeyStatusNode: React.FC<{ data: KeyStatusNodeData }> = ({ data }) => { - const { title, keys = [] } = data - const totalKeys = keys.length - const keyWidth = totalKeys > 5 ? `calc((100% - ${(totalKeys - 1) * 0.25}rem) / ${totalKeys})` : KEY_SIZE - return ( -
- -
-
{title}
-
5 ? '118px' : 'auto', - maxWidth: `calc(${MAX_KEYS} * ${KEY_SIZE} + (${MAX_KEYS} - 1) * ${KEY_GAP})`, - minHeight: KEY_SIZE - }} - > - {keys.map((key) => ( -
- ))} -
-
-
- ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx deleted file mode 100644 index ede8936b..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { $t } from '@common/locales' -import { Icon } from '@iconify/react' -import { Handle, Position } from '@xyflow/react' -import React from 'react' -import { useAiSetting } from '../contexts/AiSettingContext' -import { AiSettingListItem, ModelDetailData, ModelStatus } from '../types' - -type ModelCardNodeData = ModelDetailData & { - id: string - position: { x: number; y: number } - alternativeModel?: ModelDetailData -} - -export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => { - const { name, status, defaultLlm, logo, alternativeModel } = data - const { openConfigModal } = useAiSetting() - - 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 ( - <> -
- - -
-
-
-
- -
- {name} - -
- - {/* Action buttons */} -
- { - openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem) - }} - /> -
-
-
- {$t('默认:')} - {defaultLlm} -
-
-
- {status !== 'enabled' && alternativeModel && ( -
- {$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.defaultLlm} -
- )} - - ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx b/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx deleted file mode 100644 index e2626cc2..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export { KeyStatusNode } from './KeyStatusNode' -export { ModelCardNode } from './ModelCardNode' -export { ServiceCardNode } from './ServiceCardNode' diff --git a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx deleted file mode 100644 index 176d80da..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Icon } from '@iconify/react' -import { Handle, NodeProps, Position } from '@xyflow/react' -import React from 'react' - -export const ServiceCardNode: React.FC = () => { - return ( -
- -
- - AI Services -
-
- ) -} diff --git a/frontend/packages/core/src/pages/playground/index.tsx b/frontend/packages/core/src/pages/playground/index.tsx index 12f5ec43..12a1b689 100644 --- a/frontend/packages/core/src/pages/playground/index.tsx +++ b/frontend/packages/core/src/pages/playground/index.tsx @@ -1,7 +1,5 @@ 'use client' -import AIFlowChart from '../aiSetting/AIFlowChart' - export default function Playground() { - return + return