@@ -189,7 +189,7 @@ export default function FlowRunDetails({
189
189
Click steps to view details
190
190
</ span >
191
191
</ h3 >
192
- < div className = "grid grid-cols-1 md:grid-cols-1 gap-2" >
192
+ < div >
193
193
{ runData . step_states &&
194
194
( ( ) => {
195
195
// Sort step_states directly by step.step_index
@@ -201,7 +201,24 @@ export default function FlowRunDetails({
201
201
} ,
202
202
) ;
203
203
204
- return sortedStepStates . map ( ( step , index ) => {
204
+ // Group parallel steps based on flow definition
205
+ // We're specifically looking for summary, sentiment, and tags steps that run in parallel
206
+ const parallelStepSlugs = [ 'summary' , 'sentiment' , 'tags' ] ;
207
+ const parallelSteps = sortedStepStates . filter ( ( step ) =>
208
+ parallelStepSlugs . includes ( step . step_slug ) ,
209
+ ) ;
210
+
211
+ // Other steps will be displayed normally
212
+ const regularSteps = sortedStepStates . filter (
213
+ ( step ) => ! parallelStepSlugs . includes ( step . step_slug ) ,
214
+ ) ;
215
+
216
+ // Function to render a step
217
+ const renderStep = (
218
+ step : any ,
219
+ index : number ,
220
+ isParallel : boolean = false ,
221
+ ) => {
205
222
// Find the corresponding step tasks for this step
206
223
const stepTasks = runData . step_tasks
207
224
?. filter ( ( task ) => task . step_slug === step . step_slug )
@@ -214,40 +231,42 @@ export default function FlowRunDetails({
214
231
( task ) => task . status === 'completed' ,
215
232
) ;
216
233
234
+ // Get the pre-sorted step tasks
235
+ const latestTask =
236
+ stepTasks && stepTasks . length > 0
237
+ ? stepTasks . sort (
238
+ ( a , b ) =>
239
+ ( b . attempts_count || 0 ) - ( a . attempts_count || 0 ) ,
240
+ ) [ 0 ]
241
+ : null ;
242
+
243
+ // Check if this is a retry (attempts_count > 1)
244
+ const isRetrying =
245
+ latestTask &&
246
+ latestTask . attempts_count > 1 &&
247
+ step . status === 'started' ;
248
+
249
+ // Define status-based styles
250
+ let statusStyle = '' ;
251
+ if ( step . status === 'completed' ) {
252
+ statusStyle = 'bg-green-500/5 border-green-500/30' ;
253
+ } else if ( isRetrying ) {
254
+ statusStyle =
255
+ 'bg-red-500/5 border-red-500/30 animate-pulse' ;
256
+ } else if ( step . status === 'started' ) {
257
+ statusStyle = 'bg-yellow-500/5 border-yellow-500/30' ;
258
+ } else if ( step . status === 'failed' ) {
259
+ statusStyle = 'bg-red-500/5 border-red-500/30' ;
260
+ } else if ( step . status === 'created' ) {
261
+ statusStyle = 'bg-blue-500/5 border-blue-500/30' ;
262
+ } else {
263
+ statusStyle = 'bg-gray-500/5 border-gray-500/30' ;
264
+ }
265
+
217
266
return (
218
267
< Collapsible
219
268
key = { index }
220
- className = { `mb-1 rounded-lg border ${ ( ( ) => {
221
- // Get the pre-sorted step tasks from above
222
- const latestTask =
223
- stepTasks && stepTasks . length > 0
224
- ? stepTasks . sort (
225
- ( a , b ) =>
226
- ( b . attempts_count || 0 ) -
227
- ( a . attempts_count || 0 ) ,
228
- ) [ 0 ]
229
- : null ;
230
-
231
- // Check if this is a retry (attempts_count > 1)
232
- const isRetrying =
233
- latestTask &&
234
- latestTask . attempts_count > 1 &&
235
- step . status === 'started' ;
236
-
237
- if ( step . status === 'completed' ) {
238
- return 'bg-green-500/5 border-green-500/30' ;
239
- } else if ( isRetrying ) {
240
- return 'bg-red-500/5 border-red-500/30 animate-pulse' ;
241
- } else if ( step . status === 'started' ) {
242
- return 'bg-yellow-500/5 border-yellow-500/30' ;
243
- } else if ( step . status === 'failed' ) {
244
- return 'bg-red-500/5 border-red-500/30' ;
245
- } else if ( step . status === 'created' ) {
246
- return 'bg-blue-500/5 border-blue-500/30' ;
247
- } else {
248
- return 'bg-gray-500/5 border-gray-500/30' ;
249
- }
250
- } ) ( ) } `}
269
+ className = { `rounded-lg border ${ statusStyle } ` }
251
270
>
252
271
< CollapsibleTrigger className = "flex items-center justify-between w-full p-2 text-left" >
253
272
< div >
@@ -256,100 +275,68 @@ export default function FlowRunDetails({
256
275
</ h4 >
257
276
</ div >
258
277
< div className = "flex items-center" >
259
- { step . status === 'started' && step . started_at && (
260
- < span className = "text-xs text-yellow-600/80 mr-2" >
261
- { formatRelativeTime (
262
- step . started_at ,
263
- currentTime ,
264
- ) }
265
- </ span >
266
- ) }
267
- { step . status === 'completed' &&
268
- step . started_at &&
269
- step . completed_at && (
270
- < span className = "text-xs text-green-600/80 mr-2" >
271
- { formatTimeDifference (
272
- step . started_at ,
273
- step . completed_at ,
278
+ { ! isParallel && (
279
+ < >
280
+ { step . status === 'started' &&
281
+ step . started_at && (
282
+ < span className = "text-xs text-yellow-600/80 mr-2" >
283
+ { formatRelativeTime (
284
+ step . started_at ,
285
+ currentTime ,
286
+ ) }
287
+ </ span >
274
288
) }
275
- </ span >
276
- ) }
277
- { step . status === 'failed' &&
278
- step . started_at &&
279
- step . failed_at && (
280
- < span className = "text-xs text-red-600/80 mr-2" >
281
- Failed after{ ' ' }
282
- { formatTimeDifference (
283
- step . started_at ,
284
- step . failed_at ,
289
+ { step . status === 'completed' &&
290
+ step . started_at &&
291
+ step . completed_at && (
292
+ < span className = "text-xs text-green-600/80 mr-2" >
293
+ { formatTimeDifference (
294
+ step . started_at ,
295
+ step . completed_at ,
296
+ ) }
297
+ </ span >
285
298
) }
286
- </ span >
287
- ) }
288
- { ( ( ) => {
289
- // Use the pre-sorted step tasks from above
290
- const latestTask =
291
- stepTasks && stepTasks . length > 0
292
- ? stepTasks . sort (
293
- ( a , b ) =>
294
- ( b . attempts_count || 0 ) -
295
- ( a . attempts_count || 0 ) ,
296
- ) [ 0 ]
297
- : null ;
298
-
299
- // Check if this is a retry (attempts_count > 1)
300
- const isRetrying =
301
- latestTask &&
302
- latestTask . attempts_count > 1 &&
303
- step . status === 'started' ;
304
-
305
- return (
306
- < span
307
- className = { `inline-block w-2 h-2 rounded-full mr-1 ${
308
- step . status === 'completed'
309
- ? 'bg-green-500'
310
- : isRetrying
311
- ? 'bg-red-500 breathing'
312
- : step . status === 'started'
313
- ? 'bg-yellow-500 breathing'
314
- : step . status === 'failed'
315
- ? 'bg-red-500'
316
- : step . status === 'created'
317
- ? 'bg-blue-500'
318
- : 'bg-gray-500'
319
- } `}
320
- > </ span >
321
- ) ;
322
- } ) ( ) }
323
- < span className = "capitalize text-xs" >
324
- { ( ( ) => {
325
- // Use the pre-sorted step tasks from above
326
- const latestTask =
327
- stepTasks && stepTasks . length > 0
328
- ? stepTasks . sort (
329
- ( a , b ) =>
330
- ( b . attempts_count || 0 ) -
331
- ( a . attempts_count || 0 ) ,
332
- ) [ 0 ]
333
- : null ;
334
-
335
- // Check if this is a retry (attempts_count > 1)
336
- const isRetrying =
337
- latestTask &&
338
- latestTask . attempts_count > 1 &&
339
- step . status === 'started' ;
340
-
341
- if ( isRetrying ) {
342
- return `retrying (retry ${ latestTask . attempts_count - 1 } )` ;
343
- } else if ( step . status === 'created' ) {
344
- return 'waiting' ;
345
- } else {
346
- return step . status ;
347
- }
348
- } ) ( ) }
349
- </ span >
299
+ { step . status === 'failed' &&
300
+ step . started_at &&
301
+ step . failed_at && (
302
+ < span className = "text-xs text-red-600/80 mr-2" >
303
+ Failed after{ ' ' }
304
+ { formatTimeDifference (
305
+ step . started_at ,
306
+ step . failed_at ,
307
+ ) }
308
+ </ span >
309
+ ) }
310
+ </ >
311
+ ) }
312
+
313
+ < span
314
+ className = { `inline-block w-2 h-2 rounded-full ${ ! isParallel ? 'mr-1' : '' } ${
315
+ step . status === 'completed'
316
+ ? 'bg-green-500'
317
+ : isRetrying
318
+ ? 'bg-red-500 breathing'
319
+ : step . status === 'started'
320
+ ? 'bg-yellow-500 breathing'
321
+ : step . status === 'failed'
322
+ ? 'bg-red-500'
323
+ : step . status === 'created'
324
+ ? 'bg-blue-500'
325
+ : 'bg-gray-500'
326
+ } `}
327
+ > </ span >
328
+ { ! isParallel && (
329
+ < span className = "capitalize text-xs" >
330
+ { isRetrying
331
+ ? `retrying (retry ${ latestTask . attempts_count - 1 } )`
332
+ : step . status === 'created'
333
+ ? 'waiting'
334
+ : step . status }
335
+ </ span >
336
+ ) }
350
337
</ div >
351
338
</ CollapsibleTrigger >
352
- < CollapsibleContent className = "px-2 pb-2" >
339
+ < CollapsibleContent className = "px-2 pb-2 w-full bg-background/70 backdrop-blur-sm border-t border-foreground/10 " >
353
340
{ step . status === 'completed' && stepTask ?. output && (
354
341
< div className = "mt-1 overflow-auto" >
355
342
< div className = "max-h-32 overflow-hidden border border-gray-500/30 rounded-md" >
@@ -384,7 +371,91 @@ export default function FlowRunDetails({
384
371
</ CollapsibleContent >
385
372
</ Collapsible >
386
373
) ;
387
- } ) ;
374
+ } ;
375
+
376
+ // Separate website and saveToDb steps to place parallel steps between them
377
+ const websiteStep = regularSteps . find (
378
+ ( step ) => step . step_slug === 'website' ,
379
+ ) ;
380
+ const saveToDbStep = regularSteps . find (
381
+ ( step ) => step . step_slug === 'saveToDb' ,
382
+ ) ;
383
+ const otherRegularSteps = regularSteps . filter (
384
+ ( step ) =>
385
+ step . step_slug !== 'website' &&
386
+ step . step_slug !== 'saveToDb' ,
387
+ ) ;
388
+
389
+ return (
390
+ < div className = "space-y-3" >
391
+ { /* Website step (first step) */ }
392
+ { websiteStep && (
393
+ < div className = "grid grid-cols-1 gap-2 mb-6" >
394
+ { renderStep ( websiteStep , 0 ) }
395
+ </ div >
396
+ ) }
397
+
398
+ { /* Parallel steps with note */ }
399
+ { parallelSteps . length > 0 && (
400
+ < div className = "mb-0" >
401
+ < div className = "flex justify-between items-center mb-1" >
402
+ < span className = "text-xs text-muted-foreground italic" >
403
+ Following steps run in parallel
404
+ </ span >
405
+ < span className = "text-xs flex items-center" >
406
+ < svg
407
+ xmlns = "http://www.w3.org/2000/svg"
408
+ width = "12"
409
+ height = "12"
410
+ viewBox = "0 0 24 24"
411
+ fill = "none"
412
+ stroke = "currentColor"
413
+ strokeWidth = "2"
414
+ strokeLinecap = "round"
415
+ strokeLinejoin = "round"
416
+ className = "mr-1"
417
+ >
418
+ < path d = "M12 22v-6M9 8V2M15 8V2M6 8a3 3 0 0 1 3 3v1M18 8a3 3 0 0 0-3 3v1M12 19a3 3 0 0 1-3-3v-1M12 19a3 3 0 0 0 3-3v-1" > </ path >
419
+ </ svg >
420
+ Parallel Processing
421
+ </ span >
422
+ </ div >
423
+ < div className = "grid grid-cols-3 gap-2 relative step-container" >
424
+ { parallelSteps . map ( ( step , index ) =>
425
+ renderStep ( step , index , true ) ,
426
+ ) }
427
+ </ div >
428
+ < style jsx > { `
429
+ .step-container {
430
+ margin-bottom: 2rem;
431
+ }
432
+ .step-container > :global(*) {
433
+ height: 41px; /* Match the height of regular steps (label + padding) */
434
+ }
435
+ .step-container > :global(*[data-state="open"]) {
436
+ height: auto;
437
+ }
438
+ ` } </ style >
439
+ </ div >
440
+ ) }
441
+
442
+ { /* SaveToDb step (last step) */ }
443
+ { saveToDbStep && (
444
+ < div className = "grid grid-cols-1 gap-2" >
445
+ { renderStep ( saveToDbStep , 1 ) }
446
+ </ div >
447
+ ) }
448
+
449
+ { /* Any other regular steps */ }
450
+ { otherRegularSteps . length > 0 && (
451
+ < div className = "grid grid-cols-1 gap-2 mt-3" >
452
+ { otherRegularSteps . map ( ( step , index ) =>
453
+ renderStep ( step , index + 2 ) ,
454
+ ) }
455
+ </ div >
456
+ ) }
457
+ </ div >
458
+ ) ;
388
459
} ) ( ) }
389
460
</ div >
390
461
</ div >
0 commit comments