fix: edit image: image field is null

This commit is contained in:
jialin
2025-03-06 16:34:55 +08:00
parent 25d70b7b49
commit b0ac85d81d
8 changed files with 996 additions and 916 deletions
+74 -36
View File
@@ -1,7 +1,6 @@
import { KeyMap } from '@/config/hotkeys'; import { KeyMap } from '@/config/hotkeys';
import { import {
ClearOutlined, ClearOutlined,
CloseOutlined,
DownloadOutlined, DownloadOutlined,
ExpandOutlined, ExpandOutlined,
FormatPainterOutlined, FormatPainterOutlined,
@@ -12,8 +11,10 @@ import { Button, Checkbox, Slider, Tooltip } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import React, { import React, {
forwardRef,
useCallback, useCallback,
useEffect, useEffect,
useImperativeHandle,
useMemo, useMemo,
useRef, useRef,
useState useState
@@ -25,6 +26,7 @@ type Point = { x: number; y: number; lineWidth: number };
type Stroke = Point[]; type Stroke = Point[];
type CanvasImageEditorProps = { type CanvasImageEditorProps = {
ref?: any;
imageSrc: string; imageSrc: string;
disabled?: boolean; disabled?: boolean;
imguid: string | number; imguid: string | number;
@@ -43,7 +45,9 @@ type CanvasImageEditorProps = {
const COLOR = 'rgba(0, 0, 255, 0.3)'; const COLOR = 'rgba(0, 0, 255, 0.3)';
const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
(
{
imageSrc, imageSrc,
disabled: isDisabled, disabled: isDisabled,
imageStatus, imageStatus,
@@ -53,7 +57,9 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
imguid, imguid,
uploadButton, uploadButton,
maskUpload maskUpload
}) => { },
ref
) => {
const MIN_SCALE = 0.5; const MIN_SCALE = 0.5;
const MAX_SCALE = 8; const MAX_SCALE = 8;
const ZOOM_SPEED = 0.1; const ZOOM_SPEED = 0.1;
@@ -103,7 +109,9 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
return lineWidth / autoScale.current; return lineWidth / autoScale.current;
}, []); }, []);
const setCanvasTransformOrigin = (e: React.MouseEvent<HTMLCanvasElement>) => { const setCanvasTransformOrigin = (
e: React.MouseEvent<HTMLCanvasElement>
) => {
if (autoScale.current <= MIN_SCALE) { if (autoScale.current <= MIN_SCALE) {
return; return;
} }
@@ -177,7 +185,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
offscreenCanvas.width = canvas.width; offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height; offscreenCanvas.height = canvas.height;
}, [canvasRef.current, overlayCanvasRef.current, offscreenCanvasRef.current]); }, []);
const setStrokes = (strokes: Stroke[]) => { const setStrokes = (strokes: Stroke[]) => {
strokesRef.current = strokes; strokesRef.current = strokes;
@@ -456,7 +464,12 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
const offscreenCanvas = offscreenCanvasRef.current!; const offscreenCanvas = offscreenCanvasRef.current!;
const offscreenCtx = offscreenCanvas.getContext('2d')!; const offscreenCtx = offscreenCanvas.getContext('2d')!;
offscreenCtx.resetTransform(); offscreenCtx.resetTransform();
offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height); offscreenCtx.clearRect(
0,
0,
offscreenCanvas.width,
offscreenCanvas.height
);
const { current: scale } = autoScale; const { current: scale } = autoScale;
const { x: translateX, y: translateY } = translatePos.current; const { x: translateX, y: translateY } = translatePos.current;
offscreenCtx!.setTransform(scale, 0, 0, scale, translateX, translateY); offscreenCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
@@ -465,8 +478,8 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
const onReset = useCallback(() => { const onReset = useCallback(() => {
clearOverlayCanvas(); clearOverlayCanvas();
setStrokes([]); setStrokes([]);
currentStroke.current = [];
saveImage(); saveImage();
currentStroke.current = [];
console.log('Resetting strokes', currentStroke.current); console.log('Resetting strokes', currentStroke.current);
}, []); }, []);
@@ -585,15 +598,15 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
// fit the image to the container // fit the image to the container
autoScale.current = autoScale.current || 1; autoScale.current = autoScale.current || 1;
updateCanvasSize();
updateCanvasSize();
clearCanvas(); clearCanvas();
ctx!.drawImage(img, 0, 0, canvas!.width, canvas!.height); ctx!.drawImage(img, 0, 0, canvas!.width, canvas!.height);
resolve(); resolve();
}; };
}); });
}, [imageSrc, containerRef.current, canvasRef.current, updateCanvasSize]); }, [imageSrc]);
const resetCanvas = useCallback(() => { const resetCanvas = useCallback(() => {
const canvas = canvasRef.current!; const canvas = canvasRef.current!;
@@ -654,34 +667,46 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
}; };
const initializeImage = useCallback(async () => { const initializeImage = useCallback(async () => {
if (imguid === preImguid.current) {
return;
}
await drawImage(); await drawImage();
onScaleImageSize?.({ onScaleImageSize?.({
width: canvasRef.current!.width, width: canvasRef.current!.width,
height: canvasRef.current!.height height: canvasRef.current!.height
}); });
if (strokeCache.current[imguid]) { console.log('Image status:', imageStatus, invertMask);
strokeCache.current[preImguid.current] = strokesRef.current;
strokesRef.current = strokeCache.current[imguid]; if (imageStatus.isResetNeeded) {
} else if (preImguid.current !== imguid) {
strokeCache.current[preImguid.current] = strokesRef.current;
onReset(); onReset();
resetCanvas(); resetCanvas();
} } else if (
preImguid.current = imguid; strokesRef.current.length &&
imageStatus.isOriginal &&
if (strokesRef.current.length && imageStatus.isOriginal) { !invertMask
) {
redrawStrokes(strokesRef.current); redrawStrokes(strokesRef.current);
saveImage(); saveImage();
} else if (
strokesRef.current.length &&
imageStatus.isOriginal &&
invertMask
) {
invertPainting(true);
saveImage();
} }
updateCursorSize();
}, [drawImage, onReset, redrawStrokes, imguid]);
const updateZoom = (scaleChange: number, mouseX: number, mouseY: number) => { updateCursorSize();
}, [
drawImage,
imageStatus.isOriginal,
imageStatus.isResetNeeded,
invertMask
]);
const updateZoom = (
scaleChange: number,
mouseX: number,
mouseY: number
) => {
const newScale = _.round(autoScale.current + scaleChange, 2); const newScale = _.round(autoScale.current + scaleChange, 2);
if (newScale < MIN_SCALE || newScale > MAX_SCALE) return; if (newScale < MIN_SCALE || newScale > MAX_SCALE) return;
@@ -789,11 +814,15 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
}, []); }, []);
const handleDeleteMask = () => { const handleDeleteMask = () => {
clearUploadMask?.();
setInvertMask(false); setInvertMask(false);
redrawStrokes(strokesRef.current); redrawStrokes(strokesRef.current);
saveImage();
}; };
useImperativeHandle(ref, () => ({
clearMask: handleDeleteMask
}));
useEffect(() => { useEffect(() => {
if (maskUpload?.length) { if (maskUpload?.length) {
// clear the overlay canvas // clear the overlay canvas
@@ -801,6 +830,12 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
} }
}, [maskUpload]); }, [maskUpload]);
useEffect(() => {
if (disabled && cursorRef.current) {
cursorRef.current!.style.display = 'none';
}
}, [disabled]);
return ( return (
<div className="editor-wrapper"> <div className="editor-wrapper">
<div className="flex-between"> <div className="flex-between">
@@ -884,23 +919,22 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
<span className="flex-center upload-mask"> <span className="flex-center upload-mask">
<span className="font-size-12"> <span className="font-size-12">
{intl.formatMessage({ id: 'playground.image.mask.uploaded' })} {intl.formatMessage({ id: 'playground.image.mask.uploaded' })}
...
</span> </span>
<Button
onClick={handleDeleteMask}
size="small"
type="text"
icon={<CloseOutlined className="close-btn"></CloseOutlined>}
></Button>
</span> </span>
) : ( ) : (
<> <>
{imageStatus.isOriginal && ( {imageStatus.isOriginal && (
<> <>
<Tooltip
title={intl.formatMessage({
id: 'playground.image.negativeMask.tips'
})}
>
<Checkbox <Checkbox
onChange={handleOnChangeMask} onChange={handleOnChangeMask}
className="flex-center" className="flex-center"
value={invertMask} checked={invertMask}
disabled={isDisabled || !!maskUpload?.length}
> >
<span className="font-size-12"> <span className="font-size-12">
{intl.formatMessage({ {intl.formatMessage({
@@ -908,6 +942,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
})} })}
</span> </span>
</Checkbox> </Checkbox>
</Tooltip>
<Tooltip <Tooltip
title={intl.formatMessage({ title={intl.formatMessage({
id: 'playground.image.saveMask' id: 'playground.image.saveMask'
@@ -924,6 +959,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
)} )}
</> </>
)} )}
{!imageStatus.isOriginal && (
<Tooltip <Tooltip
title={intl.formatMessage({ id: 'playground.image.download' })} title={intl.formatMessage({ id: 'playground.image.download' })}
> >
@@ -931,6 +967,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
<DownloadOutlined className="font-size-14" /> <DownloadOutlined className="font-size-14" />
</Button> </Button>
</Tooltip> </Tooltip>
)}
</div> </div>
</div> </div>
<div <div
@@ -970,7 +1007,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
<div <div
ref={cursorRef} ref={cursorRef}
style={{ style={{
display: disabled ? 'none' : 'block', display: 'none',
position: 'fixed', position: 'fixed',
width: lineWidth * activeScale, width: lineWidth * activeScale,
height: lineWidth * activeScale, height: lineWidth * activeScale,
@@ -984,6 +1021,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
</div> </div>
</div> </div>
); );
}; }
);
export default React.memo(CanvasImageEditor); export default React.memo(CanvasImageEditor);
+5 -2
View File
@@ -141,9 +141,12 @@ export default {
'playground.image.fitview': 'Fit View', 'playground.image.fitview': 'Fit View',
'playground.chat.aithought': 'CoT', 'playground.chat.aithought': 'CoT',
'playground.image.mask.uploaded': 'Mask Uploaded', 'playground.image.mask.uploaded': 'Mask Uploaded',
'playground.image.mask.upload': 'Upload Mask: takes priority in submission.', 'playground.image.mask.upload':
'Upload Mask: No additional drawing allowed after upload.',
'playground.params.frequency_penalty.tips': `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`, 'playground.params.frequency_penalty.tips': `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`,
'playground.params.presence_penalty.tips': `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`, 'playground.params.presence_penalty.tips': `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`,
'playground.image.origin': 'Original', 'playground.image.origin': 'Original',
'playground.image.mask': 'Mask' 'playground.image.mask': 'Mask',
'playground.image.negativeMask.tips':
'After selection, no further masking can be drawn; therefore, you should draw the mask first and then check the option.'
}; };
+5 -2
View File
@@ -138,9 +138,12 @@ export default {
'playground.image.fitview': 'Подогнать размер', 'playground.image.fitview': 'Подогнать размер',
'playground.chat.aithought': 'Рассуждение (CoT)', 'playground.chat.aithought': 'Рассуждение (CoT)',
'playground.image.mask.uploaded': 'Маска загружена', 'playground.image.mask.uploaded': 'Маска загружена',
'playground.image.mask.upload':'Загрузить маску: имеет приоритет при отправке', 'playground.image.mask.upload':
'TODO: Translate key "playground.image.mask.upload"',
'playground.params.frequency_penalty.tips': `Число от -2.0 до 2.0. Положительные значения снижают вероятность повторения токенов, уже часто встречающихся в тексте, уменьшая склонность модели дословно повторять одни и те же фразы.`, 'playground.params.frequency_penalty.tips': `Число от -2.0 до 2.0. Положительные значения снижают вероятность повторения токенов, уже часто встречающихся в тексте, уменьшая склонность модели дословно повторять одни и те же фразы.`,
'playground.params.presence_penalty.tips': `Число от -2.0 до 2.0. Положительные значения снижают вероятность повторения любых токенов, присутствующих в тексте, повышая склонность модели к обсуждению новых тем.`, 'playground.params.presence_penalty.tips': `Число от -2.0 до 2.0. Положительные значения снижают вероятность повторения любых токенов, присутствующих в тексте, повышая склонность модели к обсуждению новых тем.`,
'playground.image.origin': 'Оригинал', 'playground.image.origin': 'Оригинал',
'playground.image.mask': 'Маска' 'playground.image.mask': 'Маска',
'playground.image.negativeMask.tips':
'TODO: Translate key "playground.image.negativeMask.tips"'
}; };
+4 -2
View File
@@ -136,9 +136,11 @@ export default {
'playground.image.fitview': '适应视图', 'playground.image.fitview': '适应视图',
'playground.chat.aithought': '思考过程', 'playground.chat.aithought': '思考过程',
'playground.image.mask.uploaded': '遮罩已上传', 'playground.image.mask.uploaded': '遮罩已上传',
'playground.image.mask.upload': '上传遮罩:在提交时优先使用', 'playground.image.mask.upload': '上传遮罩:上传后将不可再绘制',
'playground.params.frequency_penalty.tips': `数值介于 -2.0 和 2.0 之间。正值会根据新词在文本中已出现的频率对其进行惩罚,从而降低模型逐字重复相同内容的可能性。`, 'playground.params.frequency_penalty.tips': `数值介于 -2.0 和 2.0 之间。正值会根据新词在文本中已出现的频率对其进行惩罚,从而降低模型逐字重复相同内容的可能性。`,
'playground.params.presence_penalty.tips': `数值介于 -2.0 和 2.0 之间。正值会根据新词是否已在文本中出现过对其进行惩罚,从而增加模型谈论新话题的可能性。`, 'playground.params.presence_penalty.tips': `数值介于 -2.0 和 2.0 之间。正值会根据新词是否已在文本中出现过对其进行惩罚,从而增加模型谈论新话题的可能性。`,
'playground.image.origin': '原图', 'playground.image.origin': '原图',
'playground.image.mask': '遮罩' 'playground.image.mask': '遮罩',
'playground.image.negativeMask.tips':
'选择后,将不可再绘制遮罩;因此,你应该先绘制遮罩然后再勾选'
}; };
@@ -64,6 +64,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
}); });
const doneImage = useRef<boolean>(false); const doneImage = useRef<boolean>(false);
const [activeImgUid, setActiveImgUid] = useState<number>(0); const [activeImgUid, setActiveImgUid] = useState<number>(0);
const imageEditorRef = useRef<any>(null);
const { const {
handleOnValuesChange, handleOnValuesChange,
@@ -110,9 +111,8 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
}); });
const imageFile = useMemo(() => { const imageFile = useMemo(() => {
if (!image) return null; return base64ToFile(uploadList[0]?.dataUrl, 'image');
return base64ToFile(image, 'image'); }, [uploadList]);
}, [image]);
const maskFile = useMemo(() => { const maskFile = useMemo(() => {
if (!mask) return null; if (!mask) return null;
@@ -123,7 +123,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
if (parameters.size === 'custom') { if (parameters.size === 'custom') {
return { return {
..._.omit(parameters, ['width', 'height', 'preview', 'random_seed']), ..._.omit(parameters, ['width', 'height', 'preview', 'random_seed']),
image: imageFile, image: base64ToFile(image, 'image'),
mask: maskFile, mask: maskFile,
size: size:
parameters.width && parameters.height parameters.width && parameters.height
@@ -279,6 +279,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
const handleClearUploadMask = useCallback(() => { const handleClearUploadMask = useCallback(() => {
setMaskUpload([]); setMaskUpload([]);
setMask(null); setMask(null);
imageEditorRef.current?.clearMask();
}, []); }, []);
const handleOnSave = useCallback( const handleOnSave = useCallback(
@@ -299,6 +300,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
if (image) { if (image) {
return ( return (
<CanvasImageEditor <CanvasImageEditor
ref={imageEditorRef}
imguid={activeImgUid} imguid={activeImgUid}
imageStatus={imageStatus} imageStatus={imageStatus}
imageSrc={image} imageSrc={image}
@@ -361,7 +363,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
]); ]);
const handleOnImgClick = useCallback((item: any, isOrigin: boolean) => { const handleOnImgClick = useCallback((item: any, isOrigin: boolean) => {
if (item.progress < 100) { if (item.progress < 100 && !isOrigin) {
return; return;
} }
setActiveImgUid(item.uid); setActiveImgUid(item.uid);
@@ -421,8 +423,9 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
preview={false} preview={false}
loading={false} loading={false}
autoSize={false} autoSize={false}
editable={false} editable={true}
autoBgColor={false} autoBgColor={false}
onDelete={() => handleClearUploadMask()}
label={ label={
<span>{intl.formatMessage({ id: 'playground.image.mask' })}</span> <span>{intl.formatMessage({ id: 'playground.image.mask' })}</span>
} }
@@ -187,6 +187,7 @@ export default function useTextImage(props: any) {
const handleStopConversation = () => { const handleStopConversation = () => {
requestToken.current?.abort?.(); requestToken.current?.abort?.();
setImageList([]);
setLoading(false); setLoading(false);
}; };
@@ -118,7 +118,12 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
}} }}
></div> ></div>
)} )}
<HighlightCode theme="dark" code={code} lang="bash"></HighlightCode> <HighlightCode
theme="dark"
code={code}
lang="bash"
height={130}
></HighlightCode>
<h3 className="m-b-0 m-t-10"> <h3 className="m-b-0 m-t-10">
3. {intl.formatMessage({ id: 'resources.worker.add.step3' })} 3. {intl.formatMessage({ id: 'resources.worker.add.step3' })}
</h3> </h3>
+26 -1
View File
@@ -44,6 +44,7 @@ export const addWorkerGuide: Record<string, any> = {
return `docker run -d --name gpustack-worker --restart=unless-stopped --gpus all -p 10150:10150 -p 40000-41024:40000-41024 -p 50000-51024:50000-51024 --ipc=host -v gpustack-worker-data:/var/lib/gpustack gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; return `docker run -d --name gpustack-worker --restart=unless-stopped --gpus all -p 10150:10150 -p 40000-41024:40000-41024 -p 50000-51024:50000-51024 --ipc=host -v gpustack-worker-data:/var/lib/gpustack gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
npu: { npu: {
getToken: getToken:
'Get-Content -Path (Join-Path -Path $env:APPDATA -ChildPath "gpustack\\token") -Raw', 'Get-Content -Path (Join-Path -Path $env:APPDATA -ChildPath "gpustack\\token") -Raw',
@@ -53,7 +54,31 @@ export const addWorkerGuide: Record<string, any> = {
token: string; token: string;
workerip: string; workerip: string;
}) { }) {
return `docker run -d --name gpustack-worker --restart=unless-stopped -e ASCEND_VISIBLE_DEVICES=0 -p 10150:10150 -p 40000-41024:40000-41024 -p 50000-51024:50000-51024 --ipc=host -v gpustack-worker-data:/var/lib/gpustack gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; return `docker run -d --name gpustack-worker \\
--restart=unless-stopped \\
--device /dev/davinci0 \\
--device /dev/davinci1 \\
--device /dev/davinci2 \\
--device /dev/davinci3 \\
--device /dev/davinci4 \\
--device /dev/davinci5 \\
--device /dev/davinci6 \\
--device /dev/davinci7 \\
--device /dev/davinci_manager \\
--device /dev/devmm_svm \\
--device /dev/hisi_hdc \\
-v /usr/local/dcmi:/usr/local/dcmi \\
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \\
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \\
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \\
-v /etc/ascend_install.info:/etc/ascend_install.info \\
-p 10150:10150 \\
-p 40000-41024:40000-41024 \\
-p 50000-51024:50000-51024 \\
--ipc=host \\
-v gpustack-worker-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag} \\
--server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
musa: { musa: {