fix: check form data after blur

main
jialin 10 months ago
parent 4755cf6a25
commit afacd2a8a0

@ -5,6 +5,7 @@ import ClipboardJS from 'clipboard';
import React, { useEffect, useRef, useState } from 'react';
type CopyButtonProps = {
children?: React.ReactNode;
text: string;
disabled?: boolean;
fontSize?: string;
@ -26,6 +27,7 @@ type CopyButtonProps = {
};
const CopyButton: React.FC<CopyButtonProps> = ({
children,
tips,
text,
disabled,
@ -91,7 +93,9 @@ const CopyButton: React.FC<CopyButtonProps> = ({
<CopyOutlined style={{ fontSize: fontSize, ...style }} />
)
}
></Button>
>
{children}
</Button>
</Tooltip>
);
};

@ -9,11 +9,13 @@ interface LabelSelectorProps {
btnText?: string;
description?: React.ReactNode;
onChange?: (labels: Record<string, any>) => void;
onBlur?: (e: any, type: string, index: number) => void;
}
const LabelSelector: React.FC<LabelSelectorProps> = ({
labels,
onChange,
onBlur,
label,
btnText,
description
@ -87,6 +89,7 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
onChange={handleLabelsChange}
onLabelListChange={handleLabelListChange}
onPaste={handleOnPaste}
onBlur={onBlur}
/>
);
};

@ -13,6 +13,7 @@ interface LabelSelectorProps {
onLabelListChange: (list: { key: string; value: string }[]) => void;
onChange?: (labels: Record<string, any>) => void;
onPaste?: (e: any, index: number) => void;
onBlur?: (e: any, type: string, index: number) => void;
description?: React.ReactNode;
}
@ -22,6 +23,7 @@ const Inner: React.FC<LabelSelectorProps> = ({
onChange,
onLabelListChange,
onPaste,
onBlur,
label,
btnText,
description
@ -82,6 +84,7 @@ const Inner: React.FC<LabelSelectorProps> = ({
onDelete={() => handleOnDelete(index)}
onChange={(obj) => handleOnChange(index, obj)}
onPaste={(e) => onPaste?.(e, index)}
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
/>
);
})}

@ -19,6 +19,7 @@ interface LabelItemProps {
labelList: { key: string; value: string }[];
onChange?: (params: { key: string; value: string }) => void;
onPaste?: (e: any) => void;
onBlur?: (e: any, type: string) => void;
}
const LabelItem: React.FC<LabelItemProps> = ({
label,
@ -28,7 +29,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
valueAddon,
onChange,
onDelete,
onPaste
onPaste,
onBlur
}) => {
const intl = useIntl();
const [open, setOpen] = useState(false);
@ -49,7 +51,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
});
};
const handleKeyOnBlur = (e: any) => {
const handleKeyOnBlur = (e: any, type: string) => {
const val = e.target.value;
// has duplicate key
const duplicates = _.filter(
@ -68,6 +70,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
} else {
setOpen(false);
}
onBlur?.(e, type);
};
return (
@ -83,7 +86,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key}
onChange={handleOnKeyChange}
onBlur={handleKeyOnBlur}
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
onPaste={onPaste}
></SealInput.Input>
</Tooltip>
@ -97,6 +100,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
label={intl.formatMessage({ id: 'common.input.value' })}
value={label.value}
onChange={handleOnValueChange}
onBlur={(e: any) => onBlur?.(e, 'value')}
></SealInput.Input>
)}
</div>

