diff --git a/src/components/image-editor/index.tsx b/src/components/image-editor/index.tsx index 50b8f666..a89e870e 100644 --- a/src/components/image-editor/index.tsx +++ b/src/components/image-editor/index.tsx @@ -1,3 +1,4 @@ +import { Spin } from 'antd'; import classNames from 'classnames'; import dayjs from 'dayjs'; import React, { @@ -9,11 +10,32 @@ import React, { useRef, useState } from 'react'; +import styled from 'styled-components'; import useDrawing from './hooks/use-drawing'; import useZoom from './hooks/use-zoom'; import './index.less'; import { ImageActionsBar, ToolsBar } from './tools-bar'; +const LoadWrapper = styled.div<{ width?: number; height?: number }>` + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: ${(props) => `${props.width}px` || '100%'}; + z-index: 100; +`; +const Loading = (props: { width: number; height: number }) => { + const { width, height } = props; + return ( + + + + ); +}; + type Point = { x: number; y: number; lineWidth: number }; type Stroke = Point[]; @@ -58,6 +80,11 @@ const CanvasImageEditor: React.FC = forwardRef( const negativeMaskRef = useRef(false); const [invertMask, setInvertMask] = useState(false); const timer = useRef(null); + const [loadingSize, setLoadingSize] = useState({ + width: 0, + height: 0, + loading: false + }); const { canvasRef, @@ -174,7 +201,7 @@ const CanvasImageEditor: React.FC = forwardRef( console.log('Resetting strokes', currentStroke.current); }, []); - const loadMaskPixs = (strokes: Stroke[], isInitial?: boolean) => { + const loadMaskPixs = async (strokes: Stroke[], isInitial?: boolean) => { if (!strokes.length) { return; } @@ -458,7 +485,7 @@ const CanvasImageEditor: React.FC = forwardRef( useImperativeHandle(ref, () => ({ clearMask: handleDeleteMask, - loadMaskPixs(strokes: Stroke[]) { + loadMaskPixs: async function (strokes: Stroke[]) { setMaskStrokes(strokes); setStrokes([]); setTransform(); @@ -514,6 +541,12 @@ const CanvasImageEditor: React.FC = forwardRef( flex: 1 }} > + {loadingSize.loading && ( + + )} = ({ btnText, description }) => { + const intl = useIntl(); const [labelsData, setLabelsData] = useState({}); const [labelList, setLabelList] = useState<{ key: string; value: string }[]>( [] @@ -46,15 +48,45 @@ const LabelSelector: React.FC = ({ onChange?.(data); }; + const handleOnPaste = ( + e: React.ClipboardEvent, + index: number + ) => { + e.preventDefault(); + + const clipboardText = e.clipboardData.getData('text'); + if (!clipboardText) return; + + const lines = clipboardText + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && line.includes('=')); + + const parsedData = lines.map((line) => { + const [key, value] = line.split('=').map((part) => part.trim()); + return { key, value }; + }); + console.log(lines, parsedData); + + setLabelList((prevPairs) => { + const newPairs = [...prevPairs]; + newPairs.splice(index, 1, ...parsedData); + return newPairs; + }); + }; + return ( ); }; diff --git a/src/components/label-selector/inner.tsx b/src/components/label-selector/inner.tsx index 9d45eb18..629f7f30 100644 --- a/src/components/label-selector/inner.tsx +++ b/src/components/label-selector/inner.tsx @@ -12,6 +12,7 @@ interface LabelSelectorProps { labelList: Array<{ key: string; value: string }>; onLabelListChange: (list: { key: string; value: string }[]) => void; onChange?: (labels: Record) => void; + onPaste?: (e: any, index: number) => void; description?: React.ReactNode; } @@ -20,6 +21,7 @@ const Inner: React.FC = ({ labelList, onChange, onLabelListChange, + onPaste, label, btnText, description @@ -41,7 +43,7 @@ const Inner: React.FC = ({ ); onChange?.(newLabels); }; - const handleOnChange = (index: string, label: any) => { + const handleOnChange = (index: number, label: any) => { const list = _.cloneDeep(labelList); list[index] = label; onLabelListChange(list); @@ -60,9 +62,9 @@ const Inner: React.FC = ({ updateLabels(newLabelList); }; - const handleOnDelete = (index: string) => { + const handleOnDelete = (index: number) => { const list = _.cloneDeep(labelList); - list.splice(parseInt(index), 1); + list.splice(index, 1); onLabelListChange(list); updateLabels(list); }; @@ -70,7 +72,7 @@ const Inner: React.FC = ({ return ( <> - {_.map(labelList, (item: any, index: string) => { + {labelList?.map((item: any, index: number) => { return ( = ({ labelList={labelList} onDelete={() => handleOnDelete(index)} onChange={(obj) => handleOnChange(index, obj)} + onPaste={(e) => onPaste?.(e, index)} /> ); })} diff --git a/src/components/label-selector/label-item.tsx b/src/components/label-selector/label-item.tsx index 0e187222..d201c05c 100644 --- a/src/components/label-selector/label-item.tsx +++ b/src/components/label-selector/label-item.tsx @@ -18,6 +18,7 @@ interface LabelItemProps { onDelete?: () => void; labelList: { key: string; value: string }[]; onChange?: (params: { key: string; value: string }) => void; + onPaste?: (e: any) => void; } const LabelItem: React.FC = ({ label, @@ -26,7 +27,8 @@ const LabelItem: React.FC = ({ keyAddon, valueAddon, onChange, - onDelete + onDelete, + onPaste }) => { const intl = useIntl(); const [open, setOpen] = useState(false); @@ -82,6 +84,7 @@ const LabelItem: React.FC = ({ value={label.key} onChange={handleOnKeyChange} onBlur={handleKeyOnBlur} + onPaste={onPaste} > )} diff --git a/src/hooks/use-window-resize.ts b/src/hooks/use-window-resize.ts index 74946172..1cbdf5e0 100644 --- a/src/hooks/use-window-resize.ts +++ b/src/hooks/use-window-resize.ts @@ -7,7 +7,6 @@ export default function useWindowResize() { width: window.innerWidth, height: window.innerHeight }); - console.log('size:', window.innerWidth, window.innerHeight); const [isMobile, setIsMobile] = useState(false); const [isTablet, setIsTablet] = useState(false); const [isDesktop, setIsDesktop] = useState(false); diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 06e98f3d..7c9d301a 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -117,5 +117,7 @@ export default { 'models.form.ollama.warning': 'Deploy the Ollama model backend using llama-box.', 'models.form.backend.warning.llamabox': - 'To use the llama-box backend, specify the full path to the model file (e.g.,/data/models/model.gguf). For sharded models, provide the path to the first shard (e.g.,/data/models/model-00001-of-00004.gguf).' + 'To use the llama-box backend, specify the full path to the model file (e.g.,/data/models/model.gguf). For sharded models, provide the path to the first shard (e.g.,/data/models/model-00001-of-00004.gguf).', + 'models.form.keyvalue.paste': + 'Paste multiple lines of text, with each line containing a key-value pair. The key and value are separated by an = sign, and different key-value pairs are separated by newline characters.' }; diff --git a/src/locales/ru-RU/models.ts b/src/locales/ru-RU/models.ts index bf0b5347..b39b798e 100644 --- a/src/locales/ru-RU/models.ts +++ b/src/locales/ru-RU/models.ts @@ -13,8 +13,10 @@ 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': + 'При включении CPU оффлоудинга GPUStack загружает максимум слоев на GPU для производительности. При нехватке ресурсов GPU часть слоев переносится на CPU. Полная CPU-инференция используется только при отсутствии GPU.', + 'models.form.distribution.tips': + 'Позволяет распределить вычисления между одним или несколькими удаленными воркерами при нехватке ресурсов одного GPU/воркера.', 'models.openinplayground': 'Открыть в Песочнице', 'models.instances': 'инстансы', 'models.table.replicas.edit': 'Редактировать реплики', @@ -22,7 +24,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 +42,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 +60,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 +80,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,23 +96,32 @@ 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': 'Начните работу с 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.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': + 'Paste multiple lines of text, with each line containing a key-value pair. The key and value are separated by an = sign, and different key-value pairs are separated by newline characters.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== - - +//1. 'models.form.keyvalue.paste' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index 068c6717..e26562e4 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -110,5 +110,7 @@ export default { '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)。' + '要使用 llama-box 后端,请指定模型文件的完整路径(例如:/data/models/model.gguf)。对于分片模型,请提供第一个分片的路径(例如:/data/models/model-00001-of-00004.gguf)。', + 'models.form.keyvalue.paste': + '粘贴多行文本,每行包含一个键值对,键和值之间用 = 号分隔,不同的键值对之间用换行符分隔。' };