refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态

This commit is contained in:
ddCat
2026-09-09 22:02:10 +08:00
parent bacd502f5c
commit b5f3413ded
298 changed files with 12681 additions and 26428 deletions

View File

@@ -7,6 +7,7 @@ import { generationTaskShowsProgress, generationTaskStageLabel } from "@/lib/gen
import { formatTaskLog, type GenerationTask, type TaskLog } from "@/services/api/task-center";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
import { VideoPlayer } from "@/components/video-player";
import { AppModal } from "@/components/ui/product/app-modal";
import { modelDisplayName, useEffectiveConfig } from "@/stores/use-config-store";
type CanvasProjectStatusDialogsProps = {
@@ -74,19 +75,20 @@ export function CanvasProjectStatusDialogs({ theme, task, taskLogs, taskLoading,
<div className="py-8 text-center text-base font-medium">暂未实现</div>
</Modal>
<Modal
<AppModal
title="视频预览"
open={Boolean(previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Video)}
centered
onCancel={onClosePreview}
footer={null}
width="min(1200px, calc(100vw - 32px))"
styles={{ body: { padding: 0, display: "flex", justifyContent: "center", alignItems: "center", maxHeight: "84vh", overflow: "hidden", background: "#090909" } }}
flush
styles={{ body: { display: "flex", justifyContent: "center", alignItems: "center", maxHeight: "84vh", overflow: "hidden", background: "var(--workspace-canvas-deep)" } }}
>
{previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Video ? (
<VideoPlayer src={previewNode.metadata.content} mimeType={previewNode.metadata.mimeType} title={previewNode.title || "视频预览"} hasAudio={typeof previewNode.metadata.hasAudio === "boolean" ? previewNode.metadata.hasAudio : undefined} className="max-h-[84vh] max-w-full bg-black" />
) : null}
</Modal>
</AppModal>
{previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Image ? (
<CanvasImagePreview

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { AppModal } from "@/components/ui/product/app-modal";
import { Command, Search } from "lucide-react";
import { Kbd } from "@/components/ui/base/kbd";
import { Modal } from "antd";
import {
CANVAS_SHORTCUT_CATEGORIES,
@@ -31,7 +31,7 @@ export function CanvasShortcutsModal({ open, onClose }: { open: boolean; onClose
);
return (
<Modal
<AppModal
className="workspace-modal workspace-modal-wide canvas-shortcuts-modal"
open={open}
onCancel={onClose}
@@ -40,7 +40,7 @@ export function CanvasShortcutsModal({ open, onClose }: { open: boolean; onClose
centered
keyboard
width="min(860px, calc(100vw - 24px))"
styles={{ container: { padding: 0 }, body: { padding: 0 } }}
flush
afterOpenChange={(visible) => {
if (visible) window.requestAnimationFrame(() => inputRef.current?.focus());
}}
@@ -109,7 +109,7 @@ export function CanvasShortcutsModal({ open, onClose }: { open: boolean; onClose
<span className="canvas-shortcuts-close-hint"><Kbd>Esc</Kbd> 关闭</span>
</footer>
</div>
</Modal>
</AppModal>
);
}

View File

@@ -47,7 +47,6 @@ import { CanvasVideoFrameDialog } from "@/components/canvas/canvas-video-frame-d
import { CanvasVideoSegmentDialog } from "@/components/canvas/canvas-video-segment-dialog";
import { CanvasTimelineDialog } from "@/components/canvas/canvas-timeline-dialog";
import { syncNodeSubtitlesToTimeline } from "@/lib/timeline/timeline-build";
import type { TimelineDirectMedia } from "@/types/timeline";
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";
@@ -60,7 +59,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 { useCanvasCreateCommands } from "@/components/canvas/use-canvas-create-commands";
import { AssetPickerModal, type InsertAssetPayload } from "@/components/canvas/asset-picker-modal";
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";
@@ -126,6 +125,7 @@ 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 { useCanvasTimelineAssetInsert } from "./use-canvas-timeline-asset-insert";
import { useCanvasViewportController } from "./use-canvas-viewport-controller";
import { usePortraitClearanceCoordinator } from "./use-portrait-clearance-coordinator";
import {
@@ -205,7 +205,8 @@ export default function CanvasPage() {
}
function InfiniteCanvasPage() {
const { message } = App.useApp();
// 命令式确认必须走 App.useApp().modal;静态 Modal.confirm 拿不到主题和 App 上下文。
const { message, modal } = App.useApp();
const queryClient = useQueryClient();
const params = useParams<{ id: string }>();
const [searchParams, setSearchParams] = useSearchParams();
@@ -281,10 +282,6 @@ function InfiniteCanvasPage() {
const [stylePickerOpen, setStylePickerOpen] = useState(false);
// 新建导演台镜头必须先选模板:null 表示未在选择中,undefined position 表示用画布中心。
const [directorTemplateRequest, setDirectorTemplateRequest] = useState<{ position?: Position } | null>(null);
const [projectAssetOpen, setProjectAssetOpen] = useState(false);
const [projectAssetInitialCategory, setProjectAssetInitialCategory] = useState("all");
const [projectAssetInitialFolderId, setProjectAssetInitialFolderId] = useState("all");
const [projectAssetInsertPosition, setProjectAssetInsertPosition] = useState<Position | undefined>();
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
const [subtitleNodeId, setSubtitleNodeId] = useState<string | null>(null);
const [timelineNodeId, setTimelineNodeId] = useState<string | null>(null);
@@ -523,7 +520,7 @@ function InfiniteCanvasPage() {
message.warning("官方即梦 CLI 当前不支持可靠取消,请等待官方状态同步");
return;
}
Modal.confirm({
modal.confirm({
title: "取消生成任务?",
content: "任务会立即停止本地执行;如果已经提交到上游,系统会继续核对取消结果和积分状态。",
okText: "取消任务",
@@ -543,7 +540,7 @@ function InfiniteCanvasPage() {
},
});
},
[bindGenerationTask, message, nodesRef, projectId, queryClient, setTaskDetail],
[bindGenerationTask, message, modal, nodesRef, projectId, queryClient, setTaskDetail],
);
useEffect(() => {
@@ -703,6 +700,28 @@ function InfiniteCanvasPage() {
setDialogNodeId,
});
const replaceCanvasNodeMedia = useCallback((node: CanvasNodeData) => handleUploadRequest(node.id), [handleUploadRequest]);
const {
timelineAddNodeRef,
timelineMediaAddRef,
assetInsertScope,
projectAssetScope,
projectAssetOpen,
projectAssetInitialCategory,
projectAssetInitialFolderId,
projectAssetInsertPosition,
handleLibraryAssetsInsert,
handleTimelineProjectAssetsInsert,
openProjectAssets,
openCanvasAssetLibrary,
openTimelineAssetLibrary,
closeProjectAssets,
} = useCanvasTimelineAssetInsert({
linkedProjectId,
refetchLinkedProject,
handleAssetsInsert,
handleProjectAssetsInsert,
openAssetsAtPosition,
});
useEffect(() => {
if (!projectLoaded || searchParams.get("mode") !== "handoff") return;
@@ -745,103 +764,6 @@ function InfiniteCanvasPage() {
});
}, [assets, assetsHydrated, handleProjectAssetsInsert, message, nodesRef, projectId, projectLoaded, searchParams, setSearchParams, updateProject]);
// 时间线弹窗内新增素材的回填通道:素材库/上传创建节点后由弹窗通过 ref 加入草稿。
const timelineAddNodeRef = useRef<((node: CanvasNodeData) => void) | null>(null);
// 时间线作用域直连媒体入轨通道:素材库/项目资产/本地上传不落画布,仅加入时间线草稿。
const timelineMediaAddRef = useRef<((media: TimelineDirectMedia) => void) | null>(null);
// 素材库与项目资产弹窗的插入作用域:时间线弹窗内打开时为 timeline,其余为 canvas。
const [assetInsertScope, setAssetInsertScope] = useState<"canvas" | "timeline">("canvas");
const [projectAssetScope, setProjectAssetScope] = useState<"canvas" | "timeline">("canvas");
// InsertAssetPayload → 直连媒体:仅音视频支持直接入轨;图片/文本/角色返回 null(避免在画布重复出现)。
const payloadToTimelineMedia = (payload: InsertAssetPayload): TimelineDirectMedia | null => {
const randomSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
if (payload.kind === "video") {
return {
id: payload.assetId || `asset-${randomSuffix}`,
kind: "video",
title: payload.title,
storageKey: payload.storageKey,
url: payload.url,
width: payload.width,
height: payload.height,
durationMs: payload.durationMs,
bytes: payload.bytes,
mimeType: payload.mimeType,
};
}
if (payload.kind === "audio") {
return { id: payload.assetId || `asset-${randomSuffix}`, kind: "audio", title: payload.title, storageKey: payload.storageKey, url: payload.url, durationMs: payload.durationMs, bytes: payload.bytes, mimeType: payload.mimeType };
}
return null;
};
const handleLibraryAssetsInsert = useCallback(
async (payloads: InsertAssetPayload[]) => {
if (assetInsertScope === "timeline") {
const media = payloads.map(payloadToTimelineMedia).filter((item): item is TimelineDirectMedia => Boolean(item));
if (media.length !== payloads.length) throw new Error("图片和文本素材暂不支持直接入轨,请先插入画布");
media.forEach((item) => timelineMediaAddRef.current?.(item));
return;
}
const created = await handleAssetsInsert(payloads);
created.forEach((node) => timelineAddNodeRef.current?.(node));
},
[assetInsertScope, handleAssetsInsert],
);
// 项目资产库引入到时间线:复用现有引入逻辑,把创建出的节点回填到弹窗草稿。
const handleTimelineProjectAssetsInsert = useCallback(
async (payloads: InsertAssetPayload[]) => {
if (projectAssetScope === "timeline") {
let inserted = 0;
for (const payload of payloads) {
const media = payloadToTimelineMedia(payload);
if (media) {
timelineMediaAddRef.current?.(media);
inserted += 1;
}
}
if (inserted < payloads.length) message.info("图片/文本/角色素材暂不支持直接入轨,仅音视频素材已加入时间线");
return;
}
const created = await handleProjectAssetsInsert(payloads, projectAssetInsertPosition);
created.forEach((node) => timelineAddNodeRef.current?.(node));
},
[handleProjectAssetsInsert, message, projectAssetInsertPosition, projectAssetScope],
);
const openProjectAssets = useCallback(
(initialCategory = "all", position?: Position, scope: "canvas" | "timeline" = "canvas", initialFolderId = "all") => {
setProjectAssetScope(scope);
setProjectAssetInitialCategory(initialCategory);
setProjectAssetInitialFolderId(initialFolderId);
setProjectAssetInsertPosition(position);
setProjectAssetOpen(true);
// 资产与项目实时同步:打开弹窗前刷新关联短剧项目资产,避免缓存导致资产列表空白/过期。
if (linkedProjectId) void refetchLinkedProject();
},
[linkedProjectId, refetchLinkedProject],
);
// 素材库打开入口:画布作用域(工具栏/空态/侧栏)与时间线作用域(时间线弹窗)分别标记插入目标。
const openCanvasAssetLibrary = useCallback(
(position?: Position) => {
setAssetInsertScope("canvas");
openAssetsAtPosition(position);
},
[openAssetsAtPosition],
);
const openTimelineAssetLibrary = useCallback(() => {
setAssetInsertScope("timeline");
openAssetsAtPosition();
}, [openAssetsAtPosition]);
const closeProjectAssets = useCallback(() => {
setProjectAssetOpen(false);
setProjectAssetInsertPosition(undefined);
setProjectAssetInitialFolderId("all");
}, []);
const {
angleNodeId,
emotionNodeId,
@@ -1739,14 +1661,14 @@ function InfiniteCanvasPage() {
}, [arkPrivateAssetUploadNodeId, currentProject?.projectId, handleConfigNodeChange, message, projectId]);
const confirmUploadNodeImageToArkPrivateAsset = useCallback((node: CanvasNodeData) => {
Modal.confirm({
modal.confirm({
title: "上传到方舟素材库",
content: "仅可上传你拥有肖像、版权或其他合法使用权的图片。方舟审核通过后,Seedance 会使用受控素材标识生成视频。",
okText: "确认拥有使用权并上传",
cancelText: "取消",
onOk: () => uploadNodeImageToArkPrivateAsset(node),
});
}, [uploadNodeImageToArkPrivateAsset]);
}, [modal, uploadNodeImageToArkPrivateAsset]);
const handleCanvasContextMenu = useCallback(
(event: ReactMouseEvent) => {

View File

@@ -7,6 +7,7 @@ import { buildGenerationConfig, isGenerationCanceled } from "@/lib/canvas/canvas
import { canvasGenerationPromptMetadata, canvasGenerationRequestFingerprint, runCanvasGenerationSubmissionOnce } from "@/lib/canvas/canvas-generation-submission";
import { isGenerationTaskCapacityError } from "@/lib/canvas/canvas-generation-batch";
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
import { buildTextRewritePrompt } from "@/lib/prompts";
import { resolveCanvasStyleExecution } from "@/lib/canvas/canvas-style-execution";
import { generationErrorMessage, generationFailureMetadata } from "@/lib/generation-error";
import { modelCompatibilityError, modelGroupReferenceLimits, modelPromptLengthError, modelRequestOptions, type ModelRequirements } from "@/lib/model-selection";
@@ -143,7 +144,7 @@ export function useCanvasGenerationExecutor({
nodeId,
nodesRef.current,
connectionsRef.current,
editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : generationPrompt,
editingTextNode ? buildTextRewritePrompt(sourceTextContent, prompt) : generationPrompt,
assets,
promptOnly,
);

View File

@@ -29,6 +29,7 @@ import {
import { fitNodeSize, VIDEO_NODE_MAX_SIZE } from "@/lib/canvas/canvas-node-size";
import { compositeEmotionImage, emotionGenerationSize, emotionProviderMask, normalizeEmotionPromptForProvider, resolveEmotionEditPlan } from "@/lib/canvas/canvas-emotion";
import { DEFAULT_PORTRAIT_TEXTURE_SETTINGS } from "@/lib/canvas/canvas-portrait-texture";
import { IMAGE_PROMPT_REVERSE } from "@/lib/prompts";
import { createPortraitTextureNode } from "@/lib/canvas/canvas-image-source";
import { captureVideoFrames } from "@/lib/canvas/canvas-video-frame";
import { buildVideoFrameNodes } from "@/lib/canvas/canvas-video-frame-nodes";
@@ -71,12 +72,6 @@ const NODE_STATUS_LOADING = "loading" as const;
const NODE_STATUS_SUCCESS = "success" as const;
const NODE_STATUS_ERROR = "error" as const;
const NODE_STATUS_IDLE = "idle" as const;
const IMAGE_PROMPT_REVERSE_PRESET = `请根据参考图片反推一段适合用于 AI 生图的提示词。
要求:
1. 只输出提示词正文,不要解释。
2. 覆盖主体、构图、风格、光线、色彩、材质、镜头和氛围。
3. 尽量写成可直接用于生图模型的完整提示词。`;
export function useCanvasMediaTools({
projectId,
@@ -160,7 +155,7 @@ export function useCanvasMediaTools({
const resultSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Text];
const centerY = node.position.y + node.height / 2;
const textNode = {
...createCanvasNode(CanvasNodeType.Text, { x: node.position.x + node.width + gap + textSpec.width / 2, y: centerY }, { content: IMAGE_PROMPT_REVERSE_PRESET, prompt: IMAGE_PROMPT_REVERSE_PRESET, status: NODE_STATUS_SUCCESS, fontSize: 14 }),
...createCanvasNode(CanvasNodeType.Text, { x: node.position.x + node.width + gap + textSpec.width / 2, y: centerY }, { content: IMAGE_PROMPT_REVERSE, prompt: IMAGE_PROMPT_REVERSE, status: NODE_STATUS_SUCCESS, fontSize: 14 }),
title: "反推提示词",
};
const resultNode = {

View File

@@ -8,7 +8,7 @@ import { removeCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage";
import { normalizeCanvasNodeTimestamps } from "@/lib/canvas/canvas-node-timestamps";
import { hydrateAssistantImages, resetInterruptedGeneration } from "@/lib/canvas/canvas-project-generation";
import { listAddedSkills, type Skill } from "@/services/api/skills";
import { createCanvasProjectWithRemoteSync, deleteCanvasProjectsWithRemoteSync, loadCanvasProjectForEditing, saveRemoteUserDataNow } from "@/services/user-data-sync";
import { createCanvasProjectWithRemoteSync, deleteCanvasProjectsWithRemoteSync, loadCanvasProjectForEditing, localSavedRemotePendingMessage, saveRemoteUserDataNow } from "@/services/user-data-sync";
import { flushCanvasStorePersistence, useCanvasStore } from "@/stores/canvas/use-canvas-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { useUserStore } from "@/stores/use-user-store";
@@ -244,8 +244,8 @@ export function useCanvasProjectLifecycle({
await saveRemoteUserDataNow();
message.success("画布布局和位置已保存");
} catch (error) {
const detail = error instanceof Error ? error.message : "未知错误";
message.warning(`本地画布布局已保存,云端同步失败:${detail}`);
// 本地 IndexedDB 已写入;云端失败只排队重试,不能把导入方的画布状态回滚。
message.warning(localSavedRemotePendingMessage("本地画布布局已保存", error));
}
return true;
}, [activeChatId, backgroundMode, canvasAppearance, chatSessions, connectionsRef, currentProject?.directorScenes, message, nodesRef, projectId, showImageInfo, updateProject, viewportRef]);

View File

@@ -0,0 +1,138 @@
import { useCallback, useRef, useState } from "react";
import { App } from "antd";
import type { InsertAssetPayload } from "@/components/canvas/asset-picker-modal";
import type { CanvasNodeData, Position } from "@/types/canvas";
import type { TimelineDirectMedia } from "@/types/timeline";
type UseCanvasTimelineAssetInsertOptions = {
linkedProjectId?: string;
refetchLinkedProject: () => Promise<unknown>;
handleAssetsInsert: (payloads: InsertAssetPayload[]) => Promise<CanvasNodeData[]>;
handleProjectAssetsInsert: (payloads: InsertAssetPayload[], position?: Position) => Promise<CanvasNodeData[]>;
openAssetsAtPosition: (position?: Position) => void;
};
function payloadToTimelineMedia(payload: InsertAssetPayload): TimelineDirectMedia | null {
const randomSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
if (payload.kind === "video") {
return {
id: payload.assetId || `asset-${randomSuffix}`,
kind: "video",
title: payload.title,
storageKey: payload.storageKey,
url: payload.url,
width: payload.width,
height: payload.height,
durationMs: payload.durationMs,
bytes: payload.bytes,
mimeType: payload.mimeType,
};
}
if (payload.kind === "audio") {
return { id: payload.assetId || `asset-${randomSuffix}`, kind: "audio", title: payload.title, storageKey: payload.storageKey, url: payload.url, durationMs: payload.durationMs, bytes: payload.bytes, mimeType: payload.mimeType };
}
return null;
}
export function useCanvasTimelineAssetInsert({
linkedProjectId,
refetchLinkedProject,
handleAssetsInsert,
handleProjectAssetsInsert,
openAssetsAtPosition,
}: UseCanvasTimelineAssetInsertOptions) {
const { message } = App.useApp();
// 时间线弹窗内新增素材的回填通道:素材库/上传创建节点后由弹窗通过 ref 加入草稿。
const timelineAddNodeRef = useRef<((node: CanvasNodeData) => void) | null>(null);
// 时间线作用域直连媒体入轨通道:素材库/项目资产/本地上传不落画布,仅加入时间线草稿。
const timelineMediaAddRef = useRef<((media: TimelineDirectMedia) => void) | null>(null);
// 素材库与项目资产弹窗的插入作用域:时间线弹窗内打开时为 timeline,其余为 canvas。
const [assetInsertScope, setAssetInsertScope] = useState<"canvas" | "timeline">("canvas");
const [projectAssetScope, setProjectAssetScope] = useState<"canvas" | "timeline">("canvas");
const [projectAssetOpen, setProjectAssetOpen] = useState(false);
const [projectAssetInitialCategory, setProjectAssetInitialCategory] = useState("all");
const [projectAssetInitialFolderId, setProjectAssetInitialFolderId] = useState("all");
const [projectAssetInsertPosition, setProjectAssetInsertPosition] = useState<Position | undefined>();
const handleLibraryAssetsInsert = useCallback(
async (payloads: InsertAssetPayload[]) => {
if (assetInsertScope === "timeline") {
const media = payloads.map(payloadToTimelineMedia).filter((item): item is TimelineDirectMedia => Boolean(item));
if (media.length !== payloads.length) throw new Error("图片和文本素材暂不支持直接入轨,请先插入画布");
media.forEach((item) => timelineMediaAddRef.current?.(item));
return;
}
const created = await handleAssetsInsert(payloads);
created.forEach((node) => timelineAddNodeRef.current?.(node));
},
[assetInsertScope, handleAssetsInsert],
);
const handleTimelineProjectAssetsInsert = useCallback(
async (payloads: InsertAssetPayload[]) => {
if (projectAssetScope === "timeline") {
let inserted = 0;
for (const payload of payloads) {
const media = payloadToTimelineMedia(payload);
if (media) {
timelineMediaAddRef.current?.(media);
inserted += 1;
}
}
if (inserted < payloads.length) message.info("图片/文本/角色素材暂不支持直接入轨,仅音视频素材已加入时间线");
return;
}
const created = await handleProjectAssetsInsert(payloads, projectAssetInsertPosition);
created.forEach((node) => timelineAddNodeRef.current?.(node));
},
[handleProjectAssetsInsert, message, projectAssetInsertPosition, projectAssetScope],
);
const openProjectAssets = useCallback(
(initialCategory = "all", position?: Position, scope: "canvas" | "timeline" = "canvas", initialFolderId = "all") => {
setProjectAssetScope(scope);
setProjectAssetInitialCategory(initialCategory);
setProjectAssetInitialFolderId(initialFolderId);
setProjectAssetInsertPosition(position);
setProjectAssetOpen(true);
// 资产与项目实时同步:打开弹窗前刷新关联短剧项目资产,避免缓存导致资产列表空白/过期。
if (linkedProjectId) void refetchLinkedProject();
},
[linkedProjectId, refetchLinkedProject],
);
const openCanvasAssetLibrary = useCallback(
(position?: Position) => {
setAssetInsertScope("canvas");
openAssetsAtPosition(position);
},
[openAssetsAtPosition],
);
const openTimelineAssetLibrary = useCallback(() => {
setAssetInsertScope("timeline");
openAssetsAtPosition();
}, [openAssetsAtPosition]);
const closeProjectAssets = useCallback(() => {
setProjectAssetOpen(false);
setProjectAssetInsertPosition(undefined);
setProjectAssetInitialFolderId("all");
}, []);
return {
timelineAddNodeRef,
timelineMediaAddRef,
assetInsertScope,
projectAssetScope,
projectAssetOpen,
projectAssetInitialCategory,
projectAssetInitialFolderId,
projectAssetInsertPosition,
handleLibraryAssetsInsert,
handleTimelineProjectAssetsInsert,
openProjectAssets,
openCanvasAssetLibrary,
openTimelineAssetLibrary,
closeProjectAssets,
};
}