From 86b1ee9eeb80b29051eb489609defbbc2e8db44a Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 13 May 2026 20:36:08 +0800 Subject: [PATCH] fix: sync canvas generation results immediately --- components/canvas/CanvasChatPanel.tsx | 204 +++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 2 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 8b77e517a..bcd12705c 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -784,6 +784,135 @@ function formatMessageDateLabel(timestamp: number): string { }).format(new Date(timestamp)); } +function sortCanvasMessagesByCreatedAt(messages: ChatMessage[]): ChatMessage[] { + return [...messages].sort((a, b) => { + const timeA = getMessageTimestamp(a) ?? 0; + const timeB = getMessageTimestamp(b) ?? 0; + return timeA - timeB; + }); +} + +function buildCanvasMessageFingerprint(message: ChatMessage): string { + const parts = Array.isArray(message.parts) ? message.parts : []; + const partFingerprint = parts + .slice(0, 8) + .map((part: any) => { + if (!part) return "part:null"; + if (part.type === "text") { + const text = String(part.text || "").trim(); + return `text:${text.length}:${text.slice(0, 80)}`; + } + + const url = + typeof part.url === "string" && part.url.trim() + ? part.url.trim() + : typeof part.data === "string" && part.data.trim() + ? part.data.trim() + : ""; + return `${String(part.type || "part")}:${String( + part.mediaType || part.mimeType || part.contentType || "" + )}:${url.slice(0, 120)}`; + }) + .join("|"); + const attachmentFingerprint = (((message as any).attachments || []) as any[]) + .slice(0, 6) + .map((attachment) => { + const url = String(attachment?.url || "").trim(); + return `${String(attachment?.contentType || "")}:${url.slice(0, 120)}`; + }) + .join("|"); + + return [ + message.id, + message.role, + String(getMessageTimestamp(message) ?? 0), + partFingerprint, + attachmentFingerprint, + ].join("::"); +} + +function buildCanvasHistoryMessagesSignature( + rawChatId: string, + messages: ChatMessage[], +): string { + const recentFingerprint = messages + .slice(-12) + .map((message) => buildCanvasMessageFingerprint(message)) + .join("||"); + return `${rawChatId}:${messages.length}:${recentFingerprint}`; +} + +function hasCanvasImageMarkdown(parts?: Array) { + if (!parts || parts.length === 0) return false; + return parts.some((part) => { + if (!part) return false; + + if ( + part?.type === "text" && + typeof part.text === "string" && + (part.text.includes("![thumb|") || + part.text.includes("![thumb|||") || + part.text.includes("![Generated Image")) + ) { + return true; + } + + if (part?.type === "image" && typeof part.url === "string" && part.url.length > 0) { + return true; + } + + if ( + part?.type === "file" && + typeof part.url === "string" && + ((typeof part.mediaType === "string" && part.mediaType.startsWith("image/")) || + /\.(png|jpe?g|webp|gif|bmp|svg)(\?|$)/i.test(part.url)) + ) { + return true; + } + + return false; + }); +} + +function hasCanvasLikelyGenerationErrorMessage(message?: ChatMessage | null) { + if (!message?.parts || message.parts.length === 0) return false; + const text = message.parts + .filter((part: any) => part?.type === "text" && typeof part.text === "string") + .map((part: any) => part.text) + .join("\n") + .trim(); + if (!text) return false; + return /失败|超时|重试|error|failed/i.test(text); +} + +function getCanvasAssistantMessagesAfterTargetUser( + dbMessages: ChatMessage[], + targetUserMessageId?: string | null, +) { + if (!Array.isArray(dbMessages) || dbMessages.length === 0) { + return [] as ChatMessage[]; + } + + let targetUserIndex = -1; + if (targetUserMessageId) { + targetUserIndex = dbMessages.findIndex( + (message) => message.role === "user" && message.id === targetUserMessageId, + ); + } + + if (targetUserIndex < 0) { + targetUserIndex = dbMessages.findLastIndex((message) => message.role === "user"); + } + + if (targetUserIndex < 0) { + return dbMessages.filter((message) => message.role === "assistant"); + } + + return dbMessages + .slice(targetUserIndex + 1) + .filter((message) => message.role === "assistant"); +} + const CANVAS_IMAGE_ANALYSIS_TITLE = "图片分析"; const CANVAS_IMAGE_DONE_SUMMARY = "已完成本次生成,如需微调可继续告诉我。"; const CANVAS_AGENT_SECTION_TITLES = new Set([ @@ -5091,6 +5220,78 @@ export function CanvasChatPanel({ } } }, + onFinish: async () => { + if (!rawChatId) return; + + const syncMessagesFromDatabase = (dbMessages: ChatMessage[]) => { + const historySignature = buildCanvasHistoryMessagesSignature(rawChatId, dbMessages); + loadedHistorySignatureRef.current = historySignature; + processedMessageIds.current = new Set(); + hasHydratedHistoryRef.current = true; + + setMessages((currentMessages) => { + const mergedMap = new Map(); + currentMessages.forEach((message) => mergedMap.set(message.id, message)); + dbMessages.forEach((message) => mergedMap.set(message.id, message)); + return sortCanvasMessagesByCreatedAt(Array.from(mergedMap.values())); + }); + + requestAnimationFrame(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" }); + }); + }; + + const reloadMessagesOnce = async () => { + const response = await fetch(`/api/messages?chatId=${rawChatId}&_t=${Date.now()}`, { + cache: "no-store", + }); + if (!response.ok) { + return { hasResult: false, hasError: false }; + } + + const data = await response.json().catch(() => ({})); + if (!Array.isArray(data?.messages)) { + return { hasResult: false, hasError: false }; + } + + const dbMessages = data.messages as ChatMessage[]; + syncMessagesFromDatabase(dbMessages); + + const assistantMessages = getCanvasAssistantMessagesAfterTargetUser(dbMessages); + const hasResult = assistantMessages.some((message) => { + if (hasCanvasImageMarkdown((message as any).parts)) return true; + const text = ((message as any).parts || []) + .filter((part: any) => part?.type === "text" && typeof part.text === "string") + .map((part: any) => String(part.text)) + .join("\n"); + return extractAssistantImageUrls(text).length > 0 || extractVideoUrls(text).length > 0; + }); + const hasError = assistantMessages.some((message) => + hasCanvasLikelyGenerationErrorMessage(message), + ); + + return { hasResult, hasError }; + }; + + try { + await new Promise((resolve) => window.setTimeout(resolve, 450)); + + let { hasResult, hasError } = await reloadMessagesOnce(); + if (!hasResult && !hasError) { + for (let attempt = 0; attempt < 6; attempt += 1) { + await new Promise((resolve) => window.setTimeout(resolve, 500)); + const next = await reloadMessagesOnce(); + hasResult = next.hasResult; + hasError = next.hasError; + if (hasResult || hasError) { + break; + } + } + } + } catch (error) { + console.error("[CanvasChat] Failed to sync messages after finish:", error); + } + }, onError: (error) => { clearStreamImageGenerationStateForChat(activeChatId); setAgentProgressStateForChat(activeChatId, { @@ -5195,8 +5396,7 @@ export function CanvasChatPanel({ if ((status === "streaming" || status === "submitted") && !shouldForceHydrate) return; const historyMessages = historyData.messages || []; - const lastMessageId = historyMessages.at(-1)?.id || "none"; - const signature = `${rawChatId}:${historyMessages.length}:${lastMessageId}`; + const signature = buildCanvasHistoryMessagesSignature(rawChatId, historyMessages); if (loadedHistorySignatureRef.current === signature) return;