import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import type { MouseEvent as ReactMouseEvent } from "react"; import { useQuery } from "@tanstack/react-query"; import { useParams, useSearchParams } from "react-router"; import { useConfigStore, useEffectiveConfig } from "@/stores/use-config-store"; import { uploadMediaFile } from "@/services/file-storage"; import { resourceFileUrl, resourceIdFromStorageKey } from "@/services/api/resources"; import copyToClipboard from "copy-to-clipboard"; import { nanoid } from "nanoid"; import { canvasThemes, type CanvasBackgroundMode } from "@/lib/canvas-theme"; import { persistCanvasMediaPerformanceMode, readCanvasMediaPerformanceMode } from "@/lib/canvas/canvas-performance-mode"; import { summarizeCanvasContext } from "@/lib/canvas/canvas-context-summary"; import { refreshCanvasCharacterReferenceNodes } from "@/lib/canvas/canvas-character-reference"; import { useAssetStore } from "@/stores/use-asset-store"; import { useThemeStore } from "@/stores/use-theme-store"; import { App } from "antd"; import { getNodeSpec } from "@/constant/canvas"; import { CanvasConfigComposer } from "@/components/canvas/canvas-config-composer"; import { CanvasConfigNodePanel } from "@/components/canvas/canvas-config-node-panel"; import { CanvasAssistantPanel } from "@/components/canvas/canvas-assistant-panel"; import { CanvasActiveTaskPanel } from "@/components/canvas/canvas-active-task-panel"; import { CanvasAssetTray } from "@/components/canvas/canvas-asset-tray"; import { CanvasProjectSidebar } from "@/components/canvas/canvas-project-sidebar"; import { CanvasProjectAssetModal } from "@/components/canvas/canvas-project-asset-modal"; import { CanvasCharacterReferenceNodeContent } from "@/components/canvas/canvas-character-reference-node"; import { CanvasCharacterReferenceModal } from "@/components/canvas/canvas-character-reference-modal"; import { WorkspaceState } from "@/components/layout/workspace-state"; import { canvasStylePresets } from "@/components/canvas/canvas-style-picker-modal"; import { CanvasNodeHoverToolbar, CanvasNodeInfoModal } from "@/components/canvas/canvas-node-hover-toolbar"; import { CanvasNodeAnglePanel } from "@/components/canvas/canvas-node-angle-dialog"; import { CanvasTextEditorModal } from "@/components/canvas/canvas-text-editor-modal"; import { CanvasNodeSearchModal } from "@/components/canvas/canvas-node-search-modal"; import { CanvasStylePickerModal } from "@/components/canvas/canvas-style-picker-modal"; import { CanvasFileDropOverlay } from "@/components/canvas/canvas-file-drop-overlay"; import { InfiniteCanvas } from "@/components/canvas/infinite-canvas"; import { Minimap } from "@/components/canvas/canvas-mini-map"; import { CanvasNodePromptPanel, type CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel"; import { CanvasToolbar } from "@/components/canvas/canvas-toolbar"; import { AssetPickerModal } from "@/components/canvas/asset-picker-modal"; import { getProject } from "@/services/api/projects"; import { CanvasZoomControls } from "@/components/canvas/canvas-zoom-controls"; import { CanvasShareModal } from "@/components/canvas/canvas-share-modal"; import { CanvasScriptEditor, CanvasScriptNodeContent, STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardMinNodeHeight, storyboardTableHeight } from "@/components/canvas/canvas-script-node"; import { CanvasDirectorNodePanel } from "@/components/canvas/director/canvas-director-node-panel"; import { CanvasVersionCompareModal } from "@/components/canvas/canvas-version-compare-modal"; import { CanvasLocalAgentPanel } from "@/components/canvas/canvas-local-agent-panel"; import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store"; import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; import { CanvasConnectionCreateMenu, CanvasNodePanelOverlay } from "@/components/canvas/canvas-workspace-overlays"; import { CanvasLeaferGraphicsLayer } from "@/components/canvas/canvas-leafer-graphics-layer"; import { CanvasLinkedProjectEmptyState, CanvasShortDramaEmptyState, CanvasShortDramaGuide, CanvasStoryInputNodeContent, CanvasStylePlaceholderNodeContent } from "@/components/canvas/canvas-short-drama-entry"; import { createCanvasNode, getInputSummary, isHiddenBatchChild, persistCanvasWorkspaceMode, readCanvasWorkspaceMode, } from "@/lib/canvas/canvas-project-domain"; import { deriveStoryboardPipelineProgress, } from "@/lib/canvas/canvas-storyboard-progress"; import { CanvasAgentChangeToast, CanvasMergeStatusToast, CanvasUploadStatusToast } from "./canvas-project-feedback"; import { backendProviderConfig, getGenerationCount, } from "@/lib/canvas/canvas-project-generation"; import { CanvasTopBar } from "./canvas-project-top-bar"; import { CanvasProjectContextMenu } from "./canvas-project-context-menu"; import { CanvasProjectMediaDialogs } from "./canvas-project-media-dialogs"; import { CanvasProjectSelectionToolbar } from "./canvas-project-selection-toolbar"; import { CanvasProjectStatusDialogs } from "./canvas-project-status-dialogs"; import { CanvasProjectWorldLayers } from "./canvas-project-world-layers"; import type { CanvasImageEmotionPayload } from "@/components/canvas/canvas-node-emotion-panel"; import { CanvasEmotionWorkspace } from "@/components/canvas/canvas-emotion-workspace"; import { removeCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage"; import { useCanvasConnectionController } from "./use-canvas-connection-controller"; import { useCanvasAgentOperations } from "./use-canvas-agent-operations"; import { useCanvasAssistantVisibility } from "./use-canvas-assistant-visibility"; import { useCanvasActiveTasks } from "./use-canvas-active-tasks"; import { useCanvasStyleWorkflow } from "./use-canvas-style-workflow"; import { useCanvasDirector } from "./use-canvas-director"; import { useCanvasGeneration } from "./use-canvas-generation"; import { useCanvasGenerationBatches } from "./use-canvas-generation-batches"; import { useCanvasGenerationExecutor } from "./use-canvas-generation-executor"; import { useCanvasGenerationRetry } from "./use-canvas-generation-retry"; import { useCanvasHistory } from "./use-canvas-history"; import { useCanvasKeyboard } from "./use-canvas-keyboard"; import { useCanvasMediaTools } from "./use-canvas-media-tools"; import { useCanvasNodeEditor } from "./use-canvas-node-editor"; import { useCanvasNodeOperations } from "./use-canvas-node-operations"; import { useCanvasProjectLifecycle } from "./use-canvas-project-lifecycle"; import { useCanvasRenderModel } from "./use-canvas-render-model"; import { useCanvasSelectionController } from "./use-canvas-selection-controller"; import { useCanvasShortDrama } from "./use-canvas-short-drama"; import { useCanvasStoryboard } from "./use-canvas-storyboard"; import { useCanvasUpload } from "./use-canvas-upload"; import { useCanvasViewportController } from "./use-canvas-viewport-controller"; import { CanvasNodeType, type CanvasAssistantSession, type CanvasConnection, type CanvasNodeData, type CanvasMediaPerformanceMode, type StoryboardColumn, type StoryboardShotCount, type StoryboardShotDuration, type CanvasWorkflowKind, type CanvasWorkspaceMode, type ContextMenuState, type Position, type ViewportTransform, } from "@/types/canvas"; import type { ReferenceImage } from "@/types/image"; const CanvasDirectorWorkbench = lazy(() => import("@/components/canvas/director/canvas-director-workbench").then((module) => ({ default: module.CanvasDirectorWorkbench }))); const CanvasDrawingEditorModal = lazy(() => import("@/components/canvas/canvas-drawing-editor-modal").then((module) => ({ default: module.CanvasDrawingEditorModal }))); const NODE_STATUS_SUCCESS = "success" as const; const EMPTY_RESOURCE_REFERENCES: CanvasResourceReference[] = []; function visibleGenerationBatch(node: CanvasNodeData) { const batches = node.metadata?.generationBatches || []; for (let index = batches.length - 1; index >= 0; index -= 1) { if (batches[index].status === "queued" || batches[index].status === "running") return batches[index]; } return batches.at(-1); } export default function CanvasPage() { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) return ; return ; } function CanvasRefreshShell() { return (
); } function InfiniteCanvasPage() { const { message } = App.useApp(); const params = useParams<{ id: string }>(); const [searchParams] = useSearchParams(); const projectId = params.id || ""; const localAgentConnected = useCanvasAgentStore((state) => state.connected); const localAgentActivity = useCanvasAgentStore((state) => state.activity); const localAgentEnabled = useCanvasAgentStore((state) => state.enabled); const containerRef = useRef(null); const didInitialCenterRef = useRef(false); const toolbarHideTimerRef = useRef | null>(null); const config = useConfigStore((state) => state.config); const effectiveConfig = useEffectiveConfig(); const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady); const assets = useAssetStore((state) => state.assets); const cleanupAssetImages = useAssetStore((state) => state.cleanupImages); const theme = canvasThemes[useThemeStore((state) => state.theme)]; const [nodes, setNodes] = useState([]); const [connections, setConnections] = useState([]); const [chatSessions, setChatSessions] = useState([]); const [activeChatId, setActiveChatId] = useState(null); const [viewport, setViewport] = useState({ x: 0, y: 0, k: 1 }); const [size, setSize] = useState({ width: 1200, height: 720 }); const [selectedNodeIds, setSelectedNodeIds] = useState>(new Set()); const [selectedConnectionId, setSelectedConnectionId] = useState(null); const [hoveredNodeId, setHoveredNodeId] = useState(null); const [contextMenu, setContextMenu] = useState(null); const [isMiniMapOpen, setIsMiniMapOpen] = useState(false); const [backgroundMode, setBackgroundMode] = useState("dots"); const [showImageInfo, setShowImageInfo] = useState(false); const [mediaPerformanceMode, setMediaPerformanceMode] = useState(readCanvasMediaPerformanceMode); const [projectLoaded, setProjectLoaded] = useState(false); const [workspaceMode, setWorkspaceMode] = useState(readCanvasWorkspaceMode); const [clearConfirmOpen, setClearConfirmOpen] = useState(false); const [shareModalOpen, setShareModalOpen] = useState(false); const [nodeSearchOpen, setNodeSearchOpen] = useState(false); const [toolbarNodeId, setToolbarNodeId] = useState(null); const [nodeImageSettingsOpen, setNodeImageSettingsOpen] = useState(false); const [dialogNodeId, setDialogNodeId] = useState(null); const [textEditorNodeId, setTextEditorNodeId] = useState(null); const [characterReferenceNodeId, setCharacterReferenceNodeId] = useState(null); const [drawingNodeId, setDrawingNodeId] = useState(null); const [stylePickerOpen, setStylePickerOpen] = useState(false); const [projectAssetOpen, setProjectAssetOpen] = useState(false); const [projectAssetInitialCategory, setProjectAssetInitialCategory] = useState("all"); const [projectAssetInsertPosition, setProjectAssetInsertPosition] = useState(); const [infoNodeId, setInfoNodeId] = useState(null); const [superResolveNodeId, setSuperResolveNodeId] = useState(null); const [previewNodeId, setPreviewNodeId] = useState(null); const [scriptEditorNodeId, setScriptEditorNodeId] = useState(null); const [scriptScrollTopById, setScriptScrollTopById] = useState>({}); const [directorNodeId, setDirectorNodeId] = useState(null); const [versionCompareRootId, setVersionCompareRootId] = useState(null); const codexAutoConnect = ["new", "recent", "choose"].includes(searchParams.get("mode") || ""); const codexCompactAgent = codexAutoConnect && searchParams.has("agentUrl"); const [titleEditing, setTitleEditing] = useState(false); const [titleDraft, setTitleDraft] = useState(""); const [shortcutRequestNonce, setShortcutRequestNonce] = useState(0); const [cinematicAgentEntry, setCinematicAgentEntry] = useState(false); const { agentMode, assistantClosing, assistantMounted, assistantOpen, closeAgent, openAgent, setAgentMode } = useCanvasAssistantVisibility(); const { tasks: activeTasks } = useCanvasActiveTasks(projectId, projectLoaded); useEffect(() => { persistCanvasWorkspaceMode(workspaceMode); }, [workspaceMode]); useEffect(() => { persistCanvasMediaPerformanceMode(mediaPerformanceMode); }, [mediaPerformanceMode]); useEffect(() => { didInitialCenterRef.current = false; }, [projectId]); useEffect(() => { const openSearch = (event: KeyboardEvent) => { if (!(event.metaKey || event.ctrlKey) || event.key.toLocaleLowerCase() !== "k") return; const target = event.target; if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || (target instanceof HTMLElement && target.isContentEditable)) return; event.preventDefault(); setNodeSearchOpen(true); }; window.addEventListener("keydown", openSearch); return () => window.removeEventListener("keydown", openSearch); }, []); const nodesRef = useRef(nodes); const connectionsRef = useRef(connections); const selectedNodeIdsRef = useRef(selectedNodeIds); const viewportRef = useRef(viewport); const generateNodeRef = useRef<((nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => Promise) | null>(null); const { getHistoryCleanupContext, historyPausedRef, historyState, redoCanvas, resetHistory, undoCanvas } = useCanvasHistory({ projectLoaded, nodes, connections, chatSessions, activeChatId, backgroundMode, showImageInfo, setNodes, setConnections, setChatSessions, setActiveChatId, setBackgroundMode, setShowImageInfo, setSelectedNodeIds, setSelectedConnectionId, setContextMenu, }); const cleanupCanvasFiles = useCallback( (extra?: unknown) => { cleanupAssetImages({ extra, ...getHistoryCleanupContext() }); }, [cleanupAssetImages, getHistoryCleanupContext], ); const { activatedSkills, clearCanvasFiles, createAndOpenProject, currentProject, deleteCurrentProject, renameCurrentProject, saveCanvasProject, updateProject, } = useCanvasProjectLifecycle({ projectId, projectLoaded, nodes, connections, chatSessions, activeChatId, backgroundMode, showImageInfo, viewport, nodesRef, connectionsRef, viewportRef, historyPausedRef, setNodes, setConnections, setChatSessions, setActiveChatId, setBackgroundMode, setShowImageInfo, setViewport, setProjectLoaded, resetHistory, cleanupAssetImages, cleanupCanvasFiles, }); const linkedProjectId = currentProject?.projectId || ""; const linkedProjectQuery = useQuery({ queryKey: ["project", linkedProjectId], queryFn: () => getProject(linkedProjectId), enabled: Boolean(linkedProjectId) }); useEffect(() => { if (!projectLoaded || !linkedProjectQuery.data) return; setNodes((current) => refreshCanvasCharacterReferenceNodes(current, linkedProjectQuery.data.assets)); }, [linkedProjectQuery.data, projectLoaded, setNodes]); const canvasContext = useMemo( () => summarizeCanvasContext(nodes, selectedNodeIds, linkedProjectQuery.data?.units), [linkedProjectQuery.data?.units, nodes, selectedNodeIds], ); const { bindGenerationTask, cancelNodeTask, confirmStopGeneration, finishGenerationRequest, openNodeTaskDetails, runningNodeId, setRunningNodeId, setTaskDetail, startGenerationRequest, taskDetail, taskDetailLoading, taskDetailLogs, } = useCanvasGeneration({ projectId, domainProjectId: linkedProjectId, projectLoaded, nodes, nodesRef, setNodes }); useEffect(() => { if (!projectLoaded || !["new", "recent", "choose"].includes(searchParams.get("mode") || "")) return; if (searchParams.has("agentUrl")) { setAgentMode("local"); return; } openAgent("local"); }, [openAgent, projectLoaded, searchParams, setAgentMode]); useEffect(() => { if (!dialogNodeId) setNodeImageSettingsOpen(false); }, [dialogNodeId]); useLayoutEffect(() => { nodesRef.current = nodes; connectionsRef.current = connections; selectedNodeIdsRef.current = selectedNodeIds; viewportRef.current = viewport; }, [nodes, connections, selectedNodeIds, viewport]); useEffect(() => { if (!projectLoaded) return; const el = containerRef.current; if (!el) return; const updateSize = () => { const rect = el.getBoundingClientRect(); setSize((current) => current.width === rect.width && current.height === rect.height ? current : { width: rect.width, height: rect.height }); if (!didInitialCenterRef.current) { didInitialCenterRef.current = true; const current = viewportRef.current; if (current.x === 0 && current.y === 0 && current.k === 1) { const centered = { x: rect.width / 2, y: rect.height / 2, k: 1 }; viewportRef.current = centered; setViewport(centered); } } }; updateSize(); const resizeObserver = new ResizeObserver(updateSize); resizeObserver.observe(el); return () => resizeObserver.disconnect(); }, [projectLoaded]); const { fitCanvasContent, fitCanvasSelection, focusCanvasImageNode, focusCanvasNode, getCanvasCenter, handleCanvasDoubleClick, handleViewportChange, handleViewportPreviewChange, previewViewport, resetViewport, screenToCanvas, setZoomScale, zoomCanvasIn, zoomCanvasOut, zoomToActualSize, } = useCanvasViewportController({ containerRef, size, viewportRef, nodesRef, selectedNodeIdsRef, setViewport, setSelectedNodeIds, setSelectedConnectionId, setContextMenu, setDialogNodeId, setToolbarNodeId, }); useEffect(() => { const preset = canvasStylePresets.find((item) => item.id === linkedProjectQuery.data?.project.stylePresetId); if (!projectLoaded || !preset) return; const current = nodesRef.current.find((node) => node.type === CanvasNodeType.Text && node.metadata?.workflowKind === "styleboard"); const nextMetadata = { content: preset.prompt, prompt: preset.prompt, status: NODE_STATUS_SUCCESS, workflowKind: "styleboard" as const, workflowTitle: "项目画风", workflowDescription: preset.description, stylePresetId: preset.id, fontSize: 14, locked: true, }; if (current) { if (current.metadata?.stylePresetId === preset.id && current.metadata?.content === preset.prompt && current.metadata?.locked) return; setNodes((nodes) => nodes.map((node) => node.id === current.id ? { ...node, title: `项目画风 · ${preset.title}`, metadata: { ...node.metadata, ...nextMetadata } } : node)); return; } const node = createCanvasNode(CanvasNodeType.Text, getCanvasCenter(), nextMetadata); node.title = `项目画风 · ${preset.title}`; node.width = 420; node.height = 240; setNodes((nodes) => [...nodes, node]); }, [getCanvasCenter, linkedProjectQuery.data?.project.stylePresetId, projectLoaded, setNodes]); const { assetPickerOpen, closeAssetPicker, createImageAssetNode, fileDropActive, handleAssetInsert, handleDrop, handleFileDragEnter, handleFileDragLeave, handleFileDragOver, handleImageInputChange, handleProjectAssetsInsert, handleProjectChapterInsert, handleUploadRequest, imageInputRef, openAssetsAtPosition, pasteAssistantImage, pasteSystemClipboard, startUploadStatus, uploadStatus, } = useCanvasUpload({ canvasId: projectId, domainProjectId: linkedProjectId, nodesRef, selectedNodeIdsRef, getCanvasCenter, screenToCanvas, setNodes, setSelectedNodeIds, setSelectedConnectionId, setContextMenu, setDialogNodeId, }); const openProjectAssets = useCallback((initialCategory = "all", position?: Position) => { setProjectAssetInitialCategory(initialCategory); setProjectAssetInsertPosition(position); setProjectAssetOpen(true); }, []); const closeProjectAssets = useCallback(() => { setProjectAssetOpen(false); setProjectAssetInsertPosition(undefined); }, []); const { angleNodeId, emotionNodeId, annotationNodeId, createImageReversePromptNodes, createPortraitTextureNode, cropImageNode, cropNodeId, extractVideoLastFrame, extractingVideoFrameNodeId, generateAngleNode, generateEmotionNode, maskEditImageNode, maskEditNodeId, mergeSelectedVideos, mergeVideosByIds, mergeVideoProgress, saveAnnotatedImageNode, setAngleNodeId, setEmotionNodeId, setAnnotationNodeId, setCropNodeId, setMaskEditNodeId, setSplitNodeId, setUpscaleNodeId, splitImageNode, splitNodeId, upscaleImageNode, upscaleNodeId, } = useCanvasMediaTools({ projectId, nodesRef, connectionsRef, selectedNodeIdsRef, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setDialogNodeId, setContextMenu, setHoveredNodeId, setToolbarNodeId, setRunningNodeId, startUploadStatus, startGenerationRequest, finishGenerationRequest, bindGenerationTask, }); const handleNodesDeleted = useCallback((removedIds: Set, nextNodes: CanvasNodeData[], removedNodes: CanvasNodeData[]) => { const clearDeletedId = (current: string | null) => current && removedIds.has(current) ? null : current; setHoveredNodeId(clearDeletedId); setToolbarNodeId(clearDeletedId); setDialogNodeId(clearDeletedId); setTextEditorNodeId(clearDeletedId); setCharacterReferenceNodeId(clearDeletedId); setDrawingNodeId(clearDeletedId); setInfoNodeId(clearDeletedId); setCropNodeId(clearDeletedId); setMaskEditNodeId(clearDeletedId); setAnnotationNodeId(clearDeletedId); setSplitNodeId(clearDeletedId); setUpscaleNodeId(clearDeletedId); setAngleNodeId(clearDeletedId); setEmotionNodeId(clearDeletedId); setSuperResolveNodeId(clearDeletedId); setPreviewNodeId(clearDeletedId); setRunningNodeId(clearDeletedId); setScriptEditorNodeId(clearDeletedId); setDirectorNodeId(clearDeletedId); setVersionCompareRootId(clearDeletedId); setScriptScrollTopById((current) => Object.fromEntries(Object.entries(current).filter(([id]) => !removedIds.has(id)))); setContextMenu((current) => current?.type === "node" && removedIds.has(current.nodeId) ? null : current); const removedDrawingIds = removedNodes.flatMap((node) => node.type === CanvasNodeType.Drawing && node.metadata?.drawingId ? [node.metadata.drawingId] : []); if (removedDrawingIds.length) { void Promise.all(removedDrawingIds.map((drawingId) => removeCanvasDrawing(projectId, drawingId))) .catch(() => message.warning("绘图节点已删除,但本地绘图缓存清理失败")); } cleanupCanvasFiles({ projectId, nodes: nextNodes, chatSessions }); }, [chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setCropNodeId, setEmotionNodeId, setMaskEditNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId]); const { alignSelectedNodes, arrangeSelectedNodes, copyNodesToClipboard, copySelectedNodes, createNode, createReferenceGroup, createStoryboardGroup, deleteConnection, deleteNodes, duplicateNode, hasCopiedNodes, pasteCopiedNodes, releaseCopiedNodesPastePriority, setPrimaryVersion, shouldPreferCopiedNodes, toggleNodeLocked, } = useCanvasNodeOperations({ projectId, nodesRef, connectionsRef, selectedNodeIdsRef, getCanvasCenter, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setContextMenu, setDialogNodeId, onNodesDeleted: handleNodesDeleted, }); const { cancelPendingConnectionCreate, closeConnectionCreateMenu, connectionTargetAnchorRatio, connectionTargetNodeId, connectingParams, createConnectedNode, handleConnectStart, mouseWorld, pendingConnectionCreate, setConnecting, } = useCanvasConnectionController({ projectId, nodesRef, connectionsRef, viewportRef, scriptScrollTopById, screenToCanvas, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setContextMenu, setDialogNodeId, setDrawingNodeId, }); const handleCanvasSelectionStart = useCallback(() => { setContextMenu(null); setDialogNodeId(null); }, []); const handleNodeInteractionStart = useCallback((selectionModifier: boolean) => { setContextMenu(null); setHoveredNodeId(null); setToolbarNodeId(null); if (selectionModifier) setDialogNodeId(null); }, []); const handleSelectedNodeClick = useCallback((node: CanvasNodeData) => { if (node.type === CanvasNodeType.Drawing) { setDialogNodeId(null); setDrawingNodeId(node.id); } else if (node.type === CanvasNodeType.Script) { setDialogNodeId(null); } else if (node.type === CanvasNodeType.Text || node.type === CanvasNodeType.Frame) { setDialogNodeId((current) => current === node.id ? current : null); } else { setDialogNodeId(node.id); } }, []); const handleCanvasDeselect = useCallback(() => { setContextMenu(null); setHoveredNodeId(null); setToolbarNodeId(null); setDialogNodeId(null); }, []); const { alignmentGuides, cancelSelectionBox, deselectCanvas, dragPreview, frameDropTargetId, handleCanvasMouseDown, handleNodeMouseDown, isNodeDragging, nodeDraggingRef, selectionBoundsElementRef, selectionBox, } = useCanvasSelectionController({ containerRef, nodesRef, viewportRef, selectedNodeIdsRef, historyPausedRef, screenToCanvas, setNodes, setSelectedNodeIds, setSelectedConnectionId, cancelPendingConnectionCreate, onCanvasSelectionStart: handleCanvasSelectionStart, onNodeInteractionStart: handleNodeInteractionStart, onNodeClick: handleSelectedNodeClick, onDeselect: handleCanvasDeselect, }); const keepNodeToolbar = useCallback( (nodeId: string) => { if (nodeDraggingRef.current || nodeImageSettingsOpen) return; if (toolbarHideTimerRef.current) { clearTimeout(toolbarHideTimerRef.current); toolbarHideTimerRef.current = null; } setToolbarNodeId(nodeId); }, [nodeImageSettingsOpen], ); const hideNodeToolbar = useCallback(() => { if (toolbarHideTimerRef.current) clearTimeout(toolbarHideTimerRef.current); toolbarHideTimerRef.current = setTimeout(() => { setToolbarNodeId(null); toolbarHideTimerRef.current = null; }, 120); }, []); const { collapsingBatchIds, downloadNodeImage, handleConfigNodeChange, handleFontSizeChange, handleNodeContentChange, handleNodePromptChange, handleNodeResize, handleNodeTitleChange, openingBatchIds, saveNodeAsset, setBatchPrimary, toggleBatchExpanded, toggleFrameCollapsed, toggleNodeFreeResize, } = useCanvasNodeEditor({ canvasId: projectId, domainProjectId: linkedProjectId, nodesRef, setNodes, setSelectedNodeIds, setSelectedConnectionId, setDialogNodeId, setToolbarNodeId, setHoveredNodeId, }); const { activeDirectorScene, activeNodeId, activeScriptNode, activeStylePresetId, angleNode, emotionNode, annotationNode, batchChildCountById, batchMotionById, canvasImageNodes, configInputsById, connectionLayerBounds, contextMenuNode, cropNode, displayConnections, frameChildrenById, imageAssets, infoNode, maskEditNode, mentionReferencesByNodeId, nodeById, previewNode, reduceMediaEffects, relatedHighlight, resourceReferenceByNodeId, selectedNodeBounds, selectedVideoNodes, skillMentionReferences, splitNode, superResolveNode, toolbarNode, upscaleNode, versionCompareNodes, visibleNodes, } = useCanvasRenderModel({ nodes, connections, assets, viewport, viewportSize: size, mediaPerformanceMode, selectedNodeIds, hoveredNodeId, dragPreview, collapsingBatchIds, activatedSkills, directorScenes: currentProject?.directorScenes, toolbarNodeId, infoNodeId, cropNodeId, maskEditNodeId, annotationNodeId, splitNodeId, upscaleNodeId, superResolveNodeId, angleNodeId, emotionNodeId, previewNodeId, contextMenu, versionCompareRootId, directorNodeId, scriptEditorNodeId, dialogNodeId, }); const dialogNode = dialogNodeId ? nodeById.get(dialogNodeId) || null : null; const textEditorNode = textEditorNodeId ? nodeById.get(textEditorNodeId) || null : null; const characterReferenceNode = characterReferenceNodeId ? nodeById.get(characterReferenceNodeId) || null : null; const drawingNode = drawingNodeId ? nodeById.get(drawingNodeId) || null : null; const pendingConnectionSourceNode = pendingConnectionCreate?.connection.handleType === "source" ? nodeById.get(pendingConnectionCreate.connection.nodeId) : null; const canCreateDrawingFromConnection = pendingConnectionSourceNode?.type === CanvasNodeType.Image && Boolean(pendingConnectionSourceNode.metadata?.content); const openTextNodeEditor = useCallback((node: CanvasNodeData) => { if (node.type !== CanvasNodeType.Text) return; setSelectedNodeIds(new Set([node.id])); setSelectedConnectionId(null); setContextMenu(null); setDialogNodeId(null); setToolbarNodeId(null); if (node.metadata?.workflowKind === "character" && node.metadata.characterAssetId) { setCharacterReferenceNodeId(node.id); return; } setTextEditorNodeId(node.id); }, []); const openDrawingNode = useCallback((node: CanvasNodeData) => { if (node.type !== CanvasNodeType.Drawing) return; setSelectedNodeIds(new Set([node.id])); setSelectedConnectionId(null); setContextMenu(null); setDialogNodeId(null); setToolbarNodeId(null); setDrawingNodeId(node.id); }, []); const { agentSnapshot, agentUndoCount, applyAgentOps, canUndoAgentOps, dismissLastAgentChange, lastAgentChange, undoAgentOps, viewLastAgentChange } = useCanvasAgentOperations({ projectId, domainProjectId: currentProject?.projectId, projectTitle: currentProject?.title || "未命名画布", nodes, connections, selectedNodeIds, viewport, nodesRef, connectionsRef, selectedNodeIdsRef, viewportRef, generateNodeRef, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setViewport, setContextMenu, focusSelection: fitCanvasSelection, }); const { selectCanvasStyle } = useCanvasStyleWorkflow({ nodesRef, selectedNodeIdsRef, getCanvasCenter, setNodes, setSelectedNodeIds, setSelectedConnectionId, setDialogNodeId, setStylePickerOpen, }); const { applyDirectorOutput, createDirectorShot, openDirectorWorkbench, saveDirectorScene } = useCanvasDirector({ projectId, directorNodeId, directorScenes: currentProject?.directorScenes || [], nodesRef, connectionsRef, getCanvasCenter, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setDirectorNodeId, updateProject, }); const { activateStep: activateShortDramaStep, createPipeline: createShortDramaPipeline, guideCollapsed: shortDramaGuideCollapsed, openStoryInput, progress: shortDramaProgress, setGuideCollapsed: setShortDramaGuideCollapsed, skipGuide: skipShortDramaGuide, } = useCanvasShortDrama({ nodes, connections, nodesRef, connectionsRef, selectedNodeIdsRef, getCanvasCenter, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setStylePickerOpen, fitCanvasSelection, focusCanvasNode, openTextEditor: openTextNodeEditor, }); const clearCanvas = useCallback(() => { const drawingIds = nodesRef.current.flatMap((node) => node.type === CanvasNodeType.Drawing && node.metadata?.drawingId ? [node.metadata.drawingId] : []); if (drawingIds.length) { void Promise.all(drawingIds.map((drawingId) => removeCanvasDrawing(projectId, drawingId))) .catch(() => message.warning("画布已清空,但部分本地绘图缓存清理失败")); } setNodes([]); setConnections([]); setTextEditorNodeId(null); setDrawingNodeId(null); setInfoNodeId(null); setCropNodeId(null); setMaskEditNodeId(null); setAnnotationNodeId(null); setAngleNodeId(null); setEmotionNodeId(null); setPreviewNodeId(null); setRunningNodeId(null); deselectCanvas(); setClearConfirmOpen(false); clearCanvasFiles(); }, [clearCanvasFiles, deselectCanvas, message, nodesRef, projectId, setEmotionNodeId]); useCanvasKeyboard({ nodesRef, selectedNodeIdsRef, selectedConnectionId, setSelectedNodeIds, setSelectedConnectionId, setContextMenu, setShortcutRequestNonce, setInfoNodeId, setCropNodeId, setMaskEditNodeId, setAnnotationNodeId, saveCanvasProject, zoomToActualSize, fitCanvasContent, fitCanvasSelection, undoCanvas, redoCanvas, cancelSelectionBox, copySelectedNodes, pasteCopiedNodes, shouldPreferCopiedNodes, pasteSystemClipboard, deleteNodes, deleteConnection, deselectCanvas, zoomCanvasIn, zoomCanvasOut, }); const handleAssistantSessionsChange = useCallback((sessions: CanvasAssistantSession[], activeId: string | null) => { setChatSessions(sessions); setActiveChatId(activeId); }, []); const startTitleEditing = useCallback(() => { setTitleDraft(currentProject?.title || "未命名画布"); setTitleEditing(true); }, [currentProject?.title]); const finishTitleEditing = useCallback(() => { const nextTitle = titleDraft.trim(); if (nextTitle) renameCurrentProject(nextTitle); setTitleEditing(false); }, [renameCurrentProject, titleDraft]); const pasteAtPosition = useCallback( (position: Position) => { if (shouldPreferCopiedNodes() && pasteCopiedNodes(position)) return; void (async () => { try { // 标记写入成功时仍优先系统图片,兼容截图和从外部应用复制的媒体。 const handled = await pasteSystemClipboard(position); if (!handled) pasteCopiedNodes(position); } catch { if (!pasteCopiedNodes(position)) message.warning("无法读取剪贴板内容"); } })(); }, [message, pasteCopiedNodes, pasteSystemClipboard, shouldPreferCopiedNodes], ); const copyNodeContentToClipboard = useCallback( async (node: CanvasNodeData | null) => { releaseCopiedNodesPastePriority(); const content = node?.metadata?.content; if (!node || !content) { message.warning("没有可复制的内容"); return; } try { if (node.type === CanvasNodeType.Image && typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) { const response = await fetch(content); const blob = await response.blob(); await navigator.clipboard.write([new ClipboardItem({ [blob.type || "image/png"]: blob })]); message.success("图片已复制"); return; } if (!navigator.clipboard?.writeText) { message.warning("当前浏览器不支持写入剪贴板"); return; } await navigator.clipboard.writeText(content); message.success(node.type === CanvasNodeType.Text ? "文本已复制" : "内容链接已复制"); } catch { message.error("复制失败,请检查浏览器剪贴板权限"); } }, [message, releaseCopiedNodesPastePriority], ); const copyNodeMediaUrlToClipboard = useCallback( async (node: CanvasNodeData | null) => { releaseCopiedNodesPastePriority(); try { const storageKey = node?.metadata?.storageKey; const content = node?.metadata?.content?.trim(); const resourceId = resourceIdFromStorageKey(storageKey); const mediaPath = content && !content.startsWith("data:") && !content.startsWith("blob:") ? content : resourceId ? resourceFileUrl(resourceId) : ""; const mediaURL = mediaPath ? new URL(mediaPath, window.location.href).toString() : ""; if (!mediaURL) throw new Error("当前媒体只有本地内容,没有可复制的地址"); if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(mediaURL); else if (!await copyToClipboard(mediaURL)) throw new Error("当前浏览器不支持写入剪贴板"); message.success(node?.type === CanvasNodeType.Video ? "视频地址已复制" : "图片地址已复制"); } catch (error) { message.error(error instanceof Error ? error.message : "媒体地址复制失败"); } }, [message, releaseCopiedNodesPastePriority], ); const handleCanvasContextMenu = useCallback( (event: ReactMouseEvent) => { const target = event.target instanceof Element ? event.target : null; if (target?.closest("[data-node-id],[data-connection-id]")) return; event.preventDefault(); event.stopPropagation(); if (target?.closest("[data-canvas-no-zoom],.ant-modal,.ant-popover,.ant-dropdown")) { setContextMenu(null); return; } closeConnectionCreateMenu(); setContextMenu({ type: "canvas", x: event.clientX, y: event.clientY, position: screenToCanvas(event.clientX, event.clientY) }); }, [closeConnectionCreateMenu, screenToCanvas], ); const handleNodeContextMenu = useCallback((event: ReactMouseEvent, id: string) => { event.preventDefault(); event.stopPropagation(); setSelectedNodeIds(new Set([id])); setSelectedConnectionId(null); closeConnectionCreateMenu(); setToolbarNodeId(null); setDialogNodeId(null); setContextMenu({ type: "node", x: event.clientX, y: event.clientY, nodeId: id }); }, [closeConnectionCreateMenu]); const handleGenerateNode = useCanvasGenerationExecutor({ projectId, domainProjectId: currentProject?.projectId, activatedSkills, nodesRef, connectionsRef, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setDialogNodeId, setRunningNodeId, startGenerationRequest, finishGenerationRequest, bindGenerationTask, }); useEffect(() => { generateNodeRef.current = handleGenerateNode; }, [handleGenerateNode]); const { cancelSubmittedBatchItem, enqueueGenerationBatch, retryFailedBatchItems, stopRemainingBatchItems, } = useCanvasGenerationBatches({ projectId, projectLoaded, nodes, nodesRef, setNodes, handleGenerateNode, }); const { addScriptRow, createAndGenerateScriptVideos, createScriptActionBoards, createScriptImageNodes, createScriptVideoNodes, generateScriptImages, generateScriptRows, generateScriptVideos, removeScriptRow, replaceScriptRows, updateScriptRow, } = useCanvasStoryboard({ projectId, nodesRef, connectionsRef, setNodes, setConnections, setSelectedNodeIds, enqueueGenerationBatch, }); const handleRetryNode = useCanvasGenerationRetry({ projectId, domainProjectId: currentProject?.projectId, activatedSkills, nodesRef, connectionsRef, setNodes, setRunningNodeId, startGenerationRequest, finishGenerationRequest, bindGenerationTask, }); const generateImageFromTextNode = useCallback( (node: CanvasNodeData) => { const prompt = (node.metadata?.content || node.metadata?.prompt || "").trim(); if (!prompt) { message.warning("文本节点为空,无法生图"); return; } const sourceNode = nodesRef.current.find((item) => item.id === node.id); if (!sourceNode) return; const nodeSize = getNodeSpec(CanvasNodeType.Image); const imageNode = createCanvasNode( CanvasNodeType.Image, { x: sourceNode.position.x + sourceNode.width + 96 + nodeSize.width / 2, y: sourceNode.position.y + sourceNode.height / 2, }, { prompt: `@[node:${sourceNode.id}]`, composerContent: `@[node:${sourceNode.id}]`, model: effectiveConfig.imageModel || effectiveConfig.model, size: effectiveConfig.size, quality: effectiveConfig.quality, transparentBackground: effectiveConfig.transparentBackground, count: getGenerationCount(effectiveConfig.canvasImageCount || effectiveConfig.count), }, ); imageNode.title = "图片生成"; const connection = { id: nanoid(), fromNodeId: sourceNode.id, toNodeId: imageNode.id }; const nextNodes = nodesRef.current.map((item) => (item.id === sourceNode.id ? { ...item, metadata: { ...item.metadata, content: prompt, richText: undefined, prompt, status: NODE_STATUS_SUCCESS } } : item)).concat(imageNode); const nextConnections = [...connectionsRef.current, connection]; nodesRef.current = nextNodes; connectionsRef.current = nextConnections; setNodes(nextNodes); setConnections(nextConnections); setSelectedNodeIds(new Set([imageNode.id])); setSelectedConnectionId(null); setDialogNodeId(imageNode.id); }, [effectiveConfig, message], ); const renderCanvasNodePanel = useCallback( (panelNode: CanvasNodeData) => { if (panelNode.type === CanvasNodeType.Script || panelNode.type === CanvasNodeType.Drawing) return null; return panelNode.type === CanvasNodeType.Config ? ( handleConfigNodeChange(panelNode.id, { composerContent })} onMetadataChange={(patch) => handleConfigNodeChange(panelNode.id, patch)} onClose={() => setDialogNodeId(null)} /> ) : ( { setNodeImageSettingsOpen(open); if (open) setToolbarNodeId(null); }} /> ); }, [configInputsById, confirmStopGeneration, handleConfigNodeChange, handleGenerateNode, handleNodePromptChange, mentionReferencesByNodeId, runningNodeId, skillMentionReferences, workspaceMode], ); const renderCanvasNodeContent = useCallback((contentNode: CanvasNodeData) => { if (contentNode.metadata?.workflowKind === "character" && contentNode.metadata.characterAssetId) { return ; } if (contentNode.metadata?.workflowKind === "styleboard" && !contentNode.metadata.content) { return setStylePickerOpen(true)} />; } if (contentNode.metadata?.workflowKind === "story_input") { return openStoryInput(contentNode.id)} />; } if (contentNode.type === CanvasNodeType.Script) { const pipeline = deriveStoryboardPipelineProgress(contentNode, nodesRef.current, connectionsRef.current); const rowIds = pipeline.rows.map((item) => item.row.id); return ( setScriptEditorNodeId(contentNode.id)} onCreateImageNodes={() => createScriptImageNodes(contentNode.id)} onCreateVideoNodes={() => createScriptVideoNodes(contentNode.id)} onGenerateImages={() => void generateScriptImages(contentNode.id, rowIds)} onGenerateVideos={() => workspaceMode === "simple" ? void createAndGenerateScriptVideos(contentNode.id) : void generateScriptVideos(contentNode.id, rowIds)} onMergeVideos={() => void mergeVideosByIds(pipeline.successfulVideoNodeIds)} onCreateActionBoards={() => void createScriptActionBoards(contentNode.id)} onRetryBatch={(batchId) => retryFailedBatchItems(contentNode.id, batchId)} onRetryBatchItem={(batchId, itemId) => retryFailedBatchItems(contentNode.id, batchId, itemId)} onStopBatch={(batchId) => stopRemainingBatchItems(contentNode.id, batchId)} onCancelBatchItem={(batchId, itemId) => cancelSubmittedBatchItem(contentNode.id, batchId, itemId)} onAddRow={() => addScriptRow(contentNode.id)} onRemoveRow={(rowId) => removeScriptRow(contentNode.id, rowId)} onUpdateRow={(rowId, patch) => updateScriptRow(contentNode.id, rowId, patch)} onPromptChange={(composerContent) => handleConfigNodeChange(contentNode.id, { composerContent })} onGenerateScript={(prompt) => void generateScriptRows(contentNode.id, prompt)} onModelChange={(model) => handleConfigNodeChange(contentNode.id, { model })} onShotDurationChange={(duration: StoryboardShotDuration) => handleConfigNodeChange(contentNode.id, { storyboardShotDuration: duration })} onShotCountChange={(count: StoryboardShotCount) => handleConfigNodeChange(contentNode.id, { storyboardShotCount: count })} workspaceMode={workspaceMode} onComposerHeightChange={(height) => { if (contentNode.metadata?.storyboardComposerHeight === height) return; handleConfigNodeChange(contentNode.id, { storyboardComposerHeight: height }); const minHeight = storyboardMinNodeHeight(height); if (contentNode.height < minHeight) handleNodeResize(contentNode.id, contentNode.width, minHeight); }} onConnectStart={(event, rowId, handleType) => handleConnectStart(event, contentNode.id, handleType, rowId === "context" ? "storyboard:context" : `row:${rowId}`)} onScrollTopChange={(scrollTop) => setScriptScrollTopById((current) => current[contentNode.id] === scrollTop ? current : { ...current, [contentNode.id]: scrollTop })} /> ); } if (contentNode.metadata?.directorSceneId) { return ( scene.id === contentNode.metadata?.directorSceneId) || null} previewUrl={nodesRef.current.find((item) => item.id === contentNode.metadata?.directorPreviewNodeId)?.metadata?.content} professional={workspaceMode === "professional"} onOpen={() => openDirectorWorkbench(contentNode.id)} /> ); } return ( setDialogNodeId((current) => (current === contentNode.id ? null : contentNode.id))} onStop={confirmStopGeneration} onGenerate={(nodeId) => { const target = nodesRef.current.find((item) => item.id === nodeId); void handleGenerateNode(nodeId, target?.metadata?.generationMode || "image", target?.metadata?.composerContent ?? target?.metadata?.prompt ?? ""); }} workspaceMode={workspaceMode} /> ); }, [addScriptRow, cancelSubmittedBatchItem, configInputsById, confirmStopGeneration, createAndGenerateScriptVideos, createScriptActionBoards, createScriptImageNodes, createScriptVideoNodes, currentProject?.directorScenes, generateScriptImages, generateScriptRows, generateScriptVideos, handleConfigNodeChange, handleConnectStart, handleGenerateNode, handleNodeResize, mentionReferencesByNodeId, mergeVideosByIds, openDirectorWorkbench, openStoryInput, removeScriptRow, retryFailedBatchItems, runningNodeId, stopRemainingBatchItems, updateScriptRow, viewport.k, workspaceMode]); const handleCanvasNodeHoverStart = useCallback((nodeId: string) => { if (nodeDraggingRef.current) return; setHoveredNodeId(nodeId); keepNodeToolbar(nodeId); }, [keepNodeToolbar]); const handleCanvasNodeHoverEnd = useCallback((nodeId: string) => { setHoveredNodeId((current) => (current === nodeId ? null : current)); hideNodeToolbar(); }, [hideNodeToolbar]); const retryCanvasNode = useCallback((node: CanvasNodeData) => { if (node.type === CanvasNodeType.Script) { const prompt = (node.metadata?.composerContent || node.metadata?.prompt || "").trim(); if (!prompt) { message.warning("分镜脚本缺少剧情内容,无法重试"); return; } void generateScriptRows(node.id, prompt); return; } void handleRetryNode(node); }, [generateScriptRows, handleRetryNode, message]); const openCanvasNodeTaskDetails = useCallback((node: CanvasNodeData) => { void openNodeTaskDetails(node); }, [openNodeTaskDetails]); const openCanvasNodeVersions = useCallback((node: CanvasNodeData) => setVersionCompareRootId(node.metadata?.versionOfNodeId || node.id), []); const viewCanvasNodeImage = useCallback((node: CanvasNodeData) => setPreviewNodeId(node.id), []); const editCanvasDirector = useCallback((node: CanvasNodeData) => openDirectorWorkbench(node.id), [openDirectorWorkbench]); const locateProjectStyleNode = useCallback(() => { const styleNode = nodesRef.current.find((node) => node.type === CanvasNodeType.Text && node.metadata?.workflowKind === "styleboard"); if (!styleNode) { message.info("项目画风节点正在同步,请稍后再试"); return; } focusCanvasNode(styleNode.id); }, [focusCanvasNode, message, nodesRef]); if (!projectLoaded) return ; return (
{currentProject?.projectId ? openProjectAssets()} /> : null}
setTitleEditing(false)} canUndo={historyState.canUndo} canRedo={historyState.canRedo} onCreateProject={createAndOpenProject} onDeleteProject={deleteCurrentProject} onImportImage={() => handleUploadRequest()} onUndo={undoCanvas} onRedo={redoCanvas} onShare={() => setShareModalOpen(true)} agentOpen={assistantOpen} compactAgentStatus={codexCompactAgent ? { connected: localAgentConnected, enabled: localAgentEnabled, activity: localAgentActivity } : undefined} onToggleAgent={() => (assistantOpen ? closeAgent() : openAgent())} shortcutRequestNonce={shortcutRequestNonce} mediaPerformanceMode={mediaPerformanceMode} onMediaPerformanceModeChange={setMediaPerformanceMode} onOpenSearch={() => setNodeSearchOpen(true)} projectContext={currentProject?.projectId ? { ...canvasContext, projectId: currentProject.projectId, projectName: linkedProjectQuery.data?.project.name || currentProject.title, } : undefined} /> setNodeSearchOpen(false)} onFocus={(nodeId) => { const target = nodeById.get(nodeId); const parent = target?.parentId ? nodeById.get(target.parentId) : null; if (parent?.metadata?.frame?.collapsed) toggleFrameCollapsed(parent.id); const batchRoot = target?.metadata?.batchRootId ? nodeById.get(target.metadata.batchRootId) : null; if (batchRoot && !batchRoot.metadata?.imageBatchExpanded) toggleBatchExpanded(batchRoot.id); const selection = new Set([nodeId]); selectedNodeIdsRef.current = selection; setSelectedNodeIds(selection); setSelectedConnectionId(null); focusCanvasNode(nodeId); }} /> {!currentProject?.projectId ? setShortDramaGuideCollapsed((value) => !value)} onSkip={skipShortDramaGuide} onStepClick={activateShortDramaStep} /> : null} setShareModalOpen(false)} beforeCreate={saveCanvasProject} /> setStylePickerOpen(false)} onSelect={selectCanvasStyle} /> )} onViewportChange={handleViewportChange} onViewportPreviewChange={handleViewportPreviewChange} onCanvasMouseDown={handleCanvasMouseDown} onCanvasDoubleClick={handleCanvasDoubleClick} onCanvasDeselect={deselectCanvas} onContextMenu={handleCanvasContextMenu} onDrop={handleDrop} onFileDragEnter={handleFileDragEnter} onFileDragLeave={handleFileDragLeave} onFileDragOver={handleFileDragOver} > { setSelectedConnectionId(connectionId); setSelectedNodeIds(new Set()); setContextMenu(null); }} onConnectionContextMenu={(event, connectionId) => { setSelectedConnectionId(connectionId); setSelectedNodeIds(new Set()); closeConnectionCreateMenu(); setContextMenu({ type: "connection", x: event.clientX, y: event.clientY, connectionId }); }} onNodeMouseDown={handleNodeMouseDown} onNodeHoverStart={handleCanvasNodeHoverStart} onNodeHoverEnd={handleCanvasNodeHoverEnd} onConnectStart={handleConnectStart} onNodeResize={handleNodeResize} onToggleFrame={toggleFrameCollapsed} onNodeTitleChange={handleNodeTitleChange} onNodeContextMenu={handleNodeContextMenu} onNodeContentChange={handleNodeContentChange} onToggleBatch={toggleBatchExpanded} onSetBatchPrimary={setBatchPrimary} onRetry={retryCanvasNode} onCancelTask={cancelNodeTask} onOpenTaskDetails={openCanvasNodeTaskDetails} onOpenVersions={openCanvasNodeVersions} onViewImage={viewCanvasNodeImage} onReplaceMedia={(node) => handleUploadRequest(node.id)} onOpenTextEditor={openTextNodeEditor} onOpenDirector={editCanvasDirector} onOpenDrawing={openDrawingNode} /> {angleNode?.metadata?.content ? ( setAngleNodeId(null)} onConfirm={(params) => { void generateAngleNode(angleNode, params); }} /> ) : null} {emotionNode?.metadata?.content ? ( setEmotionNodeId(null)} onConfirm={(payload: CanvasImageEmotionPayload) => { void generateEmotionNode(emotionNode, payload); }} /> ) : null} {dialogNode && dialogNode.type !== CanvasNodeType.Script && dialogNode.type !== CanvasNodeType.Drawing && !selectionBox ? ( {renderCanvasNodePanel(dialogNode)} ) : null} {!nodes.length ? currentProject?.projectId ? { const first = linkedProjectQuery.data?.units.slice().sort((left, right) => left.position - right.position)[0]; if (first) void handleProjectChapterInsert({ id: first.id, projectId: currentProject.projectId!, title: first.title, position: first.position }); }} onOpenAssets={() => openProjectAssets()} onAddText={() => createNode(CanvasNodeType.Text)} /> : { setCinematicAgentEntry(true); setAgentMode("online"); openAgent("online"); }} onUpload={() => handleUploadRequest()} onAddText={() => createNode(CanvasNodeType.Text)} onAddScript={() => createNode(CanvasNodeType.Script)} /> : null} {pendingConnectionCreate ? void createConnectedNode(type, pendingConnectionCreate)} onClose={cancelPendingConnectionCreate} /> : null} {selectedNodeBounds && !selectionBox && !isNodeDragging ? void mergeSelectedVideos()} /> : null} {uploadStatus ? : null} {mergeVideoProgress ? : null} {lastAgentChange ? { undoAgentOps(); }} onClose={dismissLastAgentChange} /> : null} node.metadata?.workflowKind === "character" && node.metadata.characterAssetId ? openTextNodeEditor(node) : setInfoNodeId(node.id)} onEditText={openTextNodeEditor} onDecreaseFont={(node) => handleFontSizeChange(node.id, Math.max(10, (node.metadata?.fontSize || 14) - 2))} onIncreaseFont={(node) => handleFontSizeChange(node.id, Math.min(32, (node.metadata?.fontSize || 14) + 2))} onToggleDialog={(node) => setDialogNodeId((current) => (current === node.id ? null : node.id))} onGenerateImage={generateImageFromTextNode} onUpload={(node) => handleUploadRequest(node.id)} onDownload={downloadNodeImage} onSaveAsset={(node) => void saveNodeAsset(node)} onAnnotate={(node) => setAnnotationNodeId(node.id)} onMaskEdit={(node) => setMaskEditNodeId(node.id)} onEmotion={(node) => { setDialogNodeId(null); setEmotionNodeId((current) => current === node.id ? null : node.id); }} onPortraitTexture={createPortraitTextureNode} onCrop={(node) => setCropNodeId(node.id)} onSplit={(node) => setSplitNodeId(node.id)} onUpscale={(node) => setUpscaleNodeId(node.id)} onSuperResolve={(node) => setSuperResolveNodeId(node.id)} onAngle={(node) => { setDialogNodeId(null); setAngleNodeId((current) => current === node.id ? null : node.id); }} onViewImage={(node) => setPreviewNodeId(node.id)} onExtractVideoLastFrame={(node) => void extractVideoLastFrame(node)} extractingVideoFrame={toolbarNode?.id === extractingVideoFrameNodeId} onReversePrompt={createImageReversePromptNodes} onRetry={(node) => void handleRetryNode(node)} onToggleFreeResize={(node) => toggleNodeFreeResize(node.id)} onToggleLocked={(node) => toggleNodeLocked(node.id)} onDelete={(node) => deleteNodes(new Set([node.id]))} /> createNode(CanvasNodeType.Image)} onAddVideo={() => createNode(CanvasNodeType.Video)} onAddAudio={() => createNode(CanvasNodeType.Audio)} onAddText={() => createNode(CanvasNodeType.Text)} onChooseStyle={() => setStylePickerOpen(true)} onAddScript={() => createNode(CanvasNodeType.Script)} onAddFrame={() => createNode(CanvasNodeType.Frame)} onAddDrawing={() => createNode(CanvasNodeType.Drawing)} onOpenDirector={() => createDirectorShot()} onUndo={undoCanvas} onRedo={redoCanvas} onUpload={() => handleUploadRequest()} onDelete={() => deleteNodes(new Set(selectedNodeIds))} onClear={() => setClearConfirmOpen(true)} onDeselect={deselectCanvas} onBackgroundModeChange={setBackgroundMode} onShowImageInfoChange={setShowImageInfo} onOpenMyAssets={() => { openAssetsAtPosition(); }} onOpenProjectCharacters={() => openProjectAssets("character")} /> {isMiniMapOpen ? : null}
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}> setIsMiniMapOpen((value) => !value)} onOpenShortcuts={() => setShortcutRequestNonce((value) => value + 1)} /> void createImageAssetNode(asset)} onFocusCanvasImage={focusCanvasImageNode} />
setContextMenu(null)} onAddNode={(type, position) => createNode(type, position)} onOpenDirector={createDirectorShot} onUpload={(nodeId, position) => handleUploadRequest(nodeId, position)} onOpenAssets={openAssetsAtPosition} onOpenProjectCharacters={(position) => openProjectAssets("character", position)} onUndo={undoCanvas} onRedo={redoCanvas} onPaste={pasteAtPosition} onCopyNode={(nodeId) => copyNodesToClipboard(new Set([nodeId]))} onDuplicate={duplicateNode} onDeleteNode={(nodeId) => deleteNodes(new Set([nodeId]))} onDeleteConnection={deleteConnection} onSaveAsset={(node) => { void saveNodeAsset(node); }} onViewMedia={(node) => setPreviewNodeId(node.id)} onEditText={openTextNodeEditor} onOpenDrawing={openDrawingNode} onGenerateImage={generateImageFromTextNode} onCopyContent={(node) => { void copyNodeContentToClipboard(node); }} onCopyMediaUrl={(node) => { void copyNodeMediaUrlToClipboard(node); }} onSetAssetCategory={(nodeId, assetCategory) => handleConfigNodeChange(nodeId, { assetCategory })} onToggleFrame={(node) => toggleFrameCollapsed(node.id)} /> setInfoNodeId(null)} onMetadataChange={handleConfigNodeChange} /> setCharacterReferenceNodeId(null)} /> setTextEditorNodeId(null)} onSave={(nodeId, title, content, richText) => { setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, title, metadata: { ...node.metadata, content, richText } } : node)); }} /> {drawingNode ? }> setDrawingNodeId(null)} onSaved={(nodeId, summary) => { setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, drawingRevision: summary.revision, drawingUpdatedAt: summary.updatedAt, drawingShapeCount: summary.shapeCount, drawingPageCount: summary.pageCount } } : node)); message.success("绘图已保存"); }} /> : null} setScriptEditorNodeId(null)} onUpdateRows={(rows) => activeScriptNode && replaceScriptRows(activeScriptNode.id, rows)} onVisibleColumnsChange={(visibleColumns: StoryboardColumn[]) => { if (!activeScriptNode || !visibleColumns.length) return; setNodes((prev) => prev.map((node) => node.id === activeScriptNode.id ? { ...node, metadata: { ...node.metadata, storyboard: { rows: node.metadata?.storyboard?.rows || [], visibleColumns, referenceNodeIds: node.metadata?.storyboard?.referenceNodeIds || [] } } } : node)); }} onGenerateImages={(rowIds) => activeScriptNode && void generateScriptImages(activeScriptNode.id, rowIds)} onGenerateVideos={(rowIds) => activeScriptNode && void generateScriptVideos(activeScriptNode.id, rowIds)} /> {directorNodeId && activeDirectorScene ? ( }> node.type === CanvasNodeType.Image && Boolean(node.metadata?.content))} onClose={() => setDirectorNodeId(null)} onChange={saveDirectorScene} onApply={applyDirectorOutput} /> ) : null} setVersionCompareRootId(null)} onSetPrimary={setPrimaryVersion} onFocus={(nodeId) => { setVersionCompareRootId(null); focusCanvasNode(nodeId); }} /> setCropNodeId(null)} onCloseAnnotation={() => setAnnotationNodeId(null)} onCloseMaskEdit={() => setMaskEditNodeId(null)} onCloseSplit={() => setSplitNodeId(null)} onCloseUpscale={() => setUpscaleNodeId(null)} onCrop={(node, crop) => void cropImageNode(node, crop)} onAnnotate={(node, dataUrl) => void saveAnnotatedImageNode(node, dataUrl)} onMaskEdit={(node, payload) => void maskEditImageNode(node, payload)} onSplit={(node, params) => void splitImageNode(node, params)} onUpscale={(node, params) => void upscaleImageNode(node, params)} /> setTaskDetail(null)} superResolveNode={superResolveNode} onCloseSuperResolve={() => setSuperResolveNodeId(null)} previewNode={previewNode} onClosePreview={() => setPreviewNodeId(null)} clearConfirmOpen={clearConfirmOpen} onCancelClear={() => setClearConfirmOpen(false)} onConfirmClear={clearCanvas} /> handleProjectAssetsInsert(payloads, projectAssetInsertPosition)} /> {codexCompactAgent && !assistantMounted ? : null}
{assistantMounted ? ( setCinematicAgentEntry(false)} /> ) : null}
); }