11import dagre from '@dagrejs/dagre' ;
22import type { Node , Edge } from '@xyflow/react' ;
33
4- const NODE_WIDTH = 220 ;
5- const NODE_HEIGHT = 100 ;
6- const HORIZONTAL_GAP = 60 ;
7- const VERTICAL_GAP = 80 ;
4+ const HORIZONTAL_GAP = 80 ;
5+ const VERTICAL_GAP = 100 ;
86const GRID_SIZE = 16 ;
97
108/** Snap a value to the nearest grid increment. */
119function snapToGrid ( value : number ) : number {
1210 return Math . round ( value / GRID_SIZE ) * GRID_SIZE ;
1311}
1412
13+ /**
14+ * Estimate node dimensions based on type and data content.
15+ * Nodes with more inline info (LLM, Code, HTTP) are taller.
16+ */
17+ function getNodeSize ( node : Node ) : { width : number ; height : number } {
18+ const d = ( node . data ?? { } ) as Record < string , unknown > ;
19+
20+ switch ( node . type ) {
21+ // Large nodes — gradient header + multi-line content
22+ case 'llmNode' : {
23+ let h = 120 ; // header + chips
24+ if ( d . systemPrompt ) h += 16 ;
25+ if ( d . userMessage ) h += 16 ;
26+ if ( d . temperature != null ) h += 18 ;
27+ if ( d . responseFormat === 'json' ) h += 20 ;
28+ return { width : 260 , height : h } ;
29+ }
30+
31+ case 'httpRequestNode' : {
32+ let h = 110 ;
33+ if ( d . url ) h += 16 ;
34+ if ( d . auth ) h += 16 ;
35+ return { width : 250 , height : h } ;
36+ }
37+
38+ case 'codeNode' : {
39+ const code = ( d . code as string ) ?? '' ;
40+ const lines = Math . min ( code . split ( '\n' ) . length , 3 ) ;
41+ return { width : 260 , height : 100 + lines * 14 } ;
42+ }
43+
44+ // Medium nodes — header + expression/content
45+ case 'conditionNode' :
46+ return { width : 240 , height : 130 } ; // split true/false zones
47+
48+ case 'switchNode' : {
49+ const cases = ( d . cases as unknown [ ] ) ?? [ ] ;
50+ return { width : 240 , height : 110 + Math . min ( cases . length , 5 ) * 20 } ;
51+ }
52+
53+ case 'forEachNode' :
54+ return { width : 230 , height : 130 } ; // each/done zones
55+
56+ case 'triggerNode' :
57+ return { width : 230 , height : 110 } ;
58+
59+ case 'subWorkflowNode' : {
60+ let h = 110 ;
61+ const mapping = d . inputMapping as Record < string , unknown > | undefined ;
62+ if ( mapping ) h += Math . min ( Object . keys ( mapping ) . length , 3 ) * 16 ;
63+ return { width : 240 , height : h } ;
64+ }
65+
66+ case 'schemaValidatorNode' : {
67+ let h = 110 ;
68+ const schema = d . schema as Record < string , unknown > | undefined ;
69+ const props = schema ?. properties as Record < string , unknown > | undefined ;
70+ if ( props ) h += Math . min ( Object . keys ( props ) . length , 3 ) * 14 ;
71+ return { width : 240 , height : h } ;
72+ }
73+
74+ // Compact nodes — header + one line of info
75+ case 'toolNode' : {
76+ let h = 95 ;
77+ const args = d . toolArgs as Record < string , unknown > | undefined ;
78+ if ( args && Object . keys ( args ) . length > 0 ) h += 16 ;
79+ if ( d . description ) h += 14 ;
80+ return { width : 230 , height : h } ;
81+ }
82+
83+ case 'filterNode' :
84+ case 'mapNode' :
85+ return { width : 220 , height : 110 } ;
86+
87+ case 'aggregateNode' :
88+ return { width : 220 , height : 110 } ;
89+
90+ case 'dataStoreNode' :
91+ return { width : 220 , height : 110 } ;
92+
93+ case 'delayNode' :
94+ return { width : 200 , height : 110 } ;
95+
96+ case 'notificationNode' :
97+ return { width : 220 , height : d . message ? 110 : 90 } ;
98+
99+ case 'approvalNode' :
100+ return { width : 230 , height : 120 } ;
101+
102+ case 'errorHandlerNode' :
103+ return { width : 220 , height : 100 } ;
104+
105+ case 'webhookResponseNode' :
106+ return { width : 220 , height : 100 } ;
107+
108+ // Structural nodes — minimal
109+ case 'parallelNode' : {
110+ const count = ( d . branchCount as number ) ?? 2 ;
111+ return { width : 200 + count * 20 , height : 100 } ;
112+ }
113+
114+ case 'mergeNode' :
115+ return { width : 200 , height : 90 } ;
116+
117+ case 'stickyNoteNode' : {
118+ const text = ( d . text as string ) ?? '' ;
119+ const lines = Math . min ( text . split ( '\n' ) . length , 5 ) ;
120+ return { width : 180 , height : 60 + lines * 16 } ;
121+ }
122+
123+ default :
124+ return { width : 220 , height : 100 } ;
125+ }
126+ }
127+
15128/**
16129 * Compute an automatic top-to-bottom DAG layout for the given nodes and edges
17130 * using the dagre graph layout algorithm.
18131 *
132+ * Node sizes are estimated per-type so the layout adapts to the visual weight
133+ * of each node (LLM nodes are taller than Merge nodes, etc.).
134+ *
19135 * Returns a new array of nodes with updated positions — inputs are never mutated.
20136 */
21137export function autoArrangeNodes ( nodes : Node [ ] , edges : Edge [ ] ) : Node [ ] {
@@ -31,8 +147,12 @@ export function autoArrangeNodes(nodes: Node[], edges: Edge[]): Node[] {
31147 marginy : 40 ,
32148 } ) ;
33149
150+ // Register each node with its estimated dimensions
151+ const sizeCache = new Map < string , { width : number ; height : number } > ( ) ;
34152 for ( const node of nodes ) {
35- g . setNode ( node . id , { width : NODE_WIDTH , height : NODE_HEIGHT } ) ;
153+ const size = getNodeSize ( node ) ;
154+ sizeCache . set ( node . id , size ) ;
155+ g . setNode ( node . id , size ) ;
36156 }
37157
38158 for ( const edge of edges ) {
@@ -43,12 +163,13 @@ export function autoArrangeNodes(nodes: Node[], edges: Edge[]): Node[] {
43163
44164 return nodes . map ( ( node ) => {
45165 const pos = g . node ( node . id ) ;
166+ const size = sizeCache . get ( node . id ) ! ;
46167 // dagre returns center coordinates — convert to top-left for ReactFlow
47168 return {
48169 ...node ,
49170 position : {
50- x : snapToGrid ( pos . x - NODE_WIDTH / 2 ) ,
51- y : snapToGrid ( pos . y - NODE_HEIGHT / 2 ) ,
171+ x : snapToGrid ( pos . x - size . width / 2 ) ,
172+ y : snapToGrid ( pos . y - size . height / 2 ) ,
52173 } ,
53174 } ;
54175 } ) ;
0 commit comments