tracing-panel.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. 'use client'
  2. import type { FC } from 'react'
  3. import
  4. React,
  5. {
  6. useCallback,
  7. useState,
  8. } from 'react'
  9. import cn from 'classnames'
  10. import {
  11. RiArrowDownSLine,
  12. RiMenu4Line,
  13. } from '@remixicon/react'
  14. import NodePanel from './node'
  15. import {
  16. BlockEnum,
  17. } from '@/app/components/workflow/types'
  18. import type { NodeTracing } from '@/types/workflow'
  19. type TracingPanelProps = {
  20. list: NodeTracing[]
  21. onShowIterationDetail?: (detail: NodeTracing[][]) => void
  22. className?: string
  23. hideNodeInfo?: boolean
  24. hideNodeProcessDetail?: boolean
  25. }
  26. type TracingNodeProps = {
  27. id: string
  28. uniqueId: string
  29. isParallel: boolean
  30. data: NodeTracing | null
  31. children: TracingNodeProps[]
  32. parallelTitle?: string
  33. branchTitle?: string
  34. hideNodeInfo?: boolean
  35. hideNodeProcessDetail?: boolean
  36. }
  37. function buildLogTree(nodes: NodeTracing[]): TracingNodeProps[] {
  38. const rootNodes: TracingNodeProps[] = []
  39. const parallelStacks: { [key: string]: TracingNodeProps } = {}
  40. const levelCounts: { [key: string]: number } = {}
  41. const parallelChildCounts: { [key: string]: Set<string> } = {}
  42. let uniqueIdCounter = 0
  43. const getUniqueId = () => {
  44. uniqueIdCounter++
  45. return `unique-${uniqueIdCounter}`
  46. }
  47. const getParallelTitle = (parentId: string | null): string => {
  48. const levelKey = parentId || 'root'
  49. if (!levelCounts[levelKey])
  50. levelCounts[levelKey] = 0
  51. levelCounts[levelKey]++
  52. const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
  53. const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
  54. const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
  55. return `PARALLEL-${levelNumber}${letter}`
  56. }
  57. const getBranchTitle = (parentId: string | null, branchNum: number): string => {
  58. const levelKey = parentId || 'root'
  59. const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
  60. const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
  61. const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
  62. const branchLetter = String.fromCharCode(64 + branchNum)
  63. return `BRANCH-${levelNumber}${letter}-${branchLetter}`
  64. }
  65. // Count parallel children (for figuring out if we need to use letters)
  66. for (const node of nodes) {
  67. const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
  68. const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
  69. if (parallel_id) {
  70. const parentKey = parent_parallel_id || 'root'
  71. if (!parallelChildCounts[parentKey])
  72. parallelChildCounts[parentKey] = new Set()
  73. parallelChildCounts[parentKey].add(parallel_id)
  74. }
  75. }
  76. for (const node of nodes) {
  77. const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
  78. const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
  79. const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
  80. const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
  81. if (!parallel_id || node.node_type === BlockEnum.End) {
  82. rootNodes.push({
  83. id: node.id,
  84. uniqueId: getUniqueId(),
  85. isParallel: false,
  86. data: node,
  87. children: [],
  88. })
  89. }
  90. else {
  91. if (!parallelStacks[parallel_id]) {
  92. const newParallelGroup: TracingNodeProps = {
  93. id: parallel_id,
  94. uniqueId: getUniqueId(),
  95. isParallel: true,
  96. data: null,
  97. children: [],
  98. parallelTitle: '',
  99. }
  100. parallelStacks[parallel_id] = newParallelGroup
  101. if (parent_parallel_id && parallelStacks[parent_parallel_id]) {
  102. const sameBranchIndex = parallelStacks[parent_parallel_id].children.findLastIndex(c =>
  103. c.data?.execution_metadata?.parallel_start_node_id === parent_parallel_start_node_id || c.data?.parallel_start_node_id === parent_parallel_start_node_id,
  104. )
  105. parallelStacks[parent_parallel_id].children.splice(sameBranchIndex + 1, 0, newParallelGroup)
  106. newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
  107. }
  108. else {
  109. newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
  110. rootNodes.push(newParallelGroup)
  111. }
  112. }
  113. const branchTitle = parallel_start_node_id === node.node_id ? getBranchTitle(parent_parallel_id, parallelStacks[parallel_id].children.length + 1) : ''
  114. if (branchTitle) {
  115. parallelStacks[parallel_id].children.push({
  116. id: node.id,
  117. uniqueId: getUniqueId(),
  118. isParallel: false,
  119. data: node,
  120. children: [],
  121. branchTitle,
  122. })
  123. }
  124. else {
  125. let sameBranchIndex = parallelStacks[parallel_id].children.findLastIndex(c =>
  126. c.data?.execution_metadata?.parallel_start_node_id === parallel_start_node_id || c.data?.parallel_start_node_id === parallel_start_node_id,
  127. )
  128. if (parallelStacks[parallel_id].children[sameBranchIndex + 1]?.isParallel)
  129. sameBranchIndex++
  130. parallelStacks[parallel_id].children.splice(sameBranchIndex + 1, 0, {
  131. id: node.id,
  132. uniqueId: getUniqueId(),
  133. isParallel: false,
  134. data: node,
  135. children: [],
  136. branchTitle,
  137. })
  138. }
  139. }
  140. }
  141. return rootNodes
  142. }
  143. const TracingPanel: FC<TracingPanelProps> = ({
  144. list,
  145. onShowIterationDetail,
  146. className,
  147. hideNodeInfo = false,
  148. hideNodeProcessDetail = false,
  149. }) => {
  150. const treeNodes = buildLogTree(list)
  151. const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
  152. const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
  153. const toggleCollapse = (id: string) => {
  154. setCollapsedNodes((prev) => {
  155. const newSet = new Set(prev)
  156. if (newSet.has(id))
  157. newSet.delete(id)
  158. else
  159. newSet.add(id)
  160. return newSet
  161. })
  162. }
  163. const handleParallelMouseEnter = useCallback((id: string) => {
  164. setHoveredParallel(id)
  165. }, [])
  166. const handleParallelMouseLeave = useCallback((e: React.MouseEvent) => {
  167. const relatedTarget = e.relatedTarget as Element | null
  168. if (relatedTarget && 'closest' in relatedTarget) {
  169. const closestParallel = relatedTarget.closest('[data-parallel-id]')
  170. if (closestParallel)
  171. setHoveredParallel(closestParallel.getAttribute('data-parallel-id'))
  172. else
  173. setHoveredParallel(null)
  174. }
  175. else {
  176. setHoveredParallel(null)
  177. }
  178. }, [])
  179. const renderNode = (node: TracingNodeProps) => {
  180. if (node.isParallel) {
  181. const isCollapsed = collapsedNodes.has(node.id)
  182. const isHovered = hoveredParallel === node.id
  183. return (
  184. <div
  185. key={node.uniqueId}
  186. className="ml-4 mb-2 relative"
  187. data-parallel-id={node.id}
  188. onMouseEnter={() => handleParallelMouseEnter(node.id)}
  189. onMouseLeave={handleParallelMouseLeave}
  190. >
  191. <div className="flex items-center mb-1">
  192. <button
  193. onClick={() => toggleCollapse(node.id)}
  194. className={cn(
  195. 'mr-2 transition-colors',
  196. isHovered ? 'rounded border-components-button-primary-border bg-components-button-primary-bg text-text-primary-on-surface' : 'text-text-secondary hover:text-text-primary',
  197. )}
  198. >
  199. {isHovered ? <RiArrowDownSLine className="w-3 h-3" /> : <RiMenu4Line className="w-3 h-3 text-text-tertiary" />}
  200. </button>
  201. <div className="system-xs-semibold-uppercase text-text-secondary flex items-center">
  202. <span>{node.parallelTitle}</span>
  203. </div>
  204. <div
  205. className="mx-2 flex-grow h-px bg-divider-subtle"
  206. style={{ background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08), rgba(255, 255, 255, 0)' }}
  207. ></div>
  208. </div>
  209. <div className={`pl-2 relative ${isCollapsed ? 'hidden' : ''}`}>
  210. <div className={cn(
  211. 'absolute top-0 bottom-0 left-[5px] w-[2px]',
  212. isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
  213. )}></div>
  214. {node.children.map(renderNode)}
  215. </div>
  216. </div>
  217. )
  218. }
  219. else {
  220. const isHovered = hoveredParallel === node.id
  221. return (
  222. <div key={node.uniqueId}>
  223. <div className={cn('pl-4 -mb-1.5 system-2xs-medium-uppercase', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}>
  224. {node.branchTitle}
  225. </div>
  226. <NodePanel
  227. nodeInfo={node.data!}
  228. onShowIterationDetail={onShowIterationDetail}
  229. justShowIterationNavArrow={true}
  230. hideInfo={hideNodeInfo}
  231. hideProcessDetail={hideNodeProcessDetail}
  232. />
  233. </div>
  234. )
  235. }
  236. }
  237. return (
  238. <div className={cn(className || 'bg-components-panel-bg', 'py-2')}>
  239. {treeNodes.map(renderNode)}
  240. </div>
  241. )
  242. }
  243. export default TracingPanel