feat: 完成短剧项目空间与结构化资产创作闭环 (#38)
* feat(canvas): 短剧项目空间 - 建立项目章节与画布关联 MVP * feat(canvas): 短剧项目空间 - 完善项目资产、镜头与流程闭环 Refs #30 * fix(deploy): 本地部署 - 修复前端构建并忽略本地 PostgreSQL 配置 * feat(*): 短剧项目空间 - 完成项目、章节、资产、角色与画布创作闭环 Refs #30 --------- Co-authored-by: ddCat <ddcat666@88.com>
This commit is contained in:
1 parent
431b9e3294
commit
d2fbc413b3
208 files changed
+22655
-4234
No files matched your search
@@ -1,13 +1,16 @@
|
||||
import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
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 { getResourceOSSUrl } from "@/services/api/resources";
|
||||
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";
|
||||
@@ -17,9 +20,16 @@ import { CanvasConfigNodePanel } from "@/components/canvas/canvas-config-node-pa
|
||||
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 { CanvasDocumentEditorModal } from "@/components/canvas/canvas-document-editor-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";
|
||||
@@ -27,6 +37,7 @@ 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";
|
||||
@@ -36,7 +47,7 @@ import { CanvasLocalAgentPanel } from "@/components/canvas/canvas-local-agent-pa
|
||||
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { CanvasAlignmentGuides, CanvasConnectionCreateMenu, CanvasNodePanelOverlay } from "@/components/canvas/canvas-workspace-overlays";
|
||||
import { CanvasShortDramaEmptyState, CanvasShortDramaGuide, CanvasStoryInputNodeContent, CanvasStylePlaceholderNodeContent } from "@/components/canvas/canvas-short-drama-entry";
|
||||
import { CanvasLinkedProjectEmptyState, CanvasShortDramaEmptyState, CanvasShortDramaGuide, CanvasStoryInputNodeContent, CanvasStylePlaceholderNodeContent } from "@/components/canvas/canvas-short-drama-entry";
|
||||
import {
|
||||
createCanvasNode,
|
||||
getInputSummary,
|
||||
@@ -58,11 +69,14 @@ 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 { useCanvasDocumentWorkflow } from "./use-canvas-document-workflow";
|
||||
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";
|
||||
@@ -98,6 +112,7 @@ import {
|
||||
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[] = [];
|
||||
@@ -172,7 +187,6 @@ function InfiniteCanvasPage() {
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
|
||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
||||
const assets = useAssetStore((state) => state.assets);
|
||||
const cleanupAssetImages = useAssetStore((state) => state.cleanupImages);
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
@@ -198,10 +212,13 @@ function InfiniteCanvasPage() {
|
||||
const [toolbarNodeId, setToolbarNodeId] = useState<string | null>(null);
|
||||
const [nodeImageSettingsOpen, setNodeImageSettingsOpen] = useState(false);
|
||||
const [dialogNodeId, setDialogNodeId] = useState<string | null>(null);
|
||||
const [documentEditorNodeId, setDocumentEditorNodeId] = useState<string | null>(null);
|
||||
const [textEditorNodeId, setTextEditorNodeId] = useState<string | null>(null);
|
||||
const [characterReferenceNodeId, setCharacterReferenceNodeId] = useState<string | null>(null);
|
||||
const [drawingNodeId, setDrawingNodeId] = useState<string | null>(null);
|
||||
const [stylePickerOpen, setStylePickerOpen] = useState(false);
|
||||
const [editingNodeId, setEditingNodeId] = useState<string | null>(null);
|
||||
const [editRequestNonce, setEditRequestNonce] = useState(0);
|
||||
const [projectAssetOpen, setProjectAssetOpen] = useState(false);
|
||||
const [projectAssetInitialCategory, setProjectAssetInitialCategory] = useState("all");
|
||||
const [projectAssetInsertPosition, setProjectAssetInsertPosition] = useState<Position | undefined>();
|
||||
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
|
||||
const [superResolveNodeId, setSuperResolveNodeId] = useState<string | null>(null);
|
||||
const [previewNodeId, setPreviewNodeId] = useState<string | null>(null);
|
||||
@@ -309,6 +326,16 @@ function InfiniteCanvasPage() {
|
||||
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,
|
||||
@@ -323,7 +350,7 @@ function InfiniteCanvasPage() {
|
||||
taskDetail,
|
||||
taskDetailLoading,
|
||||
taskDetailLogs,
|
||||
} = useCanvasGeneration({ projectId, projectLoaded, nodes, nodesRef, setNodes });
|
||||
} = useCanvasGeneration({ projectId, domainProjectId: linkedProjectId, projectLoaded, nodes, nodesRef, setNodes });
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectLoaded || !["new", "recent", "choose"].includes(searchParams.get("mode") || "")) return;
|
||||
@@ -396,9 +423,37 @@ function InfiniteCanvasPage() {
|
||||
setSelectedConnectionId,
|
||||
setContextMenu,
|
||||
setDialogNodeId,
|
||||
setDrawingNodeId,
|
||||
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,
|
||||
@@ -410,6 +465,8 @@ function InfiniteCanvasPage() {
|
||||
handleFileDragLeave,
|
||||
handleFileDragOver,
|
||||
handleImageInputChange,
|
||||
handleProjectAssetsInsert,
|
||||
handleProjectChapterInsert,
|
||||
handleUploadRequest,
|
||||
imageInputRef,
|
||||
openAssetsAtPosition,
|
||||
@@ -418,6 +475,8 @@ function InfiniteCanvasPage() {
|
||||
startUploadStatus,
|
||||
uploadStatus,
|
||||
} = useCanvasUpload({
|
||||
canvasId: projectId,
|
||||
domainProjectId: linkedProjectId,
|
||||
nodesRef,
|
||||
selectedNodeIdsRef,
|
||||
getCanvasCenter,
|
||||
@@ -429,8 +488,19 @@ function InfiniteCanvasPage() {
|
||||
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,
|
||||
cropImageNode,
|
||||
@@ -438,6 +508,7 @@ function InfiniteCanvasPage() {
|
||||
extractVideoLastFrame,
|
||||
extractingVideoFrameNodeId,
|
||||
generateAngleNode,
|
||||
generateEmotionNode,
|
||||
maskEditImageNode,
|
||||
maskEditNodeId,
|
||||
mergeSelectedVideos,
|
||||
@@ -445,6 +516,7 @@ function InfiniteCanvasPage() {
|
||||
mergeVideoProgress,
|
||||
saveAnnotatedImageNode,
|
||||
setAngleNodeId,
|
||||
setEmotionNodeId,
|
||||
setAnnotationNodeId,
|
||||
setCropNodeId,
|
||||
setMaskEditNodeId,
|
||||
@@ -474,12 +546,14 @@ function InfiniteCanvasPage() {
|
||||
bindGenerationTask,
|
||||
});
|
||||
|
||||
const handleNodesDeleted = useCallback((removedIds: Set<string>, nextNodes: CanvasNodeData[]) => {
|
||||
const handleNodesDeleted = useCallback((removedIds: Set<string>, nextNodes: CanvasNodeData[], removedNodes: CanvasNodeData[]) => {
|
||||
const clearDeletedId = (current: string | null) => current && removedIds.has(current) ? null : current;
|
||||
setHoveredNodeId(clearDeletedId);
|
||||
setToolbarNodeId(clearDeletedId);
|
||||
setDialogNodeId(clearDeletedId);
|
||||
setEditingNodeId(clearDeletedId);
|
||||
setTextEditorNodeId(clearDeletedId);
|
||||
setCharacterReferenceNodeId(clearDeletedId);
|
||||
setDrawingNodeId(clearDeletedId);
|
||||
setInfoNodeId(clearDeletedId);
|
||||
setCropNodeId(clearDeletedId);
|
||||
setMaskEditNodeId(clearDeletedId);
|
||||
@@ -487,17 +561,22 @@ function InfiniteCanvasPage() {
|
||||
setSplitNodeId(clearDeletedId);
|
||||
setUpscaleNodeId(clearDeletedId);
|
||||
setAngleNodeId(clearDeletedId);
|
||||
setEmotionNodeId(clearDeletedId);
|
||||
setSuperResolveNodeId(clearDeletedId);
|
||||
setPreviewNodeId(clearDeletedId);
|
||||
setRunningNodeId(clearDeletedId);
|
||||
setScriptEditorNodeId(clearDeletedId);
|
||||
setDocumentEditorNodeId(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, projectId, setAngleNodeId, setAnnotationNodeId, setCropNodeId, setMaskEditNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId]);
|
||||
}, [chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setCropNodeId, setEmotionNodeId, setMaskEditNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId]);
|
||||
|
||||
const {
|
||||
alignSelectedNodes,
|
||||
@@ -515,6 +594,7 @@ function InfiniteCanvasPage() {
|
||||
setPrimaryVersion,
|
||||
toggleNodeLocked,
|
||||
} = useCanvasNodeOperations({
|
||||
projectId,
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
selectedNodeIdsRef,
|
||||
@@ -539,6 +619,7 @@ function InfiniteCanvasPage() {
|
||||
pendingConnectionCreate,
|
||||
setConnecting,
|
||||
} = useCanvasConnectionController({
|
||||
projectId,
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
viewportRef,
|
||||
@@ -550,6 +631,7 @@ function InfiniteCanvasPage() {
|
||||
setSelectedConnectionId,
|
||||
setContextMenu,
|
||||
setDialogNodeId,
|
||||
setDrawingNodeId,
|
||||
});
|
||||
|
||||
const handleCanvasSelectionStart = useCallback(() => {
|
||||
@@ -565,9 +647,9 @@ function InfiniteCanvasPage() {
|
||||
}, []);
|
||||
|
||||
const handleSelectedNodeClick = useCallback((node: CanvasNodeData) => {
|
||||
if (node.metadata?.document?.kind === "novel") {
|
||||
if (node.type === CanvasNodeType.Drawing) {
|
||||
setDialogNodeId(null);
|
||||
setDocumentEditorNodeId(node.id);
|
||||
setDrawingNodeId(node.id);
|
||||
} else if (node.type === CanvasNodeType.Script) {
|
||||
setDialogNodeId(null);
|
||||
} else if (node.type === CanvasNodeType.Text || node.type === CanvasNodeType.Frame) {
|
||||
@@ -582,7 +664,6 @@ function InfiniteCanvasPage() {
|
||||
setHoveredNodeId(null);
|
||||
setToolbarNodeId(null);
|
||||
setDialogNodeId(null);
|
||||
setEditingNodeId(null);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
@@ -643,7 +724,6 @@ function InfiniteCanvasPage() {
|
||||
handleNodePromptChange,
|
||||
handleNodeResize,
|
||||
handleNodeTitleChange,
|
||||
openTextEditor,
|
||||
openingBatchIds,
|
||||
saveNodeAsset,
|
||||
setBatchPrimary,
|
||||
@@ -651,25 +731,24 @@ function InfiniteCanvasPage() {
|
||||
toggleFrameCollapsed,
|
||||
toggleNodeFreeResize,
|
||||
} = useCanvasNodeEditor({
|
||||
canvasId: projectId,
|
||||
domainProjectId: linkedProjectId,
|
||||
nodesRef,
|
||||
setNodes,
|
||||
setSelectedNodeIds,
|
||||
setSelectedConnectionId,
|
||||
setDialogNodeId,
|
||||
setDocumentEditorNodeId,
|
||||
setEditingNodeId,
|
||||
setEditRequestNonce,
|
||||
setToolbarNodeId,
|
||||
setHoveredNodeId,
|
||||
});
|
||||
|
||||
const {
|
||||
activeDirectorScene,
|
||||
activeDocumentNode,
|
||||
activeNodeId,
|
||||
activeScriptNode,
|
||||
activeStylePresetId,
|
||||
angleNode,
|
||||
emotionNode,
|
||||
annotationNode,
|
||||
batchChildCountById,
|
||||
batchMotionById,
|
||||
@@ -720,17 +799,47 @@ function InfiniteCanvasPage() {
|
||||
upscaleNodeId,
|
||||
superResolveNodeId,
|
||||
angleNodeId,
|
||||
emotionNodeId,
|
||||
previewNodeId,
|
||||
contextMenu,
|
||||
versionCompareRootId,
|
||||
directorNodeId,
|
||||
documentEditorNodeId,
|
||||
scriptEditorNodeId,
|
||||
dialogNodeId,
|
||||
});
|
||||
const dialogNode = dialogNodeId ? nodeById.get(dialogNodeId) || null : null;
|
||||
const { agentSnapshot, applyAgentOps, canUndoAgentOps, dismissLastAgentChange, lastAgentChange, undoAgentOps, viewLastAgentChange } = useCanvasAgentOperations({
|
||||
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,
|
||||
@@ -750,32 +859,15 @@ function InfiniteCanvasPage() {
|
||||
focusSelection: fitCanvasSelection,
|
||||
});
|
||||
|
||||
const {
|
||||
analyzeDocumentCharacters,
|
||||
analyzeDocumentNode,
|
||||
createNovelNode,
|
||||
documentAnalyzing,
|
||||
documentCharacterAnalyzing,
|
||||
documentSaving,
|
||||
saveDocumentNode,
|
||||
selectCanvasStyle,
|
||||
} = useCanvasDocumentWorkflow({
|
||||
projectId,
|
||||
agentSnapshot,
|
||||
applyAgentOps,
|
||||
const { selectCanvasStyle } = useCanvasStyleWorkflow({
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
selectedNodeIdsRef,
|
||||
getCanvasCenter,
|
||||
viewportSize: size,
|
||||
setNodes,
|
||||
setConnections,
|
||||
setSelectedNodeIds,
|
||||
setSelectedConnectionId,
|
||||
setDialogNodeId,
|
||||
setDocumentEditorNodeId,
|
||||
setStylePickerOpen,
|
||||
setViewport: handleViewportChange,
|
||||
});
|
||||
|
||||
const { applyDirectorOutput, createDirectorShot, openDirectorWorkbench, saveDirectorScene } = useCanvasDirector({
|
||||
@@ -800,7 +892,6 @@ function InfiniteCanvasPage() {
|
||||
openStoryInput,
|
||||
progress: shortDramaProgress,
|
||||
setGuideCollapsed: setShortDramaGuideCollapsed,
|
||||
setStoryInputMode,
|
||||
skipGuide: skipShortDramaGuide,
|
||||
} = useCanvasShortDrama({
|
||||
nodes,
|
||||
@@ -814,26 +905,33 @@ function InfiniteCanvasPage() {
|
||||
setSelectedNodeIds,
|
||||
setSelectedConnectionId,
|
||||
setStylePickerOpen,
|
||||
setDocumentEditorNodeId,
|
||||
fitCanvasSelection,
|
||||
focusCanvasNode,
|
||||
openTextEditor,
|
||||
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]);
|
||||
}, [clearCanvasFiles, deselectCanvas, message, nodesRef, projectId, setEmotionNodeId]);
|
||||
|
||||
useCanvasKeyboard({
|
||||
nodesRef,
|
||||
@@ -916,15 +1014,20 @@ function InfiniteCanvasPage() {
|
||||
[message],
|
||||
);
|
||||
|
||||
const copyNodeOssUrlToClipboard = useCallback(
|
||||
const copyNodeMediaUrlToClipboard = useCallback(
|
||||
async (node: CanvasNodeData | null) => {
|
||||
try {
|
||||
const ossURL = await getResourceOSSUrl(node?.metadata?.storageKey);
|
||||
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(ossURL);
|
||||
else if (!copyToClipboard(ossURL)) throw new Error("当前浏览器不支持写入剪贴板");
|
||||
message.success("临时 OSS 地址已复制");
|
||||
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 (!copyToClipboard(mediaURL)) throw new Error("当前浏览器不支持写入剪贴板");
|
||||
message.success(node?.type === CanvasNodeType.Video ? "视频地址已复制" : "图片地址已复制");
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "复制 OSS 地址失败");
|
||||
message.error(error instanceof Error ? error.message : "媒体地址复制失败");
|
||||
}
|
||||
},
|
||||
[message],
|
||||
@@ -955,11 +1058,13 @@ function InfiniteCanvasPage() {
|
||||
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,
|
||||
@@ -993,7 +1098,6 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const {
|
||||
addScriptRow,
|
||||
analyzeDocumentChapter,
|
||||
createAndGenerateScriptVideos,
|
||||
createScriptActionBoards,
|
||||
createScriptImageNodes,
|
||||
@@ -1016,6 +1120,7 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const handleRetryNode = useCanvasGenerationRetry({
|
||||
projectId,
|
||||
domainProjectId: currentProject?.projectId,
|
||||
activatedSkills,
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
@@ -1052,7 +1157,7 @@ function InfiniteCanvasPage() {
|
||||
},
|
||||
);
|
||||
const connection = { id: nanoid(), fromNodeId: sourceNode.id, toNodeId: configNode.id };
|
||||
const nextNodes = nodesRef.current.map((item) => (item.id === sourceNode.id ? { ...item, metadata: { ...item.metadata, content: prompt, prompt, status: NODE_STATUS_SUCCESS } } : item)).concat(configNode);
|
||||
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(configNode);
|
||||
const nextConnections = [...connectionsRef.current, connection];
|
||||
nodesRef.current = nextNodes;
|
||||
connectionsRef.current = nextConnections;
|
||||
@@ -1067,7 +1172,7 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const renderCanvasNodePanel = useCallback(
|
||||
(panelNode: CanvasNodeData) => {
|
||||
if (panelNode.type === CanvasNodeType.Script) return null;
|
||||
if (panelNode.type === CanvasNodeType.Script || panelNode.type === CanvasNodeType.Drawing) return null;
|
||||
return panelNode.type === CanvasNodeType.Config ? (
|
||||
<CanvasConfigComposer
|
||||
value={panelNode.metadata?.composerContent ?? panelNode.metadata?.prompt ?? ""}
|
||||
@@ -1101,11 +1206,14 @@ function InfiniteCanvasPage() {
|
||||
);
|
||||
|
||||
const renderCanvasNodeContent = useCallback((contentNode: CanvasNodeData) => {
|
||||
if (contentNode.metadata?.workflowKind === "character" && contentNode.metadata.characterAssetId) {
|
||||
return <CanvasCharacterReferenceNodeContent node={contentNode} />;
|
||||
}
|
||||
if (contentNode.metadata?.workflowKind === "styleboard" && !contentNode.metadata.content) {
|
||||
return <CanvasStylePlaceholderNodeContent onChoose={() => setStylePickerOpen(true)} />;
|
||||
}
|
||||
if (contentNode.metadata?.workflowKind === "story_input") {
|
||||
return <CanvasStoryInputNodeContent node={contentNode} onModeChange={(mode) => setStoryInputMode(contentNode.id, mode)} onEdit={() => openStoryInput(contentNode.id)} />;
|
||||
return <CanvasStoryInputNodeContent node={contentNode} onEdit={() => openStoryInput(contentNode.id)} />;
|
||||
}
|
||||
if (contentNode.type === CanvasNodeType.Script) {
|
||||
const pipeline = deriveStoryboardPipelineProgress(contentNode, nodesRef.current, connectionsRef.current);
|
||||
@@ -1173,7 +1281,7 @@ function InfiniteCanvasPage() {
|
||||
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, setStoryInputMode, stopRemainingBatchItems, updateScriptRow, viewport.k, 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;
|
||||
@@ -1189,10 +1297,19 @@ function InfiniteCanvasPage() {
|
||||
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 <CanvasRefreshShell />;
|
||||
|
||||
return (
|
||||
<main className="flex h-full min-h-0 overflow-hidden" style={{ background: theme.canvas.background, color: theme.node.text }}>
|
||||
{currentProject?.projectId ? <CanvasProjectSidebar projectId={currentProject.projectId} detail={linkedProjectQuery.data} onAddChapter={handleProjectChapterInsert} onLocateStyle={locateProjectStyleNode} onOpenAssets={() => openProjectAssets()} /> : null}
|
||||
<section className="relative min-w-0 flex-1 overflow-hidden">
|
||||
<CanvasTopBar
|
||||
title={currentProject?.title || "未命名画布"}
|
||||
@@ -1219,6 +1336,11 @@ function InfiniteCanvasPage() {
|
||||
mediaPerformanceMode={mediaPerformanceMode}
|
||||
onMediaPerformanceModeChange={setMediaPerformanceMode}
|
||||
onOpenSearch={() => setNodeSearchOpen(true)}
|
||||
projectContext={currentProject?.projectId ? {
|
||||
...canvasContext,
|
||||
projectId: currentProject.projectId,
|
||||
projectName: linkedProjectQuery.data?.project.name || currentProject.title,
|
||||
} : undefined}
|
||||
/>
|
||||
|
||||
<CanvasNodeSearchModal
|
||||
@@ -1241,23 +1363,10 @@ function InfiniteCanvasPage() {
|
||||
|
||||
<CanvasActiveTaskPanel tasks={activeTasks} />
|
||||
|
||||
<CanvasShortDramaGuide progress={shortDramaProgress} collapsed={shortDramaGuideCollapsed} onToggle={() => setShortDramaGuideCollapsed((value) => !value)} onSkip={skipShortDramaGuide} onStepClick={activateShortDramaStep} />
|
||||
{!currentProject?.projectId ? <CanvasShortDramaGuide progress={shortDramaProgress} collapsed={shortDramaGuideCollapsed} onToggle={() => setShortDramaGuideCollapsed((value) => !value)} onSkip={skipShortDramaGuide} onStepClick={activateShortDramaStep} /> : null}
|
||||
|
||||
<CanvasShareModal projectId={projectId} open={shareModalOpen} onClose={() => setShareModalOpen(false)} beforeCreate={saveCanvasProject} />
|
||||
|
||||
<CanvasDocumentEditorModal
|
||||
node={activeDocumentNode}
|
||||
open={Boolean(activeDocumentNode)}
|
||||
saving={documentSaving}
|
||||
analyzing={documentAnalyzing}
|
||||
characterAnalyzing={documentCharacterAnalyzing}
|
||||
onClose={() => setDocumentEditorNodeId(null)}
|
||||
onSave={(document, title) => { if (activeDocumentNode) void saveDocumentNode(activeDocumentNode, document, title); }}
|
||||
onAnalyze={(document, title) => activeDocumentNode ? analyzeDocumentNode(activeDocumentNode, document, title) : undefined}
|
||||
onAnalyzeCharacters={(document, title, chapter) => activeDocumentNode ? analyzeDocumentCharacters(activeDocumentNode, document, title, chapter) : undefined}
|
||||
onAnalyzeChapter={async (document, chapter, title) => activeDocumentNode ? analyzeDocumentChapter(activeDocumentNode, document, chapter, title) : undefined}
|
||||
/>
|
||||
|
||||
<CanvasStylePickerModal open={stylePickerOpen} value={activeStylePresetId} onClose={() => setStylePickerOpen(false)} onSelect={selectCanvasStyle} />
|
||||
|
||||
<InfiniteCanvas
|
||||
@@ -1276,6 +1385,7 @@ function InfiniteCanvasPage() {
|
||||
onFileDragOver={handleFileDragOver}
|
||||
>
|
||||
<CanvasProjectWorldLayers
|
||||
projectId={projectId}
|
||||
theme={theme}
|
||||
viewportScale={viewport.k}
|
||||
connectionLayerBounds={connectionLayerBounds}
|
||||
@@ -1294,8 +1404,6 @@ function InfiniteCanvasPage() {
|
||||
frameDropTargetId={frameDropTargetId}
|
||||
relatedNodeIds={relatedHighlight.nodeIds}
|
||||
activeNodeId={activeNodeId}
|
||||
editingNodeId={editingNodeId}
|
||||
editRequestNonce={editRequestNonce}
|
||||
selectionBox={selectionBox}
|
||||
batchChildCountById={batchChildCountById}
|
||||
collapsingBatchIds={collapsingBatchIds}
|
||||
@@ -1305,7 +1413,7 @@ function InfiniteCanvasPage() {
|
||||
reduceMediaEffects={reduceMediaEffects}
|
||||
resourceReferenceByNodeId={resourceReferenceByNodeId}
|
||||
mentionReferencesByNodeId={mentionReferencesByNodeId}
|
||||
angleNode={angleNode}
|
||||
mediaEffectsDisabledNodeId={emotionNodeId}
|
||||
selectedNodeBounds={selectedNodeBounds}
|
||||
isNodeDragging={isNodeDragging}
|
||||
selectionBoundsElementRef={selectionBoundsElementRef}
|
||||
@@ -1328,17 +1436,31 @@ function InfiniteCanvasPage() {
|
||||
onCancelTask={cancelNodeTask}
|
||||
onOpenTaskDetails={openCanvasNodeTaskDetails}
|
||||
onOpenVersions={openCanvasNodeVersions}
|
||||
onGenerateImage={generateImageFromTextNode}
|
||||
onViewImage={viewCanvasNodeImage}
|
||||
onReplaceMedia={(node) => handleUploadRequest(node.id)}
|
||||
onOpenTextEditor={openTextNodeEditor}
|
||||
onOpenDirector={editCanvasDirector}
|
||||
onOpenDocument={openTextEditor}
|
||||
onCloseAngle={() => setAngleNodeId(null)}
|
||||
onGenerateAngle={(params) => { if (angleNode) void generateAngleNode(angleNode, params); }}
|
||||
onOpenDrawing={openDrawingNode}
|
||||
/>
|
||||
</InfiniteCanvas>
|
||||
|
||||
{dialogNode && dialogNode.type !== CanvasNodeType.Script && !selectionBox ? (
|
||||
{angleNode?.metadata?.content ? (
|
||||
<CanvasNodePanelOverlay node={angleNode} viewport={viewport} containerRef={containerRef} panelWidth={580} panelHeight={350}>
|
||||
<CanvasNodeAnglePanel dataUrl={angleNode.metadata.content} onClose={() => setAngleNodeId(null)} onConfirm={(params) => { void generateAngleNode(angleNode, params); }} />
|
||||
</CanvasNodePanelOverlay>
|
||||
) : null}
|
||||
|
||||
{emotionNode?.metadata?.content ? (
|
||||
<CanvasEmotionWorkspace
|
||||
node={emotionNode}
|
||||
viewport={viewport}
|
||||
containerRef={containerRef}
|
||||
onClose={() => setEmotionNodeId(null)}
|
||||
onConfirm={(payload: CanvasImageEmotionPayload) => { void generateEmotionNode(emotionNode, payload); }}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{dialogNode && dialogNode.type !== CanvasNodeType.Script && dialogNode.type !== CanvasNodeType.Drawing && !selectionBox ? (
|
||||
<CanvasNodePanelOverlay node={dialogNode} viewport={viewport} containerRef={containerRef}>
|
||||
{renderCanvasNodePanel(dialogNode)}
|
||||
</CanvasNodePanelOverlay>
|
||||
@@ -1346,9 +1468,9 @@ function InfiniteCanvasPage() {
|
||||
|
||||
<CanvasFileDropOverlay active={fileDropActive} theme={theme} />
|
||||
|
||||
{!nodes.length ? <CanvasShortDramaEmptyState onCreatePipeline={createShortDramaPipeline} onOpenAgent={() => { setCinematicAgentEntry(true); setAgentMode("online"); openAgent("online"); }} onUpload={() => handleUploadRequest()} onAddText={() => createNode(CanvasNodeType.Text)} onAddNovel={() => createNovelNode()} onAddScript={() => createNode(CanvasNodeType.Script)} /> : null}
|
||||
{!nodes.length ? currentProject?.projectId ? <CanvasLinkedProjectEmptyState projectName={linkedProjectQuery.data?.project.name || currentProject.title} hasChapter={Boolean(linkedProjectQuery.data?.units.length)} onAddFirstChapter={() => { 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)} /> : <CanvasShortDramaEmptyState onCreatePipeline={createShortDramaPipeline} onOpenAgent={() => { setCinematicAgentEntry(true); setAgentMode("online"); openAgent("online"); }} onUpload={() => handleUploadRequest()} onAddText={() => createNode(CanvasNodeType.Text)} onAddScript={() => createNode(CanvasNodeType.Script)} /> : null}
|
||||
|
||||
{pendingConnectionCreate ? <CanvasConnectionCreateMenu pending={pendingConnectionCreate} viewport={viewport} viewportSize={size} containerRef={containerRef} onCreate={(type) => createConnectedNode(type, pendingConnectionCreate)} onClose={cancelPendingConnectionCreate} /> : null}
|
||||
{pendingConnectionCreate ? <CanvasConnectionCreateMenu pending={pendingConnectionCreate} viewport={viewport} viewportSize={size} containerRef={containerRef} canCreateDrawing={canCreateDrawingFromConnection} onCreate={(type) => void createConnectedNode(type, pendingConnectionCreate)} onClose={cancelPendingConnectionCreate} /> : null}
|
||||
|
||||
{selectedNodeBounds && !selectionBox && !isNodeDragging ? <CanvasProjectSelectionToolbar anchorRef={selectionBoundsElementRef} containerRef={containerRef} count={selectedNodeBounds.count} selectedVideoCount={selectedVideoNodes.length} mergingVideos={Boolean(mergeVideoProgress)} onAlign={alignSelectedNodes} onArrange={arrangeSelectedNodes} onCreateStoryboard={createStoryboardGroup} onCreateReferenceGroup={createReferenceGroup} onMergeVideos={() => void mergeSelectedVideos()} /> : null}
|
||||
|
||||
@@ -1359,14 +1481,14 @@ function InfiniteCanvasPage() {
|
||||
{lastAgentChange ? <CanvasAgentChangeToast change={lastAgentChange} theme={theme} onView={viewLastAgentChange} onUndo={() => { undoAgentOps(); }} onClose={dismissLastAgentChange} /> : null}
|
||||
|
||||
<CanvasNodeHoverToolbar
|
||||
node={isNodeDragging || nodeImageSettingsOpen ? null : toolbarNode}
|
||||
node={isNodeDragging || nodeImageSettingsOpen || emotionNodeId ? null : toolbarNode}
|
||||
workspaceMode={workspaceMode}
|
||||
viewport={viewport}
|
||||
containerRef={containerRef}
|
||||
onKeep={keepNodeToolbar}
|
||||
onLeave={hideNodeToolbar}
|
||||
onInfo={(node) => setInfoNodeId(node.id)}
|
||||
onEditText={openTextEditor}
|
||||
onInfo={(node) => 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))}
|
||||
@@ -1376,6 +1498,7 @@ function InfiniteCanvasPage() {
|
||||
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); }}
|
||||
onCrop={(node) => setCropNodeId(node.id)}
|
||||
onSplit={(node) => setSplitNodeId(node.id)}
|
||||
onUpscale={(node) => setUpscaleNodeId(node.id)}
|
||||
@@ -1394,6 +1517,7 @@ function InfiniteCanvasPage() {
|
||||
<CanvasToolbar
|
||||
selectedCount={selectedNodeIds.size}
|
||||
workspaceMode={workspaceMode}
|
||||
isProjectLinked={Boolean(currentProject?.projectId)}
|
||||
canUndo={historyState.canUndo}
|
||||
canRedo={historyState.canRedo}
|
||||
backgroundMode={backgroundMode}
|
||||
@@ -1402,10 +1526,10 @@ function InfiniteCanvasPage() {
|
||||
onAddVideo={() => createNode(CanvasNodeType.Video)}
|
||||
onAddAudio={() => createNode(CanvasNodeType.Audio)}
|
||||
onAddText={() => createNode(CanvasNodeType.Text)}
|
||||
onAddNovel={createNovelNode}
|
||||
onChooseStyle={() => setStylePickerOpen(true)}
|
||||
onAddScript={() => createNode(CanvasNodeType.Script)}
|
||||
onAddFrame={() => createNode(CanvasNodeType.Frame)}
|
||||
onAddDrawing={() => createNode(CanvasNodeType.Drawing)}
|
||||
onAddConfig={() => createNode(CanvasNodeType.Config)}
|
||||
onOpenDirector={() => createDirectorShot()}
|
||||
onUndo={undoCanvas}
|
||||
@@ -1419,29 +1543,31 @@ function InfiniteCanvasPage() {
|
||||
onOpenMyAssets={() => {
|
||||
openAssetsAtPosition();
|
||||
}}
|
||||
onOpenProjectCharacters={() => openProjectAssets("character")}
|
||||
/>
|
||||
|
||||
{isMiniMapOpen ? <Minimap nodes={nodes} viewport={viewport} viewportSize={size} canvasContainerRef={containerRef} onViewportPreviewChange={previewViewport} onViewportChange={handleViewportChange} /> : null}
|
||||
|
||||
<div data-canvas-no-zoom className="absolute bottom-4 left-4 z-50 flex items-end gap-2" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
|
||||
<CanvasZoomControls scale={viewport.k} containerRef={containerRef} onScaleChange={setZoomScale} onReset={resetViewport} isMiniMapOpen={isMiniMapOpen} onToggleMiniMap={() => setIsMiniMapOpen((value) => !value)} onOpenShortcuts={() => setShortcutRequestNonce((value) => value + 1)} />
|
||||
<CanvasAssetTray assetImages={imageAssets} canvasImages={canvasImageNodes} activeNodeId={selectedNodeIds.size === 1 ? Array.from(selectedNodeIds)[0] : null} onInsertAssetImage={(asset) => void createImageAssetNode(asset)} onFocusCanvasImage={focusCanvasImageNode} />
|
||||
<CanvasAssetTray assetImages={imageAssets} canvasImages={canvasImageNodes} showLibrary={!currentProject?.projectId} activeNodeId={selectedNodeIds.size === 1 ? Array.from(selectedNodeIds)[0] : null} onInsertAssetImage={(asset) => void createImageAssetNode(asset)} onFocusCanvasImage={focusCanvasImageNode} />
|
||||
</div>
|
||||
|
||||
<CanvasProjectContextMenu
|
||||
menu={contextMenu}
|
||||
node={contextMenuNode}
|
||||
workspaceMode={workspaceMode}
|
||||
isProjectLinked={Boolean(currentProject?.projectId)}
|
||||
canUndo={historyState.canUndo}
|
||||
canRedo={historyState.canRedo}
|
||||
canPaste={hasCopiedNodes || Boolean(navigator.clipboard)}
|
||||
screenToCanvas={screenToCanvas}
|
||||
onClose={() => setContextMenu(null)}
|
||||
onAddNode={(type, position) => createNode(type, position)}
|
||||
onAddNovel={(position) => createNovelNode(position)}
|
||||
onOpenDirector={createDirectorShot}
|
||||
onUpload={(nodeId, position) => handleUploadRequest(nodeId, position)}
|
||||
onOpenAssets={openAssetsAtPosition}
|
||||
onOpenProjectCharacters={(position) => openProjectAssets("character", position)}
|
||||
onUndo={undoCanvas}
|
||||
onRedo={redoCanvas}
|
||||
onPaste={pasteAtPosition}
|
||||
@@ -1450,12 +1576,13 @@ function InfiniteCanvasPage() {
|
||||
onDeleteNode={(nodeId) => deleteNodes(new Set([nodeId]))}
|
||||
onDeleteConnection={deleteConnection}
|
||||
onSaveAsset={(node) => { void saveNodeAsset(node); }}
|
||||
onViewImage={(node) => setPreviewNodeId(node.id)}
|
||||
onEditNode={(node) => { setSelectedNodeIds(new Set([node.id])); setSelectedConnectionId(null); setDialogNodeId(node.id); }}
|
||||
onEditText={openTextEditor}
|
||||
onViewMedia={(node) => setPreviewNodeId(node.id)}
|
||||
onEditText={openTextNodeEditor}
|
||||
onOpenDrawing={openDrawingNode}
|
||||
onGenerateImage={generateImageFromTextNode}
|
||||
onCopyContent={(node) => { void copyNodeContentToClipboard(node); }}
|
||||
onCopyOssUrl={(node) => { void copyNodeOssUrlToClipboard(node); }}
|
||||
onCopyMediaUrl={(node) => { void copyNodeMediaUrlToClipboard(node); }}
|
||||
onSetAssetCategory={(nodeId, assetCategory) => handleConfigNodeChange(nodeId, { assetCategory })}
|
||||
onToggleFrame={(node) => toggleFrameCollapsed(node.id)}
|
||||
/>
|
||||
|
||||
@@ -1463,6 +1590,34 @@ function InfiniteCanvasPage() {
|
||||
|
||||
<CanvasNodeInfoModal node={infoNode} open={Boolean(infoNode)} onClose={() => setInfoNodeId(null)} onMetadataChange={handleConfigNodeChange} />
|
||||
|
||||
<CanvasCharacterReferenceModal
|
||||
node={characterReferenceNode}
|
||||
open={Boolean(characterReferenceNode)}
|
||||
onClose={() => setCharacterReferenceNodeId(null)}
|
||||
/>
|
||||
|
||||
<CanvasTextEditorModal
|
||||
node={textEditorNode}
|
||||
open={Boolean(textEditorNode)}
|
||||
onClose={() => setTextEditorNodeId(null)}
|
||||
onSave={(nodeId, title, content, richText) => {
|
||||
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, title, metadata: { ...node.metadata, content, richText } } : node));
|
||||
}}
|
||||
/>
|
||||
|
||||
{drawingNode ? <Suspense fallback={<div className="fixed inset-0 z-[500] grid place-items-center px-5" style={{ background: theme.canvas.background, color: theme.node.text }}><WorkspaceState icon="loading" title="正在加载绘图编辑器" description="正在准备绘图画布。" /></div>}>
|
||||
<CanvasDrawingEditorModal
|
||||
node={drawingNode}
|
||||
projectId={projectId}
|
||||
open={Boolean(drawingNode)}
|
||||
onClose={() => 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("绘图已保存");
|
||||
}}
|
||||
/>
|
||||
</Suspense> : null}
|
||||
|
||||
<CanvasScriptEditor
|
||||
node={activeScriptNode}
|
||||
open={Boolean(activeScriptNode)}
|
||||
@@ -1477,7 +1632,7 @@ function InfiniteCanvasPage() {
|
||||
/>
|
||||
|
||||
{directorNodeId && activeDirectorScene ? (
|
||||
<Suspense fallback={<div className="fixed inset-0 z-[500] grid place-items-center" style={{ background: theme.canvas.background, color: theme.node.text }}>正在加载 3D 导演台...</div>}>
|
||||
<Suspense fallback={<div className="fixed inset-0 z-[500] grid place-items-center px-5" style={{ background: theme.canvas.background, color: theme.node.text }}><WorkspaceState icon="loading" title="正在加载 3D 导演台" description="准备场景、镜头与空间控制。" /></div>}>
|
||||
<CanvasDirectorWorkbench
|
||||
open
|
||||
scene={activeDirectorScene}
|
||||
@@ -1529,7 +1684,8 @@ function InfiniteCanvasPage() {
|
||||
onInsert={handleAssetInsert}
|
||||
onClose={closeAssetPicker}
|
||||
/>
|
||||
{codexCompactAgent && !assistantMounted ? <CanvasLocalAgentPanel headless snapshot={agentSnapshot} canUndoOps={canUndoAgentOps} onApplyOps={applyAgentOps} onUndoOps={undoAgentOps} autoConnect={codexAutoConnect} /> : null}
|
||||
<CanvasProjectAssetModal open={projectAssetOpen} detail={linkedProjectQuery.data} initialCategory={projectAssetInitialCategory} onClose={closeProjectAssets} onInsert={(payloads) => handleProjectAssetsInsert(payloads, projectAssetInsertPosition)} />
|
||||
{codexCompactAgent && !assistantMounted ? <CanvasLocalAgentPanel headless snapshot={agentSnapshot} canUndoOps={canUndoAgentOps} undoOpsCount={agentUndoCount} onApplyOps={applyAgentOps} onUndoOps={undoAgentOps} autoConnect={codexAutoConnect} /> : null}
|
||||
</section>
|
||||
{assistantMounted ? (
|
||||
<CanvasAssistantPanel
|
||||
@@ -1543,6 +1699,7 @@ function InfiniteCanvasPage() {
|
||||
onSessionsChange={handleAssistantSessionsChange}
|
||||
onApplyOps={applyAgentOps}
|
||||
canUndoOps={canUndoAgentOps}
|
||||
undoOpsCount={agentUndoCount}
|
||||
onUndoOps={undoAgentOps}
|
||||
onPasteImage={pasteAssistantImage}
|
||||
agentMode={agentMode}
|
||||
|
||||
Reference in new issue
Block a user