Simplify video workflow canvas UI
This commit is contained in:
parent
20d0529365
commit
021f497970
@ -643,11 +643,11 @@ const AGENT_RESULT_MODE_LABELS: Record<AgentGenerationResultModeType, string> =
|
||||
sku_replacement: "SKU 替换",
|
||||
};
|
||||
const VIDEO_CANVAS_PANEL_CLASS =
|
||||
"pointer-events-auto overflow-hidden rounded-[28px] border border-slate-200 bg-white text-foreground shadow-[0_20px_50px_-28px_rgba(15,23,42,0.24)] dark:border-zinc-800 dark:bg-zinc-950";
|
||||
"pointer-events-auto overflow-hidden rounded-[24px] border border-slate-200 bg-white text-foreground shadow-[0_16px_36px_-24px_rgba(15,23,42,0.2)] dark:border-zinc-800 dark:bg-zinc-950";
|
||||
const VIDEO_CANVAS_PANEL_BADGE_CLASS =
|
||||
"border-slate-200 bg-white text-foreground/75 dark:border-zinc-800 dark:bg-zinc-950";
|
||||
"border-slate-200 bg-slate-50 text-foreground/70 dark:border-zinc-800 dark:bg-zinc-900";
|
||||
const VIDEO_CANVAS_SUBSECTION_CLASS =
|
||||
"rounded-2xl border border-slate-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-950";
|
||||
"rounded-[18px] border border-slate-200 bg-slate-50 p-3 dark:border-zinc-800 dark:bg-zinc-900";
|
||||
const VIDEO_CANVAS_TOOLBAR_CLASS =
|
||||
"flex items-center gap-1.5 rounded-[28px] border border-slate-200 bg-white px-2.5 py-1.5 shadow-[0_18px_40px_-24px_rgba(15,23,42,0.24)] dark:border-zinc-800 dark:bg-zinc-950";
|
||||
const VIDEO_CANVAS_TOOLBAR_BUTTON_CLASS =
|
||||
@ -1088,6 +1088,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
const autoTitleInFlightRef = useRef<string | null>(null);
|
||||
const autoThumbnailSyncedRef = useRef<string | null>(null);
|
||||
const lastLoadedCanvasIdRef = useRef<string>("");
|
||||
const normalizedVideoNodeSizeIdsRef = useRef<Set<string>>(new Set());
|
||||
const importVideoCanvasInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const isAgentWorkspaceEnabled = siteSettingsData?.agentEnabled !== false;
|
||||
const isCanvasAgentModeEnabled = siteSettingsData?.canvasAgentModeEnabled !== false;
|
||||
@ -1112,6 +1113,38 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
[siteSettingsData?.agentWorkflowBrandModelConfig]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVideoMode || nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (normalizedVideoNodeSizeIdsRef.current.has(node.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
normalizedVideoNodeSizeIdsRef.current.add(node.id);
|
||||
const templateType = inferVideoWorkflowTemplateType(node);
|
||||
const definition = templateType ? getVideoWorkflowNodeDefinition(templateType) : null;
|
||||
if (!definition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextWidth = Math.min(node.size.width, definition.size.width);
|
||||
const nextHeight = Math.min(node.size.height, definition.size.height);
|
||||
if (nextWidth === node.size.width && nextHeight === node.size.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateNode(node.id, {
|
||||
size: {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
},
|
||||
});
|
||||
});
|
||||
}, [isVideoMode, nodes, updateNode]);
|
||||
|
||||
// 获取选中的图片节点
|
||||
const selectedImageNode = selectedNodeIds.length === 1
|
||||
? nodes.find(n => n.id === selectedNodeIds[0] && n.type === 'image')
|
||||
@ -1185,10 +1218,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
: null;
|
||||
const videoModuleSidebarWidth = isVideoMode
|
||||
? isVideoModuleSidebarCollapsed
|
||||
? 64
|
||||
: 296
|
||||
? 56
|
||||
: 228
|
||||
: 0;
|
||||
const selectedWorkflowPanelLeft = isVideoMode ? videoModuleSidebarWidth + 28 : 16;
|
||||
const selectedWorkflowPanelLeft = isVideoMode ? videoModuleSidebarWidth + 24 : 16;
|
||||
const handleDeleteSelectedWorkflowNode = useCallback(() => {
|
||||
if (!selectedVideoWorkflowNode) return;
|
||||
deleteNode(selectedVideoWorkflowNode.id);
|
||||
@ -7538,20 +7571,20 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
style={{ width: `${videoModuleSidebarWidth}px` }}
|
||||
>
|
||||
<div className={cn(
|
||||
"overflow-hidden rounded-[28px] border border-slate-200 bg-white shadow-[0_24px_60px_-30px_rgba(15,23,42,0.18)] dark:border-zinc-800 dark:bg-zinc-950",
|
||||
isVideoModuleSidebarCollapsed ? "w-16" : "w-[296px]"
|
||||
"overflow-hidden rounded-[24px] border border-slate-200 bg-white shadow-[0_18px_40px_-28px_rgba(15,23,42,0.18)] dark:border-zinc-800 dark:bg-zinc-950",
|
||||
isVideoModuleSidebarCollapsed ? "w-14" : "w-[228px]"
|
||||
)}>
|
||||
<div className="flex items-center justify-between border-b border-slate-200 px-3 py-3 dark:border-zinc-800">
|
||||
<div className="flex items-center justify-between border-b border-slate-200 px-3 py-2.5 dark:border-zinc-800">
|
||||
{isVideoModuleSidebarCollapsed ? (
|
||||
<div className="flex w-full justify-center">
|
||||
<Layers className="h-5 w-5 text-slate-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-foreground">视频模块栏</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
按工作流层级添加模块
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-2xl border border-slate-200 bg-slate-50 text-slate-500 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300">
|
||||
<Layers className="h-4 w-4" />
|
||||
</span>
|
||||
<div className="text-sm font-semibold text-foreground">模块</div>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
@ -7571,7 +7604,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
|
||||
{!isVideoModuleSidebarCollapsed ? (
|
||||
<ScrollArea className="max-h-[calc(100vh-164px)]">
|
||||
<div className="space-y-3 p-3">
|
||||
<div className="space-y-2 p-2">
|
||||
{VIDEO_CANVAS_SIDEBAR_GROUPS.map((group) => {
|
||||
const GroupIcon = group.icon;
|
||||
return (
|
||||
@ -7585,31 +7618,31 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="rounded-2xl border border-slate-200 bg-slate-50 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<span className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300">
|
||||
<div className="rounded-[20px] border border-slate-200 bg-slate-50 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between gap-3 px-3 py-2.5 text-left">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-2xl border border-slate-200 bg-white text-slate-500 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300">
|
||||
<GroupIcon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm font-semibold text-foreground">
|
||||
{group.label}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-muted-foreground">
|
||||
{group.description}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
|
||||
{group.label}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
|
||||
videoModuleSectionOpen[group.id] !== false ? "rotate-0" : "-rotate-90"
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{group.nodeTypes.length}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
|
||||
videoModuleSectionOpen[group.id] !== false ? "rotate-0" : "-rotate-90"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="border-t border-slate-200 px-3 py-3 dark:border-zinc-800">
|
||||
<div className="grid gap-2">
|
||||
<div className="border-t border-slate-200 px-2 pb-2 pt-2 dark:border-zinc-800">
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{group.nodeTypes.map((type) => {
|
||||
const definition = getVideoWorkflowNodeDefinition(type);
|
||||
if (!definition) return null;
|
||||
@ -7619,21 +7652,17 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
key={type}
|
||||
type="button"
|
||||
onClick={() => handleAddWorkflowNode(type)}
|
||||
className="flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-left transition-colors hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900"
|
||||
className="flex items-center gap-2 rounded-2xl border border-slate-200 bg-white px-2.5 py-2 text-left transition-colors hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900"
|
||||
title={definition.label}
|
||||
>
|
||||
<span
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-slate-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300"
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-slate-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300"
|
||||
style={{ color: definition.accentColor }}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm font-medium text-foreground">
|
||||
{definition.label}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
|
||||
{definition.description}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-xs font-medium text-foreground">
|
||||
{definition.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
@ -7648,7 +7677,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 px-2 py-3">
|
||||
<div className="flex flex-col items-center gap-2 px-2 py-2">
|
||||
{VIDEO_CANVAS_SIDEBAR_GROUPS.map((group) => {
|
||||
const GroupIcon = group.icon;
|
||||
return (
|
||||
@ -7662,7 +7691,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
[group.id]: true,
|
||||
}));
|
||||
}}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-slate-200 bg-white text-xs font-semibold text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-800 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-2xl border border-slate-200 bg-white text-xs font-semibold text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-800 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
|
||||
title={group.label}
|
||||
>
|
||||
<GroupIcon className="h-4 w-4" />
|
||||
@ -7767,25 +7796,25 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
selectedVideoWorkflowDefinition &&
|
||||
selectedWorkflowIsActionable ? (
|
||||
<div
|
||||
className="pointer-events-none absolute top-16 z-20 flex w-[min(480px,calc(100vw-32px))] flex-col gap-3"
|
||||
className="pointer-events-none absolute top-16 z-20 flex w-[min(388px,calc(100vw-32px))] flex-col gap-3"
|
||||
style={{ left: `${selectedWorkflowPanelLeft}px` }}
|
||||
>
|
||||
{selectedWorkflowIsActionable ? (
|
||||
<Card className={VIDEO_CANVAS_PANEL_CLASS}>
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">工作流执行</CardTitle>
|
||||
<CardDescription className="mt-1 line-clamp-2">
|
||||
<CardTitle className="text-sm">{selectedVideoWorkflowDefinition.label}</CardTitle>
|
||||
<CardDescription className="mt-1 line-clamp-1 text-xs">
|
||||
{selectedWorkflowActionHint}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline" className={VIDEO_CANVAS_PANEL_BADGE_CLASS}>
|
||||
Phase 4
|
||||
执行
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pb-5">
|
||||
<CardContent className="space-y-3 pb-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className={VIDEO_CANVAS_PANEL_BADGE_CLASS}>
|
||||
提示词 {selectedWorkflowInputs.promptCount}
|
||||
@ -7847,10 +7876,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
</div>
|
||||
|
||||
<div className={VIDEO_CANVAS_SUBSECTION_CLASS}>
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
|
||||
上游提示
|
||||
</div>
|
||||
<div className="mt-2 line-clamp-4 whitespace-pre-line text-sm leading-6 text-foreground">
|
||||
<div className="line-clamp-3 whitespace-pre-line text-xs leading-5 text-foreground/85">
|
||||
{selectedWorkflowPrompt.trim()
|
||||
? selectedWorkflowPrompt
|
||||
: "当前还没有抽取到可执行提示词,先在上游接入脚本、分镜或参考素材。"}
|
||||
@ -7866,9 +7892,6 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
<div className="mt-2 text-sm font-medium text-foreground">
|
||||
{selectedWorkflowLinkedAgentProject.title || selectedWorkflowLinkedAgentProject.id}
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
再点下方按钮会同步当前视频画布上下文,并直接打开该项目。
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@ -79,6 +79,16 @@ const STATUS_LABELS = {
|
||||
failed: "异常",
|
||||
} as const;
|
||||
|
||||
const STATUS_CLASS_MAP = {
|
||||
pending: "border-slate-200 bg-white text-foreground/70 dark:border-zinc-800 dark:bg-zinc-950",
|
||||
generating:
|
||||
"border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/40 dark:bg-blue-950/40 dark:text-blue-200",
|
||||
completed:
|
||||
"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/40 dark:bg-emerald-950/40 dark:text-emerald-200",
|
||||
failed:
|
||||
"border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/40 dark:bg-rose-950/40 dark:text-rose-200",
|
||||
} as const;
|
||||
|
||||
export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
|
||||
const deleteNode = useCanvasStore((state) => state.deleteNode);
|
||||
const data = node.data as WorkflowNodeData;
|
||||
@ -92,42 +102,36 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
|
||||
const groupMeta = GROUP_META[resolvedGroup] || GROUP_META.tool;
|
||||
const GroupIcon = groupMeta.icon;
|
||||
const accentColor = data.accentColor || definition?.accentColor || "#e2e8f0";
|
||||
const accentBarColor = toAlphaColor(accentColor, 0.92) || accentColor;
|
||||
const title = data.title || definition?.label || "工作流节点";
|
||||
const description = data.description || definition?.description || "等待补充节点配置。";
|
||||
const previewUrl =
|
||||
String(metadata.previewMediaUrl || metadata.resultMediaUrl || "").trim();
|
||||
const statusLabel = STATUS_LABELS[data.status] || STATUS_LABELS.pending;
|
||||
const statusClassName =
|
||||
STATUS_CLASS_MAP[data.status] || STATUS_CLASS_MAP.pending;
|
||||
|
||||
const previewUrl = String(metadata.previewMediaUrl || metadata.resultMediaUrl || "").trim();
|
||||
const previewKind =
|
||||
String(metadata.previewMediaKind || metadata.resultMediaKind || "").trim() === "video"
|
||||
? "video"
|
||||
: "image";
|
||||
const compareItems = Array.isArray(metadata.compareItems)
|
||||
? metadata.compareItems.slice(0, 4)
|
||||
: [];
|
||||
const storyboardShots = Array.isArray(metadata.storyboardShots)
|
||||
? metadata.storyboardShots.slice(0, 4)
|
||||
: [];
|
||||
const analysisHighlights = Array.isArray(metadata.analysisHighlights)
|
||||
? metadata.analysisHighlights.slice(0, 4)
|
||||
: [];
|
||||
const compareItems = Array.isArray(metadata.compareItems)
|
||||
? metadata.compareItems.slice(0, 4)
|
||||
: [];
|
||||
const tableColumns = Array.isArray(metadata.tableColumns)
|
||||
? metadata.tableColumns.slice(0, 4)
|
||||
: [];
|
||||
const tableRows = Array.isArray(metadata.tableRows)
|
||||
? metadata.tableRows.slice(0, 4)
|
||||
: [];
|
||||
const characters = Array.isArray(metadata.characters)
|
||||
? metadata.characters.slice(0, 3)
|
||||
: [];
|
||||
const scenes = Array.isArray(metadata.scenes) ? metadata.scenes.slice(0, 3) : [];
|
||||
const tableRows = Array.isArray(metadata.tableRows) ? metadata.tableRows.slice(0, 4) : [];
|
||||
const characters = Array.isArray(metadata.characters) ? metadata.characters.slice(0, 4) : [];
|
||||
const scenes = Array.isArray(metadata.scenes) ? metadata.scenes.slice(0, 4) : [];
|
||||
const characterAssets = Array.isArray(metadata.characterAssets)
|
||||
? metadata.characterAssets.slice(0, 3)
|
||||
? metadata.characterAssets.slice(0, 4)
|
||||
: [];
|
||||
const sceneAssets = Array.isArray(metadata.sceneAssets)
|
||||
? metadata.sceneAssets.slice(0, 3)
|
||||
: [];
|
||||
const agentPlan = Array.isArray(metadata.agentPlan)
|
||||
? metadata.agentPlan.slice(0, 4)
|
||||
? metadata.sceneAssets.slice(0, 4)
|
||||
: [];
|
||||
const agentPlan = Array.isArray(metadata.agentPlan) ? metadata.agentPlan.slice(0, 4) : [];
|
||||
const deliverables = Array.isArray(metadata.deliverables)
|
||||
? metadata.deliverables.slice(0, 4)
|
||||
: [];
|
||||
@ -135,432 +139,209 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
|
||||
const linkedAgentProjectTitle = String(metadata.linkedAgentProjectTitle || "").trim();
|
||||
const importedAgentResultCount = Math.max(Number(metadata.importedAgentResultCount || 0), 0);
|
||||
const pendingAgentResultCount = Math.max(Number(metadata.pendingAgentResultCount || 0), 0);
|
||||
const agentResultGroups = Array.isArray(metadata.agentResultGroups)
|
||||
? metadata.agentResultGroups.slice(0, 4)
|
||||
: [];
|
||||
const batchShotCount = Math.max(
|
||||
Number(metadata.batchShotCount || 0),
|
||||
storyboardShots.length
|
||||
);
|
||||
const batchResultCount = Number(metadata.batchResultCount || 0);
|
||||
const batchFailedCount = Number(metadata.batchFailedCount || 0);
|
||||
const batchMode =
|
||||
String(metadata.batchMode || "").trim() === "gen-video" ? "视频批量" : "图片批量";
|
||||
const sectionClassName =
|
||||
"rounded-2xl border border-slate-200 bg-white p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.78)] dark:border-zinc-800 dark:bg-zinc-950";
|
||||
const mutedKickerClassName =
|
||||
"text-[11px] uppercase tracking-[0.18em] text-muted-foreground/80";
|
||||
const itemClassName =
|
||||
"rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900";
|
||||
const badgeClassName =
|
||||
"border-slate-200 bg-white text-foreground/80 dark:border-zinc-800 dark:bg-zinc-950";
|
||||
const accentBarColor = toAlphaColor(accentColor, 0.9) || accentColor;
|
||||
const batchResultCount = Math.max(Number(metadata.batchResultCount || 0), 0);
|
||||
const batchFailedCount = Math.max(Number(metadata.batchFailedCount || 0), 0);
|
||||
|
||||
const comparePreviewItems = compareItems
|
||||
.map((item, index) => {
|
||||
const compareItem = (item || {}) as Record<string, any>;
|
||||
const url = String(compareItem.url || "").trim();
|
||||
if (!url) return null;
|
||||
return {
|
||||
id: `${data.templateType}-compare-${index}`,
|
||||
url,
|
||||
kind: String(compareItem.kind || "").trim() === "video" ? "video" : "image",
|
||||
label: String(compareItem.label || compareItem.title || `对比 ${index + 1}`).trim(),
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as Array<{ id: string; url: string; kind: "image" | "video"; label: string }>;
|
||||
|
||||
const compactMetrics: string[] = [];
|
||||
if (batchShotCount > 0) compactMetrics.push(`镜头 ${batchShotCount}`);
|
||||
if (batchResultCount > 0) compactMetrics.push(`结果 ${batchResultCount}`);
|
||||
if (analysisHighlights.length > 0) compactMetrics.push(`分析 ${analysisHighlights.length}`);
|
||||
if (tableRows.length > 0) compactMetrics.push(`表格 ${tableRows.length}`);
|
||||
if (characters.length > 0) compactMetrics.push(`角色 ${characters.length}`);
|
||||
if (scenes.length > 0) compactMetrics.push(`场景 ${scenes.length}`);
|
||||
if (characterAssets.length > 0) compactMetrics.push(`角色资产 ${characterAssets.length}`);
|
||||
if (sceneAssets.length > 0) compactMetrics.push(`场景资产 ${sceneAssets.length}`);
|
||||
if (linkedAgentProjectId) compactMetrics.push("Agent");
|
||||
if (importedAgentResultCount > 0) compactMetrics.push(`已回流 ${importedAgentResultCount}`);
|
||||
if (pendingAgentResultCount > 0) compactMetrics.push(`待回流 ${pendingAgentResultCount}`);
|
||||
if (batchFailedCount > 0) compactMetrics.push(`失败 ${batchFailedCount}`);
|
||||
|
||||
const primaryTextCandidates = [
|
||||
String(data.summary || "").trim(),
|
||||
String(analysisHighlights[0] || "").trim(),
|
||||
String((storyboardShots[0] as Record<string, any>)?.title || "").trim(),
|
||||
String((storyboardShots[0] as Record<string, any>)?.description || "").trim(),
|
||||
String((characters[0] as Record<string, any>)?.description || "").trim(),
|
||||
String((scenes[0] as Record<string, any>)?.description || "").trim(),
|
||||
String(agentPlan[0] || "").trim(),
|
||||
String(deliverables[0] || "").trim(),
|
||||
quickConnectOutputs.length > 0 ? `可接 ${quickConnectOutputs.map((item) => item.label).join(" / ")}` : "",
|
||||
String(definition?.description || data.description || "").trim(),
|
||||
].filter(Boolean);
|
||||
const primaryText = primaryTextCandidates[0] || "";
|
||||
|
||||
const compactDetailLines = [
|
||||
storyboardShots[0]
|
||||
? `分镜:${String((storyboardShots[0] as Record<string, any>)?.title || "镜头草案").trim()}`
|
||||
: "",
|
||||
characters[0]
|
||||
? `角色:${String((characters[0] as Record<string, any>)?.name || (characters[0] as Record<string, any>)?.title || "角色线索").trim()}`
|
||||
: "",
|
||||
scenes[0]
|
||||
? `场景:${String((scenes[0] as Record<string, any>)?.name || (scenes[0] as Record<string, any>)?.title || "场景线索").trim()}`
|
||||
: "",
|
||||
tableRows[0] ? "已整理结构化表格" : "",
|
||||
linkedAgentProjectId ? `Agent:${linkedAgentProjectTitle || linkedAgentProjectId}` : "",
|
||||
deliverables[0] ? `交付:${String(deliverables[0]).trim()}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full w-full overflow-hidden rounded-[28px] border border-slate-200 bg-white shadow-[0_22px_55px_-30px_rgba(15,23,42,0.18)] transition-all dark:border-zinc-800 dark:bg-zinc-950",
|
||||
"relative h-full w-full overflow-hidden rounded-[22px] border border-slate-200 bg-white shadow-[0_18px_38px_-26px_rgba(15,23,42,0.18)] transition-all dark:border-zinc-800 dark:bg-zinc-950",
|
||||
isSelected &&
|
||||
"border-primary/55 ring-2 ring-primary/15 shadow-[0_24px_56px_-30px_rgba(59,130,246,0.24)]"
|
||||
"border-primary/60 ring-2 ring-primary/15 shadow-[0_20px_42px_-24px_rgba(59,130,246,0.24)]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-1"
|
||||
style={{ backgroundColor: accentBarColor }}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-x-0 top-1 h-px bg-slate-200 dark:bg-zinc-800" />
|
||||
|
||||
<div className="relative flex h-full flex-col gap-4 p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<GroupIcon className="h-4 w-4" />
|
||||
</span>
|
||||
<span>{groupMeta.label}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold leading-tight text-foreground">{title}</h3>
|
||||
<p className="mt-1 line-clamp-2 text-sm leading-6 text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
{isSelected ? (
|
||||
<button
|
||||
type="button"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
deleteNode(node.id);
|
||||
}}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-slate-200 bg-white text-muted-foreground transition-colors hover:border-destructive/30 hover:bg-red-50 hover:text-destructive dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-destructive/40 dark:hover:bg-red-950/30"
|
||||
aria-label="删除模块"
|
||||
title="删除模块"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
<Badge variant="outline" className={badgeClassName}>
|
||||
{STATUS_LABELS[data.status] || STATUS_LABELS.pending}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full flex-col gap-3 p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-slate-200 bg-slate-50 shadow-sm dark:border-zinc-800 dark:bg-zinc-900"
|
||||
style={{ color: accentColor }}
|
||||
>
|
||||
<GroupIcon className="h-4 w-4" />
|
||||
</span>
|
||||
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>节点标识</div>
|
||||
<div className="mt-1 font-mono text-xs text-foreground/80">{data.templateType}</div>
|
||||
<p className="mt-2 line-clamp-3 text-sm leading-6 text-muted-foreground">
|
||||
{data.summary || "接在上游素材之后,用来处理或输出结果。"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
{groupMeta.label}
|
||||
</span>
|
||||
<Badge variant="outline" className={cn("h-6 rounded-full px-2 text-[11px]", statusClassName)}>
|
||||
{statusLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
<h3 className="truncate text-sm font-semibold text-foreground">{title}</h3>
|
||||
{primaryText ? (
|
||||
<p className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
|
||||
{primaryText}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{batchShotCount > 0 || batchResultCount > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>批量执行</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{batchShotCount > 0 ? (
|
||||
<Badge variant="outline" className={badgeClassName}>
|
||||
镜头 {batchShotCount}
|
||||
</Badge>
|
||||
) : null}
|
||||
{batchResultCount > 0 ? (
|
||||
<Badge variant="outline" className={badgeClassName}>
|
||||
结果 {batchResultCount}
|
||||
</Badge>
|
||||
) : null}
|
||||
{batchFailedCount > 0 ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-amber-200/70 bg-amber-50 text-amber-700"
|
||||
{isSelected ? (
|
||||
<button
|
||||
type="button"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
deleteNode(node.id);
|
||||
}}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white text-muted-foreground transition-colors hover:border-destructive/30 hover:bg-red-50 hover:text-destructive dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-destructive/40 dark:hover:bg-red-950/30"
|
||||
aria-label="删除模块"
|
||||
title="删除模块"
|
||||
>
|
||||
失败 {batchFailedCount}
|
||||
</Badge>
|
||||
) : null}
|
||||
{batchResultCount > 0 ? (
|
||||
<Badge variant="outline" className={badgeClassName}>
|
||||
{batchMode}
|
||||
</Badge>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{previewUrl ? (
|
||||
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-slate-950 shadow-[0_16px_40px_-28px_rgba(15,23,42,0.65)] dark:border-zinc-800">
|
||||
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-slate-950 dark:border-zinc-800">
|
||||
{previewKind === "video" ? (
|
||||
<video
|
||||
src={previewUrl}
|
||||
className="h-36 w-full object-cover"
|
||||
className="h-28 w-full object-cover"
|
||||
controls
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={title}
|
||||
className="h-36 w-full object-cover"
|
||||
/>
|
||||
<img src={previewUrl} alt={title} className="h-28 w-full object-cover" />
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{compareItems.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>对比结果</div>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
{compareItems.map((item, index) => {
|
||||
const compareItem = (item || {}) as Record<string, any>;
|
||||
const kind =
|
||||
String(compareItem.kind || "").trim() === "video" ? "video" : "image";
|
||||
const url = String(compareItem.url || "").trim();
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={`${data.templateType}-compare-${index}`}
|
||||
className="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-zinc-800 dark:bg-zinc-950"
|
||||
>
|
||||
<div className="border-b border-slate-200 px-3 py-2 dark:border-zinc-800">
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
|
||||
{String(compareItem.label || `对比 ${index + 1}`)}
|
||||
</div>
|
||||
<div className="mt-1 truncate text-xs font-medium text-foreground/90">
|
||||
{String(compareItem.title || `素材 ${index + 1}`)}
|
||||
</div>
|
||||
</div>
|
||||
{kind === "video" ? (
|
||||
<video
|
||||
src={url}
|
||||
className="h-24 w-full object-cover"
|
||||
controls
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={url}
|
||||
alt={String(compareItem.title || `对比素材 ${index + 1}`)}
|
||||
className="h-24 w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : comparePreviewItems.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{comparePreviewItems.slice(0, 4).map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-zinc-800 dark:bg-zinc-950"
|
||||
>
|
||||
{item.kind === "video" ? (
|
||||
<video
|
||||
src={item.url}
|
||||
className="h-20 w-full object-cover"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
<img src={item.url} alt={item.label} className="h-20 w-full object-cover" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{storyboardShots.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>镜头草案</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{storyboardShots.map((item, index) => {
|
||||
const shot = (item || {}) as Record<string, any>;
|
||||
return (
|
||||
<div key={`${data.templateType}-shot-${index}`} className={itemClassName}>
|
||||
<div className="text-xs font-semibold text-foreground/90">
|
||||
{String(shot.title || `镜头 ${index + 1}`)}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
|
||||
{String(shot.description || "")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{compactMetrics.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{compactMetrics.slice(0, 6).map((metric) => (
|
||||
<span
|
||||
key={`${data.templateType}-${metric}`}
|
||||
className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 text-[11px] font-medium text-foreground/80 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200"
|
||||
>
|
||||
{metric}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tableColumns.length > 0 && tableRows.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>结构化表格</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{tableColumns.map((item, index) => {
|
||||
const column = (item || {}) as Record<string, any>;
|
||||
return (
|
||||
<Badge
|
||||
key={`${data.templateType}-column-${index}`}
|
||||
variant="outline"
|
||||
className={badgeClassName}
|
||||
>
|
||||
{String(column.label || column.key || `列 ${index + 1}`)}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{tableRows.map((item, index) => {
|
||||
const row = (item || {}) as Record<string, any>;
|
||||
return (
|
||||
<div key={`${data.templateType}-row-${index}`} className={itemClassName}>
|
||||
{tableColumns.map((columnItem, columnIndex) => {
|
||||
const column = (columnItem || {}) as Record<string, any>;
|
||||
const columnKey = String(column.key || `col-${columnIndex}`).trim();
|
||||
const label = String(column.label || columnKey || `列 ${columnIndex + 1}`).trim();
|
||||
const value = String(row[columnKey] || "").trim();
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={`${data.templateType}-row-${index}-column-${columnIndex}`}
|
||||
className={columnIndex > 0 ? "mt-2" : ""}
|
||||
>
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
|
||||
{label}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-3 text-xs leading-5 text-foreground/85">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{compactDetailLines.length > 0 ? (
|
||||
<div className="grid gap-1.5">
|
||||
{compactDetailLines.map((line) => (
|
||||
<div
|
||||
key={`${data.templateType}-${line}`}
|
||||
className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs leading-5 text-foreground/80 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200"
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{analysisHighlights.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>分析摘要</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{analysisHighlights.map((item, index) => (
|
||||
<div
|
||||
key={`${data.templateType}-analysis-${index}`}
|
||||
className={cn(itemClassName, "line-clamp-3 text-xs leading-5 text-muted-foreground")}
|
||||
>
|
||||
{String(item || "")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{characters.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>角色线索</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{characters.map((item, index) => {
|
||||
const character = (item || {}) as Record<string, any>;
|
||||
return (
|
||||
<div key={`${data.templateType}-character-${index}`} className={itemClassName}>
|
||||
<div className="text-xs font-semibold text-foreground/90">
|
||||
{String(character.name || character.title || `角色 ${index + 1}`)}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-3 text-xs leading-5 text-muted-foreground">
|
||||
{String(character.description || character.prompt || "")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{scenes.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>场景线索</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{scenes.map((item, index) => {
|
||||
const scene = (item || {}) as Record<string, any>;
|
||||
return (
|
||||
<div key={`${data.templateType}-scene-${index}`} className={itemClassName}>
|
||||
<div className="text-xs font-semibold text-foreground/90">
|
||||
{String(scene.name || scene.title || `场景 ${index + 1}`)}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-3 text-xs leading-5 text-muted-foreground">
|
||||
{String(scene.description || scene.prompt || "")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{characterAssets.length > 0 || sceneAssets.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>资产沉淀</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{[...characterAssets, ...sceneAssets].slice(0, 4).map((item, index) => {
|
||||
const asset = (item || {}) as Record<string, any>;
|
||||
return (
|
||||
<div key={`${data.templateType}-asset-${index}`} className={itemClassName}>
|
||||
<div className="text-xs font-semibold text-foreground/90">
|
||||
{String(asset.name || asset.title || `资产 ${index + 1}`)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
{String(asset.description || asset.prompt || "")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{agentPlan.length > 0 || deliverables.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>代理计划</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{agentPlan.map((item, index) => (
|
||||
<div
|
||||
key={`${data.templateType}-plan-${index}`}
|
||||
className={cn(itemClassName, "text-xs leading-5 text-muted-foreground")}
|
||||
>
|
||||
{String(item || "")}
|
||||
</div>
|
||||
))}
|
||||
{deliverables.map((item, index) => (
|
||||
<div
|
||||
key={`${data.templateType}-deliverable-${index}`}
|
||||
className="rounded-xl border border-dashed border-border/60 bg-background px-3 py-2 text-xs leading-5 text-muted-foreground"
|
||||
>
|
||||
交付物: {String(item || "")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{linkedAgentProjectId ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>Agent 工作台</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className={badgeClassName}>
|
||||
已连接
|
||||
</Badge>
|
||||
{importedAgentResultCount > 0 ? (
|
||||
<Badge variant="outline" className={badgeClassName}>
|
||||
已回流 {importedAgentResultCount}
|
||||
</Badge>
|
||||
) : null}
|
||||
{pendingAgentResultCount > 0 ? (
|
||||
<Badge variant="outline" className={badgeClassName}>
|
||||
待回流 {pendingAgentResultCount}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 text-xs font-medium leading-5 text-foreground/90">
|
||||
{linkedAgentProjectTitle || linkedAgentProjectId}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{agentResultGroups.length > 0 ? (
|
||||
<div className={sectionClassName}>
|
||||
<div className={mutedKickerClassName}>Agent 结果</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{agentResultGroups.map((item, index) => {
|
||||
const group = (item || {}) as Record<string, any>;
|
||||
const label = String(group.label || `结果组 ${index + 1}`).trim();
|
||||
const count = Math.max(Number(group.count || 0), 0);
|
||||
const importedCount = Math.max(Number(group.importedCount || 0), 0);
|
||||
return (
|
||||
<div
|
||||
key={`${data.templateType}-agent-group-${index}`}
|
||||
className={itemClassName}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-xs font-semibold text-foreground/90">{label}</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{importedCount}/{count}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
共 {count} 个结果,已回流 {importedCount} 个。
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-auto space-y-2">
|
||||
<div className={mutedKickerClassName}>下一步推荐</div>
|
||||
<div className="mt-auto flex flex-wrap gap-1.5">
|
||||
{quickConnectOutputs.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{quickConnectOutputs.slice(0, 4).map((item) => (
|
||||
<span
|
||||
key={`${data.templateType}-${item.type}`}
|
||||
className="rounded-full border border-border/60 bg-background px-2.5 py-1 text-xs text-foreground/80 shadow-sm"
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
quickConnectOutputs.slice(0, 4).map((item) => (
|
||||
<span
|
||||
key={`${data.templateType}-${item.type}`}
|
||||
className="rounded-full border border-dashed border-slate-200 px-2.5 py-1 text-[11px] text-muted-foreground dark:border-zinc-800"
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-full border border-dashed border-border/70 bg-background px-3 py-1.5 text-xs text-muted-foreground">
|
||||
当前节点通常作为流程终点或独立处理节点
|
||||
</div>
|
||||
<span className="rounded-full border border-dashed border-slate-200 px-2.5 py-1 text-[11px] text-muted-foreground dark:border-zinc-800">
|
||||
结束节点
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -126,6 +126,86 @@ const SECTION_DEFINITIONS: VideoWorkflowSectionDefinition[] = [
|
||||
{ id: "output", label: "输出", description: "预览、对比和保存结果" },
|
||||
];
|
||||
|
||||
const COMPACT_NODE_SIZE_OVERRIDES: Partial<
|
||||
Record<VideoWorkflowTemplateType, { width: number; height: number }>
|
||||
> = {
|
||||
"text-node": { width: 300, height: 170 },
|
||||
"novel-input": { width: 340, height: 240 },
|
||||
"input-image": { width: 220, height: 220 },
|
||||
"video-input": { width: 240, height: 220 },
|
||||
"video-analyze": { width: 300, height: 210 },
|
||||
"extract-characters-scenes": { width: 300, height: 210 },
|
||||
"storyboard-node": { width: 380, height: 240 },
|
||||
"storyboard-chart-node": { width: 360, height: 220 },
|
||||
"table-editor-node": { width: 340, height: 220 },
|
||||
"gen-image": { width: 280, height: 190 },
|
||||
"gen-video": { width: 280, height: 190 },
|
||||
"gen-music": { width: 300, height: 200 },
|
||||
"jimeng-super-resolution": { width: 260, height: 160 },
|
||||
"motion-control": { width: 300, height: 220 },
|
||||
"inpaint-crop": { width: 280, height: 180 },
|
||||
"inpaint-stitch": { width: 280, height: 180 },
|
||||
"professional-camera": { width: 280, height: 180 },
|
||||
"camera-movement": { width: 280, height: 180 },
|
||||
"image-compare": { width: 320, height: 220 },
|
||||
preview: { width: 320, height: 220 },
|
||||
"local-save": { width: 260, height: 180 },
|
||||
"character-description": { width: 300, height: 180 },
|
||||
"scene-description": { width: 300, height: 180 },
|
||||
"create-character": { width: 280, height: 160 },
|
||||
"create-scene": { width: 280, height: 160 },
|
||||
"generate-character-image": { width: 300, height: 200 },
|
||||
"generate-character-video": { width: 300, height: 200 },
|
||||
"generate-scene-image": { width: 300, height: 200 },
|
||||
"generate-scene-video": { width: 300, height: 200 },
|
||||
"custom-agent": { width: 340, height: 220 },
|
||||
"rh-app": { width: 280, height: 180 },
|
||||
"rh-comfy": { width: 280, height: 180 },
|
||||
"canvas-node": { width: 360, height: 240 },
|
||||
"doodle-canvas": { width: 300, height: 180 },
|
||||
};
|
||||
|
||||
function resolveCompactNodeSize(
|
||||
type: VideoWorkflowTemplateType,
|
||||
canvasNodeType: NodeType,
|
||||
size: { width: number; height: number }
|
||||
) {
|
||||
const override = COMPACT_NODE_SIZE_OVERRIDES[type];
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
|
||||
if (canvasNodeType === "workflow") {
|
||||
return {
|
||||
width: Math.min(size.width, 300),
|
||||
height: Math.min(size.height, 180),
|
||||
};
|
||||
}
|
||||
|
||||
if (canvasNodeType === "text") {
|
||||
return {
|
||||
width: Math.min(size.width, 320),
|
||||
height: Math.min(size.height, 180),
|
||||
};
|
||||
}
|
||||
|
||||
if (canvasNodeType === "image") {
|
||||
return {
|
||||
width: Math.min(size.width, 220),
|
||||
height: Math.min(size.height, 220),
|
||||
};
|
||||
}
|
||||
|
||||
if (canvasNodeType === "video") {
|
||||
return {
|
||||
width: Math.min(size.width, 240),
|
||||
height: Math.min(size.height, 220),
|
||||
};
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
const createDefinition = (
|
||||
type: VideoWorkflowTemplateType,
|
||||
label: string,
|
||||
@ -139,7 +219,7 @@ const createDefinition = (
|
||||
label,
|
||||
description,
|
||||
group,
|
||||
size,
|
||||
size: resolveCompactNodeSize(type, canvasNodeType, size),
|
||||
canvasNodeType,
|
||||
accentColor,
|
||||
quickConnectOutputs: QUICK_CONNECT_CONFIG[type] || [],
|
||||
@ -625,10 +705,10 @@ export function buildVideoWorkflowCanvasNode(
|
||||
description: definition.description,
|
||||
summary:
|
||||
definition.quickConnectOutputs.length > 0
|
||||
? `下一步可接:${definition.quickConnectOutputs
|
||||
? `可接:${definition.quickConnectOutputs
|
||||
.map((item) => item.label)
|
||||
.join(" / ")}`
|
||||
: "当前节点主要承担中间处理或输出检查。",
|
||||
: "当前节点用于处理中间结果或输出检查。",
|
||||
status: "pending",
|
||||
group: definition.group,
|
||||
accentColor: definition.accentColor,
|
||||
@ -645,7 +725,7 @@ export function buildVideoWorkflowStarterTemplateGraph(
|
||||
const template =
|
||||
VIDEO_WORKFLOW_STARTER_TEMPLATES.find((item) => item.id === templateId) ||
|
||||
VIDEO_WORKFLOW_STARTER_TEMPLATES[0];
|
||||
const laneHeight = 540;
|
||||
const laneHeight = 320;
|
||||
let cursorX = origin.x;
|
||||
|
||||
const nodes = template.nodes.map((type) => {
|
||||
@ -657,7 +737,7 @@ export function buildVideoWorkflowStarterTemplateGraph(
|
||||
x: cursorX,
|
||||
y: origin.y + Math.max(0, (laneHeight - definition.size.height) / 2),
|
||||
};
|
||||
cursorX += definition.size.width + 180;
|
||||
cursorX += definition.size.width + 96;
|
||||
return {
|
||||
type,
|
||||
node: buildVideoWorkflowCanvasNode(type, position),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user