Przeglądaj źródła

Add smart edge routing to avoid node crossings

- Create SmartEdge component with intelligent path routing
- Back-edges (target above source) route to the left
- "false" and "done" edges going down route to the right
- Regular edges use standard smoothstep path
- Update all edge creation to use smart edge type with handleType data
fszontagh 6 miesięcy temu
rodzic
commit
6878bdd472
1 zmienionych plików z 104 dodań i 4 usunięć
  1. 104 4
      webui/src/pages/WorkflowEditorPage.tsx

+ 104 - 4
webui/src/pages/WorkflowEditorPage.tsx

@@ -19,6 +19,10 @@ import ReactFlow, {
   ConnectionLineType,
   SelectionMode,
   PanOnScrollMode,
+  BaseEdge,
+  EdgeProps,
+  getSmoothStepPath,
+  EdgeLabelRenderer,
 } from 'reactflow'
 import { BackgroundVariant } from '@reactflow/background'
 import 'reactflow/dist/style.css'
@@ -70,6 +74,98 @@ const nodeTypes = {
   workflowNode: WorkflowNode,
 }
 
+// Custom edge that routes around nodes for cleaner layout
+// For "false" and "done" edges, route further to the right before going down
+// For back-edges (target above source), route to the left
+function SmartEdge({
+  id,
+  sourceX,
+  sourceY,
+  targetX,
+  targetY,
+  sourcePosition,
+  targetPosition,
+  style,
+  markerEnd,
+  data,
+  label,
+}: EdgeProps) {
+  const handleType = data?.handleType || 'main'
+  const isBackEdge = targetY < sourceY - 10 // Target is above source
+  const isSideEdge = handleType === 'false' || handleType === 'done'
+  const isGoingDown = targetY > sourceY + 50 // Target is significantly below
+
+  let edgePath: string
+  let labelX: number
+  let labelY: number
+
+  if (isBackEdge) {
+    // Back-edge: route to the left of both nodes, then up
+    const offsetX = -120 // Go left, further out to avoid nodes
+    const midY = (sourceY + targetY) / 2
+
+    edgePath = `M ${sourceX} ${sourceY}
+                L ${sourceX} ${sourceY + 15}
+                L ${Math.min(sourceX, targetX) + offsetX} ${sourceY + 15}
+                L ${Math.min(sourceX, targetX) + offsetX} ${targetY - 15}
+                L ${targetX} ${targetY - 15}
+                L ${targetX} ${targetY}`
+    labelX = Math.min(sourceX, targetX) + offsetX - 15
+    labelY = midY
+  } else if (isSideEdge && isGoingDown) {
+    // Side edge (false/done) going down: always route to the right to avoid nodes
+    const offsetX = 130 // Go right, enough to clear nodes
+    const midY = (sourceY + targetY) / 2
+
+    edgePath = `M ${sourceX} ${sourceY}
+                L ${sourceX} ${sourceY + 25}
+                L ${Math.max(sourceX, targetX) + offsetX} ${sourceY + 25}
+                L ${Math.max(sourceX, targetX) + offsetX} ${targetY - 25}
+                L ${targetX} ${targetY - 25}
+                L ${targetX} ${targetY}`
+    labelX = Math.max(sourceX, targetX) + offsetX + 15
+    labelY = midY
+  } else {
+    // Regular edge: use standard smoothstep
+    const [path, lx, ly] = getSmoothStepPath({
+      sourceX,
+      sourceY,
+      sourcePosition,
+      targetX,
+      targetY,
+      targetPosition,
+      borderRadius: 8,
+    })
+    edgePath = path
+    labelX = lx
+    labelY = ly
+  }
+
+  return (
+    <>
+      <BaseEdge id={id} path={edgePath} style={style} markerEnd={markerEnd} />
+      {label && (
+        <EdgeLabelRenderer>
+          <div
+            style={{
+              position: 'absolute',
+              transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
+              pointerEvents: 'all',
+            }}
+            className="nodrag nopan px-1.5 py-0.5 rounded text-xs font-medium bg-slate-800 text-slate-300 border border-slate-600"
+          >
+            {label}
+          </div>
+        </EdgeLabelRenderer>
+      )}
+    </>
+  )
+}
+
+const edgeTypes = {
+  smart: SmartEdge,
+}
+
 function WorkflowEditorInner() {
   const { id } = useParams<{ id: string }>()
   const navigate = useNavigate()
@@ -791,11 +887,12 @@ function WorkflowEditorInner() {
         sourceHandle: c.sourceOutput,
         target: c.targetNodeId,
         targetHandle: c.targetInput,
-        type: 'smoothstep' as const,
+        type: 'smart' as const,
         style: { strokeWidth: 2, stroke: edgeColor },
         animated: false,
         label: c.sourceOutput !== 'main' ? c.sourceOutput : undefined,
         labelStyle: { fill: edgeColor, fontWeight: 500, fontSize: 10 },
+        data: { handleType: c.sourceOutput || 'main' },
         labelBgStyle: { fill: 'var(--color-bg)', fillOpacity: 0.9 },
         zIndex: 1,
       }
@@ -862,12 +959,13 @@ function WorkflowEditorInner() {
           sourceHandle: conn.sourceOutput,
           target: conn.targetNodeId,
           targetHandle: conn.targetInput,
-          type: 'smoothstep',
+          type: 'smart',
           style: { strokeWidth: 2, stroke: edgeColor },
           animated: executionState.status === 'running',
           label: conn.sourceOutput !== 'main' ? conn.sourceOutput : undefined,
           labelStyle: { fill: edgeColor, fontWeight: 500, fontSize: 10 },
           labelBgStyle: { fill: 'var(--color-bg)', fillOpacity: 0.9 },
+          data: { handleType: conn.sourceOutput || 'main' },
           zIndex: 1,
         }
       })
@@ -958,11 +1056,12 @@ function WorkflowEditorInner() {
 
       const styledEdge = {
         ...params,
-        type: 'smoothstep',
+        type: 'smart',
         style: { strokeWidth: 2, stroke: edgeColor },
         label: sourceHandle !== 'main' ? sourceHandle : undefined,
         labelStyle: { fill: edgeColor, fontWeight: 500, fontSize: 10 },
         labelBgStyle: { fill: 'var(--color-bg)', fillOpacity: 0.9 },
+        data: { handleType: sourceHandle },
         zIndex: 1,
       }
       setEdges((eds) => addEdge(styledEdge, eds))
@@ -1935,8 +2034,9 @@ function WorkflowEditorInner() {
             nodes={isViewingExecution && viewedNodes ? viewedNodes : nodes}
             edges={isViewingExecution && viewedEdges ? viewedEdges : edges}
             nodeTypes={nodeTypes}
+            edgeTypes={edgeTypes}
             defaultEdgeOptions={{
-              type: 'smoothstep',
+              type: 'smart',
               style: { strokeWidth: 2, stroke: '#64748b' },
               animated: false,
             }}