diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index db023f67f..01713f266 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -912,6 +912,16 @@ export async function POST(request: Request) { }), }); + // 🆕 图片生成结束标记(成功) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + // 保存消息到数据库(现在是 S3 URL,可以完整保存) try { await saveMessages({ @@ -998,6 +1008,16 @@ export async function POST(request: Request) { }), }); + // 🆕 图片生成结束标记(故障转移成功) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + // 保存消息到数据库 try { await saveMessages({ @@ -1056,6 +1076,16 @@ export async function POST(request: Request) { }), }); + // 🆕 图片生成结束标记(失败) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + // 尝试保存错误消息 try { await saveMessages({ @@ -1271,6 +1301,16 @@ export async function POST(request: Request) { }), }); + // 🆕 图片生成结束标记(成功) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + // 保存图片消息到数据库 try { await saveMessages({ @@ -1322,6 +1362,16 @@ export async function POST(request: Request) { }), }); + // 🆕 图片生成结束标记(失败) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + try { await saveMessages({ messages: [ @@ -1502,6 +1552,16 @@ export async function POST(request: Request) { }), }); + // 🆕 图片生成结束标记(成功) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + // 保存图片消息到数据库 try { await saveMessages({ @@ -1552,6 +1612,16 @@ export async function POST(request: Request) { }), }); + // 🆕 图片生成结束标记(失败) + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + // 尝试保存错误消息 try { await saveMessages({ diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index f50c29fea..13cbc1520 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -1065,13 +1065,20 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF }; const isLoading = status === 'streaming' || status === 'submitted'; - const hasGeneratingMarker = messages.some(msg => + const lastUserIndexForMarkers = messages.findLastIndex(m => m.role === "user"); + const messagesAfterLastUser = lastUserIndexForMarkers >= 0 ? messages.slice(lastUserIndexForMarkers + 1) : messages; + const hasGenerationStart = messagesAfterLastUser.some(msg => msg.role === "assistant" && msg.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ) ); + const hasGenerationEnd = messagesAfterLastUser.some(msg => + msg.role === "assistant" && msg.parts?.some( + (part: any) => part.type === "text" && part.text?.includes("") + ) + ); const currentModel = allModels.find(m => m.id === selectedModelId); - const shouldShowImageLoading = hasGeneratingMarker && !!currentModel; + const shouldShowImageLoading = hasGenerationStart && !hasGenerationEnd && !!currentModel; const loadingImageCount = imageParams.imageCount || 1; return ( @@ -1184,6 +1191,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF text: part.text .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") .trim() }; } diff --git a/components/chat.tsx b/components/chat.tsx index 42dffd6e7..af6acbd9c 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -311,6 +311,9 @@ export function Chat({ const hasImageGenStartMarker = messageData.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ); + const hasImageGenEndMarker = messageData.parts?.some( + (part: any) => part.type === "text" && part.text?.includes("") + ); const hasLegacyGeneratingMarker = messageData.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ); @@ -318,11 +321,22 @@ export function Chat({ if (part.type !== "text" || !part.text) return false; const cleanedText = part.text .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") .trim(); return cleanedText.length === 0; }); + if (hasImageGenEndMarker && isMarkerOnlyMessage) { + console.log("[Chat] 🖼️ Image generation ended, hide placeholder"); + if (imagePlaceholderTimerRef.current) { + clearTimeout(imagePlaceholderTimerRef.current); + imagePlaceholderTimerRef.current = null; + } + setIsGeneratingImageFlag(false); + return; // 不渲染标记消息 + } + if (hasImageGenStartMarker && isMarkerOnlyMessage) { console.log("[Chat] 🖼️ Image generation started, show placeholder"); if (imagePlaceholderTimerRef.current) { diff --git a/components/messages.tsx b/components/messages.tsx index 84709efe7..360f91e08 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -177,11 +177,19 @@ function PureMessages({ lastUserMessage?.parts?.some((p: any) => p.type === "file" || p.type === "image"); // 🆕 检查是否有"图片正在生成"的标记(表示对话消息已发送,正在等待图片生成) - const isGeneratingImage = messages.some(msg => + const lastUserIndexForMarkers = messages.findLastIndex(m => m.role === "user"); + const messagesAfterLastUser = lastUserIndexForMarkers >= 0 ? messages.slice(lastUserIndexForMarkers + 1) : messages; + const hasGenerationStart = messagesAfterLastUser.some(msg => msg.role === "assistant" && msg.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ) ); + const hasGenerationEnd = messagesAfterLastUser.some(msg => + msg.role === "assistant" && msg.parts?.some( + (part: any) => part.type === "text" && part.text?.includes("") + ) + ); + const isGeneratingImage = hasGenerationStart && !hasGenerationEnd; // 对于图片模型,检查是否有图片内容(包含 ![thumb| 或 ![thumb||| 标记) // 🆕 在智能自动模式下,需要检查最后一条用户消息之后的所有 assistant 消息 @@ -322,6 +330,7 @@ function PureMessages({ const cleanedText = part.text .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") .trim(); return cleanedText.length > 0; }) @@ -423,6 +432,7 @@ function PureMessages({ text: part.text .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") .trim() }; } @@ -494,6 +504,7 @@ function PureMessages({ const cleanedText = part.text .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") .trim(); return cleanedText.length > 0; }) @@ -535,17 +546,25 @@ function PureMessages({ const userMessageHasImage = lastUserMsg?.parts?.some((p: any) => p.type === "file" || p.type === "image"); // 🆕 检查是否收到了 GENERATING_IMAGE 标记(表示对话已发送,开始生图) - const hasGeneratingMarker = messages.some(msg => + const lastUserIndexForMarkers = messages.findLastIndex(m => m.role === "user"); + const messagesAfterLastUser = lastUserIndexForMarkers >= 0 ? messages.slice(lastUserIndexForMarkers + 1) : messages; + const hasGeneratingMarker = messagesAfterLastUser.some(msg => msg.role === "assistant" && msg.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ) ); + const hasGenerationEnd = messagesAfterLastUser.some(msg => + msg.role === "assistant" && msg.parts?.some( + (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; const cleanedText = part.text .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") + .replace(/\n*\n*/g, "") .trim(); return cleanedText.length > 0; }) @@ -560,7 +579,7 @@ function PureMessages({ if (isAutoMode && userMessageHasImage) { // 🆕 只用 isGeneratingImageFlag 来控制是否显示生图占位图 // isGeneratingImageFlag 由 chat.tsx 控制:延迟 3 秒后设置为 true,收到图片后设置为 false - if (autoImagePlaceholderReady && isGeneratingImageFlag && hasAssistantTextReplyWithContent) { + if (autoImagePlaceholderReady && isGeneratingImageFlag && hasAssistantTextReplyWithContent && !hasGenerationEnd) { return ; } // 否则显示思考动画(在等待 AI 分析或延迟期间)