fix(chat): prevent image placeholder from sticking after generation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-12 15:14:48 +08:00
parent 73d0d9d8c2
commit b5991d2a2a
2 changed files with 91 additions and 35 deletions

View File

@ -75,14 +75,43 @@ function sortMessagesByCreatedAt(messages: ChatMessage[]): ChatMessage[] {
function hasImageMarkdown(parts?: Array<any>) {
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);
}
});

View File

@ -153,6 +153,33 @@ function PureMessages({
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\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) => {