diff --git a/src/components/nodes/GenerateAudioNode.tsx b/src/components/nodes/GenerateAudioNode.tsx
index ea2d22bb..7f1e3afa 100644
--- a/src/components/nodes/GenerateAudioNode.tsx
+++ b/src/components/nodes/GenerateAudioNode.tsx
@@ -1,7 +1,7 @@
"use client";
import React, { useCallback, useState, useEffect, useMemo } from "react";
-import { CloudUploadOutlined } from "@ant-design/icons";
+import { CloudUploadOutlined, DeleteOutlined } from "@ant-design/icons";
import { Position, NodeProps, Node, useReactFlow } from "@xyflow/react";
import { BaseNode } from "./BaseNode";
import { NodeHandle } from "./NodeHandle";
@@ -44,10 +44,13 @@ export function GenerateAudioNodeView({
id,
data,
selected,
+ suppressEmptyStateDelete = false,
}: {
id: string;
data: GenerateAudioNodeData;
selected?: boolean;
+ /** Suppress the empty-state delete capsule (e.g. when a wrapper already renders an upload toolbar). */
+ suppressEmptyStateDelete?: boolean;
}) {
const { t } = useI18n();
const nodeData = data;
@@ -198,6 +201,7 @@ export function GenerateAudioNodeView({
);
const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
+ const removeNode = useWorkflowStore((state) => state.removeNode);
const isRunning = useWorkflowStore((state) => state.runningNodeIds.has(id));
const handleRegenerate = useCallback(() => {
@@ -343,6 +347,20 @@ export function GenerateAudioNodeView({
const selectedNodeCount = useSelectedNodeCount();
const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && nodeData.outputAudio);
+ const showEmptyStateDelete = Boolean(selected && selectedNodeCount === 1 && !nodeData.outputAudio && !suppressEmptyStateDelete);
+ const emptyStateActions: NodeActionCapsuleAction[] = [
+ {
+ key: "delete",
+ icon: ,
+ label: t("audioInput.remove"),
+ tone: "danger",
+ onClick: (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ removeNode(id);
+ },
+ },
+ ];
const selectedActions: NodeActionCapsuleAction[] = nodeData.outputAudio
? [
{
@@ -421,6 +439,7 @@ export function GenerateAudioNodeView({
) : undefined}
>
{showSelectedActions && }
+ {showEmptyStateDelete && }
{nodeData.outputAudio ? (
diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx
index f7bb4f14..6ffb0a84 100644
--- a/src/components/nodes/GenerateImageNode.tsx
+++ b/src/components/nodes/GenerateImageNode.tsx
@@ -91,9 +91,11 @@ export interface GenerateImageNodeViewProps {
data: NanoBananaNodeData;
selected?: boolean;
renderInputHandle?: boolean;
+ /** Suppress the empty-state delete capsule (e.g. when a wrapper already renders an upload toolbar). */
+ suppressEmptyStateDelete?: boolean;
}
-export function GenerateImageNodeView({ id, data, selected, renderInputHandle = true }: GenerateImageNodeViewProps) {
+export function GenerateImageNodeView({ id, data, selected, renderInputHandle = true, suppressEmptyStateDelete = false }: GenerateImageNodeViewProps) {
const nodeData = data;
const { t } = useI18n();
const storedImage = (data as NanoBananaNodeData & { image?: string | null; previewImage?: string | null }).image ?? null;
@@ -173,6 +175,7 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle =
}, [id, modelOptions, nodeConfig.selectedModel, nodeData, updateImagePreference, updateNodeData]);
const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
+ const removeNode = useWorkflowStore((state) => state.removeNode);
const isRunning = useWorkflowStore((state) => state.runningNodeIds.has(id));
const handleRegenerate = useCallback(() => {
@@ -412,6 +415,24 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle =
const downloadImage = nodeData.outputImage || editableImage;
const overlayImageDimensions = cropImageDimensions || asImageDimensions(nodeData.dimensions);
const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && editableImage);
+ const showEmptyStateDelete = Boolean(selected && selectedNodeCount === 1 && !editableImage && !activeNodeTool && !suppressEmptyStateDelete);
+ const emptyStateActions: NodeActionCapsuleAction[] = [
+ {
+ key: "delete",
+ icon: (
+
+ ),
+ label: t("imageInput.remove"),
+ tone: "danger",
+ onClick: (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ removeNode(id);
+ },
+ },
+ ];
const activeImageEditTool: ImageEditTool | null =
activeNodeTool === "crop" ||
activeNodeTool === "outpainting" ||
@@ -597,6 +618,7 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle =
/>
{showSelectedActions && !activeNodeTool &&
}
+ {showEmptyStateDelete &&
}
state.updateNodeData);
@@ -66,6 +68,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV
const getNodeById = useWorkflowStore((state) => state.getNodeById);
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode);
const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
+ const removeNode = useWorkflowStore((state) => state.removeNode);
useEffect(() => {
setVideoRetryNonce(0);
@@ -344,6 +347,24 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV
const selectedNodeCount = useSelectedNodeCount();
const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && nodeData.outputVideo);
+ const showEmptyStateDelete = Boolean(selected && selectedNodeCount === 1 && !nodeData.outputVideo && !suppressEmptyStateDelete);
+ const emptyStateActions: NodeActionCapsuleAction[] = [
+ {
+ key: "delete",
+ icon: (
+
+ ),
+ label: t("videoInput.remove"),
+ tone: "danger",
+ onClick: (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ removeNode(id);
+ },
+ },
+ ];
const selectedActions: NodeActionCapsuleAction[] = nodeData.outputVideo
? [
{
@@ -452,6 +473,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV
{/* Output label */}
{showSelectedActions &&
}
+ {showEmptyStateDelete &&
}
state.updateNodeData);
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode);
+ const removeNode = useWorkflowStore((state) => state.removeNode);
const fileInputRef = useRef
(null);
const selectedNodeCount = useSelectedNodeCount();
const showSelectedActions = Boolean(selected && selectedNodeCount === 1);
@@ -138,6 +139,19 @@ function SmartAudioUploadToolbar({
handleOpenFileDialog();
},
},
+ {
+ key: "delete",
+ icon: ,
+ label: t("audioInput.remove"),
+ tone: "danger",
+ disabled: isUploading,
+ onClick: (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (isUploading) return;
+ removeNode(id);
+ },
+ },
];
return (
@@ -172,7 +186,7 @@ function SmartAudioEmptyView({
return (
-
+
;
const IMAGE_DIMENSION_TIMEOUT_MS = 10_000;
-function SmartImageToolbarIcon({ kind }: { kind: "upload" | "assets" }) {
+function SmartImageToolbarIcon({ kind }: { kind: "upload" | "assets" | "delete" }) {
if (kind === "upload") {
return (