fix: check form data after blur

This commit is contained in:
jialin
2025-04-02 20:40:57 +08:00
parent 4755cf6a25
commit afacd2a8a0
19 changed files with 339 additions and 250 deletions
+5 -1
View File
@@ -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>
);
};
+3
View File
@@ -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}
/>
);
};
+3
View File
@@ -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)}
/>
);
})}
+7 -3
View File
@@ -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>
+3 -1
View File
@@ -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%' }}
+3
View File
@@ -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)}
/>
+3 -1
View File
@@ -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}
+2 -1
View File
@@ -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,
+1 -1
View File
@@ -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':
+4 -4
View File
@@ -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 ==========
+50 -26
View File
@@ -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 && (
+3 -8
View File
@@ -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]]);
}
};
+18 -34
View File
@@ -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>
+172 -166
View File
@@ -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')
}
]}
<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
}}
>
<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')
}
]}
>
{action === PageAction.EDIT && (
<SealSelect
disabled={true}
label={intl.formatMessage({
id: 'models.form.source'
})}
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={[
<Form.Item<FormData>
name="name"
rules={[
{
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
required: true,
message: getRuleMessage('input', 'common.table.name')
}
]}
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(
>
<SealInput.Input
label={intl.formatMessage({
id: 'common.table.name'
})}
required
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="source"
rules={[
{
id: 'models.form.replicas.tips'
},
{ api: `${window.location.origin}/v1` }
required: true,
message: getRuleMessage('select', 'models.form.source')
}
]}
>
{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>
);
+2 -1
View File
@@ -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 {
+6 -1
View File
@@ -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'
];
+17 -1
View File
@@ -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">