diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index f6c39e4c5..32da74ee2 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -1116,6 +1116,14 @@ type CanvasTypewriterTextProps = { onComplete?: (typingKey: string) => void; }; +type CanvasComposerUploadProgress = { + title: string; + message: string; + current: number; + total: number; + fileName?: string; +}; + type MarkdownTableBlockRange = { start: number; end: number; @@ -1353,6 +1361,60 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) { ); } +function CanvasComposerUploadProgressDialog({ + progress, +}: { + progress: CanvasComposerUploadProgress | null; +}) { + if (!progress) return null; + + const percent = + progress.total > 1 + ? Math.max( + 8, + Math.min(100, Math.round((progress.current / progress.total) * 100)) + ) + : 100; + const isSingleFile = progress.total <= 1; + + return createPortal( +
+
+
+
+ +
+
+
+ {progress.title} +
+ {progress.fileName ? ( +
+ {progress.fileName} +
+ ) : null} +
+
+ +
+
+
+ +
+ {progress.message} +
+
+
, + document.body + ); +} + function normalizeCanvasAgentText(value: string): string { return String(value || "") .replace(/\r\n/g, "\n") @@ -1941,6 +2003,8 @@ export function CanvasChatPanel({ } | null>(null); const [downloadProgress, setDownloadProgress] = useState(null); + const [composerUploadProgress, setComposerUploadProgress] = + useState(null); const [downloadingOriginalUrl, setDownloadingOriginalUrl] = useState(""); const [addingClawdImageUrl, setAddingClawdImageUrl] = useState(""); const downloadAbortControllerRef = useRef(null); @@ -10641,30 +10705,45 @@ export function CanvasChatPanel({ if (isClawdMode) { let handledCount = 0; - for (const file of files) { - const uploaded = await uploadImageFileForComposerAttachment(file); - if (!uploaded) continue; + try { + for (const [index, file] of files.entries()) { + setComposerUploadProgress({ + title: + files.length === 1 + ? "正在上传图片" + : `正在上传 ${files.length} 张图片`, + message: `正在上传第 ${index + 1}/${files.length} 张,图片较大时可能需要一点时间。`, + current: index + 1, + total: files.length, + fileName: file.name, + }); - handledCount += 1; - setAttachments((prev) => { - const newIndex = prev.length; - setTimeout(() => { - insertImageTagInEditor(uploaded.imageUrl, uploaded.name, newIndex); - (textareaRef.current as HTMLDivElement | null)?.focus(); - }, 0); + const uploaded = await uploadImageFileForComposerAttachment(file); + if (!uploaded) continue; - return [ - ...prev, - { - url: uploaded.imageUrl, - name: uploaded.name, - contentType: uploaded.contentType, - assetId: uploaded.assetId, - width: uploaded.width, - height: uploaded.height, - }, - ]; - }); + handledCount += 1; + setAttachments((prev) => { + const newIndex = prev.length; + setTimeout(() => { + insertImageTagInEditor(uploaded.imageUrl, uploaded.name, newIndex); + (textareaRef.current as HTMLDivElement | null)?.focus(); + }, 0); + + return [ + ...prev, + { + url: uploaded.imageUrl, + name: uploaded.name, + contentType: uploaded.contentType, + assetId: uploaded.assetId, + width: uploaded.width, + height: uploaded.height, + }, + ]; + }); + } + } finally { + setComposerUploadProgress(null); } if (handledCount > 0) { @@ -10887,30 +10966,47 @@ export function CanvasChatPanel({ } let handledImageCount = 0; - for (const file of imageFiles) { - const uploaded = await uploadImageFileForComposerAttachment(file); - if (!uploaded) continue; + if (imageFiles.length > 0) { + try { + for (const [index, file] of imageFiles.entries()) { + setComposerUploadProgress({ + title: + imageFiles.length === 1 + ? "正在上传图片" + : `正在上传 ${imageFiles.length} 张图片`, + message: `正在上传第 ${index + 1}/${imageFiles.length} 张,图片较大时可能需要一点时间。`, + current: index + 1, + total: imageFiles.length, + fileName: file.name, + }); - handledImageCount += 1; - setAttachments((prev) => { - const newIndex = prev.length; - setTimeout(() => { - insertImageTagInEditor(uploaded.imageUrl, uploaded.name, newIndex); - (textareaRef.current as HTMLDivElement | null)?.focus(); - }, 0); + const uploaded = await uploadImageFileForComposerAttachment(file); + if (!uploaded) continue; - return [ - ...prev, - { - url: uploaded.imageUrl, - name: uploaded.name, - contentType: uploaded.contentType, - assetId: uploaded.assetId, - width: uploaded.width, - height: uploaded.height, - }, - ]; - }); + handledImageCount += 1; + setAttachments((prev) => { + const newIndex = prev.length; + setTimeout(() => { + insertImageTagInEditor(uploaded.imageUrl, uploaded.name, newIndex); + (textareaRef.current as HTMLDivElement | null)?.focus(); + }, 0); + + return [ + ...prev, + { + url: uploaded.imageUrl, + name: uploaded.name, + contentType: uploaded.contentType, + assetId: uploaded.assetId, + width: uploaded.width, + height: uploaded.height, + }, + ]; + }); + } + } finally { + setComposerUploadProgress(null); + } } if (handledImageCount > 0) { @@ -11200,7 +11296,9 @@ export function CanvasChatPanel({ ); const isLoading = status === 'streaming' || status === 'submitted'; - const isSubmitting = isLoading || isDetailGenerating || isClawdSubmitting; + const isComposerUploading = Boolean(composerUploadProgress); + const isSubmitting = + isLoading || isDetailGenerating || isClawdSubmitting || isComposerUploading; const hasInputOrAttachments = Boolean( input.trim() || attachments.length > 0 || (!isDetailGenerationMode && selectedSkillTag) ); @@ -12851,6 +12949,7 @@ export function CanvasChatPanel({ onCancel={handleCancelOriginalImageDownload} progress={downloadProgress} /> + {!isWorkspacePresentation && shouldShowCanvasBanner ? (