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'; import React, { useEffect, useRef, useState } from 'react';
type CopyButtonProps = { type CopyButtonProps = {
children?: React.ReactNode;
text: string; text: string;
disabled?: boolean; disabled?: boolean;
fontSize?: string; fontSize?: string;
@@ -26,6 +27,7 @@ type CopyButtonProps = {
}; };
const CopyButton: React.FC<CopyButtonProps> = ({ const CopyButton: React.FC<CopyButtonProps> = ({
children,
tips, tips,
text, text,
disabled, disabled,
@@ -91,7 +93,9 @@ const CopyButton: React.FC<CopyButtonProps> = ({
<CopyOutlined style={{ fontSize: fontSize, ...style }} /> <CopyOutlined style={{ fontSize: fontSize, ...style }} />
) )
} }
></Button> >
{children}
</Button>
</Tooltip> </Tooltip>
); );
}; };
+3
View File
@@ -9,11 +9,13 @@ interface LabelSelectorProps {
btnText?: string; btnText?: string;
description?: React.ReactNode; description?: React.ReactNode;
onChange?: (labels: Record<string, any>) => void; onChange?: (labels: Record<string, any>) => void;
onBlur?: (e: any, type: string, index: number) => void;
} }
const LabelSelector: React.FC<LabelSelectorProps> = ({ const LabelSelector: React.FC<LabelSelectorProps> = ({
labels, labels,
onChange, onChange,
onBlur,
label, label,
btnText, btnText,
description description
@@ -87,6 +89,7 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
onChange={handleLabelsChange} onChange={handleLabelsChange}
onLabelListChange={handleLabelListChange} onLabelListChange={handleLabelListChange}
onPaste={handleOnPaste} onPaste={handleOnPaste}
onBlur={onBlur}
/> />
); );
}; };
+3
View File
@@ -13,6 +13,7 @@ interface LabelSelectorProps {
onLabelListChange: (list: { key: string; value: string }[]) => void; onLabelListChange: (list: { key: string; value: string }[]) => void;
onChange?: (labels: Record<string, any>) => void; onChange?: (labels: Record<string, any>) => void;
onPaste?: (e: any, index: number) => void; onPaste?: (e: any, index: number) => void;
onBlur?: (e: any, type: string, index: number) => void;
description?: React.ReactNode; description?: React.ReactNode;
} }
@@ -22,6 +23,7 @@ const Inner: React.FC<LabelSelectorProps> = ({
onChange, onChange,
onLabelListChange, onLabelListChange,
onPaste, onPaste,
onBlur,
label, label,
btnText, btnText,
description description
@@ -82,6 +84,7 @@ const Inner: React.FC<LabelSelectorProps> = ({
onDelete={() => handleOnDelete(index)} onDelete={() => handleOnDelete(index)}
onChange={(obj) => handleOnChange(index, obj)} onChange={(obj) => handleOnChange(index, obj)}
onPaste={(e) => onPaste?.(e, index)} 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 }[]; labelList: { key: string; value: string }[];
onChange?: (params: { key: string; value: string }) => void; onChange?: (params: { key: string; value: string }) => void;
onPaste?: (e: any) => void; onPaste?: (e: any) => void;
onBlur?: (e: any, type: string) => void;
} }
const LabelItem: React.FC<LabelItemProps> = ({ const LabelItem: React.FC<LabelItemProps> = ({
label, label,
@@ -28,7 +29,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
valueAddon, valueAddon,
onChange, onChange,
onDelete, onDelete,
onPaste onPaste,
onBlur
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [open, setOpen] = useState(false); 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; const val = e.target.value;
// has duplicate key // has duplicate key
const duplicates = _.filter( const duplicates = _.filter(
@@ -68,6 +70,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
} else { } else {
setOpen(false); setOpen(false);
} }
onBlur?.(e, type);
}; };
return ( return (
@@ -83,7 +86,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
label={intl.formatMessage({ id: 'common.input.key' })} label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key} value={label.key}
onChange={handleOnKeyChange} onChange={handleOnKeyChange}
onBlur={handleKeyOnBlur} onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
onPaste={onPaste} onPaste={onPaste}
></SealInput.Input> ></SealInput.Input>
</Tooltip> </Tooltip>
@@ -97,6 +100,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
label={intl.formatMessage({ id: 'common.input.value' })} label={intl.formatMessage({ id: 'common.input.value' })}
value={label.value} value={label.value}
onChange={handleOnValueChange} onChange={handleOnValueChange}
onBlur={(e: any) => onBlur?.(e, 'value')}
></SealInput.Input> ></SealInput.Input>
)} )}
</div> </div>
+3 -1
View File
@@ -6,6 +6,7 @@ interface HintInputProps {
value: string; value: string;
label?: string; label?: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onBlur?: (e: any) => void;
placeholder?: string; placeholder?: string;
sourceOptions?: Global.HintOptions[]; sourceOptions?: Global.HintOptions[];
} }
@@ -13,7 +14,7 @@ interface HintInputProps {
const matchReg = /[^=]+=[^=]*$/; const matchReg = /[^=]+=[^=]*$/;
const HintInput: React.FC<HintInputProps> = (props) => { 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 cursorPosRef = React.useRef(0);
const contextBeforeCursorRef = React.useRef(''); const contextBeforeCursorRef = React.useRef('');
const [options, setOptions] = React.useState< const [options, setOptions] = React.useState<
@@ -89,6 +90,7 @@ const HintInput: React.FC<HintInputProps> = (props) => {
onInput={handleInput} onInput={handleInput}
onSelect={handleOnSelect} onSelect={handleOnSelect}
onFocus={getContextBeforeCursor} onFocus={getContextBeforeCursor}
onBlur={onBlur}
label={label} label={label}
options={options} options={options}
style={{ width: '100%' }} style={{ width: '100%' }}
+3
View File
@@ -15,6 +15,7 @@ interface ListInputProps {
placeholder?: string; placeholder?: string;
labelExtra?: React.ReactNode; labelExtra?: React.ReactNode;
onChange: (data: string[]) => void; onChange: (data: string[]) => void;
onBlur?: (e: any, index: number) => void;
} }
const ListInput: React.FC<ListInputProps> = (props) => { const ListInput: React.FC<ListInputProps> = (props) => {
@@ -24,6 +25,7 @@ const ListInput: React.FC<ListInputProps> = (props) => {
label, label,
description, description,
onChange, onChange,
onBlur,
btnText, btnText,
options, options,
labelExtra labelExtra
@@ -89,6 +91,7 @@ const ListInput: React.FC<ListInputProps> = (props) => {
options={options} options={options}
key={item.uid} key={item.uid}
value={item.value} value={item.value}
onBlur={(e) => onBlur?.(e, index)}
onRemove={() => handleOnRemove(index)} onRemove={() => handleOnRemove(index)}
onChange={(val) => handleOnChange(val, index)} onChange={(val) => handleOnChange(val, index)}
/> />
+3 -1
View File
@@ -8,6 +8,7 @@ import './styles/list-item.less';
interface LabelItemProps { interface LabelItemProps {
onRemove: () => void; onRemove: () => void;
onChange: (value: string) => void; onChange: (value: string) => void;
onBlur?: (e: any) => void;
value: string; value: string;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
@@ -15,7 +16,7 @@ interface LabelItemProps {
} }
const ListItem: React.FC<LabelItemProps> = (props) => { 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) => { const handleOnChange = (value: any) => {
onChange(value); onChange(value);
@@ -26,6 +27,7 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
<HintInput <HintInput
value={value} value={value}
onChange={handleOnChange} onChange={handleOnChange}
onBlur={onBlur}
label={label} label={label}
sourceOptions={options} sourceOptions={options}
placeholder={props.placeholder} placeholder={props.placeholder}
+2 -1
View File
@@ -1,6 +1,6 @@
import { AutoComplete, Form, Spin } from 'antd'; import { AutoComplete, Form, Spin } from 'antd';
import type { AutoCompleteProps } from 'antd/lib'; 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 Wrapper from './components/wrapper';
import { SealFormItemProps } from './types'; import { SealFormItemProps } from './types';
@@ -15,6 +15,7 @@ const SealAutoComplete: React.FC<
isInFormItems = true, isInFormItems = true,
trim = true, trim = true,
onSelect, onSelect,
onBlur,
extra, extra,
style, style,
addAfter, addAfter,
+1 -1
View File
@@ -94,7 +94,7 @@ export default {
'models.catalog.release.date': 'Release Date', 'models.catalog.release.date': 'Release Date',
'models.localpath.gguf.tips.title': 'GGUF format model', 'models.localpath.gguf.tips.title': 'GGUF format model',
'models.localpat.safe.tips.title': 'Safetensors 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': 'models.localpath.gguf.tips':
' Specify the model file, e.g., /data/models/model.gguf.', ' Specify the model file, e.g., /data/models/model.gguf.',
'models.localpath.safe.tips': 'models.localpath.safe.tips':
+4 -4
View File
@@ -13,10 +13,8 @@ export default {
'models.form.env': '環境変数', 'models.form.env': '環境変数',
'models.form.configurations': '設定', 'models.form.configurations': '設定',
'models.form.s3address': 'S3アドレス', 'models.form.s3address': 'S3アドレス',
'models.form.partialoffload.tips': '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.`,
'CPUオフロードを有効にすると、GPUStackは可能な限り多くのレイヤーをGPUにロードしてパフォーマンスを最大化します。GPUリソースが制限されている場合、一部のレイヤーがCPUにオフロードされ、GPUが利用できない場合は完全にCPU推論が使用されます。', '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.form.distribution.tips':
'単一のGPUまたはワーカーのリソースが不足している場合、計算の一部を単一または複数のリモートワーカーにオフロードすることを許可します。',
'models.openinplayground': 'プレイグラウンドで開く', 'models.openinplayground': 'プレイグラウンドで開く',
'models.instances': 'インスタンス', 'models.instances': 'インスタンス',
'models.table.replicas.edit': 'レプリカを編集', 'models.table.replicas.edit': 'レプリカを編集',
@@ -137,4 +135,6 @@ export default {
// 6. 'models.form.restart.onerror', // 6. 'models.form.restart.onerror',
// 7. 'models.form.restart.onerror.tips', // 7. 'models.form.restart.onerror.tips',
// 8. 'models.form.check.params', // 8. 'models.form.check.params',
// 9. 'models.form.partialoffload.tips',
// 10. 'models.form.distribution.tips
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+50 -26
View File
@@ -13,8 +13,8 @@ export default {
'models.form.env': 'Переменные окружения', 'models.form.env': 'Переменные окружения',
'models.form.configurations': 'Конфигурации', 'models.form.configurations': 'Конфигурации',
'models.form.s3address': 'S3-адрес', 'models.form.s3address': 'S3-адрес',
'models.form.partialoffload.tips': 'При включении CPU оффлоудинга GPUStack загружает максимум слоев на GPU для производительности. При нехватке ресурсов GPU часть слоев переносится на CPU. Полная CPU-инференция используется только при отсутствии 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': 'Позволяет распределить вычисления между одним или несколькими удаленными воркерами при нехватке ресурсов одного GPU/воркера.', '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.openinplayground': 'Открыть в Песочнице',
'models.instances': 'инстансы', 'models.instances': 'инстансы',
'models.table.replicas.edit': 'Редактировать реплики', 'models.table.replicas.edit': 'Редактировать реплики',
@@ -22,7 +22,8 @@ export default {
'model.form.ollamaholder': 'Выберите или введите название модели', 'model.form.ollamaholder': 'Выберите или введите название модели',
'model.deploy.sort': 'Сортировка', 'model.deploy.sort': 'Сортировка',
'model.deploy.search.placeholder': 'Поиск моделей в {source}', 'model.deploy.search.placeholder': 'Поиск моделей в {source}',
'model.form.ollamatips': 'Подсказка: ниже представлены предустановленные модели Ollama в GPUStack. Выберите нужную или введите модель для развертывания в поле 【{name}】 справа.', 'model.form.ollamatips':
'Подсказка: ниже представлены предустановленные модели Ollama в GPUStack. Выберите нужную или введите модель для развертывания в поле 【{name}】 справа.',
'models.sort.name': 'По имени', 'models.sort.name': 'По имени',
'models.sort.size': 'По размеру', 'models.sort.size': 'По размеру',
'models.sort.likes': 'По лайкам', 'models.sort.likes': 'По лайкам',
@@ -39,13 +40,16 @@ export default {
'models.search.nofiles': 'Нет доступных файлов', 'models.search.nofiles': 'Нет доступных файлов',
'models.search.networkerror': 'Ошибка сетевого подключения!', 'models.search.networkerror': 'Ошибка сетевого подключения!',
'models.search.hfvisit': 'Убедитесь, что доступен', 'models.search.hfvisit': 'Убедитесь, что доступен',
'models.search.unsupport': 'Модель не поддерживается и может быть нефункциональна после развертывания.', 'models.search.unsupport':
'Модель не поддерживается и может быть нефункциональна после развертывания.',
'models.form.scheduletype': 'Тип планирования', 'models.form.scheduletype': 'Тип планирования',
'models.form.categories': 'Категория модели', 'models.form.categories': 'Категория модели',
'models.form.scheduletype.auto': 'Авто', 'models.form.scheduletype.auto': 'Авто',
'models.form.scheduletype.manual': 'Вручную', 'models.form.scheduletype.manual': 'Вручную',
'models.form.scheduletype.auto.tips': 'Автоматическое развертывание инстансов модели на подходящие GPU/воркеры в зависимости от текущих ресурсов.', 'models.form.scheduletype.auto.tips':
'models.form.scheduletype.manual.tips': 'Позволяет вручную указать GPU/воркеры для развертывания инстансов модели.', 'Автоматическое развертывание инстансов модели на подходящие GPU/воркеры в зависимости от текущих ресурсов.',
'models.form.scheduletype.manual.tips':
'Позволяет вручную указать GPU/воркеры для развертывания инстансов модели.',
'models.form.manual.schedule': 'Ручное распределение', 'models.form.manual.schedule': 'Ручное распределение',
'models.table.gpuindex': 'Индекс GPU', 'models.table.gpuindex': 'Индекс GPU',
'models.table.backend': 'Бэкенды', 'models.table.backend': 'Бэкенды',
@@ -54,13 +58,19 @@ export default {
'models.table.layers': 'Слои', 'models.table.layers': 'Слои',
'models.form.backend': 'Бэкенд', 'models.form.backend': 'Бэкенд',
'models.form.backend_parameters': 'Параметры бэкенда', 'models.form.backend_parameters': 'Параметры бэкенда',
'models.search.gguf.tips': 'GGUF-модели используют llama-box (поддерживает Linux, macOS и Windows).', 'models.search.gguf.tips':
'models.search.vllm.tips': 'Не-GGUF модели используют vox-box для аудио и vLLM (только x86 Linux) для остальных.', 'GGUF-модели используют llama-box (поддерживает Linux, macOS и Windows).',
'models.search.voxbox.tips': 'Для развертывания аудиомодели снимите отметку GGUF.', 'models.search.vllm.tips':
'Не-GGUF модели используют vox-box для аудио и vLLM (только x86 Linux) для остальных.',
'models.search.voxbox.tips':
'Для развертывания аудиомодели снимите отметку GGUF.',
'models.form.ollamalink': 'Больше моделей в библиотеке Ollama', 'models.form.ollamalink': 'Больше моделей в библиотеке Ollama',
'models.form.backend_parameters.llamabox.placeholder': 'например: --ctx-size=8192', 'models.form.backend_parameters.llamabox.placeholder':
'models.form.backend_parameters.vllm.placeholder': 'например: --max-model-len=8192', 'например: --ctx-size=8192',
'models.form.backend_parameters.vllm.tips': 'Подробнее о параметрах {backend}', '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.prev': 'Предыдущие {lines} строк',
'models.logs.pagination.next': 'Следующие {lines} строк', 'models.logs.pagination.next': 'Следующие {lines} строк',
'models.logs.pagination.last': 'Последняя страница', 'models.logs.pagination.last': 'Последняя страница',
@@ -68,12 +78,15 @@ export default {
'models.form.localPath': 'Локальный путь', 'models.form.localPath': 'Локальный путь',
'models.form.filePath': 'Путь к модели', 'models.form.filePath': 'Путь к модели',
'models.form.backendVersion': 'Версия бэкенда', 'models.form.backendVersion': 'Версия бэкенда',
'models.form.backendVersion.tips': 'Чтобы использовать желаемую версию {backend}, система автоматически создаст виртуальную среду в онлайн-окружении для установки соответствующей версии. После обновления GPUStack версия бэкенда останется зафиксированной. {link}', 'models.form.backendVersion.tips':
'Чтобы использовать желаемую версию {backend}, система автоматически создаст виртуальную среду в онлайн-окружении для установки соответствующей версии. После обновления GPUStack версия бэкенда останется зафиксированной. {link}',
'models.form.gpuselector': 'Селектор GPU', '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.vllm': 'Для моделей не-GGUF формата. Только x86 Linux.',
'models.form.backend.voxbox': 'Для аудиомоделей не-GGUF формата.', 'models.form.backend.voxbox': 'Для аудиомоделей не-GGUF формата.',
'models.form.search.gguftips': 'Для воркеров на macOS/Windows отметьте GGUF (для аудиомоделей снимите).', 'models.form.search.gguftips':
'Для воркеров на macOS/Windows отметьте GGUF (для аудиомоделей снимите).',
'models.form.button.addlabel': 'Добавить метку', 'models.form.button.addlabel': 'Добавить метку',
'models.filter.category': 'Фильтр по категориям', 'models.filter.category': 'Фильтр по категориям',
'models.list.more.logs': 'Показать больше', 'models.list.more.logs': 'Показать больше',
@@ -81,31 +94,42 @@ export default {
'models.localpath.gguf.tips.title': 'Модель формата GGUF', 'models.localpath.gguf.tips.title': 'Модель формата GGUF',
'models.localpat.safe.tips.title': 'Модель формата Safetensors', 'models.localpat.safe.tips.title': 'Модель формата Safetensors',
'models.localpath.shared.tips.title': 'Шардированная GGUF-модель', 'models.localpath.shared.tips.title': 'Шардированная GGUF-модель',
'models.localpath.gguf.tips': 'Укажите файл модели, например: /data/models/model.gguf.', 'models.localpath.gguf.tips':
'models.localpath.safe.tips': 'Укажите директорию модели с файлами .safetensors и config.json.', 'Укажите файл модели, например: /data/models/model.gguf.',
'models.localpath.chunks.tips': 'Укажите первый шард модели, например: /data/models/model-00001-of-00004.gguf.', 'models.localpath.safe.tips':
'models.form.replicas.tips': 'Несколько реплик обеспечивают балансировку нагрузки для { api } запросов.', 'Укажите директорию модели с файлами .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.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.llamaAcrossworker': 'Llama-box между воркерами',
'models.table.vllmAcrossworker': 'vLLM между воркерами', 'models.table.vllmAcrossworker': 'vLLM между воркерами',
'models.form.releases': 'Релизы', 'models.form.releases': 'Релизы',
'models.form.moreparameters': 'Описание параметров', 'models.form.moreparameters': 'Описание параметров',
'models.table.vram.allocated': 'Выделенная VRAM', 'models.table.vram.allocated': 'Выделенная VRAM',
'models.form.backend.warning': 'Бэкенд для моделей формата GGUF использует llama-box.', 'models.form.backend.warning':
'models.form.ollama.warning': 'Чтобы развернуть бэкенд для моделей Ollama с использованием llama-box , выполните следующие шаги.', экенд для моделей формата GGUF использует 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.ollama.warning':
'models.form.keyvalue.paste': 'Вставьте несколько строк текста, где каждая строка содержит пару ключ-значение. Ключ и значение разделяются знаком равенства (=), а разные пары — символами новой строки.', 'Чтобы развернуть бэкенд для моделей 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.form.files': 'файлы',
'models.table.status': 'Статус', 'models.table.status': 'Статус',
'models.form.submit.anyway': 'Отправить в любом случае', 'models.form.submit.anyway': 'Отправить в любом случае',
'models.form.evaluating': 'Анализ совместимости модели', 'models.form.evaluating': 'Анализ совместимости модели',
'models.form.incompatible': 'Обнаружена несовместимость', 'models.form.incompatible': 'Обнаружена несовместимость',
'models.form.restart.onerror': 'Автоперезапуск при ошибке', 'models.form.restart.onerror': 'Автоперезапуск при ошибке',
'models.form.restart.onerror.tips': 'При возникновении ошибки система автоматически попытается перезапуститься.', 'models.form.restart.onerror.tips':
'При возникновении ошибки система автоматически попытается перезапуститься.',
'models.form.check.params': 'Проверка конфигурации...' 'models.form.check.params': 'Проверка конфигурации...'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.form.partialoffload.tips',
// 2. 'models.form.distribution.tips
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
@@ -26,6 +26,7 @@ import {
modelCategories, modelCategories,
placementStrategyOptions placementStrategyOptions
} from '../config'; } from '../config';
import { useFormContext } from '../config/form-context';
import llamaConfig from '../config/llama-config'; import llamaConfig from '../config/llama-config';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import vllmConfig from '../config/vllm-config'; 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 placement_strategy = Form.useWatch('placement_strategy', form);
const gpuSelectorIds = Form.useWatch(['gpu_selector', 'gpu_ids'], form); const gpuSelectorIds = Form.useWatch(['gpu_selector', 'gpu_ids'], form);
const worker_selector = Form.useWatch('worker_selector', form); const worker_selector = Form.useWatch('worker_selector', form);
const { onValuesChange } = useFormContext();
const placementStrategyTips = [ const placementStrategyTips = [
{ {
@@ -153,6 +155,37 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
form.setFieldValue('backend_parameters', list); 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 collapseItems = useMemo(() => {
const children = ( const children = (
<> <>
@@ -233,6 +266,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
})} })}
labels={wokerSelector} labels={wokerSelector}
onChange={handleWorkerLabelsChange} onChange={handleWorkerLabelsChange}
onBlur={handleSelectorOnBlur}
description={ description={
<span> <span>
{intl.formatMessage({ {intl.formatMessage({
@@ -275,6 +309,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
<Form.Item name="backend_version"> <Form.Item name="backend_version">
<SealInput.Input <SealInput.Input
onBlur={handleBackendVersionOnBlur}
label={intl.formatMessage({ id: 'models.form.backendVersion' })} label={intl.formatMessage({ id: 'models.form.backendVersion' })}
description={intl.formatMessage( description={intl.formatMessage(
{ {
@@ -324,6 +359,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
})} })}
dataList={form.getFieldValue('backend_parameters') || []} dataList={form.getFieldValue('backend_parameters') || []}
onChange={handleBackendParametersChange} onChange={handleBackendParametersChange}
onBlur={handleBackendParametersOnBlur}
options={paramsConfig} options={paramsConfig}
description={ description={
backendParamsTips && ( 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) => { const handleSetGPUIds = (backend: string) => {
if (backend === backendOptionsMap.llamaBox) { const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']) || [];
return;
}
const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']);
if (!gpuids?.length) { if (backend === backendOptionsMap.voxBox && gpuids.length > 0) {
return;
}
if (gpuids.length > 1 && Array.isArray(gpuids[0])) {
form.setFieldValue(['gpu_selector', 'gpu_ids'], [gpuids[0]]); form.setFieldValue(['gpu_selector', 'gpu_ids'], [gpuids[0]]);
} }
}; };
+18 -34
View File
@@ -77,9 +77,7 @@ const AddModal: FC<AddModalProps> = (props) => {
const { const {
handleShowCompatibleAlert, handleShowCompatibleAlert,
handleUpdateWarning,
setWarningStatus, setWarningStatus,
handleEvaluate,
handleOnValuesChange, handleOnValuesChange,
checkTokenRef, checkTokenRef,
warningStatus, 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) => { const handleBackendChange = async (backend: string) => {
handleBackendChangeBefore();
if (backend === backendOptionsMap.vllm) {
setIsGGUF(false);
}
if (backend === backendOptionsMap.llamaBox) { if (backend === backendOptionsMap.llamaBox) {
setIsGGUF(true); 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 <FormContext.Provider
value={{ value={{
isGGUF: isGGUF, isGGUF: isGGUF,
modelFileOptions: props.modelFileOptions modelFileOptions: props.modelFileOptions,
onValuesChange: handleOnValuesChange
}} }}
> >
<FormWrapper> <FormWrapper>
+172 -166
View File
@@ -21,6 +21,7 @@ import {
ollamaModelOptions, ollamaModelOptions,
sourceOptions sourceOptions
} from '../config'; } from '../config';
import { FormContext } from '../config/form-context';
import { FormData, ListItem } from '../config/types'; import { FormData, ListItem } from '../config/types';
import { useCheckCompatibility } from '../hooks'; import { useCheckCompatibility } from '../hooks';
import AdvanceConfig from './advance-config'; import AdvanceConfig from './advance-config';
@@ -55,10 +56,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
updateFormInitials: { gpuOptions, isGGUF, data: formData } updateFormInitials: { gpuOptions, isGGUF, data: formData }
} = props || {}; } = props || {};
const { const {
handleShowCompatibleAlert,
handleUpdateWarning,
setWarningStatus, setWarningStatus,
handleEvaluate,
generateGPUIds, generateGPUIds,
handleOnValuesChange, handleOnValuesChange,
checkTokenRef, checkTokenRef,
@@ -70,56 +68,42 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
const localPathCache = useRef<string>(''); const localPathCache = useRef<string>('');
const submitAnyway = useRef<boolean>(false); const submitAnyway = useRef<boolean>(false);
// voxbox is not support multi gpu
const handleSetGPUIds = (backend: string) => { const handleSetGPUIds = (backend: string) => {
if (backend === backendOptionsMap.llamaBox) { const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']) || [];
return;
}
const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']);
if (!gpuids?.length) { if (backend === backendOptionsMap.voxBox && gpuids.length > 0) {
return;
}
if (gpuids.length > 1 && Array.isArray(gpuids[0])) {
form.setFieldValue(['gpu_selector', 'gpu_ids'], [gpuids[0]]); form.setFieldValue(['gpu_selector', 'gpu_ids'], [gpuids[0]]);
} }
}; };
// trigger from local_path change or backend change const handleBackendChange = (backend: string) => {
const handleBackendChangeBefore = async () => { const updates = {
const localPath = form.getFieldValue?.('local_path'); backend_version: ''
const backend = form.getFieldValue?.('backend'); };
if (backend === backendOptionsMap.llamaBox) {
const res = handleUpdateWarning?.({ Object.assign(updates, {
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({
distributed_inference_across_workers: true, distributed_inference_across_workers: true,
cpu_offloading: true cpu_offloading: true
}); });
} }
form.setFieldValue('backend_version', ''); form.setFieldsValue(updates);
handleSetGPUIds(val); handleSetGPUIds(backend);
handleBackendChangeBefore();
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 = () => { const handleOnFocus = () => {
@@ -137,8 +121,21 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
if (!isEndwithGGUF || !isBlobFile) { if (!isEndwithGGUF || !isBlobFile) {
backend = backendOptionsMap.vllm; backend = backendOptionsMap.vllm;
} }
handleBackendChange?.(backend);
form.setFieldValue('backend', 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 = () => { const renderHuggingfaceFields = () => {
@@ -159,6 +156,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
]} ]}
> >
<SealInput.Input <SealInput.Input
onBlur={handleOnBlur}
label={intl.formatMessage({ id: 'models.form.repoid' })} label={intl.formatMessage({ id: 'models.form.repoid' })}
required required
disabled={false} disabled={false}
@@ -180,6 +178,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
]} ]}
> >
<SealInput.Input <SealInput.Input
onBlur={handleOnBlur}
label={intl.formatMessage({ id: 'models.form.filename' })} label={intl.formatMessage({ id: 'models.form.filename' })}
required required
disabled={false} disabled={false}
@@ -212,6 +211,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
defaultActiveFirstOption defaultActiveFirstOption
disabled={false} disabled={false}
options={ollamaModelOptions} options={ollamaModelOptions}
onBlur={handleOnBlur}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })} placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
description={ description={
<span> <span>
@@ -423,137 +423,143 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
></CompatibilityAlert> ></CompatibilityAlert>
} }
> >
<Form <FormContext.Provider
name="addModalForm" value={{
form={form} onValuesChange: handleOnValuesChange
onFinish={handleOk}
onValuesChange={onValuesChange}
scrollToFirstError={true}
preserve={false}
clearOnDestroy={true}
initialValues={{
...formData
}}
style={{
padding: 'var(--ant-modal-content-padding)',
paddingBlock: 0
}} }}
> >
<Form.Item<FormData> <Form
name="name" name="addModalForm"
rules={[ form={form}
{ onFinish={handleOk}
required: true, onValuesChange={onValuesChange}
message: getRuleMessage('input', 'common.table.name') scrollToFirstError={true}
} preserve={false}
]} clearOnDestroy={true}
initialValues={{
...formData
}}
style={{
padding: 'var(--ant-modal-content-padding)',
paddingBlock: 0
}}
> >
<SealInput.Input <Form.Item<FormData>
label={intl.formatMessage({ name="name"
id: 'common.table.name' rules={[
})}
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={[
{ {
label: `llama-box`, required: true,
value: backendOptionsMap.llamaBox, message: getRuleMessage('input', 'common.table.name')
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 && <SealInput.Input
formData?.source !== modelSourceMap.local_path_value label={intl.formatMessage({
} id: 'common.table.name'
></SealSelect> })}
</Form.Item> required
<Form.Item<FormData> ></SealInput.Input>
name="replicas" </Form.Item>
rules={[ <Form.Item<FormData>
{ name="source"
required: true, rules={[
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' required: true,
}, message: getRuleMessage('select', 'models.form.source')
{ api: `${window.location.origin}/v1` } }
]}
>
{action === PageAction.EDIT && (
<SealSelect
disabled={true}
label={intl.formatMessage({
id: 'models.form.source'
})}
options={sourceOptions}
required
></SealSelect>
)} )}
min={0} </Form.Item>
></SealInput.Number> {renderFieldsBySource}
</Form.Item> <Form.Item name="backend" rules={[{ required: true }]}>
<Form.Item<FormData> name="description"> <SealSelect
<SealInput.TextArea required
label={intl.formatMessage({ onChange={handleBackendChange}
id: 'common.table.description' label={intl.formatMessage({ id: 'models.form.backend' })}
})} description={<TooltipList list={backendTipsList}></TooltipList>}
></SealInput.TextArea> options={[
</Form.Item> {
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 <AdvanceConfig
form={form} form={form}
gpuOptions={gpuOptions} gpuOptions={gpuOptions}
action={PageAction.EDIT} action={PageAction.EDIT}
source={formData?.source || ''} source={formData?.source || ''}
isGGUF={formData?.backend === backendOptionsMap.llamaBox} isGGUF={formData?.backend === backendOptionsMap.llamaBox}
></AdvanceConfig> ></AdvanceConfig>
</Form> </Form>
</FormContext.Provider>
</ColumnWrapper> </ColumnWrapper>
</Modal> </Modal>
); );
+2 -1
View File
@@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
interface FormContextProps { interface FormContextProps {
isGGUF: boolean; isGGUF?: boolean;
byBuiltIn?: boolean; byBuiltIn?: boolean;
sizeOptions?: Global.BaseOption<number>[]; sizeOptions?: Global.BaseOption<number>[];
quantizationOptions?: Global.BaseOption<string>[]; quantizationOptions?: Global.BaseOption<string>[];
modelFileOptions?: any[]; modelFileOptions?: any[];
onSizeChange?: (val: number) => void; onSizeChange?: (val: number) => void;
onQuantizationChange?: (val: string) => void; onQuantizationChange?: (val: string) => void;
onValuesChange?: (val: any) => void;
} }
interface FormInnerContextProps { interface FormInnerContextProps {
+6 -1
View File
@@ -450,6 +450,8 @@ export const modelLabels = [
]; ];
export const excludeFields = [ export const excludeFields = [
'repo_id',
'file_name',
'replicas', 'replicas',
'categories', 'categories',
'name', 'name',
@@ -460,5 +462,8 @@ export const excludeFields = [
'size', 'size',
'restart_on_error', 'restart_on_error',
'worker_selector', '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 = () => { export const useCheckCompatibility = () => {
const intl = useIntl(); const intl = useIntl();
const cacheFormValuesRef = useRef<any>({});
const checkTokenRef = useRef<any>(null); const checkTokenRef = useRef<any>(null);
const submitAnyway = useRef<boolean>(false); const submitAnyway = useRef<boolean>(false);
const requestIdRef = useRef(0); const requestIdRef = useRef(0);
const updateStatusTimer = useRef<any>(null);
const [warningStatus, setWarningStatus] = useState<{ const [warningStatus, setWarningStatus] = useState<{
show: boolean; show: boolean;
title?: string; title?: string;
@@ -295,7 +297,12 @@ export const useCheckCompatibility = () => {
const handleShowCompatibleAlert = (evaluateResult: EvaluateResult | null) => { const handleShowCompatibleAlert = (evaluateResult: EvaluateResult | null) => {
const result = handleCheckCompatibility(evaluateResult); const result = handleCheckCompatibility(evaluateResult);
setWarningStatus(result); if (updateStatusTimer.current) {
clearTimeout(updateStatusTimer.current);
}
updateStatusTimer.current = setTimeout(() => {
setWarningStatus(result);
}, 300);
}; };
const updateShowWarning = (params: { const updateShowWarning = (params: {
@@ -386,6 +393,15 @@ export const useCheckCompatibility = () => {
source: string; source: string;
}) => { }) => {
const { changedValues, allValues, source } = params; 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 data = getSourceRepoConfigValue(source, allValues);
const gpuSelector = generateGPUIds(data.values); const gpuSelector = generateGPUIds(data.values);
@@ -467,7 +467,7 @@ const ModelFiles = () => {
return ( return (
record.resolved_paths?.length > 0 && ( record.resolved_paths?.length > 0 && (
<PathWrapper> <PathWrapper>
<AutoTooltip ghost> <AutoTooltip ghost title={record.resolved_paths?.[0]} showTitle>
<span>{getResolvedPath(record.resolved_paths)}</span> <span>{getResolvedPath(record.resolved_paths)}</span>
</AutoTooltip> </AutoTooltip>
<span className="btn-wrapper"> <span className="btn-wrapper">