mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-14 15:35:55 +08:00
update the workflow parallel log
This commit is contained in:
parent
f43596f226
commit
3e257ae907
@ -4,7 +4,7 @@ import type { FC } from 'react'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
RiArrowRightSLine,
|
RiArrowRightSLine,
|
||||||
RiCheckboxCircleLine,
|
RiCheckboxCircleFill,
|
||||||
RiErrorWarningLine,
|
RiErrorWarningLine,
|
||||||
RiLoader2Line,
|
RiLoader2Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
@ -72,20 +72,20 @@ const NodePanel: FC<Props> = ({
|
|||||||
onShowIterationDetail?.(nodeInfo.details || [])
|
onShowIterationDetail?.(nodeInfo.details || [])
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={cn('px-4 py-1', className, hideInfo && '!p-0')}>
|
<div className={cn('px-2 py-1', className, hideInfo && '!p-0')}>
|
||||||
<div className={cn('group transition-all bg-white border border-gray-100 rounded-2xl shadow-xs hover:shadow-md', hideInfo && '!rounded-lg')}>
|
<div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadows-shadow-xs hover:shadow-md'>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center pl-[6px] pr-3 cursor-pointer',
|
'flex items-center pl-1 pr-2 cursor-pointer',
|
||||||
hideInfo ? 'py-2' : 'py-3',
|
hideInfo ? 'py-2' : 'py-1.5',
|
||||||
!collapseState && (hideInfo ? '!pb-1' : '!pb-2'),
|
!collapseState && (hideInfo ? '!pb-1' : '!pb-1.5'),
|
||||||
)}
|
)}
|
||||||
onClick={() => setCollapseState(!collapseState)}
|
onClick={() => setCollapseState(!collapseState)}
|
||||||
>
|
>
|
||||||
{!hideProcessDetail && (
|
{!hideProcessDetail && (
|
||||||
<RiArrowRightSLine
|
<RiArrowRightSLine
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 w-3 h-3 mr-1 text-gray-400 transition-all group-hover:text-gray-500',
|
'shrink-0 w-4 h-4 mr-1 text-text-quaternary transition-all group-hover:text-text-tertiary',
|
||||||
!collapseState && 'rotate-90',
|
!collapseState && 'rotate-90',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -93,14 +93,14 @@ const NodePanel: FC<Props> = ({
|
|||||||
|
|
||||||
<BlockIcon size={hideInfo ? 'xs' : 'sm'} className={cn('shrink-0 mr-2', hideInfo && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
|
<BlockIcon size={hideInfo ? 'xs' : 'sm'} className={cn('shrink-0 mr-2', hideInfo && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'grow text-gray-700 text-[13px] leading-[16px] font-semibold truncate',
|
'grow text-text-secondary system-xs-semibold-uppercase truncate',
|
||||||
hideInfo && '!text-xs',
|
hideInfo && '!text-xs',
|
||||||
)} title={nodeInfo.title}>{nodeInfo.title}</div>
|
)} title={nodeInfo.title}>{nodeInfo.title}</div>
|
||||||
{nodeInfo.status !== 'running' && !hideInfo && (
|
{nodeInfo.status !== 'running' && !hideInfo && (
|
||||||
<div className='shrink-0 text-gray-500 text-xs leading-[18px]'>{`${getTime(nodeInfo.elapsed_time || 0)} · ${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens`}</div>
|
<div className='shrink-0 text-gray-500 text-xs leading-[18px]'>{`${getTime(nodeInfo.elapsed_time || 0)} · ${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens`}</div>
|
||||||
)}
|
)}
|
||||||
{nodeInfo.status === 'succeeded' && (
|
{nodeInfo.status === 'succeeded' && (
|
||||||
<RiCheckboxCircleLine className='shrink-0 ml-2 w-3.5 h-3.5 text-[#12B76A]' />
|
<RiCheckboxCircleFill className='shrink-0 ml-2 w-3.5 h-3.5 text-text-success' />
|
||||||
)}
|
)}
|
||||||
{nodeInfo.status === 'failed' && (
|
{nodeInfo.status === 'failed' && (
|
||||||
<RiErrorWarningLine className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F04438]' />
|
<RiErrorWarningLine className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F04438]' />
|
||||||
@ -109,7 +109,7 @@ const NodePanel: FC<Props> = ({
|
|||||||
<AlertTriangle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F79009]' />
|
<AlertTriangle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F79009]' />
|
||||||
)}
|
)}
|
||||||
{nodeInfo.status === 'running' && (
|
{nodeInfo.status === 'running' && (
|
||||||
<div className='shrink-0 flex items-center text-primary-600 text-[13px] leading-[16px] font-medium'>
|
<div className='shrink-0 flex items-center text-text-accent text-[13px] leading-[16px] font-medium'>
|
||||||
<span className='mr-2 text-xs font-normal'>Running</span>
|
<span className='mr-2 text-xs font-normal'>Running</span>
|
||||||
<RiLoader2Line className='w-3.5 h-3.5 animate-spin' />
|
<RiLoader2Line className='w-3.5 h-3.5 animate-spin' />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import
|
||||||
|
React,
|
||||||
|
{
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
RiMenu4Line,
|
||||||
|
} from '@remixicon/react'
|
||||||
import NodePanel from './node'
|
import NodePanel from './node'
|
||||||
import type { NodeTracing } from '@/types/workflow'
|
import type { NodeTracing } from '@/types/workflow'
|
||||||
|
|
||||||
@ -8,17 +19,191 @@ type TracingPanelProps = {
|
|||||||
onShowIterationDetail: (detail: NodeTracing[][]) => void
|
onShowIterationDetail: (detail: NodeTracing[][]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TracingNodeProps = {
|
||||||
|
id: string
|
||||||
|
isParallel: boolean
|
||||||
|
data: NodeTracing | null
|
||||||
|
children: TracingNodeProps[]
|
||||||
|
parallelTitle?: string
|
||||||
|
branchTitle?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLogTree(nodes: NodeTracing[]): TracingNodeProps[] {
|
||||||
|
const rootNodes: TracingNodeProps[] = []
|
||||||
|
const parallelStacks: { [key: string]: TracingNodeProps } = {}
|
||||||
|
const levelCounts: { [key: string]: number } = {}
|
||||||
|
const parallelChildCounts: { [key: string]: Set<string> } = {}
|
||||||
|
|
||||||
|
function 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 `PARALLEL-${levelNumber}${letter}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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 `BRANCH-${levelNumber}${letter}-${branchLetter}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count parallel children (for figuring out if we need to use letters)
|
||||||
|
for (const node of nodes) {
|
||||||
|
console.log(node)
|
||||||
|
const parent_parallel_id = node.execution_metadata?.parent_parallel_id ?? null
|
||||||
|
const 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.execution_metadata?.parallel_id ?? null
|
||||||
|
const parent_parallel_id = node.execution_metadata?.parent_parallel_id ?? null
|
||||||
|
const parallel_start_node_id = node.execution_metadata?.parallel_start_node_id ?? null
|
||||||
|
|
||||||
|
if (!parallel_id) {
|
||||||
|
rootNodes.push({
|
||||||
|
id: node.id,
|
||||||
|
isParallel: false,
|
||||||
|
data: node,
|
||||||
|
children: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!parallelStacks[parallel_id]) {
|
||||||
|
const newParallelGroup: TracingNodeProps = {
|
||||||
|
id: parallel_id,
|
||||||
|
isParallel: true,
|
||||||
|
data: null,
|
||||||
|
children: [],
|
||||||
|
parallelTitle: '',
|
||||||
|
}
|
||||||
|
parallelStacks[parallel_id] = newParallelGroup
|
||||||
|
|
||||||
|
if (parent_parallel_id && parallelStacks[parent_parallel_id]) {
|
||||||
|
parallelStacks[parent_parallel_id].children.push(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) : ''
|
||||||
|
parallelStacks[parallel_id].children.push({
|
||||||
|
id: node.id,
|
||||||
|
isParallel: false,
|
||||||
|
data: node,
|
||||||
|
children: [],
|
||||||
|
branchTitle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootNodes
|
||||||
|
}
|
||||||
|
|
||||||
const TracingPanel: FC<TracingPanelProps> = ({ list, onShowIterationDetail }) => {
|
const TracingPanel: FC<TracingPanelProps> = ({ list, onShowIterationDetail }) => {
|
||||||
return (
|
const treeNodes = buildLogTree(list)
|
||||||
<div className='bg-gray-50 py-2'>
|
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
|
||||||
{list.map(node => (
|
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
|
||||||
<NodePanel
|
|
||||||
|
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 HTMLElement
|
||||||
|
const closestParallel = relatedTarget?.closest('[data-parallel-id]')
|
||||||
|
if (closestParallel)
|
||||||
|
setHoveredParallel(closestParallel.getAttribute('data-parallel-id'))
|
||||||
|
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.id}
|
key={node.id}
|
||||||
nodeInfo={node}
|
className="ml-4 mb-2 relative"
|
||||||
onShowIterationDetail={onShowIterationDetail}
|
data-parallel-id={node.id}
|
||||||
justShowIterationNavArrow
|
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"></div>
|
||||||
|
</div>
|
||||||
|
<div className={`pl-2 relative ${isCollapsed ? 'hidden' : ''}`}>
|
||||||
|
<div className={cn(
|
||||||
|
'absolute top-0 bottom-0 left-1.5 w-[2px]',
|
||||||
|
isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
|
||||||
|
)}></div>
|
||||||
|
{node.children.map(renderNode)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div key={node.id}>
|
||||||
|
<div className="pl-4 text-text-quaternary system-2xs-medium-uppercase">
|
||||||
|
{node.branchTitle}
|
||||||
|
</div>
|
||||||
|
<NodePanel
|
||||||
|
nodeInfo={node.data!}
|
||||||
|
onShowIterationDetail={onShowIterationDetail}
|
||||||
|
justShowIterationNavArrow={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='bg-components-panel-bg py-2'>
|
||||||
|
{treeNodes.map(renderNode)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,10 @@ export type NodeTracing = {
|
|||||||
currency: string
|
currency: string
|
||||||
iteration_id?: string
|
iteration_id?: string
|
||||||
iteration_index?: number
|
iteration_index?: number
|
||||||
|
parallel_id?: string
|
||||||
|
parallel_start_node_id?: string
|
||||||
|
parent_parallel_id?: string
|
||||||
|
parent_parallel_start_node_id?: string
|
||||||
}
|
}
|
||||||
metadata: {
|
metadata: {
|
||||||
iterator_length: number
|
iterator_length: number
|
||||||
|
Loading…
x
Reference in New Issue
Block a user