fix: keep latest canvas image highlighted
This commit is contained in:
parent
b032774e8e
commit
f7d18f20db
@ -660,6 +660,30 @@ function getAnchoredGenerationPosition(params: {
|
||||
return fallbackBase;
|
||||
}
|
||||
|
||||
function getCenteredReplacementPosition(
|
||||
placeholderNode: CanvasNode,
|
||||
size: { width: number; height: number }
|
||||
) {
|
||||
const width = Math.max(1, Number(size.width || 300));
|
||||
const height = Math.max(1, Number(size.height || 300));
|
||||
const placeholderWidth = Math.max(
|
||||
1,
|
||||
Number(placeholderNode.size?.width || width)
|
||||
);
|
||||
const placeholderHeight = Math.max(
|
||||
1,
|
||||
Number(placeholderNode.size?.height || height)
|
||||
);
|
||||
|
||||
return {
|
||||
x: Number(placeholderNode.position?.x || 0) + placeholderWidth / 2 - width / 2,
|
||||
y:
|
||||
Number(placeholderNode.position?.y || 0) +
|
||||
placeholderHeight / 2 -
|
||||
height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
type CanvasImageGenerationRequest = {
|
||||
prompt: string;
|
||||
model?: string;
|
||||
@ -1603,6 +1627,29 @@ export function CanvasChatPanel({
|
||||
// 🆕 物品拾取
|
||||
const pendingPickedObjects = useCanvasStore((state) => state.pendingPickedObjects);
|
||||
const clearPendingPickedObjects = useCanvasStore((state) => state.clearPendingPickedObjects);
|
||||
|
||||
const clearLatestGeneratedImageHighlights = useCallback(() => {
|
||||
const currentNodes = useCanvasStore.getState().nodes;
|
||||
const clearedAt = Date.now();
|
||||
|
||||
currentNodes.forEach((node) => {
|
||||
if (node.type !== "image") return;
|
||||
const data = node.data as Record<string, any>;
|
||||
const metadata = (data?.metadata || {}) as Record<string, any>;
|
||||
if (metadata.latestGeneratedHighlight !== true) return;
|
||||
|
||||
updateNode(node.id, {
|
||||
data: {
|
||||
...data,
|
||||
metadata: {
|
||||
...metadata,
|
||||
latestGeneratedHighlight: false,
|
||||
latestGeneratedClearedAt: clearedAt,
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
});
|
||||
}, [updateNode]);
|
||||
|
||||
// 获取纯 UUID(去掉 canvas- 前缀)用于 API 调用
|
||||
const rawChatId = toRawCanvasChatId(activeChatId);
|
||||
@ -3659,17 +3706,21 @@ export function CanvasChatPanel({
|
||||
: selectedImageNodesRef.current,
|
||||
sourceImageUrls,
|
||||
});
|
||||
const placeholderNode = placeholderNodes[index];
|
||||
const reservedPlaceholderNode = placeholderNodes[index];
|
||||
const placeholderNode = reservedPlaceholderNode
|
||||
? currentNodes.find((node) => node.id === reservedPlaceholderNode.id) || null
|
||||
: null;
|
||||
const size = { width: dimensions.width, height: dimensions.height };
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
existingNodes: currentNodes,
|
||||
excludeNodeIds: placeholderNode ? [placeholderNode.id] : [],
|
||||
fallbackNodeCount: currentNodes.length + addedImageCount + addedVideoCount,
|
||||
index,
|
||||
size,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
});
|
||||
const position = placeholderNode
|
||||
? getCenteredReplacementPosition(placeholderNode, size)
|
||||
: getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
existingNodes: currentNodes,
|
||||
fallbackNodeCount: currentNodes.length + addedImageCount + addedVideoCount,
|
||||
index,
|
||||
size,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
});
|
||||
const data = {
|
||||
src: url,
|
||||
status: "completed" as const,
|
||||
@ -3691,6 +3742,7 @@ export function CanvasChatPanel({
|
||||
sourceNodeId: sourceNodeId || anchorNode?.id || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
recentlyGeneratedAt: completedAt,
|
||||
latestGeneratedHighlight: true,
|
||||
},
|
||||
};
|
||||
const nodeId = placeholderNode
|
||||
@ -3857,7 +3909,15 @@ export function CanvasChatPanel({
|
||||
typeof dataPart.data === "string" ? JSON.parse(dataPart.data) : dataPart.data;
|
||||
if (!payload || typeof payload !== "object") return;
|
||||
if (payload.phase === "completed") {
|
||||
clearStreamImageGenerationStateForChat(eventChatId);
|
||||
setStreamImageGenerationStateForChat(eventChatId, {
|
||||
phase: "finalizing",
|
||||
providerType: payload.providerType,
|
||||
modelName: getAgentSafeModelName(payload.modelName),
|
||||
imageCount: Number(payload.imageCount || 0) || undefined,
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
clearStreamImageGenerationStateForChat(eventChatId);
|
||||
}, 30_000);
|
||||
return;
|
||||
}
|
||||
setStreamImageGenerationStateForChat(eventChatId, {
|
||||
@ -3968,8 +4028,9 @@ export function CanvasChatPanel({
|
||||
]);
|
||||
});
|
||||
}
|
||||
void appendStreamMediaToCanvas(payload);
|
||||
clearStreamImageGenerationStateForChat(eventChatId);
|
||||
void appendStreamMediaToCanvas(payload).finally(() => {
|
||||
clearStreamImageGenerationStateForChat(eventChatId);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[CanvasChat] Failed to parse data-canvasMedia:", e);
|
||||
}
|
||||
@ -4082,7 +4143,13 @@ export function CanvasChatPanel({
|
||||
Math.max(1, Number(imageParamsRef.current?.imageCount || 1)),
|
||||
});
|
||||
}
|
||||
if (hasImageGenEndMarker || incomingImageUrls.length > 0 || incomingVideoUrls.length > 0) {
|
||||
const shouldDeferClearForActiveMedia =
|
||||
isEventForActiveChat &&
|
||||
(incomingImageUrls.length > 0 || incomingVideoUrls.length > 0);
|
||||
if (
|
||||
(hasImageGenEndMarker || incomingImageUrls.length > 0 || incomingVideoUrls.length > 0) &&
|
||||
!shouldDeferClearForActiveMedia
|
||||
) {
|
||||
clearStreamImageGenerationStateForChat(eventChatId);
|
||||
}
|
||||
|
||||
@ -4651,6 +4718,15 @@ export function CanvasChatPanel({
|
||||
});
|
||||
|
||||
if (hasCompletedMedia) {
|
||||
const hasActivePlaceholder = useCanvasStore.getState().nodes.some((node) => {
|
||||
if (node.type !== "image") return false;
|
||||
const metadata = ((node.data as any)?.metadata || {}) as Record<string, unknown>;
|
||||
return (
|
||||
metadata.canvasGenerationPlaceholder === true &&
|
||||
(!metadata.chatId || String(metadata.chatId) === activeChatId)
|
||||
);
|
||||
});
|
||||
if (hasActivePlaceholder) return;
|
||||
clearStreamImageGenerationStateForChat(activeChatId);
|
||||
}
|
||||
}, [
|
||||
@ -4856,6 +4932,14 @@ export function CanvasChatPanel({
|
||||
let addedVideoCount = 0;
|
||||
const insertedNodeIds: string[] = [];
|
||||
const completedAt = Date.now();
|
||||
const messagePlaceholderNodes = useCanvasStore.getState().nodes.filter((node) => {
|
||||
if (node.type !== "image") return false;
|
||||
const metadata = ((node.data as any)?.metadata || {}) as Record<string, unknown>;
|
||||
return (
|
||||
metadata.canvasGenerationPlaceholder === true &&
|
||||
(!metadata.chatId || String(metadata.chatId) === activeChatId)
|
||||
);
|
||||
});
|
||||
|
||||
const newImageEntries = imageUrls.reduce<Array<{ url: string; sourceImageKey: string }>>(
|
||||
(acc, url) => {
|
||||
@ -4881,6 +4965,11 @@ export function CanvasChatPanel({
|
||||
[]
|
||||
);
|
||||
|
||||
if (newImageEntries.length === 0 && imageUrls.length > 0 && messagePlaceholderNodes.length > 0) {
|
||||
removeCanvasGenerationPlaceholders(activeChatId);
|
||||
clearStreamImageGenerationStateForChat(activeChatId);
|
||||
}
|
||||
|
||||
for (let index = 0; index < newImageEntries.length; index += 1) {
|
||||
if (cancelled) return;
|
||||
const imageEntry = newImageEntries[index];
|
||||
@ -4937,7 +5026,13 @@ export function CanvasChatPanel({
|
||||
: selectedImageNodesRef.current,
|
||||
sourceImageUrls: nearestUserImageUrls,
|
||||
});
|
||||
const size = anchorNode
|
||||
const reservedPlaceholderNode = messagePlaceholderNodes[index];
|
||||
const placeholderNode = reservedPlaceholderNode
|
||||
? currentNodes.find((node) => node.id === reservedPlaceholderNode.id) || null
|
||||
: null;
|
||||
const size = placeholderNode
|
||||
? { width: dimensions.width, height: dimensions.height }
|
||||
: anchorNode
|
||||
? {
|
||||
width: Math.max(
|
||||
1,
|
||||
@ -4949,39 +5044,58 @@ export function CanvasChatPanel({
|
||||
),
|
||||
}
|
||||
: { width: dimensions.width, height: dimensions.height };
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
existingNodes: currentNodes,
|
||||
fallbackNodeCount: currentNodes.length + addedImageCount + addedVideoCount,
|
||||
index,
|
||||
size,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
});
|
||||
const position = placeholderNode
|
||||
? getCenteredReplacementPosition(placeholderNode, size)
|
||||
: getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
existingNodes: currentNodes,
|
||||
fallbackNodeCount: currentNodes.length + addedImageCount + addedVideoCount,
|
||||
index,
|
||||
size,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
});
|
||||
|
||||
const nodeId = addNode({
|
||||
type: "image",
|
||||
position,
|
||||
size,
|
||||
data: {
|
||||
src: url,
|
||||
status: "completed",
|
||||
title: "图片",
|
||||
prompt: generationPrompt || "对话生成",
|
||||
originalSrc: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
metadata: {
|
||||
originalWidth: dimensions.originalWidth,
|
||||
originalHeight: dimensions.originalHeight,
|
||||
generationPrompt: generationPrompt || undefined,
|
||||
sourceMessageId: message.id,
|
||||
sourceImageKey: imageEntry.sourceImageKey,
|
||||
compareSourceUrl: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
sourceNodeId: sourceNodeId || anchorNode?.id || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
recentlyGeneratedAt: completedAt,
|
||||
},
|
||||
const data = {
|
||||
src: url,
|
||||
status: "completed" as const,
|
||||
title: "图片",
|
||||
prompt: generationPrompt || "对话生成",
|
||||
originalSrc: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
metadata: {
|
||||
originalWidth: dimensions.originalWidth,
|
||||
originalHeight: dimensions.originalHeight,
|
||||
generationPrompt: generationPrompt || undefined,
|
||||
sourceMessageId: message.id,
|
||||
sourceImageKey: imageEntry.sourceImageKey,
|
||||
compareSourceUrl: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
sourceNodeId: sourceNodeId || anchorNode?.id || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
recentlyGeneratedAt: completedAt,
|
||||
latestGeneratedHighlight: true,
|
||||
},
|
||||
connections: sourceNodeId ? [sourceNodeId] : anchorNode?.id ? [anchorNode.id] : [],
|
||||
});
|
||||
};
|
||||
const connections = sourceNodeId
|
||||
? [sourceNodeId]
|
||||
: anchorNode?.id
|
||||
? [anchorNode.id]
|
||||
: [];
|
||||
const nodeId = placeholderNode
|
||||
? placeholderNode.id
|
||||
: addNode({
|
||||
type: "image",
|
||||
position,
|
||||
size,
|
||||
data,
|
||||
connections,
|
||||
});
|
||||
if (placeholderNode) {
|
||||
updateNode(nodeId, {
|
||||
position,
|
||||
size,
|
||||
data,
|
||||
connections,
|
||||
});
|
||||
}
|
||||
insertedNodeIds.push(nodeId);
|
||||
void autoNameGeneratedImageNode(nodeId, url);
|
||||
addedImageCount += 1;
|
||||
@ -4995,6 +5109,7 @@ export function CanvasChatPanel({
|
||||
if (focusNode) {
|
||||
focusCanvasOnNode(focusNode);
|
||||
}
|
||||
clearStreamImageGenerationStateForChat(activeChatId);
|
||||
}
|
||||
|
||||
const newVideoUrls = videoUrls.filter((url) => {
|
||||
@ -5037,8 +5152,10 @@ export function CanvasChatPanel({
|
||||
addNode,
|
||||
nodes,
|
||||
autoNameGeneratedImageNode,
|
||||
clearStreamImageGenerationStateForChat,
|
||||
focusCanvasOnNode,
|
||||
selectNodes,
|
||||
updateNode,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -7503,6 +7620,7 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
if (isDetailGenerationMode && canUseDetailGeneration) {
|
||||
clearLatestGeneratedImageHighlights();
|
||||
setSendGuard(activeChatId, {
|
||||
inFlight: true,
|
||||
signature: `detail:${activeChatId}:${Date.now()}`,
|
||||
@ -7723,25 +7841,26 @@ export function CanvasChatPanel({
|
||||
return;
|
||||
}
|
||||
|
||||
setSendGuard(activeChatId, {
|
||||
inFlight: true,
|
||||
signature: sendSignature,
|
||||
timestamp: Date.now(),
|
||||
clearLatestGeneratedImageHighlights();
|
||||
setSendGuard(activeChatId, {
|
||||
inFlight: true,
|
||||
signature: sendSignature,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
inFlightChatIdRef.current = activeChatId;
|
||||
inFlightRawChatIdRef.current = rawChatId;
|
||||
clearStreamImageGenerationStateForChat(activeChatId);
|
||||
clearAgentProgressStateForChat(activeChatId);
|
||||
if (shouldShowOptimisticImageGeneration) {
|
||||
const imageParamSnapshot = imageParamsRef.current || imageParams;
|
||||
setStreamImageGenerationStateForChat(activeChatId, {
|
||||
phase: "processing",
|
||||
providerType: requestModel?.providerType,
|
||||
modelName: getAgentSafeModelName(requestModel?.displayName),
|
||||
imageCount:
|
||||
Math.max(1, Math.round(Number(imageParamSnapshot?.imageCount || 1))) || 1,
|
||||
});
|
||||
inFlightChatIdRef.current = activeChatId;
|
||||
inFlightRawChatIdRef.current = rawChatId;
|
||||
clearStreamImageGenerationStateForChat(activeChatId);
|
||||
clearAgentProgressStateForChat(activeChatId);
|
||||
if (shouldShowOptimisticImageGeneration) {
|
||||
const imageParamSnapshot = imageParamsRef.current || imageParams;
|
||||
setStreamImageGenerationStateForChat(activeChatId, {
|
||||
phase: "processing",
|
||||
providerType: requestModel?.providerType,
|
||||
modelName: getAgentSafeModelName(requestModel?.displayName),
|
||||
imageCount:
|
||||
Math.max(1, Math.round(Number(imageParamSnapshot?.imageCount || 1))) || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 使用与 multimodal-input 相同的格式发送消息
|
||||
try {
|
||||
@ -7793,6 +7912,7 @@ export function CanvasChatPanel({
|
||||
selectedModelId,
|
||||
sendMessage,
|
||||
clearSelection,
|
||||
clearLatestGeneratedImageHighlights,
|
||||
buildAutoContext,
|
||||
buildContextPayload,
|
||||
allModels,
|
||||
|
||||
@ -553,6 +553,30 @@ export function CanvasWorkspace({
|
||||
);
|
||||
const clearLastAddedNodeId = useCanvasStore((state) => state.clearLastAddedNodeId);
|
||||
const centerTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
const clearLatestGeneratedImageHighlights = useCallback(() => {
|
||||
const currentNodes = useCanvasStore.getState().nodes;
|
||||
const clearedAt = Date.now();
|
||||
|
||||
currentNodes.forEach((node) => {
|
||||
if (node.type !== "image") return;
|
||||
const data = node.data as Record<string, any>;
|
||||
const metadata = (data?.metadata || {}) as Record<string, any>;
|
||||
if (metadata.latestGeneratedHighlight !== true) return;
|
||||
|
||||
updateNode(node.id, {
|
||||
data: {
|
||||
...data,
|
||||
metadata: {
|
||||
...metadata,
|
||||
latestGeneratedHighlight: false,
|
||||
latestGeneratedClearedAt: clearedAt,
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
});
|
||||
}, [updateNode]);
|
||||
|
||||
const isMacPlatform = React.useMemo(() => {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
||||
@ -746,6 +770,7 @@ export function CanvasWorkspace({
|
||||
y: event.clientY - rect.top,
|
||||
};
|
||||
|
||||
clearLatestGeneratedImageHighlights();
|
||||
onBlankCanvasDoubleClick({
|
||||
screenPosition: {
|
||||
x: event.clientX,
|
||||
@ -766,6 +791,7 @@ export function CanvasWorkspace({
|
||||
viewport.x,
|
||||
viewport.y,
|
||||
viewport.zoom,
|
||||
clearLatestGeneratedImageHighlights,
|
||||
]
|
||||
);
|
||||
|
||||
@ -925,6 +951,7 @@ export function CanvasWorkspace({
|
||||
suppressCanvasClickRef.current = false;
|
||||
return;
|
||||
}
|
||||
clearLatestGeneratedImageHighlights();
|
||||
setSelectedEdgeId(null);
|
||||
if (!isPointOnNodeElement(e.target) && !isMarqueeSelecting) {
|
||||
const now = Date.now();
|
||||
@ -935,7 +962,7 @@ export function CanvasWorkspace({
|
||||
fitAllImageNodesIntoViewport();
|
||||
}
|
||||
}
|
||||
}, [clearSelection, fitAllImageNodesIntoViewport, hasBlockingDialogOpen, isEventFromCanvasContainer, isMarqueeSelecting, isPointOnNodeElement]);
|
||||
}, [clearLatestGeneratedImageHighlights, clearSelection, fitAllImageNodesIntoViewport, hasBlockingDialogOpen, isEventFromCanvasContainer, isMarqueeSelecting, isPointOnNodeElement]);
|
||||
|
||||
// 处理画布平移开始
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
@ -944,6 +971,7 @@ export function CanvasWorkspace({
|
||||
if (connectionDraft) {
|
||||
return;
|
||||
}
|
||||
clearLatestGeneratedImageHighlights();
|
||||
const shouldPanBlankWorkflowCanvas =
|
||||
isWorkflowCanvas &&
|
||||
tool === "select" &&
|
||||
@ -983,7 +1011,7 @@ export function CanvasWorkspace({
|
||||
setMarqueeStart(start);
|
||||
setMarqueeEnd(start);
|
||||
}
|
||||
}, [cancelPanPreview, clearBrowserTextSelection, connectionDraft, hasBlockingDialogOpen, isEventFromCanvasContainer, isPointOnNodeElement, isWorkflowCanvas, toCanvasPosition, tool]);
|
||||
}, [cancelPanPreview, clearBrowserTextSelection, clearLatestGeneratedImageHighlights, connectionDraft, hasBlockingDialogOpen, isEventFromCanvasContainer, isPointOnNodeElement, isWorkflowCanvas, toCanvasPosition, tool]);
|
||||
|
||||
// 处理画布平移
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||
@ -1152,6 +1180,7 @@ export function CanvasWorkspace({
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
if (hasBlockingDialogOpen()) return;
|
||||
if (!isEventFromCanvasContainer(e.target)) return;
|
||||
clearLatestGeneratedImageHighlights();
|
||||
if (e.cancelable) {
|
||||
e.preventDefault();
|
||||
}
|
||||
@ -1180,7 +1209,7 @@ export function CanvasWorkspace({
|
||||
const newX = mouseX - (mouseX - currentViewport.x) * scaleChange;
|
||||
const newY = mouseY - (mouseY - currentViewport.y) * scaleChange;
|
||||
scheduleViewport({ ...currentViewport, x: newX, y: newY, zoom: newZoom });
|
||||
}, [hasBlockingDialogOpen, isEventFromCanvasContainer, isMacPlatform, scheduleViewport]);
|
||||
}, [clearLatestGeneratedImageHighlights, hasBlockingDialogOpen, isEventFromCanvasContainer, isMacPlatform, scheduleViewport]);
|
||||
|
||||
const getImageDimensionsFromFile = useCallback((file: File): Promise<{
|
||||
width: number;
|
||||
@ -1221,6 +1250,7 @@ export function CanvasWorkspace({
|
||||
|
||||
const processDroppedFiles = useCallback(async (files: File[], dropClientX: number, dropClientY: number) => {
|
||||
if (files.length === 0) return;
|
||||
clearLatestGeneratedImageHighlights();
|
||||
|
||||
const rect = containerRef.current?.getBoundingClientRect();
|
||||
const baseX = rect ? (dropClientX - rect.left - viewport.x) / viewport.zoom : 100;
|
||||
@ -1312,7 +1342,7 @@ export function CanvasWorkspace({
|
||||
toast.error("图片上传失败");
|
||||
});
|
||||
}
|
||||
}, [addNode, getImageDimensionsFromFile, updateNode, viewport.x, viewport.y, viewport.zoom]);
|
||||
}, [addNode, clearLatestGeneratedImageHighlights, getImageDimensionsFromFile, updateNode, viewport.x, viewport.y, viewport.zoom]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleWindowDrop = async (e: DragEvent) => {
|
||||
@ -1574,6 +1604,7 @@ export function CanvasWorkspace({
|
||||
}}
|
||||
scale={1}
|
||||
onDragStart={() => {
|
||||
clearLatestGeneratedImageHighlights();
|
||||
setSnapGuides(null);
|
||||
if (!isSelected) {
|
||||
selectNode(node.id);
|
||||
@ -1682,6 +1713,9 @@ export function CanvasWorkspace({
|
||||
groupDragRef.current = null;
|
||||
pushHistory();
|
||||
}}
|
||||
onResizeStart={() => {
|
||||
clearLatestGeneratedImageHighlights();
|
||||
}}
|
||||
onResizeStop={(e, direction, ref, delta, position) => {
|
||||
const newWidth = parseInt(ref.style.width) / viewport.zoom;
|
||||
const newHeight = parseInt(ref.style.height) / viewport.zoom;
|
||||
@ -1705,6 +1739,7 @@ export function CanvasWorkspace({
|
||||
style={{ overflow: 'visible' }} // 允许工具栏显示在节点外部
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
clearLatestGeneratedImageHighlights();
|
||||
setSelectedEdgeId(null);
|
||||
selectNode(node.id, e.shiftKey);
|
||||
}}
|
||||
|
||||
@ -1412,10 +1412,16 @@ export function ImageNode({
|
||||
Number.isFinite(data.metadata.recentlyGeneratedAt)
|
||||
? data.metadata.recentlyGeneratedAt
|
||||
: 0;
|
||||
const isRecentlyGenerated =
|
||||
const hasPersistentGeneratedHighlight =
|
||||
data.metadata?.latestGeneratedHighlight === true;
|
||||
const hasTransientGeneratedHighlight =
|
||||
data.metadata?.latestGeneratedHighlight !== false &&
|
||||
Boolean(data.src) &&
|
||||
recentlyGeneratedAt > 0 &&
|
||||
Date.now() - recentlyGeneratedAt < 20_000;
|
||||
const isRecentlyGenerated =
|
||||
Boolean(data.src) &&
|
||||
(hasPersistentGeneratedHighlight || hasTransientGeneratedHighlight);
|
||||
const flipRotateOriginalSrc =
|
||||
data.metadata && typeof data.metadata === "object"
|
||||
? String((data.metadata as Record<string, unknown>).flipRotateOriginalSrc || "").trim()
|
||||
@ -7823,10 +7829,13 @@ export function ImageNode({
|
||||
ref={containerRef}
|
||||
>
|
||||
{isRecentlyGenerated ? (
|
||||
<div
|
||||
className="pointer-events-none absolute -inset-2 z-[3] rounded-[inherit] border border-[#8fb0ff]/55"
|
||||
style={{ animation: "canvas-generated-pulse 1.35s ease-out 0s 4" }}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 z-[3] rounded-[inherit]">
|
||||
<div
|
||||
className="absolute inset-0 rounded-[inherit] border border-[#8fb0ff]/75"
|
||||
style={{ animation: "canvas-generated-pulse 1.35s ease-out infinite" }}
|
||||
/>
|
||||
<div className="absolute inset-1 rounded-[inherit] border border-white/70 shadow-[0_0_22px_rgba(124,156,255,0.34)] dark:border-white/35" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="relative h-full w-full" style={scaledContentStyle}>
|
||||
{/* 图片内容 - 使用 object-contain 保持原始比例 */}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user