@ -6,6 +6,7 @@ interface HintInputProps {
value: string;
label?: string;
onChange: (value: string) => void;
onBlur?: (e: any) => void;
placeholder?: string;
sourceOptions?: Global.HintOptions[];
}
@ -13,7 +14,7 @@ interface HintInputProps {
const matchReg = /[^=]+=[^=]*$/;
const HintInput: React.FC<HintInputProps> = (props) => {
const { value, label, onChange, sourceOptions } = props;
const { value, label, onChange, onBlur, sourceOptions } = props;
const cursorPosRef = React.useRef(0);
const contextBeforeCursorRef = React.useRef('');
const [options, setOptions] = React.useState<
@ -89,6 +90,7 @@ const HintInput: React.FC<HintInputProps> = (props) => {
onInput={handleInput}
onSelect={handleOnSelect}
onFocus={getContextBeforeCursor}
onBlur={onBlur}
label={label}
options={options}
style={{ width: '100%' }}

@ -15,6 +15,7 @@ interface ListInputProps {
placeholder?: string;
labelExtra?: React.ReactNode;
onChange: (data: string[]) => void;
onBlur?: (e: any, index: number) => void;
}
const ListInput: React.FC<ListInputProps> = (props) => {
@ -24,6 +25,7 @@ const ListInput: React.FC<ListInputProps> = (props) => {
label,
description,
onChange,
onBlur,
btnText,
options,
labelExtra
@ -89,6 +91,7 @@ const ListInput: React.FC<ListInputProps> = (props) => {
options={options}
key={item.uid}
value={item.value}
onBlur={(e) => onBlur?.(e, index)}
onRemove={() => handleOnRemove(index)}
onChange={(val) => handleOnChange(val, index)}
/>

@ -8,6 +8,7 @@ import './styles/list-item.less';
interface LabelItemProps {
onRemove: () => void;
onChange: (value: string) => void;
onBlur?: (e: any) => void;
value: string;
label?: string;
placeholder?: string;
@ -15,7 +16,7 @@ interface LabelItemProps {
}
const ListItem: React.FC<LabelItemProps> = (props) => {
const { onRemove, onChange, label, value, options } = props;
const { onRemove, onChange, onBlur, label, value, options } = props;
const handleOnChange = (value: any) => {
onChange(value);
@ -26,6 +27,7 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
<HintInput
value={value}
onChange={handleOnChange}
onBlur={onBlur}
label={label}
sourceOptions={options}
placeholder={props.placeholder}

@ -1,6 +1,6 @@
import { AutoComplete, Form, Spin } from 'antd';
import type { AutoCompleteProps } from 'antd/lib';
import { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import Wrapper from './components/wrapper';
import { SealFormItemProps } from './types';
@ -15,6 +15,7 @@ const SealAutoComplete: React.FC<
isInFormItems = true,
trim = true,
onSelect,
onBlur,
extra,
style,
addAfter,

@ -94,7 +94,7 @@ export default {
'models.catalog.release.date': 'Release Date',
'models.localpath.gguf.tips.title': 'GGUF format model',
'models.localpat.safe.tips.title': 'Safetensors format model',
'models.localpath.shared.tips.title': 'Sharded GGUF Format Model',
'models.localpath.shared.tips.title': 'Sharded GGUF format model',
'models.localpath.gguf.tips':
' Specify the model file, e.g., /data/models/model.gguf.',
'models.localpath.safe.tips':

@ -13,10 +13,8 @@ export default {
'models.form.env': '環境変数',
'models.form.configurations': '設定',
'models.form.s3address': 'S3アドレス',
'models.form.partialoffload.tips':
'CPUオフロードを有効にすると、GPUStackは可能な限り多くのレイヤーをGPUにロードしてパフォーマンスを最大化します。GPUリソースが制限されている場合、一部のレイヤーがCPUにオフロードされ、GPUが利用できない場合は完全にCPU推論が使用されます。',
'models.form.distribution.tips':
'単一のGPUまたはワーカーのリソースが不足している場合、計算の一部を単一または複数のリモートワーカーにオフロードすることを許可します。',
'models.form.partialoffload.tips': `When CPU offloading is enabled, if GPU resources are insufficient, part of the model's layers will be offloaded to the CPU. If no GPU is available, full CPU inference will be used.`,
'models.form.distribution.tips': `Allows for offloading part of the model's layers to single or multiple remote workers when the resources of a worker are insufficient.`,
'models.openinplayground': 'プレイグラウンドで開く',
'models.instances': 'インスタンス',
'models.table.replicas.edit': 'レプリカを編集',
@ -137,4 +135,6 @@ export default {
// 6. 'models.form.restart.onerror',
// 7. 'models.form.restart.onerror.tips',
// 8. 'models.form.check.params',
// 9. 'models.form.partialoffload.tips',
// 10. 'models.form.distribution.tips
// ========== End of To-Do List ==========

@ -13,8 +13,8 @@ export default {
'models.form.env': 'Переменные окружения',
'models.form.configurations': 'Конфигурации',
'models.form.s3address': 'S3-адрес',
'models.form.partialoffload.tips': 'При включении CPU оффлоудинга GPUStack загружает максимум слоев на GPU для производительности. При нехватке ресурсов GPU часть слоев переносится на CPU. Полная CPU-инференция используется только при отсутствии GPU.',
'models.form.distribution.tips': 'Позволяет распределить вычисления между одним или несколькими удаленными воркерами при нехватке ресурсов одного GPU/воркера.',
'models.form.partialoffload.tips': `When CPU offloading is enabled, if GPU resources are insufficient, part of the model's layers will be offloaded to the CPU. If no GPU is available, full CPU inference will be used.`,
'models.form.distribution.tips': `Allows for offloading part of the model's layers to single or multiple remote workers when the resources of a worker are insufficient.`,
'models.openinplayground': 'Открыть в Песочнице',
'models.instances': 'инстансы',
'models.table.replicas.edit': 'Редактировать реплики',
@ -22,7 +22,8 @@ export default {
'model.form.ollamaholder': 'Выберите или введите название модели',
'model.deploy.sort': 'Сортировка',
'model.deploy.search.placeholder': 'Поиск моделей в {source}',
'model.form.ollamatips': 'Подсказка: ниже представлены предустановленные модели Ollama в GPUStack. Выберите нужную или введите модель для развертывания в поле 【{name}】 справа.',
'model.form.ollamatips':
'Подсказка: ниже представлены предустановленные модели Ollama в GPUStack. Выберите нужную или введите модель для развертывания в поле 【{name}】 справа.',
'models.sort.name': 'По имени',
'models.sort.size': 'По размеру',
'models.sort.likes': 'По лайкам',
@ -39,13 +40,16 @@ export default {
'models.search.nofiles': 'Нет доступных файлов',
'models.search.networkerror': 'Ошибка сетевого подключения!',
'models.search.hfvisit': 'Убедитесь, что доступен',
'models.search.unsupport': 'Модель не поддерживается и может быть нефункциональна после развертывания.',
'models.search.unsupport':
'Модель не поддерживается и может быть нефункциональна после развертывания.',
'models.form.scheduletype': 'Тип планирования',
'models.form.categories': 'Категория модели',
'models.form.scheduletype.auto': 'Авто',
'models.form.scheduletype.manual': 'Вручную',
'models.form.scheduletype.auto.tips': 'Автоматическое развертывание инстансов модели на подходящие GPU/воркеры в зависимости от текущих ресурсов.',
'models.form.scheduletype.manual.tips': 'Позволяет вручную указать GPU/воркеры для развертывания инстансов модели.',
'models.form.scheduletype.auto.tips':
'Автоматическое развертывание инстансов модели на подходящие GPU/воркеры в зависимости от текущих ресурсов.',
'models.form.scheduletype.manual.tips':
'Позволяет вручную указать GPU/воркеры для развертывания инстансов модели.',
'models.form.manual.schedule': 'Ручное распределение',
'models.table.gpuindex': 'Индекс GPU',
'models.table.backend': 'Бэкенды',
@ -54,13 +58,19 @@ export default {
'models.table.layers': 'Слои',
'models.form.backend': 'Бэкенд',
'models.form.backend_parameters': 'Параметры бэкенда',
'models.search.gguf.tips': 'GGUF-модели используют llama-box (поддерживает Linux, macOS и Windows).',
'models.search.vllm.tips': 'Не-GGUF модели используют vox-box для аудио и vLLM (только x86 Linux) для остальных.',
'models.search.voxbox.tips': 'Для развертывания аудиомодели снимите отметку GGUF.',
'models.search.gguf.tips':
'GGUF-модели используют llama-box (поддерживает Linux, macOS и Windows).',
'models.search.vllm.tips':
'Не-GGUF модели используют vox-box для аудио и vLLM (только x86 Linux) для остальных.',
'models.search.voxbox.tips':
'Для развертывания аудиомодели снимите отметку GGUF.',
'models.form.ollamalink': 'Больше моделей в библиотеке Ollama',
'models.form.backend_parameters.llamabox.placeholder': 'например: --ctx-size=8192',
'models.form.backend_parameters.vllm.placeholder': 'например: --max-model-len=8192',
'models.form.backend_parameters.vllm.tips': 'Подробнее о параметрах {backend}',
'models.form.backend_parameters.llamabox.placeholder':
'например: --ctx-size=8192',
'models.form.backend_parameters.vllm.placeholder':
'например: --max-model-len=8192',
'models.form.backend_parameters.vllm.tips':
'Подробнее о параметрах {backend}',
'models.logs.pagination.prev': 'Предыдущие {lines} строк',
'models.logs.pagination.next': 'Следующие {lines} строк',
'models.logs.pagination.last': 'Последняя страница',
@ -68,12 +78,15 @@ export default {
'models.form.localPath': 'Локальный путь',
'models.form.filePath': 'Путь к модели',
'models.form.backendVersion': 'Версия бэкенда',
'models.form.backendVersion.tips': 'Чтобы использовать желаемую версию {backend}, система автоматически создаст виртуальную среду в онлайн-окружении для установки соответствующей версии. После обновления GPUStack версия бэкенда останется зафиксированной. {link}',
'models.form.backendVersion.tips':
'Чтобы использовать желаемую версию {backend}, система автоматически создаст виртуальную среду в онлайн-окружении для установки соответствующей версии. После обновления GPUStack версия бэкенда останется зафиксированной. {link}',
'models.form.gpuselector': 'Селектор GPU',
'models.form.backend.llamabox': 'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
'models.form.backend.llamabox':
'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
'models.form.backend.vllm': 'Для моделей не-GGUF формата. Только x86 Linux.',
'models.form.backend.voxbox': 'Для аудиомоделей не-GGUF формата.',
'models.form.search.gguftips': 'Для воркеров на macOS/Windows отметьте GGUF (для аудиомоделей снимите).',
'models.form.search.gguftips':
'Для воркеров на macOS/Windows отметьте GGUF (для аудиомоделей снимите).',
'models.form.button.addlabel': 'Добавить метку',
'models.filter.category': 'Фильтр по категориям',
'models.list.more.logs': 'Показать больше',
@ -81,31 +94,42 @@ export default {
'models.localpath.gguf.tips.title': 'Модель формата GGUF',
'models.localpat.safe.tips.title': 'Модель формата Safetensors',
'models.localpath.shared.tips.title': 'Шардированная GGUF-модель',
'models.localpath.gguf.tips': 'Укажите файл модели, например: /data/models/model.gguf.',
'models.localpath.safe.tips': 'Укажите директорию модели с файлами .safetensors и config.json.',
'models.localpath.chunks.tips': 'Укажите первый шард модели, например: /data/models/model-00001-of-00004.gguf.',
'models.form.replicas.tips': 'Несколько реплик обеспечивают балансировку нагрузки для { api } запросов.',
'models.localpath.gguf.tips':
'Укажите файл модели, например: /data/models/model.gguf.',
'models.localpath.safe.tips':
'Укажите директорию модели с файлами .safetensors и config.json.',
'models.localpath.chunks.tips':
'Укажите первый шард модели, например: /data/models/model-00001-of-00004.gguf.',
'models.form.replicas.tips':
'Несколько реплик обеспечивают балансировку нагрузки для { api } запросов.',
'models.table.list.empty': 'Модели отсутствуют!',
'models.table.list.getStart': '<span style="margin-right: 5px;font-size: 13px;">Начните работу с</span> <span style="font-size: 14px;font-weight: 700">DeepSeek-R1-Distill-Qwen-1.5B</span>',
'models.table.list.getStart':
'<span style="margin-right: 5px;font-size: 13px;">Начните работу с</span> <span style="font-size: 14px;font-weight: 700">DeepSeek-R1-Distill-Qwen-1.5B</span>',
'models.table.llamaAcrossworker': 'Llama-box между воркерами',
'models.table.vllmAcrossworker': 'vLLM между воркерами',
'models.form.releases': 'Релизы',
'models.form.moreparameters': 'Описание параметров',
'models.table.vram.allocated': 'Выделенная VRAM',
'models.form.backend.warning': 'Бэкенд для моделей формата GGUF использует llama-box.',
'models.form.ollama.warning': 'Чтобы развернуть бэкенд для моделей Ollama с использованием llama-box , выполните следующие шаги.',
'models.form.backend.warning.llamabox': 'Чтобы использовать бэкенд llama-box , укажите полный путь к файлу модели (например,<span style="font-weight: 700">/data/models/model.gguf</span>). Для шардированных моделей укажите путь к первому шарду (например,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).',
'models.form.keyvalue.paste': 'Вставьте несколько строк текста, где каждая строка содержит пару ключ-значение. Ключ и значение разделяются знаком равенства (=), а разные пары — символами новой строки.',
'models.form.backend.warning':
'Бэкенд для моделей формата GGUF использует llama-box.',
'models.form.ollama.warning':
'Чтобы развернуть бэкенд для моделей Ollama с использованием llama-box , выполните следующие шаги.',
'models.form.backend.warning.llamabox':
'Чтобы использовать бэкенд llama-box , укажите полный путь к файлу модели (например,<span style="font-weight: 700">/data/models/model.gguf</span>). Для шардированных моделей укажите путь к первому шарду (например,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).',
'models.form.keyvalue.paste':
'Вставьте несколько строк текста, где каждая строка содержит пару ключ-значение. Ключ и значение разделяются знаком равенства (=), а разные пары — символами новой строки.',
'models.form.files': 'файлы',
'models.table.status': 'Статус',
'models.form.submit.anyway': 'Отправить в любом случае',
'models.form.evaluating': 'Анализ совместимости модели',
'models.form.incompatible': 'Обнаружена несовместимость',
'models.form.restart.onerror': 'Автоперезапуск при ошибке',
'models.form.restart.onerror.tips': 'При возникновении ошибки система автоматически попытается перезапуститься.',
'models.form.restart.onerror.tips':
'При возникновении ошибки система автоматически попытается перезапуститься.',
'models.form.check.params': 'Проверка конфигурации...'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.form.partialoffload.tips',
// 2. 'models.form.distribution.tips
// ========== End of To-Do List ==========

@ -26,6 +26,7 @@ import {
modelCategories,
placementStrategyOptions
} from '../config';
import { useFormContext } from '../config/form-context';
import llamaConfig from '../config/llama-config';
import { FormData } from '../config/types';
import vllmConfig from '../config/vllm-config';
@ -73,6 +74,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
const placement_strategy = Form.useWatch('placement_strategy', form);
const gpuSelectorIds = Form.useWatch(['gpu_selector', 'gpu_ids'], form);
const worker_selector = Form.useWatch('worker_selector', form);
const { onValuesChange } = useFormContext();
const placementStrategyTips = [
{
@ -153,6 +155,37 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
form.setFieldValue('backend_parameters', list);
}, []);
const handleBackendParametersOnBlur = () => {
const backendParams = form.getFieldValue('backend_parameters');
console.log('backendParams==', backendParams);
onValuesChange?.({
source: source,
allValues: form.getFieldsValue(),
changedValues: {}
});
};
const handleSelectorOnBlur = () => {
const workerSelector = form.getFieldValue('worker_selector');
console.log('workerSelector==', workerSelector);
onValuesChange?.({
source: source,
allValues: form.getFieldsValue(),
changedValues: {}
});
};
const handleBackendVersionOnBlur = () => {
const backendVersion = form.getFieldValue('backend_version');
if (backendVersion) {
onValuesChange?.({
source: source,
allValues: form.getFieldsValue(),
changedValues: {}
});
}
};
const collapseItems = useMemo(() => {
const children = (
<>
@ -233,6 +266,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
})}
labels={wokerSelector}
onChange={handleWorkerLabelsChange}
onBlur={handleSelectorOnBlur}
description={
<span>
{intl.formatMessage({
@ -275,6 +309,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
<Form.Item name="backend_version">
<SealInput.Input
onBlur={handleBackendVersionOnBlur}
label={intl.formatMessage({ id: 'models.form.backendVersion' })}
description={intl.formatMessage(
{
@ -324,6 +359,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
})}
dataList={form.getFieldValue('backend_parameters') || []}
onChange={handleBackendParametersChange}
onBlur={handleBackendParametersOnBlur}
options={paramsConfig}
description={
backendParamsTips && (

@ -132,16 +132,11 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}
};
// voxbox is not support multi gpu
const handleSetGPUIds = (backend: string) => {
if (backend === backendOptionsMap.llamaBox) {
return;
}
const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']);
const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']) || [];
if (!gpuids?.length) {
return;
}
if (gpuids.length > 1 && Array.isArray(gpuids[0])) {
if (backend === backendOptionsMap.voxBox && gpuids.length > 0) {
form.setFieldValue(['gpu_selector', 'gpu_ids'], [gpuids[0]]);
}
};

@ -77,9 +77,7 @@ const AddModal: FC<AddModalProps> = (props) => {
const {
handleShowCompatibleAlert,
handleUpdateWarning,
setWarningStatus,
handleEvaluate,
handleOnValuesChange,
checkTokenRef,
warningStatus,
@ -140,40 +138,25 @@ const AddModal: FC<AddModalProps> = (props) => {
}
};
// trigger from local_path change or backend change
const handleBackendChangeBefore = async () => {
const localPath = form.current.form.getFieldValue?.('local_path');
const backend = form.current.form.getFieldValue?.('backend');
const res = handleUpdateWarning?.({
backend,
localPath: localPath,
source: props.source
});
if (!res.show) {
const values = form.current.form.getFieldsValue?.();
const data = getSourceRepoConfigValue(props.source, values);
const evalutionData = await handleEvaluate(
_.omit(data.values, [
'cpu_offloading',
'distributed_inference_across_workers'
])
);
handleShowCompatibleAlert?.(evalutionData);
} else {
setWarningStatus?.(res);
}
};
const handleBackendChange = async (backend: string) => {
handleBackendChangeBefore();
if (backend === backendOptionsMap.vllm) {
setIsGGUF(false);
}
if (backend === backendOptionsMap.llamaBox) {
setIsGGUF(true);
} else {
setIsGGUF(false);
}
const data = form.current.form.getFieldsValue?.();
if (data.local_path || props.source !== modelSourceMap.local_path_value) {
handleOnValuesChange?.({
changedValues: {},
allValues:
backend === backendOptionsMap.llamaBox
? data
: _.omit(data, [
'cpu_offloading',
'distributed_inference_across_workers'
]),
source: props.source
});
}
};
@ -291,7 +274,8 @@ const AddModal: FC<AddModalProps> = (props) => {
<FormContext.Provider
value={{
isGGUF: isGGUF,
modelFileOptions: props.modelFileOptions
modelFileOptions: props.modelFileOptions,
onValuesChange: handleOnValuesChange
}}
>
<FormWrapper>

@ -21,6 +21,7 @@ import {
ollamaModelOptions,
sourceOptions
} from '../config';
import { FormContext } from '../config/form-context';
import { FormData, ListItem } from '../config/types';
import { useCheckCompatibility } from '../hooks';
import AdvanceConfig from './advance-config';
@ -55,10 +56,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
updateFormInitials: { gpuOptions, isGGUF, data: formData }
} = props || {};
const {
handleShowCompatibleAlert,
handleUpdateWarning,
setWarningStatus,
handleEvaluate,
generateGPUIds,
handleOnValuesChange,
checkTokenRef,
@ -70,56 +68,42 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
const localPathCache = useRef<string>('');
const submitAnyway = useRef<boolean>(false);
// voxbox is not support multi gpu
const handleSetGPUIds = (backend: string) => {
if (backend === backendOptionsMap.llamaBox) {
return;
}
const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']);
const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']) || [];
if (!gpuids?.length) {
return;
}
if (gpuids.length > 1 && Array.isArray(gpuids[0])) {
if (backend === backendOptionsMap.voxBox && gpuids.length > 0) {
form.setFieldValue(['gpu_selector', 'gpu_ids'], [gpuids[0]]);
}
};
// trigger from local_path change or backend change
const handleBackendChangeBefore = async () => {
const localPath = form.getFieldValue?.('local_path');
const backend = form.getFieldValue?.('backend');
const res = handleUpdateWarning?.({
backend,
localPath: localPath,
source: formData?.source as string
});
if (!res.show) {
const values = form.getFieldsValue?.();
const data = getSourceRepoConfigValue(formData?.source as string, values);
const evalutionData = await handleEvaluate(
_.omit(data.values, [
'cpu_offloading',
'distributed_inference_across_workers'
])
);
handleShowCompatibleAlert?.(evalutionData);
} else {
setWarningStatus?.(res);
}
};
const handleBackendChange = (val: string) => {
if (val === backendOptionsMap.llamaBox) {
form.setFieldsValue({
const handleBackendChange = (backend: string) => {
const updates = {
backend_version: ''
};
if (backend === backendOptionsMap.llamaBox) {
Object.assign(updates, {
distributed_inference_across_workers: true,
cpu_offloading: true
});
}
form.setFieldValue('backend_version', '');
handleSetGPUIds(val);
handleBackendChangeBefore();
form.setFieldsValue(updates);
handleSetGPUIds(backend);
const data = form.getFieldsValue?.();
if (data.local_path || data.source !== modelSourceMap.local_path_value) {
handleOnValuesChange?.({
changedValues: {},
allValues:
backend === backendOptionsMap.llamaBox
? data
: _.omit(data, [
'cpu_offloading',
'distributed_inference_across_workers'
]),
source: data.source
});
}
};
const handleOnFocus = () => {
@ -137,8 +121,21 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
if (!isEndwithGGUF || !isBlobFile) {
backend = backendOptionsMap.vllm;
}
handleBackendChange?.(backend);
form.setFieldValue('backend', backend);
handleBackendChange?.(backend);
};
const handleOnBlur = (e: any) => {
const value = e.target.value;
if (value) {
handleOnValuesChange?.({
changedValues: {},
allValues: {
...form.getFieldsValue?.()
},
source: formData?.source
});
}
};
const renderHuggingfaceFields = () => {
@ -159,6 +156,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
]}
>
<SealInput.Input
onBlur={handleOnBlur}
label={intl.formatMessage({ id: 'models.form.repoid' })}
required
disabled={false}
@ -180,6 +178,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
]}
>
<SealInput.Input
onBlur={handleOnBlur}
label={intl.formatMessage({ id: 'models.form.filename' })}
required
disabled={false}
@ -212,6 +211,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
defaultActiveFirstOption
disabled={false}
options={ollamaModelOptions}
onBlur={handleOnBlur}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
description={
<span>
@ -423,137 +423,143 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
></CompatibilityAlert>
}
>
<Form
name="addModalForm"
form={form}
onFinish={handleOk}
onValuesChange={onValuesChange}
scrollToFirstError={true}
preserve={false}
clearOnDestroy={true}
initialValues={{
...formData
}}
style={{
padding: 'var(--ant-modal-content-padding)',
paddingBlock: 0
<FormContext.Provider
value={{
onValuesChange: handleOnValuesChange
}}
>
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
}
]}
>
<SealInput.Input
label={intl.formatMessage({
id: 'common.table.name'
})}
required
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="source"
rules={[
{
required: true,
message: getRuleMessage('select', 'models.form.source')
}
]}
<Form
name="addModalForm"
form={form}
onFinish={handleOk}
onValuesChange={onValuesChange}
scrollToFirstError={true}
preserve={false}
clearOnDestroy={true}
initialValues={{
...formData
}}
style={{
padding: 'var(--ant-modal-content-padding)',
paddingBlock: 0
}}
>
{action === PageAction.EDIT && (
<SealSelect
disabled={true}
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
}
]}
>
<SealInput.Input
label={intl.formatMessage({
id: 'models.form.source'
id: 'common.table.name'
})}
options={sourceOptions}
required
></SealSelect>
)}
</Form.Item>
{renderFieldsBySource}
<Form.Item name="backend" rules={[{ required: true }]}>
<SealSelect
required
onChange={handleBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })}
description={<TooltipList list={backendTipsList}></TooltipList>}
options={[
{
label: `llama-box`,
value: backendOptionsMap.llamaBox,
disabled:
formData?.source === modelSourceMap.local_path_value
? false
: !isGGUF
},
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="source"
rules={[
{
label: 'vLLM',
value: backendOptionsMap.vllm,
disabled:
formData?.source === modelSourceMap.local_path_value
? false
: isGGUF
},
{
label: 'vox-box',
value: backendOptionsMap.voxBox,
disabled:
formData?.source === modelSourceMap.local_path_value
? false
: isGGUF
required: true,
message: getRuleMessage('select', 'models.form.source')
}
]}
disabled={
action === PageAction.EDIT &&
formData?.source !== modelSourceMap.local_path_value
}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="replicas"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.form.replicas')
}
]}
>
<SealInput.Number
style={{ width: '100%' }}
label={intl.formatMessage({
id: 'models.form.replicas'
})}
required
description={intl.formatMessage(
{
id: 'models.form.replicas.tips'
},
{ api: `${window.location.origin}/v1` }
>
{action === PageAction.EDIT && (
<SealSelect
disabled={true}
label={intl.formatMessage({
id: 'models.form.source'
})}
options={sourceOptions}
required
></SealSelect>
)}
min={0}
></SealInput.Number>
</Form.Item>
<Form.Item<FormData> name="description">
<SealInput.TextArea
label={intl.formatMessage({
id: 'common.table.description'
})}
></SealInput.TextArea>
</Form.Item>
</Form.Item>
{renderFieldsBySource}
<Form.Item name="backend" rules={[{ required: true }]}>
<SealSelect
required
onChange={handleBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })}
description={<TooltipList list={backendTipsList}></TooltipList>}
options={[
{
label: `llama-box`,
value: backendOptionsMap.llamaBox,
disabled:
formData?.source === modelSourceMap.local_path_value
? false
: !isGGUF
},
{
label: 'vLLM',
value: backendOptionsMap.vllm,
disabled:
formData?.source === modelSourceMap.local_path_value
? false
: isGGUF
},
{
label: 'vox-box',
value: backendOptionsMap.voxBox,
disabled:
formData?.source === modelSourceMap.local_path_value
? false
: isGGUF
}
]}
disabled={
action === PageAction.EDIT &&
formData?.source !== modelSourceMap.local_path_value
}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="replicas"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.form.replicas')
}
]}
>
<SealInput.Number
style={{ width: '100%' }}
label={intl.formatMessage({
id: 'models.form.replicas'
})}
required
description={intl.formatMessage(
{
id: 'models.form.replicas.tips'
},
{ api: `${window.location.origin}/v1` }
)}
min={0}
></SealInput.Number>
</Form.Item>
<Form.Item<FormData> name="description">
<SealInput.TextArea
label={intl.formatMessage({
id: 'common.table.description'
})}
></SealInput.TextArea>
</Form.Item>
<AdvanceConfig
form={form}
gpuOptions={gpuOptions}
action={PageAction.EDIT}
source={formData?.source || ''}
isGGUF={formData?.backend === backendOptionsMap.llamaBox}
></AdvanceConfig>
</Form>
<AdvanceConfig
form={form}
gpuOptions={gpuOptions}
action={PageAction.EDIT}
source={formData?.source || ''}
isGGUF={formData?.backend === backendOptionsMap.llamaBox}
></AdvanceConfig>
</Form>
</FormContext.Provider>
</ColumnWrapper>
</Modal>
);

@ -1,13 +1,14 @@
import React from 'react';
interface FormContextProps {
isGGUF: boolean;
isGGUF?: boolean;
byBuiltIn?: boolean;
sizeOptions?: Global.BaseOption<number>[];
quantizationOptions?: Global.BaseOption<string>[];
modelFileOptions?: any[];
onSizeChange?: (val: number) => void;
onQuantizationChange?: (val: string) => void;
onValuesChange?: (val: any) => void;
}
interface FormInnerContextProps {

@ -450,6 +450,8 @@ export const modelLabels = [
];
export const excludeFields = [
'repo_id',
'file_name',
'replicas',
'categories',
'name',
@ -460,5 +462,8 @@ export const excludeFields = [
'size',
'restart_on_error',
'worker_selector',
'backend_parameters'
'backend_parameters',
'local_path',
'backend_version',
'ollama_library_model_name'
];

@ -215,9 +215,11 @@ export const useGenerateModelFileOptions = () => {
export const useCheckCompatibility = () => {
const intl = useIntl();
const cacheFormValuesRef = useRef<any>({});
const checkTokenRef = useRef<any>(null);
const submitAnyway = useRef<boolean>(false);
const requestIdRef = useRef(0);
const updateStatusTimer = useRef<any>(null);
const [warningStatus, setWarningStatus] = useState<{
show: boolean;
title?: string;
@ -295,7 +297,12 @@ export const useCheckCompatibility = () => {
const handleShowCompatibleAlert = (evaluateResult: EvaluateResult | null) => {
const result = handleCheckCompatibility(evaluateResult);
setWarningStatus(result);
if (updateStatusTimer.current) {
clearTimeout(updateStatusTimer.current);
}
updateStatusTimer.current = setTimeout(() => {
setWarningStatus(result);
}, 300);
};
const updateShowWarning = (params: {
@ -386,6 +393,15 @@ export const useCheckCompatibility = () => {
source: string;
}) => {
const { changedValues, allValues, source } = params;
if (
_.isEqual(cacheFormValuesRef.current, allValues) ||
(allValues.source === modelSourceMap.local_path_value &&
!allValues.local_path)
) {
return;
}
cacheFormValuesRef.current = allValues;
const data = getSourceRepoConfigValue(source, allValues);
const gpuSelector = generateGPUIds(data.values);

@ -467,7 +467,7 @@ const ModelFiles = () => {
return (
record.resolved_paths?.length > 0 && (
<PathWrapper>
<AutoTooltip ghost>
<AutoTooltip ghost title={record.resolved_paths?.[0]} showTitle>
<span>{getResolvedPath(record.resolved_paths)}</span>
</AutoTooltip>
<span className="btn-wrapper">

Loading…
Cancel
Save