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 ( @@ -28,6 +28,14 @@ function SmartImageToolbarIcon({ kind }: { kind: "upload" | "assets" }) { ); } + if (kind === "delete") { + return ( + + + + ); + } + return ( @@ -51,6 +59,7 @@ function SmartImageUploadToolbar({ const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const updateMediaNodeData = useWorkflowStore((state) => state.updateMediaNodeData); const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); + const removeNode = useWorkflowStore((state) => state.removeNode); const fileInputRef = useRef(null); const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); const selectedNodeCount = useSelectedNodeCount(); @@ -186,6 +195,19 @@ function SmartImageUploadToolbar({ setIsAssetPickerOpen(true); }, }, + { + key: "delete", + icon: , + label: t("imageInput.remove"), + tone: "danger", + disabled: isUploading, + onClick: (event) => { + event.preventDefault(); + event.stopPropagation(); + if (isUploading) return; + removeNode(id); + }, + }, ]; const handleSelectAsset = useCallback((asset: PopiAssetItem) => { @@ -296,7 +318,7 @@ function SmartImageNeutralView({ return (
- + {!hideUploadToolbar && ( @@ -31,6 +31,14 @@ function SmartVideoToolbarIcon({ kind }: { kind: "upload" | "assets" }) { ); } + if (kind === "delete") { + return ( + + + + ); + } + return ( @@ -54,6 +62,7 @@ function SmartVideoUploadToolbar({ const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const updateMediaNodeData = useWorkflowStore((state) => state.updateMediaNodeData); const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); + const removeNode = useWorkflowStore((state) => state.removeNode); const fileInputRef = useRef(null); const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); const selectedNodeCount = useSelectedNodeCount(); @@ -287,6 +296,19 @@ function SmartVideoUploadToolbar({ setIsAssetPickerOpen(true); }, }, + { + key: "delete", + icon: , + label: t("videoInput.remove"), + tone: "danger", + disabled: isUploading, + onClick: (event) => { + event.preventDefault(); + event.stopPropagation(); + if (isUploading) return; + removeNode(id); + }, + }, ]; return ( @@ -329,7 +351,7 @@ function SmartVideoNeutralView({ return (
- +