From b5991d2a2aced7146bcecd6390f39ae410c25aa7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 12 Feb 2026 15:14:48 +0800 Subject: [PATCH] fix(chat): prevent image placeholder from sticking after generation --- components/chat.tsx | 65 +++++++++++++++++++++++++++++++++++++---- components/messages.tsx | 61 +++++++++++++++++++------------------- 2 files changed, 91 insertions(+), 35 deletions(-) diff --git a/components/chat.tsx b/components/chat.tsx index ffe5445e1..998b1b9fd 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -75,14 +75,43 @@ function sortMessagesByCreatedAt(messages: ChatMessage[]): ChatMessage[] { function hasImageMarkdown(parts?: Array) { if (!parts || parts.length === 0) return false; - return parts.some( - (part) => + 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 getMessageTextLength(message?: ChatMessage | null) { + if (!message?.parts || message.parts.length === 0) return 0; + return message.parts + .filter((part: any) => part?.type === "text" && typeof part.text === "string") + .map((part: any) => part.text.trim()) + .join("\n") + .length; } export function Chat({ @@ -465,7 +494,7 @@ export function Chat({ content: messageData.parts?.[0]?.text || "", createdAt: new Date(), parts: messageData.parts || [], - attachments: [], + attachments: Array.isArray(messageData.attachments) ? messageData.attachments : [], } as ChatMessage; setAppendedMessages((prev) => { @@ -562,6 +591,15 @@ export function Chat({ } } catch (error) { console.error("[Chat] Failed to reload messages:", error); + } finally { + // 兜底:即使消息重载失败,也不能让占位图卡死在处理中状态 + if ( + imageGenerationPhaseRef.current === "queued" || + imageGenerationPhaseRef.current === "processing" || + imageGenerationPhaseRef.current === "finalizing" + ) { + updateImageGenerationPhase("idle"); + } } }, onError: (error) => { @@ -1450,8 +1488,25 @@ export function Chat({ // 3. 最后添加 appendedMessages(data-appendMessage 的消息) appendedMessages.forEach(m => { - if (!messageMap.has(m.id)) { + const existing = messageMap.get(m.id); + if (!existing) { messageMap.set(m.id, m); + return; + } + + const existingHasImage = hasImageMarkdown((existing as any).parts); + const nextHasImage = hasImageMarkdown((m as any).parts); + + const shouldReplace = + (nextHasImage && !existingHasImage) || + getMessageTextLength(m) > getMessageTextLength(existing); + + if (shouldReplace) { + messageMap.set(m.id, { + ...existing, + ...m, + createdAt: (existing as any).createdAt || (m as any).createdAt, + } as ChatMessage); } }); diff --git a/components/messages.tsx b/components/messages.tsx index fa4293ad8..5d3589455 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -153,6 +153,33 @@ function PureMessages({ .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") .trim(); + const hasImageContentInMessage = (message?: ChatMessage | null) => { + if (!message?.parts || message.parts.length === 0) return false; + return message.parts.some((part: any) => { + if (part?.type === "text" && typeof part.text === "string") { + return ( + part.text.includes("![thumb|") || + part.text.includes("![thumb|||") || + part.text.includes("![Generated Image") + ); + } + + 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; + }); + }; useEffect(() => { return () => { @@ -234,22 +261,10 @@ function PureMessages({ const lastUserIndex = messages.findLastIndex(m => m.role === "user"); // 检查之后所有的 assistant 消息是否包含图片 const assistantMessagesAfterUser = messages.slice(lastUserIndex + 1).filter(m => m.role === "assistant"); - hasImageContent = assistantMessagesAfterUser.some(msg => - msg.parts?.some((part: any) => part.type === "text" && part.text && ( - part.text.includes("![thumb|") || - part.text.includes("![thumb|||") || - part.text.includes("![Generated Image") - )) - ); + hasImageContent = assistantMessagesAfterUser.some((msg) => hasImageContentInMessage(msg)); } else { // 普通模式:只检查最后一条消息 - hasImageContent = lastMessage?.parts && lastMessage.parts.some( - (part: any) => part.type === "text" && part.text && ( - part.text.includes("![thumb|") || - part.text.includes("![thumb|||") || - part.text.includes("![Generated Image") - ) - ) || false; + hasImageContent = hasImageContentInMessage(lastMessage); } // 对于其他模型,检查是否有实际的文本内容 @@ -373,14 +388,7 @@ function PureMessages({ ); const hasImageContent = assistantMessagesAfterUser.some((msg) => - msg.parts?.some( - (part: any) => - part.type === "text" && - part.text && - (part.text.includes("![thumb|") || - part.text.includes("![thumb|||") || - part.text.includes("![Generated Image")) - ) + hasImageContentInMessage(msg) ); const hasGenerationStart = assistantMessagesAfterUser.some((msg) => msg.parts?.some( @@ -602,14 +610,7 @@ function PureMessages({ ? messages.slice(lastUserIndexForMarkers + 1).filter((msg) => msg.role === "assistant") : messages.filter((msg) => msg.role === "assistant"); const hasImageContent = assistantMessagesAfterUser.some((msg) => - msg.parts?.some( - (part: any) => - part.type === "text" && - part.text && - (part.text.includes("![thumb|") || - part.text.includes("![thumb|||") || - part.text.includes("![Generated Image")) - ) + hasImageContentInMessage(msg) ); const hasAssistantTextReplyWithContent = assistantMessagesAfterUser.some((msg) => msg.parts?.some((part: any) => {