fix: sync canvas generation results immediately
This commit is contained in:
parent
1b1ff65de8
commit
86b1ee9eeb
@ -784,6 +784,135 @@ function formatMessageDateLabel(timestamp: number): string {
|
||||
}).format(new Date(timestamp));
|
||||
}
|
||||
|
||||
function sortCanvasMessagesByCreatedAt(messages: ChatMessage[]): ChatMessage[] {
|
||||
return [...messages].sort((a, b) => {
|
||||
const timeA = getMessageTimestamp(a) ?? 0;
|
||||
const timeB = getMessageTimestamp(b) ?? 0;
|
||||
return timeA - timeB;
|
||||
});
|
||||
}
|
||||
|
||||
function buildCanvasMessageFingerprint(message: ChatMessage): string {
|
||||
const parts = Array.isArray(message.parts) ? message.parts : [];
|
||||
const partFingerprint = parts
|
||||
.slice(0, 8)
|
||||
.map((part: any) => {
|
||||
if (!part) return "part:null";
|
||||
if (part.type === "text") {
|
||||
const text = String(part.text || "").trim();
|
||||
return `text:${text.length}:${text.slice(0, 80)}`;
|
||||
}
|
||||
|
||||
const url =
|
||||
typeof part.url === "string" && part.url.trim()
|
||||
? part.url.trim()
|
||||
: typeof part.data === "string" && part.data.trim()
|
||||
? part.data.trim()
|
||||
: "";
|
||||
return `${String(part.type || "part")}:${String(
|
||||
part.mediaType || part.mimeType || part.contentType || ""
|
||||
)}:${url.slice(0, 120)}`;
|
||||
})
|
||||
.join("|");
|
||||
const attachmentFingerprint = (((message as any).attachments || []) as any[])
|
||||
.slice(0, 6)
|
||||
.map((attachment) => {
|
||||
const url = String(attachment?.url || "").trim();
|
||||
return `${String(attachment?.contentType || "")}:${url.slice(0, 120)}`;
|
||||
})
|
||||
.join("|");
|
||||
|
||||
return [
|
||||
message.id,
|
||||
message.role,
|
||||
String(getMessageTimestamp(message) ?? 0),
|
||||
partFingerprint,
|
||||
attachmentFingerprint,
|
||||
].join("::");
|
||||
}
|
||||
|
||||
function buildCanvasHistoryMessagesSignature(
|
||||
rawChatId: string,
|
||||
messages: ChatMessage[],
|
||||
): string {
|
||||
const recentFingerprint = messages
|
||||
.slice(-12)
|
||||
.map((message) => buildCanvasMessageFingerprint(message))
|
||||
.join("||");
|
||||
return `${rawChatId}:${messages.length}:${recentFingerprint}`;
|
||||
}
|
||||
|
||||
function hasCanvasImageMarkdown(parts?: Array<any>) {
|
||||
if (!parts || parts.length === 0) return false;
|
||||
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 hasCanvasLikelyGenerationErrorMessage(message?: ChatMessage | null) {
|
||||
if (!message?.parts || message.parts.length === 0) return false;
|
||||
const text = message.parts
|
||||
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
||||
.map((part: any) => part.text)
|
||||
.join("\n")
|
||||
.trim();
|
||||
if (!text) return false;
|
||||
return /失败|超时|重试|error|failed/i.test(text);
|
||||
}
|
||||
|
||||
function getCanvasAssistantMessagesAfterTargetUser(
|
||||
dbMessages: ChatMessage[],
|
||||
targetUserMessageId?: string | null,
|
||||
) {
|
||||
if (!Array.isArray(dbMessages) || dbMessages.length === 0) {
|
||||
return [] as ChatMessage[];
|
||||
}
|
||||
|
||||
let targetUserIndex = -1;
|
||||
if (targetUserMessageId) {
|
||||
targetUserIndex = dbMessages.findIndex(
|
||||
(message) => message.role === "user" && message.id === targetUserMessageId,
|
||||
);
|
||||
}
|
||||
|
||||
if (targetUserIndex < 0) {
|
||||
targetUserIndex = dbMessages.findLastIndex((message) => message.role === "user");
|
||||
}
|
||||
|
||||
if (targetUserIndex < 0) {
|
||||
return dbMessages.filter((message) => message.role === "assistant");
|
||||
}
|
||||
|
||||
return dbMessages
|
||||
.slice(targetUserIndex + 1)
|
||||
.filter((message) => message.role === "assistant");
|
||||
}
|
||||
|
||||
const CANVAS_IMAGE_ANALYSIS_TITLE = "图片分析";
|
||||
const CANVAS_IMAGE_DONE_SUMMARY = "已完成本次生成,如需微调可继续告诉我。";
|
||||
const CANVAS_AGENT_SECTION_TITLES = new Set([
|
||||
@ -5091,6 +5220,78 @@ export function CanvasChatPanel({
|
||||
}
|
||||
}
|
||||
},
|
||||
onFinish: async () => {
|
||||
if (!rawChatId) return;
|
||||
|
||||
const syncMessagesFromDatabase = (dbMessages: ChatMessage[]) => {
|
||||
const historySignature = buildCanvasHistoryMessagesSignature(rawChatId, dbMessages);
|
||||
loadedHistorySignatureRef.current = historySignature;
|
||||
processedMessageIds.current = new Set();
|
||||
hasHydratedHistoryRef.current = true;
|
||||
|
||||
setMessages((currentMessages) => {
|
||||
const mergedMap = new Map<string, ChatMessage>();
|
||||
currentMessages.forEach((message) => mergedMap.set(message.id, message));
|
||||
dbMessages.forEach((message) => mergedMap.set(message.id, message));
|
||||
return sortCanvasMessagesByCreatedAt(Array.from(mergedMap.values()));
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||
});
|
||||
};
|
||||
|
||||
const reloadMessagesOnce = async () => {
|
||||
const response = await fetch(`/api/messages?chatId=${rawChatId}&_t=${Date.now()}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
return { hasResult: false, hasError: false };
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!Array.isArray(data?.messages)) {
|
||||
return { hasResult: false, hasError: false };
|
||||
}
|
||||
|
||||
const dbMessages = data.messages as ChatMessage[];
|
||||
syncMessagesFromDatabase(dbMessages);
|
||||
|
||||
const assistantMessages = getCanvasAssistantMessagesAfterTargetUser(dbMessages);
|
||||
const hasResult = assistantMessages.some((message) => {
|
||||
if (hasCanvasImageMarkdown((message as any).parts)) return true;
|
||||
const text = ((message as any).parts || [])
|
||||
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
||||
.map((part: any) => String(part.text))
|
||||
.join("\n");
|
||||
return extractAssistantImageUrls(text).length > 0 || extractVideoUrls(text).length > 0;
|
||||
});
|
||||
const hasError = assistantMessages.some((message) =>
|
||||
hasCanvasLikelyGenerationErrorMessage(message),
|
||||
);
|
||||
|
||||
return { hasResult, hasError };
|
||||
};
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 450));
|
||||
|
||||
let { hasResult, hasError } = await reloadMessagesOnce();
|
||||
if (!hasResult && !hasError) {
|
||||
for (let attempt = 0; attempt < 6; attempt += 1) {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 500));
|
||||
const next = await reloadMessagesOnce();
|
||||
hasResult = next.hasResult;
|
||||
hasError = next.hasError;
|
||||
if (hasResult || hasError) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[CanvasChat] Failed to sync messages after finish:", error);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
clearStreamImageGenerationStateForChat(activeChatId);
|
||||
setAgentProgressStateForChat(activeChatId, {
|
||||
@ -5195,8 +5396,7 @@ export function CanvasChatPanel({
|
||||
if ((status === "streaming" || status === "submitted") && !shouldForceHydrate) return;
|
||||
|
||||
const historyMessages = historyData.messages || [];
|
||||
const lastMessageId = historyMessages.at(-1)?.id || "none";
|
||||
const signature = `${rawChatId}:${historyMessages.length}:${lastMessageId}`;
|
||||
const signature = buildCanvasHistoryMessagesSignature(rawChatId, historyMessages);
|
||||
|
||||
if (loadedHistorySignatureRef.current === signature) return;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user