diff --git a/src/components/copy-button/index.tsx b/src/components/copy-button/index.tsx index 36e9994b..368f40d0 100644 --- a/src/components/copy-button/index.tsx +++ b/src/components/copy-button/index.tsx @@ -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 = ({ + children, tips, text, disabled, @@ -91,7 +93,9 @@ const CopyButton: React.FC = ({ ) } - > + > + {children} + ); }; diff --git a/src/components/label-selector/index.tsx b/src/components/label-selector/index.tsx index 441402b4..ae8efde2 100644 --- a/src/components/label-selector/index.tsx +++ b/src/components/label-selector/index.tsx @@ -9,11 +9,13 @@ interface LabelSelectorProps { btnText?: string; description?: React.ReactNode; onChange?: (labels: Record) => void; + onBlur?: (e: any, type: string, index: number) => void; } const LabelSelector: React.FC = ({ labels, onChange, + onBlur, label, btnText, description @@ -87,6 +89,7 @@ const LabelSelector: React.FC = ({ onChange={handleLabelsChange} onLabelListChange={handleLabelListChange} onPaste={handleOnPaste} + onBlur={onBlur} /> ); }; diff --git a/src/components/label-selector/inner.tsx b/src/components/label-selector/inner.tsx index 629f7f30..0cb2c847 100644 --- a/src/components/label-selector/inner.tsx +++ b/src/components/label-selector/inner.tsx @@ -13,6 +13,7 @@ interface LabelSelectorProps { onLabelListChange: (list: { key: string; value: string }[]) => void; onChange?: (labels: Record) => 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 = ({ onChange, onLabelListChange, onPaste, + onBlur, label, btnText, description @@ -82,6 +84,7 @@ const Inner: React.FC = ({ onDelete={() => handleOnDelete(index)} onChange={(obj) => handleOnChange(index, obj)} onPaste={(e) => onPaste?.(e, index)} + onBlur={(e: any, type: string) => onBlur?.(e, type, index)} /> ); })} diff --git a/src/components/label-selector/label-item.tsx b/src/components/label-selector/label-item.tsx index d201c05c..1c365d28 100644 --- a/src/components/label-selector/label-item.tsx +++ b/src/components/label-selector/label-item.tsx @@ -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 = ({ label, @@ -28,7 +29,8 @@ const LabelItem: React.FC = ({ valueAddon, onChange, onDelete, - onPaste + onPaste, + onBlur }) => { const intl = useIntl(); const [open, setOpen] = useState(false); @@ -49,7 +51,7 @@ const LabelItem: React.FC = ({ }); }; - 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 = ({ } else { setOpen(false); } + onBlur?.(e, type); }; return ( @@ -83,7 +86,7 @@ const LabelItem: React.FC = ({ label={intl.formatMessage({ id: 'common.input.key' })} value={label.key} onChange={handleOnKeyChange} - onBlur={handleKeyOnBlur} + onBlur={(e: any) => handleKeyOnBlur(e, 'key')} onPaste={onPaste} > @@ -97,6 +100,7 @@ const LabelItem: React.FC = ({ label={intl.formatMessage({ id: 'common.input.value' })} value={label.value} onChange={handleOnValueChange} + onBlur={(e: any) => onBlur?.(e, 'value')} > )} diff --git a/src/components/list-input/hint-input.tsx b/src/components/list-input/hint-input.tsx index 1a9ff9c0..c1df138a 100644 --- a/src/components/list-input/hint-input.tsx +++ b/src/components/list-input/hint-input.tsx @@ -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 = (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 = (props) => { onInput={handleInput} onSelect={handleOnSelect} onFocus={getContextBeforeCursor} + onBlur={onBlur} label={label} options={options} style={{ width: '100%' }} diff --git a/src/components/list-input/index.tsx b/src/components/list-input/index.tsx index 8bb200ad..83213180 100644 --- a/src/components/list-input/index.tsx +++ b/src/components/list-input/index.tsx @@ -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 = (props) => { @@ -24,6 +25,7 @@ const ListInput: React.FC = (props) => { label, description, onChange, + onBlur, btnText, options, labelExtra @@ -89,6 +91,7 @@ const ListInput: React.FC = (props) => { options={options} key={item.uid} value={item.value} + onBlur={(e) => onBlur?.(e, index)} onRemove={() => handleOnRemove(index)} onChange={(val) => handleOnChange(val, index)} /> diff --git a/src/components/list-input/list-item.tsx b/src/components/list-input/list-item.tsx index 78733f10..7bc01570 100644 --- a/src/components/list-input/list-item.tsx +++ b/src/components/list-input/list-item.tsx @@ -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 = (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 = (props) => { Начните работу с DeepSeek-R1-Distill-Qwen-1.5B', + 'models.table.list.getStart': + 'Начните работу с DeepSeek-R1-Distill-Qwen-1.5B', '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 , укажите полный путь к файлу модели (например,/data/models/model.gguf). Для шардированных моделей укажите путь к первому шарду (например,/data/models/model-00001-of-00004.gguf).', - '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 , укажите полный путь к файлу модели (например,/data/models/model.gguf). Для шардированных моделей укажите путь к первому шарду (например,/data/models/model-00001-of-00004.gguf).', + '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 ========== diff --git a/src/pages/llmodels/components/advance-config.tsx b/src/pages/llmodels/components/advance-config.tsx index 5b620181..18701c16 100644 --- a/src/pages/llmodels/components/advance-config.tsx +++ b/src/pages/llmodels/components/advance-config.tsx @@ -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 = (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 = (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 = (props) => { })} labels={wokerSelector} onChange={handleWorkerLabelsChange} + onBlur={handleSelectorOnBlur} description={ {intl.formatMessage({ @@ -275,6 +309,7 @@ const AdvanceConfig: React.FC = (props) => { = (props) => { })} dataList={form.getFieldValue('backend_parameters') || []} onChange={handleBackendParametersChange} + onBlur={handleBackendParametersOnBlur} options={paramsConfig} description={ backendParamsTips && ( diff --git a/src/pages/llmodels/components/data-form.tsx b/src/pages/llmodels/components/data-form.tsx index adb12d75..8ec80884 100644 --- a/src/pages/llmodels/components/data-form.tsx +++ b/src/pages/llmodels/components/data-form.tsx @@ -132,16 +132,11 @@ const DataForm: React.FC = 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]]); } }; diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index 3943b6dc..3c7dc8fa 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -77,9 +77,7 @@ const AddModal: FC = (props) => { const { handleShowCompatibleAlert, - handleUpdateWarning, setWarningStatus, - handleEvaluate, handleOnValuesChange, checkTokenRef, warningStatus, @@ -140,40 +138,25 @@ const AddModal: FC = (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 = (props) => { diff --git a/src/pages/llmodels/components/update-modal.tsx b/src/pages/llmodels/components/update-modal.tsx index 31211333..640a6644 100644 --- a/src/pages/llmodels/components/update-modal.tsx +++ b/src/pages/llmodels/components/update-modal.tsx @@ -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 = (props) => { updateFormInitials: { gpuOptions, isGGUF, data: formData } } = props || {}; const { - handleShowCompatibleAlert, - handleUpdateWarning, setWarningStatus, - handleEvaluate, generateGPUIds, handleOnValuesChange, checkTokenRef, @@ -70,56 +68,42 @@ const UpdateModal: React.FC = (props) => { const localPathCache = useRef(''); const submitAnyway = useRef(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 = (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 = (props) => { ]} > = (props) => { ]} > = (props) => { defaultActiveFirstOption disabled={false} options={ollamaModelOptions} + onBlur={handleOnBlur} placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })} description={ @@ -423,137 +423,143 @@ const UpdateModal: React.FC = (props) => { > } > -
- - name="name" - rules={[ - { - required: true, - message: getRuleMessage('input', 'common.table.name') - } - ]} - > - - - - name="source" - rules={[ - { - required: true, - message: getRuleMessage('select', 'models.form.source') - } - ]} + - {action === PageAction.EDIT && ( - + name="name" + rules={[ + { + required: true, + message: getRuleMessage('input', 'common.table.name') + } + ]} + > + - )} - - {renderFieldsBySource} - - } - options={[ - { - label: `llama-box`, - value: backendOptionsMap.llamaBox, - disabled: - formData?.source === modelSourceMap.local_path_value - ? false - : !isGGUF - }, + > + + + 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 - } - > - - - name="replicas" - rules={[ - { - required: true, - message: getRuleMessage('input', 'models.form.replicas') - } - ]} - > - + {action === PageAction.EDIT && ( + )} - min={0} - > - - name="description"> - - + + {renderFieldsBySource} + + } + 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 + } + > + + + name="replicas" + rules={[ + { + required: true, + message: getRuleMessage('input', 'models.form.replicas') + } + ]} + > + + + name="description"> + + - - + + +
); diff --git a/src/pages/llmodels/config/form-context.ts b/src/pages/llmodels/config/form-context.ts index d52a2ba9..d71509dd 100644 --- a/src/pages/llmodels/config/form-context.ts +++ b/src/pages/llmodels/config/form-context.ts @@ -1,13 +1,14 @@ import React from 'react'; interface FormContextProps { - isGGUF: boolean; + isGGUF?: boolean; byBuiltIn?: boolean; sizeOptions?: Global.BaseOption[]; quantizationOptions?: Global.BaseOption[]; modelFileOptions?: any[]; onSizeChange?: (val: number) => void; onQuantizationChange?: (val: string) => void; + onValuesChange?: (val: any) => void; } interface FormInnerContextProps { diff --git a/src/pages/llmodels/config/index.ts b/src/pages/llmodels/config/index.ts index 238610ec..9949dcb0 100644 --- a/src/pages/llmodels/config/index.ts +++ b/src/pages/llmodels/config/index.ts @@ -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' ]; diff --git a/src/pages/llmodels/hooks/index.ts b/src/pages/llmodels/hooks/index.ts index a6ca4be5..2b224911 100644 --- a/src/pages/llmodels/hooks/index.ts +++ b/src/pages/llmodels/hooks/index.ts @@ -215,9 +215,11 @@ export const useGenerateModelFileOptions = () => { export const useCheckCompatibility = () => { const intl = useIntl(); + const cacheFormValuesRef = useRef({}); const checkTokenRef = useRef(null); const submitAnyway = useRef(false); const requestIdRef = useRef(0); + const updateStatusTimer = useRef(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); diff --git a/src/pages/resources/components/model-files.tsx b/src/pages/resources/components/model-files.tsx index 9911739c..98344c33 100644 --- a/src/pages/resources/components/model-files.tsx +++ b/src/pages/resources/components/model-files.tsx @@ -467,7 +467,7 @@ const ModelFiles = () => { return ( record.resolved_paths?.length > 0 && ( - + {getResolvedPath(record.resolved_paths)}