feat(canvas): implement retry and delete actions for failed multi-image slots and maintain node scale during primary image selection

This commit is contained in:
HouYunFei
2026-08-06 12:09:30 +08:00
parent a10f16d983
commit ae327e9315
5 changed files with 84 additions and 28 deletions
+42 -25
View File
@@ -1461,7 +1461,8 @@ function InfiniteCanvasPage() {
if (node.id !== nodeId) return node;
const image = node.metadata?.images?.find((item) => item.id === imageId);
if (!image?.content) return node;
const size = fitNodeSize(image.naturalWidth, image.naturalHeight);
const edge = Math.max(node.width, node.height);
const size = node.metadata?.freeResize ? { width: node.width, height: node.height } : fitNodeSize(image.naturalWidth, image.naturalHeight, edge, edge);
return {
...node,
position: { x: node.position.x + node.width / 2 - size.width / 2, y: node.position.y + node.height / 2 - size.height / 2 },
@@ -2364,7 +2365,7 @@ function InfiniteCanvasPage() {
}, [handleGenerateNode]);
const handleRetryNode = useCallback(
async (node: CanvasNodeData) => {
async (node: CanvasNodeData, imageId?: string) => {
const sourceNode = findRetrySourceNode(node.id, nodesRef.current, connectionsRef.current) || node;
const savedImageMetadata = node.type === CanvasNodeType.Image ? node.metadata : undefined;
const hasSavedImageMetadata = Boolean(savedImageMetadata?.generationType);
@@ -2396,13 +2397,13 @@ function InfiniteCanvasPage() {
hasSavedImageMetadata && savedImageMetadata ? await resolveMetadataReferences(savedImageMetadata) : useReferenceImages ? (context?.referenceImages.length ? context.referenceImages : sourceNodeReferenceImages(sourceNode)) : [];
if (useReferenceImages && !retryReferenceImages) {
message.error(t("canvas.projectPage.referenceMissing"));
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails: t("canvas.projectPage.referenceMissing") } } : item)));
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: item.metadata?.content ? NODE_STATUS_SUCCESS : NODE_STATUS_ERROR, errorDetails: item.metadata?.content ? undefined : t("canvas.projectPage.referenceMissing"), images: item.metadata?.images?.map((image) => (image.id === imageId ? { ...image, status: NODE_STATUS_ERROR, errorDetails: t("canvas.projectPage.referenceMissing") } : image)) } } : item)));
return;
}
const retryImages = retryReferenceImages || [];
setRunningNodeId(node.id);
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_LOADING, errorDetails: undefined } } : item)));
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_LOADING, errorDetails: undefined, images: item.metadata?.images?.map((image) => (image.id === imageId ? { ...image, status: NODE_STATUS_LOADING, errorDetails: undefined } : image)) } } : item)));
const controller = startGenerationRequest(node.id, sourceNode.id, node.id);
try {
@@ -2460,9 +2461,8 @@ function InfiniteCanvasPage() {
: await requestGeneration(generationConfig, prompt, { signal: controller.signal }).then((items) => items[0]);
const uploadedImage = await uploadImage(image.dataUrl);
const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
const imageSize = fitNodeSize(uploadedImage.width, uploadedImage.height, imageConfig.width, imageConfig.height);
const retryImage: CanvasNodeImage = {
id: node.metadata?.primaryImageId || nanoid(),
id: imageId || node.metadata?.primaryImageId || nanoid(),
status: NODE_STATUS_SUCCESS,
content: uploadedImage.url,
storageKey: uploadedImage.storageKey,
@@ -2483,30 +2483,31 @@ function InfiniteCanvasPage() {
}
: buildImageGenerationMetadata(useReferenceImages ? "edit" : "generation", generationConfig, 1, retryImages);
setNodes((prev) =>
prev.map((item) =>
item.id === node.id
? {
...item,
type: CanvasNodeType.Image,
width: imageSize.width,
height: imageSize.height,
metadata: {
...item.metadata,
...imageMetadata(uploadedImage),
images: item.metadata?.images?.map((current) => (current.id === retryImage.id ? retryImage : current)),
primaryImageId: retryImage.id,
prompt,
...generationMetadata,
},
}
: item,
),
prev.map((item) => {
if (item.id !== node.id) return item;
const makePrimary = !imageId || !item.metadata?.content;
const edge = imageId ? Math.max(item.width, item.height) : 0;
const imageSize = imageId && item.metadata?.freeResize ? { width: item.width, height: item.height } : imageId ? fitNodeSize(uploadedImage.width, uploadedImage.height, edge, edge) : fitNodeSize(uploadedImage.width, uploadedImage.height, imageConfig.width, imageConfig.height);
return {
...item,
type: CanvasNodeType.Image,
...(makePrimary ? { width: imageSize.width, height: imageSize.height, ...(imageId ? { position: { x: item.position.x + item.width / 2 - imageSize.width / 2, y: item.position.y + item.height / 2 - imageSize.height / 2 } } : {}) } : {}),
metadata: {
...item.metadata,
...(makePrimary ? imageMetadata(uploadedImage) : { status: NODE_STATUS_SUCCESS }),
images: item.metadata?.images?.map((current) => (current.id === retryImage.id ? retryImage : current)),
primaryImageId: makePrimary ? retryImage.id : item.metadata?.primaryImageId,
prompt,
...generationMetadata,
},
};
}),
);
} catch (error) {
if (isGenerationCanceled(error)) return;
const errorDetails = error instanceof Error ? error.message : t("canvas.projectPage.generationFailed");
message.error(errorDetails);
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails } } : item)));
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: item.metadata?.content ? NODE_STATUS_SUCCESS : NODE_STATUS_ERROR, errorDetails: item.metadata?.content ? undefined : errorDetails, images: item.metadata?.images?.map((image) => (image.id === imageId ? { ...image, status: NODE_STATUS_ERROR, errorDetails } : image)) } } : item)));
} finally {
finishGenerationRequest(node.id, controller);
setRunningNodeId(null);
@@ -2515,6 +2516,20 @@ function InfiniteCanvasPage() {
[effectiveConfig, finishGenerationRequest, isAiConfigReady, message, openConfigDialog, startGenerationRequest, t],
);
const deleteBatchImage = useCallback((nodeId: string, imageId: string) => {
const node = nodesRef.current.find((item) => item.id === nodeId);
if ((node?.metadata?.images?.length || 0) <= 2) setExpandedImageNodeId(null);
setNodes((prev) =>
prev.map((item) => {
if (item.id !== nodeId) return item;
const images = item.metadata?.images?.filter((image) => image.id !== imageId) || [];
return { ...item, metadata: { ...item.metadata, images, count: images.length, primaryImageId: item.metadata?.primaryImageId === imageId ? images[0]?.id : item.metadata?.primaryImageId } };
}),
);
}, []);
const retryBatchImage = useCallback((node: CanvasNodeData, imageId: string) => void handleRetryNode(node, imageId), [handleRetryNode]);
const generateImageFromTextNode = useCallback(
(node: CanvasNodeData) => {
const prompt = (node.metadata?.content || node.metadata?.prompt || "").trim();
@@ -2799,6 +2814,8 @@ function InfiniteCanvasPage() {
onTitleChange={handleNodeTitleChange}
onToggleBatch={toggleBatchExpanded}
onSetBatchPrimary={setBatchPrimary}
onRetryBatchImage={retryBatchImage}
onDeleteBatchImage={deleteBatchImage}
onRetry={handleNodeRetry}
onGenerateImage={generateImageFromTextNode}
onViewImage={handleNodeViewImage}