fix: images did not get cleared after clicking stop

This commit is contained in:
jialin
2025-07-04 15:42:03 +08:00
parent 28994cb9da
commit 6943571a6d
11 changed files with 172 additions and 25 deletions
@@ -107,7 +107,6 @@ export default function useZoom(props: {
if (isLoadingMaskRef.current) { if (isLoadingMaskRef.current) {
return; return;
} }
event.preventDefault();
// stop // stop
handleZoom(event); handleZoom(event);
updateCursorSize(); updateCursorSize();
+3 -1
View File
@@ -152,5 +152,7 @@ export default {
'1. After selection, no further masking can be drawn; therefore, you should draw the mask first and then check the option.\n 2. Once a mask image is uploaded, no further masks can be generated.', '1. After selection, no further masking can be drawn; therefore, you should draw the mask first and then check the option.\n 2. Once a mask image is uploaded, no further masks can be generated.',
'playground.model.noavailable.tips2': 'playground.model.noavailable.tips2':
'If the expected model isnt showing up, make sure its running and correctly categorized. If the category is incorrect, you can manually adjust it in the models settings.', 'If the expected model isnt showing up, make sure its running and correctly categorized. If the category is incorrect, you can manually adjust it in the models settings.',
'playground.rerank.query.validate': 'The query is required.' 'playground.rerank.query.validate': 'The query is required.',
'playground.image.generate.error':
'Something went wrong. The image could not be generated.'
}; };
+4 -1
View File
@@ -155,9 +155,12 @@ export default {
'1. 選択後は追加のマスクを描画できません。そのため、最初にマスクを描画してからオプションを選択してください。\n 2. マスク画像をアップロードすると、追加のマスクを生成することはできません。', '1. 選択後は追加のマスクを描画できません。そのため、最初にマスクを描画してからオプションを選択してください。\n 2. マスク画像をアップロードすると、追加のマスクを生成することはできません。',
'playground.model.noavailable.tips2': 'playground.model.noavailable.tips2':
'期待するモデルが表示されない場合は、モデルが実行中で正しく分類されていることを確認してください。カテゴリが間違っている場合は、モデルの設定で手動で調整できます。', '期待するモデルが表示されない場合は、モデルが実行中で正しく分類されていることを確認してください。カテゴリが間違っている場合は、モデルの設定で手動で調整できます。',
'playground.rerank.query.validate': 'The query is required.' 'playground.rerank.query.validate': 'The query is required.',
'playground.image.generate.error':
'Something went wrong. The image could not be generated.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.rerank.query.validate': 'The query is required.' // 1. 'playground.rerank.query.validate': 'The query is required.'
// 2. 'playground.image.generate.error': 'Something went wrong. The image could not be generated.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+4 -1
View File
@@ -149,9 +149,12 @@ export default {
'1. После выделения области создание маски станет недоступным; поэтому сначала нарисуйте маску, а затем активируйте опцию. \n 2. После загрузки изображения маски создание новых масок невозможно.', '1. После выделения области создание маски станет недоступным; поэтому сначала нарисуйте маску, а затем активируйте опцию. \n 2. После загрузки изображения маски создание новых масок невозможно.',
'playground.model.noavailable.tips2': 'playground.model.noavailable.tips2':
'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.', 'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.',
'playground.rerank.query.validate': 'The query is required.' 'playground.rerank.query.validate': 'The query is required.',
'playground.image.generate.error':
'Something went wrong. The image could not be generated.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.rerank.query.validate': 'The query is required.' // 1. 'playground.rerank.query.validate': 'The query is required.'
// 2. 'playground.image.generate.error': 'Something went wrong. The image could not be generated.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+2 -1
View File
@@ -146,5 +146,6 @@ export default {
'1. 选择后,将不可再绘制遮罩;因此,你应该先绘制遮罩然后再勾选.\n 2. 当上传了遮罩图片,将不可再生成遮罩', '1. 选择后,将不可再绘制遮罩;因此,你应该先绘制遮罩然后再勾选.\n 2. 当上传了遮罩图片,将不可再生成遮罩',
'playground.model.noavailable.tips2': 'playground.model.noavailable.tips2':
'若预期的模型未显示,请检查模型是否已正常运行并被正确分类。如分类不正确,请编辑模型并手动调整其类别。', '若预期的模型未显示,请检查模型是否已正常运行并被正确分类。如分类不正确,请编辑模型并手动调整其类别。',
'playground.rerank.query.validate': '查询内容不能为空' 'playground.rerank.query.validate': '查询内容不能为空',
'playground.image.generate.error': '出了一点问题,图片未能生成。'
}; };
@@ -343,7 +343,7 @@ const AddModal: FC<AddModalProps> = (props) => {
} }
if (manual) { if (manual) {
form.current?.form?.resetFields(resetFieldsByModel); form.current?.form?.setFieldsValue(defaultFormValues);
} }
// If the item is empty // If the item is empty
setIsGGUF(item.isGGUF); setIsGGUF(item.isGGUF);
+40 -9
View File
@@ -5,6 +5,7 @@ import {
fetchChunkedDataPostFormData, fetchChunkedDataPostFormData,
readLargeStreamData as readStreamData readLargeStreamData as readStreamData
} from '@/utils/fetch-chunk-data'; } from '@/utils/fetch-chunk-data';
import { useIntl } from '@umijs/max';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { CREAT_IMAGE_API, EDIT_IMAGE_API } from '../apis'; import { CREAT_IMAGE_API, EDIT_IMAGE_API } from '../apis';
@@ -12,6 +13,7 @@ import { CREAT_IMAGE_API, EDIT_IMAGE_API } from '../apis';
const ODD_STRING = 'AAAABJRU5ErkJgg==='; const ODD_STRING = 'AAAABJRU5ErkJgg===';
export default function useTextImage(props: any) { export default function useTextImage(props: any) {
const intl = useIntl();
const { scroller, paramsRef, chunkFields, API } = props; const { scroller, paramsRef, chunkFields, API } = props;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [tokenResult, setTokenResult] = useState<any>(null); const [tokenResult, setTokenResult] = useState<any>(null);
@@ -34,6 +36,8 @@ export default function useTextImage(props: any) {
const requestToken = useRef<any>(null); const requestToken = useRef<any>(null);
const { initialize } = useOverlayScroller(); const { initialize } = useOverlayScroller();
const { initialize: innitializeParams } = useOverlayScroller(); const { initialize: innitializeParams } = useOverlayScroller();
const streamReaderRef = useRef<any>(null);
const requestIdRef = useRef<number>(0);
useEffect(() => { useEffect(() => {
if (scroller.current) { if (scroller.current) {
@@ -46,6 +50,11 @@ export default function useTextImage(props: any) {
} }
}, [innitializeParams]); }, [innitializeParams]);
const updateRequestId = () => {
requestIdRef.current = requestIdRef.current + 1;
return requestIdRef.current;
};
const removeBase64Suffix = (str: string, suffix: string) => { const removeBase64Suffix = (str: string, suffix: string) => {
return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str; return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
}; };
@@ -78,9 +87,18 @@ export default function useTextImage(props: any) {
return Math.floor(Math.random() * (max - min + 1) + min); return Math.floor(Math.random() * (max - min + 1) + min);
}; };
const stopDebounce = _.debounce(() => {
setImageList([]);
setLoading(false);
}, 200);
const submitMessage = async (parameters: any) => { const submitMessage = async (parameters: any) => {
try { try {
if (!parameters.model) return; if (!parameters.model) return;
requestToken.current?.abort?.('cancel');
requestToken.current = new AbortController();
const currentRequestId = updateRequestId();
const size: any = setImageSize(parameters); const size: any = setImageSize(parameters);
setLoading(true); setLoading(true);
setMessageId(); setMessageId();
@@ -107,9 +125,6 @@ export default function useTextImage(props: any) {
}); });
setImageList(newImageList); setImageList(newImageList);
requestToken.current?.abort?.('cancel');
requestToken.current = new AbortController();
let result: any = {}; let result: any = {};
if (API === CREAT_IMAGE_API) { if (API === CREAT_IMAGE_API) {
result = await fetchChunkedData({ result = await fetchChunkedData({
@@ -135,10 +150,15 @@ export default function useTextImage(props: any) {
setImageList([]); setImageList([]);
return; return;
} }
console.log('result:', result);
const { reader, decoder } = result; const { reader, decoder } = result;
streamReaderRef.current = reader;
await readStreamData(reader, decoder, (chunk: any) => { await readStreamData(reader, decoder, (chunk: any, done?: boolean) => {
console.log('chunk done:', chunk, done);
if (requestIdRef.current !== currentRequestId) {
// If the request ID has changed, ignore this chunk
return;
}
if (chunk?.error) { if (chunk?.error) {
setTokenResult({ setTokenResult({
error: true, error: true,
@@ -168,12 +188,25 @@ export default function useTextImage(props: any) {
progress: progress progress: progress
}; };
}); });
if (
done &&
!chunk?.error &&
newImageList.some((item) => item.progress < 100)
) {
setTokenResult({
error: true,
errorMessage: intl.formatMessage({
id: 'playground.image.generate.error'
})
});
}
setImageList([...newImageList]); setImageList([...newImageList]);
}); });
} catch (error) { } catch (error) {
console.log('error:', error); console.log('error:', error);
updateRequestId();
stopDebounce();
requestToken.current?.abort?.('cancel'); requestToken.current?.abort?.('cancel');
setImageList([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -187,8 +220,6 @@ export default function useTextImage(props: any) {
const handleStopConversation = () => { const handleStopConversation = () => {
requestToken.current?.abort?.('stop'); requestToken.current?.abort?.('stop');
setImageList([]);
setLoading(false);
}; };
useEffect(() => { useEffect(() => {
@@ -0,0 +1,69 @@
import { useIntl } from '@umijs/max';
import { Space, Typography } from 'antd';
import React, { useMemo } from 'react';
interface AddWorkerMacOSProps {
token?: string;
}
const registerWorkerSteps = [
'Open the quick configuration file.',
`Select the "General" option.`,
`Select "Worker" as the service role.`,
'Enter the Server URL.',
'Paste the Token.',
`Click "Restart".`
];
const AddWorkerMacOS: React.FC<AddWorkerMacOSProps> = () => {
const intl = useIntl();
const labels = useMemo(
() => ({
step1: intl.formatMessage({ id: 'resources.worker.add.step1' }),
step2: intl.formatMessage({ id: 'resources.worker.add.step2' }),
step2Tips: intl.formatMessage({ id: 'resources.worker.add.step2.tips' }),
step3: intl.formatMessage({ id: 'resources.worker.add.step3' }),
linuxOrMac: intl.formatMessage({ id: 'resources.worker.linuxormaxos' })
}),
[intl]
);
return (
<div className="script-install">
<h3 className="font-size-14 font-600">1. Install GPUStack</h3>
<Typography.Paragraph type="secondary" style={{ paddingLeft: 14 }}>
<Typography.Link>Download Package</Typography.Link> and install it on
your macOS machine.
</Typography.Paragraph>
<h3 className="font-size-14 font-600">
2. <span dangerouslySetInnerHTML={{ __html: labels.step1 }}></span>
</h3>
<Typography.Paragraph style={{ paddingLeft: 14 }}>
<Typography.Text type="secondary">
Click <span className="font-600">Copy the token</span>
</Typography.Text>
</Typography.Paragraph>
<h3 className="m-t-10 font-size-14 font-600">
3. {labels.step2}{' '}
<span
className="font-size-12"
style={{ color: 'var(--ant-color-text-tertiary)' }}
dangerouslySetInnerHTML={{
__html: `${labels.step2Tips}`
}}
></span>
</h3>
<Typography.Paragraph style={{ paddingLeft: 14 }}>
<Space direction="vertical">
{registerWorkerSteps.map((step, index) => (
<Typography.Text key={index} type="secondary">
{index + 1}. {step}
</Typography.Text>
))}
</Space>
</Typography.Paragraph>
<h3 className="m-b-0 m-t-10 font-size-14 font-600">4. {labels.step3}</h3>
</div>
);
};
export default AddWorkerMacOS;
@@ -0,0 +1,11 @@
import React from 'react';
interface AddWorkerWindowsProps {
token?: string;
}
const AddWorkerWindows: React.FC<AddWorkerWindowsProps> = () => {
return <div>add worker in windows</div>;
};
export default AddWorkerWindows;
+14 -1
View File
@@ -23,9 +23,22 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
}, },
{ {
key: 'container', key: 'container',
label: intl.formatMessage({ id: 'resources.worker.container.install' }), label: 'Linux',
// icon: <LinuxOutlined />,
children: <ContainerInstall token={token} /> children: <ContainerInstall token={token} />
} }
// {
// key: 'macos',
// label: 'macOS',
// icon: <AppleOutlined />,
// children: <MacOS token={token} />
// },
// {
// key: 'windows',
// label: 'Windows',
// icon: <WindowsOutlined />,
// children: <Windows token={token} />
// }
]; ];
return ( return (
+24 -9
View File
@@ -193,7 +193,11 @@ export const readStreamData = async (
}; };
// Process the remainder of the buffer // Process the remainder of the buffer
const processBuffer = async (buffer: string, callback: (data: any) => void) => { const processBuffer = async (
buffer: string,
callback: (data: any, done?: boolean) => void,
done?: boolean
) => {
if (!buffer) return; if (!buffer) return;
const lines = buffer.split('\n'); const lines = buffer.split('\n');
@@ -205,7 +209,7 @@ const processBuffer = async (buffer: string, callback: (data: any) => void) => {
try { try {
if (jsonStr !== '[DONE]') { if (jsonStr !== '[DONE]') {
const jsonData = JSON.parse(jsonStr); const jsonData = JSON.parse(jsonStr);
callback(jsonData); callback(jsonData, done);
} }
} catch (e) { } catch (e) {
console.error('Failed to parse JSON from line:', jsonStr, e); console.error('Failed to parse JSON from line:', jsonStr, e);
@@ -214,7 +218,7 @@ const processBuffer = async (buffer: string, callback: (data: any) => void) => {
const errorStr = trimmedLine.slice(7).trim(); const errorStr = trimmedLine.slice(7).trim();
try { try {
const jsonData = JSON.parse(errorStr); const jsonData = JSON.parse(errorStr);
callback({ error: jsonData }); callback({ error: jsonData, done });
} catch (e) { } catch (e) {
console.error('Failed to parse error JSON from line:', errorStr, e); console.error('Failed to parse error JSON from line:', errorStr, e);
} }
@@ -225,7 +229,7 @@ const processBuffer = async (buffer: string, callback: (data: any) => void) => {
export const readLargeStreamData = async ( export const readLargeStreamData = async (
reader: any, reader: any,
decoder: TextDecoder, decoder: TextDecoder,
callback: (data: any) => void, callback: (data: any, done?: boolean) => void,
throttleDelay = 200 throttleDelay = 200
) => { ) => {
let buffer = ''; // cache incomplete line let buffer = ''; // cache incomplete line
@@ -234,9 +238,9 @@ export const readLargeStreamData = async (
private buffer: any[] = []; private buffer: any[] = [];
private failed: boolean = false; private failed: boolean = false;
private isFlushing: boolean = false; private isFlushing: boolean = false;
private callback: (data: any) => void; private callback: (data: any, done?: boolean) => void;
constructor(callback: (data: any) => void) { constructor(callback: (data: any, done?: boolean) => void) {
this.callback = callback; this.callback = callback;
} }
@@ -244,7 +248,12 @@ export const readLargeStreamData = async (
this.buffer.push(data); this.buffer.push(data);
} }
public async flush() { public async flush(done?: boolean) {
if (this.buffer.length === 0 && done) {
// If buffer is empty and done is true, we can call the callback with an empty object
this.callback({}, done);
return;
}
if (this.buffer.length === 0 || this.isFlushing) { if (this.buffer.length === 0 || this.isFlushing) {
return; return;
} }
@@ -255,7 +264,7 @@ export const readLargeStreamData = async (
const data = this.buffer.shift(); const data = this.buffer.shift();
try { try {
processBuffer(data, this.callback); processBuffer(data, this.callback, done);
} catch (error) { } catch (error) {
console.error('Error processing buffer:', error); console.error('Error processing buffer:', error);
this.failed = true; this.failed = true;
@@ -263,6 +272,10 @@ export const readLargeStreamData = async (
break; break;
} }
} }
if (done) {
// If done is true, we should call the callback with an empty object
this.callback({}, done);
}
this.isFlushing = false; this.isFlushing = false;
} }
@@ -282,13 +295,15 @@ export const readLargeStreamData = async (
while (true) { while (true) {
const { done, value } = await reader?.read?.(); const { done, value } = await reader?.read?.();
console.log('done:', done, 'value:', value);
if (done) { if (done) {
isReading = false; isReading = false;
// Process remaining buffered data // Process remaining buffered data
if (buffer) { if (buffer) {
bufferManager.add(buffer); bufferManager.add(buffer);
} }
bufferManager.flush(); bufferManager.flush(done);
break; break;
} }