From b3d9b70ed4743dac794671cbf54b2f35091f174f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 5 Apr 2026 21:12:24 +0800 Subject: [PATCH] feat: move enhance pro into realtime canvas workspace --- app/canvas/[id]/page.tsx | 3144 ++++++++++++-------- app/canvas/[id]/realtime/page.tsx | 74 +- components/canvas/EnhanceProWorkspace.tsx | 1107 +++++++ components/canvas/RealtimeCanvasDialog.tsx | 1347 +++++---- components/canvas/nodes/ImageNode.tsx | 2117 +++++++------ services/canvas/index.ts | 4 +- 6 files changed, 5083 insertions(+), 2710 deletions(-) create mode 100644 components/canvas/EnhanceProWorkspace.tsx diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index d29f4e100..e31b29547 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -1,57 +1,101 @@ "use client"; -import { useEffect, useLayoutEffect, useState, useCallback, use, useRef, useMemo, type ChangeEvent } from "react"; -import { useRouter, useParams, useSearchParams } from "next/navigation"; -import useSWR from "swr"; import { AlertCircle, ArrowLeft, Bot, + Box, ChevronDown, ChevronLeft, ChevronRight, - Trash2, - Save, - Undo2, - Redo2, - ZoomIn, - ZoomOut, Clapperboard, - MousePointer, - Hand, - GitBranch, - Plus, - ImagePlus, + Crosshair, + Download, + Eye, FileImage, FileText, - Eye, Film, - Type, + FolderOpen, + GitBranch, + Grid3X3, + Hand, + ImagePlus, + Layers, Loader2, + type LucideIcon, MessageCircle, + MousePointer, Music2, Palette, - Scissors, - Sparkles, - Table2, - Video, - Download, - Upload, - Wand2, - Grid3X3, - Settings, Pencil, PenTool, - Box, - Layers, - FolderOpen, - Crosshair, + Plus, + Redo2, + Save, Scan, - type LucideIcon, + Scissors, + Settings, + Sparkles, + Table2, + Trash2, + Type, + Undo2, + Upload, + Video, + Wand2, + ZoomIn, + ZoomOut, } from "lucide-react"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; +import { + type ChangeEvent, + use, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { toast } from "sonner"; +import useSWR from "swr"; +import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark"; +import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2"; +import { AgentGenerationWorkspaceNew } from "@/components/agents/AgentGenerationWorkspaceNew"; +// 深色主题组件 V2 (新版布局) +import { AgentPresetPanelDarkV2 } from "@/components/agents/AgentPresetPanelDarkV2"; +import { AgentPresetPanelNew } from "@/components/agents/AgentPresetPanelNew"; +import { AgentProjectListSheet } from "@/components/agents/AgentProjectListSheet"; +import { AgentWorkflowBrandModelPicker } from "@/components/agents/AgentWorkflowBrandModelPicker"; +import { AgentWorkflowMaterialSelectionPanel } from "@/components/agents/AgentWorkflowMaterialSelectionPanel"; +import { + AgentWorkflowBrandStorePanel, + AgentWorkflowPanelDarkV2, +} from "@/components/agents/AgentWorkflowPanelDarkV2"; +import { + type AgentWorkflowInputs, + AgentWorkflowPanelNew, +} from "@/components/agents/AgentWorkflowPanelNew"; +import { CanvasWorkspace } from "@/components/canvas"; +import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel"; +import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel"; +import { ObjectPickerOverlay } from "@/components/canvas/ObjectPickerOverlay"; +import { SceneFusionDialog } from "@/components/scene-fusion-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { ClientErrorBoundary } from "@/components/ui/client-error-boundary"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { Dialog, DialogContent, @@ -60,20 +104,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Label } from "@/components/ui/label"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { DropdownMenu, DropdownMenuContent, @@ -84,6 +114,9 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, @@ -92,15 +125,43 @@ import { SelectValue, } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { toast } from "sonner"; -import { useCanvasStore, canvasSelectors } from "@/store/canvas"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; import { - canvasService, - imageGenerationService, - videoGenerationService, - workflowAnalysisService, -} from "@/services/canvas"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + createEmptyAgentGenerationProjectPersistedState, + createEmptyAgentGenerationProjectWorkspaceState, + createEmptyAgentWorkflowPersistedState, + normalizeAgentGenerationProjectPersistedState, +} from "@/lib/agents/generation-project-state"; +import { + createEmptyAgentModeWorkspaceState, + normalizeAgentModeWorkspaceState, +} from "@/lib/agents/mode-workspace-state"; +import { normalizeAgentPresetModeConfig } from "@/lib/agents/preset-mode-config"; +import type { AgentPsdTemplateCategoryTagIndex } from "@/lib/agents/psd-template-category-tag-config"; +import { normalizeAgentWorkflowBrandModelConfig } from "@/lib/agents/workflow-brand-model-config"; +import { + buildAgentWorkflowComposerContext, + getAgentWorkflowProgress, +} from "@/lib/agents/workflow-composer"; +import { + createEmptyAgentWorkflowMaterialSelections, + normalizeAgentWorkflowMaterialSelections, +} from "@/lib/agents/workflow-material-config"; +import { + defaultAgentWorkflowUiConfig, + flattenVisibleAgentWorkflowSteps, + getEnabledAgentWorkflowStepTree, + normalizeAgentWorkflowUiConfig, + sanitizeAgentWorkflowSelections, +} from "@/lib/agents/workflow-ui-config"; +import { cn, fetcher } from "@/lib/utils"; import { buildVideoWorkflowCanvasNode, getVideoWorkflowNodeDefinition, @@ -125,69 +186,23 @@ import { type WorkflowMediaReference, type WorkflowSceneRecord, } from "@/lib/video-workflow/runtime"; -import { - AgentWorkflowPanelNew, - type AgentWorkflowInputs, -} from "@/components/agents/AgentWorkflowPanelNew"; -import { AgentGenerationWorkspaceNew } from "@/components/agents/AgentGenerationWorkspaceNew"; -import { AgentWorkflowBrandModelPicker } from "@/components/agents/AgentWorkflowBrandModelPicker"; -import { AgentWorkflowMaterialSelectionPanel } from "@/components/agents/AgentWorkflowMaterialSelectionPanel"; -import { AgentPresetPanelNew } from "@/components/agents/AgentPresetPanelNew"; -// 深色主题组件 V2 (新版布局) -import { AgentPresetPanelDarkV2 } from "@/components/agents/AgentPresetPanelDarkV2"; -import { - AgentWorkflowBrandStorePanel, - AgentWorkflowPanelDarkV2, -} from "@/components/agents/AgentWorkflowPanelDarkV2"; -import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark"; -import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2"; -import { AgentProjectListSheet } from "@/components/agents/AgentProjectListSheet"; -import { CanvasWorkspace } from "@/components/canvas"; -import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel"; -import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel"; -import { ObjectPickerOverlay } from "@/components/canvas/ObjectPickerOverlay"; -import { SceneFusionDialog } from "@/components/scene-fusion-dialog"; -import { ClientErrorBoundary } from "@/components/ui/client-error-boundary"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { cn, fetcher } from "@/lib/utils"; -import { - defaultAgentWorkflowUiConfig, - flattenVisibleAgentWorkflowSteps, - getEnabledAgentWorkflowStepTree, - normalizeAgentWorkflowUiConfig, - sanitizeAgentWorkflowSelections, -} from "@/lib/agents/workflow-ui-config"; -import { - createEmptyAgentWorkflowMaterialSelections, - normalizeAgentWorkflowMaterialSelections, -} from "@/lib/agents/workflow-material-config"; -import { normalizeAgentWorkflowBrandModelConfig } from "@/lib/agents/workflow-brand-model-config"; -import { - buildAgentWorkflowComposerContext, - getAgentWorkflowProgress, -} from "@/lib/agents/workflow-composer"; -import { - createEmptyAgentModeWorkspaceState, - normalizeAgentModeWorkspaceState, -} from "@/lib/agents/mode-workspace-state"; -import { - createEmptyAgentGenerationProjectPersistedState, - createEmptyAgentWorkflowPersistedState, - createEmptyAgentGenerationProjectWorkspaceState, - normalizeAgentGenerationProjectPersistedState, -} from "@/lib/agents/generation-project-state"; -import { normalizeAgentPresetModeConfig } from "@/lib/agents/preset-mode-config"; -import type { AgentPsdTemplateCategoryTagIndex } from "@/lib/agents/psd-template-category-tag-config"; import { agentProjectService } from "@/services/agent-projects"; +import { + canvasService, + imageGenerationService, + videoGenerationService, + workflowAnalysisService, +} from "@/services/canvas"; +import { canvasSelectors, useCanvasStore } from "@/store/canvas"; import type { - AgentPreset, AgentGenerationProject, AgentGenerationProjectPersistedState, AgentGenerationProjectResultCollections, - AgentGenerationResultModeType, AgentGenerationProjectWorkspaceAttachment, AgentGenerationProjectWorkspaceState, + AgentGenerationResultModeType, AgentModeWorkspaceState, + AgentPreset, AgentWorkflowComposerContext, AgentWorkflowMaterialSelections, AgentWorkflowPersistedState, @@ -281,19 +296,47 @@ const IMPORTABLE_CANVAS_TOOL_TYPES = new Set([ const VIDEO_CANVAS_INSERT_MENU_ITEMS: VideoCanvasInsertMenuItem[] = [ { id: "image", label: "图片", icon: FileImage, templateType: "input-image" }, { id: "text", label: "文字", icon: FileText, templateType: "text-node" }, - { id: "video-frame", label: "视频抽帧", icon: Film, templateType: "video-input" }, - { id: "video-analyze", label: "视频拆解", icon: Scissors, templateType: "video-analyze" }, + { + id: "video-frame", + label: "视频抽帧", + icon: Film, + templateType: "video-input", + }, + { + id: "video-analyze", + label: "视频拆解", + icon: Scissors, + templateType: "video-analyze", + }, { id: "storyboard", label: "分镜表", icon: GitBranch, children: [ - { id: "storyboard-node", label: "分镜节点", templateType: "storyboard-node" }, - { id: "storyboard-chart-node", label: "分镜图表", templateType: "storyboard-chart-node" }, + { + id: "storyboard-node", + label: "分镜节点", + templateType: "storyboard-node", + }, + { + id: "storyboard-chart-node", + label: "分镜图表", + templateType: "storyboard-chart-node", + }, ], }, - { id: "table", label: "表格编辑", icon: Table2, templateType: "table-editor-node" }, - { id: "doodle", label: "涂鸦画板", icon: PenTool, templateType: "doodle-canvas" }, + { + id: "table", + label: "表格编辑", + icon: Table2, + templateType: "table-editor-node", + }, + { + id: "doodle", + label: "涂鸦画板", + icon: PenTool, + templateType: "doodle-canvas", + }, { id: "paint", label: "绘画", icon: Palette, templateType: "gen-image" }, { id: "video", label: "视频", icon: Video, templateType: "gen-video" }, { id: "music", label: "音乐", icon: Music2, templateType: "gen-music" }, @@ -313,8 +356,16 @@ const VIDEO_CANVAS_INSERT_MENU_ITEMS: VideoCanvasInsertMenuItem[] = [ label: "局部重绘", icon: Scissors, children: [ - { id: "inpaint-crop", label: "裁剪局部重绘", templateType: "inpaint-crop" }, - { id: "inpaint-stitch", label: "无缝拼接", templateType: "inpaint-stitch" }, + { + id: "inpaint-crop", + label: "裁剪局部重绘", + templateType: "inpaint-crop", + }, + { + id: "inpaint-stitch", + label: "无缝拼接", + templateType: "inpaint-stitch", + }, ], }, { @@ -323,7 +374,12 @@ const VIDEO_CANVAS_INSERT_MENU_ITEMS: VideoCanvasInsertMenuItem[] = [ icon: Sparkles, templateType: "jimeng-super-resolution", }, - { id: "motion", label: "动作迁移", icon: Clapperboard, templateType: "motion-control" }, + { + id: "motion", + label: "动作迁移", + icon: Clapperboard, + templateType: "motion-control", + }, { id: "preview", label: "预览", icon: Eye, templateType: "preview" }, { id: "save", label: "保存", icon: Save, templateType: "local-save" }, { @@ -332,8 +388,16 @@ const VIDEO_CANVAS_INSERT_MENU_ITEMS: VideoCanvasInsertMenuItem[] = [ icon: Wand2, accent: "highlight", children: [ - { id: "professional-camera", label: "专业镜头", templateType: "professional-camera" }, - { id: "camera-movement", label: "镜头运动", templateType: "camera-movement" }, + { + id: "professional-camera", + label: "专业镜头", + templateType: "professional-camera", + }, + { + id: "camera-movement", + label: "镜头运动", + templateType: "camera-movement", + }, ], }, ]; @@ -504,7 +568,9 @@ function normalizeImportedCanvasNode( ? (value.data as unknown as CanvasNode["data"]) : ({} as CanvasNode["data"]), connections: Array.isArray(value.connections) - ? value.connections.map((item) => String(item || "").trim()).filter(Boolean) + ? value.connections + .map((item) => String(item || "").trim()) + .filter(Boolean) : [], parentId: String(value.parentId || "").trim() || undefined, createdAt: normalizeImportedDate(value.createdAt), @@ -541,7 +607,12 @@ function normalizeImportedCanvasState(payload: unknown): { if (!isPlainObject(item)) return null; const source = String(item.source || "").trim(); const target = String(item.target || "").trim(); - if (!source || !target || !nodeIds.has(source) || !nodeIds.has(target)) { + if ( + !source || + !target || + !nodeIds.has(source) || + !nodeIds.has(target) + ) { return null; } return { @@ -558,7 +629,9 @@ function normalizeImportedCanvasState(payload: unknown): { .filter((item): item is CanvasEdge => item !== null) : []; - const viewport = isPlainObject(rawCanvasData.viewport) ? rawCanvasData.viewport : {}; + const viewport = isPlainObject(rawCanvasData.viewport) + ? rawCanvasData.viewport + : {}; const selectedNodeIds = Array.isArray(rawCanvasData.selectedNodeIds) ? rawCanvasData.selectedNodeIds .map((item) => String(item || "").trim()) @@ -566,16 +639,24 @@ function normalizeImportedCanvasState(payload: unknown): { : []; const toolValue = String(rawCanvasData.tool || "").trim() as ToolType; const resolvedWorkspaceType = ["canvas", "agent", "video"].includes( - String(payload.workspaceType || (isPlainObject(payload.project) ? payload.project.workspaceType : "") || "") + String( + payload.workspaceType || + (isPlainObject(payload.project) ? payload.project.workspaceType : "") || + "" + ) ) ? (String( payload.workspaceType || - (isPlainObject(payload.project) ? payload.project.workspaceType : "") || + (isPlainObject(payload.project) + ? payload.project.workspaceType + : "") || "" ) as CanvasWorkspaceType) : undefined; const projectTitle = String( - (isPlainObject(payload.project) ? payload.project.title : "") || payload.title || "" + (isPlainObject(payload.project) ? payload.project.title : "") || + payload.title || + "" ).trim(); return { @@ -588,7 +669,10 @@ function normalizeImportedCanvasState(payload: unknown): { viewport: { x: normalizeFiniteNumber(viewport.x, 0), y: normalizeFiniteNumber(viewport.y, 0), - zoom: Math.min(Math.max(normalizeFiniteNumber(viewport.zoom, 1), 0.1), 4), + zoom: Math.min( + Math.max(normalizeFiniteNumber(viewport.zoom, 1), 0.1), + 4 + ), }, tool: IMPORTABLE_CANVAS_TOOL_TYPES.has(toolValue) ? toolValue : "select", suppressedMediaKeys: Array.isArray(rawCanvasData.suppressedMediaKeys) @@ -620,11 +704,15 @@ function hasMeaningfulWorkflowState( String(inputs?.priceBand || "").trim() ); - const hasSelections = Object.values(workflowState.selections || {}).some((value) => - Boolean(String(value || "").trim()) + const hasSelections = Object.values(workflowState.selections || {}).some( + (value) => Boolean(String(value || "").trim()) + ); + const hasBrandModel = Boolean( + String(workflowState.selectedBrandModel?.imageUrl || "").trim() + ); + const hasPreset = Boolean( + String(workflowState.selectedAgentPresetId || "").trim() ); - const hasBrandModel = Boolean(String(workflowState.selectedBrandModel?.imageUrl || "").trim()); - const hasPreset = Boolean(String(workflowState.selectedAgentPresetId || "").trim()); return hasInputs || hasSelections || hasBrandModel || hasPreset; } @@ -636,12 +724,13 @@ const FLOATING_UI_GAP = 16; const FLOATING_BOTTOM_INSET = 20; const WORKFLOW_AGENT_CONTEXT_START = "【视频画布同步上下文】"; const WORKFLOW_AGENT_CONTEXT_END = "【/视频画布同步上下文】"; -const AGENT_RESULT_MODE_LABELS: Record = { - scene_generation: "场景结果", - detail_generation: "细节结果", - detail_replacement: "细节替换", - sku_replacement: "SKU 替换", -}; +const AGENT_RESULT_MODE_LABELS: Record = + { + scene_generation: "场景结果", + detail_generation: "细节结果", + detail_replacement: "细节替换", + sku_replacement: "SKU 替换", + }; const VIDEO_CANVAS_PANEL_CLASS = "pointer-events-auto overflow-hidden rounded-[24px] border border-slate-200 bg-white text-foreground shadow-[0_16px_36px_-24px_rgba(15,23,42,0.2)] dark:border-zinc-800 dark:bg-zinc-950"; const VIDEO_CANVAS_PANEL_BADGE_CLASS = @@ -658,7 +747,10 @@ const VIDEO_CANVAS_COMPACT_CONTROLS_CLASS = "rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm dark:border-zinc-800 dark:bg-zinc-950"; function buildScopedCanvasChatId(projectId: string, scope: "agent-mode") { - const normalized = String(projectId || "").trim().toLowerCase().replace(/-/g, ""); + const normalized = String(projectId || "") + .trim() + .toLowerCase() + .replace(/-/g, ""); if (!/^[0-9a-f]{32}$/.test(normalized)) { return projectId; } @@ -728,7 +820,9 @@ interface AgentProjectDraftSnapshot { projectState?: AgentGenerationProjectPersistedState; } -function readAgentProjectDraftSnapshot(projectId: string): AgentProjectDraftSnapshot | null { +function readAgentProjectDraftSnapshot( + projectId: string +): AgentProjectDraftSnapshot | null { if (typeof window === "undefined") { return null; } @@ -750,7 +844,9 @@ function readAgentProjectDraftSnapshot(projectId: string): AgentProjectDraftSnap } return { - savedAt: Number.isFinite(Number(parsed.savedAt)) ? Number(parsed.savedAt) : 0, + savedAt: Number.isFinite(Number(parsed.savedAt)) + ? Number(parsed.savedAt) + : 0, title: String(parsed.title || "").trim() || null, thumbnail: String(parsed.thumbnail || "").trim() || null, projectState: @@ -786,7 +882,10 @@ function isUnauthorizedApiError(error?: string | null) { } function clearAppearanceSummaryFromResultCollection( - result: AgentWorkflowMaterialSelections["primary"]["result"] | null | undefined + result: + | AgentWorkflowMaterialSelections["primary"]["result"] + | null + | undefined ) { if (!result) { return result; @@ -901,11 +1000,16 @@ export default function CanvasEditorPage({ params }: PageProps) { const requestedWorkspaceType = searchParams.get("workspaceType"); const requestedAgentMode = mode === "agent"; const isVideoMode = - !requestedAgentMode && (mode === "video" || requestedWorkspaceType === "video"); - const activeCanvasWorkspaceType: CanvasWorkspaceType = isVideoMode ? "video" : "canvas"; + !requestedAgentMode && + (mode === "video" || requestedWorkspaceType === "video"); + const activeCanvasWorkspaceType: CanvasWorkspaceType = isVideoMode + ? "video" + : "canvas"; const defaultCanvasTitle = isVideoMode ? "未命名视频画布" : "未命名画布"; const workspaceLabel = isVideoMode ? "视频画布" : "画布"; - const createNewCanvasUrl = isVideoMode ? "/video-canvas?new=1" : "/canvas?new=1"; + const createNewCanvasUrl = isVideoMode + ? "/video-canvas?new=1" + : "/canvas?new=1"; const shouldOpenBatchProcessing = searchParams.get("batch") === "1"; const goHome = useCallback(() => { if (typeof window !== "undefined") { @@ -919,7 +1023,7 @@ export default function CanvasEditorPage({ params }: PageProps) { : `/canvas/${projectId}`, [isVideoMode] ); - + // Store state const projectTitle = useCanvasStore((state) => state.projectTitle); const viewport = useCanvasStore(canvasSelectors.viewport); @@ -929,7 +1033,7 @@ export default function CanvasEditorPage({ params }: PageProps) { const selectedNodeIds = useCanvasStore(canvasSelectors.selectedNodeIds); const nodes = useCanvasStore(canvasSelectors.nodes); const edges = useCanvasStore(canvasSelectors.edges); - + // Store actions const setProject = useCanvasStore((state) => state.setProject); const setProjectTitle = useCanvasStore((state) => state.setProjectTitle); @@ -948,17 +1052,23 @@ export default function CanvasEditorPage({ params }: PageProps) { const updateNode = useCanvasStore((state) => state.updateNode); const setGenerating = useCanvasStore((state) => state.setGenerating); const setPickerMode = useCanvasStore((state) => state.setPickerMode); - const requestImageNodeAction = useCanvasStore((state) => state.requestImageNodeAction); - + const requestImageNodeAction = useCanvasStore( + (state) => state.requestImageNodeAction + ); + // Local state const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false); const [editTitle, setEditTitle] = useState(""); - const [activeWorkflowExecutionTaskKey, setActiveWorkflowExecutionTaskKey] = useState(null); + const [activeWorkflowExecutionTaskKey, setActiveWorkflowExecutionTaskKey] = + useState(null); const [isPanelOpen, setIsPanelOpen] = useState(() => !isVideoMode); - const [isVideoModuleSidebarCollapsed, setIsVideoModuleSidebarCollapsed] = useState(false); - const [videoModuleSectionOpen, setVideoModuleSectionOpen] = useState>({ + const [isVideoModuleSidebarCollapsed, setIsVideoModuleSidebarCollapsed] = + useState(false); + const [videoModuleSectionOpen, setVideoModuleSectionOpen] = useState< + Record + >({ input: true, analysis: true, storyboard: true, @@ -970,7 +1080,7 @@ export default function CanvasEditorPage({ params }: PageProps) { useState(null); const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false); // 面板模式: 'chat' 聊天面板, 'fileList' 文件列表 - const [panelMode, setPanelMode] = useState<'chat' | 'fileList'>('chat'); + const [panelMode, setPanelMode] = useState<"chat" | "fileList">("chat"); const [viewportWidth, setViewportWidth] = useState(0); const [workflowInputs, setWorkflowInputs] = useState({ primarySku: "", @@ -982,9 +1092,10 @@ export default function CanvasEditorPage({ params }: PageProps) { fifthCategory: "", priceBand: "", }); - const [workflowMaterials, setWorkflowMaterials] = useState( - () => createEmptyAgentWorkflowMaterialSelections() - ); + const [workflowMaterials, setWorkflowMaterials] = + useState(() => + createEmptyAgentWorkflowMaterialSelections() + ); const [workflowSelections, setWorkflowSelections] = useState< Partial> >({}); @@ -1000,7 +1111,8 @@ export default function CanvasEditorPage({ params }: PageProps) { setIsVideoModuleSidebarCollapsed(false); } }, [isVideoMode]); - const [appearanceRecognitionEnabled, setAppearanceRecognitionEnabled] = useState(true); + const [appearanceRecognitionEnabled, setAppearanceRecognitionEnabled] = + useState(true); const [selectedBrandModel, setSelectedBrandModel] = useState(null); const workflowInputsRef = useRef(workflowInputs); @@ -1008,37 +1120,52 @@ export default function CanvasEditorPage({ params }: PageProps) { const workflowSelectionsRef = useRef(workflowSelections); const appearanceRecognitionEnabledRef = useRef(appearanceRecognitionEnabled); const selectedBrandModelRef = useRef(selectedBrandModel); - const [selectedAgentPresetId, setSelectedAgentPresetId] = useState(null); - const [currentAgentStage, setCurrentAgentStage] = useState<"workflow" | "mode">("workflow"); + const [selectedAgentPresetId, setSelectedAgentPresetId] = useState< + string | null + >(null); + const [currentAgentStage, setCurrentAgentStage] = useState< + "workflow" | "mode" + >("workflow"); const [agentModeState, setAgentModeState] = useState( () => createEmptyAgentModeWorkspaceState() ); const [agentWorkspaceState, setAgentWorkspaceState] = - useState( - () => createEmptyAgentGenerationProjectWorkspaceState() + useState(() => + createEmptyAgentGenerationProjectWorkspaceState() ); const [agentProjectTitle, setAgentProjectTitle] = useState("未命名生图项目"); - const [agentProjectThumbnail, setAgentProjectThumbnail] = useState(null); + const [agentProjectThumbnail, setAgentProjectThumbnail] = useState< + string | null + >(null); const [agentProjectResultCollections, setAgentProjectResultCollections] = useState({}); - const [agentProjectDeletedResultCollections, setAgentProjectDeletedResultCollections] = - useState({}); + const [ + agentProjectDeletedResultCollections, + setAgentProjectDeletedResultCollections, + ] = useState({}); const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false); - const [loadedAgentProjectId, setLoadedAgentProjectId] = useState(null); + const [loadedAgentProjectId, setLoadedAgentProjectId] = useState< + string | null + >(null); const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false); - const [isSyncingAgentResultsToCanvas, setIsSyncingAgentResultsToCanvas] = useState(false); + const [isSyncingAgentResultsToCanvas, setIsSyncingAgentResultsToCanvas] = + useState(false); const [isImportingVideoCanvas, setIsImportingVideoCanvas] = useState(false); const [isAgentProjectSheetOpen, setIsAgentProjectSheetOpen] = useState(false); const [isAgentProjectMenuOpen, setIsAgentProjectMenuOpen] = useState(false); const [isLoginDialogOpen, setIsLoginDialogOpen] = useState(false); - const [loginDialogMessage, setLoginDialogMessage] = useState("当前登录状态已失效,请重新登录后继续操作。"); + const [loginDialogMessage, setLoginDialogMessage] = useState( + "当前登录状态已失效,请重新登录后继续操作。" + ); const [authSessionChecked, setAuthSessionChecked] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(true); const [workflowHydrationSource, setWorkflowHydrationSource] = useState< "pending" | "project" | "local" | "empty" >("pending"); const requestedAgentProjectId = searchParams.get("agentProjectId"); - const agentProjectId = requestedAgentProjectId ? requestedAgentProjectId.trim() : ""; + const agentProjectId = requestedAgentProjectId + ? requestedAgentProjectId.trim() + : ""; const hasActiveAgentProject = Boolean(agentProjectId); const agentModeChatId = useMemo( () => buildScopedCanvasChatId(agentProjectId || id, "agent-mode"), @@ -1075,7 +1202,7 @@ export default function CanvasEditorPage({ params }: PageProps) { }>; }; }>("/api/site-settings", fetcher); - + // 生成配置 const [prompt, setPrompt] = useState(""); const [negativePrompt, setNegativePrompt] = useState(""); @@ -1091,13 +1218,16 @@ export default function CanvasEditorPage({ params }: PageProps) { const normalizedVideoNodeSizeIdsRef = useRef>(new Set()); const importVideoCanvasInputRef = useRef(null); const isAgentWorkspaceEnabled = siteSettingsData?.agentEnabled !== false; - const isCanvasAgentModeEnabled = siteSettingsData?.canvasAgentModeEnabled !== false; - const isAgentMode = - isAgentWorkspaceEnabled && requestedAgentMode; + const isCanvasAgentModeEnabled = + siteSettingsData?.canvasAgentModeEnabled !== false; + const isAgentMode = isAgentWorkspaceEnabled && requestedAgentMode; const hasLoadedCurrentAgentProject = - !requestedAgentMode || !agentProjectId || loadedAgentProjectId === agentProjectId; + !requestedAgentMode || + !agentProjectId || + loadedAgentProjectId === agentProjectId; const resolvedWorkflowUiConfig = useMemo( - () => normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig), + () => + normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig), [siteSettingsData?.agentWorkflowUiConfig] ); const sanitizedWorkflowSelections = useMemo( @@ -1109,7 +1239,10 @@ export default function CanvasEditorPage({ params }: PageProps) { [resolvedWorkflowUiConfig, workflowSelections] ); const resolvedBrandModelConfig = useMemo( - () => normalizeAgentWorkflowBrandModelConfig(siteSettingsData?.agentWorkflowBrandModelConfig), + () => + normalizeAgentWorkflowBrandModelConfig( + siteSettingsData?.agentWorkflowBrandModelConfig + ), [siteSettingsData?.agentWorkflowBrandModelConfig] ); @@ -1125,7 +1258,9 @@ export default function CanvasEditorPage({ params }: PageProps) { normalizedVideoNodeSizeIdsRef.current.add(node.id); const templateType = inferVideoWorkflowTemplateType(node); - const definition = templateType ? getVideoWorkflowNodeDefinition(templateType) : null; + const definition = templateType + ? getVideoWorkflowNodeDefinition(templateType) + : null; if (!definition) { return; } @@ -1146,9 +1281,10 @@ export default function CanvasEditorPage({ params }: PageProps) { }, [isVideoMode, nodes, updateNode]); // 获取选中的图片节点 - const selectedImageNode = selectedNodeIds.length === 1 - ? nodes.find(n => n.id === selectedNodeIds[0] && n.type === 'image') - : null; + const selectedImageNode = + selectedNodeIds.length === 1 + ? nodes.find((n) => n.id === selectedNodeIds[0] && n.type === "image") + : null; const selectedVideoWorkflowNode = useMemo( () => isVideoMode && selectedNodeIds.length === 1 @@ -1190,8 +1326,10 @@ export default function CanvasEditorPage({ params }: PageProps) { return null; } - const nodeLeft = selectedVideoWorkflowNode.position.x * viewport.zoom + viewport.x; - const nodeTop = selectedVideoWorkflowNode.position.y * viewport.zoom + viewport.y; + const nodeLeft = + selectedVideoWorkflowNode.position.x * viewport.zoom + viewport.x; + const nodeTop = + selectedVideoWorkflowNode.position.y * viewport.zoom + viewport.y; const nodeWidth = selectedVideoWorkflowNode.size.width * viewport.zoom; const anchorX = nodeLeft + nodeWidth; const anchorY = @@ -1221,12 +1359,20 @@ export default function CanvasEditorPage({ params }: PageProps) { ? 56 : 228 : 0; - const selectedWorkflowPanelLeft = isVideoMode ? videoModuleSidebarWidth + 24 : 16; + const selectedWorkflowPanelLeft = isVideoMode + ? videoModuleSidebarWidth + 24 + : 16; const handleDeleteSelectedWorkflowNode = useCallback(() => { if (!selectedVideoWorkflowNode) return; deleteNode(selectedVideoWorkflowNode.id); - toast.success(`已删除模块:${selectedVideoWorkflowDefinition?.label || "当前模块"}`); - }, [deleteNode, selectedVideoWorkflowDefinition?.label, selectedVideoWorkflowNode]); + toast.success( + `已删除模块:${selectedVideoWorkflowDefinition?.label || "当前模块"}` + ); + }, [ + deleteNode, + selectedVideoWorkflowDefinition?.label, + selectedVideoWorkflowNode, + ]); const workflowGraph = useMemo( () => createWorkflowGraphContext(nodes, edges), [edges, nodes] @@ -1248,14 +1394,20 @@ export default function CanvasEditorPage({ params }: PageProps) { const selectedWorkflowStoryboardShots = useMemo( () => selectedVideoWorkflowNode - ? collectWorkflowStoryboardShots(selectedVideoWorkflowNode.id, workflowGraph) + ? collectWorkflowStoryboardShots( + selectedVideoWorkflowNode.id, + workflowGraph + ) : [], [selectedVideoWorkflowNode, workflowGraph] ); const selectedWorkflowStructuredData = useMemo( () => selectedVideoWorkflowNode - ? collectWorkflowStructuredData(selectedVideoWorkflowNode.id, workflowGraph) + ? collectWorkflowStructuredData( + selectedVideoWorkflowNode.id, + workflowGraph + ) : { characters: [] as WorkflowCharacterRecord[], scenes: [] as WorkflowSceneRecord[], @@ -1286,17 +1438,26 @@ export default function CanvasEditorPage({ params }: PageProps) { node.type === "workflow" && inferVideoWorkflowTemplateType(node) === "custom-agent" && String( - ((node.data as WorkflowNodeData).metadata || {})?.linkedAgentProjectId || "" + ((node.data as WorkflowNodeData).metadata || {}) + ?.linkedAgentProjectId || "" ).trim() === agentProjectId ) .sort((left, right) => { const leftSyncedAt = Number( new Date( String( - (((left.data as WorkflowNodeData).metadata || {}) as Record) - ?.lastAgentCanvasSyncAt || - (((left.data as WorkflowNodeData).metadata || {}) as Record) - ?.lastAgentWorkspaceSyncAt || + ( + ((left.data as WorkflowNodeData).metadata || {}) as Record< + string, + any + > + )?.lastAgentCanvasSyncAt || + ( + ((left.data as WorkflowNodeData).metadata || {}) as Record< + string, + any + > + )?.lastAgentWorkspaceSyncAt || left.updatedAt ) ).getTime() @@ -1304,10 +1465,18 @@ export default function CanvasEditorPage({ params }: PageProps) { const rightSyncedAt = Number( new Date( String( - (((right.data as WorkflowNodeData).metadata || {}) as Record) - ?.lastAgentCanvasSyncAt || - (((right.data as WorkflowNodeData).metadata || {}) as Record) - ?.lastAgentWorkspaceSyncAt || + ( + ((right.data as WorkflowNodeData).metadata || {}) as Record< + string, + any + > + )?.lastAgentCanvasSyncAt || + ( + ((right.data as WorkflowNodeData).metadata || {}) as Record< + string, + any + > + )?.lastAgentWorkspaceSyncAt || right.updatedAt ) ).getTime() @@ -1324,7 +1493,8 @@ export default function CanvasEditorPage({ params }: PageProps) { [linkedAgentWorkflowNode] ); const linkedAgentImportedResultUrls = useMemo(() => { - const metadata = ((linkedAgentWorkflowData?.metadata || {}) as Record) || {}; + const metadata = + ((linkedAgentWorkflowData?.metadata || {}) as Record) || {}; return Array.isArray(metadata.importedAgentResultUrls) ? metadata.importedAgentResultUrls .map((item) => String(item || "").trim()) @@ -1341,7 +1511,9 @@ export default function CanvasEditorPage({ params }: PageProps) { "sku_replacement", ] as AgentGenerationResultModeType[] ).flatMap((modeType) => - normalizeResultCollectionUrls(agentProjectResultCollections[modeType]).map((url, index) => ({ + normalizeResultCollectionUrls( + agentProjectResultCollections[modeType] + ).map((url, index) => ({ modeType, url, index, @@ -1355,7 +1527,9 @@ export default function CanvasEditorPage({ params }: PageProps) { nodes .flatMap((node) => { if (node.type === "image" || node.type === "video") { - return [String((node.data as Record)?.src || "").trim()]; + return [ + String((node.data as Record)?.src || "").trim(), + ]; } return []; }) @@ -1373,17 +1547,23 @@ export default function CanvasEditorPage({ params }: PageProps) { flattenedAgentResultEntries.length > 0 ); const selectedWorkflowLinkedAgentProject = useMemo(() => { - const metadata = ((selectedVideoWorkflowData?.metadata || {}) as Record) || {}; + const metadata = + ((selectedVideoWorkflowData?.metadata || {}) as Record) || + {}; return { id: String(metadata.linkedAgentProjectId || "").trim(), title: String(metadata.linkedAgentProjectTitle || "").trim(), syncedAt: String( - metadata.lastAgentWorkspaceSyncAt || metadata.linkedAgentProjectUpdatedAt || "" + metadata.lastAgentWorkspaceSyncAt || + metadata.linkedAgentProjectUpdatedAt || + "" ).trim(), }; }, [selectedVideoWorkflowData]); const selectedWorkflowAgentResultGroups = useMemo(() => { - const metadata = ((selectedVideoWorkflowData?.metadata || {}) as Record) || {}; + const metadata = + ((selectedVideoWorkflowData?.metadata || {}) as Record) || + {}; return Array.isArray(metadata.agentResultGroups) ? metadata.agentResultGroups .map((item) => { @@ -1405,7 +1585,9 @@ export default function CanvasEditorPage({ params }: PageProps) { assets: WorkflowMediaReference[]; }> >(() => { - const metadata = ((selectedVideoWorkflowData?.metadata || {}) as Record) || {}; + const metadata = + ((selectedVideoWorkflowData?.metadata || {}) as Record) || + {}; return Array.isArray(metadata.agentResultAssets) ? metadata.agentResultAssets.reduce< Array<{ @@ -1414,45 +1596,64 @@ export default function CanvasEditorPage({ params }: PageProps) { assets: WorkflowMediaReference[]; }> >((groups, item) => { - const record = (item || {}) as Record; - const modeType = String(record.modeType || "").trim(); - const label = String(record.label || "").trim(); - const assets = Array.isArray(record.assets) - ? record.assets.reduce((nextAssets, asset, index) => { - const assetRecord = (asset || {}) as Record; - const url = String(assetRecord.url || "").trim(); - if (!/^https?:\/\//i.test(url)) return nextAssets; - nextAssets.push({ - nodeId: selectedVideoWorkflowNode?.id || "", - kind: "image" as const, - url, - title: String(assetRecord.title || `${label || "Agent 结果"} ${index + 1}`).trim(), - prompt: selectedWorkflowPrompt, - source: "workflow-result" as const, - }); - return nextAssets; - }, []) - : []; - if (!modeType || !label || assets.length === 0) return groups; - groups.push({ - modeType, - label, - assets, - }); - return groups; - }, []) + const record = (item || {}) as Record; + const modeType = String(record.modeType || "").trim(); + const label = String(record.label || "").trim(); + const assets = Array.isArray(record.assets) + ? record.assets.reduce( + (nextAssets, asset, index) => { + const assetRecord = (asset || {}) as Record; + const url = String(assetRecord.url || "").trim(); + if (!/^https?:\/\//i.test(url)) return nextAssets; + nextAssets.push({ + nodeId: selectedVideoWorkflowNode?.id || "", + kind: "image" as const, + url, + title: String( + assetRecord.title || + `${label || "Agent 结果"} ${index + 1}` + ).trim(), + prompt: selectedWorkflowPrompt, + source: "workflow-result" as const, + }); + return nextAssets; + }, + [] + ) + : []; + if (!modeType || !label || assets.length === 0) return groups; + groups.push({ + modeType, + label, + assets, + }); + return groups; + }, []) : []; - }, [selectedVideoWorkflowData, selectedVideoWorkflowNode?.id, selectedWorkflowPrompt]); + }, [ + selectedVideoWorkflowData, + selectedVideoWorkflowNode?.id, + selectedWorkflowPrompt, + ]); const selectedWorkflowAgentResultCount = useMemo( - () => selectedWorkflowAgentResultGroups.reduce((total, item) => total + item.count, 0), + () => + selectedWorkflowAgentResultGroups.reduce( + (total, item) => total + item.count, + 0 + ), [selectedWorkflowAgentResultGroups] ); const selectedWorkflowPendingAgentResultCount = useMemo(() => { - const metadata = ((selectedVideoWorkflowData?.metadata || {}) as Record) || {}; + const metadata = + ((selectedVideoWorkflowData?.metadata || {}) as Record) || + {}; return Math.max(Number(metadata.pendingAgentResultCount || 0), 0); }, [selectedVideoWorkflowData]); const selectedStoryboardDownstreamNodes = useMemo(() => { - if (!selectedVideoWorkflowNode || selectedVideoWorkflowType !== "storyboard-node") { + if ( + !selectedVideoWorkflowNode || + selectedVideoWorkflowType !== "storyboard-node" + ) { return []; } @@ -1460,7 +1661,9 @@ export default function CanvasEditorPage({ params }: PageProps) { .map((node) => ({ node, templateType: - node.type === "workflow" ? inferVideoWorkflowTemplateType(node) : null, + node.type === "workflow" + ? inferVideoWorkflowTemplateType(node) + : null, })) .filter((item) => Boolean(item.templateType)); }, [selectedVideoWorkflowNode, selectedVideoWorkflowType, workflowGraph]); @@ -1692,12 +1895,20 @@ export default function CanvasEditorPage({ params }: PageProps) { activeWorkflowExecutionTaskKey === selectedWorkflowBranchTaskKey; const isAnyWorkflowExecutionRunning = Boolean(activeWorkflowExecutionTaskKey); - const resolvedPanelWidth = viewportWidth > 0 - ? Math.min(FLOATING_PANEL_WIDTH, Math.max(viewportWidth - DOCKED_PANEL_INSET, 0)) - : FLOATING_PANEL_WIDTH; + const resolvedPanelWidth = + viewportWidth > 0 + ? Math.min( + FLOATING_PANEL_WIDTH, + Math.max(viewportWidth - DOCKED_PANEL_INSET, 0) + ) + : FLOATING_PANEL_WIDTH; const isFloatingSidebarVisible = !isVideoMode && isPanelOpen; - const panelOcclusionRight = isFloatingSidebarVisible ? resolvedPanelWidth + DOCKED_PANEL_INSET : 0; - const floatingToolbarOffset = isFloatingSidebarVisible ? -(panelOcclusionRight / 2) : 0; + const panelOcclusionRight = isFloatingSidebarVisible + ? resolvedPanelWidth + DOCKED_PANEL_INSET + : 0; + const floatingToolbarOffset = isFloatingSidebarVisible + ? -(panelOcclusionRight / 2) + : 0; const compactControlsLeft = FLOATING_UI_GAP; const updateCanvasSearchParams = useCallback( @@ -1712,14 +1923,17 @@ export default function CanvasEditorPage({ params }: PageProps) { }); const nextQuery = nextParams.toString(); - router.replace(nextQuery ? `/canvas/${id}?${nextQuery}` : `/canvas/${id}`); + router.replace( + nextQuery ? `/canvas/${id}?${nextQuery}` : `/canvas/${id}` + ); }, [id, router, searchParams] ); const openLoginDialog = useCallback((message?: string) => { setLoginDialogMessage( - String(message || "").trim() || "当前登录状态已失效,请重新登录后继续操作。" + String(message || "").trim() || + "当前登录状态已失效,请重新登录后继续操作。" ); setIsLoginDialogOpen(true); }, []); @@ -1731,7 +1945,9 @@ export default function CanvasEditorPage({ params }: PageProps) { } const callbackUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`; - window.location.assign(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`); + window.location.assign( + `/login?callbackUrl=${encodeURIComponent(callbackUrl)}` + ); }, [router]); const checkCanvasAuthSession = useCallback(async () => { @@ -1741,10 +1957,10 @@ export default function CanvasEditorPage({ params }: PageProps) { const authed = Boolean(session?.user?.id); setIsAuthenticated(authed); setAuthSessionChecked(true); - if (!authed) { - openLoginDialog(); - } else { + if (authed) { setIsLoginDialogOpen(false); + } else { + openLoginDialog(); } return authed; } catch (error) { @@ -1774,46 +1990,48 @@ export default function CanvasEditorPage({ params }: PageProps) { setPanelMode("chat"); }, [agentProjectId, isAgentMode, updateCanvasSearchParams]); - const buildCurrentWorkflowPersistedState = useCallback((): AgentWorkflowPersistedState => { - const persistedMaterials = appearanceRecognitionEnabled - ? workflowMaterials - : clearAppearanceSummaryFromMaterials(workflowMaterials); + const buildCurrentWorkflowPersistedState = + useCallback((): AgentWorkflowPersistedState => { + const persistedMaterials = appearanceRecognitionEnabled + ? workflowMaterials + : clearAppearanceSummaryFromMaterials(workflowMaterials); - return { - inputs: { - primarySku: workflowInputs.primarySku.trim(), - secondarySku: workflowInputs.secondarySku.trim(), - brand: workflowInputs.brand.trim(), - store: workflowInputs.store.trim(), - thirdCategory: workflowInputs.thirdCategory.trim(), - fourthCategory: workflowInputs.fourthCategory.trim(), - fifthCategory: workflowInputs.fifthCategory.trim(), - priceBand: workflowInputs.priceBand.trim(), - }, - materials: persistedMaterials, - appearanceRecognitionEnabled, - selections: sanitizedWorkflowSelections, - selectedBrandModel, - selectedAgentPresetId: selectedAgentPresetId || null, - currentStage: currentAgentStage, + return { + inputs: { + primarySku: workflowInputs.primarySku.trim(), + secondarySku: workflowInputs.secondarySku.trim(), + brand: workflowInputs.brand.trim(), + store: workflowInputs.store.trim(), + thirdCategory: workflowInputs.thirdCategory.trim(), + fourthCategory: workflowInputs.fourthCategory.trim(), + fifthCategory: workflowInputs.fifthCategory.trim(), + priceBand: workflowInputs.priceBand.trim(), + }, + materials: persistedMaterials, + appearanceRecognitionEnabled, + selections: sanitizedWorkflowSelections, + selectedBrandModel, + selectedAgentPresetId: selectedAgentPresetId || null, + currentStage: currentAgentStage, + agentModeState, + }; + }, [ agentModeState, - }; - }, [ - agentModeState, - currentAgentStage, - appearanceRecognitionEnabled, - selectedAgentPresetId, - selectedBrandModel, - workflowInputs, - workflowMaterials, - sanitizedWorkflowSelections, - ]); + currentAgentStage, + appearanceRecognitionEnabled, + selectedAgentPresetId, + selectedBrandModel, + workflowInputs, + workflowMaterials, + sanitizedWorkflowSelections, + ]); const currentWorkflowPersistedStateRef = useRef( buildCurrentWorkflowPersistedState() ); useEffect(() => { - currentWorkflowPersistedStateRef.current = buildCurrentWorkflowPersistedState(); + currentWorkflowPersistedStateRef.current = + buildCurrentWorkflowPersistedState(); }, [buildCurrentWorkflowPersistedState]); useEffect(() => { @@ -1838,8 +2056,8 @@ export default function CanvasEditorPage({ params }: PageProps) { // 加载画布数据 useLayoutEffect(() => { - console.log('[Canvas] Loading project:', id); - + console.log("[Canvas] Loading project:", id); + const loadProject = async () => { const preservedWorkflowState = currentWorkflowPersistedStateRef.current; const shouldPreserveCurrentWorkflowState = @@ -1872,12 +2090,12 @@ export default function CanvasEditorPage({ params }: PageProps) { setAgentProjectResultCollections({}); setAgentProjectDeletedResultCollections({}); setLoadedAgentProjectId(null); - + // 🆕 先清空当前画布状态,确保切换时不会显示旧数据 useCanvasStore.getState().clearCanvas(); - + const result = await canvasService.getProject(id); - + if (result.success && result.data) { useCanvasStore.getState().setProject(id, result.data.title); if (result.data.canvasData) { @@ -1887,19 +2105,35 @@ export default function CanvasEditorPage({ params }: PageProps) { const nextAppearanceRecognitionEnabled = persistedWorkflow.appearanceRecognitionEnabled !== false; const nextWorkflowMaterials = nextAppearanceRecognitionEnabled - ? normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) + ? normalizeAgentWorkflowMaterialSelections( + persistedWorkflow.materials + ) : clearAppearanceSummaryFromMaterials( - normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) + normalizeAgentWorkflowMaterialSelections( + persistedWorkflow.materials + ) ); setWorkflowInputs({ - primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(), - secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(), + primarySku: String( + persistedWorkflow.inputs?.primarySku || "" + ).trim(), + secondarySku: String( + persistedWorkflow.inputs?.secondarySku || "" + ).trim(), brand: String(persistedWorkflow.inputs?.brand || "").trim(), store: String(persistedWorkflow.inputs?.store || "").trim(), - thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(), - fourthCategory: String(persistedWorkflow.inputs?.fourthCategory || "").trim(), - fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(), - priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(), + thirdCategory: String( + persistedWorkflow.inputs?.thirdCategory || "" + ).trim(), + fourthCategory: String( + persistedWorkflow.inputs?.fourthCategory || "" + ).trim(), + fifthCategory: String( + persistedWorkflow.inputs?.fifthCategory || "" + ).trim(), + priceBand: String( + persistedWorkflow.inputs?.priceBand || "" + ).trim(), }); setWorkflowMaterials(nextWorkflowMaterials); setAppearanceRecognitionEnabled(nextAppearanceRecognitionEnabled); @@ -1908,15 +2142,27 @@ export default function CanvasEditorPage({ params }: PageProps) { persistedWorkflow.selectedBrandModel ? { ...persistedWorkflow.selectedBrandModel, - brandName: String(persistedWorkflow.selectedBrandModel.brandName || "").trim(), - id: String(persistedWorkflow.selectedBrandModel.id || "").trim(), - imageUrl: String(persistedWorkflow.selectedBrandModel.imageUrl || "").trim(), + brandName: String( + persistedWorkflow.selectedBrandModel.brandName || "" + ).trim(), + id: String( + persistedWorkflow.selectedBrandModel.id || "" + ).trim(), + imageUrl: String( + persistedWorkflow.selectedBrandModel.imageUrl || "" + ).trim(), assetId: - String(persistedWorkflow.selectedBrandModel.assetId || "").trim() || null, + String( + persistedWorkflow.selectedBrandModel.assetId || "" + ).trim() || null, displayName: - String(persistedWorkflow.selectedBrandModel.displayName || "").trim() || null, + String( + persistedWorkflow.selectedBrandModel.displayName || "" + ).trim() || null, mimeType: - String(persistedWorkflow.selectedBrandModel.mimeType || "").trim() || null, + String( + persistedWorkflow.selectedBrandModel.mimeType || "" + ).trim() || null, sortOrder: Number.isFinite( Number(persistedWorkflow.selectedBrandModel.sortOrder) ) @@ -1924,7 +2170,11 @@ export default function CanvasEditorPage({ params }: PageProps) { 0, Math.min( 999, - Math.round(Number(persistedWorkflow.selectedBrandModel.sortOrder)) + Math.round( + Number( + persistedWorkflow.selectedBrandModel.sortOrder + ) + ) ) ) : 0, @@ -1932,7 +2182,8 @@ export default function CanvasEditorPage({ params }: PageProps) { : null ); setSelectedAgentPresetId( - String(persistedWorkflow.selectedAgentPresetId || "").trim() || null + String(persistedWorkflow.selectedAgentPresetId || "").trim() || + null ); setCurrentAgentStage( persistedWorkflow.currentStage === "mode" ? "mode" : "workflow" @@ -1952,7 +2203,9 @@ export default function CanvasEditorPage({ params }: PageProps) { } else { if (isUnauthorizedApiError(result.error)) { setIsAuthenticated(false); - openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); setWorkflowHydrationSource("empty"); setIsLoading(false); lastLoadedCanvasIdRef.current = id; @@ -1960,11 +2213,13 @@ export default function CanvasEditorPage({ params }: PageProps) { } // 画布不存在时,创建一个新画布 - console.log('[Canvas] 画布不存在,创建新画布:', id); - useCanvasStore.getState().setProject( - id, - requestedAgentMode ? '智能体工作台' : defaultCanvasTitle - ); + console.log("[Canvas] 画布不存在,创建新画布:", id); + useCanvasStore + .getState() + .setProject( + id, + requestedAgentMode ? "智能体工作台" : defaultCanvasTitle + ); setWorkflowHydrationSource("empty"); if (!requestedAgentMode) { // 保存新画布到数据库 @@ -1976,21 +2231,27 @@ export default function CanvasEditorPage({ params }: PageProps) { edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, - tool: 'select', + tool: "select", }, activeCanvasWorkspaceType ); } catch (error) { - console.error('[Canvas] 创建新画布失败:', error); + console.error("[Canvas] 创建新画布失败:", error); } } } setIsLoading(false); lastLoadedCanvasIdRef.current = id; }; - + loadProject(); - }, [activeCanvasWorkspaceType, defaultCanvasTitle, id, openLoginDialog, requestedAgentMode]); + }, [ + activeCanvasWorkspaceType, + defaultCanvasTitle, + id, + openLoginDialog, + requestedAgentMode, + ]); useEffect(() => { const updateViewportWidth = () => setViewportWidth(window.innerWidth); @@ -2017,9 +2278,7 @@ export default function CanvasEditorPage({ params }: PageProps) { return { ...getCanvasState(), }; - }, [ - getCanvasState, - ]); + }, [getCanvasState]); const handleExportVideoCanvasProject = useCallback(() => { if (!isVideoMode || typeof window === "undefined") { @@ -2052,7 +2311,13 @@ export default function CanvasEditorPage({ params }: PageProps) { anchor.remove(); URL.revokeObjectURL(url); toast.success("已导出当前视频画布工程"); - }, [activeCanvasWorkspaceType, buildPersistedCanvasData, id, isVideoMode, projectTitle]); + }, [ + activeCanvasWorkspaceType, + buildPersistedCanvasData, + id, + isVideoMode, + projectTitle, + ]); const handleImportVideoCanvasProjectClick = useCallback(() => { if (!isVideoMode) { @@ -2099,7 +2364,11 @@ export default function CanvasEditorPage({ params }: PageProps) { setProjectTitle(nextTitle); const [saveResult, updateResult] = await Promise.all([ - canvasService.saveCanvasState(id, normalized.canvasData, activeCanvasWorkspaceType), + canvasService.saveCanvasState( + id, + normalized.canvasData, + activeCanvasWorkspaceType + ), canvasService.updateProject( id, { @@ -2114,12 +2383,16 @@ export default function CanvasEditorPage({ params }: PageProps) { } if (!updateResult.success) { - console.warn("[Canvas] import title update failed:", updateResult.error); + console.warn( + "[Canvas] import title update failed:", + updateResult.error + ); } toast.success(`已导入视频画布:${nextTitle}`); } catch (error) { - const message = error instanceof Error ? error.message : "导入视频画布失败"; + const message = + error instanceof Error ? error.message : "导入视频画布失败"; toast.error(message); } finally { setIsImportingVideoCanvas(false); @@ -2194,10 +2467,18 @@ export default function CanvasEditorPage({ params }: PageProps) { }, resultImageUrls: Array.from( new Set([ - ...normalizeResultCollectionUrls(agentProjectResultCollections.scene_generation), - ...normalizeResultCollectionUrls(agentProjectResultCollections.detail_generation), - ...normalizeResultCollectionUrls(agentProjectResultCollections.detail_replacement), - ...normalizeResultCollectionUrls(agentProjectResultCollections.sku_replacement), + ...normalizeResultCollectionUrls( + agentProjectResultCollections.scene_generation + ), + ...normalizeResultCollectionUrls( + agentProjectResultCollections.detail_generation + ), + ...normalizeResultCollectionUrls( + agentProjectResultCollections.detail_replacement + ), + ...normalizeResultCollectionUrls( + agentProjectResultCollections.sku_replacement + ), ]) ), }; @@ -2214,7 +2495,9 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowMaterials, sanitizedWorkflowSelections, ]); - const persistedAgentProjectStateRef = useRef(buildPersistedAgentProjectState()); + const persistedAgentProjectStateRef = useRef( + buildPersistedAgentProjectState() + ); useEffect(() => { persistedAgentProjectStateRef.current = buildPersistedAgentProjectState(); @@ -2227,28 +2510,42 @@ export default function CanvasEditorPage({ params }: PageProps) { thumbnail?: string | null; projectState?: AgentGenerationProjectPersistedState | null; }) => { - const persistedState = normalizeAgentGenerationProjectPersistedState(params.projectState); + const persistedState = normalizeAgentGenerationProjectPersistedState( + params.projectState + ); const persistedWorkflow = - persistedState.workflowState || createEmptyAgentWorkflowPersistedState(); + persistedState.workflowState || + createEmptyAgentWorkflowPersistedState(); const nextAppearanceRecognitionEnabled = persistedWorkflow.appearanceRecognitionEnabled !== false; const nextWorkflowMaterials = nextAppearanceRecognitionEnabled ? normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) : clearAppearanceSummaryFromMaterials( - normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials) + normalizeAgentWorkflowMaterialSelections( + persistedWorkflow.materials + ) ); const nextWorkflowInputs = { primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(), - secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(), + secondarySku: String( + persistedWorkflow.inputs?.secondarySku || "" + ).trim(), brand: String(persistedWorkflow.inputs?.brand || "").trim(), store: String(persistedWorkflow.inputs?.store || "").trim(), - thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(), - fourthCategory: String(persistedWorkflow.inputs?.fourthCategory || "").trim(), - fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(), + thirdCategory: String( + persistedWorkflow.inputs?.thirdCategory || "" + ).trim(), + fourthCategory: String( + persistedWorkflow.inputs?.fourthCategory || "" + ).trim(), + fifthCategory: String( + persistedWorkflow.inputs?.fifthCategory || "" + ).trim(), priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(), }; const nextWorkflowSelections = persistedWorkflow.selections || {}; - const nextSelectedBrandModel = persistedWorkflow.selectedBrandModel || null; + const nextSelectedBrandModel = + persistedWorkflow.selectedBrandModel || null; const nextSelectedAgentPresetId = String(persistedWorkflow.selectedAgentPresetId || "").trim() || null; const nextCurrentAgentStage = @@ -2257,12 +2554,14 @@ export default function CanvasEditorPage({ params }: PageProps) { persistedWorkflow.agentModeState ); const nextAgentWorkspaceState = - persistedState.workspaceState || createEmptyAgentGenerationProjectWorkspaceState(); + persistedState.workspaceState || + createEmptyAgentGenerationProjectWorkspaceState(); workflowInputsRef.current = nextWorkflowInputs; workflowMaterialsRef.current = nextWorkflowMaterials; workflowSelectionsRef.current = nextWorkflowSelections; - appearanceRecognitionEnabledRef.current = nextAppearanceRecognitionEnabled; + appearanceRecognitionEnabledRef.current = + nextAppearanceRecognitionEnabled; selectedBrandModelRef.current = nextSelectedBrandModel; setWorkflowInputs(nextWorkflowInputs); @@ -2274,7 +2573,9 @@ export default function CanvasEditorPage({ params }: PageProps) { setCurrentAgentStage(nextCurrentAgentStage); setAgentModeState(nextAgentModeState); setAgentWorkspaceState(nextAgentWorkspaceState); - setAgentProjectTitle(String(params.title || "").trim() || "未命名生图项目"); + setAgentProjectTitle( + String(params.title || "").trim() || "未命名生图项目" + ); setAgentProjectThumbnail(String(params.thumbnail || "").trim() || null); setAgentProjectResultCollections(persistedState.resultCollections || {}); setAgentProjectDeletedResultCollections( @@ -2298,7 +2599,8 @@ export default function CanvasEditorPage({ params }: PageProps) { } const parsed = JSON.parse(raw) as AgentWorkflowPersistedState; - const nextAppearanceRecognitionEnabled = parsed.appearanceRecognitionEnabled !== false; + const nextAppearanceRecognitionEnabled = + parsed.appearanceRecognitionEnabled !== false; const nextWorkflowMaterials = nextAppearanceRecognitionEnabled ? normalizeAgentWorkflowMaterialSelections(parsed.materials) : clearAppearanceSummaryFromMaterials( @@ -2321,23 +2623,46 @@ export default function CanvasEditorPage({ params }: PageProps) { parsed.selectedBrandModel ? { ...parsed.selectedBrandModel, - brandName: String(parsed.selectedBrandModel.brandName || "").trim(), + brandName: String( + parsed.selectedBrandModel.brandName || "" + ).trim(), id: String(parsed.selectedBrandModel.id || "").trim(), imageUrl: String(parsed.selectedBrandModel.imageUrl || "").trim(), - assetId: String(parsed.selectedBrandModel.assetId || "").trim() || null, - displayName: String(parsed.selectedBrandModel.displayName || "").trim() || null, - mimeType: String(parsed.selectedBrandModel.mimeType || "").trim() || null, - sortOrder: Number.isFinite(Number(parsed.selectedBrandModel.sortOrder)) - ? Math.max(0, Math.min(999, Math.round(Number(parsed.selectedBrandModel.sortOrder)))) + assetId: + String(parsed.selectedBrandModel.assetId || "").trim() || null, + displayName: + String(parsed.selectedBrandModel.displayName || "").trim() || + null, + mimeType: + String(parsed.selectedBrandModel.mimeType || "").trim() || null, + sortOrder: Number.isFinite( + Number(parsed.selectedBrandModel.sortOrder) + ) + ? Math.max( + 0, + Math.min( + 999, + Math.round(Number(parsed.selectedBrandModel.sortOrder)) + ) + ) : 0, } : null ); - setSelectedAgentPresetId(String(parsed.selectedAgentPresetId || "").trim() || null); - setCurrentAgentStage(parsed.currentStage === "mode" ? "mode" : "workflow"); - setAgentModeState(normalizeAgentModeWorkspaceState(parsed.agentModeState)); + setSelectedAgentPresetId( + String(parsed.selectedAgentPresetId || "").trim() || null + ); + setCurrentAgentStage( + parsed.currentStage === "mode" ? "mode" : "workflow" + ); + setAgentModeState( + normalizeAgentModeWorkspaceState(parsed.agentModeState) + ); } catch (error) { - console.warn("[Canvas] Failed to hydrate workflow state from localStorage:", error); + console.warn( + "[Canvas] Failed to hydrate workflow state from localStorage:", + error + ); } finally { setWorkflowHydrationSource("local"); } @@ -2345,7 +2670,8 @@ export default function CanvasEditorPage({ params }: PageProps) { useEffect(() => { if (isAgentMode) return; - if (typeof window === "undefined" || workflowHydrationSource === "pending") return; + if (typeof window === "undefined" || workflowHydrationSource === "pending") + return; try { const persistedMaterials = appearanceRecognitionEnabled @@ -2382,7 +2708,8 @@ export default function CanvasEditorPage({ params }: PageProps) { ]); useEffect(() => { - if (!isAgentMode || !agentProjectId || !hasLoadedCurrentAgentProject) return; + if (!isAgentMode || !agentProjectId || !hasLoadedCurrentAgentProject) + return; if (typeof window === "undefined") return; try { @@ -2439,14 +2766,18 @@ export default function CanvasEditorPage({ params }: PageProps) { if (!result.success) { if (isUnauthorizedApiError(result.error)) { setIsAuthenticated(false); - openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); } console.error(`[Agent Project] ${reason} save failed:`, result.error); return false; } if (result.data) { - setAgentProjectTitle(result.data.title || buildDefaultAgentProjectTitle()); + setAgentProjectTitle( + result.data.title || buildDefaultAgentProjectTitle() + ); setAgentProjectThumbnail(result.data.thumbnail || null); } @@ -2476,7 +2807,9 @@ export default function CanvasEditorPage({ params }: PageProps) { if (!result.success || !result.data) { if (isUnauthorizedApiError(result.error)) { setIsAuthenticated(false); - openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); if (!cancelled && localDraft?.projectState) { hydrateAgentProjectSnapshot({ projectId: agentProjectId, @@ -2491,7 +2824,8 @@ export default function CanvasEditorPage({ params }: PageProps) { } if (cancelled) return; - const serverUpdatedAt = Number(new Date(result.data.updatedAt).getTime()) || 0; + const serverUpdatedAt = + Number(new Date(result.data.updatedAt).getTime()) || 0; const shouldUseLocalDraft = Boolean(localDraft?.projectState) && Number(localDraft?.savedAt || 0) > serverUpdatedAt; @@ -2514,7 +2848,9 @@ export default function CanvasEditorPage({ params }: PageProps) { }); } catch (error) { if (!cancelled) { - toast.error(error instanceof Error ? error.message : "加载生图项目失败"); + toast.error( + error instanceof Error ? error.message : "加载生图项目失败" + ); } } finally { if (!cancelled) { @@ -2527,7 +2863,12 @@ export default function CanvasEditorPage({ params }: PageProps) { return () => { cancelled = true; }; - }, [agentProjectId, hydrateAgentProjectSnapshot, openLoginDialog, requestedAgentMode]); + }, [ + agentProjectId, + hydrateAgentProjectSnapshot, + openLoginDialog, + requestedAgentMode, + ]); useEffect(() => { if (isAgentProjectSheetOpen) { @@ -2602,7 +2943,10 @@ export default function CanvasEditorPage({ params }: PageProps) { JSON.stringify(draftSnapshot) ); } catch (error) { - console.warn("[Agent Project] Failed to persist local draft before unload:", error); + console.warn( + "[Agent Project] Failed to persist local draft before unload:", + error + ); } try { @@ -2610,7 +2954,9 @@ export default function CanvasEditorPage({ params }: PageProps) { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - title: String(agentProjectTitle || "").trim() || buildDefaultAgentProjectTitle(), + title: + String(agentProjectTitle || "").trim() || + buildDefaultAgentProjectTitle(), thumbnail: agentProjectThumbnail, projectState: persistedAgentProjectStateRef.current, }), @@ -2638,7 +2984,10 @@ export default function CanvasEditorPage({ params }: PageProps) { const handleSaveAsAgentProject = useCallback(async () => { const suggestedTitle = buildSaveAsAgentProjectTitle(agentProjectTitle); - const promptedTitle = window.prompt("请输入另存为的新项目名称", suggestedTitle); + const promptedTitle = window.prompt( + "请输入另存为的新项目名称", + suggestedTitle + ); if (promptedTitle === null) { return; } @@ -2662,7 +3011,9 @@ export default function CanvasEditorPage({ params }: PageProps) { if (!result.success || !result.data) { if (isUnauthorizedApiError(result.error)) { setIsAuthenticated(false); - openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); } toast.error(result.error || "另存为新项目失败"); return; @@ -2706,7 +3057,9 @@ export default function CanvasEditorPage({ params }: PageProps) { if (!result.success || !result.data) { if (isUnauthorizedApiError(result.error)) { setIsAuthenticated(false); - openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); } toast.error(result.error || "更新生图项目名称失败"); return; @@ -2741,7 +3094,9 @@ export default function CanvasEditorPage({ params }: PageProps) { if (!result.success || !result.data) { if (isUnauthorizedApiError(result.error)) { setIsAuthenticated(false); - openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); } toast.error(result.error || "创建生图项目失败"); return; @@ -2779,7 +3134,9 @@ export default function CanvasEditorPage({ params }: PageProps) { if (!result.success) { if (isUnauthorizedApiError(result.error)) { setIsAuthenticated(false); - openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); } toast.error(result.error || "删除生图项目失败"); return; @@ -2791,10 +3148,14 @@ export default function CanvasEditorPage({ params }: PageProps) { const listResult = await agentProjectService.getProjects(); if (!listResult.success && isUnauthorizedApiError(listResult.error)) { setIsAuthenticated(false); - openLoginDialog(listResult.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + listResult.error || "当前登录状态已失效,请重新登录后继续操作。" + ); } const nextProject = listResult.success - ? listResult.data?.find((project) => project.id !== targetProjectId) || null + ? listResult.data?.find( + (project) => project.id !== targetProjectId + ) || null : null; if (nextProject) { @@ -2824,7 +3185,9 @@ export default function CanvasEditorPage({ params }: PageProps) { ...current, [modeType]: normalizedUrls, }; - return areResultCollectionsEqual(current, nextCollections) ? current : nextCollections; + return areResultCollectionsEqual(current, nextCollections) + ? current + : nextCollections; }); setAgentProjectThumbnail((current) => { const nextThumbnail = @@ -2863,7 +3226,9 @@ export default function CanvasEditorPage({ params }: PageProps) { }; setAgentProjectResultCollections((current) => - areResultCollectionsEqual(current, nextCollections) ? current : nextCollections + areResultCollectionsEqual(current, nextCollections) + ? current + : nextCollections ); setAgentProjectDeletedResultCollections((current) => areResultCollectionsEqual(current, nextDeletedCollections) @@ -2898,7 +3263,10 @@ export default function CanvasEditorPage({ params }: PageProps) { autoSelectedBrandModelBrandRef.current = ""; } - if (selectedBrandModel && selectedBrandModel.brandName.toLowerCase() !== normalizedBrand) { + if ( + selectedBrandModel && + selectedBrandModel.brandName.toLowerCase() !== normalizedBrand + ) { setSelectedBrandModel(null); return; } @@ -2926,7 +3294,11 @@ export default function CanvasEditorPage({ params }: PageProps) { ...defaultBrandModel, brandName: matchedEntry?.brandName || workflowInputs.brand.trim(), }); - }, [resolvedBrandModelConfig.entries, selectedBrandModel, workflowInputs.brand]); + }, [ + resolvedBrandModelConfig.entries, + selectedBrandModel, + workflowInputs.brand, + ]); const persistCanvasState = useCallback( async (reason: "autosave" | "delete" | "beforeunload") => { @@ -3002,7 +3374,10 @@ export default function CanvasEditorPage({ params }: PageProps) { void fetch(`/api/canvas/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ canvasData, workspaceType: activeCanvasWorkspaceType }), + body: JSON.stringify({ + canvasData, + workspaceType: activeCanvasWorkspaceType, + }), keepalive: true, }); } catch (error) { @@ -3016,7 +3391,13 @@ export default function CanvasEditorPage({ params }: PageProps) { window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("pagehide", handleBeforeUnload); }; - }, [activeCanvasWorkspaceType, buildPersistedCanvasData, id, isAgentMode, isLoading]); + }, [ + activeCanvasWorkspaceType, + buildPersistedCanvasData, + id, + isAgentMode, + isLoading, + ]); // 智能命名与缩略图逻辑 useEffect(() => { @@ -3027,7 +3408,9 @@ export default function CanvasEditorPage({ params }: PageProps) { projectTitle === "新视频画布"; if (!hasDefaultTitle || nodes.length === 0) return; - const imageNode = nodes.find((n) => n.type === "image" && (n.data as any)?.src); + const imageNode = nodes.find( + (n) => n.type === "image" && (n.data as any)?.src + ); const imageSrc = (imageNode?.data as any)?.src; if (!imageSrc || typeof imageSrc !== "string") return; @@ -3104,7 +3487,7 @@ export default function CanvasEditorPage({ params }: PageProps) { canvasData, activeCanvasWorkspaceType ); - + if (result.success) { toast.success("保存成功"); } else { @@ -3119,7 +3502,7 @@ export default function CanvasEditorPage({ params }: PageProps) { setIsEditingTitle(false); return; } - + const result = await canvasService.updateProject( id, { title: editTitle.trim() }, @@ -3141,49 +3524,61 @@ export default function CanvasEditorPage({ params }: PageProps) { [viewport.x, viewport.y, viewport.zoom] ); - const guessCanvasAspectRatio = useCallback((nodeId: string | null | undefined) => { - const targetNode = nodeId ? nodes.find((node) => node.id === nodeId) || null : null; - if (!targetNode) return "auto"; - const currentRatio = targetNode.size.width / Math.max(1, targetNode.size.height); + const guessCanvasAspectRatio = useCallback( + (nodeId: string | null | undefined) => { + const targetNode = nodeId + ? nodes.find((node) => node.id === nodeId) || null + : null; + if (!targetNode) return "auto"; + const currentRatio = + targetNode.size.width / Math.max(1, targetNode.size.height); - let bestPreset = SIZE_PRESETS[0]; - let bestDelta = Number.POSITIVE_INFINITY; - SIZE_PRESETS.forEach((preset) => { - const ratio = preset.width / preset.height; - const delta = Math.abs(ratio - currentRatio); - if (delta < bestDelta) { - bestDelta = delta; - bestPreset = preset; - } - }); + let bestPreset = SIZE_PRESETS[0]; + let bestDelta = Number.POSITIVE_INFINITY; + SIZE_PRESETS.forEach((preset) => { + const ratio = preset.width / preset.height; + const delta = Math.abs(ratio - currentRatio); + if (delta < bestDelta) { + bestDelta = delta; + bestPreset = preset; + } + }); - return bestPreset?.value || "auto"; - }, [nodes]); + return bestPreset?.value || "auto"; + }, + [nodes] + ); - const guessVideoRatio = useCallback((nodeId: string | null | undefined) => { - const targetNode = nodeId ? nodes.find((node) => node.id === nodeId) || null : null; - if (!targetNode) return "16:9"; - const currentRatio = targetNode.size.width / Math.max(1, targetNode.size.height); - const ratios = [ - { value: "16:9", width: 16, height: 9 }, - { value: "4:3", width: 4, height: 3 }, - { value: "1:1", width: 1, height: 1 }, - { value: "3:4", width: 3, height: 4 }, - { value: "9:16", width: 9, height: 16 }, - { value: "21:9", width: 21, height: 9 }, - ] as const; + const guessVideoRatio = useCallback( + (nodeId: string | null | undefined) => { + const targetNode = nodeId + ? nodes.find((node) => node.id === nodeId) || null + : null; + if (!targetNode) return "16:9"; + const currentRatio = + targetNode.size.width / Math.max(1, targetNode.size.height); + const ratios = [ + { value: "16:9", width: 16, height: 9 }, + { value: "4:3", width: 4, height: 3 }, + { value: "1:1", width: 1, height: 1 }, + { value: "3:4", width: 3, height: 4 }, + { value: "9:16", width: 9, height: 16 }, + { value: "21:9", width: 21, height: 9 }, + ] as const; - let best: (typeof ratios)[number] = ratios[0]; - let bestDelta = Number.POSITIVE_INFINITY; - ratios.forEach((ratio) => { - const delta = Math.abs(ratio.width / ratio.height - currentRatio); - if (delta < bestDelta) { - bestDelta = delta; - best = ratio; - } - }); - return best.value; - }, [nodes]); + let best: (typeof ratios)[number] = ratios[0]; + let bestDelta = Number.POSITIVE_INFINITY; + ratios.forEach((ratio) => { + const delta = Math.abs(ratio.width / ratio.height - currentRatio); + if (delta < bestDelta) { + bestDelta = delta; + best = ratio; + } + }); + return best.value; + }, + [nodes] + ); const patchWorkflowNode = useCallback( ( @@ -3234,7 +3629,11 @@ export default function CanvasEditorPage({ params }: PageProps) { const nodeId = addNode({ type: params.media.kind, position: { - x: anchorNode.position.x + anchorNode.size.width + 180 + (params.offsetX || 0), + x: + anchorNode.position.x + + anchorNode.size.width + + 180 + + (params.offsetX || 0), y: anchorNode.position.y + anchorNode.size.height / 2 - @@ -3278,7 +3677,11 @@ export default function CanvasEditorPage({ params }: PageProps) { ); const syncDirectPreviewNodes = useCallback( - (sourceNodeId: string, media: WorkflowMediaReference, sourcePrompt: string) => { + ( + sourceNodeId: string, + media: WorkflowMediaReference, + sourcePrompt: string + ) => { getDirectDownstreamNodes(sourceNodeId, workflowGraph).forEach((node) => { const downstreamType = inferVideoWorkflowTemplateType(node); if (node.type !== "workflow" || downstreamType !== "preview") return; @@ -3299,18 +3702,18 @@ export default function CanvasEditorPage({ params }: PageProps) { ); const handleSyncAgentResultsToCanvas = useCallback(async () => { - if (!linkedAgentWorkflowNode || !linkedAgentWorkflowData || !agentProjectId) { + if ( + !linkedAgentWorkflowNode || + !linkedAgentWorkflowData || + !agentProjectId + ) { toast.error("当前 Agent 项目还没有关联到视频画布节点。"); return; } const composeText = (parts: unknown[]) => Array.from( - new Set( - parts - .map((item) => String(item || "").trim()) - .filter(Boolean) - ) + new Set(parts.map((item) => String(item || "").trim()).filter(Boolean)) ).join("\n"); const resolveBatchOffset = (index: number) => ({ x: (index % 2) * (320 + 28), @@ -3321,7 +3724,9 @@ export default function CanvasEditorPage({ params }: PageProps) { nodes .flatMap((node) => { if (node.type === "image" || node.type === "video") { - return [String((node.data as Record)?.src || "").trim()]; + return [ + String((node.data as Record)?.src || "").trim(), + ]; } return []; }) @@ -3338,19 +3743,25 @@ export default function CanvasEditorPage({ params }: PageProps) { String(linkedAgentWorkflowData.summary || "").trim(), String(agentWorkspaceState.input || "").trim(), ]) || "来自 Agent 工作台的结果回流。"; - const previewResult = (resultsToImport[0] || flattenedAgentResultEntries[0] || null) - ? { - nodeId: sourceNodeId, - kind: "image" as const, - url: String((resultsToImport[0] || flattenedAgentResultEntries[0])?.url || "").trim(), - title: `${String((resultsToImport[0] || flattenedAgentResultEntries[0])?.label || "Agent 结果").trim()} ${Number((resultsToImport[0] || flattenedAgentResultEntries[0])?.index || 0) + 1}`, - prompt: composeText([ - sourcePrompt, - String((resultsToImport[0] || flattenedAgentResultEntries[0])?.label || "").trim(), - ]), - source: "workflow-result" as const, - } - : null; + const previewResult = + resultsToImport[0] || flattenedAgentResultEntries[0] || null + ? { + nodeId: sourceNodeId, + kind: "image" as const, + url: String( + (resultsToImport[0] || flattenedAgentResultEntries[0])?.url || "" + ).trim(), + title: `${String((resultsToImport[0] || flattenedAgentResultEntries[0])?.label || "Agent 结果").trim()} ${Number((resultsToImport[0] || flattenedAgentResultEntries[0])?.index || 0) + 1}`, + prompt: composeText([ + sourcePrompt, + String( + (resultsToImport[0] || flattenedAgentResultEntries[0])?.label || + "" + ).trim(), + ]), + source: "workflow-result" as const, + } + : null; const resultGroupSummaries = ( [ "scene_generation", @@ -3366,19 +3777,19 @@ export default function CanvasEditorPage({ params }: PageProps) { importedCount: number; }> >((groups, modeType) => { - const urls = flattenedAgentResultEntries - .filter((item) => item.modeType === modeType) - .map((item) => item.url); - if (urls.length === 0) return groups; - const importedCount = urls.filter((url) => importedUrls.has(url)).length; - groups.push({ - modeType, - label: AGENT_RESULT_MODE_LABELS[modeType], - count: urls.length, - importedCount, - }); - return groups; - }, []); + const urls = flattenedAgentResultEntries + .filter((item) => item.modeType === modeType) + .map((item) => item.url); + if (urls.length === 0) return groups; + const importedCount = urls.filter((url) => importedUrls.has(url)).length; + groups.push({ + modeType, + label: AGENT_RESULT_MODE_LABELS[modeType], + count: urls.length, + importedCount, + }); + return groups; + }, []); setIsSyncingAgentResultsToCanvas(true); patchWorkflowNode(sourceNodeId, { @@ -3402,10 +3813,7 @@ export default function CanvasEditorPage({ params }: PageProps) { resultsToImport.forEach((item, index) => { const offset = resolveBatchOffset(index); - const promptText = composeText([ - sourcePrompt, - item.label, - ]); + const promptText = composeText([sourcePrompt, item.label]); const media: WorkflowMediaReference = { nodeId: sourceNodeId, kind: "image", @@ -3428,9 +3836,14 @@ export default function CanvasEditorPage({ params }: PageProps) { return; } - const createdNode = useCanvasStore.getState().nodes.find((node) => node.id === createdNodeId); + const createdNode = useCanvasStore + .getState() + .nodes.find((node) => node.id === createdNodeId); if (createdNode) { - const currentImageData = (createdNode.data || {}) as Record; + const currentImageData = (createdNode.data || {}) as Record< + string, + any + >; updateNode(createdNodeId, { data: { src: String(currentImageData.src || media.url).trim(), @@ -3459,17 +3872,23 @@ export default function CanvasEditorPage({ params }: PageProps) { const latestNodes = useCanvasStore.getState().nodes; const allImportedImageNodes = latestNodes.filter((node) => { if (node.type !== "image") return false; - const metadata = (((node.data as Record)?.metadata || {}) as Record); + const metadata = ((node.data as Record)?.metadata || + {}) as Record; return ( String(metadata.sourceWorkflowNodeId || "").trim() === sourceNodeId && metadata.importedFromAgentProject === true && String(metadata.sourceAgentProjectId || "").trim() === agentProjectId ); }); - const directOutputNodes = getDirectDownstreamNodes(sourceNodeId, workflowGraph).filter( + const directOutputNodes = getDirectDownstreamNodes( + sourceNodeId, + workflowGraph + ).filter( (node) => node.type === "workflow" && - ["preview", "local-save"].includes(inferVideoWorkflowTemplateType(node) || "") + ["preview", "local-save"].includes( + inferVideoWorkflowTemplateType(node) || "" + ) ); const syncedAgentAssets = allImportedImageNodes.map((node, index) => { const imageData = (node.data || {}) as Record; @@ -3579,7 +3998,10 @@ export default function CanvasEditorPage({ params }: PageProps) { : "没有新的 Agent 结果需要回流" ); } catch (error) { - const message = error instanceof Error ? error.message : "同步 Agent 结果到视频画布失败"; + const message = + error instanceof Error + ? error.message + : "同步 Agent 结果到视频画布失败"; patchWorkflowNode(sourceNodeId, { status: "failed", summary: message, @@ -3634,7 +4056,10 @@ export default function CanvasEditorPage({ params }: PageProps) { })); getDirectDownstreamNodes(sourceNodeId, workflowGraph).forEach((node) => { const downstreamType = inferVideoWorkflowTemplateType(node); - if (node.type !== "workflow" || downstreamType !== "storyboard-chart-node") { + if ( + node.type !== "workflow" || + downstreamType !== "storyboard-chart-node" + ) { return; } @@ -3656,36 +4081,42 @@ export default function CanvasEditorPage({ params }: PageProps) { [patchWorkflowNode, workflowGraph] ); - const downloadWorkflowAssets = useCallback((assets: WorkflowMediaReference[], folderLabel: string) => { - if (typeof document === "undefined" || assets.length === 0) { - return; - } + const downloadWorkflowAssets = useCallback( + (assets: WorkflowMediaReference[], folderLabel: string) => { + if (typeof document === "undefined" || assets.length === 0) { + return; + } - const now = new Date(); - const folderName = `${folderLabel}-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`; - const form = document.createElement("form"); - form.method = "POST"; - form.action = "/api/canvas/download-zip"; - form.style.display = "none"; + const now = new Date(); + const folderName = `${folderLabel}-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`; + const form = document.createElement("form"); + form.method = "POST"; + form.action = "/api/canvas/download-zip"; + form.style.display = "none"; - const payloadInput = document.createElement("input"); - payloadInput.type = "hidden"; - payloadInput.name = "payload"; - payloadInput.value = JSON.stringify({ - folderName, - files: assets.map((asset, index) => ({ - url: asset.url, - name: asset.title || `${asset.kind}-result-${index + 1}`, - })), - }); - form.appendChild(payloadInput); - document.body.appendChild(form); - form.submit(); - form.remove(); - }, []); + const payloadInput = document.createElement("input"); + payloadInput.type = "hidden"; + payloadInput.name = "payload"; + payloadInput.value = JSON.stringify({ + folderName, + files: assets.map((asset, index) => ({ + url: asset.url, + name: asset.title || `${asset.kind}-result-${index + 1}`, + })), + }); + form.appendChild(payloadInput); + document.body.appendChild(form); + form.submit(); + form.remove(); + }, + [] + ); const buildStoryboardShots = useCallback( - (promptText: string, mediaSummary: { imageCount: number; videoCount: number }) => { + ( + promptText: string, + mediaSummary: { imageCount: number; videoCount: number } + ) => { const fragments = Array.from( new Set( promptText @@ -3699,7 +4130,12 @@ export default function CanvasEditorPage({ params }: PageProps) { const seeds = fragments.length > 0 ? fragments.slice(0, 4) - : ["围绕主体建立开场氛围", "推进主体动作与情绪", "强化关键细节", "用结尾镜头完成收束"]; + : [ + "围绕主体建立开场氛围", + "推进主体动作与情绪", + "强化关键细节", + "用结尾镜头完成收束", + ]; const suffix = mediaSummary.imageCount > 0 @@ -3730,11 +4166,7 @@ export default function CanvasEditorPage({ params }: PageProps) { const buildWorkflowTextBlock = useCallback((parts: unknown[]) => { return Array.from( - new Set( - parts - .map((item) => String(item || "").trim()) - .filter(Boolean) - ) + new Set(parts.map((item) => String(item || "").trim()).filter(Boolean)) ).join("\n"); }, []); @@ -3952,30 +4384,37 @@ export default function CanvasEditorPage({ params }: PageProps) { [buildStoryboardChartRows, selectedWorkflowStructuredData] ); - const buildWorkflowCompareItems = useCallback((media: WorkflowMediaReference[]) => { - return media - .filter((item) => /^https?:\/\//i.test(item.url)) - .slice(0, 4) - .map((item, index) => ({ - label: - index === 0 - ? "基准" - : item.source === "workflow-result" - ? `结果 ${index + 1}` - : `对照 ${index + 1}`, - kind: item.kind, - url: item.url, - title: - String(item.title || item.prompt || `${item.kind === "video" ? "视频" : "图片"} ${index + 1}`).trim(), - })); - }, []); + const buildWorkflowCompareItems = useCallback( + (media: WorkflowMediaReference[]) => { + return media + .filter((item) => /^https?:\/\//i.test(item.url)) + .slice(0, 4) + .map((item, index) => ({ + label: + index === 0 + ? "基准" + : item.source === "workflow-result" + ? `结果 ${index + 1}` + : `对照 ${index + 1}`, + kind: item.kind, + url: item.url, + title: String( + item.title || + item.prompt || + `${item.kind === "video" ? "视频" : "图片"} ${index + 1}` + ).trim(), + })); + }, + [] + ); const buildWorkflowDefaultCropRect = useCallback((promptText: string) => { const normalizedPrompt = String(promptText || "").toLowerCase(); const isPortraitFocus = /人物|角色|主体|局部|特写|脸|肖像|portrait|close/.test(normalizedPrompt); - const isObjectFocus = - /产品|物体|商品|道具|item|product/.test(normalizedPrompt); + const isObjectFocus = /产品|物体|商品|道具|item|product/.test( + normalizedPrompt + ); const width = isPortraitFocus ? 0.5 : isObjectFocus ? 0.56 : 0.64; const height = isPortraitFocus ? 0.72 : isObjectFocus ? 0.58 : 0.64; @@ -4004,10 +4443,14 @@ export default function CanvasEditorPage({ params }: PageProps) { let xScale = match ? Number(match[1]) : 1.45; let yScale = match ? Number(match[1]) : 1.45; - if (/左右|横向|宽屏|全景|横幅|拓宽|扩宽|panorama|wide/.test(normalizedPrompt)) { + if ( + /左右|横向|宽屏|全景|横幅|拓宽|扩宽|panorama|wide/.test(normalizedPrompt) + ) { xScale = Math.max(xScale, 1.7); } - if (/上下|纵向|天空|地面|抬高|向上|向下|tall|vertical/.test(normalizedPrompt)) { + if ( + /上下|纵向|天空|地面|抬高|向上|向下|tall|vertical/.test(normalizedPrompt) + ) { yScale = Math.max(yScale, 1.7); } if (/补边|延展|继续|stitch|outpaint|扩展/.test(normalizedPrompt)) { @@ -4259,13 +4702,19 @@ export default function CanvasEditorPage({ params }: PageProps) { const normalizedPrompt = String(promptText || "").trim(); const rows = [ { - area: /左|left/.test(normalizedPrompt.toLowerCase()) ? "左侧区域" : "主体区域", + area: /左|left/.test(normalizedPrompt.toLowerCase()) + ? "左侧区域" + : "主体区域", intent: /替换|replace/.test(normalizedPrompt) ? "替换元素" : "补充结构", note: normalizedPrompt || "用涂鸦勾出主体、边缘和希望补充的视觉结构。", }, { - area: /背景|environment|scene/.test(normalizedPrompt.toLowerCase()) ? "背景区域" : "外围区域", - intent: /光|氛围|mood/.test(normalizedPrompt.toLowerCase()) ? "调整氛围" : "补充背景", + area: /背景|environment|scene/.test(normalizedPrompt.toLowerCase()) + ? "背景区域" + : "外围区域", + intent: /光|氛围|mood/.test(normalizedPrompt.toLowerCase()) + ? "调整氛围" + : "补充背景", note: "建议用大笔触勾出背景方向、透视和光影分区。", }, { @@ -4291,7 +4740,10 @@ export default function CanvasEditorPage({ params }: PageProps) { }, []); const resolvePreferredCharacterPrompt = useCallback( - (sourcePrompt: string, structuredData: typeof selectedWorkflowStructuredData) => { + ( + sourcePrompt: string, + structuredData: typeof selectedWorkflowStructuredData + ) => { return buildWorkflowTextBlock([ sourcePrompt, structuredData.characterAssets[0]?.prompt, @@ -4302,7 +4754,10 @@ export default function CanvasEditorPage({ params }: PageProps) { ); const resolvePreferredScenePrompt = useCallback( - (sourcePrompt: string, structuredData: typeof selectedWorkflowStructuredData) => { + ( + sourcePrompt: string, + structuredData: typeof selectedWorkflowStructuredData + ) => { return buildWorkflowTextBlock([ sourcePrompt, structuredData.sceneAssets[0]?.prompt, @@ -4320,30 +4775,34 @@ export default function CanvasEditorPage({ params }: PageProps) { }) => { const rawNodeTitle = String(params.nodeTitle || "").trim(); const normalizedNodeTitle = - rawNodeTitle && !["Agent", "自定义代理", "custom-agent"].includes(rawNodeTitle) + rawNodeTitle && + !["Agent", "自定义代理", "custom-agent"].includes(rawNodeTitle) ? rawNodeTitle : ""; - const characterName = - String( - params.structuredData.characterAssets[0]?.name || - params.structuredData.characters[0]?.name || - "" - ).trim(); - const sceneName = - String( - params.structuredData.sceneAssets[0]?.name || - params.structuredData.scenes[0]?.name || - "" - ).trim(); + const characterName = String( + params.structuredData.characterAssets[0]?.name || + params.structuredData.characters[0]?.name || + "" + ).trim(); + const sceneName = String( + params.structuredData.sceneAssets[0]?.name || + params.structuredData.scenes[0]?.name || + "" + ).trim(); const promptSeed = params.promptText .split(/\n+/) .map((item) => item.trim()) .find(Boolean) || "视频画布任务"; const baseTitle = - normalizedNodeTitle || [characterName, sceneName].filter(Boolean).join(" / ") || promptSeed; - const compactTitle = baseTitle.replace(/\s+/g, " ").trim().slice(0, 40) || "视频画布任务"; - return compactTitle.includes("Agent") ? compactTitle : `${compactTitle} Agent`; + normalizedNodeTitle || + [characterName, sceneName].filter(Boolean).join(" / ") || + promptSeed; + const compactTitle = + baseTitle.replace(/\s+/g, " ").trim().slice(0, 40) || "视频画布任务"; + return compactTitle.includes("Agent") + ? compactTitle + : `${compactTitle} Agent`; }, [] ); @@ -4373,9 +4832,7 @@ export default function CanvasEditorPage({ params }: PageProps) { const toLines = (items: string[], limit = 6) => Array.from( new Set( - items - .map((item) => String(item || "").trim()) - .filter(Boolean) + items.map((item) => String(item || "").trim()).filter(Boolean) ) ).slice(0, limit); @@ -4393,7 +4850,9 @@ export default function CanvasEditorPage({ params }: PageProps) { const lines = items .slice(0, limit) .map((item, index) => { - const name = String(item.name || item.title || `条目 ${index + 1}`).trim(); + const name = String( + item.name || item.title || `条目 ${index + 1}` + ).trim(); const detail = buildWorkflowTextBlock([ item.description, item.prompt, @@ -4411,12 +4870,14 @@ export default function CanvasEditorPage({ params }: PageProps) { return buildSection(label, lines, limit); }; - const shotLines = params.storyboardShots.slice(0, 6).map((item, index) => - buildWorkflowTextBlock([ - String(item.title || `镜头 ${index + 1}`).trim(), - String(item.description || "").trim(), - ]).replace(/\n+/g, " / ") - ); + const shotLines = params.storyboardShots + .slice(0, 6) + .map((item, index) => + buildWorkflowTextBlock([ + String(item.title || `镜头 ${index + 1}`).trim(), + String(item.description || "").trim(), + ]).replace(/\n+/g, " / ") + ); return [ buildSection( @@ -4437,7 +4898,10 @@ export default function CanvasEditorPage({ params }: PageProps) { buildSection("交付物", params.structuredData.deliverables, 6), buildSection( "推荐后续动作", - [...params.structuredData.recommendedNextSteps, ...params.structuredData.recommendedNodes], + [ + ...params.structuredData.recommendedNextSteps, + ...params.structuredData.recommendedNodes, + ], 6 ), ] @@ -4448,7 +4912,9 @@ export default function CanvasEditorPage({ params }: PageProps) { ); const buildWorkflowAgentAttachments = useCallback( - (media: WorkflowMediaReference[]): AgentGenerationProjectWorkspaceAttachment[] => { + ( + media: WorkflowMediaReference[] + ): AgentGenerationProjectWorkspaceAttachment[] => { const seen = new Set(); return media .filter((item) => item.kind === "image") @@ -4471,7 +4937,9 @@ export default function CanvasEditorPage({ params }: PageProps) { const mergeWorkflowAgentAttachments = useCallback( ( - existing: Array, + existing: Array< + AgentGenerationProjectWorkspaceAttachment | null | undefined + >, incoming: AgentGenerationProjectWorkspaceAttachment[] ) => { const seen = new Set(); @@ -4494,30 +4962,38 @@ export default function CanvasEditorPage({ params }: PageProps) { [] ); - const mergeWorkflowAgentInput = useCallback((existingInput: string, syncInput: string) => { - const contextBlock = - `${WORKFLOW_AGENT_CONTEXT_START}\n${syncInput.trim()}\n${WORKFLOW_AGENT_CONTEXT_END}`.trim(); - const normalizedExisting = String(existingInput || "").trim(); - if (!normalizedExisting) { - return contextBlock; - } + const mergeWorkflowAgentInput = useCallback( + (existingInput: string, syncInput: string) => { + const contextBlock = + `${WORKFLOW_AGENT_CONTEXT_START}\n${syncInput.trim()}\n${WORKFLOW_AGENT_CONTEXT_END}`.trim(); + const normalizedExisting = String(existingInput || "").trim(); + if (!normalizedExisting) { + return contextBlock; + } - const startIndex = normalizedExisting.indexOf(WORKFLOW_AGENT_CONTEXT_START); - const endIndex = normalizedExisting.indexOf(WORKFLOW_AGENT_CONTEXT_END); + const startIndex = normalizedExisting.indexOf( + WORKFLOW_AGENT_CONTEXT_START + ); + const endIndex = normalizedExisting.indexOf(WORKFLOW_AGENT_CONTEXT_END); - if (startIndex >= 0 && endIndex > startIndex) { - const before = normalizedExisting.slice(0, startIndex).trim(); - const after = normalizedExisting - .slice(endIndex + WORKFLOW_AGENT_CONTEXT_END.length) - .trim(); - return [before, contextBlock, after].filter(Boolean).join("\n\n"); - } + if (startIndex >= 0 && endIndex > startIndex) { + const before = normalizedExisting.slice(0, startIndex).trim(); + const after = normalizedExisting + .slice(endIndex + WORKFLOW_AGENT_CONTEXT_END.length) + .trim(); + return [before, contextBlock, after].filter(Boolean).join("\n\n"); + } - return [contextBlock, normalizedExisting].filter(Boolean).join("\n\n"); - }, []); + return [contextBlock, normalizedExisting].filter(Boolean).join("\n\n"); + }, + [] + ); const handleOpenSelectedWorkflowInAgent = useCallback(async () => { - if (!selectedVideoWorkflowNode || selectedVideoWorkflowType !== "custom-agent") { + if ( + !selectedVideoWorkflowNode || + selectedVideoWorkflowType !== "custom-agent" + ) { return; } @@ -4538,20 +5014,22 @@ export default function CanvasEditorPage({ params }: PageProps) { storyboardShots, structuredData: selectedWorkflowStructuredData, }); - const incomingAttachments = buildWorkflowAgentAttachments(selectedWorkflowMedia); - const firstIncomingAttachmentNodeId = - incomingAttachments[0] - ? selectedWorkflowMedia.find( - (item) => - item.kind === "image" && item.url === incomingAttachments[0]?.url - )?.nodeId || null - : null; + const incomingAttachments = buildWorkflowAgentAttachments( + selectedWorkflowMedia + ); + const firstIncomingAttachmentNodeId = incomingAttachments[0] + ? selectedWorkflowMedia.find( + (item) => + item.kind === "image" && item.url === incomingAttachments[0]?.url + )?.nodeId || null + : null; const inferredAspectRatio = incomingAttachments.length > 0 ? guessCanvasAspectRatio(firstIncomingAttachmentNodeId) : selectedWorkflowInputs.videoCount > 0 ? guessVideoRatio( - selectedWorkflowMedia.find((item) => item.kind === "video")?.nodeId || nodeId + selectedWorkflowMedia.find((item) => item.kind === "video") + ?.nodeId || nodeId ) : "auto"; const nextProjectTitle = buildWorkflowAgentProjectTitle({ @@ -4584,20 +5062,29 @@ export default function CanvasEditorPage({ params }: PageProps) { try { let existingProject: AgentGenerationProject | null = null; if (selectedWorkflowLinkedAgentProject.id) { - const linkedResult = await agentProjectService.getProject(selectedWorkflowLinkedAgentProject.id); + const linkedResult = await agentProjectService.getProject( + selectedWorkflowLinkedAgentProject.id + ); if (linkedResult.success && linkedResult.data) { existingProject = linkedResult.data; } else if (isUnauthorizedApiError(linkedResult.error)) { setIsAuthenticated(false); - openLoginDialog(linkedResult.error || "当前登录状态已失效,请重新登录后继续操作。"); - throw new Error(linkedResult.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + linkedResult.error || "当前登录状态已失效,请重新登录后继续操作。" + ); + throw new Error( + linkedResult.error || "当前登录状态已失效,请重新登录后继续操作。" + ); } } const existingPersistedState = existingProject - ? normalizeAgentGenerationProjectPersistedState(existingProject.projectState) + ? normalizeAgentGenerationProjectPersistedState( + existingProject.projectState + ) : createEmptyAgentGenerationProjectPersistedState(); - const defaultWorkspaceState = createEmptyAgentGenerationProjectWorkspaceState(); + const defaultWorkspaceState = + createEmptyAgentGenerationProjectWorkspaceState(); const nextWorkspaceState: AgentGenerationProjectWorkspaceState = { ...defaultWorkspaceState, ...(existingPersistedState.workspaceState || {}), @@ -4610,12 +5097,17 @@ export default function CanvasEditorPage({ params }: PageProps) { incomingAttachments ), selectedModelId: - String(existingPersistedState.workspaceState?.selectedModelId || "").trim() || "auto", + String( + existingPersistedState.workspaceState?.selectedModelId || "" + ).trim() || "auto", imageParams: { ...defaultWorkspaceState.imageParams, ...(existingPersistedState.workspaceState?.imageParams || {}), aspectRatio: - String(existingPersistedState.workspaceState?.imageParams?.aspectRatio || "").trim() || + String( + existingPersistedState.workspaceState?.imageParams?.aspectRatio || + "" + ).trim() || inferredAspectRatio || "auto", }, @@ -4624,20 +5116,24 @@ export default function CanvasEditorPage({ params }: PageProps) { ...existingPersistedState, workflowState: existingProject ? { - ...(existingPersistedState.workflowState || fallbackWorkflowState), + ...(existingPersistedState.workflowState || + fallbackWorkflowState), currentStage: "mode", } : fallbackWorkflowState, workspaceState: nextWorkspaceState, resultCollections: existingPersistedState.resultCollections || {}, - deletedResultCollections: existingPersistedState.deletedResultCollections || {}, + deletedResultCollections: + existingPersistedState.deletedResultCollections || {}, resultImageUrls: existingPersistedState.resultImageUrls || [], }; const payload = { title: existingProject?.title || nextProjectTitle, - description: existingProject?.description || nextProjectDescription || null, - thumbnail: incomingAttachments[0]?.url || existingProject?.thumbnail || null, + description: + existingProject?.description || nextProjectDescription || null, + thumbnail: + incomingAttachments[0]?.url || existingProject?.thumbnail || null, projectState: nextPersistedState, }; @@ -4648,8 +5144,12 @@ export default function CanvasEditorPage({ params }: PageProps) { if (!result.success || !result.data) { if (isUnauthorizedApiError(result.error)) { setIsAuthenticated(false); - openLoginDialog(result.error || "当前登录状态已失效,请重新登录后继续操作。"); - throw new Error(result.error || "当前登录状态已失效,请重新登录后继续操作。"); + openLoginDialog( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); + throw new Error( + result.error || "当前登录状态已失效,请重新登录后继续操作。" + ); } throw new Error(result.error || "同步 Agent 项目失败"); } @@ -4674,9 +5174,12 @@ export default function CanvasEditorPage({ params }: PageProps) { mode: "agent", agentProjectId: result.data.id, }); - toast.success(existingProject ? "已同步到 Agent 工作台" : "已创建 Agent 项目"); + toast.success( + existingProject ? "已同步到 Agent 工作台" : "已创建 Agent 项目" + ); } catch (error) { - const message = error instanceof Error ? error.message : "打开 Agent 工作台失败"; + const message = + error instanceof Error ? error.message : "打开 Agent 工作台失败"; patchWorkflowNode(nodeId, { status: "failed", summary: message, @@ -4687,7 +5190,9 @@ export default function CanvasEditorPage({ params }: PageProps) { }); toast.error(message); } finally { - setActiveWorkflowExecutionTaskKey((current) => (current === taskKey ? null : current)); + setActiveWorkflowExecutionTaskKey((current) => + current === taskKey ? null : current + ); } }, [ buildCurrentWorkflowPersistedState, @@ -4797,7 +5302,8 @@ export default function CanvasEditorPage({ params }: PageProps) { ) => { const definition = getVideoWorkflowNodeDefinition(templateType); if (!definition) return; - const placement = options?.position || getVideoWorkflowPlacement(definition.size); + const placement = + options?.position || getVideoWorkflowPlacement(definition.size); createCanvasNode(buildVideoWorkflowCanvasNode(templateType, placement), { connectFromSelection: options?.connectFromSelection ?? true, selectNewNode: options?.selectNewNode ?? true, @@ -4863,7 +5369,8 @@ export default function CanvasEditorPage({ params }: PageProps) { if (node.type !== "workflow") return false; const templateType = inferVideoWorkflowTemplateType(node); if (templateType !== branchRole) return false; - const metadata = (((node.data as WorkflowNodeData).metadata || {}) as Record); + const metadata = ((node.data as WorkflowNodeData).metadata || + {}) as Record; return ( String(metadata.sourceWorkflowNodeId || "").trim() === sourceNodeId && String(metadata.agentResultModeType || "").trim() === modeType && @@ -4900,7 +5407,11 @@ export default function CanvasEditorPage({ params }: PageProps) { selectNewNode: false, } ); - return useCanvasStore.getState().nodes.find((node) => node.id === nodeId) || null; + return ( + useCanvasStore + .getState() + .nodes.find((node) => node.id === nodeId) || null + ); })(); const saveNode = findExistingBranchNode(group.modeType, "local-save") || @@ -4912,7 +5423,11 @@ export default function CanvasEditorPage({ params }: PageProps) { selectNewNode: false, } ); - return useCanvasStore.getState().nodes.find((node) => node.id === nodeId) || null; + return ( + useCanvasStore + .getState() + .nodes.find((node) => node.id === nodeId) || null + ); })(); const firstAsset = group.assets[0] || null; @@ -4927,7 +5442,8 @@ export default function CanvasEditorPage({ params }: PageProps) { summary: `已整理 ${group.assets.length} 个 ${group.label} 结果,可直接预览。`, metadata: { sourceWorkflowNodeId: sourceNodeId, - sourceAgentProjectId: selectedWorkflowLinkedAgentProject.id || agentProjectId, + sourceAgentProjectId: + selectedWorkflowLinkedAgentProject.id || agentProjectId, agentResultModeType: group.modeType, agentResultGroupLabel: group.label, agentBranchRole: "preview", @@ -4958,7 +5474,8 @@ export default function CanvasEditorPage({ params }: PageProps) { summary: `已整理 ${group.assets.length} 个 ${group.label} 结果,可直接打包下载。`, metadata: { sourceWorkflowNodeId: sourceNodeId, - sourceAgentProjectId: selectedWorkflowLinkedAgentProject.id || agentProjectId, + sourceAgentProjectId: + selectedWorkflowLinkedAgentProject.id || agentProjectId, agentResultModeType: group.modeType, agentResultGroupLabel: group.label, agentBranchRole: "local-save", @@ -5001,9 +5518,12 @@ export default function CanvasEditorPage({ params }: PageProps) { if (touchedNodeIds.length > 0) { selectNodes(Array.from(new Set(touchedNodeIds))); } - toast.success(`已展开 ${selectedWorkflowAgentResultAssets.length} 组结果分支`); + toast.success( + `已展开 ${selectedWorkflowAgentResultAssets.length} 组结果分支` + ); } catch (error) { - const message = error instanceof Error ? error.message : "展开 Agent 结果分支失败"; + const message = + error instanceof Error ? error.message : "展开 Agent 结果分支失败"; patchWorkflowNode(sourceNodeId, { status: "failed", summary: message, @@ -5014,7 +5534,9 @@ export default function CanvasEditorPage({ params }: PageProps) { }); toast.error(message); } finally { - setActiveWorkflowExecutionTaskKey((current) => (current === taskKey ? null : current)); + setActiveWorkflowExecutionTaskKey((current) => + current === taskKey ? null : current + ); } }, [ addEdge, @@ -5088,14 +5610,19 @@ export default function CanvasEditorPage({ params }: PageProps) { }, [createCanvasNode, handleAddWorkflowNode, isVideoMode]); const handleRunStoryboardBatch = useCallback(async () => { - if (selectedVideoWorkflowType !== "storyboard-node" || !selectedVideoWorkflowNode) { + if ( + selectedVideoWorkflowType !== "storyboard-node" || + !selectedVideoWorkflowNode + ) { return; } const nodeId = selectedVideoWorkflowNode.id; const taskKey = `${nodeId}:batch`; const nowIso = new Date().toISOString(); - const imageMedia = selectedWorkflowMedia.filter((item) => item.kind === "image"); + const imageMedia = selectedWorkflowMedia.filter( + (item) => item.kind === "image" + ); const primaryImage = imageMedia[0] || null; const shots = selectedWorkflowStoryboardShots.length > 0 @@ -5165,7 +5692,10 @@ export default function CanvasEditorPage({ params }: PageProps) { const failureMessages: string[] = []; for (const shot of shots) { - const shotPrompt = buildStoryboardShotPrompt(selectedWorkflowPrompt, shot); + const shotPrompt = buildStoryboardShotPrompt( + selectedWorkflowPrompt, + shot + ); const outputIndex = successCount; try { @@ -5175,7 +5705,9 @@ export default function CanvasEditorPage({ params }: PageProps) { shotPrompt || "基于当前分镜镜头生成一张适合后续视频工作流使用的关键帧。", referenceImage: primaryImage?.url, - aspectRatio: guessCanvasAspectRatio(primaryImage?.nodeId || targetNodeId), + aspectRatio: guessCanvasAspectRatio( + primaryImage?.nodeId || targetNodeId + ), imageCount: 1, }); @@ -5195,7 +5727,10 @@ export default function CanvasEditorPage({ params }: PageProps) { prompt: shotPrompt, source: "workflow-result", }; - const offset = getWorkflowBatchOutputOffset(media.kind, outputIndex); + const offset = getWorkflowBatchOutputOffset( + media.kind, + outputIndex + ); const createdNodeId = createWorkflowOutputNode({ sourceNodeId: targetNodeId, anchorNodeId: targetNodeId, @@ -5241,7 +5776,10 @@ export default function CanvasEditorPage({ params }: PageProps) { prompt: shotPrompt, source: "workflow-result", }; - const offset = getWorkflowBatchOutputOffset(media.kind, outputIndex); + const offset = getWorkflowBatchOutputOffset( + media.kind, + outputIndex + ); const createdNodeId = createWorkflowOutputNode({ sourceNodeId: targetNodeId, anchorNodeId: targetNodeId, @@ -5263,7 +5801,9 @@ export default function CanvasEditorPage({ params }: PageProps) { failureCount += 1; failureMessages.push( `${shot.title || `镜头 ${failureCount}`}: ${ - error instanceof Error ? error.message : `${targetLabel}生成失败` + error instanceof Error + ? error.message + : `${targetLabel}生成失败` }` ); } @@ -5340,7 +5880,8 @@ export default function CanvasEditorPage({ params }: PageProps) { toast.error("分镜已派发,但下游生成全部失败"); } } catch (error) { - const message = error instanceof Error ? error.message : "分镜批量执行失败"; + const message = + error instanceof Error ? error.message : "分镜批量执行失败"; patchWorkflowNode(nodeId, { status: "failed", summary: message, @@ -5390,8 +5931,12 @@ export default function CanvasEditorPage({ params }: PageProps) { const nodeId = selectedVideoWorkflowNode.id; const taskKey = `${nodeId}:primary`; - const imageMedia = selectedWorkflowMedia.filter((item) => item.kind === "image"); - const videoMedia = selectedWorkflowMedia.filter((item) => item.kind === "video"); + const imageMedia = selectedWorkflowMedia.filter( + (item) => item.kind === "image" + ); + const videoMedia = selectedWorkflowMedia.filter( + (item) => item.kind === "video" + ); const primaryImage = imageMedia[0] || null; const nowIso = new Date().toISOString(); setActiveWorkflowExecutionTaskKey(taskKey); @@ -5399,7 +5944,9 @@ export default function CanvasEditorPage({ params }: PageProps) { try { if (selectedVideoWorkflowType === "storyboard-node") { if (!selectedWorkflowPrompt.trim()) { - throw new Error("请先在上游接入脚本、画面描述或参考素材,再整理分镜。"); + throw new Error( + "请先在上游接入脚本、画面描述或参考素材,再整理分镜。" + ); } patchWorkflowNode(nodeId, { @@ -5432,8 +5979,14 @@ export default function CanvasEditorPage({ params }: PageProps) { } if (selectedVideoWorkflowType === "video-analyze") { - if (!selectedWorkflowPrompt.trim() && imageMedia.length === 0 && videoMedia.length === 0) { - throw new Error("请先接入参考视频、封面图或脚本说明,再执行视频分析。"); + if ( + !selectedWorkflowPrompt.trim() && + imageMedia.length === 0 && + videoMedia.length === 0 + ) { + throw new Error( + "请先接入参考视频、封面图或脚本说明,再执行视频分析。" + ); } patchWorkflowNode(nodeId, { @@ -5451,8 +6004,8 @@ export default function CanvasEditorPage({ params }: PageProps) { imageUrls: imageMedia.map((item) => item.url).slice(0, 4), videoUrls: videoMedia.map((item) => item.url).slice(0, 4), nodeTitle: - (selectedVideoWorkflowNode.data as { title?: string } | undefined)?.title || - "视频分析", + (selectedVideoWorkflowNode.data as { title?: string } | undefined) + ?.title || "视频分析", }); if (!result.success) { throw new Error(result.error || "视频分析失败"); @@ -5514,15 +6067,19 @@ export default function CanvasEditorPage({ params }: PageProps) { prompt: selectedWorkflowPrompt, imageUrls: imageMedia.map((item) => item.url).slice(0, 4), nodeTitle: - (selectedVideoWorkflowNode.data as { title?: string } | undefined)?.title || - "提取角色场景", + (selectedVideoWorkflowNode.data as { title?: string } | undefined) + ?.title || "提取角色场景", }); if (!result.success) { throw new Error(result.error || "角色场景提取失败"); } - const characters = Array.isArray(result.data?.characters) ? result.data?.characters : []; - const scenes = Array.isArray(result.data?.scenes) ? result.data?.scenes : []; + const characters = Array.isArray(result.data?.characters) + ? result.data?.characters + : []; + const scenes = Array.isArray(result.data?.scenes) + ? result.data?.scenes + : []; patchWorkflowNode(nodeId, { status: "completed", @@ -5563,7 +6120,8 @@ export default function CanvasEditorPage({ params }: PageProps) { metadata: { sourcePrompt: descriptionText, characters: selectedWorkflowStructuredData.characters, - recommendedNextSteps: selectedWorkflowStructuredData.recommendedNextSteps, + recommendedNextSteps: + selectedWorkflowStructuredData.recommendedNextSteps, lastExecutedAt: nowIso, }, }); @@ -5589,7 +6147,8 @@ export default function CanvasEditorPage({ params }: PageProps) { metadata: { sourcePrompt: descriptionText, scenes: selectedWorkflowStructuredData.scenes, - recommendedNextSteps: selectedWorkflowStructuredData.recommendedNextSteps, + recommendedNextSteps: + selectedWorkflowStructuredData.recommendedNextSteps, lastExecutedAt: nowIso, }, }); @@ -5678,7 +6237,10 @@ export default function CanvasEditorPage({ params }: PageProps) { throw new Error("请先接入脚本、分镜或参考素材,再生成分镜表。"); } - const chart = buildStoryboardChartRows(chartShots, selectedWorkflowPrompt); + const chart = buildStoryboardChartRows( + chartShots, + selectedWorkflowPrompt + ); patchWorkflowNode(nodeId, { status: "completed", @@ -5710,7 +6272,9 @@ export default function CanvasEditorPage({ params }: PageProps) { }); if (tableState.rows.length === 0) { - throw new Error("上游还没有可整理成表格的内容,请先接入脚本、角色、场景或分镜。"); + throw new Error( + "上游还没有可整理成表格的内容,请先接入脚本、角色、场景或分镜。" + ); } patchWorkflowNode(nodeId, { @@ -5764,7 +6328,9 @@ export default function CanvasEditorPage({ params }: PageProps) { }, }); - const result = await imageGenerationService.enhanceImage(primaryImage.url); + const result = await imageGenerationService.enhanceImage( + primaryImage.url + ); const enhancedUrl = String(result.data?.url || "").trim(); if (!result.success || !enhancedUrl) { throw new Error(result.error || "超分增强失败"); @@ -5901,11 +6467,14 @@ export default function CanvasEditorPage({ params }: PageProps) { }, }); - const result = await imageGenerationService.extendImage(primaryImage.url, { - prompt: promptText, - xScale: extendScales.xScale, - yScale: extendScales.yScale, - }); + const result = await imageGenerationService.extendImage( + primaryImage.url, + { + prompt: promptText, + xScale: extendScales.xScale, + yScale: extendScales.yScale, + } + ); const extendedUrl = String(result.data?.url || "").trim(); if (!result.success || !extendedUrl) { throw new Error(result.error || "拼接补画失败"); @@ -6048,7 +6617,9 @@ export default function CanvasEditorPage({ params }: PageProps) { selectedVideoWorkflowType === "camera-movement" || selectedVideoWorkflowType === "motion-control" ) { - const directive = buildWorkflowMovementDirective(selectedWorkflowPrompt); + const directive = buildWorkflowMovementDirective( + selectedWorkflowPrompt + ); const basePrompt = buildWorkflowTextBlock([ selectedWorkflowPrompt, directive.cameraMove, @@ -6144,8 +6715,10 @@ export default function CanvasEditorPage({ params }: PageProps) { resultMediaKind: media.kind, resultMediaTitle: media.title, resultPosterUrl: String( - (result.data?.metadata as Record | undefined)?.lastFrameUrl || - (result.data?.metadata as Record | undefined)?.coverUrl || + (result.data?.metadata as Record | undefined) + ?.lastFrameUrl || + (result.data?.metadata as Record | undefined) + ?.coverUrl || "" ).trim(), videoGenerationMode: result.data?.mode || mode, @@ -6196,7 +6769,9 @@ export default function CanvasEditorPage({ params }: PageProps) { } if (selectedVideoWorkflowType === "doodle-canvas") { - const doodleState = buildWorkflowDoodleInstructions(selectedWorkflowPrompt); + const doodleState = buildWorkflowDoodleInstructions( + selectedWorkflowPrompt + ); patchWorkflowNode(nodeId, { status: "completed", summary: doodleState.summary, @@ -6244,15 +6819,18 @@ export default function CanvasEditorPage({ params }: PageProps) { selectedVideoWorkflowType === "rh-app" || selectedVideoWorkflowType === "rh-comfy" ) { - const handoffPlan = buildWorkflowHandoffPlan(selectedVideoWorkflowType, { - promptText: selectedWorkflowPrompt, - shots: selectedWorkflowStoryboardShots.map((shot) => ({ - title: shot.title, - description: shot.description, - })), - structuredData: selectedWorkflowStructuredData, - media: selectedWorkflowMedia, - }); + const handoffPlan = buildWorkflowHandoffPlan( + selectedVideoWorkflowType, + { + promptText: selectedWorkflowPrompt, + shots: selectedWorkflowStoryboardShots.map((shot) => ({ + title: shot.title, + description: shot.description, + })), + structuredData: selectedWorkflowStructuredData, + media: selectedWorkflowMedia, + } + ); patchWorkflowNode(nodeId, { status: "completed", @@ -6284,14 +6862,12 @@ export default function CanvasEditorPage({ params }: PageProps) { ? resolvePreferredCharacterPrompt( selectedWorkflowPrompt, selectedWorkflowStructuredData - ) || - "根据角色资产生成一张适合后续视频工作流使用的角色关键帧。" + ) || "根据角色资产生成一张适合后续视频工作流使用的角色关键帧。" : selectedVideoWorkflowType === "generate-scene-image" ? resolvePreferredScenePrompt( selectedWorkflowPrompt, selectedWorkflowStructuredData - ) || - "根据场景资产生成一张适合后续视频工作流使用的环境关键帧。" + ) || "根据场景资产生成一张适合后续视频工作流使用的环境关键帧。" : selectedWorkflowPrompt.trim() || "基于上游脚本与素材生成一张适合视频工作流继续推进的关键帧。"; @@ -6317,7 +6893,9 @@ export default function CanvasEditorPage({ params }: PageProps) { }); const imageUrl = - result.data?.images?.[0]?.originalUrl || result.data?.images?.[0]?.thumbnailUrl || ""; + result.data?.images?.[0]?.originalUrl || + result.data?.images?.[0]?.thumbnailUrl || + ""; if (!result.success || !imageUrl) { throw new Error(result.error || "图片生成失败"); } @@ -6370,8 +6948,7 @@ export default function CanvasEditorPage({ params }: PageProps) { ? resolvePreferredCharacterPrompt( selectedWorkflowPrompt, selectedWorkflowStructuredData - ) || - "根据角色资产生成一段镜头感明确、动作自然的角色视频片段。" + ) || "根据角色资产生成一段镜头感明确、动作自然的角色视频片段。" : selectedVideoWorkflowType === "generate-scene-video" ? resolvePreferredScenePrompt( selectedWorkflowPrompt, @@ -6438,8 +7015,10 @@ export default function CanvasEditorPage({ params }: PageProps) { resultModel: result.data?.modelDisplayName || "", videoGenerationMode: result.data?.mode || mode, resultPosterUrl: String( - (result.data?.metadata as Record | undefined)?.lastFrameUrl || - (result.data?.metadata as Record | undefined)?.coverUrl || + (result.data?.metadata as Record | undefined) + ?.lastFrameUrl || + (result.data?.metadata as Record | undefined) + ?.coverUrl || "" ).trim(), lastExecutedAt: nowIso, @@ -6457,7 +7036,11 @@ export default function CanvasEditorPage({ params }: PageProps) { } if (selectedVideoWorkflowType === "custom-agent") { - if (!selectedWorkflowPrompt.trim() && imageMedia.length === 0 && videoMedia.length === 0) { + if ( + !selectedWorkflowPrompt.trim() && + imageMedia.length === 0 && + videoMedia.length === 0 + ) { throw new Error("请先给自定义代理节点提供脚本、参考图或视频上下文。"); } @@ -6501,11 +7084,18 @@ export default function CanvasEditorPage({ params }: PageProps) { if (selectedVideoWorkflowType === "preview") { const fallbackPreviewMedia = (() => { - const metadata = ((selectedVideoWorkflowData?.metadata || {}) as Record) || {}; + const metadata = + ((selectedVideoWorkflowData?.metadata || {}) as Record< + string, + any + >) || {}; const syncedAsset = Array.isArray(metadata.syncedAgentAssets) - ? ((metadata.syncedAgentAssets as Array>)[0] || null) + ? (metadata.syncedAgentAssets as Array>)[0] || + null : null; - const previewUrl = String(metadata.previewMediaUrl || syncedAsset?.url || "").trim(); + const previewUrl = String( + metadata.previewMediaUrl || syncedAsset?.url || "" + ).trim(); if (!/^https?:\/\//i.test(previewUrl)) { return null; } @@ -6513,14 +7103,18 @@ export default function CanvasEditorPage({ params }: PageProps) { nodeId, kind: "image" as const, url: previewUrl, - title: String(metadata.previewMediaTitle || syncedAsset?.title || "预览结果").trim(), + title: String( + metadata.previewMediaTitle || syncedAsset?.title || "预览结果" + ).trim(), prompt: selectedWorkflowPrompt || String(metadata.sourcePrompt || "").trim(), source: "workflow-preview" as const, }; })(); - const previewMedia = getBestWorkflowPreviewMedia(nodeId, workflowGraph) || fallbackPreviewMedia; + const previewMedia = + getBestWorkflowPreviewMedia(nodeId, workflowGraph) || + fallbackPreviewMedia; if (!previewMedia) { throw new Error("预览节点上游还没有可预览的图片或视频。"); } @@ -6532,8 +7126,7 @@ export default function CanvasEditorPage({ params }: PageProps) { previewMediaUrl: previewMedia.url, previewMediaKind: previewMedia.kind, previewMediaTitle: previewMedia.title || "", - sourcePrompt: - selectedWorkflowPrompt || previewMedia.prompt || "", + sourcePrompt: selectedWorkflowPrompt || previewMedia.prompt || "", lastExecutedAt: nowIso, }, }); @@ -6543,12 +7136,17 @@ export default function CanvasEditorPage({ params }: PageProps) { if (selectedVideoWorkflowType === "local-save") { const syncedAgentAssets: WorkflowMediaReference[] = Array.isArray( - ((selectedVideoWorkflowData?.metadata || {}) as Record)?.syncedAgentAssets + ((selectedVideoWorkflowData?.metadata || {}) as Record) + ?.syncedAgentAssets ) - ? ((((selectedVideoWorkflowData?.metadata || {}) as Record) - .syncedAgentAssets || []) as Array>).reduce< - WorkflowMediaReference[] - >((assets, item, index) => { + ? ( + (( + (selectedVideoWorkflowData?.metadata || {}) as Record< + string, + any + > + ).syncedAgentAssets || []) as Array> + ).reduce((assets, item, index) => { const url = String(item?.url || "").trim(); if (!/^https?:\/\//i.test(url)) { return assets; @@ -6565,7 +7163,9 @@ export default function CanvasEditorPage({ params }: PageProps) { }, []) : []; const assetsToDownload = - syncedAgentAssets.length > 0 ? syncedAgentAssets : selectedWorkflowMedia.slice(0, 24); + syncedAgentAssets.length > 0 + ? syncedAgentAssets + : selectedWorkflowMedia.slice(0, 24); if (assetsToDownload.length === 0) { throw new Error("保存节点上游还没有可下载的图片或视频。"); @@ -6579,7 +7179,10 @@ export default function CanvasEditorPage({ params }: PageProps) { }, }); - downloadWorkflowAssets(assetsToDownload.slice(0, 24), "video-workflow-export"); + downloadWorkflowAssets( + assetsToDownload.slice(0, 24), + "video-workflow-export" + ); patchWorkflowNode(nodeId, { status: "completed", @@ -6649,39 +7252,42 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowGraph, ]); - const handleBatchAddToCanvas = useCallback((imageUrls: string[]) => { - if (!imageUrls || imageUrls.length === 0) { - toast.error("没有可添加到画布的图片"); - return; - } + const handleBatchAddToCanvas = useCallback( + (imageUrls: string[]) => { + if (!imageUrls || imageUrls.length === 0) { + toast.error("没有可添加到画布的图片"); + return; + } - const columns = Math.min(4, imageUrls.length); - const startX = 120; - const startY = 120; - const createdNodeIds: string[] = []; + const columns = Math.min(4, imageUrls.length); + const startX = 120; + const startY = 120; + const createdNodeIds: string[] = []; - for (const [index, imageUrl] of imageUrls.entries()) { - const x = startX + (index % columns) * 320; - const y = startY + Math.floor(index / columns) * 320; - const nodeId = addNode({ - type: "image", - position: { x, y }, - size: { width: 300, height: 300 }, - data: { - src: imageUrl, - status: "completed", - title: `批量结果-${index + 1}`, - prompt: "批量处理结果", - }, - connections: [], - }); - createdNodeIds.push(nodeId); - } + for (const [index, imageUrl] of imageUrls.entries()) { + const x = startX + (index % columns) * 320; + const y = startY + Math.floor(index / columns) * 320; + const nodeId = addNode({ + type: "image", + position: { x, y }, + size: { width: 300, height: 300 }, + data: { + src: imageUrl, + status: "completed", + title: `批量结果-${index + 1}`, + prompt: "批量处理结果", + }, + connections: [], + }); + createdNodeIds.push(nodeId); + } - if (createdNodeIds.length > 0) { - selectNodes(createdNodeIds); - } - }, [addNode, selectNodes]); + if (createdNodeIds.length > 0) { + selectNodes(createdNodeIds); + } + }, + [addNode, selectNodes] + ); // 处理图片操作(通过事件分发到选中图片节点执行) const handleAction = (action: string) => { @@ -6697,7 +7303,7 @@ export default function CanvasEditorPage({ params }: PageProps) { nodeId: selectedImageNode.id, action, }, - }), + }) ); } }; @@ -6710,22 +7316,22 @@ export default function CanvasEditorPage({ params }: PageProps) { } setGenerating(true); - + // 先创建占位节点(显示生成中状态) const placeholderNodeIds: string[] = []; for (let i = 0; i < imageCount; i++) { const nodeId = addNode({ - type: 'image', - position: { - x: 100 + (i % 3) * 320, - y: 100 + Math.floor(i / 3) * 320 + type: "image", + position: { + x: 100 + (i % 3) * 320, + y: 100 + Math.floor(i / 3) * 320, }, size: { width: 300, height: 300 }, - data: { - src: '', - status: 'generating', - title: prompt.slice(0, 24) || '生成中', - prompt: prompt, + data: { + src: "", + status: "generating", + title: prompt.slice(0, 24) || "生成中", + prompt, }, connections: [], }); @@ -6748,25 +7354,25 @@ export default function CanvasEditorPage({ params }: PageProps) { updateNode(placeholderNodeIds[index], { data: { src: image.originalUrl || image.thumbnailUrl, - status: 'completed', - title: `${prompt.slice(0, 20) || '生成图片'}-${index + 1}`, - prompt: prompt, + status: "completed", + title: `${prompt.slice(0, 20) || "生成图片"}-${index + 1}`, + prompt, }, }); } }); - + toast.success(`成功生成 ${result.data.images.length} 张图片`); setPrompt(""); // 清空提示词 } else { // 生成失败,更新节点状态 - placeholderNodeIds.forEach(nodeId => { + placeholderNodeIds.forEach((nodeId) => { updateNode(nodeId, { data: { - src: '', - status: 'failed', - title: '生成失败', - prompt: prompt, + src: "", + status: "failed", + title: "生成失败", + prompt, }, }); }); @@ -6775,13 +7381,13 @@ export default function CanvasEditorPage({ params }: PageProps) { } catch (error) { console.error("AI 图片生成失败:", error); // 生成失败,更新节点状态 - placeholderNodeIds.forEach(nodeId => { + placeholderNodeIds.forEach((nodeId) => { updateNode(nodeId, { data: { - src: '', - status: 'failed', - title: '生成失败', - prompt: prompt, + src: "", + status: "failed", + title: "生成失败", + prompt, }, }); }); @@ -6816,21 +7422,27 @@ export default function CanvasEditorPage({ params }: PageProps) { ) { return; } - + // Ctrl/Cmd + S 保存 - if ((e.ctrlKey || e.metaKey) && e.key === 's') { + if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); handleSave(); } // Ctrl/Cmd + A:当已选中图片时,选中画布内全部图片 - if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') { - const { nodes: currentNodes, selectedNodeIds: currentSelectedNodeIds } = useCanvasStore.getState(); + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") { + const { nodes: currentNodes, selectedNodeIds: currentSelectedNodeIds } = + useCanvasStore.getState(); const selectedHasImage = currentSelectedNodeIds.some((selectedId) => - currentNodes.some((node) => node.id === selectedId && node.type === "image") + currentNodes.some( + (node) => node.id === selectedId && node.type === "image" + ) ); if (selectedHasImage) { const allImageNodeIds = currentNodes - .filter((node) => node.type === "image" && Boolean((node.data as any)?.src)) + .filter( + (node) => + node.type === "image" && Boolean((node.data as any)?.src) + ) .map((node) => node.id); if (allImageNodeIds.length > 0) { e.preventDefault(); @@ -6839,36 +7451,45 @@ export default function CanvasEditorPage({ params }: PageProps) { } } // Ctrl/Cmd + Z 撤销 - if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { + if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) { e.preventDefault(); undo(); } // Ctrl/Cmd + Shift + Z 重做 - if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) { + if ((e.ctrlKey || e.metaKey) && e.key === "z" && e.shiftKey) { e.preventDefault(); redo(); } // V 选择工具 - if (e.key === 'v' && !e.ctrlKey && !e.metaKey) { - setTool('select'); + if (e.key === "v" && !e.ctrlKey && !e.metaKey) { + setTool("select"); } // H 平移工具 - if (e.key === 'h' && !e.ctrlKey && !e.metaKey) { - setTool('pan'); + if (e.key === "h" && !e.ctrlKey && !e.metaKey) { + setTool("pan"); } // L 连线工具(仅视频画布) - if (isVideoMode && e.key === 'l' && !e.ctrlKey && !e.metaKey) { - setTool('draw'); + if (isVideoMode && e.key === "l" && !e.ctrlKey && !e.metaKey) { + setTool("draw"); } // Delete 删除选中 - if (e.key === 'Delete' || e.key === 'Backspace') { + if (e.key === "Delete" || e.key === "Backspace") { useCanvasStore.getState().deleteSelectedNodes(); } }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [handleSave, isVideoMode, nodes, redo, selectNodes, selectedNodeIds, setTool, undo]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + handleSave, + isVideoMode, + nodes, + redo, + selectNodes, + selectedNodeIds, + setTool, + undo, + ]); useEffect(() => { const html = document.documentElement; @@ -6938,27 +7559,38 @@ export default function CanvasEditorPage({ params }: PageProps) { useEffect(() => { selectedBrandModelRef.current = selectedBrandModel; }, [selectedBrandModel]); - const handleWorkflowInputsChange = useCallback((next: AgentWorkflowInputs) => { - workflowInputsRef.current = next; - setWorkflowInputs(next); - }, []); - const handleWorkflowMaterialsChange = useCallback((next: AgentWorkflowMaterialSelections) => { - const nextMaterials = appearanceRecognitionEnabledRef.current - ? next - : clearAppearanceSummaryFromMaterials(next); - workflowMaterialsRef.current = nextMaterials; - setWorkflowMaterials(nextMaterials); - }, []); - const handleAppearanceRecognitionEnabledChange = useCallback((next: boolean) => { - appearanceRecognitionEnabledRef.current = next; - setAppearanceRecognitionEnabled(next); + const handleWorkflowInputsChange = useCallback( + (next: AgentWorkflowInputs) => { + workflowInputsRef.current = next; + setWorkflowInputs(next); + }, + [] + ); + const handleWorkflowMaterialsChange = useCallback( + (next: AgentWorkflowMaterialSelections) => { + const nextMaterials = appearanceRecognitionEnabledRef.current + ? next + : clearAppearanceSummaryFromMaterials(next); + workflowMaterialsRef.current = nextMaterials; + setWorkflowMaterials(nextMaterials); + }, + [] + ); + const handleAppearanceRecognitionEnabledChange = useCallback( + (next: boolean) => { + appearanceRecognitionEnabledRef.current = next; + setAppearanceRecognitionEnabled(next); - if (!next) { - const clearedMaterials = clearAppearanceSummaryFromMaterials(workflowMaterialsRef.current); - workflowMaterialsRef.current = clearedMaterials; - setWorkflowMaterials(clearedMaterials); - } - }, []); + if (!next) { + const clearedMaterials = clearAppearanceSummaryFromMaterials( + workflowMaterialsRef.current + ); + workflowMaterialsRef.current = clearedMaterials; + setWorkflowMaterials(clearedMaterials); + } + }, + [] + ); const handleWorkflowSelectionsChange = useCallback( (next: Partial>) => { const normalizedCurrentSelections = sanitizeAgentWorkflowSelections({ @@ -6977,7 +7609,10 @@ export default function CanvasEditorPage({ params }: PageProps) { normalizedCurrentSelections ).map(({ step }) => step.key); const earliestChangedIndex = orderedStepKeys.findIndex((stepKey) => { - return String(normalizedCurrentSelections[stepKey] || "") !== String(normalizedNextSelections[stepKey] || ""); + return ( + String(normalizedCurrentSelections[stepKey] || "") !== + String(normalizedNextSelections[stepKey] || "") + ); }); let clearedSelections = { ...normalizedNextSelections }; @@ -7017,7 +7652,10 @@ export default function CanvasEditorPage({ params }: PageProps) { const { data: selectedAgentPresetResponse } = useSWR<{ success?: boolean; data?: AgentPreset; - }>(selectedAgentPresetId ? `/api/agents/${selectedAgentPresetId}` : null, fetcher); + }>( + selectedAgentPresetId ? `/api/agents/${selectedAgentPresetId}` : null, + fetcher + ); const selectedAgentPresetData = useMemo( () => selectedAgentPresetResponse?.success && selectedAgentPresetResponse.data @@ -7026,7 +7664,8 @@ export default function CanvasEditorPage({ params }: PageProps) { [selectedAgentPresetResponse] ); const selectedAgentPresetModeConfig = useMemo( - () => normalizeAgentPresetModeConfig(selectedAgentPresetData?.defaultParams), + () => + normalizeAgentPresetModeConfig(selectedAgentPresetData?.defaultParams), [selectedAgentPresetData?.defaultParams] ); const materialProductSelectionLimit = useMemo(() => { @@ -7085,17 +7724,16 @@ export default function CanvasEditorPage({ params }: PageProps) { }), [resolvedWorkflowUiConfig, sanitizedWorkflowSelections] ); - const resolvedAgentProjectTitle = - hasActiveAgentProject - ? String(agentProjectTitle || "").trim() || "未命名生图项目" - : "未选择项目"; + const resolvedAgentProjectTitle = hasActiveAgentProject + ? String(agentProjectTitle || "").trim() || "未命名生图项目" + : "未选择项目"; if (isLoading) { return ( -
+
- 加载中... + 加载中...
); @@ -7104,21 +7742,23 @@ export default function CanvasEditorPage({ params }: PageProps) { const brandEntry = (
@@ -7127,8 +7767,8 @@ export default function CanvasEditorPage({ params }: PageProps) { const agentDisabledNotice = requestedAgentMode && !isAgentWorkspaceEnabled ? ( -
-
+
+
智能体功能未开启,请联系管理员在系统设置中开启。
@@ -7139,13 +7779,17 @@ export default function CanvasEditorPage({ params }: PageProps) {
{canSyncAgentResultsToCanvas ? (
- +
) : null; const loginDialogElement = ( - + 登录已失效 @@ -7260,15 +7914,15 @@ export default function CanvasEditorPage({ params }: PageProps) { - @@ -7276,64 +7930,65 @@ export default function CanvasEditorPage({ params }: PageProps) { ); - const agentProjectSelectionScreen = isAgentMode && !hasActiveAgentProject ? ( -
- -
-
-
-
- + const agentProjectSelectionScreen = + isAgentMode && !hasActiveAgentProject ? ( +
+ +
+
+
+
+ +
+

+ 先选择一个生图项目 +

+

+ 打开历史项目可以继续沿用之前的生图结果、模式和工作流。只有在你明确点击新建项目时,系统才会创建一个全新的智能体项目。 +

-

- 先选择一个生图项目 -

-

- 打开历史项目可以继续沿用之前的生图结果、模式和工作流。只有在你明确点击新建项目时,系统才会创建一个全新的智能体项目。 -

-
-
- +
+ - + +
-
-
-
- ) : null; + +
+ ) : null; if (isAgentMode) { if (!hasActiveAgentProject) { @@ -7348,88 +8003,96 @@ export default function CanvasEditorPage({ params }: PageProps) { return ( <> { - updateCanvasSearchParams({ - mode: null, - }); - }} - onClearPreset={() => setSelectedAgentPresetId(null)} + brandModelPanel={ + + } brandStorePanel={ } - workflowStylePanel={ - } - // 嵌入的智能体选择面板 + onAgentModeStateChange={setAgentModeState} + onClearPreset={() => setSelectedAgentPresetId(null)} + onDeleteResultImage={handleAgentResultImageDelete} + onOpenCanvasEditor={() => { + updateCanvasSearchParams({ + mode: null, + }); + }} + onResultPreviewUrlsChange={handleAgentResultPreviewUrlsChange} + onWorkspaceStateChange={setAgentWorkspaceState} presetPanel={ } - brandModelPanel={ - + projectId={agentProjectId} + resultCollections={agentProjectResultCollections} + selectedAgentPresetId={ + isAgentWorkspaceEnabled ? selectedAgentPresetId : null } - materialSelectionPanel={ - } + workspaceState={agentWorkspaceState} /> {loginDialogElement} @@ -7439,52 +8102,55 @@ export default function CanvasEditorPage({ params }: PageProps) { return (
- {/* 1. Canvas Layer (Full Screen) - 使用 key 强制在 id 变化时重新挂载 */} -
- +
+
{isVideoMode && videoCanvasInsertMenu ? ( { if (!open) { setVideoCanvasInsertMenu(null); } }} - modal={false} + open={Boolean(videoCanvasInsertMenu)} >
- {!isVideoModuleSidebarCollapsed ? ( + {isVideoModuleSidebarCollapsed ? ( +
+ {VIDEO_CANVAS_SIDEBAR_GROUPS.map((group) => { + const GroupIcon = group.icon; + return ( + + ); + })} +
+ ) : (
{VIDEO_CANVAS_SIDEBAR_GROUPS.map((group) => { @@ -7610,13 +8316,13 @@ export default function CanvasEditorPage({ params }: PageProps) { return ( setVideoModuleSectionOpen((prev) => ({ ...prev, [group.id]: open, })) } + open={videoModuleSectionOpen[group.id] !== false} >
@@ -7624,7 +8330,7 @@ export default function CanvasEditorPage({ params }: PageProps) { - + {group.label}
@@ -7635,33 +8341,41 @@ export default function CanvasEditorPage({ params }: PageProps) {
-
+
{group.nodeTypes.map((type) => { - const definition = getVideoWorkflowNodeDefinition(type); + const definition = + getVideoWorkflowNodeDefinition(type); if (!definition) return null; - const Icon = getVideoWorkflowQuickConnectIcon(type); + const Icon = + getVideoWorkflowQuickConnectIcon(type); return ( @@ -7676,29 +8390,6 @@ export default function CanvasEditorPage({ params }: PageProps) { })}
- ) : ( -
- {VIDEO_CANVAS_SIDEBAR_GROUPS.map((group) => { - const GroupIcon = group.icon; - return ( - - ); - })} -
)}
@@ -7717,33 +8408,36 @@ export default function CanvasEditorPage({ params }: PageProps) { }} > {selectedVideoWorkflowQuickConnects.map((item, index, items) => { const total = items.length; const y = (index - (total - 1) / 2) * 72; const x = 96 + Math.abs(y) * 0.28; - const direction = selectedWorkflowQuickConnectAnchor.openLeft ? -1 : 1; + const direction = selectedWorkflowQuickConnectAnchor.openLeft + ? -1 + : 1; const targetX = x * direction; const cp1X = 36 * direction; const cp2X = (x - 26) * direction; return ( ); })} @@ -7770,20 +8463,22 @@ export default function CanvasEditorPage({ params }: PageProps) { const total = items.length; const y = (index - (total - 1) / 2) * 72; const x = 96 + Math.abs(y) * 0.28; - const direction = selectedWorkflowQuickConnectAnchor.openLeft ? -1 : 1; + const direction = selectedWorkflowQuickConnectAnchor.openLeft + ? -1 + : 1; const Icon = getVideoWorkflowQuickConnectIcon(item.type); return ( @@ -7804,79 +8499,123 @@ export default function CanvasEditorPage({ params }: PageProps) {
- {selectedVideoWorkflowDefinition.label} + + {selectedVideoWorkflowDefinition.label} + {selectedWorkflowActionHint}
- + 执行
- + 提示词 {selectedWorkflowInputs.promptCount} - + 图片 {selectedWorkflowInputs.imageCount} - + 视频 {selectedWorkflowInputs.videoCount} {selectedVideoWorkflowType === "storyboard-node" ? ( - + 分镜 {selectedWorkflowStoryboardShots.length} ) : null} {selectedVideoWorkflowType === "storyboard-node" ? ( - + 下游 {selectedStoryboardBatchTargets.length} ) : null} {selectedWorkflowStructuredData.characters.length > 0 ? ( - + 角色 {selectedWorkflowStructuredData.characters.length} ) : null} {selectedWorkflowStructuredData.scenes.length > 0 ? ( - + 场景 {selectedWorkflowStructuredData.scenes.length} ) : null} - {selectedWorkflowStructuredData.characterAssets.length > 0 ? ( - - 角色资产 {selectedWorkflowStructuredData.characterAssets.length} + {selectedWorkflowStructuredData.characterAssets.length > + 0 ? ( + + 角色资产{" "} + {selectedWorkflowStructuredData.characterAssets.length} ) : null} {selectedWorkflowStructuredData.sceneAssets.length > 0 ? ( - - 场景资产 {selectedWorkflowStructuredData.sceneAssets.length} + + 场景资产{" "} + {selectedWorkflowStructuredData.sceneAssets.length} ) : null} {selectedVideoWorkflowType === "custom-agent" && selectedWorkflowLinkedAgentProject.id ? ( - + Agent 已连接 ) : null} {selectedVideoWorkflowType === "custom-agent" && selectedWorkflowAgentResultCount > 0 ? ( - + Agent 结果 {selectedWorkflowAgentResultCount} ) : null} {selectedVideoWorkflowType === "custom-agent" && selectedWorkflowPendingAgentResultCount > 0 ? ( - + 待回流 {selectedWorkflowPendingAgentResultCount} ) : null}
-
+
{selectedWorkflowPrompt.trim() ? selectedWorkflowPrompt : "当前还没有抽取到可执行提示词,先在上游接入脚本、分镜或参考素材。"} @@ -7886,21 +8625,22 @@ export default function CanvasEditorPage({ params }: PageProps) { {selectedVideoWorkflowType === "custom-agent" && selectedWorkflowLinkedAgentProject.id ? (
-
+
已关联 Agent 项目
-
- {selectedWorkflowLinkedAgentProject.title || selectedWorkflowLinkedAgentProject.id} +
+ {selectedWorkflowLinkedAgentProject.title || + selectedWorkflowLinkedAgentProject.id}
) : null}
@@ -8093,16 +8844,18 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8114,16 +8867,18 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8136,16 +8891,18 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8157,12 +8914,14 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8173,43 +8932,51 @@ export default function CanvasEditorPage({ params }: PageProps) { - {isVideoMode ? "添加图片输入" : "添加图片"} + + {isVideoMode ? "添加图片输入" : "添加图片"} + - {isVideoMode ? "添加脚本节点" : "添加文本"} + + {isVideoMode ? "添加脚本节点" : "添加文本"} + {isVideoMode ? ( @@ -8222,59 +8989,69 @@ export default function CanvasEditorPage({ params }: PageProps) {
-
视频工作流节点库
-
- 这里按参考 APP 的节点目录整理,先把结构补齐,再逐步接入具体能力。 +
+ 视频工作流节点库 +
+
+ 这里按参考 APP + 的节点目录整理,先把结构补齐,再逐步接入具体能力。
{VIDEO_WORKFLOW_LIBRARY_SECTIONS.map((section) => { - const sectionNodes = getVideoWorkflowSectionNodes(section.id); + const sectionNodes = getVideoWorkflowSectionNodes( + section.id + ); return ( -
+
-
+
{section.label}
-
+
{section.description}
{sectionNodes.map((nodeDefinition) => ( handleAddWorkflowNode(nodeDefinition.type)} className="flex items-start gap-3 rounded-xl px-3 py-3" + key={nodeDefinition.type} + onClick={() => + handleAddWorkflowNode(nodeDefinition.type) + } >
- + {nodeDefinition.label} {nodeDefinition.type}
-
+
{nodeDefinition.description}
@@ -8293,8 +9070,11 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8319,8 +9098,11 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8346,12 +9127,14 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8360,15 +9143,49 @@ export default function CanvasEditorPage({ params }: PageProps) { ) : null} + {siteSettingsData?.realtimeCanvasEnabled === true && + siteSettingsData?.realtimeCanvasServiceEnabled !== false ? ( + + + + + + {selectedImageNode + ? "图像增强 Pro" + : "先选中图片后进入图像增强 Pro"} + + + ) : null} + @@ -8380,10 +9197,10 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8396,11 +9213,11 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8432,17 +9251,20 @@ export default function CanvasEditorPage({ params }: PageProps) {
{/* 6. Bottom Left Compact Controls */}
@@ -8471,14 +9293,14 @@ export default function CanvasEditorPage({ params }: PageProps) { @@ -8489,27 +9311,27 @@ export default function CanvasEditorPage({ params }: PageProps) {
- + {Math.round(viewport.zoom * 100)}% @@ -8519,9 +9341,9 @@ export default function CanvasEditorPage({ params }: PageProps) { {/* 7. 物品拾取模式覆盖层 */} {loginDialogElement}
diff --git a/app/canvas/[id]/realtime/page.tsx b/app/canvas/[id]/realtime/page.tsx index 9596f20c4..c701496af 100644 --- a/app/canvas/[id]/realtime/page.tsx +++ b/app/canvas/[id]/realtime/page.tsx @@ -1,16 +1,19 @@ "use client"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useCallback } from "react"; -import { useParams, useRouter } from "next/navigation"; import { toast } from "sonner"; import { RealtimeCanvasDialog } from "@/components/canvas/RealtimeCanvasDialog"; import { canvasService } from "@/services/canvas"; -import { useCanvasStore, canvasSelectors } from "@/store/canvas"; +import { canvasSelectors, useCanvasStore } from "@/store/canvas"; export default function CanvasRealtimePage() { const router = useRouter(); const routeParams = useParams(); + const searchParams = useSearchParams(); const id = String(routeParams?.id || ""); + const initialMode = + searchParams?.get("mode") === "enhance-pro" ? "enhance-pro" : "realtime"; const viewport = useCanvasStore(canvasSelectors.viewport); const selectedNodeIds = useCanvasStore(canvasSelectors.selectedNodeIds); @@ -20,7 +23,9 @@ export default function CanvasRealtimePage() { const selectedImageNode = selectedNodeIds.length === 1 - ? nodes.find((node) => node.id === selectedNodeIds[0] && node.type === "image") + ? nodes.find( + (node) => node.id === selectedNodeIds[0] && node.type === "image" + ) : null; const handleOpenChange = useCallback( @@ -33,17 +38,47 @@ export default function CanvasRealtimePage() { ); const handleApplyResult = useCallback( - async ({ imageUrl, prompt }: { imageUrl: string; prompt: string }) => { + async ({ + imageUrl, + prompt, + title, + model, + width, + height, + }: { + imageUrl: string; + prompt: string; + title?: string; + model?: string; + width?: number; + height?: number; + }) => { const viewportX = viewport.x || 0; const viewportY = viewport.y || 0; const viewportZoom = viewport.zoom || 1; - const stageWidth = typeof window !== "undefined" ? window.innerWidth : 1280; - const stageHeight = typeof window !== "undefined" ? window.innerHeight : 720; - const targetWidth = 360; - const targetHeight = 360; + const stageWidth = + typeof window !== "undefined" ? window.innerWidth : 1280; + const stageHeight = + typeof window !== "undefined" ? window.innerHeight : 720; + const safeWidth = Number(width); + const safeHeight = Number(height); + const hasNaturalSize = safeWidth > 0 && safeHeight > 0; + const maxPreviewEdge = 420; + const fallbackSize = 360; + const scale = hasNaturalSize + ? Math.min(1, maxPreviewEdge / Math.max(safeWidth, safeHeight)) + : 1; + const targetWidth = hasNaturalSize + ? Math.max(140, Math.round(safeWidth * scale)) + : fallbackSize; + const targetHeight = hasNaturalSize + ? Math.max(140, Math.round(safeHeight * scale)) + : fallbackSize; - const x = (stageWidth * 0.45 - viewportX) / viewportZoom - targetWidth / 2; - const y = (stageHeight * 0.45 - viewportY) / viewportZoom - targetHeight / 2; + const x = + (stageWidth * 0.45 - viewportX) / viewportZoom - targetWidth / 2; + const y = + (stageHeight * 0.45 - viewportY) / viewportZoom - targetHeight / 2; addNode({ type: "image", @@ -52,9 +87,9 @@ export default function CanvasRealtimePage() { data: { src: imageUrl, status: "completed", - title: "实时画布结果", + title: title || "实时画布结果", prompt, - model: "bfl/flux-2", + model: model || "bfl/flux-2", }, connections: [], }); @@ -65,18 +100,23 @@ export default function CanvasRealtimePage() { console.error("[RealtimeCanvasPage] 保存结果失败:", error); } - toast.success("实时画布结果已添加"); + toast.success(`${title || "结果"}已添加到画布`); }, [addNode, getCanvasState, id, viewport.x, viewport.y, viewport.zoom] ); return ( ); } diff --git a/components/canvas/EnhanceProWorkspace.tsx b/components/canvas/EnhanceProWorkspace.tsx new file mode 100644 index 000000000..879a233b4 --- /dev/null +++ b/components/canvas/EnhanceProWorkspace.tsx @@ -0,0 +1,1107 @@ +"use client"; + +import { + CheckCircle2, + ImageIcon, + Loader2, + Send, + Sparkles, + Wand2, +} from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import useSWR from "swr"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; +import { + defaultEnhanceProPresets, + type EnhanceProKreaModel, + type EnhanceProOutputFormat, + type EnhanceProPreset, + type EnhanceProSubjectDetection, +} from "@/lib/enhance-pro/config"; +import { buildImageProxyUrl } from "@/lib/image-delivery"; +import { cn } from "@/lib/utils"; +import { + type EnhanceProImageResult, + type EnhanceProRequestParams, + imageGenerationService, +} from "@/services/canvas"; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +type EnhanceProClientConfig = { + enabled: boolean; + providerName: string; + baseUrl: string; + defaultModel: EnhanceProKreaModel; + defaultOutputFormat: EnhanceProOutputFormat; + allowBatchSize: boolean; + maxBatchSize: number; + maxOutputEdge: number; + enableAdvancedControls: boolean; + presets: EnhanceProPreset[]; +}; + +type FormState = { + prompt: string; + width: number; + height: number; + batchSize: number; + seed: number; + output_format: EnhanceProOutputFormat; + subject_detection: EnhanceProSubjectDetection; + face_enhancement: boolean; + face_enhancement_creativity: number; + face_enhancement_strength: number; + crop_to_fill: boolean; + model: EnhanceProKreaModel; + upscaling_activated: boolean; + image_scaling_factor: number; + creativity: number; + texture: number; + sharpen: number; + denoise: number; + detail: number; +}; + +const FALLBACK_CONFIG: EnhanceProClientConfig = { + enabled: false, + providerName: "Krea Topaz Generative", + baseUrl: "https://api.krea.ai", + defaultModel: "Redefine", + defaultOutputFormat: "jpg", + allowBatchSize: true, + maxBatchSize: 4, + maxOutputEdge: 4096, + enableAdvancedControls: true, + presets: defaultEnhanceProPresets(), +}; + +function clampNumber(value: number, min: number, max: number) { + return Math.min(max, Math.max(min, value)); +} + +function fitDimensions(width: number, height: number, maxOutputEdge: number) { + const safeWidth = Math.max(1, Math.round(width || 1024)); + const safeHeight = Math.max(1, Math.round(height || 1024)); + const maxEdge = Math.max( + 512, + Math.round(maxOutputEdge || FALLBACK_CONFIG.maxOutputEdge) + ); + const currentMax = Math.max(safeWidth, safeHeight); + + if (currentMax <= maxEdge) { + return { width: safeWidth, height: safeHeight }; + } + + const scale = maxEdge / currentMax; + return { + width: Math.max(1, Math.round(safeWidth * scale)), + height: Math.max(1, Math.round(safeHeight * scale)), + }; +} + +function createDefaultFormState( + config: EnhanceProClientConfig, + width: number, + height: number +): FormState { + const size = fitDimensions(width, height, config.maxOutputEdge); + return { + prompt: "", + width: size.width, + height: size.height, + batchSize: 1, + seed: 1337, + output_format: config.defaultOutputFormat, + subject_detection: "All", + face_enhancement: false, + face_enhancement_creativity: 0.5, + face_enhancement_strength: 0.5, + crop_to_fill: false, + model: config.defaultModel, + upscaling_activated: false, + image_scaling_factor: 1, + creativity: 3, + texture: 3, + sharpen: 0.5, + denoise: 0.5, + detail: 0.5, + }; +} + +function applyPresetToForm( + config: EnhanceProClientConfig, + width: number, + height: number, + preset?: EnhanceProPreset +): FormState { + const baseState = createDefaultFormState(config, width, height); + + if (!preset) { + return baseState; + } + + return { + ...baseState, + prompt: preset.prompt, + batchSize: config.allowBatchSize + ? clampNumber(Math.round(preset.batchSize || 1), 1, config.maxBatchSize) + : 1, + seed: Number.isFinite(Number(preset.seed)) + ? Math.round(Number(preset.seed)) + : baseState.seed, + output_format: preset.outputFormat, + subject_detection: preset.subjectDetection, + face_enhancement: preset.faceEnhancement, + face_enhancement_creativity: clampNumber( + preset.faceEnhancementCreativity, + 0, + 1 + ), + face_enhancement_strength: clampNumber( + preset.faceEnhancementStrength, + 0, + 1 + ), + crop_to_fill: preset.cropToFill, + model: preset.model, + upscaling_activated: preset.upscalingActivated, + image_scaling_factor: clampNumber(preset.imageScalingFactor, 1, 32), + creativity: clampNumber(Math.round(preset.creativity), 1, 6), + texture: clampNumber(Math.round(preset.texture), 1, 5), + sharpen: clampNumber(preset.sharpen, 0, 1), + denoise: clampNumber(preset.denoise, 0, 1), + detail: clampNumber(preset.detail, 0, 1), + }; +} + +function getDisplayImageUrl(url?: string | null, width = 1400) { + const raw = String(url || "").trim(); + if (!raw) return ""; + return /^https?:\/\//i.test(raw) + ? buildImageProxyUrl(raw, { + width, + quality: 88, + format: "webp", + }) + : raw; +} + +function getSubjectDetectionLabel( + subjectDetection: EnhanceProSubjectDetection +) { + if (subjectDetection === "Foreground") return "前景"; + if (subjectDetection === "Background") return "背景"; + return "全部"; +} + +function getActiveResultLabel(index: number) { + return `结果 ${index + 1}`; +} + +function buildEnhanceSourcePayload( + sourceImageUrl?: string | null +): Pick< + EnhanceProRequestParams, + "imageUrl" | "image_base64" | "mimeType" +> | null { + const raw = String(sourceImageUrl || "").trim(); + if (!raw) return null; + + const dataUrlMatch = raw.match(/^data:([^;]+);base64,(.+)$/); + if (dataUrlMatch) { + return { + image_base64: dataUrlMatch[2], + mimeType: dataUrlMatch[1] || "image/png", + }; + } + + if (/^https?:\/\//i.test(raw)) { + return { + imageUrl: raw, + }; + } + + return null; +} + +interface EnhanceProWorkspaceProps { + sourceImageUrl?: string | null; + onApplyResult: (payload: { + imageUrl: string; + prompt: string; + title?: string; + model?: string; + width?: number; + height?: number; + }) => void; +} + +export function EnhanceProWorkspace({ + sourceImageUrl, + onApplyResult, +}: EnhanceProWorkspaceProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [naturalSize, setNaturalSize] = useState({ width: 1024, height: 1024 }); + const [selectedPresetId, setSelectedPresetId] = useState(""); + const [selectedResultIndex, setSelectedResultIndex] = useState(-1); + const [formState, setFormState] = useState(() => + createDefaultFormState(FALLBACK_CONFIG, 1024, 1024) + ); + const [generatedImages, setGeneratedImages] = useState< + EnhanceProImageResult[] + >([]); + const [lastErrorMessage, setLastErrorMessage] = useState(""); + + const { data: configPayload, isLoading } = useSWR<{ + success?: boolean; + config?: EnhanceProClientConfig; + }>("/api/enhance/pro/config", fetcher); + + const config = useMemo( + () => configPayload?.config || FALLBACK_CONFIG, + [configPayload?.config] + ); + const presets = useMemo( + () => (config.presets.length ? config.presets : FALLBACK_CONFIG.presets), + [config.presets] + ); + const selectedPreset = useMemo( + () => presets.find((item) => item.id === selectedPresetId) || null, + [presets, selectedPresetId] + ); + const selectedGeneratedImage = + selectedResultIndex >= 0 + ? generatedImages[selectedResultIndex] || null + : null; + const activePreviewUrl = + selectedGeneratedImage?.originalUrl || sourceImageUrl || ""; + const activePreviewLabel = + selectedGeneratedImage && selectedResultIndex >= 0 + ? getActiveResultLabel(selectedResultIndex) + : "原图"; + + useEffect(() => { + const imageSource = String(sourceImageUrl || "").trim(); + if (!imageSource) { + setNaturalSize({ width: 1024, height: 1024 }); + return; + } + + const image = new window.Image(); + image.onload = () => { + setNaturalSize({ + width: image.naturalWidth || 1024, + height: image.naturalHeight || 1024, + }); + }; + image.onerror = () => { + setNaturalSize({ width: 1024, height: 1024 }); + }; + image.src = imageSource; + }, [sourceImageUrl]); + + useEffect(() => { + const defaultPreset = + presets.find((preset) => preset.isDefault) || presets[0]; + setSelectedPresetId(defaultPreset?.id || ""); + setFormState( + applyPresetToForm( + config, + naturalSize.width, + naturalSize.height, + defaultPreset + ) + ); + }, [config, naturalSize.height, naturalSize.width, presets]); + + useEffect(() => { + setGeneratedImages([]); + setSelectedResultIndex(-1); + setLastErrorMessage(""); + }, [sourceImageUrl]); + + const handleNumericField = ( + key: K, + nextValue: number + ) => { + setFormState((prev) => ({ + ...prev, + [key]: nextValue, + })); + }; + + const handleSelectPreset = (presetId: string) => { + const preset = presets.find((item) => item.id === presetId); + setSelectedPresetId(presetId); + setFormState( + applyPresetToForm(config, naturalSize.width, naturalSize.height, preset) + ); + }; + + const handleSubmit = async () => { + if (!config.enabled) { + toast.error("图像增强 Pro 当前未开启,请先在系统设置中启用"); + return; + } + + const sourcePayload = buildEnhanceSourcePayload(sourceImageUrl); + if (!sourcePayload) { + toast.error( + "当前没有可用于增强的图片,请先选择画布图片或切回实时模式准备一张结果图" + ); + return; + } + + setIsSubmitting(true); + setLastErrorMessage(""); + try { + const result = await imageGenerationService.enhanceImagePro({ + ...sourcePayload, + prompt: formState.prompt.trim() || "图像增强 Pro", + width: clampNumber( + Math.round(formState.width), + 1, + config.maxOutputEdge + ), + height: clampNumber( + Math.round(formState.height), + 1, + config.maxOutputEdge + ), + batchSize: config.allowBatchSize + ? clampNumber(Math.round(formState.batchSize), 1, config.maxBatchSize) + : 1, + seed: Math.round(formState.seed), + output_format: formState.output_format, + subject_detection: formState.subject_detection, + face_enhancement: formState.face_enhancement, + face_enhancement_creativity: clampNumber( + formState.face_enhancement_creativity, + 0, + 1 + ), + face_enhancement_strength: clampNumber( + formState.face_enhancement_strength, + 0, + 1 + ), + crop_to_fill: formState.crop_to_fill, + model: formState.model, + upscaling_activated: formState.upscaling_activated, + image_scaling_factor: clampNumber( + formState.image_scaling_factor, + 1, + 32 + ), + creativity: clampNumber(Math.round(formState.creativity), 1, 6), + texture: clampNumber(Math.round(formState.texture), 1, 5), + sharpen: clampNumber(formState.sharpen, 0, 1), + denoise: clampNumber(formState.denoise, 0, 1), + detail: clampNumber(formState.detail, 0, 1), + }); + + if (!result.success || !result.data?.images?.length) { + throw new Error(result.error || "图像增强 Pro 失败"); + } + + setGeneratedImages(result.data.images); + setSelectedResultIndex(0); + toast.success( + `图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果` + ); + } catch (error) { + const message = + error instanceof Error ? error.message : "图像增强 Pro 失败"; + setLastErrorMessage(message); + toast.error(message); + } finally { + setIsSubmitting(false); + } + }; + + const handleApply = () => { + if (!selectedGeneratedImage?.originalUrl) { + toast.error("请先生成结果并选择一张增强图"); + return; + } + onApplyResult({ + imageUrl: selectedGeneratedImage.originalUrl, + prompt: formState.prompt.trim() || "图像增强 Pro", + title: "图像增强 Pro 结果", + model: selectedPreset?.model || config.defaultModel, + width: selectedGeneratedImage.width, + height: selectedGeneratedImage.height, + }); + }; + + const sidePreviewItems = useMemo(() => { + const items: Array<{ + key: string; + label: string; + url: string; + isSource?: boolean; + }> = []; + + if (sourceImageUrl) { + items.push({ + key: "source", + label: "原图", + url: sourceImageUrl, + isSource: true, + }); + } + + generatedImages.forEach((image, index) => { + items.push({ + key: `result-${index}`, + label: getActiveResultLabel(index), + url: image.thumbnailUrl || image.originalUrl, + }); + }); + + return items; + }, [generatedImages, sourceImageUrl]); + + return ( +
+
+ {activePreviewUrl ? ( + <> + {activePreviewLabel} +
+ + {activePreviewLabel} +
+ + ) : ( +
+ +
+ 还没有可增强的图片 +
+
+ 请先在主画布中选中一张图片,再从底部工具栏进入“图像增强 Pro”, + 或者先在实时画布模式里生成一张结果后再切回来。 +
+
+ )} + + {sidePreviewItems.length > 0 ? ( +
+
+ {sidePreviewItems.map((item, index) => { + const isActive = item.isSource + ? selectedResultIndex < 0 + : selectedResultIndex === index - 1; + return ( + + ); + })} +
+
+ ) : null} +
+ +
+
+
+
+
+ + 图像增强模式 +
+
+ 使用 {config.providerName}{" "} + 对单张图片进行更可控的增强、修复和细节重建。 +
+
+
+ {selectedPreset?.model || config.defaultModel} +
+
+ +
+ {sourceImageUrl ? ( + 增强源图 + ) : ( +
+ 暂无源图 +
+ )} +
+
+ + 原图 {naturalSize.width} x {naturalSize.height} + + + 最大输出长边 {config.maxOutputEdge}px + +
+
+ +
+ {isLoading ? ( +
+ + 正在加载增强配置... +
+ ) : null} + + {!config.enabled && !isLoading ? ( +
+ 当前未启用图像增强 Pro。请先在系统设置的“本地AI设置”里完成 Krea + 配置。 +
+ ) : null} + + {lastErrorMessage ? ( +
+ {lastErrorMessage} +
+ ) : null} + +
+ + + {selectedPreset ? ( +
+ {selectedPreset.description || + `${selectedPreset.model} · ${getSubjectDetectionLabel(selectedPreset.subjectDetection)}`} +
+ ) : null} +
+ +
+ +