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,可以完整保存) // 保存消息到数据库(现在是 S3 URL,可以完整保存)
try { try {
await saveMessages({ 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 { try {
await saveMessages({ 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 { try {
await saveMessages({ 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 { try {
await saveMessages({ 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 { try {
await saveMessages({ await saveMessages({
messages: [ 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 { try {
await saveMessages({ 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 { try {
await saveMessages({ await saveMessages({

View File

@ -1065,13 +1065,20 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
}; };
const isLoading = status === 'streaming' || status === 'submitted'; 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( msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->") (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 currentModel = allModels.find(m => m.id === selectedModelId);
const shouldShowImageLoading = hasGeneratingMarker && !!currentModel; const shouldShowImageLoading = hasGenerationStart && !hasGenerationEnd && !!currentModel;
const loadingImageCount = imageParams.imageCount || 1; const loadingImageCount = imageParams.imageCount || 1;
return ( return (
@ -1184,6 +1191,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
text: part.text text: part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "") .replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "") .replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim() .trim()
}; };
} }

View File

@ -311,6 +311,9 @@ export function Chat({
const hasImageGenStartMarker = messageData.parts?.some( const hasImageGenStartMarker = messageData.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->") (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( const hasLegacyGeneratingMarker = messageData.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->") (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; if (part.type !== "text" || !part.text) return false;
const cleanedText = part.text const cleanedText = part.text
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "") .replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "") .replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.trim(); .trim();
return cleanedText.length === 0; 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) { if (hasImageGenStartMarker && isMarkerOnlyMessage) {
console.log("[Chat] 🖼️ Image generation started, show placeholder"); console.log("[Chat] 🖼️ Image generation started, show placeholder");
if (imagePlaceholderTimerRef.current) { if (imagePlaceholderTimerRef.current) {

View File

@ -177,11 +177,19 @@ function PureMessages({
lastUserMessage?.parts?.some((p: any) => p.type === "file" || p.type === "image"); 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( msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->") (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||| 标记) // 对于图片模型,检查是否有图片内容(包含 ![thumb| 或 ![thumb||| 标记)
// 🆕 在智能自动模式下,需要检查最后一条用户消息之后的所有 assistant 消息 // 🆕 在智能自动模式下,需要检查最后一条用户消息之后的所有 assistant 消息
@ -322,6 +330,7 @@ function PureMessages({
const cleanedText = part.text const cleanedText = part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "") .replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "") .replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim(); .trim();
return cleanedText.length > 0; return cleanedText.length > 0;
}) })
@ -423,6 +432,7 @@ function PureMessages({
text: part.text text: part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "") .replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "") .replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim() .trim()
}; };
} }
@ -494,6 +504,7 @@ function PureMessages({
const cleanedText = part.text const cleanedText = part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "") .replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "") .replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim(); .trim();
return cleanedText.length > 0; return cleanedText.length > 0;
}) })
@ -535,17 +546,25 @@ function PureMessages({
const userMessageHasImage = lastUserMsg?.parts?.some((p: any) => p.type === "file" || p.type === "image"); const userMessageHasImage = lastUserMsg?.parts?.some((p: any) => p.type === "file" || p.type === "image");
// 🆕 检查是否收到了 GENERATING_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( msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->") (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 => const hasAssistantTextReplyWithContent = messages.some(msg =>
msg.role === "assistant" && msg.parts?.some((part: any) => { msg.role === "assistant" && msg.parts?.some((part: any) => {
if (part.type !== "text" || !part.text) return false; if (part.type !== "text" || !part.text) return false;
const cleanedText = part.text const cleanedText = part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "") .replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "") .replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim(); .trim();
return cleanedText.length > 0; return cleanedText.length > 0;
}) })
@ -560,7 +579,7 @@ function PureMessages({
if (isAutoMode && userMessageHasImage) { if (isAutoMode && userMessageHasImage) {
// 🆕 只用 isGeneratingImageFlag 来控制是否显示生图占位图 // 🆕 只用 isGeneratingImageFlag 来控制是否显示生图占位图
// isGeneratingImageFlag 由 chat.tsx 控制:延迟 3 秒后设置为 true,收到图片后设置为 false // 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="智能处理" />; return <ImageGenerationLoading imageCount={imageCount} maxImages={6} providerType="alibaba-image" modelName="智能处理" />;
} }
// 否则显示思考动画(在等待 AI 分析或延迟期间) // 否则显示思考动画(在等待 AI 分析或延迟期间)