= 0
+ ? "linear-gradient(135deg, rgba(255,188,109,0.9), rgba(255,232,194,0.42))"
+ : "linear-gradient(135deg, rgba(109,180,255,0.86), rgba(219,240,255,0.36))",
+ mixBlendMode: "soft-light",
+ opacity: temperatureOpacity,
+ }}
+ />
+ ) : null}
+ {tintOpacity > 0 ? (
+
= 0
+ ? "linear-gradient(135deg, rgba(244,105,214,0.78), rgba(255,203,236,0.28))"
+ : "linear-gradient(135deg, rgba(76,214,141,0.76), rgba(194,255,221,0.28))",
+ mixBlendMode: "overlay",
+ opacity: tintOpacity,
+ }}
+ />
+ ) : null}
+ {softLightOpacity > 0 ? (
+
+ ) : null}
+ {glowOpacity > 0 ? (
+
+ ) : null}
+ {vignetteOpacity > 0 ? (
+
+ ) : null}
+ {grainOpacity > 0 ? (
+
+ ) : null}
+ >
+ );
+}
diff --git a/components/canvas/nodes/image-node/image-node-dialogs.tsx b/components/canvas/nodes/image-node/image-node-dialogs.tsx
new file mode 100644
index 000000000..180fb57bd
--- /dev/null
+++ b/components/canvas/nodes/image-node/image-node-dialogs.tsx
@@ -0,0 +1,215 @@
+"use client";
+
+import { Box, Globe2, Loader2, RefreshCw } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Textarea } from "@/components/ui/textarea";
+
+interface ThreeDModelOption {
+ id: string;
+ displayName: string;
+ description?: string | null;
+}
+
+type ThreeDGenerationDialogMode = "model" | "world" | null;
+
+interface PromptRegenerateDialogProps {
+ isProcessing: boolean;
+ nodeId: string;
+ onConfirm: () => void;
+ onOpenChange: (open: boolean) => void;
+ onPromptChange: (value: string) => void;
+ open: boolean;
+ prompt: string;
+}
+
+interface ThreeDGenerationDialogProps {
+ isProcessing: boolean;
+ mode: ThreeDGenerationDialogMode;
+ onConfirm: () => void;
+ onModeChange: (mode: ThreeDGenerationDialogMode) => void;
+ onTripoModelChange: (value: string) => void;
+ onWorldModelChange: (value: string) => void;
+ selectedTripoModelEntry?: ThreeDModelOption | null;
+ selectedTripoModelVersion: string;
+ selectedWorldModelEntry?: ThreeDModelOption | null;
+ selectedWorldModelVersion: string;
+ tripoModelOptions: ThreeDModelOption[];
+ worldModelOptions: ThreeDModelOption[];
+}
+
+export function PromptRegenerateDialog({
+ isProcessing,
+ nodeId,
+ onConfirm,
+ onOpenChange,
+ onPromptChange,
+ open,
+ prompt,
+}: PromptRegenerateDialogProps) {
+ return (
+
+ );
+}
+
+export function ThreeDGenerationDialog({
+ isProcessing,
+ mode,
+ onConfirm,
+ onModeChange,
+ onTripoModelChange,
+ onWorldModelChange,
+ selectedTripoModelEntry,
+ selectedTripoModelVersion,
+ selectedWorldModelEntry,
+ selectedWorldModelVersion,
+ tripoModelOptions,
+ worldModelOptions,
+}: ThreeDGenerationDialogProps) {
+ const isWorldMode = mode === "world";
+ const selectedEntry = isWorldMode
+ ? selectedWorldModelEntry
+ : selectedTripoModelEntry;
+ const options = isWorldMode ? worldModelOptions : tripoModelOptions;
+
+ return (
+
+ );
+}
diff --git a/components/canvas/nodes/image-node/image-node-empty-state.tsx b/components/canvas/nodes/image-node/image-node-empty-state.tsx
new file mode 100644
index 000000000..2ec5aa881
--- /dev/null
+++ b/components/canvas/nodes/image-node/image-node-empty-state.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { AlertCircle, ImagePlus, Loader2 } from "lucide-react";
+import type React from "react";
+
+interface ImageNodeEmptyStateProps {
+ detailProgressLabel?: string;
+ fileInputRef: React.Ref
;
+ onImageUpload: (event: React.ChangeEvent) => void;
+ onSelectFile: (event: React.MouseEvent) => void;
+ status?: string;
+}
+
+export function ImageNodeDeferredLoadState() {
+ return (
+
+ );
+}
+
+export function ImageNodeProcessingOverlay() {
+ return (
+
+ );
+}
+
+export function ImageNodeEmptyState({
+ detailProgressLabel,
+ fileInputRef,
+ onImageUpload,
+ onSelectFile,
+ status,
+}: ImageNodeEmptyStateProps) {
+ if (status === "generating") {
+ return (
+
+
+
+
+
+ {detailProgressLabel || "生成中..."}
+
+ 图片处理中
+
+ );
+ }
+
+ if (status === "failed") {
+ return (
+
+
+
+
+
+ 生成失败
+
+
请重新上传或再试一次
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ 上传图片
+
+
+ 添加参考图、角色图或关键帧素材
+
+
+
+
+ );
+}
diff --git a/components/canvas/nodes/image-node/image-node-title.tsx b/components/canvas/nodes/image-node/image-node-title.tsx
new file mode 100644
index 000000000..b92fa7897
--- /dev/null
+++ b/components/canvas/nodes/image-node/image-node-title.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { ImagePlus } from "lucide-react";
+import type React from "react";
+import { Input } from "@/components/ui/input";
+
+interface ImageNodeSelectedTitleProps {
+ displayTitle: string;
+ draftTitle: string;
+ isEditingTitle: boolean;
+ onDraftTitleChange: (value: string) => void;
+ onCancelEditing: () => void;
+ onSaveTitle: () => void;
+ onStartEditing: () => void;
+ resolutionLabel?: string;
+ style?: React.CSSProperties;
+}
+
+export function ImageNodeSelectedTitle({
+ displayTitle,
+ draftTitle,
+ isEditingTitle,
+ onCancelEditing,
+ onDraftTitleChange,
+ onSaveTitle,
+ onStartEditing,
+ resolutionLabel,
+ style,
+}: ImageNodeSelectedTitleProps) {
+ return (
+
+
+
+ {isEditingTitle ? (
+ onDraftTitleChange(event.target.value)}
+ onClick={(event) => event.stopPropagation()}
+ onKeyDown={(event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ onSaveTitle();
+ } else if (event.key === "Escape") {
+ event.preventDefault();
+ onDraftTitleChange(displayTitle);
+ onCancelEditing();
+ }
+ }}
+ onMouseDown={(event) => event.stopPropagation()}
+ onPointerDown={(event) => event.stopPropagation()}
+ value={draftTitle}
+ />
+ ) : (
+ {
+ event.stopPropagation();
+ onDraftTitleChange(displayTitle);
+ onStartEditing();
+ }}
+ title="双击重命名"
+ >
+ {displayTitle}
+
+ )}
+
+ {resolutionLabel ? (
+
+ {resolutionLabel}
+
+ ) : null}
+
+ );
+}