Severian's picture
initial commit
a8b3f00
raw
history blame
9.44 kB
'use client'
import type { FC } from 'react'
import
React,
{
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 { NodeTracing } from '@/types/workflow'
type TracingPanelProps = {
list: NodeTracing[]
onShowIterationDetail?: (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,
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}
justShowIterationNavArrow={true}
hideInfo={hideNodeInfo}
hideProcessDetail={hideNodeProcessDetail}
/>
</div>
)
}
}
return (
<div className={cn(className || 'bg-components-panel-bg', 'py-2')}>
{treeNodes.map(renderNode)}
</div>
)
}
export default TracingPanel