fix: close image placeholder on generation end

Emit generation end markers for success/failure and scope marker handling to the latest request so placeholders stop on errors.
This commit is contained in:
Your Name 2026-02-03 20:45:47 +08:00
parent 35b190011a
commit 9fae58cfd3
4 changed files with 116 additions and 5 deletions

View File

@ -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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
// 保存消息到数据库(现在是 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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
// 保存消息到数据库
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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
// 尝试保存错误消息
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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
// 保存图片消息到数据库
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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
// 保存图片消息到数据库
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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
// 尝试保存错误消息
try {
await saveMessages({

View File

@ -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("<!-- IMAGE_GENERATION_START -->")
)
);
const hasGenerationEnd = messagesAfterLastUser.some(msg =>
msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_END -->")
)
);
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*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim()
};
}

View File

@ -311,6 +311,9 @@ export function Chat({
const hasImageGenStartMarker = messageData.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->")
);
const hasImageGenEndMarker = messageData.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_END -->")
);
const hasLegacyGeneratingMarker = messageData.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->")
);
@ -318,11 +321,22 @@ export function Chat({
if (part.type !== "text" || !part.text) return false;
const cleanedText = part.text
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.replace(/\n*<!-- GENERATING_IMAGE -->\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) {

View File

@ -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("<!-- IMAGE_GENERATION_START -->")
)
);
const hasGenerationEnd = messagesAfterLastUser.some(msg =>
msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_END -->")
)
);
const isGeneratingImage = hasGenerationStart && !hasGenerationEnd;
// 对于图片模型,检查是否有图片内容(包含 ![thumb| 或 ![thumb||| 标记)
// 🆕 在智能自动模式下,需要检查最后一条用户消息之后的所有 assistant 消息
@ -322,6 +330,7 @@ function PureMessages({
const cleanedText = part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim();
return cleanedText.length > 0;
})
@ -423,6 +432,7 @@ function PureMessages({
text: part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim()
};
}
@ -494,6 +504,7 @@ function PureMessages({
const cleanedText = part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\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("<!-- IMAGE_GENERATION_START -->")
)
);
const hasGenerationEnd = messagesAfterLastUser.some(msg =>
msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_END -->")
)
);
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*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\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 <ImageGenerationLoading imageCount={imageCount} maxImages={6} providerType="alibaba-image" modelName="智能处理" />;
}
// 否则显示思考动画(在等待 AI 分析或延迟期间)