Add inline workflow quick connect bubbles
This commit is contained in:
parent
584579e987
commit
d0c26e9fab
@ -321,6 +321,40 @@ const VIDEO_CANVAS_INSERT_MENU_ITEMS: VideoCanvasInsertMenuItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const VIDEO_WORKFLOW_QUICK_CONNECT_ICON_MAP: Partial<
|
||||
Record<VideoWorkflowTemplateType, LucideIcon>
|
||||
> = {
|
||||
"text-node": FileText,
|
||||
"input-image": FileImage,
|
||||
"video-input": Film,
|
||||
"novel-input": FileText,
|
||||
"video-analyze": Scissors,
|
||||
"storyboard-node": GitBranch,
|
||||
"storyboard-chart-node": GitBranch,
|
||||
"table-editor-node": Table2,
|
||||
"gen-image": Palette,
|
||||
"gen-video": Video,
|
||||
"gen-music": Music2,
|
||||
"motion-control": Clapperboard,
|
||||
"camera-movement": Clapperboard,
|
||||
"professional-camera": Wand2,
|
||||
"inpaint-crop": Scissors,
|
||||
"inpaint-stitch": Scissors,
|
||||
"local-save": Save,
|
||||
preview: Eye,
|
||||
"custom-agent": Bot,
|
||||
"rh-app": Box,
|
||||
"rh-comfy": Box,
|
||||
"jimeng-super-resolution": Sparkles,
|
||||
"doodle-canvas": PenTool,
|
||||
"canvas-node": Layers,
|
||||
"image-compare": Layers,
|
||||
};
|
||||
|
||||
function getVideoWorkflowQuickConnectIcon(type: VideoWorkflowTemplateType) {
|
||||
return VIDEO_WORKFLOW_QUICK_CONNECT_ICON_MAP[type] || Plus;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@ -1004,6 +1038,41 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
: [],
|
||||
[selectedVideoWorkflowType]
|
||||
);
|
||||
const selectedWorkflowQuickConnectAnchor = useMemo(() => {
|
||||
if (
|
||||
!isVideoMode ||
|
||||
!selectedVideoWorkflowNode ||
|
||||
selectedVideoWorkflowQuickConnects.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeLeft = selectedVideoWorkflowNode.position.x * viewport.zoom + viewport.x;
|
||||
const nodeTop = selectedVideoWorkflowNode.position.y * viewport.zoom + viewport.y;
|
||||
const nodeWidth = selectedVideoWorkflowNode.size.width * viewport.zoom;
|
||||
const anchorX = nodeLeft + nodeWidth;
|
||||
const anchorY =
|
||||
nodeTop + (selectedVideoWorkflowNode.size.height * viewport.zoom) / 2;
|
||||
const openLeft = viewportWidth > 0 && anchorX + 260 > viewportWidth - 32;
|
||||
|
||||
return {
|
||||
x: anchorX,
|
||||
y: anchorY,
|
||||
openLeft,
|
||||
};
|
||||
}, [
|
||||
isVideoMode,
|
||||
selectedVideoWorkflowNode,
|
||||
selectedVideoWorkflowQuickConnects.length,
|
||||
viewport.x,
|
||||
viewport.y,
|
||||
viewport.zoom,
|
||||
viewportWidth,
|
||||
]);
|
||||
const selectedWorkflowQuickConnectSourceNode =
|
||||
selectedWorkflowQuickConnectAnchor && selectedVideoWorkflowNode
|
||||
? selectedVideoWorkflowNode
|
||||
: null;
|
||||
const workflowGraph = useMemo(
|
||||
() => createWorkflowGraphContext(nodes, edges),
|
||||
[edges, nodes]
|
||||
@ -7343,40 +7412,97 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
) : null}
|
||||
|
||||
{isVideoMode &&
|
||||
selectedVideoWorkflowDefinition &&
|
||||
(selectedVideoWorkflowQuickConnects.length > 0 || selectedWorkflowIsActionable) ? (
|
||||
<div className="pointer-events-none absolute left-4 top-16 z-20 flex w-[min(480px,calc(100vw-32px))] flex-col gap-3">
|
||||
{selectedVideoWorkflowQuickConnects.length > 0 ? (
|
||||
<Card className={VIDEO_CANVAS_PANEL_CLASS}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">{selectedVideoWorkflowDefinition.label}</CardTitle>
|
||||
<CardDescription className="mt-1 line-clamp-2">
|
||||
常用下游分支会放在这里,点击后自动放到右侧并连线。
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline" className={VIDEO_CANVAS_PANEL_BADGE_CLASS}>
|
||||
Quick Connect
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2 pb-5">
|
||||
{selectedVideoWorkflowQuickConnects.map((item) => (
|
||||
<Button
|
||||
key={`${selectedVideoWorkflowDefinition.type}-${item.type}`}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full border border-slate-200 bg-white hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900"
|
||||
onClick={() => handleAddWorkflowNode(item.type)}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
selectedWorkflowQuickConnectAnchor &&
|
||||
selectedWorkflowQuickConnectSourceNode &&
|
||||
selectedVideoWorkflowQuickConnects.length > 0 &&
|
||||
!videoCanvasInsertMenu ? (
|
||||
<div
|
||||
className="pointer-events-none absolute z-20"
|
||||
style={{
|
||||
left: selectedWorkflowQuickConnectAnchor.x,
|
||||
top: selectedWorkflowQuickConnectAnchor.y,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="pointer-events-none absolute left-0 top-0 overflow-visible"
|
||||
width="1"
|
||||
height="1"
|
||||
>
|
||||
{selectedVideoWorkflowQuickConnects.map((item, index, items) => {
|
||||
const total = items.length;
|
||||
const y = (index - (total - 1) / 2) * 72;
|
||||
const x = 96 + Math.abs(y) * 0.28;
|
||||
const direction = selectedWorkflowQuickConnectAnchor.openLeft ? -1 : 1;
|
||||
const targetX = x * direction;
|
||||
const cp1X = 36 * direction;
|
||||
const cp2X = (x - 26) * direction;
|
||||
return (
|
||||
<path
|
||||
key={`quick-connect-line-${item.type}`}
|
||||
d={`M 0 0 C ${cp1X} 0, ${cp2X} ${y}, ${targetX} ${y}`}
|
||||
fill="none"
|
||||
stroke="rgba(59,130,246,0.9)"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setVideoCanvasInsertMenu({
|
||||
screenPosition: {
|
||||
x: selectedWorkflowQuickConnectAnchor.x,
|
||||
y: selectedWorkflowQuickConnectAnchor.y,
|
||||
},
|
||||
canvasPosition: {
|
||||
x:
|
||||
selectedWorkflowQuickConnectSourceNode.position.x +
|
||||
selectedWorkflowQuickConnectSourceNode.size.width,
|
||||
y:
|
||||
selectedWorkflowQuickConnectSourceNode.position.y +
|
||||
selectedWorkflowQuickConnectSourceNode.size.height / 2,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="pointer-events-auto absolute left-0 top-0 flex h-16 w-16 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 shadow-[0_16px_32px_-18px_rgba(15,23,42,0.28)] transition-colors hover:bg-slate-50 hover:text-slate-700 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
|
||||
aria-label="展开关联模块"
|
||||
>
|
||||
<Plus className="h-8 w-8" />
|
||||
</button>
|
||||
|
||||
{selectedVideoWorkflowQuickConnects.map((item, index, items) => {
|
||||
const total = items.length;
|
||||
const y = (index - (total - 1) / 2) * 72;
|
||||
const x = 96 + Math.abs(y) * 0.28;
|
||||
const direction = selectedWorkflowQuickConnectAnchor.openLeft ? -1 : 1;
|
||||
const Icon = getVideoWorkflowQuickConnectIcon(item.type);
|
||||
return (
|
||||
<button
|
||||
key={`quick-connect-node-${item.type}`}
|
||||
type="button"
|
||||
onClick={() => handleAddWorkflowNode(item.type)}
|
||||
className="pointer-events-auto absolute flex h-14 w-14 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 shadow-[0_14px_30px_-18px_rgba(15,23,42,0.24)] transition-all hover:scale-105 hover:bg-slate-50 hover:text-slate-800 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
|
||||
style={{
|
||||
left: `${x * direction}px`,
|
||||
top: `${y}px`,
|
||||
}}
|
||||
aria-label={item.label}
|
||||
title={item.label}
|
||||
>
|
||||
<Icon className="h-6 w-6" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isVideoMode &&
|
||||
selectedVideoWorkflowDefinition &&
|
||||
selectedWorkflowIsActionable ? (
|
||||
<div className="pointer-events-none absolute left-4 top-16 z-20 flex w-[min(480px,calc(100vw-32px))] flex-col gap-3">
|
||||
{selectedWorkflowIsActionable ? (
|
||||
<Card className={VIDEO_CANVAS_PANEL_CLASS}>
|
||||
<CardHeader className="pb-3">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user