From 7f3d6bb55ff716775fe1a916912bcb58ac758738 Mon Sep 17 00:00:00 2001 From: jialin Date: Fri, 6 Feb 2026 11:09:54 +0800 Subject: [PATCH] fix: ui issues --- src/components/seal-form/auto-complete.tsx | 3 +- src/components/seal-form/input-number.tsx | 2 +- src/components/seal-form/password.tsx | 2 +- src/components/seal-form/seal-cascader.tsx | 4 +- src/components/seal-form/seal-input.tsx | 2 +- src/components/seal-form/wrapper/select.ts | 7 +- src/hooks/use-table-fetch.ts | 2 +- src/locales/en-US/benchmark.ts | 5 +- src/locales/en-US/provider.ts | 8 +- src/locales/ja-JP/benchmark.ts | 5 +- src/locales/ja-JP/provider.ts | 8 +- src/locales/ru-RU/benchmark.ts | 5 +- src/locales/ru-RU/provider.ts | 8 +- src/locales/zh-CN/benchmark.ts | 4 +- src/locales/zh-CN/provider.ts | 8 +- .../benchmark/components/right-actions.tsx | 2 +- .../benchmark/components/row-actions.tsx | 24 +-- src/pages/benchmark/forms/model-instance.tsx | 35 ++++- src/pages/benchmark/index.tsx | 8 +- .../services/use-export-benchmark.ts | 8 +- .../components/volumes-config.tsx | 10 +- .../components/provider-models.tsx | 140 ++++++++++++------ src/pages/maas-provider/config/types.ts | 1 + src/pages/maas-provider/forms/api-keys.tsx | 5 +- src/pages/maas-provider/forms/basic.tsx | 35 ++++- src/pages/maas-provider/forms/index.tsx | 4 +- src/pages/maas-provider/forms/model-item.tsx | 31 ++-- .../maas-provider/forms/supported-models.tsx | 45 +++++- .../hooks/use-query-provider-models.ts | 4 +- src/pages/model-routes/config/index.ts | 2 +- src/pages/model-routes/config/types.ts | 3 + src/pages/model-routes/forms/targets.tsx | 65 ++++---- .../model-routes/hooks/use-access-control.ts | 4 + .../model-routes/hooks/use-create-route.ts | 4 + src/pages/model-routes/index.tsx | 2 +- 35 files changed, 364 insertions(+), 141 deletions(-) diff --git a/src/components/seal-form/auto-complete.tsx b/src/components/seal-form/auto-complete.tsx index cb50485e..dca8076c 100644 --- a/src/components/seal-form/auto-complete.tsx +++ b/src/components/seal-form/auto-complete.tsx @@ -29,6 +29,7 @@ const SealAutoComplete: React.FC< extra, style, addAfter, + suffixIcon, loading, allowClear, clearSpaceOnBlur, @@ -103,7 +104,7 @@ const SealAutoComplete: React.FC< ); } - return null; + return suffixIcon || null; }; const popupRender = (originNode: React.ReactElement): React.ReactElement => { diff --git a/src/components/seal-form/input-number.tsx b/src/components/seal-form/input-number.tsx index 80752e65..c0f0a0fa 100644 --- a/src/components/seal-form/input-number.tsx +++ b/src/components/seal-form/input-number.tsx @@ -22,7 +22,7 @@ const SealInputNumber: React.FC = ( let status = ''; if (isInFormItems) { const statusData = Form?.Item?.useStatus?.(); - status = statusData?.status || ''; + status = props.status || statusData?.status || ''; } else { status = props.status || ''; } diff --git a/src/components/seal-form/password.tsx b/src/components/seal-form/password.tsx index 3d9a5e19..e0732515 100644 --- a/src/components/seal-form/password.tsx +++ b/src/components/seal-form/password.tsx @@ -51,8 +51,8 @@ const SealPassword: React.FC = (props) => { e.target.value = e.target.value?.trim?.(); if (!inputRef.current?.input?.value) { setIsFocus(false); - props.onBlur?.(e); } + props.onBlur?.(e); }; return ( diff --git a/src/components/seal-form/seal-cascader.tsx b/src/components/seal-form/seal-cascader.tsx index 3b2330a9..a8854e4f 100644 --- a/src/components/seal-form/seal-cascader.tsx +++ b/src/components/seal-form/seal-cascader.tsx @@ -106,7 +106,9 @@ const SealCascader: React.FC< // the status can be controlled by Form.Item if (isInFormItems) { const statusData = Form?.Item?.useStatus?.(); - status = statusData?.status || ''; + status = props.status || statusData?.status || ''; + } else { + status = props.status || ''; } const _options = useMemo(() => { diff --git a/src/components/seal-form/seal-input.tsx b/src/components/seal-form/seal-input.tsx index 4c5ad3a5..04f7eeab 100644 --- a/src/components/seal-form/seal-input.tsx +++ b/src/components/seal-form/seal-input.tsx @@ -30,7 +30,7 @@ const SealInput: React.FC = (props) => { let status = ''; if (isInFormItems) { const statusData = Form?.Item?.useStatus?.(); - status = statusData?.status || ''; + status = props.status || statusData?.status || ''; } else { status = props.status || ''; } diff --git a/src/components/seal-form/wrapper/select.ts b/src/components/seal-form/wrapper/select.ts index 9091971a..09c17100 100644 --- a/src/components/seal-form/wrapper/select.ts +++ b/src/components/seal-form/wrapper/select.ts @@ -181,8 +181,11 @@ const SelectWrapper = styled.div` } &.validate-status-error { - &.dropdown-visible { - .ant-select-dropdown { + .ant-select-dropdown { + border-color: var(--ant-color-error) !important; + } + .__wrapper__ { + .ant-cascader.ant-select.ant-select-outlined { border-color: var(--ant-color-error) !important; } } diff --git a/src/hooks/use-table-fetch.ts b/src/hooks/use-table-fetch.ts index c46902df..0dddc10c 100644 --- a/src/hooks/use-table-fetch.ts +++ b/src/hooks/use-table-fetch.ts @@ -283,7 +283,7 @@ export default function useTableFetch( } ) => { loadendRef.current = false; - const newQueryParams = { ...queryParams, ...params }; + const newQueryParams = { ...queryParams, ...params, page: 1 }; setQueryParams(newQueryParams); await fetchData({ query: newQueryParams }); if (watch && !options?.paginate) { diff --git a/src/locales/en-US/benchmark.ts b/src/locales/en-US/benchmark.ts index 007300fd..7d99c89c 100644 --- a/src/locales/en-US/benchmark.ts +++ b/src/locales/en-US/benchmark.ts @@ -80,5 +80,8 @@ export default { 'benchmark.env.system': 'System', 'benchmark.env.runtimeVersion': 'Runtime Version', 'benchmark.env.driverVersion': 'Driver Version', - 'benchmark.env.cpuCounts': 'CPU Counts' + 'benchmark.env.cpuCounts': 'CPU Cores', + 'benchmark.table.export.results': 'Export Results', + 'benchmark.form.nonLlmModel.tips': + 'Benchmarking currently only supports LLM models' }; diff --git a/src/locales/en-US/provider.ts b/src/locales/en-US/provider.ts index 0748dc24..2ce73a26 100644 --- a/src/locales/en-US/provider.ts +++ b/src/locales/en-US/provider.ts @@ -15,5 +15,11 @@ export default { 'providers.form.model.test': 'Test', 'providers.form.model.test.tips': 'Test Connection', 'providers.form.target.placeholder': 'provider/models', - 'providers.form.fallback.token': 'Fallback API Keys' + 'providers.form.fallback.token': 'Fallback API Keys', + 'providers.form.custombeckendUrl': 'Custom Base URL', + 'providers.form.more': '+{count} more', + 'providers.form.addAll': 'Add All', + 'providers.form.rules.models': 'Please select at least one model', + 'providers.form.rules.tokens': 'Please enter a valid API Key', + 'providers.form.rules.model': 'Please select a model' }; diff --git a/src/locales/ja-JP/benchmark.ts b/src/locales/ja-JP/benchmark.ts index 007300fd..7d99c89c 100644 --- a/src/locales/ja-JP/benchmark.ts +++ b/src/locales/ja-JP/benchmark.ts @@ -80,5 +80,8 @@ export default { 'benchmark.env.system': 'System', 'benchmark.env.runtimeVersion': 'Runtime Version', 'benchmark.env.driverVersion': 'Driver Version', - 'benchmark.env.cpuCounts': 'CPU Counts' + 'benchmark.env.cpuCounts': 'CPU Cores', + 'benchmark.table.export.results': 'Export Results', + 'benchmark.form.nonLlmModel.tips': + 'Benchmarking currently only supports LLM models' }; diff --git a/src/locales/ja-JP/provider.ts b/src/locales/ja-JP/provider.ts index 0748dc24..2ce73a26 100644 --- a/src/locales/ja-JP/provider.ts +++ b/src/locales/ja-JP/provider.ts @@ -15,5 +15,11 @@ export default { 'providers.form.model.test': 'Test', 'providers.form.model.test.tips': 'Test Connection', 'providers.form.target.placeholder': 'provider/models', - 'providers.form.fallback.token': 'Fallback API Keys' + 'providers.form.fallback.token': 'Fallback API Keys', + 'providers.form.custombeckendUrl': 'Custom Base URL', + 'providers.form.more': '+{count} more', + 'providers.form.addAll': 'Add All', + 'providers.form.rules.models': 'Please select at least one model', + 'providers.form.rules.tokens': 'Please enter a valid API Key', + 'providers.form.rules.model': 'Please select a model' }; diff --git a/src/locales/ru-RU/benchmark.ts b/src/locales/ru-RU/benchmark.ts index 007300fd..7d99c89c 100644 --- a/src/locales/ru-RU/benchmark.ts +++ b/src/locales/ru-RU/benchmark.ts @@ -80,5 +80,8 @@ export default { 'benchmark.env.system': 'System', 'benchmark.env.runtimeVersion': 'Runtime Version', 'benchmark.env.driverVersion': 'Driver Version', - 'benchmark.env.cpuCounts': 'CPU Counts' + 'benchmark.env.cpuCounts': 'CPU Cores', + 'benchmark.table.export.results': 'Export Results', + 'benchmark.form.nonLlmModel.tips': + 'Benchmarking currently only supports LLM models' }; diff --git a/src/locales/ru-RU/provider.ts b/src/locales/ru-RU/provider.ts index 0748dc24..2ce73a26 100644 --- a/src/locales/ru-RU/provider.ts +++ b/src/locales/ru-RU/provider.ts @@ -15,5 +15,11 @@ export default { 'providers.form.model.test': 'Test', 'providers.form.model.test.tips': 'Test Connection', 'providers.form.target.placeholder': 'provider/models', - 'providers.form.fallback.token': 'Fallback API Keys' + 'providers.form.fallback.token': 'Fallback API Keys', + 'providers.form.custombeckendUrl': 'Custom Base URL', + 'providers.form.more': '+{count} more', + 'providers.form.addAll': 'Add All', + 'providers.form.rules.models': 'Please select at least one model', + 'providers.form.rules.tokens': 'Please enter a valid API Key', + 'providers.form.rules.model': 'Please select a model' }; diff --git a/src/locales/zh-CN/benchmark.ts b/src/locales/zh-CN/benchmark.ts index 376777b0..73263c8a 100644 --- a/src/locales/zh-CN/benchmark.ts +++ b/src/locales/zh-CN/benchmark.ts @@ -80,5 +80,7 @@ export default { 'benchmark.env.system': '系统', 'benchmark.env.runtimeVersion': '运行时版本', 'benchmark.env.driverVersion': '驱动版本', - 'benchmark.env.cpuCounts': 'CPU 核心数' + 'benchmark.env.cpuCounts': 'CPU 核心数', + 'benchmark.table.export.results': '导出结果', + 'benchmark.form.nonLlmModel.tips': '基准测试目前仅支持 LLM 模型' }; diff --git a/src/locales/zh-CN/provider.ts b/src/locales/zh-CN/provider.ts index e85c2120..914d3f8b 100644 --- a/src/locales/zh-CN/provider.ts +++ b/src/locales/zh-CN/provider.ts @@ -15,5 +15,11 @@ export default { 'providers.form.model.test': '测试', 'providers.form.model.test.tips': '测试连接', 'providers.form.target.placeholder': '提供商/模型', - 'providers.form.fallback.token': '备用 API Key' + 'providers.form.fallback.token': '备用 API Key', + 'providers.form.custombeckendUrl': '自定义 Base URL', + 'providers.form.more': '+{count} 更多', + 'providers.form.addAll': '添加全部', + 'providers.form.rules.models': '请选择至少一个模型', + 'providers.form.rules.tokens': '请输入有效的 API Key', + 'providers.form.rules.model': '请选择模型' }; diff --git a/src/pages/benchmark/components/right-actions.tsx b/src/pages/benchmark/components/right-actions.tsx index 9a49a070..7da00fc5 100644 --- a/src/pages/benchmark/components/right-actions.tsx +++ b/src/pages/benchmark/components/right-actions.tsx @@ -25,7 +25,7 @@ const RightActions: React.FC = ({ }) => { const ButtonList = [ { - label: 'common.button.export', + label: 'benchmark.table.export.results', key: 'export', icon: ( diff --git a/src/pages/benchmark/components/row-actions.tsx b/src/pages/benchmark/components/row-actions.tsx index 4baa7c4a..36dc93fd 100644 --- a/src/pages/benchmark/components/row-actions.tsx +++ b/src/pages/benchmark/components/row-actions.tsx @@ -8,17 +8,6 @@ import { BenchmarkStatusValueMap } from '../config'; import { BenchmarkListItem as ListItem } from '../config/types'; const actionList = [ - { - key: 'edit', - label: 'common.button.edit', - icon: icons.EditOutlined - }, - { - label: 'common.button.stop', - key: 'stop', - icon: icons.Stop, - status: [BenchmarkStatusValueMap.QUEUED, BenchmarkStatusValueMap.Running] - }, { label: 'common.button.viewlog', key: 'viewlog', @@ -30,9 +19,20 @@ const actionList = [ ], icon: }, + { + key: 'edit', + label: 'common.button.edit', + icon: icons.EditOutlined + }, + { + label: 'common.button.stop', + key: 'stop', + icon: icons.Stop, + status: [BenchmarkStatusValueMap.QUEUED, BenchmarkStatusValueMap.Running] + }, { key: 'export', - label: 'common.button.export', + label: 'benchmark.table.export.results', icon: ( ) diff --git a/src/pages/benchmark/forms/model-instance.tsx b/src/pages/benchmark/forms/model-instance.tsx index 3ec4eed3..dcddc9a9 100644 --- a/src/pages/benchmark/forms/model-instance.tsx +++ b/src/pages/benchmark/forms/model-instance.tsx @@ -3,19 +3,22 @@ import { PageAction } from '@/config'; import useAppUtils from '@/hooks/use-app-utils'; import { InstanceStatusMap, - InstanceStatusMapValue + InstanceStatusMapValue, + modelCategoriesMap } from '@/pages/llmodels/config'; import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark'; import { useQueryModelInstancesList } from '@/pages/llmodels/services/use-query-model-instances'; import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-list'; import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; +import { Form, Tooltip } from 'antd'; import React, { useEffect } from 'react'; import { useFormContext } from '../config/form-context'; import { FormData } from '../config/types'; +// benchmark.form.nonLlmModel.tips const InstanceNode = (props: any) => { const { data: instance } = props; + const intl = useIntl(); return instance.isLeaf ? ( {instance.label} @@ -24,7 +27,17 @@ const InstanceNode = (props: any) => { )} ) : ( - {instance.label} + <> + {instance.disabled ? ( + + {instance.label} + + ) : ( + {instance.label} + )} + ); }; @@ -99,6 +112,7 @@ const ModelInstanceForm: React.FC = () => { .map((model: any) => ({ label: model.name, value: model.name, + disabled: modelCategoriesMap.llm !== model.categories?.[0], id: model.id, isLeaf: false, children: [] @@ -109,12 +123,17 @@ const ModelInstanceForm: React.FC = () => { } // preload instances for the first model - const instanceList = await fetchInstanceList({ id: modelOptions[0]?.id }); + const selectedllmModel = modelOptions.find((model) => !model.disabled); + if (!selectedllmModel) { + setModelList(modelOptions); + return; + } + const instanceList = await fetchInstanceList({ id: selectedllmModel.id }); const instanceOptions = instanceList.map((instance: any) => renderInstance(instance) ); - if (modelOptions[0]) { - modelOptions[0].children = [...instanceOptions] as never[]; + if (selectedllmModel) { + selectedllmModel.children = [...instanceOptions] as never[]; } // init form value for model instance @@ -127,8 +146,8 @@ const ModelInstanceForm: React.FC = () => { }); } else { handleOnChange( - [modelOptions[0].value, instanceOptions[0]?.value], - [modelOptions[0], instanceOptions[0]] + [selectedllmModel.value, instanceOptions[0]?.value], + [selectedllmModel, instanceOptions[0]] ); } diff --git a/src/pages/benchmark/index.tsx b/src/pages/benchmark/index.tsx index 01848e5d..b88ea1ef 100644 --- a/src/pages/benchmark/index.tsx +++ b/src/pages/benchmark/index.tsx @@ -147,7 +147,7 @@ const Benchmark: React.FC = () => { } else if (val === 'stop') { handleStopBenchmark(row.id); } else if (val === 'export') { - exportData([row.id]); + exportData([row.id], row.name); } }); @@ -192,7 +192,11 @@ const Benchmark: React.FC = () => { }); const handleExportData = () => { - exportData(rowSelection.selectedRowKeys); + const firstSelectedRow = dataSource.dataList.find( + (item) => item.id === rowSelection.selectedRowKeys[0] + ); + const name = firstSelectedRow ? firstSelectedRow.name : 'benchmark'; + exportData(rowSelection.selectedRowKeys, name); }; return ( diff --git a/src/pages/benchmark/services/use-export-benchmark.ts b/src/pages/benchmark/services/use-export-benchmark.ts index dd066a02..a21435fe 100644 --- a/src/pages/benchmark/services/use-export-benchmark.ts +++ b/src/pages/benchmark/services/use-export-benchmark.ts @@ -1,6 +1,7 @@ import { GPUSTACK_API_BASE_URL } from '@/config/settings'; import { downloadFile } from '@/utils/download-stream'; import { message } from 'antd'; +import dayjs from 'dayjs'; import { EXPORT_BENCHMARK_LIST } from '../apis'; const matchFilename = (disposition: string | null): string | undefined => { @@ -12,7 +13,9 @@ const matchFilename = (disposition: string | null): string | undefined => { }; export function useExportBenchmark() { - const exportData = async (data: any[]) => { + const exportData = async (data: any[], name: string) => { + const date = dayjs().format('YYYYMMDD_HHmmss'); + const fileName = `${name || 'benchmark'}_${date}`; try { const res = await fetch( `${GPUSTACK_API_BASE_URL}${EXPORT_BENCHMARK_LIST}`, @@ -26,8 +29,7 @@ export function useExportBenchmark() { ); // header const contentDispostion = res.headers.get('content-Disposition'); - const filename = - matchFilename(contentDispostion) || `benchmark-export.yml`; + const filename = matchFilename(contentDispostion) || `${fileName}.yml`; if (res.ok) { const blob = await res.blob(); downloadFile(blob, filename); diff --git a/src/pages/cluster-management/components/volumes-config.tsx b/src/pages/cluster-management/components/volumes-config.tsx index 52fe57d3..42941399 100644 --- a/src/pages/cluster-management/components/volumes-config.tsx +++ b/src/pages/cluster-management/components/volumes-config.tsx @@ -15,7 +15,8 @@ const CloudOptions: React.FC<{ const { disabled } = props; const intl = useIntl(); const form = Form.useFormInstance(); - const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form); + const volumes = + Form.useWatch(['cloud_options', volumeOptions.name], form) || []; const [validateStatusList, setValidateStatusList] = useState< { [key: string]: statusType }[] >([]); @@ -45,10 +46,15 @@ const CloudOptions: React.FC<{ form.setFieldValue(['cloud_options', volumeOptions.name], data); toggleValidation(true); }; - console.log('disabled=====', disabled); return ( = ({ dataList }) => { const iconsMap = { accessible: , - inaccessible: , - none: + inaccessible: null, + none: null + }; + + const intl = useIntl(); + + const head12Items = dataList.slice(0, 8); + const restItems = dataList.slice(8); + + const renderModels = (dataList: ProviderModel[]) => { + return ( + <> + {dataList.map((model, index) => ( + + + + {model.name} + + + {categoryConfig[model.category]?.icon} + + + + ))} + + ); }; return ( - - {dataList.map((model) => ( - + + {renderModels(head12Items)} + {restItems.length > 0 && ( + + + {renderModels(restItems)} + + } - }} - color={ - model.accessible === true - ? 'success' - : model.accessible === false - ? 'error' - : 'warning' - } - > - - - {model.name} - - - {categoryConfig[model.category]?.icon} - - - - ))} - + styles={{ + root: { maxWidth: '400px' }, + container: { + paddingInlineEnd: 4 + } + }} + > + + {intl.formatMessage( + { id: 'providers.form.more' }, + { count: restItems.length } + )} + + + )} + + ); }; diff --git a/src/pages/maas-provider/config/types.ts b/src/pages/maas-provider/config/types.ts index f5d70096..9a02d220 100644 --- a/src/pages/maas-provider/config/types.ts +++ b/src/pages/maas-provider/config/types.ts @@ -12,6 +12,7 @@ export interface FormData { api_key: string; config: { type: maasProviderType; + openaiCustomUrl?: string; [key: string]: any; }; } diff --git a/src/pages/maas-provider/forms/api-keys.tsx b/src/pages/maas-provider/forms/api-keys.tsx index 3356686e..354f6c8d 100644 --- a/src/pages/maas-provider/forms/api-keys.tsx +++ b/src/pages/maas-provider/forms/api-keys.tsx @@ -42,10 +42,7 @@ const AccessToken = () => { rules={[ { required: false, - message: getRuleMessage( - 'input', - intl.formatMessage({ id: 'providers.form.tokens.title' }) - ) + message: getRuleMessage('input', 'providers.form.tokens.title') } ]} > diff --git a/src/pages/maas-provider/forms/basic.tsx b/src/pages/maas-provider/forms/basic.tsx index 2289ad15..7170febd 100644 --- a/src/pages/maas-provider/forms/basic.tsx +++ b/src/pages/maas-provider/forms/basic.tsx @@ -4,12 +4,15 @@ import useAppUtils from '@/hooks/use-app-utils'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import ProviderLogo from '../components/provider-logo'; -import { maasProviderOptions } from '../config/providers'; +import { maasProviderOptions, ProviderEnum } from '../config/providers'; import { FormData } from '../config/types'; -const Basic = () => { +const Basic: React.FC<{ + onAPIKeyBlur?: (e: any) => void; +}> = ({ onAPIKeyBlur }) => { const intl = useIntl(); const form = Form.useFormInstance(); + const providerType = Form.useWatch(['config', 'type'], form); const { getRuleMessage } = useAppUtils(); const optionRender = (option: any) => { @@ -30,7 +33,17 @@ const Basic = () => { return ( <> - name="name" data-field="name"> + + name="name" + data-field="name" + required + rules={[ + { + required: true, + message: getRuleMessage('input', 'common.table.name') + } + ]} + > { rules={[ { required: true, - message: getRuleMessage( - 'select', - intl.formatMessage({ id: 'common.table.type' }) - ) + message: getRuleMessage('select', 'common.table.type') } ]} > @@ -63,6 +73,16 @@ const Basic = () => { })} /> + {providerType === ProviderEnum.OPENAI && ( + name={['config', 'openaiCustomUrl']}> + + + )} name="api_key" rules={[ @@ -74,6 +94,7 @@ const Basic = () => { > = forwardRef((props, ref) => { api_key: currentData.api_tokens?.[0] || '', api_tokens: currentData.api_tokens?.slice(1) || [], proxy_enabled: !!currentData.proxy_url, - custom_config: json2Yaml(_.omit(currentData.config, ['type']) || {}) + custom_config: json2Yaml( + _.omit(currentData.config, ['type', 'openaiCustomUrl']) || {} + ) }); } }, [form, currentData, action]); diff --git a/src/pages/maas-provider/forms/model-item.tsx b/src/pages/maas-provider/forms/model-item.tsx index c732ec79..03b63863 100644 --- a/src/pages/maas-provider/forms/model-item.tsx +++ b/src/pages/maas-provider/forms/model-item.tsx @@ -3,8 +3,8 @@ import SealSelect from '@/components/seal-form/seal-select'; import { categoryOptions } from '@/pages/llmodels/config'; import { CheckCircleFilled, - CloseCircleFilled, - LoadingOutlined + LoadingOutlined, + WarningFilled } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Button, Form, Tooltip } from 'antd'; @@ -27,9 +27,15 @@ const SelectWrapper = styled.div` `; interface ModelItemProps { - onOpenChange: (open: boolean) => void; + onOpenChange?: (open: boolean) => void; onChange: (data: ProviderModel) => void; - providerModelList: Global.BaseOption[]; + providerModelList: Global.BaseOption< + string, + { + category: string; + accessible: boolean; + } + >[]; selectedModelList: ProviderModel[]; item: ProviderModel; loading?: boolean; @@ -63,10 +69,9 @@ const ModelItem: React.FC = ({ }); }; - const handleOnChange = (value: string) => { + const handleOnChange = (value: string, option: any) => { onChange({ - ...item, - accessible: null, + ...option, name: value }); }; @@ -79,20 +84,26 @@ const ModelItem: React.FC = ({ }; const renderSuffixIcon = () => { + console.log( + 'testLoading', + testLoading, + item.accessible, + item.accessible === false + ); if (testLoading) { return ; } if (item.accessible === true) { return ( ); } if (item.accessible === false) { return ( - ); } diff --git a/src/pages/maas-provider/forms/supported-models.tsx b/src/pages/maas-provider/forms/supported-models.tsx index 4ba0a2ae..942045b2 100644 --- a/src/pages/maas-provider/forms/supported-models.tsx +++ b/src/pages/maas-provider/forms/supported-models.tsx @@ -1,6 +1,8 @@ import MetadataList from '@/components/metadata-list'; +import useAppUtils from '@/hooks/use-app-utils'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; +import { useRef } from 'react'; import { FormData, ProviderModel } from '../config/types'; import { useQueryProviderModels } from '../hooks/use-query-provider-models'; import ModelItem from './model-item'; @@ -11,15 +13,26 @@ const SupportedModels = () => { useQueryProviderModels(); const form = Form.useFormInstance(); const modelList = Form.useWatch('models', form) || []; + const prevAPIKeyRef = useRef(''); + const { getRuleMessage } = useAppUtils(); const handleOpenChange = async (open: boolean) => { try { await form.validateFields(['api_key']); - if (open && providerModelList.length === 0) { + const currentAPIKey = form.getFieldValue('api_key') || ''; + + // Avoid repeated requests with the same API key + if ( + open && + providerModelList.length === 0 && + prevAPIKeyRef.current !== currentAPIKey && + currentAPIKey + ) { + prevAPIKeyRef.current = currentAPIKey; fetchProviderModels({ data: { - api_token: form.getFieldValue('api_key') || '', + api_token: currentAPIKey, config: { type: form.getFieldValue(['config', 'type']) || '' } @@ -55,7 +68,33 @@ const SupportedModels = () => { return ( <> - + { + if (!value || value.length === 0) { + return Promise.reject( + new Error( + getRuleMessage('input', 'providers.form.rules.models') + ) + ); + } + + if (value.some((item: ProviderModel) => !item.name)) { + return Promise.reject( + new Error( + getRuleMessage('select', 'providers.form.rules.model') + ) + ); + } + return Promise.resolve(); + } + } + ]} + > { setProviderModelList( response.data?.map((item: any) => ({ label: item.id, - value: item.id + value: item.id, + accessible: item.accessible, + category: item.categories?.[0] || '' })) || [] ); }, diff --git a/src/pages/model-routes/config/index.ts b/src/pages/model-routes/config/index.ts index e66067be..ab61272b 100644 --- a/src/pages/model-routes/config/index.ts +++ b/src/pages/model-routes/config/index.ts @@ -14,7 +14,7 @@ export const TargetStatusLabelMap = { export const TargetStatus: Record = { [TargetStatusValueMap.Active]: StatusMaps.success, - [TargetStatusValueMap.Unavailable]: StatusMaps.error + [TargetStatusValueMap.Unavailable]: StatusMaps.warning }; // actions for each row diff --git a/src/pages/model-routes/config/types.ts b/src/pages/model-routes/config/types.ts index ad028542..83803b58 100644 --- a/src/pages/model-routes/config/types.ts +++ b/src/pages/model-routes/config/types.ts @@ -43,5 +43,8 @@ export interface RouteTarget { name: string; route_name: string; route_id: number; + provider_id: number; + provider_model_name: string; + fallback_status_codes: string[]; state: string; } diff --git a/src/pages/model-routes/forms/targets.tsx b/src/pages/model-routes/forms/targets.tsx index 7934c8ba..245bc3db 100644 --- a/src/pages/model-routes/forms/targets.tsx +++ b/src/pages/model-routes/forms/targets.tsx @@ -1,6 +1,7 @@ import MetadataList from '@/components/metadata-list'; import SealCascader from '@/components/seal-form/seal-cascader'; import SealInput from '@/components/seal-form/seal-input'; +import useAppUtils from '@/hooks/use-app-utils'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import _ from 'lodash'; @@ -10,6 +11,7 @@ import useTargetSourceModels from '../hooks/use-target-source-models'; const TargetsForm = forwardRef((props, ref) => { const intl = useIntl(); + const { getRuleMessage } = useAppUtils(); const { sourceModels, loading, fetchSourceModels } = useTargetSourceModels(); const form = Form.useFormInstance(); const targets = Form.useWatch('targets', form) || []; @@ -38,19 +40,11 @@ const TargetsForm = forwardRef((props, ref) => { })); const handleTargetsChange = (value: any[], index: number, options: any[]) => { - console.log( - 'handleTargetsChange:', - value, - index, - options, - dataList, - sourceModels - ); const selectedOption = options?.find?.((opt) => opt.value === value[1]) || {}; const targetList = [...targets]; targetList[index] = { - weight: targetList[index]?.weight || null, + weight: targetList[index]?.weight, ...selectedOption?.data }; @@ -68,11 +62,19 @@ const TargetsForm = forwardRef((props, ref) => { const newDataList = [ ...dataList, { - weight: null, + weight: 100, value: [] } ]; setDataList(newDataList); + const newTargets = [ + ...targets, + { + weight: 100, + value: [] + } + ]; + form.setFieldValue('targets', newTargets); }; const handleOnDelete = (index: number, item: any) => { @@ -85,13 +87,19 @@ const TargetsForm = forwardRef((props, ref) => { }; const handleFallbackChange = (value: any[], options?: any[]) => { + if (!value || value.length === 0) { + form.setFieldValue('fallback_target', null); + setFallbackValues({ + value: [] + }); + return; + } const selectedOption = options?.find?.((opt) => opt.value === value[1]) || {}; form.setFieldValue('fallback_target', { ...selectedOption?.data }); - console.log('handleFallbackChange:', value, dataList); setFallbackValues({ value: value }); @@ -163,27 +171,29 @@ const TargetsForm = forwardRef((props, ref) => { name="targets" data-field="targets" rules={[ - ({ getFieldValue }) => ({ + { validator(rule, value) { - if (_.keys(value).length > 0) { - if (_.some(_.keys(value), (k: string) => !value[k])) { + if (value && value?.length > 0) { + if (_.some(value, (item: any) => !item.weight)) { return Promise.reject( - intl.formatMessage( - { - id: 'common.validate.value' - }, - { - name: intl.formatMessage({ - id: 'models.form.selector' - }) - } - ) + getRuleMessage('input', 'routes.form.target.weight') + ); + } + + if ( + _.some( + dataList, + (item: any) => !item.value || item.value.length === 0 + ) + ) { + return Promise.reject( + getRuleMessage('input', 'providers.form.target.placeholder') ); } } return Promise.resolve(); } - }) + } ]} > { { handleOnWeightChange(value, index)} placeholder={intl.formatMessage({ diff --git a/src/pages/model-routes/hooks/use-access-control.ts b/src/pages/model-routes/hooks/use-access-control.ts index 2bc78728..8e1a5e87 100644 --- a/src/pages/model-routes/hooks/use-access-control.ts +++ b/src/pages/model-routes/hooks/use-access-control.ts @@ -1,9 +1,11 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; +import useBodyScroll from '@/hooks/use-body-scroll'; import { useState } from 'react'; import { RouteItem as ListItem } from '../config/types'; const useAccessControl = () => { + const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const [openModalStatus, setOpenModalStatus] = useState<{ open: boolean; action: PageActionType; @@ -27,6 +29,7 @@ const useAccessControl = () => { currentData, title: title }); + saveScrollHeight(); }; const closeModal = () => { @@ -36,6 +39,7 @@ const useAccessControl = () => { currentData: undefined, title: '' }); + restoreScrollHeight(); }; return { diff --git a/src/pages/model-routes/hooks/use-create-route.ts b/src/pages/model-routes/hooks/use-create-route.ts index a7ef2138..c4af6da7 100644 --- a/src/pages/model-routes/hooks/use-create-route.ts +++ b/src/pages/model-routes/hooks/use-create-route.ts @@ -1,9 +1,11 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; +import useBodyScroll from '@/hooks/use-body-scroll'; import { useState } from 'react'; import { RouteItem as ListItem } from '../config/types'; const useCreateRoute = (options?: { refresh: () => void }) => { + const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const [openModalStatus, setOpenModalStatus] = useState<{ open: boolean; action: PageActionType; @@ -27,6 +29,7 @@ const useCreateRoute = (options?: { refresh: () => void }) => { currentData, title: title }); + saveScrollHeight(); }; const closeModal = () => { @@ -36,6 +39,7 @@ const useCreateRoute = (options?: { refresh: () => void }) => { currentData: undefined, title: '' }); + restoreScrollHeight(); }; return { diff --git a/src/pages/model-routes/index.tsx b/src/pages/model-routes/index.tsx index 19d312b0..670c165d 100644 --- a/src/pages/model-routes/index.tsx +++ b/src/pages/model-routes/index.tsx @@ -275,7 +275,7 @@ const ModelRoutes: React.FC = () => { loadend={dataSource.loadend} rowSelection={rowSelection} columns={columns} - childParentKey="access_id" + childParentKey="route_id" expandable={true} empty={