fix: trigger image placeholder after analysis

Send a dedicated generation-start marker when image generation begins and stop embedding markers in assistant text.
This commit is contained in:
Your Name 2026-02-03 19:45:49 +08:00
parent 2e6f421b6d
commit 20efbbbb53
4 changed files with 92 additions and 36 deletions

View File

@ -794,11 +794,9 @@ export async function POST(request: Request) {
execute: async ({ writer }) => {
// 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到
// 在消息末尾加上特殊标记,告诉前端"图片正在生成中"
const messageWithMarker = smartAutoFriendlyMessage
? `${smartAutoFriendlyMessage}\n\n<!-- GENERATING_IMAGE -->`
: 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: "<!-- IMAGE_GENERATION_START -->" }],
}),
});
try {
// 首先尝试主模型
@ -1109,12 +1117,9 @@ export async function POST(request: Request) {
execute: async ({ writer }) => {
try {
// 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到
// 在消息末尾加上特殊标记,告诉前端"图片正在生成中"
const messageWithMarker = smartAutoFriendlyMessage
? `${smartAutoFriendlyMessage}\n\n<!-- GENERATING_IMAGE -->`
: 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: "<!-- IMAGE_GENERATION_START -->" }],
}),
});
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<!-- GENERATING_IMAGE -->`
: 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: "<!-- IMAGE_GENERATION_START -->" }],
}),
});
try {
imageMarkdown = await tryGenerateImage(actualModelId, false);
} catch (primaryError) {

View File

@ -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("<!-- GENERATING_IMAGE -->")
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->")
)
);
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*<!-- GENERATING_IMAGE -->\n*/g, '').trim()
text: part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.trim()
};
}
return part;

View File

@ -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("<!-- IMAGE_GENERATION_START -->")
);
const hasLegacyGeneratingMarker = messageData.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->")
);
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*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- GENERATING_IMAGE -->\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*<!-- GENERATING_IMAGE -->\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);
}
// 🆕 检测是否包含图片(生成完成)

View File

@ -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("<!-- GENERATING_IMAGE -->")
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->")
)
);
@ -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("<!-- GENERATING_IMAGE -->")) return false;
const cleanedText = part.text.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "").trim();
const cleanedText = part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\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*<!-- GENERATING_IMAGE -->\n*/g, "").trim()
text: part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\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("<!-- GENERATING_IMAGE -->")
)
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, "")
.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("<!-- GENERATING_IMAGE -->")
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->")
)
);
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("<!-- GENERATING_IMAGE -->")) return false;
const cleanedText = part.text.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "").trim();
const cleanedText = part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.trim();
return cleanedText.length > 0;
})
);