diff --git a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx index ebb71b1..57c3618 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx @@ -21,7 +21,7 @@ import { type ReactFlowInstance, } from "@xyflow/react" import { AlertCircleIcon, CheckCircle2Icon, PlusIcon } from "lucide-react" -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -50,8 +50,6 @@ import { } from "./workflow-utils" import { NodeConfigPanel } from "./node-config-panel" -const workflowDragType = "application/agent-desk-workflow-node" - type WorkflowNodeData = Record & { nodeType?: string name?: string @@ -68,6 +66,15 @@ type WorkflowNodeData = Record & { type WorkflowFlowNode = Node type WorkflowFlowEdge = Edge +type PendingNodeDrag = { + spec: AIWorkflowNodeSpec + startX: number + startY: number + x: number + y: number + active: boolean +} + const nodeTypes = { workflowNode: WorkflowCanvasNode, } @@ -151,6 +158,10 @@ export function WorkflowEditor({ ) const [flowInstance, setFlowInstance] = useState | null>(null) const [selectedNodeId, setSelectedNodeId] = useState(null) + const [pendingNodeDrag, setPendingNodeDrag] = useState(null) + const canvasRef = useRef(null) + const pendingNodeDragRef = useRef(null) + const suppressNextClickRef = useRef(false) const selectedNode = useMemo( () => nodes.find((node) => node.id === selectedNodeId) ?? null, [nodes, selectedNodeId] @@ -232,37 +243,70 @@ export function WorkflowEditor({ }) } - const onNodeDragStart = (event: React.DragEvent, spec: AIWorkflowNodeSpec) => { - event.dataTransfer.setData(workflowDragType, spec.type) - event.dataTransfer.effectAllowed = "copy" - } + const dropNodeOnCanvas = useCallback( + (spec: AIWorkflowNodeSpec, x: number, y: number) => { + if (!flowInstance || !canvasRef.current) { + return false + } + const rect = canvasRef.current.getBoundingClientRect() + if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { + return false + } + const position = flowInstance.screenToFlowPosition({ x, y }) + setNodes((current) => [ + ...current, + createWorkflowNodeFromSpec(spec, current, position) as WorkflowFlowNode, + ]) + return true + }, + [flowInstance, setNodes] + ) - const onCanvasDragOver = (event: React.DragEvent) => { - if (!event.dataTransfer.types.includes(workflowDragType)) { + const onNodePointerDown = (event: React.PointerEvent, spec: AIWorkflowNodeSpec) => { + if (event.button !== 0) { return } - event.preventDefault() - event.dataTransfer.dropEffect = "copy" - } - - const onCanvasDrop = (event: React.DragEvent) => { - event.preventDefault() - const nodeType = event.dataTransfer.getData(workflowDragType) - if (!nodeType || !flowInstance) { - return - } - const spec = nodeSpecs.find((item) => item.type === nodeType) - if (!spec) { - return - } - const position = flowInstance.screenToFlowPosition({ + const initialDrag = { + spec, + startX: event.clientX, + startY: event.clientY, x: event.clientX, y: event.clientY, - }) - setNodes((current) => [ - ...current, - createWorkflowNodeFromSpec(spec, current, position) as WorkflowFlowNode, - ]) + active: false, + } + pendingNodeDragRef.current = initialDrag + setPendingNodeDrag(initialDrag) + + const handlePointerMove = (event: PointerEvent) => { + const current = pendingNodeDragRef.current + if (!current) { + return + } + const moved = Math.hypot(event.clientX - current.startX, event.clientY - current.startY) + const nextDrag = { + ...current, + x: event.clientX, + y: event.clientY, + active: current.active || moved > 6, + } + pendingNodeDragRef.current = nextDrag + setPendingNodeDrag(nextDrag) + } + + const handlePointerUp = (event: PointerEvent) => { + window.removeEventListener("pointermove", handlePointerMove) + window.removeEventListener("pointerup", handlePointerUp) + const current = pendingNodeDragRef.current + pendingNodeDragRef.current = null + setPendingNodeDrag(null) + if (current?.active) { + suppressNextClickRef.current = true + dropNodeOnCanvas(current.spec, event.clientX, event.clientY) + } + } + + window.addEventListener("pointermove", handlePointerMove) + window.addEventListener("pointerup", handlePointerUp) } const updateNodeData = (nodeId: string, data: WorkflowNodeData) => { @@ -291,9 +335,14 @@ export function WorkflowEditor({