fix: ui issues

This commit is contained in:
jialin
2026-02-06 11:13:49 +08:00
parent d54722609c
commit 7f3d6bb55f
35 changed files with 364 additions and 141 deletions
+2 -1
View File
@@ -29,6 +29,7 @@ const SealAutoComplete: React.FC<
extra, extra,
style, style,
addAfter, addAfter,
suffixIcon,
loading, loading,
allowClear, allowClear,
clearSpaceOnBlur, clearSpaceOnBlur,
@@ -103,7 +104,7 @@ const SealAutoComplete: React.FC<
</Link> </Link>
); );
} }
return null; return suffixIcon || null;
}; };
const popupRender = (originNode: React.ReactElement): React.ReactElement => { const popupRender = (originNode: React.ReactElement): React.ReactElement => {
+1 -1
View File
@@ -22,7 +22,7 @@ const SealInputNumber: React.FC<InputNumberProps & SealFormItemProps> = (
let status = ''; let status = '';
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = props.status || statusData?.status || '';
} else { } else {
status = props.status || ''; status = props.status || '';
} }
+1 -1
View File
@@ -51,8 +51,8 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
e.target.value = e.target.value?.trim?.(); e.target.value = e.target.value?.trim?.();
if (!inputRef.current?.input?.value) { if (!inputRef.current?.input?.value) {
setIsFocus(false); setIsFocus(false);
props.onBlur?.(e);
} }
props.onBlur?.(e);
}; };
return ( return (
+3 -1
View File
@@ -106,7 +106,9 @@ const SealCascader: React.FC<
// the status can be controlled by Form.Item // the status can be controlled by Form.Item
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = props.status || statusData?.status || '';
} else {
status = props.status || '';
} }
const _options = useMemo(() => { const _options = useMemo(() => {
+1 -1
View File
@@ -30,7 +30,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
let status = ''; let status = '';
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = props.status || statusData?.status || '';
} else { } else {
status = props.status || ''; status = props.status || '';
} }
+5 -2
View File
@@ -181,8 +181,11 @@ const SelectWrapper = styled.div`
} }
&.validate-status-error { &.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; border-color: var(--ant-color-error) !important;
} }
} }
+1 -1
View File
@@ -283,7 +283,7 @@ export default function useTableFetch<T>(
} }
) => { ) => {
loadendRef.current = false; loadendRef.current = false;
const newQueryParams = { ...queryParams, ...params }; const newQueryParams = { ...queryParams, ...params, page: 1 };
setQueryParams(newQueryParams); setQueryParams(newQueryParams);
await fetchData({ query: newQueryParams }); await fetchData({ query: newQueryParams });
if (watch && !options?.paginate) { if (watch && !options?.paginate) {
+4 -1
View File
@@ -80,5 +80,8 @@ export default {
'benchmark.env.system': 'System', 'benchmark.env.system': 'System',
'benchmark.env.runtimeVersion': 'Runtime Version', 'benchmark.env.runtimeVersion': 'Runtime Version',
'benchmark.env.driverVersion': 'Driver 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'
}; };
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
'providers.form.model.test': 'Test', 'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection', 'providers.form.model.test.tips': 'Test Connection',
'providers.form.target.placeholder': 'provider/models', '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'
}; };
+4 -1
View File
@@ -80,5 +80,8 @@ export default {
'benchmark.env.system': 'System', 'benchmark.env.system': 'System',
'benchmark.env.runtimeVersion': 'Runtime Version', 'benchmark.env.runtimeVersion': 'Runtime Version',
'benchmark.env.driverVersion': 'Driver 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'
}; };
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
'providers.form.model.test': 'Test', 'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection', 'providers.form.model.test.tips': 'Test Connection',
'providers.form.target.placeholder': 'provider/models', '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'
}; };
+4 -1
View File
@@ -80,5 +80,8 @@ export default {
'benchmark.env.system': 'System', 'benchmark.env.system': 'System',
'benchmark.env.runtimeVersion': 'Runtime Version', 'benchmark.env.runtimeVersion': 'Runtime Version',
'benchmark.env.driverVersion': 'Driver 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'
}; };
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
'providers.form.model.test': 'Test', 'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection', 'providers.form.model.test.tips': 'Test Connection',
'providers.form.target.placeholder': 'provider/models', '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'
}; };
+3 -1
View File
@@ -80,5 +80,7 @@ export default {
'benchmark.env.system': '系统', 'benchmark.env.system': '系统',
'benchmark.env.runtimeVersion': '运行时版本', 'benchmark.env.runtimeVersion': '运行时版本',
'benchmark.env.driverVersion': '驱动版本', 'benchmark.env.driverVersion': '驱动版本',
'benchmark.env.cpuCounts': 'CPU 核心数' 'benchmark.env.cpuCounts': 'CPU 核心数',
'benchmark.table.export.results': '导出结果',
'benchmark.form.nonLlmModel.tips': '基准测试目前仅支持 LLM 模型'
}; };
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
'providers.form.model.test': '测试', 'providers.form.model.test': '测试',
'providers.form.model.test.tips': '测试连接', 'providers.form.model.test.tips': '测试连接',
'providers.form.target.placeholder': '提供商/模型', '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': '请选择模型'
}; };
@@ -25,7 +25,7 @@ const RightActions: React.FC<RightActionsProps> = ({
}) => { }) => {
const ButtonList = [ const ButtonList = [
{ {
label: 'common.button.export', label: 'benchmark.table.export.results',
key: 'export', key: 'export',
icon: ( icon: (
<IconFont type="icon-export" style={{ lineHeight: 1, fontSize: 16 }} /> <IconFont type="icon-export" style={{ lineHeight: 1, fontSize: 16 }} />
+12 -12
View File
@@ -8,17 +8,6 @@ import { BenchmarkStatusValueMap } from '../config';
import { BenchmarkListItem as ListItem } from '../config/types'; import { BenchmarkListItem as ListItem } from '../config/types';
const actionList = [ 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', label: 'common.button.viewlog',
key: 'viewlog', key: 'viewlog',
@@ -30,9 +19,20 @@ const actionList = [
], ],
icon: <IconFont type="icon-logs" /> icon: <IconFont type="icon-logs" />
}, },
{
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', key: 'export',
label: 'common.button.export', label: 'benchmark.table.export.results',
icon: ( icon: (
<IconFont type="icon-export" style={{ lineHeight: 1, fontSize: 16 }} /> <IconFont type="icon-export" style={{ lineHeight: 1, fontSize: 16 }} />
) )
+27 -8
View File
@@ -3,19 +3,22 @@ import { PageAction } from '@/config';
import useAppUtils from '@/hooks/use-app-utils'; import useAppUtils from '@/hooks/use-app-utils';
import { import {
InstanceStatusMap, InstanceStatusMap,
InstanceStatusMapValue InstanceStatusMapValue,
modelCategoriesMap
} from '@/pages/llmodels/config'; } from '@/pages/llmodels/config';
import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark'; import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark';
import { useQueryModelInstancesList } from '@/pages/llmodels/services/use-query-model-instances'; import { useQueryModelInstancesList } from '@/pages/llmodels/services/use-query-model-instances';
import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-list'; import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-list';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form, Tooltip } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
// benchmark.form.nonLlmModel.tips
const InstanceNode = (props: any) => { const InstanceNode = (props: any) => {
const { data: instance } = props; const { data: instance } = props;
const intl = useIntl();
return instance.isLeaf ? ( return instance.isLeaf ? (
<span className="flex-center"> <span className="flex-center">
{instance.label} {instance.label}
@@ -24,7 +27,17 @@ const InstanceNode = (props: any) => {
)} )}
</span> </span>
) : ( ) : (
<span>{instance.label}</span> <>
{instance.disabled ? (
<Tooltip
title={intl.formatMessage({ id: 'benchmark.form.nonLlmModel.tips' })}
>
<span>{instance.label}</span>
</Tooltip>
) : (
<span>{instance.label}</span>
)}
</>
); );
}; };
@@ -99,6 +112,7 @@ const ModelInstanceForm: React.FC = () => {
.map((model: any) => ({ .map((model: any) => ({
label: model.name, label: model.name,
value: model.name, value: model.name,
disabled: modelCategoriesMap.llm !== model.categories?.[0],
id: model.id, id: model.id,
isLeaf: false, isLeaf: false,
children: [] children: []
@@ -109,12 +123,17 @@ const ModelInstanceForm: React.FC = () => {
} }
// preload instances for the first model // 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) => const instanceOptions = instanceList.map((instance: any) =>
renderInstance(instance) renderInstance(instance)
); );
if (modelOptions[0]) { if (selectedllmModel) {
modelOptions[0].children = [...instanceOptions] as never[]; selectedllmModel.children = [...instanceOptions] as never[];
} }
// init form value for model instance // init form value for model instance
@@ -127,8 +146,8 @@ const ModelInstanceForm: React.FC = () => {
}); });
} else { } else {
handleOnChange( handleOnChange(
[modelOptions[0].value, instanceOptions[0]?.value], [selectedllmModel.value, instanceOptions[0]?.value],
[modelOptions[0], instanceOptions[0]] [selectedllmModel, instanceOptions[0]]
); );
} }
+6 -2
View File
@@ -147,7 +147,7 @@ const Benchmark: React.FC = () => {
} else if (val === 'stop') { } else if (val === 'stop') {
handleStopBenchmark(row.id); handleStopBenchmark(row.id);
} else if (val === 'export') { } else if (val === 'export') {
exportData([row.id]); exportData([row.id], row.name);
} }
}); });
@@ -192,7 +192,11 @@ const Benchmark: React.FC = () => {
}); });
const handleExportData = () => { 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 ( return (
@@ -1,6 +1,7 @@
import { GPUSTACK_API_BASE_URL } from '@/config/settings'; import { GPUSTACK_API_BASE_URL } from '@/config/settings';
import { downloadFile } from '@/utils/download-stream'; import { downloadFile } from '@/utils/download-stream';
import { message } from 'antd'; import { message } from 'antd';
import dayjs from 'dayjs';
import { EXPORT_BENCHMARK_LIST } from '../apis'; import { EXPORT_BENCHMARK_LIST } from '../apis';
const matchFilename = (disposition: string | null): string | undefined => { const matchFilename = (disposition: string | null): string | undefined => {
@@ -12,7 +13,9 @@ const matchFilename = (disposition: string | null): string | undefined => {
}; };
export function useExportBenchmark() { 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 { try {
const res = await fetch( const res = await fetch(
`${GPUSTACK_API_BASE_URL}${EXPORT_BENCHMARK_LIST}`, `${GPUSTACK_API_BASE_URL}${EXPORT_BENCHMARK_LIST}`,
@@ -26,8 +29,7 @@ export function useExportBenchmark() {
); );
// header // header
const contentDispostion = res.headers.get('content-Disposition'); const contentDispostion = res.headers.get('content-Disposition');
const filename = const filename = matchFilename(contentDispostion) || `${fileName}.yml`;
matchFilename(contentDispostion) || `benchmark-export.yml`;
if (res.ok) { if (res.ok) {
const blob = await res.blob(); const blob = await res.blob();
downloadFile(blob, filename); downloadFile(blob, filename);
@@ -15,7 +15,8 @@ const CloudOptions: React.FC<{
const { disabled } = props; const { disabled } = props;
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance(); 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< const [validateStatusList, setValidateStatusList] = useState<
{ [key: string]: statusType }[] { [key: string]: statusType }[]
>([]); >([]);
@@ -45,10 +46,15 @@ const CloudOptions: React.FC<{
form.setFieldValue(['cloud_options', volumeOptions.name], data); form.setFieldValue(['cloud_options', volumeOptions.name], data);
toggleValidation(true); toggleValidation(true);
}; };
console.log('disabled=====', disabled);
return ( return (
<Form.Item <Form.Item
name={['cloud_options', volumeOptions.name as string]} name={['cloud_options', volumeOptions.name as string]}
style={{
backgroundColor:
disabled && volumes?.length === 0
? 'var(--ant-color-bg-container-disabled)'
: ''
}}
rules={[ rules={[
{ {
validator: listMapValidator validator: listMapValidator
@@ -1,10 +1,8 @@
import AutoTooltip from '@/components/auto-tooltip'; import AutoTooltip from '@/components/auto-tooltip';
import { import OverlayScroller from '@/components/overlay-scroller';
CheckCircleOutlined, import { CheckCircleOutlined } from '@ant-design/icons';
CloseCircleOutlined, import { useIntl } from '@umijs/max';
WarningOutlined import { Flex, Popover, Tag } from 'antd';
} from '@ant-design/icons';
import { Flex, Tag } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import { categoryConfig } from '../../_components/model-tag'; import { categoryConfig } from '../../_components/model-tag';
@@ -17,47 +15,101 @@ interface ProviderModelProps {
const ProviderModels: React.FC<ProviderModelProps> = ({ dataList }) => { const ProviderModels: React.FC<ProviderModelProps> = ({ dataList }) => {
const iconsMap = { const iconsMap = {
accessible: <CheckCircleOutlined />, accessible: <CheckCircleOutlined />,
inaccessible: <CloseCircleOutlined />, inaccessible: null,
none: <WarningOutlined /> 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) => (
<Tag
key={index}
icon={
_.isBoolean(model.accessible)
? iconsMap[model.accessible ? 'accessible' : 'inaccessible']
: iconsMap['none']
}
variant="outlined"
styles={{
root: {
backgroundColor: 'transparent',
display: 'flex',
alignItems: 'center',
borderRadius: 4
}
}}
color={
model.accessible === true ? 'var(--ant-color-success)' : 'default'
}
>
<span className="flex-center">
<AutoTooltip ghost maxWidth={'120px'}>
{model.name}
</AutoTooltip>
<span style={{ marginLeft: 8 }}>
{categoryConfig[model.category]?.icon}
</span>
</span>
</Tag>
))}
</>
);
}; };
return ( return (
<Flex gap="8px" wrap="wrap"> <div>
{dataList.map((model) => ( <Flex gap="8px" wrap="wrap">
<Tag {renderModels(head12Items)}
key={model.name} {restItems.length > 0 && (
icon={ <Popover
_.isBoolean(model.accessible) placement="right"
? iconsMap[model.accessible ? 'accessible' : 'inaccessible'] content={
: iconsMap['none'] <OverlayScroller
} maxHeight={420}
variant="outlined" styles={{
styles={{ wrapper: {
root: { paddingInlineStart: 0
backgroundColor: 'transparent', }
display: 'flex', }}
alignItems: 'center', >
borderRadius: 4 <Flex gap="8px" wrap="wrap">
{renderModels(restItems)}
</Flex>
</OverlayScroller>
} }
}} styles={{
color={ root: { maxWidth: '400px' },
model.accessible === true container: {
? 'success' paddingInlineEnd: 4
: model.accessible === false }
? 'error' }}
: 'warning' >
} <Tag
> variant="outlined"
<span className="flex-center"> styles={{
<AutoTooltip ghost maxWidth={'120px'}> root: {
{model.name} width: 'fit-content',
</AutoTooltip> backgroundColor: 'transparent',
<span style={{ marginLeft: 8 }}> display: 'flex',
{categoryConfig[model.category]?.icon} alignItems: 'center',
</span> borderRadius: 4
</span> }
</Tag> }}
))} >
</Flex> {intl.formatMessage(
{ id: 'providers.form.more' },
{ count: restItems.length }
)}
</Tag>
</Popover>
)}
</Flex>
</div>
); );
}; };
+1
View File
@@ -12,6 +12,7 @@ export interface FormData {
api_key: string; api_key: string;
config: { config: {
type: maasProviderType; type: maasProviderType;
openaiCustomUrl?: string;
[key: string]: any; [key: string]: any;
}; };
} }
+1 -4
View File
@@ -42,10 +42,7 @@ const AccessToken = () => {
rules={[ rules={[
{ {
required: false, required: false,
message: getRuleMessage( message: getRuleMessage('input', 'providers.form.tokens.title')
'input',
intl.formatMessage({ id: 'providers.form.tokens.title' })
)
} }
]} ]}
> >
+28 -7
View File
@@ -4,12 +4,15 @@ import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import ProviderLogo from '../components/provider-logo'; import ProviderLogo from '../components/provider-logo';
import { maasProviderOptions } from '../config/providers'; import { maasProviderOptions, ProviderEnum } from '../config/providers';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
const Basic = () => { const Basic: React.FC<{
onAPIKeyBlur?: (e: any) => void;
}> = ({ onAPIKeyBlur }) => {
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const providerType = Form.useWatch(['config', 'type'], form);
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
const optionRender = (option: any) => { const optionRender = (option: any) => {
@@ -30,7 +33,17 @@ const Basic = () => {
return ( return (
<> <>
<Form.Item<FormData> name="name" data-field="name"> <Form.Item<FormData>
name="name"
data-field="name"
required
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
}
]}
>
<SealInput.Input <SealInput.Input
required required
label={intl.formatMessage({ label={intl.formatMessage({
@@ -43,10 +56,7 @@ const Basic = () => {
rules={[ rules={[
{ {
required: true, required: true,
message: getRuleMessage( message: getRuleMessage('select', 'common.table.type')
'select',
intl.formatMessage({ id: 'common.table.type' })
)
} }
]} ]}
> >
@@ -63,6 +73,16 @@ const Basic = () => {
})} })}
/> />
</Form.Item> </Form.Item>
{providerType === ProviderEnum.OPENAI && (
<Form.Item<FormData> name={['config', 'openaiCustomUrl']}>
<SealInput.Input
placeholder="http://<your-inference-server>/v1"
label={intl.formatMessage({
id: 'providers.form.custombeckendUrl'
})}
/>
</Form.Item>
)}
<Form.Item<FormData> <Form.Item<FormData>
name="api_key" name="api_key"
rules={[ rules={[
@@ -74,6 +94,7 @@ const Basic = () => {
> >
<SealInput.Password <SealInput.Password
required required
onBlur={onAPIKeyBlur}
label={intl.formatMessage({ label={intl.formatMessage({
id: 'providers.form.tokens.title' id: 'providers.form.tokens.title'
})} })}
+3 -1
View File
@@ -107,7 +107,9 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
api_key: currentData.api_tokens?.[0] || '', api_key: currentData.api_tokens?.[0] || '',
api_tokens: currentData.api_tokens?.slice(1) || [], api_tokens: currentData.api_tokens?.slice(1) || [],
proxy_enabled: !!currentData.proxy_url, proxy_enabled: !!currentData.proxy_url,
custom_config: json2Yaml(_.omit(currentData.config, ['type']) || {}) custom_config: json2Yaml(
_.omit(currentData.config, ['type', 'openaiCustomUrl']) || {}
)
}); });
} }
}, [form, currentData, action]); }, [form, currentData, action]);
+21 -10
View File
@@ -3,8 +3,8 @@ import SealSelect from '@/components/seal-form/seal-select';
import { categoryOptions } from '@/pages/llmodels/config'; import { categoryOptions } from '@/pages/llmodels/config';
import { import {
CheckCircleFilled, CheckCircleFilled,
CloseCircleFilled, LoadingOutlined,
LoadingOutlined WarningFilled
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Form, Tooltip } from 'antd'; import { Button, Form, Tooltip } from 'antd';
@@ -27,9 +27,15 @@ const SelectWrapper = styled.div`
`; `;
interface ModelItemProps { interface ModelItemProps {
onOpenChange: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
onChange: (data: ProviderModel) => void; onChange: (data: ProviderModel) => void;
providerModelList: Global.BaseOption<string>[]; providerModelList: Global.BaseOption<
string,
{
category: string;
accessible: boolean;
}
>[];
selectedModelList: ProviderModel[]; selectedModelList: ProviderModel[];
item: ProviderModel; item: ProviderModel;
loading?: boolean; loading?: boolean;
@@ -63,10 +69,9 @@ const ModelItem: React.FC<ModelItemProps> = ({
}); });
}; };
const handleOnChange = (value: string) => { const handleOnChange = (value: string, option: any) => {
onChange({ onChange({
...item, ...option,
accessible: null,
name: value name: value
}); });
}; };
@@ -79,20 +84,26 @@ const ModelItem: React.FC<ModelItemProps> = ({
}; };
const renderSuffixIcon = () => { const renderSuffixIcon = () => {
console.log(
'testLoading',
testLoading,
item.accessible,
item.accessible === false
);
if (testLoading) { if (testLoading) {
return <LoadingOutlined />; return <LoadingOutlined />;
} }
if (item.accessible === true) { if (item.accessible === true) {
return ( return (
<CheckCircleFilled <CheckCircleFilled
style={{ color: 'var(--ant-color-success)', fontSize: 16 }} style={{ color: 'var(--ant-color-success)', fontSize: 14 }}
/> />
); );
} }
if (item.accessible === false) { if (item.accessible === false) {
return ( return (
<CloseCircleFilled <WarningFilled
style={{ color: 'var(--ant-color-error)', fontSize: 16 }} style={{ color: 'var(--ant-color-error)', fontSize: 14 }}
/> />
); );
} }
@@ -1,6 +1,8 @@
import MetadataList from '@/components/metadata-list'; import MetadataList from '@/components/metadata-list';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { useRef } from 'react';
import { FormData, ProviderModel } from '../config/types'; import { FormData, ProviderModel } from '../config/types';
import { useQueryProviderModels } from '../hooks/use-query-provider-models'; import { useQueryProviderModels } from '../hooks/use-query-provider-models';
import ModelItem from './model-item'; import ModelItem from './model-item';
@@ -11,15 +13,26 @@ const SupportedModels = () => {
useQueryProviderModels(); useQueryProviderModels();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const modelList = Form.useWatch('models', form) || []; const modelList = Form.useWatch('models', form) || [];
const prevAPIKeyRef = useRef<string>('');
const { getRuleMessage } = useAppUtils();
const handleOpenChange = async (open: boolean) => { const handleOpenChange = async (open: boolean) => {
try { try {
await form.validateFields(['api_key']); 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({ fetchProviderModels({
data: { data: {
api_token: form.getFieldValue('api_key') || '', api_token: currentAPIKey,
config: { config: {
type: form.getFieldValue(['config', 'type']) || '' type: form.getFieldValue(['config', 'type']) || ''
} }
@@ -55,7 +68,33 @@ const SupportedModels = () => {
return ( return (
<> <>
<Form.Item name="models" data-field="supportedModels"> <Form.Item
name="models"
data-field="supportedModels"
rules={[
{
required: true,
validator: async (_, value) => {
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();
}
}
]}
>
<MetadataList <MetadataList
styles={{ styles={{
wrapper: { wrapper: {
@@ -33,7 +33,9 @@ export const useQueryProviderModels = () => {
setProviderModelList( setProviderModelList(
response.data?.map((item: any) => ({ response.data?.map((item: any) => ({
label: item.id, label: item.id,
value: item.id value: item.id,
accessible: item.accessible,
category: item.categories?.[0] || ''
})) || [] })) || []
); );
}, },
+1 -1
View File
@@ -14,7 +14,7 @@ export const TargetStatusLabelMap = {
export const TargetStatus: Record<string, StatusType> = { export const TargetStatus: Record<string, StatusType> = {
[TargetStatusValueMap.Active]: StatusMaps.success, [TargetStatusValueMap.Active]: StatusMaps.success,
[TargetStatusValueMap.Unavailable]: StatusMaps.error [TargetStatusValueMap.Unavailable]: StatusMaps.warning
}; };
// actions for each row // actions for each row
+3
View File
@@ -43,5 +43,8 @@ export interface RouteTarget {
name: string; name: string;
route_name: string; route_name: string;
route_id: number; route_id: number;
provider_id: number;
provider_model_name: string;
fallback_status_codes: string[];
state: string; state: string;
} }
+40 -25
View File
@@ -1,6 +1,7 @@
import MetadataList from '@/components/metadata-list'; import MetadataList from '@/components/metadata-list';
import SealCascader from '@/components/seal-form/seal-cascader'; import SealCascader from '@/components/seal-form/seal-cascader';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
@@ -10,6 +11,7 @@ import useTargetSourceModels from '../hooks/use-target-source-models';
const TargetsForm = forwardRef((props, ref) => { const TargetsForm = forwardRef((props, ref) => {
const intl = useIntl(); const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const { sourceModels, loading, fetchSourceModels } = useTargetSourceModels(); const { sourceModels, loading, fetchSourceModels } = useTargetSourceModels();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const targets = Form.useWatch('targets', form) || []; const targets = Form.useWatch('targets', form) || [];
@@ -38,19 +40,11 @@ const TargetsForm = forwardRef((props, ref) => {
})); }));
const handleTargetsChange = (value: any[], index: number, options: any[]) => { const handleTargetsChange = (value: any[], index: number, options: any[]) => {
console.log(
'handleTargetsChange:',
value,
index,
options,
dataList,
sourceModels
);
const selectedOption = const selectedOption =
options?.find?.((opt) => opt.value === value[1]) || {}; options?.find?.((opt) => opt.value === value[1]) || {};
const targetList = [...targets]; const targetList = [...targets];
targetList[index] = { targetList[index] = {
weight: targetList[index]?.weight || null, weight: targetList[index]?.weight,
...selectedOption?.data ...selectedOption?.data
}; };
@@ -68,11 +62,19 @@ const TargetsForm = forwardRef((props, ref) => {
const newDataList = [ const newDataList = [
...dataList, ...dataList,
{ {
weight: null, weight: 100,
value: [] value: []
} }
]; ];
setDataList(newDataList); setDataList(newDataList);
const newTargets = [
...targets,
{
weight: 100,
value: []
}
];
form.setFieldValue('targets', newTargets);
}; };
const handleOnDelete = (index: number, item: any) => { const handleOnDelete = (index: number, item: any) => {
@@ -85,13 +87,19 @@ const TargetsForm = forwardRef((props, ref) => {
}; };
const handleFallbackChange = (value: any[], options?: any[]) => { const handleFallbackChange = (value: any[], options?: any[]) => {
if (!value || value.length === 0) {
form.setFieldValue('fallback_target', null);
setFallbackValues({
value: []
});
return;
}
const selectedOption = const selectedOption =
options?.find?.((opt) => opt.value === value[1]) || {}; options?.find?.((opt) => opt.value === value[1]) || {};
form.setFieldValue('fallback_target', { form.setFieldValue('fallback_target', {
...selectedOption?.data ...selectedOption?.data
}); });
console.log('handleFallbackChange:', value, dataList);
setFallbackValues({ setFallbackValues({
value: value value: value
}); });
@@ -163,27 +171,29 @@ const TargetsForm = forwardRef((props, ref) => {
name="targets" name="targets"
data-field="targets" data-field="targets"
rules={[ rules={[
({ getFieldValue }) => ({ {
validator(rule, value) { validator(rule, value) {
if (_.keys(value).length > 0) { if (value && value?.length > 0) {
if (_.some(_.keys(value), (k: string) => !value[k])) { if (_.some(value, (item: any) => !item.weight)) {
return Promise.reject( return Promise.reject(
intl.formatMessage( getRuleMessage('input', 'routes.form.target.weight')
{ );
id: 'common.validate.value' }
},
{ if (
name: intl.formatMessage({ _.some(
id: 'models.form.selector' dataList,
}) (item: any) => !item.value || item.value.length === 0
} )
) ) {
return Promise.reject(
getRuleMessage('input', 'providers.form.target.placeholder')
); );
} }
} }
return Promise.resolve(); return Promise.resolve();
} }
}) }
]} ]}
> >
<MetadataList <MetadataList
@@ -203,6 +213,9 @@ const TargetsForm = forwardRef((props, ref) => {
<SealCascader <SealCascader
required required
showSearch showSearch
status={
!item.value || item.value.length === 0 ? 'error' : 'success'
}
expandTrigger="hover" expandTrigger="hover"
multiple={false} multiple={false}
alwaysFocus={true} alwaysFocus={true}
@@ -235,6 +248,8 @@ const TargetsForm = forwardRef((props, ref) => {
<SealInput.Number <SealInput.Number
style={{ flex: 100 }} style={{ flex: 100 }}
min={0} min={0}
step={1}
status={item.weight === null ? 'error' : 'success'}
value={item.weight} value={item.weight}
onChange={(value) => handleOnWeightChange(value, index)} onChange={(value) => handleOnWeightChange(value, index)}
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
@@ -1,9 +1,11 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react'; import { useState } from 'react';
import { RouteItem as ListItem } from '../config/types'; import { RouteItem as ListItem } from '../config/types';
const useAccessControl = () => { const useAccessControl = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{ const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean; open: boolean;
action: PageActionType; action: PageActionType;
@@ -27,6 +29,7 @@ const useAccessControl = () => {
currentData, currentData,
title: title title: title
}); });
saveScrollHeight();
}; };
const closeModal = () => { const closeModal = () => {
@@ -36,6 +39,7 @@ const useAccessControl = () => {
currentData: undefined, currentData: undefined,
title: '' title: ''
}); });
restoreScrollHeight();
}; };
return { return {
@@ -1,9 +1,11 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react'; import { useState } from 'react';
import { RouteItem as ListItem } from '../config/types'; import { RouteItem as ListItem } from '../config/types';
const useCreateRoute = (options?: { refresh: () => void }) => { const useCreateRoute = (options?: { refresh: () => void }) => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{ const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean; open: boolean;
action: PageActionType; action: PageActionType;
@@ -27,6 +29,7 @@ const useCreateRoute = (options?: { refresh: () => void }) => {
currentData, currentData,
title: title title: title
}); });
saveScrollHeight();
}; };
const closeModal = () => { const closeModal = () => {
@@ -36,6 +39,7 @@ const useCreateRoute = (options?: { refresh: () => void }) => {
currentData: undefined, currentData: undefined,
title: '' title: ''
}); });
restoreScrollHeight();
}; };
return { return {
+1 -1
View File
@@ -275,7 +275,7 @@ const ModelRoutes: React.FC = () => {
loadend={dataSource.loadend} loadend={dataSource.loadend}
rowSelection={rowSelection} rowSelection={rowSelection}
columns={columns} columns={columns}
childParentKey="access_id" childParentKey="route_id"
expandable={true} expandable={true}
empty={ empty={
<NoResult <NoResult