You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
179 lines
6.6 KiB
179 lines
6.6 KiB
"use client";
|
|
|
|
import { useWorkflowStore } from "@/store/workflowStore";
|
|
import { WorkflowEdgeData } from "@/types";
|
|
import { useMemo, useEffect, useState, useRef } from "react";
|
|
|
|
export function EdgeToolbar() {
|
|
const { edges, toggleEdgePause, removeEdge, setLoopCount } = useWorkflowStore();
|
|
const [clickPosition, setClickPosition] = useState<{ x: number; y: number } | null>(null);
|
|
const previousSelectedEdgeId = useRef<string | null>(null);
|
|
|
|
const selectedEdge = useMemo(
|
|
() => edges.find((edge) => edge.selected),
|
|
[edges]
|
|
);
|
|
|
|
// Helper function to compute the image connection sequence number
|
|
const getImageSequenceNumber = (edge: typeof selectedEdge): number | null => {
|
|
if (!edge) return null;
|
|
|
|
// Only show for image connections
|
|
const sourceHandle = edge.sourceHandle;
|
|
const targetHandle = edge.targetHandle;
|
|
const isImageConnection =
|
|
(sourceHandle === "image" || sourceHandle?.startsWith("image-")) ||
|
|
(targetHandle === "image" || targetHandle?.startsWith("image-"));
|
|
|
|
if (!isImageConnection) return null;
|
|
|
|
// Find all image edges going to the same target + target handle
|
|
const siblingEdges = edges.filter(
|
|
(e) => e.target === edge.target && e.targetHandle === edge.targetHandle
|
|
);
|
|
|
|
// If only one connection, no need for numbering
|
|
if (siblingEdges.length <= 1) return null;
|
|
|
|
// Sort by createdAt timestamp (fallback to edge ID for legacy edges without timestamp)
|
|
const sorted = [...siblingEdges].sort((a, b) => {
|
|
const timeA = (a.data as WorkflowEdgeData)?.createdAt || 0;
|
|
const timeB = (b.data as WorkflowEdgeData)?.createdAt || 0;
|
|
if (timeA !== timeB) return timeA - timeB;
|
|
return a.id.localeCompare(b.id);
|
|
});
|
|
|
|
const index = sorted.findIndex((e) => e.id === edge.id);
|
|
return index >= 0 ? index + 1 : null;
|
|
};
|
|
|
|
const sequenceNumber = getImageSequenceNumber(selectedEdge);
|
|
|
|
// Track mouse position when edge selection changes
|
|
useEffect(() => {
|
|
const handleMouseDown = (e: MouseEvent) => {
|
|
// Check if clicking on an edge
|
|
const target = e.target as Element;
|
|
if (target.closest('.react-flow__edge')) {
|
|
setClickPosition({ x: e.clientX, y: e.clientY - 40 }); // 40px above click
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleMouseDown);
|
|
return () => document.removeEventListener('mousedown', handleMouseDown);
|
|
}, []);
|
|
|
|
// Reset click position when edge is deselected
|
|
useEffect(() => {
|
|
if (!selectedEdge && previousSelectedEdgeId.current) {
|
|
setClickPosition(null);
|
|
}
|
|
previousSelectedEdgeId.current = selectedEdge?.id || null;
|
|
}, [selectedEdge]);
|
|
|
|
const toolbarPosition = clickPosition;
|
|
|
|
const handleTogglePause = () => {
|
|
if (selectedEdge) {
|
|
toggleEdgePause(selectedEdge.id);
|
|
}
|
|
};
|
|
|
|
const handleLoopCountChange = (delta: number) => {
|
|
if (selectedEdge) {
|
|
setLoopCount(selectedEdge.id, loopCount + delta);
|
|
}
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
if (selectedEdge) {
|
|
removeEdge(selectedEdge.id);
|
|
}
|
|
};
|
|
|
|
if (!toolbarPosition || !selectedEdge) return null;
|
|
|
|
const hasPause = selectedEdge.data?.hasPause;
|
|
const isLoop = selectedEdge.data?.isLoop;
|
|
const loopCount = selectedEdge.data?.loopCount ?? 3;
|
|
|
|
return (
|
|
<div
|
|
className="fixed z-[100] flex items-center gap-1 bg-neutral-800 border border-neutral-600 rounded-lg shadow-xl p-1"
|
|
style={{
|
|
left: toolbarPosition.x,
|
|
top: toolbarPosition.y,
|
|
transform: "translateX(-50%)",
|
|
}}
|
|
>
|
|
{sequenceNumber !== null && (
|
|
<span className="text-[10px] font-medium text-neutral-300 px-2 border-r border-neutral-600">
|
|
Image {sequenceNumber}
|
|
</span>
|
|
)}
|
|
{isLoop && (
|
|
<>
|
|
<span className="text-[10px] font-medium text-fuchsia-300 px-1.5">Loop</span>
|
|
<button
|
|
onClick={() => handleLoopCountChange(-1)}
|
|
disabled={loopCount <= 1}
|
|
className="p-1 rounded hover:bg-neutral-700 text-fuchsia-300 hover:text-fuchsia-100 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
title="Decrease loop count"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" d="M5 12h14" />
|
|
</svg>
|
|
</button>
|
|
<span className="text-[11px] font-mono text-fuchsia-100 min-w-[20px] text-center">{loopCount}</span>
|
|
<button
|
|
onClick={() => handleLoopCountChange(1)}
|
|
disabled={loopCount >= 100}
|
|
className="p-1 rounded hover:bg-neutral-700 text-fuchsia-300 hover:text-fuchsia-100 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
title="Increase loop count"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" d="M12 5v14M5 12h14" />
|
|
</svg>
|
|
</button>
|
|
<div className="w-px h-4 bg-neutral-600" />
|
|
</>
|
|
)}
|
|
{!isLoop && (
|
|
<button
|
|
onClick={handleTogglePause}
|
|
className={`p-1.5 rounded hover:bg-neutral-700 transition-colors ${
|
|
hasPause
|
|
? "text-amber-400 hover:text-amber-300"
|
|
: "text-neutral-400 hover:text-neutral-100"
|
|
}`}
|
|
title={hasPause ? "Remove pause" : "Add pause"}
|
|
>
|
|
{hasPause ? (
|
|
// Play icon (resume)
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8 5v14l11-7z" />
|
|
</svg>
|
|
) : (
|
|
// Pause icon
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={handleDelete}
|
|
className="p-1.5 rounded hover:bg-neutral-700 text-neutral-400 hover:text-red-400 transition-colors"
|
|
title="Delete"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|