import { Button, Dropdown, Input, InputNumber, Modal, Segmented, Select, Table } from "antd"; import { Tooltip } from "@/components/ui/base/tooltip"; import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; import { CheckboxGroup } from "@/components/ui/base/checkbox"; import type { MenuProps } from "antd"; import type { ColumnsType } from "antd/es/table"; import { ChevronDown, ChevronUp, Clapperboard, Copy, Expand, Film, Grid3X3, Image as ImageIcon, ListTree, Merge, MoreHorizontal, Plus, RefreshCw, Send, Square, Trash2, Video } from "lucide-react"; import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea"; import { StoryboardAssetsCell } from "@/components/canvas/storyboard-assets-cell"; import { ModelPicker } from "@/components/model-picker"; import { buildGenerationConfig } from "@/lib/canvas/canvas-project-generation"; import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; import { pipelineStatusLabel, type CanvasStoryboardPipelineProgress, type StoryboardPipelineStage } from "@/lib/canvas/canvas-storyboard-progress"; import { generationErrorMessage, isContentModerationError } from "@/lib/generation-error"; import { generationTaskShowsProgress, generationTaskStageLabel } from "@/lib/generation-task-display"; import { navigateToSettings } from "@/lib/settings-navigation"; import { canvasThemes } from "@/lib/canvas-theme"; import { useEffectiveConfig } from "@/stores/use-config-store"; import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store"; import { STORYBOARD_COMPOSER_MIN_HEIGHT, STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardTableHeight } from "@/lib/canvas/canvas-storyboard-layout"; import type { CanvasGenerationBatch, CanvasGenerationBatchItem, CanvasGenerationBatchItemStatus, CanvasNodeData, CanvasNodeStatus, CanvasWorkspaceMode, StoryboardColumn, StoryboardRow, StoryboardShotCount, StoryboardShotDuration, StoryboardVideoInputMode, } from "@/types/canvas"; import type { TaskStatus } from "@/services/api/task-center"; const STORYBOARD_PROMPT_MIN_HEIGHT = 40; const STORYBOARD_PROMPT_MAX_HEIGHT = 116; const SCRIPT_GRID_TEMPLATE = "64px 76px minmax(300px, 1.55fr) minmax(220px, 1fr) 180px"; const EMPTY_STORYBOARD_ROWS: StoryboardRow[] = []; const DEFAULT_STORYBOARD_COLUMNS: StoryboardColumn[] = ["shotNumber", "durationSeconds", "videoMotionPrompt", "dialogue", "assets"]; const LEGACY_STORYBOARD_COLUMNS: StoryboardColumn[] = ["shotNumber", "durationSeconds", "plotDescription", "dialogue"]; const PREVIOUS_DEFAULT_STORYBOARD_COLUMNS: StoryboardColumn[] = ["shotNumber", "plotDescription", "videoMotionPrompt", "dialogue"]; function resolveStoryboardVisibleColumns(columns?: StoryboardColumn[]) { const isLegacyDefault = columns?.length === LEGACY_STORYBOARD_COLUMNS.length && LEGACY_STORYBOARD_COLUMNS.every((column) => columns.includes(column)); const isPreviousDefault = columns?.length === PREVIOUS_DEFAULT_STORYBOARD_COLUMNS.length && PREVIOUS_DEFAULT_STORYBOARD_COLUMNS.every((column) => columns.includes(column)); if (!columns?.length || isLegacyDefault || isPreviousDefault) { return DEFAULT_STORYBOARD_COLUMNS; } return columns; } const columnOptions: Array<{ label: string; value: StoryboardColumn }> = [ { label: "序号", value: "shotNumber" }, { label: "时长", value: "durationSeconds" }, { label: "画面描述", value: "plotDescription" }, { label: "台词/旁白", value: "dialogue" }, { label: "镜头意图", value: "narrativeIntent" }, { label: "观众视点", value: "viewerPOV" }, { label: "表演调度", value: "performanceBlocking" }, { label: "景别", value: "shotSize" }, { label: "情绪", value: "emotion" }, { label: "光影氛围", value: "lightingAndAtmosphere" }, { label: "音效", value: "audioEffects" }, { label: "镜头设计", value: "camera" }, { label: "运镜", value: "motion" }, { label: "时间节拍", value: "timeBeats" }, { label: "图片提示词", value: "imageGenerationPrompt" }, { label: "视频提示词", value: "videoMotionPrompt" }, { label: "关联资产", value: "assets" }, { label: "连续性出口", value: "continuityOut" }, { label: "负面要求", value: "negativePrompt" }, ]; export function CanvasScriptNodeContent({ node, nodes, batch, pipeline, scale, mentionReferences, onOpen, onCreateImageNodes, onCreateVideoNodes, onGenerateImages, onGenerateVideos, onVideoInputModeChange, onMergeVideos, onCreateActionBoards, onRetryBatch, onRetryBatchItem, onStopBatch, onAddRow, onRemoveRow, onUpdateRow, onPromptChange, onGenerateScript, onModelChange, onShotDurationChange, onShotCountChange, onComposerHeightChange, onConnectStart, onScrollTopChange, workspaceMode = "professional", }: { node: CanvasNodeData; nodes: CanvasNodeData[]; batch?: CanvasGenerationBatch; pipeline: CanvasStoryboardPipelineProgress; scale: number; mentionReferences: CanvasResourceReference[]; onOpen: () => void; onCreateImageNodes: () => void; onCreateVideoNodes: () => void; onGenerateImages: (rowIds: string[]) => void; onGenerateVideos: (rowIds: string[]) => void; onVideoInputModeChange: (mode: StoryboardVideoInputMode) => void; onMergeVideos: () => void; onCreateActionBoards: () => void; onRetryBatch: (batchId: string) => void; onRetryBatchItem: (batchId: string, itemId: string) => void; onStopBatch: (batchId: string) => void; onAddRow: () => void; onRemoveRow: (rowId: string) => void; onUpdateRow: (rowId: string, patch: Partial) => void; onPromptChange: (prompt: string) => void; onGenerateScript: (prompt: string) => void; onModelChange: (model: string) => void; onShotDurationChange: (duration: StoryboardShotDuration) => void; onShotCountChange: (count: StoryboardShotCount) => void; onComposerHeightChange: (height: number) => void; onConnectStart: (event: ReactPointerEvent, rowId: string, handleType: "source" | "target") => void; onScrollTopChange: (scrollTop: number) => void; workspaceMode?: CanvasWorkspaceMode; }) { const theme = canvasThemes[useActiveTheme()]; const effectiveConfig = useEffectiveConfig(); const generationConfig = buildGenerationConfig(effectiveConfig, node, "text"); const simpleMode = workspaceMode === "simple"; const rows = node.metadata?.storyboard?.rows || []; const [prompt, setPrompt] = useState(node.metadata?.composerContent || ""); const [scrollTop, setScrollTop] = useState(0); const composerHeightChangeRef = useRef(onComposerHeightChange); const reportedComposerHeightRef = useRef(null); const composerHeight = node.metadata?.storyboardComposerHeight || STORYBOARD_COMPOSER_MIN_HEIGHT; const tableHeight = storyboardTableHeight(node.height, composerHeight); const totalDuration = rows.reduce((sum, row) => sum + (Number(row.durationSeconds) || 0), 0); const shotDuration = node.metadata?.storyboardShotDuration || "auto"; const shotCount = node.metadata?.storyboardShotCount || "auto"; const videoInputMode = node.metadata?.storyboardVideoInputMode || "direct"; const batchItemByRowId = useMemo(() => new Map((batch?.items || []).map((item) => [item.rowId, item])), [batch?.items]); const batchSummary = batch ? generationBatchSummary(batch) : null; const hasFailedBatchItems = Boolean(batch?.items.some((item) => item.status === "failed")); const hasWaitingBatchItems = Boolean(batch?.items.some((item) => item.status === "waiting" || item.status === "submitting")); const hasActiveBatchItems = Boolean(batch?.items.some((item) => item.status === "waiting" || item.status === "submitting" || item.status === "queued" || item.status === "running")); const taskStatus = node.metadata?.taskStatus; const displayStatus: TaskStatus = taskStatus === "queued" || taskStatus === "succeeded" || taskStatus === "failed" || taskStatus === "cancelled" ? taskStatus : "running"; const displayTask = node.metadata?.taskId ? { provider: node.metadata.taskProvider, status: displayStatus, stage: node.metadata.taskStage, officialStatus: node.metadata.taskOfficialStatus, errorCode: node.metadata.taskErrorCode, } : null; const taskFeedback = node.metadata?.status === "loading" ? displayTask ? `${generationTaskStageLabel(displayTask)}${generationTaskShowsProgress(displayTask) && typeof node.metadata.taskProgress === "number" ? ` · ${node.metadata.taskProgress}%` : ""}` : "正在创建任务" : node.metadata?.status === "error" ? generationErrorMessage(node.metadata.errorDetails) : ""; const [batchDetailsOpen, setBatchDetailsOpen] = useState(false); const [moreMenuOpen, setMoreMenuOpen] = useState(false); const pipelineDisabled = !rows.length || node.metadata?.status === "loading" || hasActiveBatchItems; const missingImages = Math.max(0, pipeline.images.total - pipeline.images.created); const missingVideos = Math.max(0, pipeline.videos.total - pipeline.videos.created); const canMerge = pipeline.successfulVideoNodeIds.length >= 2 && pipeline.final.success === 0; const allRowIds = pipeline.rows.map((item) => item.row.id); const moreMenuItems: MenuProps["items"] = [ { key: "generate-images", icon: , label: "生成未完成分镜图", disabled: pipelineDisabled || pipeline.images.incomplete === 0, onClick: () => onGenerateImages(allRowIds) }, { key: "generate-videos", icon: