Browse Source

模型增加统一的删除方案

feature/handleReconstruction
yun 6 hours ago
parent
commit
c11f877b3e
  1. 21
      src/components/nodes/GenerateAudioNode.tsx
  2. 24
      src/components/nodes/GenerateImageNode.tsx
  3. 24
      src/components/nodes/GenerateVideoNode.tsx
  4. 18
      src/components/nodes/SmartAudioNode.tsx
  5. 26
      src/components/nodes/SmartImageNode.tsx
  6. 26
      src/components/nodes/SmartVideoNode.tsx

21
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: <DeleteOutlined className="text-xl" />,
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 && <NodeActionCapsule actions={selectedActions} />}
{showEmptyStateDelete && <NodeActionCapsule actions={emptyStateActions} />}
<div className="relative h-full w-full min-h-0">
{nodeData.outputAudio ? (

24
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: (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7h12M9 7V5h6v2m-8 0 1 13h8l1-13" />
</svg>
),
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 && <NodeActionCapsule actions={selectedActions} />}
{showEmptyStateDelete && <NodeActionCapsule actions={emptyStateActions} />}
<div
ref={registerMediaFrame}

24
src/components/nodes/GenerateVideoNode.tsx

@ -46,9 +46,11 @@ export interface GenerateVideoNodeViewProps {
id: string;
data: GenerateVideoNodeData;
selected?: boolean;
/** Suppress the empty-state delete capsule (e.g. when a wrapper already renders an upload toolbar). */
suppressEmptyStateDelete?: boolean;
}
export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeViewProps) {
export function GenerateVideoNodeView({ id, data, selected, suppressEmptyStateDelete = false }: GenerateVideoNodeViewProps) {
const nodeData = data;
const { t } = useI18n();
const updateNodeData = useWorkflowStore((state) => 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: (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7h12M9 7V5h6v2m-8 0 1 13h8l1-13" />
</svg>
),
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 && <NodeActionCapsule actions={selectedActions} />}
{showEmptyStateDelete && <NodeActionCapsule actions={emptyStateActions} />}
<div
className={nodeData.outputVideo ? getAutoMediaFrameClassName("bg-black") : "relative w-full h-full min-h-0 overflow-hidden rounded-lg"}

18
src/components/nodes/SmartAudioNode.tsx

@ -1,7 +1,7 @@
"use client";
import { useCallback, useRef } from "react";
import { UploadOutlined } from "@ant-design/icons";
import { DeleteOutlined, UploadOutlined } from "@ant-design/icons";
import { Node, NodeProps } from "@xyflow/react";
import { Spin } from "antd";
import { useI18n } from "@/i18n";
@ -32,6 +32,7 @@ function SmartAudioUploadToolbar({
const { t } = useI18n();
const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode);
const removeNode = useWorkflowStore((state) => state.removeNode);
const fileInputRef = useRef<HTMLInputElement>(null);
const selectedNodeCount = useSelectedNodeCount();
const showSelectedActions = Boolean(selected && selectedNodeCount === 1);
@ -138,6 +139,19 @@ function SmartAudioUploadToolbar({
handleOpenFileDialog();
},
},
{
key: "delete",
icon: <DeleteOutlined className="text-base" />,
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 (
<div className="relative h-full w-full">
<GenerateAudioNodeView id={id} data={data} selected={selected} />
<GenerateAudioNodeView id={id} data={data} selected={selected} suppressEmptyStateDelete />
<SmartAudioUploadToolbar
id={id}
selected={selected}

26
src/components/nodes/SmartImageNode.tsx

@ -19,7 +19,7 @@ import { getNodeConnectionSpec } from "@/utils/nodeConnectionSpec";
type SmartImageNodeType = Node<SmartImageNodeData, "smartImage">;
const IMAGE_DIMENSION_TIMEOUT_MS = 10_000;
function SmartImageToolbarIcon({ kind }: { kind: "upload" | "assets" }) {
function SmartImageToolbarIcon({ kind }: { kind: "upload" | "assets" | "delete" }) {
if (kind === "upload") {
return (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
@ -28,6 +28,14 @@ function SmartImageToolbarIcon({ kind }: { kind: "upload" | "assets" }) {
);
}
if (kind === "delete") {
return (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7h12M9 7V5h6v2m-8 0 1 13h8l1-13" />
</svg>
);
}
return (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5z" />
@ -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<HTMLInputElement>(null);
const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false);
const selectedNodeCount = useSelectedNodeCount();
@ -186,6 +195,19 @@ function SmartImageUploadToolbar({
setIsAssetPickerOpen(true);
},
},
{
key: "delete",
icon: <SmartImageToolbarIcon kind="delete" />,
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 (
<div className="relative h-full w-full">
<GenerateImageNodeView id={id} data={data} selected={selected} renderInputHandle={renderInputHandle} />
<GenerateImageNodeView id={id} data={data} selected={selected} renderInputHandle={renderInputHandle} suppressEmptyStateDelete={!hideUploadToolbar} />
{!hideUploadToolbar && (
<SmartImageUploadToolbar
id={id}

26
src/components/nodes/SmartVideoNode.tsx

@ -22,7 +22,7 @@ const MAX_FILE_SIZE = 400 * 1024 * 1024;
const ACCEPTED_FORMATS = "video/mp4,video/webm,video/quicktime";
const ACCEPTED_MIME_TYPES = ACCEPTED_FORMATS.split(",");
function SmartVideoToolbarIcon({ kind }: { kind: "upload" | "assets" }) {
function SmartVideoToolbarIcon({ kind }: { kind: "upload" | "assets" | "delete" }) {
if (kind === "upload") {
return (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
@ -31,6 +31,14 @@ function SmartVideoToolbarIcon({ kind }: { kind: "upload" | "assets" }) {
);
}
if (kind === "delete") {
return (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7h12M9 7V5h6v2m-8 0 1 13h8l1-13" />
</svg>
);
}
return (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5z" />
@ -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<HTMLInputElement>(null);
const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false);
const selectedNodeCount = useSelectedNodeCount();
@ -287,6 +296,19 @@ function SmartVideoUploadToolbar({
setIsAssetPickerOpen(true);
},
},
{
key: "delete",
icon: <SmartVideoToolbarIcon kind="delete" />,
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 (
<div className="relative h-full w-full">
<GenerateVideoNodeView id={id} data={data} selected={selected} />
<GenerateVideoNodeView id={id} data={data} selected={selected} suppressEmptyStateDelete />
<SmartVideoUploadToolbar
id={id}
selected={selected}

Loading…
Cancel
Save