| | |
| | | RiMenu4Line, |
| | | } from '@remixicon/react' |
| | | import { useTranslation } from 'react-i18next' |
| | | import { useLogs } from './hooks' |
| | | import NodePanel from './node' |
| | | import SpecialResultPanel from './special-result-panel' |
| | | import type { NodeTracing } from '@/types/workflow' |
| | | import formatNodeList from '@/app/components/workflow/run/utils/format-log' |
| | | 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 = formatNodeList(list, t) |
| | | const treeNodes = buildLogTree(list, t) |
| | | const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set()) |
| | | const [hoveredParallel, setHoveredParallel] = useState<string | null>(null) |
| | | |
| | |
| | | } |
| | | }, []) |
| | | |
| | | const { |
| | | showSpecialResultPanel, |
| | | |
| | | showRetryDetail, |
| | | setShowRetryDetailFalse, |
| | | retryResultList, |
| | | handleShowRetryResultList, |
| | | |
| | | showIteratingDetail, |
| | | setShowIteratingDetailFalse, |
| | | iterationResultList, |
| | | iterationResultDurationMap, |
| | | handleShowIterationResultList, |
| | | |
| | | showLoopingDetail, |
| | | setShowLoopingDetailFalse, |
| | | loopResultList, |
| | | loopResultDurationMap, |
| | | loopResultVariableMap, |
| | | handleShowLoopResultList, |
| | | |
| | | agentOrToolLogItemStack, |
| | | agentOrToolLogListMap, |
| | | handleShowAgentOrToolLog, |
| | | } = useLogs() |
| | | |
| | | const renderNode = (node: NodeTracing) => { |
| | | const isParallelFirstNode = !!node.parallelDetail?.isParallelStartNode |
| | | if (isParallelFirstNode) { |
| | | const parallelDetail = node.parallelDetail! |
| | | const renderNode = (node: TracingNodeProps) => { |
| | | if (node.isParallel) { |
| | | const isCollapsed = collapsedNodes.has(node.id) |
| | | const isHovered = hoveredParallel === node.id |
| | | return ( |
| | | <div |
| | | key={node.id} |
| | | className="relative mb-2 ml-4" |
| | | key={node.uniqueId} |
| | | className="ml-4 mb-2 relative" |
| | | data-parallel-id={node.id} |
| | | onMouseEnter={() => handleParallelMouseEnter(node.id)} |
| | | onMouseLeave={handleParallelMouseLeave} |
| | | > |
| | | <div className="mb-1 flex items-center"> |
| | | <div className="flex items-center mb-1"> |
| | | <button |
| | | onClick={() => toggleCollapse(node.id)} |
| | | className={cn( |
| | |
| | | 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="h-3 w-3" /> : <RiMenu4Line className="h-3 w-3 text-text-tertiary" />} |
| | | {isHovered ? <RiArrowDownSLine className="w-3 h-3" /> : <RiMenu4Line className="w-3 h-3 text-text-tertiary" />} |
| | | </button> |
| | | <div className="system-xs-semibold-uppercase flex items-center text-text-secondary"> |
| | | <span>{parallelDetail.parallelTitle}</span> |
| | | <div className="system-xs-semibold-uppercase text-text-secondary flex items-center"> |
| | | <span>{node.parallelTitle}</span> |
| | | </div> |
| | | <div |
| | | className="mx-2 h-px grow bg-divider-subtle" |
| | | 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={`relative pl-2 ${isCollapsed ? 'hidden' : ''}`}> |
| | | <div className={`pl-2 relative ${isCollapsed ? 'hidden' : ''}`}> |
| | | <div className={cn( |
| | | 'absolute bottom-0 left-[5px] top-0 w-[2px]', |
| | | 'absolute top-0 bottom-0 left-[5px] w-[2px]', |
| | | isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle', |
| | | )}></div> |
| | | {parallelDetail.children!.map(renderNode)} |
| | | {node.children.map(renderNode)} |
| | | </div> |
| | | </div> |
| | | ) |
| | |
| | | else { |
| | | const isHovered = hoveredParallel === node.id |
| | | return ( |
| | | <div key={node.id}> |
| | | <div className={cn('system-2xs-medium-uppercase -mb-1.5 pl-4', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}> |
| | | {node?.parallelDetail?.branchTitle} |
| | | <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!} |
| | | allExecutions={list} |
| | | onShowIterationDetail={handleShowIterationResultList} |
| | | onShowLoopDetail={handleShowLoopResultList} |
| | | onShowRetryDetail={handleShowRetryResultList} |
| | | onShowAgentOrToolLog={handleShowAgentOrToolLog} |
| | | nodeInfo={node.data!} |
| | | onShowIterationDetail={onShowIterationDetail} |
| | | onShowRetryDetail={onShowRetryDetail} |
| | | justShowIterationNavArrow={true} |
| | | justShowRetryNavArrow={true} |
| | | hideInfo={hideNodeInfo} |
| | | hideProcessDetail={hideNodeProcessDetail} |
| | | /> |
| | |
| | | } |
| | | } |
| | | |
| | | if (showSpecialResultPanel) { |
| | | return ( |
| | | <SpecialResultPanel |
| | | showRetryDetail={showRetryDetail} |
| | | setShowRetryDetailFalse={setShowRetryDetailFalse} |
| | | retryResultList={retryResultList} |
| | | |
| | | showIteratingDetail={showIteratingDetail} |
| | | setShowIteratingDetailFalse={setShowIteratingDetailFalse} |
| | | iterationResultList={iterationResultList} |
| | | iterationResultDurationMap={iterationResultDurationMap} |
| | | |
| | | showLoopingDetail={showLoopingDetail} |
| | | setShowLoopingDetailFalse={setShowLoopingDetailFalse} |
| | | loopResultList={loopResultList} |
| | | loopResultDurationMap={loopResultDurationMap} |
| | | loopResultVariableMap={loopResultVariableMap} |
| | | |
| | | agentOrToolLogItemStack={agentOrToolLogItemStack} |
| | | agentOrToolLogListMap={agentOrToolLogListMap} |
| | | handleShowAgentOrToolLog={handleShowAgentOrToolLog} |
| | | /> |
| | | ) |
| | | } |
| | | |
| | | return ( |
| | | <div |
| | | className={cn('py-2', className)} |
| | | onClick={(e) => { |
| | | e.stopPropagation() |
| | | e.nativeEvent.stopImmediatePropagation() |
| | | }} |
| | | > |
| | | <div className={cn(className || 'bg-components-panel-bg', 'py-2')}> |
| | | {treeNodes.map(renderNode)} |
| | | </div> |
| | | ) |