show canvas reference upload progress
This commit is contained in:
parent
66cdf6d3d1
commit
8acf1f60ec
@ -58,7 +58,10 @@ import { toast } from "sonner";
|
||||
import { useCanvasStore } from "@/store/canvas";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import {
|
||||
fetchUploadWithRetry,
|
||||
type UploadProgress,
|
||||
} from "@/lib/client/upload-with-retry";
|
||||
import {
|
||||
downloadCanvasImageAsPng,
|
||||
type CanvasImageDownloadProgress,
|
||||
@ -1186,8 +1189,78 @@ type CanvasComposerUploadProgress = {
|
||||
current: number;
|
||||
total: number;
|
||||
fileName?: string;
|
||||
filePercent?: number;
|
||||
percent?: number;
|
||||
phase?: UploadProgress["phase"];
|
||||
target?: "composer-reference" | "canvas" | "slot";
|
||||
};
|
||||
|
||||
type CanvasUploadProgressOptions = {
|
||||
title: string;
|
||||
fileName?: string;
|
||||
fileIndex?: number;
|
||||
fileTotal?: number;
|
||||
target?: CanvasComposerUploadProgress["target"];
|
||||
};
|
||||
|
||||
function getCanvasUploadProgressPhaseLabel(phase?: UploadProgress["phase"]) {
|
||||
switch (phase) {
|
||||
case "preparing":
|
||||
return "正在准备上传";
|
||||
case "direct":
|
||||
return "正在上传到云储存";
|
||||
case "multipart":
|
||||
return "正在分片上传到云储存";
|
||||
case "server":
|
||||
return "正在通过服务器上传";
|
||||
case "finalizing":
|
||||
return "正在确认图片地址";
|
||||
case "complete":
|
||||
return "上传完成";
|
||||
default:
|
||||
return "正在上传图片";
|
||||
}
|
||||
}
|
||||
|
||||
function buildCanvasComposerUploadProgress(
|
||||
options: CanvasUploadProgressOptions,
|
||||
progress?: UploadProgress
|
||||
): CanvasComposerUploadProgress {
|
||||
const total = Math.max(1, Math.round(options.fileTotal || 1));
|
||||
const fileIndex = Math.min(
|
||||
total - 1,
|
||||
Math.max(0, Math.round(options.fileIndex || 0))
|
||||
);
|
||||
const current = fileIndex + 1;
|
||||
const filePercent = Math.max(
|
||||
1,
|
||||
Math.min(100, Math.round(progress?.percent || 1))
|
||||
);
|
||||
const percent =
|
||||
total <= 1
|
||||
? filePercent
|
||||
: Math.max(
|
||||
1,
|
||||
Math.min(100, Math.round(((fileIndex + filePercent / 100) / total) * 100))
|
||||
);
|
||||
const phaseLabel = getCanvasUploadProgressPhaseLabel(progress?.phase);
|
||||
|
||||
return {
|
||||
title: options.title,
|
||||
message:
|
||||
total > 1
|
||||
? `${phaseLabel},第 ${current}/${total} 张,当前 ${filePercent}%`
|
||||
: `${phaseLabel},当前 ${filePercent}%`,
|
||||
current,
|
||||
total,
|
||||
fileName: options.fileName,
|
||||
filePercent,
|
||||
percent,
|
||||
phase: progress?.phase,
|
||||
target: options.target,
|
||||
};
|
||||
}
|
||||
|
||||
const OPENCLAW_ACTIONS_CODE_BLOCK_REGEX =
|
||||
/```openclaw-actions\s*([\s\S]*?)```/gi;
|
||||
|
||||
@ -1632,13 +1705,17 @@ function CanvasComposerUploadProgressDialog({
|
||||
if (!progress) return null;
|
||||
|
||||
const percent =
|
||||
progress.total > 1
|
||||
typeof progress.percent === "number"
|
||||
? Math.max(1, Math.min(100, Math.round(progress.percent)))
|
||||
: progress.total > 1
|
||||
? Math.max(
|
||||
8,
|
||||
Math.min(100, Math.round((progress.current / progress.total) * 100))
|
||||
)
|
||||
: 100;
|
||||
const isSingleFile = progress.total <= 1;
|
||||
const isIndeterminate =
|
||||
typeof progress.percent !== "number" && progress.total <= 1;
|
||||
const phaseLabel = getCanvasUploadProgressPhaseLabel(progress.phase);
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[1000] flex items-center justify-center bg-black/35 px-4 backdrop-blur-[2px]">
|
||||
@ -1663,12 +1740,19 @@ function CanvasComposerUploadProgressDialog({
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full bg-[#e0ff3a] transition-all duration-300",
|
||||
isSingleFile && "animate-pulse"
|
||||
isIndeterminate && "animate-pulse"
|
||||
)}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between gap-3 text-[11px] leading-4 text-zinc-500">
|
||||
<span className="min-w-0 truncate">
|
||||
{progress.total > 1 ? `${progress.current}/${progress.total}` : phaseLabel}
|
||||
</span>
|
||||
<span className="shrink-0 tabular-nums">{percent}%</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-[12px] leading-5 text-zinc-400">
|
||||
{progress.message}
|
||||
</div>
|
||||
@ -2287,6 +2371,35 @@ export function CanvasChatPanel({
|
||||
useState<CanvasImageDownloadProgress | null>(null);
|
||||
const [composerUploadProgress, setComposerUploadProgress] =
|
||||
useState<CanvasComposerUploadProgress | null>(null);
|
||||
const composerUploadClearTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const clearComposerUploadProgress = useCallback((delayMs = 0) => {
|
||||
if (composerUploadClearTimeoutRef.current) {
|
||||
clearTimeout(composerUploadClearTimeoutRef.current);
|
||||
composerUploadClearTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (delayMs <= 0) {
|
||||
setComposerUploadProgress(null);
|
||||
return;
|
||||
}
|
||||
|
||||
composerUploadClearTimeoutRef.current = setTimeout(() => {
|
||||
composerUploadClearTimeoutRef.current = null;
|
||||
setComposerUploadProgress(null);
|
||||
}, delayMs);
|
||||
}, []);
|
||||
const updateComposerUploadProgress = useCallback(
|
||||
(options: CanvasUploadProgressOptions, progress?: UploadProgress) => {
|
||||
if (composerUploadClearTimeoutRef.current) {
|
||||
clearTimeout(composerUploadClearTimeoutRef.current);
|
||||
composerUploadClearTimeoutRef.current = null;
|
||||
}
|
||||
setComposerUploadProgress(
|
||||
buildCanvasComposerUploadProgress(options, progress)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const [downloadingOriginalUrl, setDownloadingOriginalUrl] = useState("");
|
||||
const [addingClawdImageUrl, setAddingClawdImageUrl] = useState("");
|
||||
const downloadAbortControllerRef = useRef<AbortController | null>(null);
|
||||
@ -2300,6 +2413,14 @@ export function CanvasChatPanel({
|
||||
format: "webp",
|
||||
});
|
||||
}, [fullscreenPreview]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (composerUploadClearTimeoutRef.current) {
|
||||
clearTimeout(composerUploadClearTimeoutRef.current);
|
||||
composerUploadClearTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 🆕 图片参数状态
|
||||
const [imageParams, setImageParams] = useState<ImageGenerationParams>({
|
||||
@ -4852,12 +4973,12 @@ export function CanvasChatPanel({
|
||||
setAttachments([...attachments, ...toAdd]);
|
||||
return toAdd.length;
|
||||
}, [
|
||||
agentUploadSlots.length,
|
||||
agentUploadSlots,
|
||||
attachments,
|
||||
hasAgentUploadSlots,
|
||||
insertImageTagInEditor,
|
||||
isVideoModel,
|
||||
videoFrameSlots.length,
|
||||
videoFrameSlots,
|
||||
]);
|
||||
|
||||
// 监听 pendingChatAttachments:视频模式时填充首尾帧槽位,非视频模式插入对话标签
|
||||
@ -8343,12 +8464,22 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
setVideoSlotUploading(slotIndex);
|
||||
const progressOptions: CanvasUploadProgressOptions = {
|
||||
title: `正在上传${videoFrameSlots[slotIndex] || "参考图"}`,
|
||||
fileName: file.name,
|
||||
target: "slot",
|
||||
};
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("purpose", "canvas-upload");
|
||||
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
updateComposerUploadProgress(progressOptions);
|
||||
const response = await fetchUploadWithRetry(formData, {
|
||||
onProgress: (progress) => {
|
||||
updateComposerUploadProgress(progressOptions, progress);
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败");
|
||||
@ -8388,11 +8519,12 @@ export function CanvasChatPanel({
|
||||
} catch (_error) {
|
||||
toast.error("图片上传失败");
|
||||
} finally {
|
||||
clearComposerUploadProgress(450);
|
||||
setVideoSlotUploading(null);
|
||||
event.currentTarget.value = "";
|
||||
}
|
||||
},
|
||||
[addNode, nodes.length, updateAttachmentAtVideoSlot]
|
||||
[addNode, clearComposerUploadProgress, nodes.length, updateAttachmentAtVideoSlot, updateComposerUploadProgress, videoFrameSlots]
|
||||
);
|
||||
|
||||
const handleAgentSlotFileChange = useCallback(
|
||||
@ -8408,12 +8540,23 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
setAgentSlotUploading(slotIndex);
|
||||
const slotLabel = agentUploadSlots[slotIndex]?.label || `第${slotIndex + 1}张`;
|
||||
const progressOptions: CanvasUploadProgressOptions = {
|
||||
title: `正在上传${slotLabel}`,
|
||||
fileName: file.name,
|
||||
target: "slot",
|
||||
};
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("purpose", "canvas-upload");
|
||||
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
updateComposerUploadProgress(progressOptions);
|
||||
const response = await fetchUploadWithRetry(formData, {
|
||||
onProgress: (progress) => {
|
||||
updateComposerUploadProgress(progressOptions, progress);
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败");
|
||||
@ -8450,16 +8593,16 @@ export function CanvasChatPanel({
|
||||
connections: [],
|
||||
});
|
||||
|
||||
const slotLabel = agentUploadSlots[slotIndex]?.label || `第${slotIndex + 1}张`;
|
||||
toast.success(`${slotLabel}上传成功`);
|
||||
} catch (_error) {
|
||||
toast.error("图片上传失败");
|
||||
} finally {
|
||||
clearComposerUploadProgress(450);
|
||||
setAgentSlotUploading(null);
|
||||
event.currentTarget.value = "";
|
||||
}
|
||||
},
|
||||
[addNode, agentUploadSlots, nodes.length, updateAttachmentAtAgentSlot]
|
||||
[addNode, agentUploadSlots, clearComposerUploadProgress, nodes.length, updateAttachmentAtAgentSlot, updateComposerUploadProgress]
|
||||
);
|
||||
|
||||
const updateDetailSlotAttachment = useCallback(
|
||||
@ -8559,12 +8702,22 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
setDetailSlotUploading(slotIndex);
|
||||
const progressOptions: CanvasUploadProgressOptions = {
|
||||
title: `正在上传${DETAIL_SLOT_CONFIGS[slotIndex]?.label || "图片"}`,
|
||||
fileName: file.name,
|
||||
target: "slot",
|
||||
};
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("purpose", "canvas-upload");
|
||||
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
updateComposerUploadProgress(progressOptions);
|
||||
const response = await fetchUploadWithRetry(formData, {
|
||||
onProgress: (progress) => {
|
||||
updateComposerUploadProgress(progressOptions, progress);
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败");
|
||||
@ -8585,11 +8738,12 @@ export function CanvasChatPanel({
|
||||
} catch (_error) {
|
||||
toast.error("图片上传失败");
|
||||
} finally {
|
||||
clearComposerUploadProgress(450);
|
||||
setDetailSlotUploading(null);
|
||||
event.currentTarget.value = "";
|
||||
}
|
||||
},
|
||||
[updateDetailSlotAttachment]
|
||||
[clearComposerUploadProgress, updateComposerUploadProgress, updateDetailSlotAttachment]
|
||||
);
|
||||
|
||||
const updateLocalDetailArchive = useCallback(
|
||||
@ -11313,7 +11467,8 @@ export function CanvasChatPanel({
|
||||
|
||||
const uploadImageFileAndAddToCanvas = useCallback(async (
|
||||
file: File,
|
||||
nodeIndex: number
|
||||
nodeIndex: number,
|
||||
progressOptions?: CanvasUploadProgressOptions
|
||||
): Promise<{
|
||||
imageUrl: string;
|
||||
name: string;
|
||||
@ -11340,7 +11495,16 @@ export function CanvasChatPanel({
|
||||
formData.append('file', file);
|
||||
formData.append('purpose', 'canvas-upload');
|
||||
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
if (progressOptions) {
|
||||
updateComposerUploadProgress(progressOptions);
|
||||
}
|
||||
const response = await fetchUploadWithRetry(formData, progressOptions
|
||||
? {
|
||||
onProgress: (progress) => {
|
||||
updateComposerUploadProgress(progressOptions, progress);
|
||||
},
|
||||
}
|
||||
: undefined);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('上传失败');
|
||||
@ -11385,10 +11549,11 @@ export function CanvasChatPanel({
|
||||
toast.error('图片上传失败');
|
||||
return null;
|
||||
}
|
||||
}, [addNode]);
|
||||
}, [addNode, updateComposerUploadProgress]);
|
||||
|
||||
const uploadImageFileForComposerAttachment = useCallback(async (
|
||||
file: File
|
||||
file: File,
|
||||
progressOptions?: CanvasUploadProgressOptions
|
||||
): Promise<{
|
||||
imageUrl: string;
|
||||
name: string;
|
||||
@ -11416,7 +11581,16 @@ export function CanvasChatPanel({
|
||||
formData.append("file", file);
|
||||
formData.append("purpose", "canvas-upload");
|
||||
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
if (progressOptions) {
|
||||
updateComposerUploadProgress(progressOptions);
|
||||
}
|
||||
const response = await fetchUploadWithRetry(formData, progressOptions
|
||||
? {
|
||||
onProgress: (progress) => {
|
||||
updateComposerUploadProgress(progressOptions, progress);
|
||||
},
|
||||
}
|
||||
: undefined);
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败");
|
||||
}
|
||||
@ -11441,7 +11615,7 @@ export function CanvasChatPanel({
|
||||
toast.error("图片上传失败");
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
}, [updateComposerUploadProgress]);
|
||||
|
||||
const handlePastedImages = useCallback(async (files: File[]) => {
|
||||
if (files.length === 0) return;
|
||||
@ -11454,18 +11628,21 @@ export function CanvasChatPanel({
|
||||
let handledCount = 0;
|
||||
try {
|
||||
for (const [index, file] of files.entries()) {
|
||||
setComposerUploadProgress({
|
||||
const progressOptions: CanvasUploadProgressOptions = {
|
||||
title:
|
||||
files.length === 1
|
||||
? "正在上传图片"
|
||||
: `正在上传 ${files.length} 张图片`,
|
||||
message: `正在上传第 ${index + 1}/${files.length} 张,图片较大时可能需要一点时间。`,
|
||||
current: index + 1,
|
||||
total: files.length,
|
||||
fileName: file.name,
|
||||
});
|
||||
fileIndex: index,
|
||||
fileTotal: files.length,
|
||||
target: "canvas",
|
||||
};
|
||||
|
||||
const uploaded = await uploadImageFileForComposerAttachment(file);
|
||||
const uploaded = await uploadImageFileForComposerAttachment(
|
||||
file,
|
||||
progressOptions
|
||||
);
|
||||
if (!uploaded) continue;
|
||||
|
||||
handledCount += 1;
|
||||
@ -11490,7 +11667,7 @@ export function CanvasChatPanel({
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setComposerUploadProgress(null);
|
||||
clearComposerUploadProgress(450);
|
||||
}
|
||||
|
||||
if (handledCount > 0) {
|
||||
@ -11508,11 +11685,24 @@ export function CanvasChatPanel({
|
||||
const currentSlots = Array.from({ length: slotCount }, (_, index) => attachments[index] ?? null);
|
||||
let handledCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
for (const [index, file] of files.entries()) {
|
||||
const targetSlotIndex = currentSlots.findIndex((slot) => !slot);
|
||||
if (targetSlotIndex < 0) break;
|
||||
|
||||
const uploaded = await uploadImageFileAndAddToCanvas(file, baseNodeCount + uploadIndex);
|
||||
const slotLabel =
|
||||
agentUploadSlots[targetSlotIndex]?.label || `第${targetSlotIndex + 1}张`;
|
||||
const uploaded = await uploadImageFileAndAddToCanvas(
|
||||
file,
|
||||
baseNodeCount + uploadIndex,
|
||||
{
|
||||
title: `正在上传${slotLabel}`,
|
||||
fileName: file.name,
|
||||
fileIndex: index,
|
||||
fileTotal: files.length,
|
||||
target: "slot",
|
||||
}
|
||||
);
|
||||
if (!uploaded) continue;
|
||||
|
||||
const attachment: PickedObjectAttachment = {
|
||||
@ -11531,6 +11721,9 @@ export function CanvasChatPanel({
|
||||
firstImageUrl = uploaded.imageUrl;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clearComposerUploadProgress(450);
|
||||
}
|
||||
|
||||
if (handledCount > 0) {
|
||||
toast.success(
|
||||
@ -11551,11 +11744,22 @@ export function CanvasChatPanel({
|
||||
const currentSlots = Array.from({ length: slotCount }, (_, index) => attachments[index] ?? null);
|
||||
let handledCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
for (const [index, file] of files.entries()) {
|
||||
const targetSlotIndex = currentSlots.findIndex((slot) => !slot);
|
||||
if (targetSlotIndex < 0) break;
|
||||
|
||||
const uploaded = await uploadImageFileAndAddToCanvas(file, baseNodeCount + uploadIndex);
|
||||
const uploaded = await uploadImageFileAndAddToCanvas(
|
||||
file,
|
||||
baseNodeCount + uploadIndex,
|
||||
{
|
||||
title: `正在上传${videoFrameSlots[targetSlotIndex] || "参考图"}`,
|
||||
fileName: file.name,
|
||||
fileIndex: index,
|
||||
fileTotal: files.length,
|
||||
target: "slot",
|
||||
}
|
||||
);
|
||||
if (!uploaded) continue;
|
||||
|
||||
const attachment: PickedObjectAttachment = {
|
||||
@ -11573,6 +11777,9 @@ export function CanvasChatPanel({
|
||||
firstImageUrl = uploaded.imageUrl;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clearComposerUploadProgress(450);
|
||||
}
|
||||
|
||||
if (handledCount > 0) {
|
||||
toast.success(handledCount === 1 ? "已粘贴图片到视频槽位" : `已粘贴 ${handledCount} 张图片到视频槽位`);
|
||||
@ -11585,8 +11792,22 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
let handledCount = 0;
|
||||
for (const file of files) {
|
||||
const uploaded = await uploadImageFileAndAddToCanvas(file, baseNodeCount + uploadIndex);
|
||||
try {
|
||||
for (const [index, file] of files.entries()) {
|
||||
const uploaded = await uploadImageFileAndAddToCanvas(
|
||||
file,
|
||||
baseNodeCount + uploadIndex,
|
||||
{
|
||||
title:
|
||||
files.length === 1
|
||||
? "正在上传参考图"
|
||||
: `正在上传 ${files.length} 张参考图`,
|
||||
fileName: file.name,
|
||||
fileIndex: index,
|
||||
fileTotal: files.length,
|
||||
target: "composer-reference",
|
||||
}
|
||||
);
|
||||
if (!uploaded) continue;
|
||||
|
||||
handledCount += 1;
|
||||
@ -11615,6 +11836,9 @@ export function CanvasChatPanel({
|
||||
];
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
clearComposerUploadProgress(450);
|
||||
}
|
||||
|
||||
if (handledCount > 0) {
|
||||
toast.success(handledCount === 1 ? "图片已添加到画布" : `已粘贴并添加 ${handledCount} 张图片`);
|
||||
@ -11622,8 +11846,9 @@ export function CanvasChatPanel({
|
||||
|
||||
void maybeAutoSetCanvasTitle(firstImageUrl);
|
||||
}, [
|
||||
agentUploadSlots.length,
|
||||
agentUploadSlots,
|
||||
attachments,
|
||||
clearComposerUploadProgress,
|
||||
hasAgentUploadSlots,
|
||||
insertImageTagInEditor,
|
||||
isClawdMode,
|
||||
@ -11634,7 +11859,7 @@ export function CanvasChatPanel({
|
||||
updateAttachmentAtVideoSlot,
|
||||
uploadImageFileForComposerAttachment,
|
||||
uploadImageFileAndAddToCanvas,
|
||||
videoFrameSlots.length,
|
||||
videoFrameSlots,
|
||||
]);
|
||||
|
||||
const handleComposerPaste = useCallback((event: ReactClipboardEvent<HTMLDivElement>) => {
|
||||
@ -11716,18 +11941,21 @@ export function CanvasChatPanel({
|
||||
if (imageFiles.length > 0) {
|
||||
try {
|
||||
for (const [index, file] of imageFiles.entries()) {
|
||||
setComposerUploadProgress({
|
||||
const progressOptions: CanvasUploadProgressOptions = {
|
||||
title:
|
||||
imageFiles.length === 1
|
||||
? "正在上传图片"
|
||||
: `正在上传 ${imageFiles.length} 张图片`,
|
||||
message: `正在上传第 ${index + 1}/${imageFiles.length} 张,图片较大时可能需要一点时间。`,
|
||||
current: index + 1,
|
||||
total: imageFiles.length,
|
||||
fileName: file.name,
|
||||
});
|
||||
fileIndex: index,
|
||||
fileTotal: imageFiles.length,
|
||||
target: "canvas",
|
||||
};
|
||||
|
||||
const uploaded = await uploadImageFileForComposerAttachment(file);
|
||||
const uploaded = await uploadImageFileForComposerAttachment(
|
||||
file,
|
||||
progressOptions
|
||||
);
|
||||
if (!uploaded) continue;
|
||||
|
||||
handledImageCount += 1;
|
||||
@ -11752,7 +11980,7 @@ export function CanvasChatPanel({
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setComposerUploadProgress(null);
|
||||
clearComposerUploadProgress(450);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11777,11 +12005,24 @@ export function CanvasChatPanel({
|
||||
const currentSlots = Array.from({ length: slotCount }, (_, index) => attachments[index] ?? null);
|
||||
let handledCount = 0;
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
try {
|
||||
for (const [index, file] of selectedFiles.entries()) {
|
||||
const targetSlotIndex = currentSlots.findIndex((slot) => !slot);
|
||||
if (targetSlotIndex < 0) break;
|
||||
|
||||
const uploaded = await uploadImageFileAndAddToCanvas(file, baseNodeCount + uploadIndex);
|
||||
const slotLabel =
|
||||
agentUploadSlots[targetSlotIndex]?.label || `第${targetSlotIndex + 1}张`;
|
||||
const uploaded = await uploadImageFileAndAddToCanvas(
|
||||
file,
|
||||
baseNodeCount + uploadIndex,
|
||||
{
|
||||
title: `正在上传${slotLabel}`,
|
||||
fileName: file.name,
|
||||
fileIndex: index,
|
||||
fileTotal: selectedFiles.length,
|
||||
target: "slot",
|
||||
}
|
||||
);
|
||||
if (!uploaded) continue;
|
||||
|
||||
updateAttachmentAtAgentSlot(targetSlotIndex, {
|
||||
@ -11807,6 +12048,9 @@ export function CanvasChatPanel({
|
||||
firstImageUrl = uploaded.imageUrl;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clearComposerUploadProgress(450);
|
||||
}
|
||||
|
||||
if (handledCount > 0) {
|
||||
toast.success(
|
||||
@ -11823,8 +12067,9 @@ export function CanvasChatPanel({
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
|
||||
try {
|
||||
for (const [index, file] of selectedFiles.entries()) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('请选择图片文件');
|
||||
continue;
|
||||
@ -11840,8 +12085,23 @@ export function CanvasChatPanel({
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('purpose', 'canvas-upload');
|
||||
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
|
||||
const progressOptions: CanvasUploadProgressOptions = {
|
||||
title:
|
||||
selectedFiles.length === 1
|
||||
? "正在上传参考图"
|
||||
: `正在上传 ${selectedFiles.length} 张参考图`,
|
||||
fileName: file.name,
|
||||
fileIndex: index,
|
||||
fileTotal: selectedFiles.length,
|
||||
target: "composer-reference",
|
||||
};
|
||||
updateComposerUploadProgress(progressOptions);
|
||||
const response = await fetchUploadWithRetry(formData, {
|
||||
onProgress: (progress) => {
|
||||
updateComposerUploadProgress(progressOptions, progress);
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('上传失败');
|
||||
@ -11908,6 +12168,9 @@ export function CanvasChatPanel({
|
||||
toast.error('图片上传失败');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clearComposerUploadProgress(450);
|
||||
}
|
||||
|
||||
console.log('[CanvasChat] 检查是否需要自动命名:', {
|
||||
firstImageUrl: !!firstImageUrl,
|
||||
@ -12048,6 +12311,10 @@ export function CanvasChatPanel({
|
||||
const isComposerUploading = Boolean(composerUploadProgress);
|
||||
const isSubmitting =
|
||||
isActiveChatPending || isDetailGenerating || isClawdSubmitting || isComposerUploading;
|
||||
const composerReferenceUploadProgress =
|
||||
composerUploadProgress?.target === "composer-reference"
|
||||
? composerUploadProgress
|
||||
: null;
|
||||
const hasInputOrAttachments = Boolean(
|
||||
input.trim() || attachments.length > 0 || (!isDetailGenerationMode && selectedSkillTag)
|
||||
);
|
||||
@ -15124,7 +15391,7 @@ export function CanvasChatPanel({
|
||||
{!isWorkspacePresentation && !isDetailGenerationMode && !isVideoWorkspace && activeComposerMode === "image" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mx-4 mt-3 flex h-16 w-16 shrink-0 flex-col items-center justify-center gap-1.5 overflow-hidden rounded-[18px] bg-[#f3f3f4] text-[#a8adb5] transition-colors hover:bg-[#ececee] hover:text-[#7d848e] dark:bg-white/8 dark:text-zinc-400 dark:hover:bg-white/12"
|
||||
className="relative mx-4 mt-3 flex h-16 w-16 shrink-0 flex-col items-center justify-center gap-1.5 overflow-hidden rounded-[18px] bg-[#f3f3f4] text-[#a8adb5] transition-colors hover:bg-[#ececee] hover:text-[#7d848e] dark:bg-white/8 dark:text-zinc-400 dark:hover:bg-white/12"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isSubmitting}
|
||||
title="添加参考图"
|
||||
@ -15148,6 +15415,36 @@ export function CanvasChatPanel({
|
||||
<span className="text-[13px] font-medium leading-none">参考图</span>
|
||||
</>
|
||||
)}
|
||||
{composerReferenceUploadProgress ? (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1 bg-black/65 px-1.5 text-white backdrop-blur-[1px]">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-[10px] font-medium leading-none">上传中</span>
|
||||
<span className="text-[11px] font-semibold leading-none tabular-nums">
|
||||
{Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
100,
|
||||
Math.round(composerReferenceUploadProgress.percent || 1)
|
||||
)
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
<span className="absolute inset-x-2 bottom-2 h-1 overflow-hidden rounded-full bg-white/20">
|
||||
<span
|
||||
className="block h-full rounded-full bg-[#e0ff3a] transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
100,
|
||||
Math.round(composerReferenceUploadProgress.percent || 1)
|
||||
)
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
) : null}
|
||||
{isClawdMode && clawdTableFiles.length > 0 ? (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user