fix: dedupe canvas media nodes for streamed assistant messages
This commit is contained in:
parent
ba10d4e5be
commit
cf1f431208
@ -2640,105 +2640,122 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
useEffect(() => {
|
||||
if (!hasHydratedHistoryRef.current) return;
|
||||
|
||||
// 只处理新消息
|
||||
messages.forEach(async (message, messageIndex) => {
|
||||
if (message.role === 'assistant' && !processedMessageIds.current.has(message.id)) {
|
||||
const content = message.parts?.map((p: any) => p.text || '').join('') || '';
|
||||
let cancelled = false;
|
||||
|
||||
const processNewAssistantMessages = async () => {
|
||||
const existingImageUrlKeys = new Set(
|
||||
nodes
|
||||
.filter((n) => n.type === "image" && (n.data as any)?.src)
|
||||
.map((n) => normalizeMediaUrl((n.data as any).src))
|
||||
.filter(Boolean)
|
||||
);
|
||||
const existingVideoUrlKeys = new Set(
|
||||
nodes
|
||||
.filter((n) => n.type === "video" && (n.data as any)?.src)
|
||||
.map((n) => normalizeMediaUrl((n.data as any).src))
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) {
|
||||
if (cancelled) return;
|
||||
const message = messages[messageIndex];
|
||||
if (message.role !== "assistant" || processedMessageIds.current.has(message.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = message.parts?.map((p: any) => p.text || "").join("") || "";
|
||||
const imageUrls = extractImageUrls(content);
|
||||
const videoUrls = extractVideoUrls(content);
|
||||
if (imageUrls.length === 0 && videoUrls.length === 0) continue;
|
||||
|
||||
processedMessageIds.current.add(message.id);
|
||||
const generationPrompt = findNearestUserPrompt(messages, messageIndex);
|
||||
|
||||
if (imageUrls.length > 0 || videoUrls.length > 0) {
|
||||
processedMessageIds.current.add(message.id);
|
||||
let addedImageCount = 0;
|
||||
let addedVideoCount = 0;
|
||||
|
||||
let addedImageCount = 0;
|
||||
let addedVideoCount = 0;
|
||||
const newImageUrls = imageUrls.filter((url) => {
|
||||
const key = normalizeMediaUrl(url);
|
||||
if (!key || existingImageUrlKeys.has(key)) return false;
|
||||
existingImageUrlKeys.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (imageUrls.length > 0) {
|
||||
// 检查画布中是否已存在相同 URL 的图片
|
||||
const existingUrls = new Set(
|
||||
nodes.filter(n => n.type === 'image' && (n.data as any)?.src)
|
||||
.map(n => (n.data as any).src)
|
||||
);
|
||||
|
||||
// 只添加不存在的图片
|
||||
const newUrls = imageUrls.filter(url => !existingUrls.has(url));
|
||||
|
||||
// 异步获取每张图片的尺寸并添加到画布
|
||||
for (let index = 0; index < newUrls.length; index++) {
|
||||
const url = newUrls[index];
|
||||
// 获取图片原始尺寸
|
||||
const dimensions = await getImageDimensions(url);
|
||||
|
||||
// 计算位置(避免重叠,根据图片宽度动态调整间距)
|
||||
const existingNodes = nodes.length + index;
|
||||
const spacing = 20; // 图片间距
|
||||
const x = 100 + (existingNodes % 3) * (dimensions.width + spacing);
|
||||
const y = 100 + Math.floor(existingNodes / 3) * (dimensions.height + spacing);
|
||||
|
||||
const nodeId = addNode({
|
||||
type: 'image',
|
||||
position: { x, y },
|
||||
size: { width: dimensions.width, height: dimensions.height },
|
||||
data: {
|
||||
src: url,
|
||||
status: 'completed',
|
||||
title: '图片',
|
||||
prompt: generationPrompt || '对话生成',
|
||||
metadata: {
|
||||
originalWidth: dimensions.originalWidth,
|
||||
originalHeight: dimensions.originalHeight,
|
||||
generationPrompt: generationPrompt || undefined,
|
||||
sourceMessageId: message.id,
|
||||
},
|
||||
for (let index = 0; index < newImageUrls.length; index += 1) {
|
||||
if (cancelled) return;
|
||||
const url = newImageUrls[index];
|
||||
const dimensions = await getImageDimensions(url);
|
||||
const existingNodes = nodes.length + addedImageCount + addedVideoCount;
|
||||
const spacing = 20;
|
||||
const x = 100 + (existingNodes % 3) * (dimensions.width + spacing);
|
||||
const y = 100 + Math.floor(existingNodes / 3) * (dimensions.height + spacing);
|
||||
|
||||
const nodeId = addNode({
|
||||
type: "image",
|
||||
position: { x, y },
|
||||
size: { width: dimensions.width, height: dimensions.height },
|
||||
data: {
|
||||
src: url,
|
||||
status: "completed",
|
||||
title: "图片",
|
||||
prompt: generationPrompt || "对话生成",
|
||||
metadata: {
|
||||
originalWidth: dimensions.originalWidth,
|
||||
originalHeight: dimensions.originalHeight,
|
||||
generationPrompt: generationPrompt || undefined,
|
||||
sourceMessageId: message.id,
|
||||
},
|
||||
connections: [],
|
||||
});
|
||||
void autoNameGeneratedImageNode(nodeId, url);
|
||||
addedImageCount += 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
connections: [],
|
||||
});
|
||||
void autoNameGeneratedImageNode(nodeId, url);
|
||||
addedImageCount += 1;
|
||||
}
|
||||
|
||||
if (videoUrls.length > 0) {
|
||||
const existingVideoUrls = new Set(
|
||||
nodes.filter(n => n.type === 'video' && (n.data as any)?.src)
|
||||
.map(n => (n.data as any).src)
|
||||
);
|
||||
const newVideoUrls = videoUrls.filter(url => !existingVideoUrls.has(url));
|
||||
const newVideoUrls = videoUrls.filter((url) => {
|
||||
const key = normalizeMediaUrl(url);
|
||||
if (!key || existingVideoUrlKeys.has(key)) return false;
|
||||
existingVideoUrlKeys.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
for (let index = 0; index < newVideoUrls.length; index++) {
|
||||
const url = newVideoUrls[index];
|
||||
const existingNodes = nodes.length + index;
|
||||
const x = 120 + (existingNodes % 2) * 420;
|
||||
const y = 120 + Math.floor(existingNodes / 2) * 260;
|
||||
addNode({
|
||||
type: "video",
|
||||
position: { x, y },
|
||||
size: { width: 380, height: 220 },
|
||||
data: {
|
||||
src: url,
|
||||
status: "completed",
|
||||
prompt: generationPrompt || "对话生成",
|
||||
},
|
||||
connections: [],
|
||||
});
|
||||
addedVideoCount += 1;
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < newVideoUrls.length; index += 1) {
|
||||
if (cancelled) return;
|
||||
const url = newVideoUrls[index];
|
||||
const existingNodes = nodes.length + addedImageCount + addedVideoCount;
|
||||
const x = 120 + (existingNodes % 2) * 420;
|
||||
const y = 120 + Math.floor(existingNodes / 2) * 260;
|
||||
addNode({
|
||||
type: "video",
|
||||
position: { x, y },
|
||||
size: { width: 380, height: 220 },
|
||||
data: {
|
||||
src: url,
|
||||
status: "completed",
|
||||
prompt: generationPrompt || "对话生成",
|
||||
},
|
||||
connections: [],
|
||||
});
|
||||
addedVideoCount += 1;
|
||||
}
|
||||
|
||||
if (addedImageCount > 0 || addedVideoCount > 0) {
|
||||
const notice = [
|
||||
addedImageCount > 0 ? `${addedImageCount} 张图片` : "",
|
||||
addedVideoCount > 0 ? `${addedVideoCount} 条视频` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("、");
|
||||
toast.success(`已将 ${notice} 添加到画布`);
|
||||
}
|
||||
if (addedImageCount > 0 || addedVideoCount > 0) {
|
||||
const notice = [
|
||||
addedImageCount > 0 ? `${addedImageCount} 张图片` : "",
|
||||
addedVideoCount > 0 ? `${addedVideoCount} 条视频` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("、");
|
||||
toast.success(`已将 ${notice} 添加到画布`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [messages, addNode, nodes]);
|
||||
};
|
||||
|
||||
void processNewAssistantMessages();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [messages, addNode, nodes, autoNameGeneratedImageNode]);
|
||||
|
||||
// 为历史节点补全可编辑的原始提示词(旧数据里可能只有“对话生成”占位文案)
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user