From 20efbbbb537691fba1671c2a47a227ee55c2dbe2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 3 Feb 2026 19:45:49 +0800 Subject: [PATCH] fix: trigger image placeholder after analysis Send a dedicated generation-start marker when image generation begins and stop embedding markers in assistant text. --- app/(chat)/api/chat/route.ts | 56 +++++++++++++++++++-------- components/canvas/CanvasChatPanel.tsx | 7 +++- components/chat.tsx | 33 ++++++++++++---- components/messages.tsx | 32 ++++++++++----- 4 files changed, 92 insertions(+), 36 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 6d45fa7..db023f6 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -794,11 +794,9 @@ export async function POST(request: Request) { execute: async ({ writer }) => { // 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到 // 在消息末尾加上特殊标记,告诉前端"图片正在生成中" - const messageWithMarker = smartAutoFriendlyMessage - ? `${smartAutoFriendlyMessage}\n\n` - : null; + const conversationMessage = smartAutoFriendlyMessage || null; - if (messageWithMarker) { + if (conversationMessage) { const chatMessageId = generateUUID(); console.log("[Chat API] 💬 Gemini: Sending conversational reply first:", smartAutoFriendlyMessage); @@ -808,7 +806,7 @@ export async function POST(request: Request) { data: JSON.stringify({ id: chatMessageId, role: "assistant", - parts: [{ type: "text", text: messageWithMarker }], + parts: [{ type: "text", text: conversationMessage }], }), }); @@ -859,6 +857,16 @@ export async function POST(request: Request) { try { let imageMarkdown = ""; // 初始化为空字符串 let usedModelId = actualModelId; + + // 🆕 图片生成真正开始时发出标记(用于前端显示占位图) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); try { // 首先尝试主模型 @@ -1109,12 +1117,9 @@ export async function POST(request: Request) { execute: async ({ writer }) => { try { // 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到 - // 在消息末尾加上特殊标记,告诉前端"图片正在生成中" - const messageWithMarker = smartAutoFriendlyMessage - ? `${smartAutoFriendlyMessage}\n\n` - : null; + const conversationMessage = smartAutoFriendlyMessage || null; - if (messageWithMarker) { + if (conversationMessage) { const chatMessageId = generateUUID(); console.log("[Chat API] 💬 Sending conversational reply first:", smartAutoFriendlyMessage); @@ -1124,7 +1129,7 @@ export async function POST(request: Request) { data: JSON.stringify({ id: chatMessageId, role: "assistant", - parts: [{ type: "text", text: messageWithMarker }], + parts: [{ type: "text", text: conversationMessage }], }), }); @@ -1215,6 +1220,16 @@ export async function POST(request: Request) { let imageMarkdown = ""; let usedModelId = actualModelId; + + // 🆕 图片生成真正开始时发出标记(用于前端显示占位图) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); try { imageMarkdown = await tryGenerateImage(actualModelId, false); @@ -1388,12 +1403,9 @@ export async function POST(request: Request) { execute: async ({ writer }) => { try { // 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到 - // 在消息末尾加上特殊标记,告诉前端"图片正在生成中" - const messageWithMarker = smartAutoFriendlyMessage - ? `${smartAutoFriendlyMessage}\n\n` - : null; + const conversationMessage = smartAutoFriendlyMessage || null; - if (messageWithMarker) { + if (conversationMessage) { const chatMessageId = generateUUID(); console.log("[Chat API] 💬 Volcano: Sending conversational reply first:", smartAutoFriendlyMessage); @@ -1403,7 +1415,7 @@ export async function POST(request: Request) { data: JSON.stringify({ id: chatMessageId, role: "assistant", - parts: [{ type: "text", text: messageWithMarker }], + parts: [{ type: "text", text: conversationMessage }], }), }); @@ -1443,6 +1455,16 @@ export async function POST(request: Request) { let imageMarkdown = ""; let usedModelId = actualModelId; + + // 🆕 图片生成真正开始时发出标记(用于前端显示占位图) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); try { imageMarkdown = await tryGenerateImage(actualModelId, false); } catch (primaryError) { diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 836da49..abd3406 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -1067,7 +1067,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const isLoading = status === 'streaming' || status === 'submitted'; const hasGeneratingMarker = messages.some(msg => msg.role === "assistant" && msg.parts?.some( - (part: any) => part.type === "text" && part.text?.includes("") + (part: any) => part.type === "text" && part.text?.includes("") ) ); const currentModel = allModels.find(m => m.id === selectedModelId); @@ -1181,7 +1181,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF if (part.type === 'text' && part.text) { return { ...part, - text: part.text.replace(/\n*\n*/g, '').trim() + text: part.text + .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") + .trim() }; } return part; diff --git a/components/chat.tsx b/components/chat.tsx index 7d52058..42dffd6 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -308,12 +308,33 @@ export function Chat({ console.log("[Chat] Received appendMessage:", messageData); - // 🆕 检测是否包含 GENERATING_IMAGE 标记 - const hasGeneratingMarker = messageData.parts?.some( + const hasImageGenStartMarker = messageData.parts?.some( + (part: any) => part.type === "text" && part.text?.includes("") + ); + const hasLegacyGeneratingMarker = messageData.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ); - if (hasGeneratingMarker) { - console.log("[Chat] 🖼️ Detected GENERATING_IMAGE marker, will show placeholder after delay"); + const isMarkerOnlyMessage = messageData.parts?.every((part: any) => { + if (part.type !== "text" || !part.text) return false; + const cleanedText = part.text + .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") + .trim(); + return cleanedText.length === 0; + }); + + if (hasImageGenStartMarker && isMarkerOnlyMessage) { + console.log("[Chat] 🖼️ Image generation started, show placeholder"); + if (imagePlaceholderTimerRef.current) { + clearTimeout(imagePlaceholderTimerRef.current); + imagePlaceholderTimerRef.current = null; + } + setIsGeneratingImageFlag(true); + return; // 不渲染标记消息 + } + + if (hasLegacyGeneratingMarker) { + console.log("[Chat] 🖼️ Detected legacy marker, will show placeholder after delay"); const hasDisplayText = messageData.parts?.some((part: any) => { if (part.type !== "text" || !part.text) return false; const cleanedText = part.text.replace(/\n*\n*/g, "").trim(); @@ -323,8 +344,6 @@ export function Chat({ console.log("[Chat] ⚠️ Marker message has no display text, skip image placeholder timer"); return; } - // 🆕 延迟显示占位图,让用户先看到 AI 的文字回复 - // 消息会立即添加到 appendedMessages 并显示,3 秒后再显示动态占位图 if (imagePlaceholderTimerRef.current) { clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; @@ -333,7 +352,7 @@ export function Chat({ console.log("[Chat] 🖼️ Now showing placeholder after 3s delay"); setIsGeneratingImageFlag(true); imagePlaceholderTimerRef.current = null; - }, 3000); // 延迟 3 秒,让用户有时间阅读文字 + }, 3000); } // 🆕 检测是否包含图片(生成完成) diff --git a/components/messages.tsx b/components/messages.tsx index 1faff2f..84709ef 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -179,7 +179,7 @@ function PureMessages({ // 🆕 检查是否有"图片正在生成"的标记(表示对话消息已发送,正在等待图片生成) const isGeneratingImage = messages.some(msg => msg.role === "assistant" && msg.parts?.some( - (part: any) => part.type === "text" && part.text?.includes("") + (part: any) => part.type === "text" && part.text?.includes("") ) ); @@ -319,8 +319,10 @@ function PureMessages({ const hasAssistantTextReplyWithContent = messages.some(msg => msg.role === "assistant" && msg.parts?.some((part: any) => { if (part.type !== "text" || !part.text) return false; - if (!part.text.includes("")) return false; - const cleanedText = part.text.replace(/\n*\n*/g, "").trim(); + const cleanedText = part.text + .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") + .trim(); return cleanedText.length > 0; }) ); @@ -418,7 +420,10 @@ function PureMessages({ if (part.type === "text" && part.text) { return { ...part, - text: part.text.replace(/\n*\n*/g, "").trim() + text: part.text + .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") + .trim() }; } return part; @@ -484,9 +489,14 @@ function PureMessages({ // 恢复的待处理状态:有 pendingGeneration 且最后一条是用户消息 // 🆕 但如果收到了 AI 回复(包含 GENERATING_IMAGE 标记),不应该再显示恢复的占位图 const hasAssistantReply = messages.some(msg => - msg.role === "assistant" && msg.parts?.some( - (part: any) => part.type === "text" && part.text?.includes("") - ) + msg.role === "assistant" && msg.parts?.some((part: any) => { + if (part.type !== "text" || !part.text) return false; + const cleanedText = part.text + .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") + .trim(); + return cleanedText.length > 0; + }) ); const hasRestoredPending = pendingGeneration && isWaitingForResponse && !hasAssistantReply; @@ -527,14 +537,16 @@ function PureMessages({ // 🆕 检查是否收到了 GENERATING_IMAGE 标记(表示对话已发送,开始生图) const hasGeneratingMarker = messages.some(msg => msg.role === "assistant" && msg.parts?.some( - (part: any) => part.type === "text" && part.text?.includes("") + (part: any) => part.type === "text" && part.text?.includes("") ) ); const hasAssistantTextReplyWithContent = messages.some(msg => msg.role === "assistant" && msg.parts?.some((part: any) => { if (part.type !== "text" || !part.text) return false; - if (!part.text.includes("")) return false; - const cleanedText = part.text.replace(/\n*\n*/g, "").trim(); + const cleanedText = part.text + .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") + .trim(); return cleanedText.length > 0; }) );