Merge branch 'main' of github.com:ddcat-ai/open-ai-canvas
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import { Button, Checkbox, Input, InputNumber, Modal, Popover, Segmented, Select, Table, Tooltip } from "antd";
|
||||
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import { Button, Checkbox, Dropdown, Input, InputNumber, Modal, Segmented, Select, Table, Tooltip } from "antd";
|
||||
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, Minus, Plus, RefreshCw, Send, Square, Trash2, Video, X } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, Clapperboard, Copy, Expand, Film, Grid3X3, Image as ImageIcon, ListTree, Merge, Minus, MoreHorizontal, Plus, RefreshCw, Send, Square, Trash2, Video, X } from "lucide-react";
|
||||
|
||||
import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea";
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -68,8 +69,8 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
|
||||
onOpen: () => void;
|
||||
onCreateImageNodes: () => void;
|
||||
onCreateVideoNodes: () => void;
|
||||
onGenerateImages: () => void;
|
||||
onGenerateVideos: () => void;
|
||||
onGenerateImages: (rowIds: string[]) => void;
|
||||
onGenerateVideos: (rowIds: string[]) => void;
|
||||
onVideoInputModeChange: (mode: StoryboardVideoInputMode) => void;
|
||||
onMergeVideos: () => void;
|
||||
onCreateActionBoards: () => void;
|
||||
@@ -113,6 +114,34 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
|
||||
const taskFeedback = node.metadata?.status === "loading"
|
||||
? `${node.metadata.taskStage || "正在创建任务"}${typeof node.metadata.taskProgress === "number" ? ` · ${node.metadata.taskProgress}%` : ""}`
|
||||
: node.metadata?.status === "error" ? generationErrorMessage(node.metadata.errorDetails) : "";
|
||||
const [batchDetailsOpen, setBatchDetailsOpen] = 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: <ImageIcon className="size-3.5" />, label: "生成未完成分镜图", disabled: pipelineDisabled || pipeline.images.incomplete === 0, onClick: () => onGenerateImages(allRowIds) },
|
||||
{ key: "generate-videos", icon: <Video className="size-3.5" />, label: "生成未完成视频", disabled: pipelineDisabled || pipeline.videos.incomplete === 0, onClick: () => onGenerateVideos(allRowIds) },
|
||||
{ key: "merge", icon: <Merge className="size-3.5" />, label: pipeline.final.success ? "成片已完成" : pipeline.successfulVideoNodeIds.length >= 2 ? `合并 ${pipeline.successfulVideoNodeIds.length} 段视频` : "合并成片(至少 2 段视频)", disabled: !canMerge, onClick: () => onMergeVideos() },
|
||||
{ type: "divider" },
|
||||
{ key: "video-input", icon: <Film className="size-3.5" />, label: "视频输入模式", children: [
|
||||
{ key: "video-input-direct", label: videoInputMode === "direct" ? "✓ 直接生成" : "直接生成", onClick: () => onVideoInputModeChange("direct") },
|
||||
{ key: "video-input-keyframe", label: videoInputMode === "keyframe" ? "✓ 先做首帧" : "先做首帧", onClick: () => onVideoInputModeChange("keyframe") },
|
||||
] },
|
||||
...(!simpleMode ? [
|
||||
{ type: "divider" as const },
|
||||
{ key: "create-image-nodes", icon: <Grid3X3 className="size-3.5" />, label: missingImages ? `创建 ${missingImages} 个图片节点` : "图片节点已创建", disabled: pipelineDisabled || missingImages === 0, onClick: () => onCreateImageNodes() },
|
||||
{ key: "create-video-nodes", icon: <Film className="size-3.5" />, label: missingVideos ? `创建 ${missingVideos} 个视频节点` : "视频节点已创建", disabled: pipelineDisabled || missingVideos === 0, onClick: () => onCreateVideoNodes() },
|
||||
{ key: "action-boards", icon: <Grid3X3 className="size-3.5" />, label: "生成动作拆分 12 宫格", disabled: !rows.length || hasActiveBatchItems, onClick: () => onCreateActionBoards() },
|
||||
] : []),
|
||||
...(batch ? [
|
||||
{ type: "divider" as const },
|
||||
{ key: "retry", icon: <RefreshCw className="size-3.5" />, label: "重试失败项", disabled: !hasFailedBatchItems, onClick: () => onRetryBatch(batch.id) },
|
||||
{ key: "stop", icon: <Square className="size-3.5" />, label: "停止剩余任务", disabled: !hasWaitingBatchItems, onClick: () => onStopBatch(batch.id) },
|
||||
{ key: "details", icon: <ListTree className="size-3.5" />, label: "查看批次详情", onClick: () => setBatchDetailsOpen(true) },
|
||||
] : []),
|
||||
];
|
||||
const submitPrompt = () => {
|
||||
const value = prompt.trim();
|
||||
if (value && node.metadata?.status !== "loading") onGenerateScript(value);
|
||||
@@ -135,27 +164,15 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-semibold" title={node.title || "分镜脚本"}>{node.title || "分镜脚本"}</span>
|
||||
{batchSummary ? <span className="min-w-0 max-w-[42%] truncate text-[var(--fs-label)] font-medium" title={batchSummary} style={{ color: batch?.status === "partial_failed" ? theme.accent.danger : theme.node.muted }}>{batchSummary}</span> : taskFeedback ? <span className="min-w-0 max-w-[38%] truncate text-[var(--fs-label)] font-medium" title={taskFeedback} style={{ color: node.metadata?.status === "error" ? theme.accent.danger : theme.node.muted }}>{taskFeedback}</span> : null}
|
||||
<span className="text-[var(--fs-caption)] font-semibold tabular-nums" style={{ color: theme.node.muted }}>{rows.length} 镜 · {totalDuration}s</span>
|
||||
{batch ? <>
|
||||
{hasFailedBatchItems ? <Tooltip title="重试失败项"><button type="button" className="grid size-7 place-items-center rounded outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onRetryBatch(batch.id); }} aria-label="重试失败项"><RefreshCw className="size-3.5" /></button></Tooltip> : null}
|
||||
{hasWaitingBatchItems ? <Tooltip title="停止剩余任务"><button type="button" className="grid size-7 place-items-center rounded outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onStopBatch(batch.id); }} aria-label="停止剩余任务"><Square className="size-3.5" /></button></Tooltip> : null}
|
||||
<Popover placement="bottomRight" trigger="click" content={<GenerationBatchDetails batch={batch} rows={rows} onRetryItem={(itemId) => onRetryBatchItem(batch.id, itemId)} onCancelItem={(itemId) => onCancelBatchItem(batch.id, itemId)} />}><Tooltip title="查看详情"><button type="button" className="grid size-7 place-items-center rounded outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()} aria-label="查看批次详情"><ListTree className="size-3.5" /></button></Tooltip></Popover>
|
||||
</> : null}
|
||||
{simpleMode ? null : <Tooltip title="生成动作拆分 12 宫格"><button type="button" disabled={!rows.length || hasActiveBatchItems} className="grid size-7 place-items-center rounded outline-none transition hover:bg-black/5 focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-30 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onCreateActionBoards(); }}><Grid3X3 className="size-3.5" /></button></Tooltip>}
|
||||
{simpleMode ? null : <Tooltip title="全屏编辑"><button type="button" className="grid size-7 place-items-center rounded outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onOpen(); }}><Expand className="size-3.5" /></button></Tooltip>}
|
||||
<Tooltip title="全屏编辑"><button type="button" className="grid size-7 place-items-center rounded outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onOpen(); }} aria-label="全屏编辑"><Expand className="size-3.5" /></button></Tooltip>
|
||||
<Dropdown menu={{ items: moreMenuItems }} trigger={["click"]} placement="bottomRight">
|
||||
<button type="button" className="grid size-7 place-items-center rounded outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()} aria-label="更多操作"><MoreHorizontal className="size-3.5" /></button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<StoryboardPipelineBar
|
||||
pipeline={pipeline}
|
||||
simpleMode={simpleMode}
|
||||
disabled={!rows.length || node.metadata?.status === "loading" || hasActiveBatchItems}
|
||||
theme={theme}
|
||||
onCreateImageNodes={onCreateImageNodes}
|
||||
onCreateVideoNodes={onCreateVideoNodes}
|
||||
onGenerateImages={onGenerateImages}
|
||||
onGenerateVideos={onGenerateVideos}
|
||||
videoInputMode={videoInputMode}
|
||||
onVideoInputModeChange={onVideoInputModeChange}
|
||||
onMergeVideos={onMergeVideos}
|
||||
/>
|
||||
{batch ? <Modal title="批次详情" open={batchDetailsOpen} onCancel={() => setBatchDetailsOpen(false)} footer={null} width={560} centered destroyOnHidden>
|
||||
<GenerationBatchDetails batch={batch} rows={rows} onRetryItem={(itemId) => onRetryBatchItem(batch.id, itemId)} onCancelItem={(itemId) => onCancelBatchItem(batch.id, itemId)} />
|
||||
</Modal> : null}
|
||||
<StoryboardMiniPipeline pipeline={pipeline} theme={theme} rows={rows} />
|
||||
<div className="grid h-9 shrink-0 items-center border-b text-xs font-semibold" style={{ borderColor: theme.node.stroke, color: theme.node.muted, gridTemplateColumns: SCRIPT_GRID_TEMPLATE }}>
|
||||
<HeaderCell borderColor={theme.node.stroke} align="center">序号</HeaderCell>
|
||||
<HeaderCell borderColor={theme.node.stroke} align="center">时长</HeaderCell>
|
||||
@@ -187,7 +204,14 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
|
||||
<button type="button" disabled={rows.length <= 1} className="grid size-7 place-items-center rounded outline-none opacity-55 transition enabled:hover:bg-red-500/10 enabled:hover:opacity-100 focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-20" style={{ color: theme.accent.danger, "--tw-ring-color": theme.accent.danger } as CSSProperties} title={rows.length <= 1 ? "至少保留一个镜头" : "删除镜头"} aria-label={`删除镜头 ${row.shotNumber}`} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onRemoveRow(row.id); }}><Trash2 className="size-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
)) : <button type="button" className="grid h-full min-h-24 w-full place-items-center text-sm" style={{ color: theme.node.muted }} onClick={(event) => { event.stopPropagation(); onAddRow(); }}>+ 添加第一个镜头</button>}
|
||||
)) : (
|
||||
<button type="button" className="grid h-full min-h-36 w-full place-items-center" onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onAddRow(); }}>
|
||||
<span className="flex flex-col items-center gap-2.5">
|
||||
<span className="text-sm font-bold">+ 添加第一个镜头</span>
|
||||
<span className="text-[var(--fs-label)] font-medium" style={{ color: theme.node.faint }}>可先连接「故事梗概 / 项目画风」节点,或在下方输入提示词一键生成分镜表</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex h-9 shrink-0 items-center justify-center border-b" style={{ borderColor: theme.node.stroke, background: theme.node.panel }}>
|
||||
<button type="button" className="inline-flex h-7 items-center gap-1 rounded px-2 text-xs font-medium outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onAddRow(); }}><Plus className="size-3.5" />添加行</button>
|
||||
@@ -233,26 +257,26 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{simpleMode ? <span className="text-[var(--fs-label)]" style={{ color: theme.node.muted }}>自动拆分 · 默认时长</span> : <Select<StoryboardShotCount>
|
||||
className="min-w-32"
|
||||
{simpleMode ? <span className="text-[var(--fs-label)]" style={{ color: theme.node.muted }}>自动拆分 · 时长自动</span> : <Select<StoryboardShotCount>
|
||||
className="min-w-24"
|
||||
size="small"
|
||||
value={shotCount}
|
||||
disabled={node.metadata?.status === "loading"}
|
||||
options={[{ value: "auto", label: "分镜数量:自动拆分" }, ...Array.from({ length: 10 }, (_, index) => ({ value: String(index + 1) as StoryboardShotCount, label: `分镜数量:${index + 1}` }))]}
|
||||
options={[{ value: "auto", label: "自动拆分" }, ...Array.from({ length: 10 }, (_, index) => ({ value: String(index + 1) as StoryboardShotCount, label: `${index + 1} 镜` }))]}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={onShotCountChange}
|
||||
/>}
|
||||
{simpleMode ? null : <Select<StoryboardShotDuration>
|
||||
className="min-w-36"
|
||||
className="min-w-24"
|
||||
size="small"
|
||||
value={shotDuration}
|
||||
disabled={node.metadata?.status === "loading"}
|
||||
options={[
|
||||
{ value: "auto", label: "镜头:自动拆分" },
|
||||
{ value: "5", label: "镜头:单个5S" },
|
||||
{ value: "10", label: "镜头:单个10S" },
|
||||
{ value: "15", label: "镜头:单个15S" },
|
||||
{ value: "30", label: "镜头:单个30S" },
|
||||
{ value: "auto", label: "时长自动" },
|
||||
{ value: "5", label: "5 秒" },
|
||||
{ value: "10", label: "10 秒" },
|
||||
{ value: "15", label: "15 秒" },
|
||||
{ value: "30", label: "30 秒" },
|
||||
]}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={onShotDurationChange}
|
||||
@@ -283,83 +307,31 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
|
||||
);
|
||||
}
|
||||
|
||||
function StoryboardPipelineBar({ pipeline, simpleMode, disabled, theme, videoInputMode, onCreateImageNodes, onCreateVideoNodes, onGenerateImages, onGenerateVideos, onVideoInputModeChange, onMergeVideos }: {
|
||||
pipeline: CanvasStoryboardPipelineProgress;
|
||||
simpleMode: boolean;
|
||||
disabled: boolean;
|
||||
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
|
||||
videoInputMode: StoryboardVideoInputMode;
|
||||
onCreateImageNodes: () => void;
|
||||
onCreateVideoNodes: () => void;
|
||||
onGenerateImages: () => void;
|
||||
onGenerateVideos: () => void;
|
||||
onVideoInputModeChange: (mode: StoryboardVideoInputMode) => void;
|
||||
onMergeVideos: () => void;
|
||||
}) {
|
||||
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;
|
||||
return (
|
||||
<div className="grid h-12 shrink-0 grid-cols-3 border-b" style={{ borderColor: theme.node.stroke, background: theme.node.fill }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<PipelineStageCell label={videoInputMode === "keyframe" ? "首帧" : "分镜图(可选)"} stage={pipeline.images} theme={theme}>
|
||||
{simpleMode ? (
|
||||
<Button size="small" type="text" icon={<ImageIcon className="size-3" />} disabled={disabled || pipeline.images.incomplete === 0} onClick={onGenerateImages}>
|
||||
{pipeline.images.incomplete ? `生成 ${pipeline.images.incomplete} 张${videoInputMode === "keyframe" ? "首帧" : "分镜图"}` : `${videoInputMode === "keyframe" ? "首帧" : "分镜图"}已完成`}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" type="text" disabled={disabled || missingImages === 0} onClick={onCreateImageNodes}>{missingImages ? `创建 ${missingImages} 个图片节点` : "图片节点已创建"}</Button>
|
||||
<Button size="small" type="text" disabled={disabled || pipeline.images.incomplete === 0} onClick={onGenerateImages}>生成未完成的图片</Button>
|
||||
</>
|
||||
)}
|
||||
</PipelineStageCell>
|
||||
<PipelineStageCell label="镜头视频" stage={pipeline.videos} theme={theme}>
|
||||
<Segmented<StoryboardVideoInputMode>
|
||||
size="small"
|
||||
value={videoInputMode}
|
||||
options={[{ value: "direct", label: "直接生成" }, { value: "keyframe", label: "先做首帧" }]}
|
||||
onChange={onVideoInputModeChange}
|
||||
/>
|
||||
{simpleMode ? (
|
||||
<Button size="small" type="text" icon={<Video className="size-3" />} disabled={disabled || pipeline.videos.incomplete === 0} onClick={onGenerateVideos}>
|
||||
{pipeline.videos.incomplete ? videoInputMode === "keyframe" ? "确认首帧并生成" : `生成 ${pipeline.videos.incomplete} 个镜头视频` : "镜头视频已完成"}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" type="text" disabled={disabled || missingVideos === 0} onClick={onCreateVideoNodes}>{missingVideos ? `创建 ${missingVideos} 个视频节点` : "视频节点已创建"}</Button>
|
||||
<Button size="small" type="text" disabled={disabled || pipeline.videos.incomplete === 0} onClick={onGenerateVideos}>{videoInputMode === "keyframe" ? "确认首帧并生成" : "生成未完成的视频"}</Button>
|
||||
</>
|
||||
)}
|
||||
</PipelineStageCell>
|
||||
<PipelineStageCell label="合并成片" stage={pipeline.final} theme={theme} last>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-7 max-w-full items-center justify-center gap-1.5 rounded-[var(--r-sm)] px-2 text-[var(--fs-tiny)] font-semibold transition-colors hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:hover:brightness-100"
|
||||
style={{
|
||||
background: canMerge ? theme.accent.primary : "transparent",
|
||||
color: canMerge ? "var(--primary-foreground)" : theme.node.faint,
|
||||
"--tw-ring-color": theme.accent.primary,
|
||||
"--tw-ring-offset-color": theme.node.fill,
|
||||
} as CSSProperties}
|
||||
disabled={!canMerge}
|
||||
onClick={onMergeVideos}
|
||||
>
|
||||
<Merge className="size-3 shrink-0" />
|
||||
{pipeline.final.success ? "成片已完成" : pipeline.successfulVideoNodeIds.length >= 2 ? `合并 ${pipeline.successfulVideoNodeIds.length} 段视频` : "至少完成 2 段视频"}
|
||||
</button>
|
||||
</PipelineStageCell>
|
||||
</div>
|
||||
);
|
||||
function storyboardStepState(stage: StoryboardPipelineStage): "done" | "current" | "error" | "idle" {
|
||||
if (stage.failed > 0 && stage.success === 0) return "error";
|
||||
if (stage.success > 0) return "done";
|
||||
if (stage.loading > 0 || stage.incomplete > 0) return "current";
|
||||
return "idle";
|
||||
}
|
||||
|
||||
function PipelineStageCell({ label, stage, theme, children, last = false }: { label: string; stage: StoryboardPipelineStage; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; children: ReactNode; last?: boolean }) {
|
||||
function StoryboardMiniPipeline({ pipeline, theme, rows }: { pipeline: CanvasStoryboardPipelineProgress; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; rows: StoryboardRow[] }) {
|
||||
const steps: Array<{ key: string; label: string; state: "done" | "current" | "error" | "idle"; hint: string }> = [
|
||||
{ key: "script", label: "分镜", state: rows.length > 0 ? "done" : "idle", hint: rows.length > 0 ? `${rows.length} 个镜头` : "待添加镜头" },
|
||||
{ key: "images", label: "分镜图(可选)", state: storyboardStepState(pipeline.images), hint: pipelineStatusLabel(pipeline.images) },
|
||||
{ key: "videos", label: "视频", state: storyboardStepState(pipeline.videos), hint: pipelineStatusLabel(pipeline.videos) },
|
||||
{ key: "final", label: "合并成片", state: pipeline.final.success > 0 ? "done" : pipeline.final.failed > 0 ? "error" : pipeline.final.loading > 0 || pipeline.successfulVideoNodeIds.length >= 2 ? "current" : "idle", hint: pipelineStatusLabel(pipeline.final) },
|
||||
];
|
||||
return (
|
||||
<div className={`flex min-w-0 items-center gap-2 px-3 ${last ? "" : "border-r"}`} style={{ borderColor: theme.node.stroke }}>
|
||||
<div className="min-w-[64px] shrink-0">
|
||||
<div className="text-[var(--fs-label)] font-semibold">{label}</div>
|
||||
<div className="text-[var(--fs-micro)] leading-3" style={{ color: stage.failed ? theme.accent.danger : theme.node.muted }}>{pipelineStatusLabel(stage)}</div>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-1 overflow-hidden [&_.ant-btn]:!h-7 [&_.ant-btn]:!px-2 [&_.ant-btn]:!text-[var(--fs-tiny)]">{children}</div>
|
||||
<div className="flex h-9 shrink-0 items-center justify-center overflow-hidden border-b px-4" style={{ borderColor: theme.node.stroke, background: theme.node.fill }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
{steps.map((step, index) => (
|
||||
<Fragment key={step.key}>
|
||||
{index > 0 ? <span className="mx-2.5 h-px min-w-3.5 flex-1 max-w-20" style={{ background: theme.node.stroke }} /> : null}
|
||||
<span className="flex items-center gap-1.5 whitespace-nowrap text-[var(--fs-tiny)]" title={step.hint} style={{ color: step.state === "done" ? theme.node.muted : step.state === "current" ? theme.accent.primary : step.state === "error" ? theme.accent.danger : theme.node.faint, fontWeight: step.state === "current" || step.state === "error" ? 700 : 500 }}>
|
||||
<span className="size-2 shrink-0 rounded-full" style={{ background: step.state === "done" ? theme.node.activeStroke : step.state === "current" ? theme.accent.primary : step.state === "error" ? theme.accent.danger : theme.node.stroke, boxShadow: step.state === "current" ? `0 0 0 3px ${theme.accent.primarySoft}` : undefined }} />
|
||||
{step.label}
|
||||
</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -488,8 +460,8 @@ function HeaderCell({ children, borderColor, align = "left" }: { children: React
|
||||
return <span className={`flex h-full items-center border-r px-4 ${align === "center" ? "justify-center text-center" : "justify-start"}`} style={{ borderColor }}>{children}</span>;
|
||||
}
|
||||
|
||||
function SmallButton({ title, children, onClick }: { title: string; children: ReactNode; onClick: () => void }) {
|
||||
return <button type="button" className="grid size-7 shrink-0 place-items-center rounded opacity-65 transition hover:bg-black/5 hover:opacity-100 dark:hover:bg-white/10" title={title} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onClick(); }}>{children}</button>;
|
||||
function SmallButton({ title, children, onClick, disabled }: { title: string; children: ReactNode; onClick: () => void; disabled?: boolean }) {
|
||||
return <button type="button" disabled={disabled} className="grid size-7 shrink-0 place-items-center rounded opacity-65 transition enabled:hover:bg-black/5 enabled:hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-25 dark:enabled:hover:bg-white/10" title={title} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onClick(); }}>{children}</button>;
|
||||
}
|
||||
|
||||
function editorRow(shotNumber: number): StoryboardRow {
|
||||
|
||||
@@ -1192,7 +1192,6 @@ function InfiniteCanvasPage() {
|
||||
}
|
||||
if (contentNode.type === CanvasNodeType.Script) {
|
||||
const pipeline = deriveStoryboardPipelineProgress(contentNode, nodesRef.current, connectionsRef.current);
|
||||
const rowIds = pipeline.rows.map((item) => item.row.id);
|
||||
return (
|
||||
<CanvasScriptNodeContent
|
||||
node={contentNode}
|
||||
@@ -1203,8 +1202,8 @@ function InfiniteCanvasPage() {
|
||||
onOpen={() => setScriptEditorNodeId(contentNode.id)}
|
||||
onCreateImageNodes={() => createScriptImageNodes(contentNode.id)}
|
||||
onCreateVideoNodes={() => createScriptVideoNodes(contentNode.id)}
|
||||
onGenerateImages={() => void generateScriptImages(contentNode.id, rowIds)}
|
||||
onGenerateVideos={() => (contentNode.metadata?.storyboardVideoInputMode === "keyframe" ? void generateScriptVideos(contentNode.id, rowIds) : void createAndGenerateScriptVideos(contentNode.id, rowIds))}
|
||||
onGenerateImages={(rowIds) => void generateScriptImages(contentNode.id, rowIds)}
|
||||
onGenerateVideos={(rowIds) => (contentNode.metadata?.storyboardVideoInputMode === "keyframe" ? void generateScriptVideos(contentNode.id, rowIds) : void createAndGenerateScriptVideos(contentNode.id, rowIds))}
|
||||
onVideoInputModeChange={(storyboardVideoInputMode) => handleConfigNodeChange(contentNode.id, { storyboardVideoInputMode })}
|
||||
onMergeVideos={() => void mergeVideosByIds(pipeline.successfulVideoNodeIds)}
|
||||
onCreateActionBoards={() => void createScriptActionBoards(contentNode.id)}
|
||||
|
||||
Reference in New Issue
Block a user