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));
|
}).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_ANALYSIS_TITLE = "图片分析";
|
||||||
const CANVAS_IMAGE_DONE_SUMMARY = "已完成本次生成,如需微调可继续告诉我。";
|
const CANVAS_IMAGE_DONE_SUMMARY = "已完成本次生成,如需微调可继续告诉我。";
|
||||||
const CANVAS_AGENT_SECTION_TITLES = new Set([
|
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) => {
|
onError: (error) => {
|
||||||
clearStreamImageGenerationStateForChat(activeChatId);
|
clearStreamImageGenerationStateForChat(activeChatId);
|
||||||
setAgentProgressStateForChat(activeChatId, {
|
setAgentProgressStateForChat(activeChatId, {
|
||||||
@ -5195,8 +5396,7 @@ export function CanvasChatPanel({
|
|||||||
if ((status === "streaming" || status === "submitted") && !shouldForceHydrate) return;
|
if ((status === "streaming" || status === "submitted") && !shouldForceHydrate) return;
|
||||||
|
|
||||||
const historyMessages = historyData.messages || [];
|
const historyMessages = historyData.messages || [];
|
||||||
const lastMessageId = historyMessages.at(-1)?.id || "none";
|
const signature = buildCanvasHistoryMessagesSignature(rawChatId, historyMessages);
|
||||||
const signature = `${rawChatId}:${historyMessages.length}:${lastMessageId}`;
|
|
||||||
|
|
||||||
if (loadedHistorySignatureRef.current === signature) return;
|
if (loadedHistorySignatureRef.current === signature) return;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user