| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 | 'use client'import type { FC } from 'react'importReact,{  useCallback,  useState,} from 'react'import cn from 'classnames'import {  RiArrowDownSLine,  RiMenu4Line,} from '@remixicon/react'import { useTranslation } from 'react-i18next'import NodePanel from './node'import {  BlockEnum,} from '@/app/components/workflow/types'import type { IterationDurationMap, NodeTracing } from '@/types/workflow'type TracingPanelProps = {  list: NodeTracing[]  onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void  onShowRetryDetail?: (detail: NodeTracing[]) => void  className?: string  hideNodeInfo?: boolean  hideNodeProcessDetail?: boolean}type TracingNodeProps = {  id: string  uniqueId: string  isParallel: boolean  data: NodeTracing | null  children: TracingNodeProps[]  parallelTitle?: string  branchTitle?: string  hideNodeInfo?: boolean  hideNodeProcessDetail?: boolean}function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): TracingNodeProps[] {  const rootNodes: TracingNodeProps[] = []  const parallelStacks: { [key: string]: TracingNodeProps } = {}  const levelCounts: { [key: string]: number } = {}  const parallelChildCounts: { [key: string]: Set<string> } = {}  let uniqueIdCounter = 0  const getUniqueId = () => {    uniqueIdCounter++    return `unique-${uniqueIdCounter}`  }  const getParallelTitle = (parentId: string | null): string => {    const levelKey = parentId || 'root'    if (!levelCounts[levelKey])      levelCounts[levelKey] = 0    levelCounts[levelKey]++    const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''    const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1    const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''    return `${t('workflow.common.parallel')}-${levelNumber}${letter}`  }  const getBranchTitle = (parentId: string | null, branchNum: number): string => {    const levelKey = parentId || 'root'    const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''    const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1    const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''    const branchLetter = String.fromCharCode(64 + branchNum)    return `${t('workflow.common.branch')}-${levelNumber}${letter}-${branchLetter}`  }  // Count parallel children (for figuring out if we need to use letters)  for (const node of nodes) {    const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null    const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null    if (parallel_id) {      const parentKey = parent_parallel_id || 'root'      if (!parallelChildCounts[parentKey])        parallelChildCounts[parentKey] = new Set()      parallelChildCounts[parentKey].add(parallel_id)    }  }  for (const node of nodes) {    const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null    const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null    const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null    const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null    if (!parallel_id || node.node_type === BlockEnum.End) {      rootNodes.push({        id: node.id,        uniqueId: getUniqueId(),        isParallel: false,        data: node,        children: [],      })    }    else {      if (!parallelStacks[parallel_id]) {        const newParallelGroup: TracingNodeProps = {          id: parallel_id,          uniqueId: getUniqueId(),          isParallel: true,          data: null,          children: [],          parallelTitle: '',        }        parallelStacks[parallel_id] = newParallelGroup        if (parent_parallel_id && parallelStacks[parent_parallel_id]) {          const sameBranchIndex = parallelStacks[parent_parallel_id].children.findLastIndex(c =>            c.data?.execution_metadata?.parallel_start_node_id === parent_parallel_start_node_id || c.data?.parallel_start_node_id === parent_parallel_start_node_id,          )          parallelStacks[parent_parallel_id].children.splice(sameBranchIndex + 1, 0, newParallelGroup)          newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)        }        else {          newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)          rootNodes.push(newParallelGroup)        }      }      const branchTitle = parallel_start_node_id === node.node_id ? getBranchTitle(parent_parallel_id, parallelStacks[parallel_id].children.length + 1) : ''      if (branchTitle) {        parallelStacks[parallel_id].children.push({          id: node.id,          uniqueId: getUniqueId(),          isParallel: false,          data: node,          children: [],          branchTitle,        })      }      else {        let sameBranchIndex = parallelStacks[parallel_id].children.findLastIndex(c =>          c.data?.execution_metadata?.parallel_start_node_id === parallel_start_node_id || c.data?.parallel_start_node_id === parallel_start_node_id,        )        if (parallelStacks[parallel_id].children[sameBranchIndex + 1]?.isParallel)          sameBranchIndex++        parallelStacks[parallel_id].children.splice(sameBranchIndex + 1, 0, {          id: node.id,          uniqueId: getUniqueId(),          isParallel: false,          data: node,          children: [],          branchTitle,        })      }    }  }  return rootNodes}const TracingPanel: FC<TracingPanelProps> = ({  list,  onShowIterationDetail,  onShowRetryDetail,  className,  hideNodeInfo = false,  hideNodeProcessDetail = false,}) => {  const { t } = useTranslation()  const treeNodes = buildLogTree(list, t)  const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())  const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)  const toggleCollapse = (id: string) => {    setCollapsedNodes((prev) => {      const newSet = new Set(prev)      if (newSet.has(id))        newSet.delete(id)      else        newSet.add(id)      return newSet    })  }  const handleParallelMouseEnter = useCallback((id: string) => {    setHoveredParallel(id)  }, [])  const handleParallelMouseLeave = useCallback((e: React.MouseEvent) => {    const relatedTarget = e.relatedTarget as Element | null    if (relatedTarget && 'closest' in relatedTarget) {      const closestParallel = relatedTarget.closest('[data-parallel-id]')      if (closestParallel)        setHoveredParallel(closestParallel.getAttribute('data-parallel-id'))      else        setHoveredParallel(null)    }    else {      setHoveredParallel(null)    }  }, [])  const renderNode = (node: TracingNodeProps) => {    if (node.isParallel) {      const isCollapsed = collapsedNodes.has(node.id)      const isHovered = hoveredParallel === node.id      return (        <div          key={node.uniqueId}          className="ml-4 mb-2 relative"          data-parallel-id={node.id}          onMouseEnter={() => handleParallelMouseEnter(node.id)}          onMouseLeave={handleParallelMouseLeave}        >          <div className="flex items-center mb-1">            <button              onClick={() => toggleCollapse(node.id)}              className={cn(                'mr-2 transition-colors',                isHovered ? 'rounded border-components-button-primary-border bg-components-button-primary-bg text-text-primary-on-surface' : 'text-text-secondary hover:text-text-primary',              )}            >              {isHovered ? <RiArrowDownSLine className="w-3 h-3" /> : <RiMenu4Line className="w-3 h-3 text-text-tertiary" />}            </button>            <div className="system-xs-semibold-uppercase text-text-secondary flex items-center">              <span>{node.parallelTitle}</span>            </div>            <div              className="mx-2 flex-grow h-px bg-divider-subtle"              style={{ background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08), rgba(255, 255, 255, 0)' }}            ></div>          </div>          <div className={`pl-2 relative ${isCollapsed ? 'hidden' : ''}`}>            <div className={cn(              'absolute top-0 bottom-0 left-[5px] w-[2px]',              isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',            )}></div>            {node.children.map(renderNode)}          </div>        </div>      )    }    else {      const isHovered = hoveredParallel === node.id      return (        <div key={node.uniqueId}>          <div className={cn('pl-4 -mb-1.5 system-2xs-medium-uppercase', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}>            {node.branchTitle}          </div>          <NodePanel            nodeInfo={node.data!}            onShowIterationDetail={onShowIterationDetail}            onShowRetryDetail={onShowRetryDetail}            justShowIterationNavArrow={true}            justShowRetryNavArrow={true}            hideInfo={hideNodeInfo}            hideProcessDetail={hideNodeProcessDetail}          />        </div>      )    }  }  return (    <div className={cn(className || 'bg-components-panel-bg', 'py-2')}>      {treeNodes.map(renderNode)}    </div>  )}export default TracingPanel
 |