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 ? (