fix: edit mask for undo

This commit is contained in:
jialin
2025-03-18 17:15:55 +08:00
parent 08b3c9d1ac
commit 00ef7ab6ca
13 changed files with 200 additions and 134 deletions
@@ -43,6 +43,7 @@
padding-inline: 12px 12px; padding-inline: 12px 12px;
border-radius: 0 0 var(--border-radius-mini) var(--border-radius-mini); border-radius: 0 0 var(--border-radius-mini) var(--border-radius-mini);
position: relative; position: relative;
white-space: pre-wrap;
code { code {
line-height: 1.5; line-height: 1.5;
@@ -24,6 +24,7 @@ export default function useDrawing(props: {
const mouseDownState = useRef<boolean>(false); const mouseDownState = useRef<boolean>(false);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const overlayCanvasRef = useRef<HTMLCanvasElement>(null); const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
const offscreenCanvasRef = useRef<any>(null);
const currentStroke = useRef<Point[]>([]); const currentStroke = useRef<Point[]>([]);
const strokesRef = useRef<Stroke[]>([]); const strokesRef = useRef<Stroke[]>([]);
const isDrawing = useRef<boolean>(false); const isDrawing = useRef<boolean>(false);
@@ -101,20 +102,29 @@ export default function useDrawing(props: {
onSave({ mask, img }); onSave({ mask, img });
}; };
const creatOffscreenCanvas = useCallback(() => {
if (!offscreenCanvasRef.current) {
offscreenCanvasRef.current = document.createElement('canvas');
}
}, []);
const setTransform = useCallback(() => { const setTransform = useCallback(() => {
creatOffscreenCanvas();
const ctx = canvasRef.current?.getContext('2d'); const ctx = canvasRef.current?.getContext('2d');
const overlayCtx = overlayCanvasRef.current?.getContext('2d'); const overlayCtx = overlayCanvasRef.current?.getContext('2d');
const offCtx = offscreenCanvasRef.current?.getContext('2d');
if (!ctx || !overlayCtx) return; if (!ctx || !overlayCtx) return;
ctx!.resetTransform(); ctx!.resetTransform();
overlayCtx!.resetTransform(); overlayCtx!.resetTransform();
offCtx!.resetTransform();
const { current: scale } = autoScale; const { current: scale } = autoScale;
const { x: translateX, y: translateY } = translatePos.current; const { x: translateX, y: translateY } = translatePos.current;
ctx!.setTransform(scale, 0, 0, scale, translateX, translateY); ctx!.setTransform(scale, 0, 0, scale, translateX, translateY);
overlayCtx!.setTransform(scale, 0, 0, scale, translateX, translateY); overlayCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
offCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
}, []); }, []);
const getTransformedPoint = useCallback( const getTransformedPoint = useCallback(
@@ -134,9 +144,13 @@ export default function useDrawing(props: {
[] []
); );
const getTransformLineWidth = useCallback((lineWidth = 1) => { const getTransformLineWidth = useCallback(
return lineWidth / autoScale.current; (w = 1) => {
}, []); const width = w || lineWidth;
return width / autoScale.current;
},
[lineWidth]
);
const drawLine = useCallback( const drawLine = useCallback(
( (
@@ -180,6 +194,7 @@ export default function useDrawing(props: {
ctx.lineCap = 'round'; ctx.lineCap = 'round';
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
ctx.globalCompositeOperation = compositeOperation; ctx.globalCompositeOperation = compositeOperation;
ctx.save();
ctx.beginPath(); ctx.beginPath();
@@ -196,6 +211,7 @@ export default function useDrawing(props: {
ctx.strokeStyle = color; ctx.strokeStyle = color;
} }
ctx.stroke(); ctx.stroke();
ctx.restore();
}, },
[getTransformLineWidth, getTransformedPoint] [getTransformLineWidth, getTransformedPoint]
); );
@@ -323,6 +339,9 @@ export default function useDrawing(props: {
const clearOverlayCanvas = useCallback(() => { const clearOverlayCanvas = useCallback(() => {
const ctx = overlayCanvasRef.current!.getContext('2d'); const ctx = overlayCanvasRef.current!.getContext('2d');
const offCtx = offscreenCanvasRef.current?.getContext('2d');
offCtx!.resetTransform();
ctx!.resetTransform(); ctx!.resetTransform();
ctx!.clearRect( ctx!.clearRect(
0, 0,
@@ -330,33 +349,48 @@ export default function useDrawing(props: {
overlayCanvasRef.current!.width, overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height overlayCanvasRef.current!.height
); );
offCtx!.clearRect(
0,
0,
overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height
);
}, []); }, []);
const clearCanvas = useCallback(() => { const clearCanvas = useCallback(() => {
const canvas = canvasRef.current!; const canvas = canvasRef.current!;
const ctx = canvasRef.current!.getContext('2d'); const ctx = canvasRef.current!.getContext('2d');
const offCtx = offscreenCanvasRef.current?.getContext('2d');
ctx!.resetTransform(); ctx!.resetTransform();
ctx!.clearRect(0, 0, canvas.width, canvas.height); ctx!.clearRect(0, 0, canvas.width, canvas.height);
offCtx!.resetTransform();
offCtx!.clearRect(0, 0, canvas.width, canvas.height);
}, []); }, []);
const resetCanvas = useCallback(() => { const resetCanvas = useCallback(() => {
const canvas = canvasRef.current!; const canvas = canvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!; const overlayCanvas = overlayCanvasRef.current!;
const offscreenCanvas = offscreenCanvasRef.current!;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const overlayCtx = overlayCanvas.getContext('2d'); const overlayCtx = overlayCanvas.getContext('2d');
const offCtx = offscreenCanvasRef.current?.getContext('2d');
autoScale.current = 1; autoScale.current = 1;
baseScale.current = 1; baseScale.current = 1;
translatePos.current = { x: 0, y: 0 }; translatePos.current = { x: 0, y: 0 };
contentPos.current = { x: 0, y: 0 }; contentPos.current = { x: 0, y: 0 };
canvas.style.transform = 'scale(1)'; canvas.style.transform = 'scale(1)';
overlayCanvas.style.transform = 'scale(1)'; overlayCanvas.style.transform = 'scale(1)';
offscreenCanvas.style.transform = 'scale(1)';
cursorRef.current!.style.width = `${lineWidth}px`; cursorRef.current!.style.width = `${lineWidth}px`;
cursorRef.current!.style.height = `${lineWidth}px`; cursorRef.current!.style.height = `${lineWidth}px`;
ctx!.resetTransform(); ctx!.resetTransform();
overlayCtx!.resetTransform(); overlayCtx!.resetTransform();
offCtx!.resetTransform();
}, []); }, []);
const fitView = () => { const fitView = () => {
@@ -366,11 +400,13 @@ export default function useDrawing(props: {
setTransform(); setTransform();
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`; overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
canvasRef.current!.style.transform = `scale(${autoScale.current})`; canvasRef.current!.style.transform = `scale(${autoScale.current})`;
offscreenCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
}; };
return { return {
canvasRef, canvasRef,
overlayCanvasRef, overlayCanvasRef,
offscreenCanvasRef,
cursorRef, cursorRef,
strokesRef, strokesRef,
currentStroke, currentStroke,
@@ -379,6 +415,7 @@ export default function useDrawing(props: {
autoScale, autoScale,
baseScale, baseScale,
maskStorkeRef, maskStorkeRef,
creatOffscreenCanvas,
setMaskStrokes, setMaskStrokes,
fitView, fitView,
setStrokes, setStrokes,
@@ -4,6 +4,7 @@ import { MutableRefObject, useState } from 'react';
export default function useZoom(props: { export default function useZoom(props: {
overlayCanvasRef: any; overlayCanvasRef: any;
canvasRef: any; canvasRef: any;
offscreenCanvasRef: any;
cursorRef: any; cursorRef: any;
lineWidth: number; lineWidth: number;
autoScale: MutableRefObject<number>; autoScale: MutableRefObject<number>;
@@ -16,6 +17,7 @@ export default function useZoom(props: {
const { const {
overlayCanvasRef, overlayCanvasRef,
canvasRef, canvasRef,
offscreenCanvasRef,
cursorRef, cursorRef,
lineWidth, lineWidth,
translatePos, translatePos,
@@ -34,7 +36,6 @@ export default function useZoom(props: {
return; return;
} }
console.log('Setting transform origin:', autoScale.current);
const rect = overlayCanvasRef.current!.getBoundingClientRect(); const rect = overlayCanvasRef.current!.getBoundingClientRect();
const mouseX = e.clientX - rect.left; const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top; const mouseY = e.clientY - rect.top;
@@ -44,6 +45,7 @@ export default function useZoom(props: {
overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`; overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`; canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
offscreenCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
}; };
const updateZoom = (scaleChange: number, mouseX: number, mouseY: number) => { const updateZoom = (scaleChange: number, mouseX: number, mouseY: number) => {
@@ -77,6 +79,8 @@ export default function useZoom(props: {
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`; overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
canvasRef.current!.style.transform = `scale(${autoScale.current})`; canvasRef.current!.style.transform = `scale(${autoScale.current})`;
offscreenCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
setCanvasTransformOrigin(event); setCanvasTransformOrigin(event);
updateZoom(scaleChange, mouseX, mouseY); updateZoom(scaleChange, mouseX, mouseY);
}; };
+49 -42
View File
@@ -62,6 +62,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
const { const {
canvasRef, canvasRef,
overlayCanvasRef, overlayCanvasRef,
offscreenCanvasRef,
cursorRef, cursorRef,
strokesRef, strokesRef,
currentStroke, currentStroke,
@@ -79,6 +80,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
handleMouseEnter, handleMouseEnter,
saveImage, saveImage,
resetCanvas, resetCanvas,
creatOffscreenCanvas,
generateMask, generateMask,
getTransformLineWidth, getTransformLineWidth,
getTransformedPoint, getTransformedPoint,
@@ -99,6 +101,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
const { handleOnWheel, setActiveScale, activeScale } = useZoom({ const { handleOnWheel, setActiveScale, activeScale } = useZoom({
overlayCanvasRef, overlayCanvasRef,
canvasRef, canvasRef,
offscreenCanvasRef,
cursorRef, cursorRef,
lineWidth, lineWidth,
translatePos, translatePos,
@@ -114,9 +117,12 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
const updateCanvasSize = useCallback(() => { const updateCanvasSize = useCallback(() => {
const canvas = canvasRef.current!; const canvas = canvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!; const overlayCanvas = overlayCanvasRef.current!;
const offscreenCanvas = offscreenCanvasRef.current!;
overlayCanvas.width = canvas.width; overlayCanvas.width = canvas.width;
overlayCanvas.height = canvas.height; overlayCanvas.height = canvas.height;
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
}, []); }, []);
const downloadMask = useCallback(() => { const downloadMask = useCallback(() => {
@@ -135,26 +141,26 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
options: { options: {
lineWidth?: number; lineWidth?: number;
color: string; color: string;
isInitial?: boolean;
} }
) => { ) => {
const { color } = options; const { color, isInitial } = options;
stroke.forEach((point) => { stroke.forEach((point) => {
const { x, y } = getTransformedPoint(point.x, point.y); const { x, y } = getTransformedPoint(point.x, point.y);
const width = getTransformLineWidth(point.lineWidth); const width = getTransformLineWidth(point.lineWidth);
if (isInitial) {
ctx.save(); ctx.save();
ctx.fillStyle = 'rgba(0,0,0,1)';
// erase the previous stroke ctx.globalCompositeOperation = 'destination-out';
ctx.globalCompositeOperation = 'destination-out'; ctx.fillRect(x - width / 2, y - width / 2, width, width);
ctx.fillRect(x - width / 2, y - width / 2, width, width); ctx.restore();
}
// draw the new stroke // draw the new stroke
ctx.globalCompositeOperation = 'source-over'; ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.fillRect(x - width / 2, y - width / 2, width, width); ctx.fillRect(x - width / 2, y - width / 2, width, width);
ctx.restore();
}); });
}, },
[getTransformLineWidth, getTransformedPoint] [getTransformLineWidth, getTransformedPoint]
@@ -168,27 +174,31 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
console.log('Resetting strokes', currentStroke.current); console.log('Resetting strokes', currentStroke.current);
}, []); }, []);
const loadMaskPixs = (strokes: Stroke[]) => { const loadMaskPixs = (strokes: Stroke[], isInitial?: boolean) => {
clearOverlayCanvas();
if (!strokes.length) { if (!strokes.length) {
return; return;
} }
console.log('loadin mask pixs:');
const overlayCanvas = overlayCanvasRef.current!; const overlayCanvas = overlayCanvasRef.current!;
const overlayCtx = overlayCanvas!.getContext('2d')!; const overlayCtx = overlayCanvas.getContext('2d')!;
setTransform(); strokes.forEach((stroke: Point[]) => {
strokes?.forEach((stroke: Point[], index) => {
drawFillRect(overlayCtx, stroke, { drawFillRect(overlayCtx, stroke, {
color: COLOR color: COLOR,
isInitial: isInitial
}); });
}); });
}; };
const redrawStrokes = (strokes: Stroke[], type?: string) => {
console.log('Redrawing strokes:', strokes, type); const redrawStrokes = (strokes: Stroke[]) => {
clearOverlayCanvas(); clearOverlayCanvas();
if (!strokes.length) { setTransform();
console.log(
'Redrawing strokes:',
strokes.length,
maskStorkeRef.current.length
);
if (!strokes.length && !maskStorkeRef.current.length) {
return; return;
} }
@@ -198,8 +208,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
// clear offscreen canvas // clear offscreen canvas
setTransform();
overlayCtx.save();
loadMaskPixs(maskStorkeRef.current); loadMaskPixs(maskStorkeRef.current);
strokes?.forEach((stroke: Point[], index) => { strokes?.forEach((stroke: Point[], index) => {
@@ -213,10 +221,9 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
compositeOperation: 'source-over' compositeOperation: 'source-over'
}); });
}); });
overlayCtx.restore();
}; };
const undo = () => { const undo = useCallback(() => {
if ( if (
strokesRef.current.length === 0 && strokesRef.current.length === 0 &&
maskStorkeRef.current.length === 0 maskStorkeRef.current.length === 0
@@ -225,25 +232,23 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
return; return;
} }
const lastlength = strokesRef.current.length;
const newStrokes = strokesRef.current.slice(0, -1); const newStrokes = strokesRef.current.slice(0, -1);
setStrokes(newStrokes); setStrokes(newStrokes);
console.log( if (
'newstrokes=======', !newStrokes.length &&
newStrokes.length, lastlength === 0 &&
maskStorkeRef.current.length maskStorkeRef.current.length
); ) {
if (strokesRef.current.length) {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
redrawStrokes(strokesRef.current);
}, 100);
} else if (maskStorkeRef.current.length) {
loadMaskPixs(maskStorkeRef.current);
setMaskStrokes([]); setMaskStrokes([]);
} }
}; clearTimeout(timer.current);
timer.current = setTimeout(() => {
redrawStrokes(newStrokes);
}, 100);
}, []);
const downloadOriginImage = () => { const downloadOriginImage = () => {
const canvas = canvasRef.current!; const canvas = canvasRef.current!;
@@ -308,7 +313,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
// fit the image to the container // fit the image to the container
autoScale.current = 1; autoScale.current = 1;
creatOffscreenCanvas();
updateCanvasSize(); updateCanvasSize();
clearCanvas(); clearCanvas();
@@ -335,7 +340,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
setTransform(); setTransform();
ctx.globalCompositeOperation = 'destination-out'; ctx.globalCompositeOperation = 'destination-out';
strokesRef.current.forEach((stroke) => { [...strokesRef.current, ...maskStorkeRef.current].forEach((stroke) => {
stroke.forEach((point: Point) => { stroke.forEach((point: Point) => {
const { x, y } = getTransformedPoint(point.x, point.y); const { x, y } = getTransformedPoint(point.x, point.y);
const lineWidth = getTransformLineWidth(point.lineWidth); const lineWidth = getTransformLineWidth(point.lineWidth);
@@ -369,14 +374,14 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
onReset(); onReset();
resetCanvas(); resetCanvas();
} else if ( } else if (
strokesRef.current.length && (strokesRef.current.length || maskStorkeRef.current.length) &&
imageStatus.isOriginal && imageStatus.isOriginal &&
!invertMask !invertMask
) { ) {
redrawStrokes(strokesRef.current); redrawStrokes(strokesRef.current);
saveImage(); saveImage();
} else if ( } else if (
strokesRef.current.length && (strokesRef.current.length || maskStorkeRef.current.length) &&
imageStatus.isOriginal && imageStatus.isOriginal &&
invertMask invertMask
) { ) {
@@ -456,7 +461,9 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
loadMaskPixs(strokes: Stroke[]) { loadMaskPixs(strokes: Stroke[]) {
setMaskStrokes(strokes); setMaskStrokes(strokes);
setStrokes([]); setStrokes([]);
loadMaskPixs(strokes); setTransform();
clearOverlayCanvas();
loadMaskPixs(strokes, true);
} }
})); }));
+8 -3
View File
@@ -1,11 +1,13 @@
import { useIntl } from '@umijs/max';
import { Typography } from 'antd'; import { Typography } from 'antd';
import React from 'react'; import React from 'react';
interface TooltipListProps { interface TooltipListProps {
list: { title: React.ReactNode; tips: React.ReactNode }[]; list: { title: any; tips: string }[];
} }
const TooltipList: React.FC<TooltipListProps> = (props) => { const TooltipList: React.FC<TooltipListProps> = (props) => {
const intl = useIntl();
const { list } = props; const { list } = props;
return ( return (
<ul className="tips-desc-list"> <ul className="tips-desc-list">
@@ -20,7 +22,10 @@ const TooltipList: React.FC<TooltipListProps> = (props) => {
marginBottom: 0 marginBottom: 0
}} }}
> >
{item.title}: {item.title?.locale
? intl.formatMessage({ id: item.title?.text || '' })
: item.title}
:
</Typography.Title> </Typography.Title>
<Typography.Text <Typography.Text
style={{ style={{
@@ -29,7 +34,7 @@ const TooltipList: React.FC<TooltipListProps> = (props) => {
lineHeight: 1.5 lineHeight: 1.5
}} }}
> >
{item.tips} {intl.formatMessage({ id: item.tips })}
</Typography.Text> </Typography.Text>
</li> </li>
); );
+3 -1
View File
@@ -81,7 +81,7 @@ export default {
'models.form.filePath': 'Model Path', 'models.form.filePath': 'Model Path',
'models.form.backendVersion': 'Backend Version', 'models.form.backendVersion': 'Backend Version',
'models.form.backendVersion.tips': 'models.form.backendVersion.tips':
'To use the desired version of vLLM/llama-box/vox-box, the system will automatically create a virtual environment in the online environment to install the corresponding version. After a GPUStack upgrade, the backend version will remain fixed. {link}', 'To use the desired version of {backend}, the system will automatically create a virtual environment in the online environment to install the corresponding version. After a GPUStack upgrade, the backend version will remain fixed. {link}',
'models.form.gpuselector': 'GPU Selector', 'models.form.gpuselector': 'GPU Selector',
'models.form.backend.llamabox': 'models.form.backend.llamabox':
'For GGUF format models, supports Linux, macOS, and Windows.', 'For GGUF format models, supports Linux, macOS, and Windows.',
@@ -114,6 +114,8 @@ export default {
'models.table.vram.allocated': 'Allocated VRAM', 'models.table.vram.allocated': 'Allocated VRAM',
'models.form.backend.warning': 'models.form.backend.warning':
'The backend for GGUF format models uses llama-box.', 'The backend for GGUF format models uses llama-box.',
'models.form.ollama.warning':
'Deploy the Ollama model backend using llama-box.',
'models.form.backend.warning.llamabox': 'models.form.backend.warning.llamabox':
'To use the llama-box backend, specify the full path to the model file (e.g.,<span style="font-weight: 700">/data/models/model.gguf</span>). For sharded models, provide the path to the first shard (e.g.,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).' 'To use the llama-box backend, specify the full path to the model file (e.g.,<span style="font-weight: 700">/data/models/model.gguf</span>). For sharded models, provide the path to the first shard (e.g.,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).'
}; };
+4 -1
View File
@@ -81,7 +81,7 @@ export default {
'models.form.filePath': 'Путь к модели', 'models.form.filePath': 'Путь к модели',
'models.form.backendVersion': 'Версия бэкенда', 'models.form.backendVersion': 'Версия бэкенда',
'models.form.backendVersion.tips': 'models.form.backendVersion.tips':
'To use the desired version of vLLM/llama-box/vox-box, the system will automatically create a virtual environment in the online environment to install the corresponding version. After a GPUStack upgrade, the backend version will remain fixed. {link}', 'To use the desired version of {backend}, the system will automatically create a virtual environment in the online environment to install the corresponding version. After a GPUStack upgrade, the backend version will remain fixed. {link}',
'models.form.gpuselector': 'Селектор GPU', 'models.form.gpuselector': 'Селектор GPU',
'models.form.backend.llamabox': 'models.form.backend.llamabox':
'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.', 'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
@@ -113,6 +113,8 @@ export default {
'models.table.vram.allocated': 'Allocated VRAM', 'models.table.vram.allocated': 'Allocated VRAM',
'models.form.backend.warning': 'models.form.backend.warning':
'The backend for GGUF format models uses llama-box.', 'The backend for GGUF format models uses llama-box.',
'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.,<span style="font-weight: 700">/data/models/model.gguf</span>). For sharded models, provide the path to the first shard (e.g.,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).` 'models.form.backend.warning.llamabox': `To use the llama-box backend, specify the full path to the model file (e.g.,<span style="font-weight: 700">/data/models/model.gguf</span>). For sharded models, provide the path to the first shard (e.g.,<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>).`
}; };
@@ -122,5 +124,6 @@ export default {
// 2. models.form.backend.warning // 2. models.form.backend.warning
// 3. models.form.backend.warning.llamabox // 3. models.form.backend.warning.llamabox
// 4. models.table.vram.allocated // 4. models.table.vram.allocated
// 5. models.form.ollama.warning
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+2 -1
View File
@@ -78,7 +78,7 @@ export default {
'models.form.filePath': '模型路径', 'models.form.filePath': '模型路径',
'models.form.backendVersion': '后端版本', 'models.form.backendVersion': '后端版本',
'models.form.backendVersion.tips': 'models.form.backendVersion.tips':
'固定以使用期望的 vLLM/llama-box/vox-box 版本,在线环境会自动创建虚拟环境安装对应版本的vLLM/llama-box/vox-box。在 GPUStack 升级后也将保持固定的后端版本。{link}', '固定以使用期望的 {backend} 版本,在线环境会自动创建虚拟环境安装对应版本的 {backend}。在 GPUStack 升级后也将保持固定的后端版本。{link}',
'models.form.gpuselector': 'GPU 选择器', 'models.form.gpuselector': 'GPU 选择器',
'models.form.backend.llamabox': 'models.form.backend.llamabox':
'用于 GGUF 格式模型,支持 Linux, macOS 和 Windows', '用于 GGUF 格式模型,支持 Linux, macOS 和 Windows',
@@ -108,6 +108,7 @@ export default {
'models.form.moreparameters': '参数说明', 'models.form.moreparameters': '参数说明',
'models.table.vram.allocated': '分配显存', 'models.table.vram.allocated': '分配显存',
'models.form.backend.warning': 'GGUF 格式模型后端用 llama-box。', 'models.form.backend.warning': 'GGUF 格式模型后端用 llama-box。',
'models.form.ollama.warning': '部署 Ollama 模型后端使用 llama-box。',
'models.form.backend.warning.llamabox': '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>)。' '要使用 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>)。'
}; };
@@ -340,6 +340,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
id: 'models.form.backendVersion.tips' id: 'models.form.backendVersion.tips'
}, },
{ {
backend: backend,
link: backendParamsTips?.releases && ( link: backendParamsTips?.releases && (
<span <span
style={{ style={{
+1 -30
View File
@@ -21,6 +21,7 @@ import {
HuggingFaceTaskMap, HuggingFaceTaskMap,
ModelscopeTaskMap, ModelscopeTaskMap,
backendOptionsMap, backendOptionsMap,
backendTipsList,
modelSourceMap, modelSourceMap,
modelTaskMap, modelTaskMap,
ollamaModelOptions, ollamaModelOptions,
@@ -89,36 +90,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const localPathCache = useRef<string>(''); const localPathCache = useRef<string>('');
const backendTipsList = [
{
title: 'llama-box',
tips: intl.formatMessage({ id: 'models.form.backend.llamabox' })
},
{
title: 'vLLM',
tips: intl.formatMessage({ id: 'models.form.backend.vllm' })
},
{
title: 'vox-box',
tips: intl.formatMessage({ id: 'models.form.backend.voxbox' })
}
];
const localPathTipsList = [
{
title: intl.formatMessage({ id: 'models.localpath.gguf.tips.title' }),
tips: intl.formatMessage({ id: 'models.localpath.gguf.tips' })
},
{
title: intl.formatMessage({ id: 'models.localpath.shared.tips.title' }),
tips: intl.formatMessage({ id: 'models.localpath.chunks.tips' })
},
{
title: intl.formatMessage({ id: 'models.localpat.safe.tips.title' }),
tips: intl.formatMessage({ id: 'models.localpath.safe.tips' })
}
];
const handleSumit = () => { const handleSumit = () => {
form.submit(); form.submit();
}; };
+20 -25
View File
@@ -81,39 +81,34 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const updateShowWarning = (backend: string) => { const updateShowWarning = (backend: string) => {
const localPath = form.current?.getFieldValue?.('local_path'); const localPath = form.current?.getFieldValue?.('local_path');
const isBlobFile = localPath?.split('/').pop()?.includes('sha256');
if (isBlobFile) {
setWarningStatus({
show: false,
message: ''
});
return;
}
if (source !== modelSourceMap.local_path_value || !localPath) { if (source !== modelSourceMap.local_path_value || !localPath) {
return; return;
} }
if (localPath.endsWith('.gguf') && backend !== backendOptionsMap.llamaBox) { const isBlobFile = localPath?.split('/').pop()?.includes('sha256');
setWarningStatus({ const isOllamaModel = localPath?.includes('ollama');
show: true, const isGGUFFile = localPath.endsWith('.gguf');
message: 'models.form.backend.warning'
}); let warningMessage = '';
if (isBlobFile && isOllamaModel && backend === backendOptionsMap.llamaBox) {
warningMessage = '';
} else if ( } else if (
!localPath.endsWith('.gguf') && isBlobFile &&
backend === backendOptionsMap.llamaBox isOllamaModel &&
backend !== backendOptionsMap.llamaBox
) { ) {
setWarningStatus({ warningMessage = 'models.form.ollama.warning';
show: true, } else if (isGGUFFile && backend !== backendOptionsMap.llamaBox) {
message: 'models.form.backend.warning.llamabox' warningMessage = 'models.form.backend.warning';
}); } else if (!isGGUFFile && backend === backendOptionsMap.llamaBox) {
} else { warningMessage = 'models.form.backend.warning.llamabox';
setWarningStatus({
show: false,
message: ''
});
} }
setWarningStatus({
show: !!warningMessage,
message: warningMessage
});
}; };
const handleBackendChange = (backend: string) => { const handleBackendChange = (backend: string) => {
+26 -26
View File
@@ -4,6 +4,7 @@ import ModalFooter from '@/components/modal-footer';
import SealAutoComplete from '@/components/seal-form/auto-complete'; import SealAutoComplete from '@/components/seal-form/auto-complete';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import TooltipList from '@/components/tooltip-list';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
@@ -12,6 +13,8 @@ import _ from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { import {
backendOptionsMap, backendOptionsMap,
backendTipsList,
localPathTipsList,
modelSourceMap, modelSourceMap,
ollamaModelOptions ollamaModelOptions
} from '../config'; } from '../config';
@@ -98,39 +101,34 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
const updateShowWarning = (backend: string) => { const updateShowWarning = (backend: string) => {
const localPath = form.getFieldValue?.('local_path'); const localPath = form.getFieldValue?.('local_path');
const isBlobFile = localPath?.split('/').pop()?.includes('sha256');
if (isBlobFile) {
setWarningStatus({
show: false,
message: ''
});
return;
}
if (formData?.source !== modelSourceMap.local_path_value || !localPath) { if (formData?.source !== modelSourceMap.local_path_value || !localPath) {
return; return;
} }
if (localPath.endsWith('.gguf') && backend !== backendOptionsMap.llamaBox) { const isBlobFile = localPath?.split('/').pop()?.includes('sha256');
setWarningStatus({ const isOllamaModel = localPath?.includes('ollama');
show: true, const isGGUFFile = localPath.endsWith('.gguf');
message: 'models.form.backend.warning'
}); let warningMessage = '';
if (isBlobFile && isOllamaModel && backend === backendOptionsMap.llamaBox) {
warningMessage = '';
} else if ( } else if (
!localPath.endsWith('.gguf') && isBlobFile &&
backend === backendOptionsMap.llamaBox isOllamaModel &&
backend !== backendOptionsMap.llamaBox
) { ) {
setWarningStatus({ warningMessage = 'models.form.ollama.warning';
show: true, } else if (isGGUFFile && backend !== backendOptionsMap.llamaBox) {
message: 'models.form.backend.warning.llamabox' warningMessage = 'models.form.backend.warning';
}); } else if (!isGGUFFile && backend === backendOptionsMap.llamaBox) {
} else { warningMessage = 'models.form.backend.warning.llamabox';
setWarningStatus({
show: false,
message: ''
});
} }
setWarningStatus({
show: !!warningMessage,
message: warningMessage
});
}; };
const handleBackendChange = (val: string) => { const handleBackendChange = (val: string) => {
@@ -284,6 +282,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
onFocus={handleOnFocus} onFocus={handleOnFocus}
disabled={false} disabled={false}
label={intl.formatMessage({ id: 'models.form.filePath' })} label={intl.formatMessage({ id: 'models.form.filePath' })}
description={<TooltipList list={localPathTipsList}></TooltipList>}
required required
></SealInput.Input> ></SealInput.Input>
</Form.Item> </Form.Item>
@@ -519,6 +518,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
required required
onChange={handleBackendChange} onChange={handleBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })} label={intl.formatMessage({ id: 'models.form.backend' })}
description={<TooltipList list={backendTipsList}></TooltipList>}
options={[ options={[
{ {
label: `llama-box`, label: `llama-box`,
+39
View File
@@ -88,6 +88,45 @@ export const ollamaModelOptions = [
} }
]; ];
export const backendTipsList = [
{
title: 'llama-box',
tips: 'models.form.backend.llamabox'
},
{
title: 'vLLM',
tips: 'models.form.backend.vllm'
},
{
title: 'vox-box',
tips: 'models.form.backend.voxbox'
}
];
export const localPathTipsList = [
{
title: {
text: 'models.localpath.gguf.tips.title',
locale: true
},
tips: 'models.localpath.gguf.tips'
},
{
title: {
text: 'models.localpath.shared.tips.title',
locale: true
},
tips: 'models.localpath.chunks.tips'
},
{
title: {
text: 'models.localpat.safe.tips.title',
locale: true
},
tips: 'models.localpath.safe.tips'
}
];
export const backendOptionsMap = { export const backendOptionsMap = {
llamaBox: 'llama-box', llamaBox: 'llama-box',
vllm: 'vllm', vllm: 'vllm',