@@ -8,6 +8,7 @@ import clsx from 'clsx'
88import { Button , ChevronDown } from '@/components/emcn'
99import type { TraceSpan } from '@/stores/logs/filters/types'
1010import '@/components/emcn/components/code/code.css'
11+ import { WorkflowIcon } from '@/components/icons'
1112import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config'
1213import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
1314import { getBlock , getBlockByToolName } from '@/blocks'
@@ -120,6 +121,14 @@ function getBlockColor(type: string): string {
120121 return '#2FA1FF'
121122 case 'api' :
122123 return '#2F55FF'
124+ case 'loop' :
125+ case 'loop-iteration' :
126+ return '#2FB3FF'
127+ case 'parallel' :
128+ case 'parallel-iteration' :
129+ return '#FEE12B'
130+ case 'workflow' :
131+ return '#705335'
123132 default :
124133 return '#6b7280'
125134 }
@@ -134,12 +143,15 @@ function getBlockIconAndColor(type: string): {
134143} {
135144 const lowerType = type . toLowerCase ( )
136145
137- if ( lowerType === 'loop' ) {
146+ if ( lowerType === 'loop' || lowerType === 'loop-iteration' ) {
138147 return { icon : LoopTool . icon , bgColor : LoopTool . bgColor }
139148 }
140- if ( lowerType === 'parallel' ) {
149+ if ( lowerType === 'parallel' || lowerType === 'parallel-iteration' ) {
141150 return { icon : ParallelTool . icon , bgColor : ParallelTool . bgColor }
142151 }
152+ if ( lowerType === 'workflow' ) {
153+ return { icon : WorkflowIcon , bgColor : '#705335' }
154+ }
143155
144156 const blockType = lowerType === 'model' ? 'agent' : lowerType
145157 const blockConfig = getBlock ( blockType )
@@ -289,15 +301,11 @@ function InputOutputSection({
289301 { isExpanded && (
290302 < div >
291303 { isError && typeof data === 'object' && data !== null && 'error' in data ? (
292- < div
293- className = 'rounded-[6px] px-[10px] py-[8px]'
294- style = { {
295- backgroundColor : 'var(--terminal-status-error-bg)' ,
296- color : 'var(--text-error)' ,
297- } }
298- >
299- < div className = 'font-medium text-[12px]' > Error</ div >
300- < div className = 'mt-[4px] text-[12px]' > { ( data as { error : string } ) . error } </ div >
304+ < div className = 'rounded-[4px] border border-[rgba(234,67,53,0.24)] bg-[rgba(234,67,53,0.08)] px-[10px] py-[8px]' >
305+ < div className = 'font-medium text-[#EA4335] text-[12px]' > Error</ div >
306+ < div className = 'mt-[4px] text-[#FF8076] text-[12px]' >
307+ { ( data as { error : string } ) . error }
308+ </ div >
301309 </ div >
302310 ) : (
303311 < div className = 'code-editor-theme overflow-hidden rounded-[6px] bg-[var(--surface-3)] px-[10px] py-[8px]' >
@@ -313,6 +321,116 @@ function InputOutputSection({
313321 )
314322}
315323
324+ interface NestedBlockItemProps {
325+ span : TraceSpan
326+ parentId : string
327+ index : number
328+ expandedSections : Set < string >
329+ onToggle : ( section : string ) => void
330+ workflowStartTime : number
331+ totalDuration : number
332+ }
333+
334+ /**
335+ * Recursive component for rendering nested blocks at any depth
336+ */
337+ function NestedBlockItem ( {
338+ span,
339+ parentId,
340+ index,
341+ expandedSections,
342+ onToggle,
343+ workflowStartTime,
344+ totalDuration,
345+ } : NestedBlockItemProps ) : React . ReactNode {
346+ const spanId = span . id || `${ parentId } -nested-${ index } `
347+ const isError = span . status === 'error'
348+ const toolBlock =
349+ span . type ?. toLowerCase ( ) === 'tool' && span . name ? getBlockByToolName ( span . name ) : null
350+ const { icon : SpanIcon , bgColor } = toolBlock
351+ ? { icon : toolBlock . icon , bgColor : toolBlock . bgColor }
352+ : getBlockIconAndColor ( span . type )
353+
354+ return (
355+ < div className = 'flex flex-col gap-[8px]' >
356+ < div className = 'flex items-center justify-between' >
357+ < div className = 'flex items-center gap-[8px]' >
358+ < div
359+ className = 'relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
360+ style = { { background : bgColor } }
361+ >
362+ { SpanIcon && < SpanIcon className = { clsx ( 'text-white' , '!h-[9px] !w-[9px]' ) } /> }
363+ </ div >
364+ < span
365+ className = 'font-medium text-[12px]'
366+ style = { {
367+ color : isError ? 'var(--text-error)' : 'var(--text-secondary)' ,
368+ } }
369+ >
370+ { span . name }
371+ </ span >
372+ </ div >
373+ < span className = 'font-medium text-[12px] text-[var(--text-tertiary)]' >
374+ { formatDuration ( span . duration || 0 ) }
375+ </ span >
376+ </ div >
377+
378+ < ProgressBar
379+ span = { span }
380+ childSpans = { span . children }
381+ workflowStartTime = { workflowStartTime }
382+ totalDuration = { totalDuration }
383+ />
384+
385+ { span . input && (
386+ < InputOutputSection
387+ label = 'Input'
388+ data = { span . input }
389+ isError = { false }
390+ spanId = { `${ spanId } -input` }
391+ sectionType = 'input'
392+ expandedSections = { expandedSections }
393+ onToggle = { onToggle }
394+ />
395+ ) }
396+
397+ { span . input && span . output && (
398+ < div className = 'border-[var(--border)] border-t border-dashed' />
399+ ) }
400+
401+ { span . output && (
402+ < InputOutputSection
403+ label = { isError ? 'Error' : 'Output' }
404+ data = { span . output }
405+ isError = { isError }
406+ spanId = { `${ spanId } -output` }
407+ sectionType = 'output'
408+ expandedSections = { expandedSections }
409+ onToggle = { onToggle }
410+ />
411+ ) }
412+
413+ { /* Recursively render children */ }
414+ { span . children && span . children . length > 0 && (
415+ < div className = 'mt-[8px] flex flex-col gap-[16px] border-[var(--border)] border-l-2 pl-[10px]' >
416+ { span . children . map ( ( child , childIndex ) => (
417+ < NestedBlockItem
418+ key = { child . id || `${ spanId } -child-${ childIndex } ` }
419+ span = { child }
420+ parentId = { spanId }
421+ index = { childIndex }
422+ expandedSections = { expandedSections }
423+ onToggle = { onToggle }
424+ workflowStartTime = { workflowStartTime }
425+ totalDuration = { totalDuration }
426+ />
427+ ) ) }
428+ </ div >
429+ ) }
430+ </ div >
431+ )
432+ }
433+
316434interface TraceSpanItemProps {
317435 span : TraceSpan
318436 totalDuration : number
@@ -346,11 +464,22 @@ function TraceSpanItem({
346464 const hasOutput = Boolean ( span . output )
347465 const isError = span . status === 'error'
348466
349- const inlineChildTypes = new Set ( [ 'tool' , 'model' ] )
350- const inlineChildren =
351- span . children ?. filter ( ( child ) => inlineChildTypes . has ( child . type ?. toLowerCase ( ) || '' ) ) || [ ]
352- const otherChildren =
353- span . children ?. filter ( ( child ) => ! inlineChildTypes . has ( child . type ?. toLowerCase ( ) || '' ) ) || [ ]
467+ const inlineChildTypes = new Set ( [
468+ 'tool' ,
469+ 'model' ,
470+ 'loop-iteration' ,
471+ 'parallel-iteration' ,
472+ 'workflow' ,
473+ ] )
474+
475+ // For workflow-in-workflow blocks, all children should be rendered inline/nested
476+ const isWorkflowBlock = span . type ?. toLowerCase ( ) === 'workflow'
477+ const inlineChildren = isWorkflowBlock
478+ ? span . children || [ ]
479+ : span . children ?. filter ( ( child ) => inlineChildTypes . has ( child . type ?. toLowerCase ( ) || '' ) ) || [ ]
480+ const otherChildren = isWorkflowBlock
481+ ? [ ]
482+ : span . children ?. filter ( ( child ) => ! inlineChildTypes . has ( child . type ?. toLowerCase ( ) || '' ) ) || [ ]
354483
355484 const toolCallSpans = useMemo ( ( ) => {
356485 if ( ! hasToolCalls ) return [ ]
@@ -502,7 +631,14 @@ function TraceSpanItem({
502631
503632 < ProgressBar
504633 span = { childSpan }
505- childSpans = { undefined }
634+ childSpans = {
635+ childSpan . type ?. toLowerCase ( ) === 'loop-iteration' ||
636+ childSpan . type ?. toLowerCase ( ) === 'parallel-iteration' ||
637+ childSpan . type ?. toLowerCase ( ) === 'workflow' ||
638+ ( isWorkflowBlock && childSpan . children && childSpan . children . length > 0 )
639+ ? childSpan . children
640+ : undefined
641+ }
506642 workflowStartTime = { workflowStartTime }
507643 totalDuration = { totalDuration }
508644 />
@@ -534,6 +670,29 @@ function TraceSpanItem({
534670 onToggle = { handleSectionToggle }
535671 />
536672 ) }
673+
674+ { /* Render nested blocks for loop/parallel iterations, nested workflows, and workflow block children */ }
675+ { ( childSpan . type ?. toLowerCase ( ) === 'loop-iteration' ||
676+ childSpan . type ?. toLowerCase ( ) === 'parallel-iteration' ||
677+ childSpan . type ?. toLowerCase ( ) === 'workflow' ||
678+ isWorkflowBlock ) &&
679+ childSpan . children &&
680+ childSpan . children . length > 0 && (
681+ < div className = 'mt-[8px] flex flex-col gap-[16px] border-[var(--border)] border-l-2 pl-[10px]' >
682+ { childSpan . children . map ( ( nestedChild , nestedIndex ) => (
683+ < NestedBlockItem
684+ key = { nestedChild . id || `${ childId } -nested-${ nestedIndex } ` }
685+ span = { nestedChild }
686+ parentId = { childId }
687+ index = { nestedIndex }
688+ expandedSections = { expandedSections }
689+ onToggle = { handleSectionToggle }
690+ workflowStartTime = { workflowStartTime }
691+ totalDuration = { totalDuration }
692+ />
693+ ) ) }
694+ </ div >
695+ ) }
537696 </ div >
538697 </ div >
539698 )
0 commit comments