This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
import {
useMemo,
useState,
} from 'react'
import {
RiArrowRightSLine,
RiListView,
} from '@remixicon/react'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
import type { AgentLogItemWithChildren } from '@/types/workflow'
import NodeStatusIcon from '@/app/components/workflow/nodes/_base/components/node-status-icon'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import BlockIcon from '@/app/components/workflow/block-icon'
import { BlockEnum } from '@/app/components/workflow/types'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
type AgentLogItemProps = {
item: AgentLogItemWithChildren
onShowAgentOrToolLog: (detail: AgentLogItemWithChildren) => void
}
const AgentLogItem = ({
item,
onShowAgentOrToolLog,
}: AgentLogItemProps) => {
const {
label,
status,
children,
data,
metadata,
} = item
const [expanded, setExpanded] = useState(false)
const { getIconUrl } = useGetIcon()
const toolIcon = useMemo(() => {
const icon = metadata?.icon
if (icon) {
if (icon.includes('http'))
return icon
return getIconUrl(icon)
}
return ''
}, [getIconUrl, metadata?.icon])
const mergeStatus = useMemo(() => {
if (status === 'start')
return 'running'
return status
}, [status])
return (
<div className='rounded-[10px] border-[0.5px] border-components-panel-border bg-background-default'>
<div
className={cn(
'flex cursor-pointer items-center pb-2 pl-1.5 pr-3 pt-2',
expanded && 'pb-1',
)}
onClick={() => setExpanded(!expanded)}
>
{
expanded
? <RiArrowRightSLine className='h-4 w-4 shrink-0 rotate-90 text-text-quaternary' />
: <RiArrowRightSLine className='h-4 w-4 shrink-0 text-text-quaternary' />
}
<BlockIcon
className='mr-1.5 shrink-0'
type={toolIcon ? BlockEnum.Tool : BlockEnum.Agent}
toolIcon={toolIcon}
/>
<div
className='system-sm-semibold-uppercase grow truncate text-text-secondary'
title={label}
>
{label}
</div>
{
metadata?.elapsed_time && (
<div className='system-xs-regular mr-2 shrink-0 text-text-tertiary'>{metadata?.elapsed_time?.toFixed(3)}s</div>
)
}
<NodeStatusIcon status={mergeStatus} />
</div>
{
expanded && (
<div className='p-1 pt-0'>
{
!!children?.length && (
<Button
className='mb-1 flex w-full items-center justify-between'
variant='tertiary'
onClick={() => onShowAgentOrToolLog(item)}
>
<div className='flex items-center'>
<RiListView className='mr-1 h-4 w-4 shrink-0 text-components-button-tertiary-text' />
{`${children.length} Action Logs`}
</div>
<div className='flex'>
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
</div>
</Button>
)
}
{
data && (
<CodeEditor
readOnly
title={<div>{'data'.toLocaleUpperCase()}</div>}
language={CodeLanguage.json}
value={data}
isJSONStringifyBeauty
/>
)
}
</div>
)
}
</div>
)
}
export default AgentLogItem

View File

@@ -0,0 +1,61 @@
import { useState } from 'react'
import { RiMoreLine } from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import type { AgentLogItemWithChildren } from '@/types/workflow'
type AgentLogNavMoreProps = {
options: AgentLogItemWithChildren[]
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
}
const AgentLogNavMore = ({
options,
onShowAgentOrToolLog,
}: AgentLogNavMoreProps) => {
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
placement='bottom-start'
offset={{
mainAxis: 2,
crossAxis: -54,
}}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger>
<Button
className='h-6 w-6'
variant='ghost-accent'
>
<RiMoreLine className='h-4 w-4' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<div className='w-[136px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
{
options.map(option => (
<div
key={option.message_id}
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover'
onClick={() => {
onShowAgentOrToolLog(option)
setOpen(false)
}}
>
{option.label}
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default AgentLogNavMore

View File

@@ -0,0 +1,78 @@
import { RiArrowLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import AgentLogNavMore from './agent-log-nav-more'
import Button from '@/app/components/base/button'
import type { AgentLogItemWithChildren } from '@/types/workflow'
type AgentLogNavProps = {
agentOrToolLogItemStack: AgentLogItemWithChildren[]
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
}
const AgentLogNav = ({
agentOrToolLogItemStack,
onShowAgentOrToolLog,
}: AgentLogNavProps) => {
const { t } = useTranslation()
const agentOrToolLogItemStackLength = agentOrToolLogItemStack.length
const first = agentOrToolLogItemStack[0]
const mid = agentOrToolLogItemStack.slice(1, -1)
const end = agentOrToolLogItemStack.at(-1)
return (
<div className='flex h-8 items-center bg-components-panel-bg p-1 pr-3'>
<Button
className='shrink-0 px-[5px]'
size='small'
variant='ghost-accent'
onClick={() => {
onShowAgentOrToolLog()
}}
>
<RiArrowLeftLine className='mr-1 h-3.5 w-3.5' />
AGENT
</Button>
<div className='system-xs-regular mx-0.5 shrink-0 text-divider-deep'>/</div>
{
agentOrToolLogItemStackLength > 1
? (
<Button
className='shrink-0 px-[5px]'
size='small'
variant='ghost-accent'
onClick={() => onShowAgentOrToolLog(first)}
>
{t('workflow.nodes.agent.strategy.label')}
</Button>
)
: (
<div className='system-xs-medium-uppercase flex items-center px-[5px] text-text-tertiary'>
{t('workflow.nodes.agent.strategy.label')}
</div>
)
}
{
!!mid.length && (
<>
<div className='system-xs-regular mx-0.5 shrink-0 text-divider-deep'>/</div>
<AgentLogNavMore
options={mid}
onShowAgentOrToolLog={onShowAgentOrToolLog}
/>
</>
)
}
{
!!end && agentOrToolLogItemStackLength > 1 && (
<>
<div className='system-xs-regular mx-0.5 shrink-0 text-divider-deep'>/</div>
<div className='system-xs-medium-uppercase flex items-center px-[5px] text-text-tertiary'>
{end.label}
</div>
</>
)
}
</div>
)
}
export default AgentLogNav

View File

@@ -0,0 +1,49 @@
import { RiArrowRightLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type {
AgentLogItemWithChildren,
NodeTracing,
} from '@/types/workflow'
type AgentLogTriggerProps = {
nodeInfo: NodeTracing
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
}
const AgentLogTrigger = ({
nodeInfo,
onShowAgentOrToolLog,
}: AgentLogTriggerProps) => {
const { t } = useTranslation()
const { agentLog, execution_metadata } = nodeInfo
const agentStrategy = execution_metadata?.tool_info?.agent_strategy
return (
<div
className='cursor-pointer rounded-[10px] bg-components-button-tertiary-bg'
onClick={() => {
onShowAgentOrToolLog({ message_id: nodeInfo.id, children: agentLog || [] } as AgentLogItemWithChildren)
}}
>
<div className='system-2xs-medium-uppercase flex items-center px-3 pt-2 text-text-tertiary'>
{t('workflow.nodes.agent.strategy.label')}
</div>
<div className='flex items-center pb-1.5 pl-3 pr-2 pt-1'>
{
agentStrategy && (
<div className='system-xs-medium grow text-text-secondary'>
{agentStrategy}
</div>
)
}
<div
className='system-xs-regular-uppercase flex shrink-0 cursor-pointer items-center px-[1px] text-text-tertiary'
>
{t('runLog.detail')}
<RiArrowRightLine className='ml-0.5 h-3.5 w-3.5' />
</div>
</div>
</div>
)
}
export default AgentLogTrigger

View File

@@ -0,0 +1,60 @@
import { RiAlertFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import AgentLogItem from './agent-log-item'
import AgentLogNav from './agent-log-nav'
import type { AgentLogItemWithChildren } from '@/types/workflow'
type AgentResultPanelProps = {
agentOrToolLogItemStack: AgentLogItemWithChildren[]
agentOrToolLogListMap: Record<string, AgentLogItemWithChildren[]>
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
}
const AgentResultPanel = ({
agentOrToolLogItemStack,
agentOrToolLogListMap,
onShowAgentOrToolLog,
}: AgentResultPanelProps) => {
const { t } = useTranslation()
const top = agentOrToolLogItemStack[agentOrToolLogItemStack.length - 1]
const list = agentOrToolLogListMap[top.message_id]
return (
<div className='overflow-y-auto bg-background-section'>
<AgentLogNav
agentOrToolLogItemStack={agentOrToolLogItemStack}
onShowAgentOrToolLog={onShowAgentOrToolLog}
/>
{
<div className='space-y-1 p-2'>
{
list.map(item => (
<AgentLogItem
key={item.message_id}
item={item}
onShowAgentOrToolLog={onShowAgentOrToolLog}
/>
))
}
</div>
}
{
top.hasCircle && (
<div className='mt-1 flex items-center rounded-xl border border-components-panel-border bg-components-panel-bg-blur px-3 pr-2 shadow-md'>
<div
className='absolute inset-0 rounded-xl opacity-[0.4]'
style={{
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
}}
></div>
<RiAlertFill className='mr-1.5 h-4 w-4 text-text-warning-secondary' />
<div className='system-xs-medium text-text-primary'>
{t('runLog.circularInvocationTip')}
</div>
</div>
)
}
</div>
)
}
export default AgentResultPanel

View File

@@ -0,0 +1,2 @@
export { default as AgentLogTrigger } from './agent-log-trigger'
export { default as AgentResultPanel } from './agent-result-panel'

View File

@@ -0,0 +1,3 @@
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M0 0H368M0 2H368M0 4H368M0 6H368M0 8H368M0 10H368M0 12H368M0 14H368M0 16H368M0 18H368M0 20H368M0 22H368M0 24H368M0 26H368M0 28H368M0 30H368M0 32H368M0 34H368M0 36H368M0 38H368M0 40H368M0 42H368M0 44H368M0 46H368M0 48H368M0 50H368M0 52H368M0 54H368M0 56H368M0 58H368M0 60H368M0 62H368M0 64H368M0 66H368M0 68H368M0 70H368M0 72H368M0 74H368M0 76H368M0 78H368M0 80H368M0 82H368M0 84H368M0 86H368M0 88H368M0 90H368M0 92H368M0 94H368M0 96H368M0 98H368M0 100H368M0 102H368M0 104H368M0 106H368M0 108H368M0 110H368M0 112H368M0 114H368M0 116H368M0 118H368M0 120H368M0 122H368M0 124H368M0 126H368M0 128H368M0 130H368M0 132H368M0 134H368M0 136H368M0 138H368M0 140H368M0 142H368M0 144H368M0 146H368M0 148H368M0 150H368M0 152H368M0 154H368M0 156H368M0 158H368M0 160H368M0 162H368M0 164H368M0 166H368M0 168H368M0 170H368M0 172H368M0 174H368M0 176H368M0 178H368M0 180H368M0 182H368M0 184H368M0 186H368M0 188H368M0 190H368M0 192H368M0 194H368M0 196H368M0 198H368M0 200H368M0 202H368M0 204H368M0 206H368M0 208H368M0 210H368M0 212H368M0 214H368M0 216H368M0 218H368M0 220H368M0 222H368M0 224H368M0 226H368" stroke="#F04438" stroke-opacity="0.3" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M0 0.5H368M0 2.5H368M0 4.5H368M0 6.5H368M0 8.5H368M0 10.5H368M0 12.5H368M0 14.5H368M0 16.5H368M0 18.5H368M0 20.5H368M0 22.5H368M0 24.5H368M0 26.5H368M0 28.5H368M0 30.5H368M0 32.5H368M0 34.5H368M0 36.5H368M0 38.5H368M0 40.5H368M0 42.5H368M0 44.5H368M0 46.5H368M0 48.5H368M0 50.5H368M0 52.5H368M0 54.5H368M0 56.5H368M0 58.5H368M0 60.5H368M0 62.5H368M0 64.5H368M0 66.5H368M0 68.5H368M0 70.5H368M0 72.5H368M0 74.5H368M0 76.5H368M0 78.5H368M0 80.5H368M0 82.5H368M0 84.5H368M0 86.5H368M0 88.5H368M0 90.5H368M0 92.5H368M0 94.5H368M0 96.5H368M0 98.5H368M0 100.5H368M0 102.5H368M0 104.5H368M0 106.5H368M0 108.5H368M0 110.5H368M0 112.5H368M0 114.5H368M0 116.5H368M0 118.5H368M0 120.5H368M0 122.5H368M0 124.5H368M0 126.5H368M0 128.5H368M0 130.5H368M0 132.5H368M0 134.5H368M0 136.5H368M0 138.5H368M0 140.5H368M0 142.5H368M0 144.5H368M0 146.5H368M0 148.5H368M0 150.5H368M0 152.5H368M0 154.5H368M0 156.5H368M0 158.5H368M0 160.5H368M0 162.5H368M0 164.5H368M0 166.5H368M0 168.5H368M0 170.5H368M0 172.5H368M0 174.5H368M0 176.5H368M0 178.5H368M0 180.5H368M0 182.5H368M0 184.5H368M0 186.5H368M0 188.5H368M0 190.5H368M0 192.5H368M0 194.5H368M0 196.5H368M0 198.5H368M0 200.5H368M0 202.5H368M0 204.5H368M0 206.5H368M0 208.5H368M0 210.5H368M0 212.5H368M0 214.5H368M0 216.5H368M0 218.5H368M0 220.5H368M0 222.5H368M0 224.5H368M0 226.5H368" stroke="#0BA5EC" stroke-opacity="0.3" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M0 0H368M0 2H368M0 4H368M0 6H368M0 8H368M0 10H368M0 12H368M0 14H368M0 16H368M0 18H368M0 20H368M0 22H368M0 24H368M0 26H368M0 28H368M0 30H368M0 32H368M0 34H368M0 36H368M0 38H368M0 40H368M0 42H368M0 44H368M0 46H368M0 48H368M0 50H368M0 52H368M0 54H368M0 56H368M0 58H368M0 60H368M0 62H368M0 64H368M0 66H368M0 68H368M0 70H368M0 72H368M0 74H368M0 76H368M0 78H368M0 80H368M0 82H368M0 84H368M0 86H368M0 88H368M0 90H368M0 92H368M0 94H368M0 96H368M0 98H368M0 100H368M0 102H368M0 104H368M0 106H368M0 108H368M0 110H368M0 112H368M0 114H368M0 116H368M0 118H368M0 120H368M0 122H368M0 124H368M0 126H368M0 128H368M0 130H368M0 132H368M0 134H368M0 136H368M0 138H368M0 140H368M0 142H368M0 144H368M0 146H368M0 148H368M0 150H368M0 152H368M0 154H368M0 156H368M0 158H368M0 160H368M0 162H368M0 164H368M0 166H368M0 168H368M0 170H368M0 172H368M0 174H368M0 176H368M0 178H368M0 180H368M0 182H368M0 184H368M0 186H368M0 188H368M0 190H368M0 192H368M0 194H368M0 196H368M0 198H368M0 200H368M0 202H368M0 204H368M0 206H368M0 208H368M0 210H368M0 212H368M0 214H368M0 216H368M0 218H368M0 220H368M0 222H368M0 224H368M0 226H368" stroke="#17B26A" stroke-opacity="0.3" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M0 0.5H368M0 2.5H368M0 4.5H368M0 6.5H368M0 8.5H368M0 10.5H368M0 12.5H368M0 14.5H368M0 16.5H368M0 18.5H368M0 20.5H368M0 22.5H368M0 24.5H368M0 26.5H368M0 28.5H368M0 30.5H368M0 32.5H368M0 34.5H368M0 36.5H368M0 38.5H368M0 40.5H368M0 42.5H368M0 44.5H368M0 46.5H368M0 48.5H368M0 50.5H368M0 52.5H368M0 54.5H368M0 56.5H368M0 58.5H368M0 60.5H368M0 62.5H368M0 64.5H368M0 66.5H368M0 68.5H368M0 70.5H368M0 72.5H368M0 74.5H368M0 76.5H368M0 78.5H368M0 80.5H368M0 82.5H368M0 84.5H368M0 86.5H368M0 88.5H368M0 90.5H368M0 92.5H368M0 94.5H368M0 96.5H368M0 98.5H368M0 100.5H368M0 102.5H368M0 104.5H368M0 106.5H368M0 108.5H368M0 110.5H368M0 112.5H368M0 114.5H368M0 116.5H368M0 118.5H368M0 120.5H368M0 122.5H368M0 124.5H368M0 126.5H368M0 128.5H368M0 130.5H368M0 132.5H368M0 134.5H368M0 136.5H368M0 138.5H368M0 140.5H368M0 142.5H368M0 144.5H368M0 146.5H368M0 148.5H368M0 150.5H368M0 152.5H368M0 154.5H368M0 156.5H368M0 158.5H368M0 160.5H368M0 162.5H368M0 164.5H368M0 166.5H368M0 168.5H368M0 170.5H368M0 172.5H368M0 174.5H368M0 176.5H368M0 178.5H368M0 180.5H368M0 182.5H368M0 184.5H368M0 186.5H368M0 188.5H368M0 190.5H368M0 192.5H368M0 194.5H368M0 196.5H368M0 198.5H368M0 200.5H368M0 202.5H368M0 204.5H368M0 206.5H368M0 208.5H368M0 210.5H368M0 212.5H368M0 214.5H368M0 216.5H368M0 218.5H368M0 220.5H368M0 222.5H368M0 224.5H368M0 226.5H368" stroke="#F79009" stroke-opacity="0.3" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,9 @@
<svg width="237" height="50" viewBox="0 0 237 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M0 8C0 3.58172 3.58172 0 8 0H237L215.033 50H8C3.58172 50 0 46.4183 0 42V8Z" fill="url(#paint0_linear_3552_29170)"/>
<defs>
<linearGradient id="paint0_linear_3552_29170" x1="-4.89158e-08" y1="4.62963" x2="168.013" y2="23.1752" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.03"/>
<stop offset="1" stop-color="white" stop-opacity="0.05"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@@ -0,0 +1,9 @@
<svg width="237" height="50" viewBox="0 0 237 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M0 8C0 3.58172 3.58172 0 8 0H237L215.033 50H8C3.58172 50 0 46.4183 0 42V8Z" fill="url(#paint0_linear_3552_29170)"/>
<defs>
<linearGradient id="paint0_linear_3552_29170" x1="-4.89158e-08" y1="4.62963" x2="168.013" y2="23.1752" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.12"/>
<stop offset="1" stop-color="white" stop-opacity="0.5"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 516 B

View File

@@ -0,0 +1,115 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { useBoolean } from 'ahooks'
import type {
AgentLogItemWithChildren,
IterationDurationMap,
LoopDurationMap,
LoopVariableMap,
NodeTracing,
} from '@/types/workflow'
export const useLogs = () => {
const [showRetryDetail, {
setTrue: setShowRetryDetailTrue,
setFalse: setShowRetryDetailFalse,
}] = useBoolean(false)
const [retryResultList, setRetryResultList] = useState<NodeTracing[]>([])
const handleShowRetryResultList = useCallback((detail: NodeTracing[]) => {
setShowRetryDetailTrue()
setRetryResultList(detail)
}, [setShowRetryDetailTrue, setRetryResultList])
const [showIteratingDetail, {
setTrue: setShowIteratingDetailTrue,
setFalse: setShowIteratingDetailFalse,
}] = useBoolean(false)
const [iterationResultList, setIterationResultList] = useState<NodeTracing[][]>([])
const [iterationResultDurationMap, setIterationResultDurationMap] = useState<IterationDurationMap>({})
const handleShowIterationResultList = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => {
setShowIteratingDetailTrue()
setIterationResultList(detail)
setIterationResultDurationMap(iterDurationMap)
}, [setShowIteratingDetailTrue, setIterationResultList, setIterationResultDurationMap])
const [showLoopingDetail, {
setTrue: setShowLoopingDetailTrue,
setFalse: setShowLoopingDetailFalse,
}] = useBoolean(false)
const [loopResultList, setLoopResultList] = useState<NodeTracing[][]>([])
const [loopResultDurationMap, setLoopResultDurationMap] = useState<LoopDurationMap>({})
const [loopResultVariableMap, setLoopResultVariableMap] = useState<Record<string, any>>({})
const handleShowLoopResultList = useCallback((detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => {
setShowLoopingDetailTrue()
setLoopResultList(detail)
setLoopResultDurationMap(loopDurationMap)
setLoopResultVariableMap(loopVariableMap)
}, [setShowLoopingDetailTrue, setLoopResultList, setLoopResultDurationMap])
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
const agentOrToolLogItemStackRef = useRef(agentOrToolLogItemStack)
const [agentOrToolLogListMap, setAgentOrToolLogListMap] = useState<Record<string, AgentLogItemWithChildren[]>>({})
const agentOrToolLogListMapRef = useRef(agentOrToolLogListMap)
const handleShowAgentOrToolLog = useCallback((detail?: AgentLogItemWithChildren) => {
if (!detail) {
setAgentOrToolLogItemStack([])
agentOrToolLogItemStackRef.current = []
return
}
const { message_id: id, children } = detail
let currentAgentOrToolLogItemStack = agentOrToolLogItemStackRef.current.slice()
const index = currentAgentOrToolLogItemStack.findIndex(logItem => logItem.message_id === id)
if (index > -1)
currentAgentOrToolLogItemStack = currentAgentOrToolLogItemStack.slice(0, index + 1)
else
currentAgentOrToolLogItemStack = [...currentAgentOrToolLogItemStack.slice(), detail]
setAgentOrToolLogItemStack(currentAgentOrToolLogItemStack)
agentOrToolLogItemStackRef.current = currentAgentOrToolLogItemStack
if (children) {
setAgentOrToolLogListMap({
...agentOrToolLogListMapRef.current,
[id]: children,
})
}
}, [setAgentOrToolLogItemStack, setAgentOrToolLogListMap])
return {
showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length,
showRetryDetail,
setShowRetryDetailTrue,
setShowRetryDetailFalse,
retryResultList,
setRetryResultList,
handleShowRetryResultList,
showIteratingDetail,
setShowIteratingDetailTrue,
setShowIteratingDetailFalse,
iterationResultList,
setIterationResultList,
iterationResultDurationMap,
setIterationResultDurationMap,
handleShowIterationResultList,
showLoopingDetail,
setShowLoopingDetailTrue,
setShowLoopingDetailFalse,
loopResultList,
setLoopResultList,
loopResultDurationMap,
setLoopResultDurationMap,
loopResultVariableMap,
setLoopResultVariableMap,
handleShowLoopResultList,
agentOrToolLogItemStack,
agentOrToolLogListMap,
handleShowAgentOrToolLog,
}
}

View File

@@ -0,0 +1,197 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import OutputPanel from './output-panel'
import ResultPanel from './result-panel'
import StatusPanel from './status'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import TracingPanel from './tracing-panel'
import cn from '@/utils/classnames'
import { ToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import { fetchRunDetail, fetchTracingList } from '@/service/log'
import type { NodeTracing } from '@/types/workflow'
import type { WorkflowRunDetailResponse } from '@/models/log'
import { useStore } from '../store'
export type RunProps = {
hideResult?: boolean
activeTab?: 'RESULT' | 'DETAIL' | 'TRACING'
getResultCallback?: (result: WorkflowRunDetailResponse) => void
runDetailUrl: string
tracingListUrl: string
}
const RunPanel: FC<RunProps> = ({
hideResult,
activeTab = 'RESULT',
getResultCallback,
runDetailUrl,
tracingListUrl,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [currentTab, setCurrentTab] = useState<string>(activeTab)
const [loading, setLoading] = useState<boolean>(true)
const [runDetail, setRunDetail] = useState<WorkflowRunDetailResponse>()
const [list, setList] = useState<NodeTracing[]>([])
const isListening = useStore(s => s.isListening)
const executor = useMemo(() => {
if (runDetail?.created_by_role === 'account')
return runDetail.created_by_account?.name || ''
if (runDetail?.created_by_role === 'end_user')
return runDetail.created_by_end_user?.session_id || ''
return 'N/A'
}, [runDetail])
const getResult = useCallback(async () => {
try {
const res = await fetchRunDetail(runDetailUrl)
setRunDetail(res)
if (getResultCallback)
getResultCallback(res)
}
catch (err) {
notify({
type: 'error',
message: `${err}`,
})
}
}, [notify, getResultCallback, runDetailUrl])
const getTracingList = useCallback(async () => {
try {
const { data: nodeList } = await fetchTracingList({
url: tracingListUrl,
})
setList(nodeList)
}
catch (err) {
notify({
type: 'error',
message: `${err}`,
})
}
}, [notify, tracingListUrl])
const getData = useCallback(async () => {
setLoading(true)
await getResult()
await getTracingList()
setLoading(false)
}, [getResult, getTracingList])
const switchTab = async (tab: string) => {
setCurrentTab(tab)
if (tab === 'RESULT') {
if (runDetailUrl)
await getResult()
}
if (tracingListUrl)
await getTracingList()
}
useEffect(() => {
if (isListening)
setCurrentTab('DETAIL')
}, [isListening])
useEffect(() => {
// fetch data
if (runDetailUrl && tracingListUrl)
getData()
}, [runDetailUrl, tracingListUrl])
const [height, setHeight] = useState(0)
const ref = useRef<HTMLDivElement>(null)
const adjustResultHeight = () => {
if (ref.current)
setHeight(ref.current?.clientHeight - 16 - 16 - 2 - 1)
}
useEffect(() => {
adjustResultHeight()
}, [loading])
return (
<div className='relative flex grow flex-col'>
{/* tab */}
<div className='flex shrink-0 items-center border-b-[0.5px] border-divider-subtle px-4'>
{!hideResult && (
<div
className={cn(
'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
currentTab === 'RESULT' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
)}
onClick={() => switchTab('RESULT')}
>{t('runLog.result')}</div>
)}
<div
className={cn(
'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
currentTab === 'DETAIL' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
)}
onClick={() => switchTab('DETAIL')}
>{t('runLog.detail')}</div>
<div
className={cn(
'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
currentTab === 'TRACING' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
)}
onClick={() => switchTab('TRACING')}
>{t('runLog.tracing')}</div>
</div>
{/* panel detail */}
<div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-components-panel-bg')}>
{loading && (
<div className='flex h-full items-center justify-center bg-components-panel-bg'>
<Loading />
</div>
)}
{!loading && currentTab === 'RESULT' && runDetail && (
<OutputPanel
outputs={runDetail.outputs}
error={runDetail.error}
height={height}
/>
)}
{!loading && currentTab === 'DETAIL' && runDetail && (
<ResultPanel
inputs={runDetail.inputs}
inputs_truncated={runDetail.inputs_truncated}
outputs={runDetail.outputs}
outputs_truncated={runDetail.outputs_truncated}
outputs_full_content={runDetail.outputs_full_content}
status={runDetail.status}
error={runDetail.error}
elapsed_time={runDetail.elapsed_time}
total_tokens={runDetail.total_tokens}
created_at={runDetail.created_at}
created_by={executor}
steps={runDetail.total_steps}
exceptionCounts={runDetail.exceptions_count}
isListening={isListening}
/>
)}
{!loading && currentTab === 'DETAIL' && !runDetail && isListening && (
<StatusPanel
status={WorkflowRunningStatus.Running}
isListening={true}
/>
)}
{!loading && currentTab === 'TRACING' && (
<TracingPanel
className='bg-background-section-burn'
list={list}
/>
)}
</div>
</div>
)
}
export default RunPanel

View File

@@ -0,0 +1,2 @@
export { default as IterationLogTrigger } from './iteration-log-trigger'
export { default as IterationResultPanel } from './iteration-result-panel'

View File

@@ -0,0 +1,147 @@
import { useTranslation } from 'react-i18next'
import { RiArrowRightSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import type {
IterationDurationMap,
NodeTracing,
} from '@/types/workflow'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
type IterationLogTriggerProps = {
nodeInfo: NodeTracing
allExecutions?: NodeTracing[]
onShowIterationResultList: (iterationResultList: NodeTracing[][], iterationResultDurationMap: IterationDurationMap) => void
}
const IterationLogTrigger = ({
nodeInfo,
allExecutions,
onShowIterationResultList,
}: IterationLogTriggerProps) => {
const { t } = useTranslation()
const filterNodesForInstance = (key: string): NodeTracing[] => {
if (!allExecutions) return []
const parallelNodes = allExecutions.filter(exec =>
exec.execution_metadata?.parallel_mode_run_id === key,
)
if (parallelNodes.length > 0)
return parallelNodes
const serialIndex = Number.parseInt(key, 10)
if (!isNaN(serialIndex)) {
const serialNodes = allExecutions.filter(exec =>
exec.execution_metadata?.iteration_id === nodeInfo.node_id
&& exec.execution_metadata?.iteration_index === serialIndex,
)
if (serialNodes.length > 0)
return serialNodes
}
return []
}
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
const iterationNodeMeta = nodeInfo.execution_metadata
const iterDurationMap = nodeInfo?.iterDurationMap || iterationNodeMeta?.iteration_duration_map || {}
let structuredList: NodeTracing[][] = []
if (iterationNodeMeta?.iteration_duration_map) {
const instanceKeys = Object.keys(iterationNodeMeta.iteration_duration_map)
structuredList = instanceKeys
.map(key => filterNodesForInstance(key))
.filter(branchNodes => branchNodes.length > 0)
// Also include failed iterations that might not be in duration map
if (allExecutions && nodeInfo.details?.length) {
const existingIterationIndices = new Set<number>()
structuredList.forEach((iteration) => {
iteration.forEach((node) => {
if (node.execution_metadata?.iteration_index !== undefined)
existingIterationIndices.add(node.execution_metadata.iteration_index)
})
})
// Find failed iterations that are not in the structured list
nodeInfo.details.forEach((iteration, index) => {
if (!existingIterationIndices.has(index) && iteration.some(node => node.status === NodeRunningStatus.Failed))
structuredList.push(iteration)
})
// Sort by iteration index to maintain order
structuredList.sort((a, b) => {
const aIndex = a[0]?.execution_metadata?.iteration_index ?? 0
const bIndex = b[0]?.execution_metadata?.iteration_index ?? 0
return aIndex - bIndex
})
}
}
else if (nodeInfo.details?.length) {
structuredList = nodeInfo.details
}
onShowIterationResultList(structuredList, iterDurationMap)
}
let displayIterationCount = 0
const iterMap = nodeInfo.execution_metadata?.iteration_duration_map
if (iterMap)
displayIterationCount = Object.keys(iterMap).length
else if (nodeInfo.details?.length)
displayIterationCount = nodeInfo.details.length
else if (nodeInfo.metadata?.iterator_length)
displayIterationCount = nodeInfo.metadata.iterator_length
const getErrorCount = (details: NodeTracing[][] | undefined, iterationNodeMeta?: any) => {
if (!details || details.length === 0)
return 0
// Use Set to track failed iteration indices to avoid duplicate counting
const failedIterationIndices = new Set<number>()
// Collect failed iteration indices from details
details.forEach((iteration, index) => {
if (iteration.some(item => item.status === NodeRunningStatus.Failed)) {
// Try to get iteration index from first node, fallback to array index
const iterationIndex = iteration[0]?.execution_metadata?.iteration_index ?? index
failedIterationIndices.add(iterationIndex)
}
})
// If allExecutions exists, check for additional failed iterations
if (iterationNodeMeta?.iteration_duration_map && allExecutions) {
// Find all failed iteration nodes
allExecutions.forEach((exec) => {
if (exec.execution_metadata?.iteration_id === nodeInfo.node_id
&& exec.status === NodeRunningStatus.Failed
&& exec.execution_metadata?.iteration_index !== undefined)
failedIterationIndices.add(exec.execution_metadata.iteration_index)
})
}
return failedIterationIndices.size
}
const errorCount = getErrorCount(nodeInfo.details, nodeInfo.execution_metadata)
return (
<Button
className='flex w-full cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-components-button-tertiary-bg-hover px-3 py-2 hover:bg-components-button-tertiary-bg-hover'
onClick={handleOnShowIterationDetail}
>
<Iteration className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
<div className='system-sm-medium flex-1 text-left text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: displayIterationCount })}{errorCount > 0 && (
<>
{t('workflow.nodes.iteration.comma')}
{t('workflow.nodes.iteration.error', { count: errorCount })}
</>
)}</div>
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
</Button>
)
}
export default IterationLogTrigger

View File

@@ -0,0 +1,128 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowLeftLine,
RiArrowRightSLine,
RiErrorWarningLine,
RiLoader2Line,
} from '@remixicon/react'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
import cn from '@/utils/classnames'
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
const i18nPrefix = 'workflow.singleRun'
type Props = {
list: NodeTracing[][]
onBack: () => void
iterDurationMap?: IterationDurationMap
}
const IterationResultPanel: FC<Props> = ({
list,
onBack,
iterDurationMap,
}) => {
const { t } = useTranslation()
const [expandedIterations, setExpandedIterations] = useState<Record<number, boolean>>({})
const toggleIteration = useCallback((index: number) => {
setExpandedIterations(prev => ({
...prev,
[index]: !prev[index],
}))
}, [])
const countIterDuration = (iteration: NodeTracing[], iterDurationMap: IterationDurationMap): string => {
const IterRunIndex = iteration[0]?.execution_metadata?.iteration_index as number
const iterRunId = iteration[0]?.execution_metadata?.parallel_mode_run_id
const iterItem = iterDurationMap[iterRunId || IterRunIndex]
const duration = iterItem
return `${(duration && duration > 0.01) ? duration.toFixed(2) : 0.01}s`
}
const iterationStatusShow = (index: number, iteration: NodeTracing[], iterDurationMap?: IterationDurationMap) => {
const hasFailed = iteration.some(item => item.status === NodeRunningStatus.Failed)
const isRunning = iteration.some(item => item.status === NodeRunningStatus.Running)
const hasDurationMap = iterDurationMap && Object.keys(iterDurationMap).length !== 0
if (hasFailed)
return <RiErrorWarningLine className='h-4 w-4 text-text-destructive' />
if (isRunning)
return <RiLoader2Line className='h-3.5 w-3.5 animate-spin text-primary-600' />
return (
<>
{hasDurationMap && (
<div className='system-xs-regular text-text-tertiary'>
{countIterDuration(iteration, iterDurationMap)}
</div>
)}
<RiArrowRightSLine
className={cn(
'h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200',
expandedIterations[index] && 'rotate-90',
)}
/>
</>
)
}
return (
<div className='bg-components-panel-bg'>
<div
className='flex h-8 cursor-pointer items-center border-b-[0.5px] border-b-divider-regular px-4 text-text-accent-secondary'
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onBack()
}}
>
<RiArrowLeftLine className='mr-1 h-4 w-4' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
</div>
{/* List */}
<div className='bg-components-panel-bg p-2'>
{list.map((iteration, index) => (
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl border-none bg-background-section-burn')}>
<div
className={cn(
'flex w-full cursor-pointer items-center justify-between px-3',
expandedIterations[index] ? 'pb-2 pt-3' : 'py-3',
'rounded-xl text-left',
)}
onClick={() => toggleIteration(index)}
>
<div className={cn('flex grow items-center gap-2')}>
<div className='flex h-4 w-4 shrink-0 items-center justify-center rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500'>
<Iteration className='h-3 w-3 text-text-primary-on-surface' />
</div>
<span className='system-sm-semibold-uppercase grow text-text-primary'>
{t(`${i18nPrefix}.iteration`)} {index + 1}
</span>
{iterationStatusShow(index, iteration, iterDurationMap)}
</div>
</div>
{expandedIterations[index] && <div
className="h-px grow bg-divider-subtle"
></div>}
<div className={cn(
'transition-all duration-200',
expandedIterations[index]
? 'opacity-100'
: 'max-h-0 overflow-hidden opacity-0',
)}>
<TracingPanel
list={iteration}
className='bg-background-section-burn'
/>
</div>
</div>
))}
</div>
</div>
)
}
export default React.memo(IterationResultPanel)

View File

@@ -0,0 +1,2 @@
export { default as LoopLogTrigger } from './loop-log-trigger'
export { default as LoopResultPanel } from './loop-result-panel'

View File

@@ -0,0 +1,108 @@
import { useTranslation } from 'react-i18next'
import { RiArrowRightSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import type {
LoopDurationMap,
LoopVariableMap,
NodeTracing,
} from '@/types/workflow'
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
type LoopLogTriggerProps = {
nodeInfo: NodeTracing
allExecutions?: NodeTracing[]
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
}
const LoopLogTrigger = ({
nodeInfo,
allExecutions,
onShowLoopResultList,
}: LoopLogTriggerProps) => {
const { t } = useTranslation()
const filterNodesForInstance = (key: string): NodeTracing[] => {
if (!allExecutions) return []
const parallelNodes = allExecutions.filter(exec =>
exec.execution_metadata?.parallel_mode_run_id === key,
)
if (parallelNodes.length > 0)
return parallelNodes
const serialIndex = Number.parseInt(key, 10)
if (!isNaN(serialIndex)) {
const serialNodes = allExecutions.filter(exec =>
exec.execution_metadata?.loop_id === nodeInfo.node_id
&& exec.execution_metadata?.loop_index === serialIndex,
)
if (serialNodes.length > 0)
return serialNodes
}
return []
}
const handleOnShowLoopDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
const loopNodeMeta = nodeInfo.execution_metadata
const loopDurMap = nodeInfo?.loopDurationMap || loopNodeMeta?.loop_duration_map || {}
const loopVarMap = loopNodeMeta?.loop_variable_map || {}
let structuredList: NodeTracing[][] = []
if (nodeInfo.details?.length) {
structuredList = nodeInfo.details
}
else if (loopNodeMeta?.loop_duration_map) {
const instanceKeys = Object.keys(loopNodeMeta.loop_duration_map)
structuredList = instanceKeys
.map(key => filterNodesForInstance(key))
.filter(branchNodes => branchNodes.length > 0)
}
onShowLoopResultList(
structuredList,
loopDurMap,
loopVarMap,
)
}
let displayLoopCount = 0
const loopMap = nodeInfo.execution_metadata?.loop_duration_map
if (loopMap)
displayLoopCount = Object.keys(loopMap).length
else if (nodeInfo.details?.length)
displayLoopCount = nodeInfo.details.length
else if (nodeInfo.metadata?.loop_length)
displayLoopCount = nodeInfo.metadata.loop_length
const getErrorCount = (details: NodeTracing[][] | undefined) => {
if (!details || details.length === 0)
return 0
return details.reduce((acc, loop) => {
if (loop.some(item => item.status === 'failed'))
acc++
return acc
}, 0)
}
const errorCount = getErrorCount(nodeInfo.details)
return (
<Button
className='flex w-full cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-components-button-tertiary-bg-hover px-3 py-2 hover:bg-components-button-tertiary-bg-hover'
onClick={handleOnShowLoopDetail}
>
<Loop className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
<div className='system-sm-medium flex-1 text-left text-components-button-tertiary-text'>{t('workflow.nodes.loop.loop', { count: displayLoopCount })}{errorCount > 0 && (
<>
{t('workflow.nodes.loop.comma')}
{t('workflow.nodes.loop.error', { count: errorCount })}
</>
)}</div>
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
</Button>
)
}
export default LoopLogTrigger

View File

@@ -0,0 +1,148 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowLeftLine,
RiArrowRightSLine,
RiErrorWarningLine,
RiLoader2Line,
} from '@remixicon/react'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
import cn from '@/utils/classnames'
import type { LoopDurationMap, LoopVariableMap, NodeTracing } from '@/types/workflow'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
const i18nPrefix = 'workflow.singleRun'
type Props = {
list: NodeTracing[][]
onBack: () => void
loopDurationMap?: LoopDurationMap
loopVariableMap?: LoopVariableMap
}
const LoopResultPanel: FC<Props> = ({
list,
onBack,
loopDurationMap,
loopVariableMap,
}) => {
const { t } = useTranslation()
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>({})
const toggleLoop = useCallback((index: number) => {
setExpandedLoops(prev => ({
...prev,
[index]: !prev[index],
}))
}, [])
const countLoopDuration = (loop: NodeTracing[], loopDurationMap: LoopDurationMap): string => {
const loopRunIndex = loop[0]?.execution_metadata?.loop_index as number
const loopRunId = loop[0]?.execution_metadata?.parallel_mode_run_id
const loopItem = loopDurationMap[loopRunId || loopRunIndex]
const duration = loopItem
return `${(duration && duration > 0.01) ? duration.toFixed(2) : 0.01}s`
}
const loopStatusShow = (index: number, loop: NodeTracing[], loopDurationMap?: LoopDurationMap) => {
const hasFailed = loop.some(item => item.status === NodeRunningStatus.Failed)
const isRunning = loop.some(item => item.status === NodeRunningStatus.Running)
const hasDurationMap = loopDurationMap && Object.keys(loopDurationMap).length !== 0
if (hasFailed)
return <RiErrorWarningLine className='h-4 w-4 text-text-destructive' />
if (isRunning)
return <RiLoader2Line className='h-3.5 w-3.5 animate-spin text-primary-600' />
return (
<>
{hasDurationMap && (
<div className='system-xs-regular text-text-tertiary'>
{countLoopDuration(loop, loopDurationMap)}
</div>
)}
<RiArrowRightSLine
className={cn(
'h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200',
expandedLoops[index] && 'rotate-90',
)}
/>
</>
)
}
return (
<div className='bg-components-panel-bg'>
<div
className='flex h-8 cursor-pointer items-center border-b-[0.5px] border-b-divider-regular px-4 text-text-accent-secondary'
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onBack()
}}
>
<RiArrowLeftLine className='mr-1 h-4 w-4' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
</div>
{/* List */}
<div className='bg-components-panel-bg p-2'>
{list.map((loop, index) => (
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl border-none bg-background-section-burn')}>
<div
className={cn(
'flex w-full cursor-pointer items-center justify-between px-3',
expandedLoops[index] ? 'pb-2 pt-3' : 'py-3',
'rounded-xl text-left',
)}
onClick={() => toggleLoop(index)}
>
<div className={cn('flex grow items-center gap-2')}>
<div className='flex h-4 w-4 shrink-0 items-center justify-center rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500'>
<Loop className='h-3 w-3 text-text-primary-on-surface' />
</div>
<span className='system-sm-semibold-uppercase grow text-text-primary'>
{t(`${i18nPrefix}.loop`)} {index + 1}
</span>
{loopStatusShow(index, loop, loopDurationMap)}
</div>
</div>
{expandedLoops[index] && <div
className="h-px grow bg-divider-subtle"
></div>}
<div className={cn(
'transition-all duration-200',
expandedLoops[index]
? 'opacity-100'
: 'max-h-0 overflow-hidden opacity-0',
)}>
{
loopVariableMap?.[index] && (
<div className='p-2 pb-0'>
<CodeEditor
readOnly
title={<div>{t('workflow.nodes.loop.loopVariables').toLocaleUpperCase()}</div>}
language={CodeLanguage.json}
height={112}
value={loopVariableMap[index]}
isJSONStringifyBeauty
/>
</div>
)
}
<TracingPanel
list={loop}
className='bg-background-section-burn'
/>
</div>
</div>
))}
</div>
</div>
)
}
export default React.memo(LoopResultPanel)

View File

@@ -0,0 +1,124 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiCloseLine,
} from '@remixicon/react'
import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
import TracingPanel from './tracing-panel'
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
import cn from '@/utils/classnames'
import type { NodeTracing } from '@/types/workflow'
const i18nPrefix = 'workflow.singleRun'
type Props = {
list: NodeTracing[][]
onHide: () => void
onBack: () => void
noWrap?: boolean
}
const LoopResultPanel: FC<Props> = ({
list,
onHide,
onBack,
noWrap,
}) => {
const { t } = useTranslation()
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>([])
const toggleLoop = useCallback((index: number) => {
setExpandedLoops(prev => ({
...prev,
[index]: !prev[index],
}))
}, [])
const main = (
<>
<div className={cn(!noWrap && 'shrink-0 ', 'px-4 pt-3')}>
<div className='flex h-8 shrink-0 items-center justify-between'>
<div className='system-xl-semibold truncate text-text-primary'>
{t(`${i18nPrefix}.testRunLoop`)}
</div>
<div className='ml-2 shrink-0 cursor-pointer p-1' onClick={onHide}>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
</div>
<div className='flex cursor-pointer items-center space-x-1 py-2 text-text-accent-secondary' onClick={onBack}>
<ArrowNarrowLeft className='h-4 w-4' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
</div>
</div>
{/* List */}
<div className={cn(!noWrap ? 'grow overflow-auto' : 'max-h-full', 'bg-components-panel-bg p-2')}>
{list.map((loop, index) => (
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl border-none bg-background-section-burn')}>
<div
className={cn(
'flex w-full cursor-pointer items-center justify-between px-3',
expandedLoops[index] ? 'pb-2 pt-3' : 'py-3',
'rounded-xl text-left',
)}
onClick={() => toggleLoop(index)}
>
<div className={cn('flex grow items-center gap-2')}>
<div className='flex h-4 w-4 shrink-0 items-center justify-center rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500'>
<Loop className='h-3 w-3 text-text-primary-on-surface' />
</div>
<span className='system-sm-semibold-uppercase grow text-text-primary'>
{t(`${i18nPrefix}.loop`)} {index + 1}
</span>
<RiArrowRightSLine className={cn(
'h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200',
expandedLoops[index] && 'rotate-90',
)} />
</div>
</div>
{expandedLoops[index] && <div
className="h-px grow bg-divider-subtle"
></div>}
<div className={cn(
'transition-all duration-200',
expandedLoops[index]
? 'opacity-100'
: 'max-h-0 overflow-hidden opacity-0',
)}>
<TracingPanel
list={loop}
className='bg-background-section-burn'
/>
</div>
</div>
))}
</div>
</>
)
const handleNotBubble = useCallback((e: React.MouseEvent) => {
// if not do this, it will trigger the message log modal disappear(useClickAway)
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}, [])
if (noWrap)
return main
return (
<div
className='absolute inset-0 z-10 rounded-2xl pt-10'
style={{
backgroundColor: 'rgba(16, 24, 40, 0.20)',
}}
onClick={handleNotBubble}
>
<div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'>
{main}
</div>
</div >
)
}
export default React.memo(LoopResultPanel)

View File

@@ -0,0 +1,117 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import useTimestamp from '@/hooks/use-timestamp'
type Props = {
status: string
executor?: string
startTime?: number
time?: number
tokens?: number
steps?: number
showSteps?: boolean
}
const MetaData: FC<Props> = ({
status,
executor,
startTime,
time,
tokens,
steps = 1,
showSteps = true,
}) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
return (
<div className='relative'>
<div className='system-xs-medium-uppercase h-6 py-1 text-text-tertiary'>{t('runLog.meta.title')}</div>
<div className='py-1'>
<div className='flex'>
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.status')}</div>
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
{status === 'running' && (
<div className='my-1 h-2 w-16 rounded-sm bg-text-quaternary'/>
)}
{status === 'succeeded' && (
<span>SUCCESS</span>
)}
{status === 'partial-succeeded' && (
<span>PARTIAL SUCCESS</span>
)}
{status === 'exception' && (
<span>EXCEPTION</span>
)}
{status === 'failed' && (
<span>FAIL</span>
)}
{status === 'stopped' && (
<span>STOP</span>
)}
</div>
</div>
<div className='flex'>
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.executor')}</div>
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
{status === 'running' && (
<div className='my-1 h-2 w-[88px] rounded-sm bg-text-quaternary'/>
)}
{status !== 'running' && (
<span>{executor || 'N/A'}</span>
)}
</div>
</div>
<div className='flex'>
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.startTime')}</div>
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
{status === 'running' && (
<div className='my-1 h-2 w-[72px] rounded-sm bg-text-quaternary'/>
)}
{status !== 'running' && (
<span>{startTime ? formatTime(startTime, t('appLog.dateTimeFormat') as string) : '-'}</span>
)}
</div>
</div>
<div className='flex'>
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.time')}</div>
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
{status === 'running' && (
<div className='my-1 h-2 w-[72px] rounded-sm bg-text-quaternary'/>
)}
{status !== 'running' && (
<span>{time ? `${time.toFixed(3)}s` : '-'}</span>
)}
</div>
</div>
<div className='flex'>
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.tokens')}</div>
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
{status === 'running' && (
<div className='my-1 h-2 w-[48px] rounded-sm bg-text-quaternary'/>
)}
{status !== 'running' && (
<span>{`${tokens || 0} Tokens`}</span>
)}
</div>
</div>
{showSteps && (
<div className='flex'>
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.steps')}</div>
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
{status === 'running' && (
<div className='my-1 h-2 w-[24px] rounded-sm bg-text-quaternary'/>
)}
{status !== 'running' && (
<span>{steps}</span>
)}
</div>
</div>
)}
</div>
</div>
)
}
export default MetaData

View File

@@ -0,0 +1,270 @@
'use client'
import { useTranslation } from 'react-i18next'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import {
RiAlertFill,
RiArrowRightSLine,
RiCheckboxCircleFill,
RiErrorWarningLine,
RiLoader2Line,
} from '@remixicon/react'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import { RetryLogTrigger } from './retry-log'
import { IterationLogTrigger } from './iteration-log'
import { LoopLogTrigger } from './loop-log'
import { AgentLogTrigger } from './agent-log'
import cn from '@/utils/classnames'
import StatusContainer from '@/app/components/workflow/run/status-container'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type {
AgentLogItemWithChildren,
IterationDurationMap,
LoopDurationMap,
LoopVariableMap,
NodeTracing,
} from '@/types/workflow'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
import { hasRetryNode } from '@/app/components/workflow/utils'
import { useDocLink } from '@/context/i18n'
import Tooltip from '@/app/components/base/tooltip'
import LargeDataAlert from '../variable-inspect/large-data-alert'
type Props = {
className?: string
nodeInfo: NodeTracing
allExecutions?: NodeTracing[]
inMessage?: boolean
hideInfo?: boolean
hideProcessDetail?: boolean
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
notShowIterationNav?: boolean
notShowLoopNav?: boolean
}
const NodePanel: FC<Props> = ({
className,
nodeInfo,
allExecutions,
inMessage = false,
hideInfo = false,
hideProcessDetail,
onShowIterationDetail,
onShowLoopDetail,
onShowRetryDetail,
onShowAgentOrToolLog,
notShowIterationNav,
notShowLoopNav,
}) => {
const [collapseState, doSetCollapseState] = useState<boolean>(true)
const setCollapseState = useCallback((state: boolean) => {
if (hideProcessDetail)
return
doSetCollapseState(state)
}, [hideProcessDetail])
const { t } = useTranslation()
const docLink = useDocLink()
const getTime = (time: number) => {
if (time < 1)
return `${(time * 1000).toFixed(3)} ms`
if (time > 60)
return `${Math.floor(time / 60)} m ${(time % 60).toFixed(3)} s`
return `${time.toFixed(3)} s`
}
const getTokenCount = (tokens: number) => {
if (tokens < 1000)
return tokens
if (tokens >= 1000 && tokens < 1000000)
return `${Number.parseFloat((tokens / 1000).toFixed(3))}K`
if (tokens >= 1000000)
return `${Number.parseFloat((tokens / 1000000).toFixed(3))}M`
}
useEffect(() => {
setCollapseState(!nodeInfo.expand)
}, [nodeInfo.expand, setCollapseState])
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!nodeInfo.details?.length
const isLoopNode = nodeInfo.node_type === BlockEnum.Loop && !!nodeInfo.details?.length
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
const inputsTitle = useMemo(() => {
let text = t('workflow.common.input')
if (nodeInfo.node_type === BlockEnum.Loop)
text = t('workflow.nodes.loop.initialLoopVariables')
return text.toLocaleUpperCase()
}, [nodeInfo.node_type, t])
const processDataTitle = t('workflow.common.processData').toLocaleUpperCase()
const outputTitle = useMemo(() => {
let text = t('workflow.common.output')
if (nodeInfo.node_type === BlockEnum.Loop)
text = t('workflow.nodes.loop.finalLoopVariables')
return text.toLocaleUpperCase()
}, [nodeInfo.node_type, t])
return (
<div className={cn('px-2 py-1', className)}>
<div className='group rounded-[10px] border border-components-panel-border bg-background-default shadow-xs transition-all hover:shadow-md'>
<div
className={cn(
'flex cursor-pointer items-center pl-1 pr-3',
hideInfo ? 'py-2 pl-2' : 'py-1.5',
!collapseState && (hideInfo ? '!pb-1' : '!pb-1.5'),
)}
onClick={() => setCollapseState(!collapseState)}
>
{!hideProcessDetail && (
<RiArrowRightSLine
className={cn(
'mr-1 h-4 w-4 shrink-0 text-text-quaternary transition-all group-hover:text-text-tertiary',
!collapseState && 'rotate-90',
)}
/>
)}
<BlockIcon size={inMessage ? 'xs' : 'sm'} className={cn('mr-2 shrink-0', inMessage && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
<Tooltip
popupContent={
<div className='max-w-xs'>{nodeInfo.title}</div>
}
>
<div className={cn(
'system-xs-semibold-uppercase grow truncate text-text-secondary',
hideInfo && '!text-xs',
)}>{nodeInfo.title}</div>
</Tooltip>
{nodeInfo.status !== 'running' && !hideInfo && (
<div className='system-xs-regular shrink-0 text-text-tertiary'>{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}</div>
)}
{nodeInfo.status === 'succeeded' && (
<RiCheckboxCircleFill className='ml-2 h-3.5 w-3.5 shrink-0 text-text-success' />
)}
{nodeInfo.status === 'failed' && (
<RiErrorWarningLine className='ml-2 h-3.5 w-3.5 shrink-0 text-text-warning' />
)}
{nodeInfo.status === 'stopped' && (
<RiAlertFill className={cn('ml-2 h-4 w-4 shrink-0 text-text-warning-secondary', inMessage && 'h-3.5 w-3.5')} />
)}
{nodeInfo.status === 'exception' && (
<RiAlertFill className={cn('ml-2 h-4 w-4 shrink-0 text-text-warning-secondary', inMessage && 'h-3.5 w-3.5')} />
)}
{nodeInfo.status === 'running' && (
<div className='flex shrink-0 items-center text-[13px] font-medium leading-[16px] text-text-accent'>
<span className='mr-2 text-xs font-normal'>Running</span>
<RiLoader2Line className='h-3.5 w-3.5 animate-spin' />
</div>
)}
</div>
{!collapseState && !hideProcessDetail && (
<div className='px-1 pb-1'>
{/* The nav to the iteration detail */}
{isIterationNode && !notShowIterationNav && onShowIterationDetail && (
<IterationLogTrigger
nodeInfo={nodeInfo}
allExecutions={allExecutions}
onShowIterationResultList={onShowIterationDetail}
/>
)}
{/* The nav to the Loop detail */}
{isLoopNode && !notShowLoopNav && onShowLoopDetail && (
<LoopLogTrigger
nodeInfo={nodeInfo}
allExecutions={allExecutions}
onShowLoopResultList={onShowLoopDetail}
/>
)}
{isRetryNode && onShowRetryDetail && (
<RetryLogTrigger
nodeInfo={nodeInfo}
onShowRetryResultList={onShowRetryDetail}
/>
)}
{
(isAgentNode || isToolNode) && onShowAgentOrToolLog && (
<AgentLogTrigger
nodeInfo={nodeInfo}
onShowAgentOrToolLog={onShowAgentOrToolLog}
/>
)
}
<div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}>
{(nodeInfo.status === 'stopped') && (
<StatusContainer status='stopped'>
{t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}
</StatusContainer>
)}
{(nodeInfo.status === 'exception') && (
<StatusContainer status='stopped'>
{nodeInfo.error}
<a
href={docLink('/guides/workflow/error-handling/error-type')}
target='_blank'
className='text-text-accent'
>
{t('workflow.common.learnMore')}
</a>
</StatusContainer>
)}
{nodeInfo.status === 'failed' && (
<StatusContainer status='failed'>
{nodeInfo.error}
</StatusContainer>
)}
{nodeInfo.status === 'retry' && (
<StatusContainer status='failed'>
{nodeInfo.error}
</StatusContainer>
)}
</div>
{nodeInfo.inputs && (
<div className={cn('mb-1')}>
<CodeEditor
readOnly
title={<div>{inputsTitle}</div>}
language={CodeLanguage.json}
value={nodeInfo.inputs}
isJSONStringifyBeauty
footer={nodeInfo.inputs_truncated && <LargeDataAlert textHasNoExport className='mx-1 mb-1 mt-2 h-7' />}
/>
</div>
)}
{nodeInfo.process_data && (
<div className={cn('mb-1')}>
<CodeEditor
readOnly
title={<div>{processDataTitle}</div>}
language={CodeLanguage.json}
value={nodeInfo.process_data}
isJSONStringifyBeauty
/>
</div>
)}
{nodeInfo.outputs && (
<div>
<CodeEditor
readOnly
title={<div>{outputTitle}</div>}
language={CodeLanguage.json}
value={nodeInfo.outputs}
isJSONStringifyBeauty
tip={<ErrorHandleTip type={nodeInfo.execution_metadata?.error_strategy} />}
footer={nodeInfo.outputs_truncated && <LargeDataAlert textHasNoExport downloadUrl={nodeInfo.outputs_full_content?.download_url} className='mx-1 mb-1 mt-2 h-7' />}
/>
</div>
)}
</div>
)}
</div>
</div>
)
}
export default NodePanel

View File

@@ -0,0 +1,111 @@
'use client'
import type { FC } from 'react'
import { useMemo } from 'react'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { Markdown } from '@/app/components/base/markdown'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
import { FileList } from '@/app/components/base/file-uploader'
import StatusContainer from '@/app/components/workflow/run/status-container'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
type OutputPanelProps = {
isRunning?: boolean
outputs?: any
error?: string
height?: number
}
const OutputPanel: FC<OutputPanelProps> = ({
isRunning,
outputs,
error,
height,
}) => {
const isTextOutput = useMemo(() => {
if (!outputs || typeof outputs !== 'object')
return false
const keys = Object.keys(outputs)
const value = outputs[keys[0]]
return keys.length === 1 && (
typeof value === 'string'
|| (Array.isArray(value) && value.every(item => typeof item === 'string'))
)
}, [outputs])
const fileList = useMemo(() => {
const fileList: any[] = []
if (!outputs)
return fileList
if (Object.keys(outputs).length > 1)
return fileList
for (const key in outputs) {
if (Array.isArray(outputs[key])) {
outputs[key].map((output: any) => {
if (output?.dify_model_identity === '__dify__file__')
fileList.push(output)
return null
})
}
else if (outputs[key]?.dify_model_identity === '__dify__file__') {
fileList.push(outputs[key])
}
}
return getProcessedFilesFromResponse(fileList)
}, [outputs])
return (
<div className='p-2'>
{isRunning && (
<div className='pl-[26px] pt-4'>
<LoadingAnim type='text' />
</div>
)}
{!isRunning && error && (
<div className='px-4'>
<StatusContainer status='failed'>{error}</StatusContainer>
</div>
)}
{!isRunning && !outputs && (
<div className='px-4 py-2'>
<Markdown content='No Output' />
</div>
)}
{isTextOutput && (
<div className='px-4 py-2'>
<Markdown
content={
Array.isArray(outputs[Object.keys(outputs)[0]])
? outputs[Object.keys(outputs)[0]].join('\n')
: (outputs[Object.keys(outputs)[0]] || '')
}
/>
</div>
)}
{fileList.length > 0 && (
<div className='px-4 py-2'>
<FileList
files={fileList}
showDeleteAction={false}
showDownloadAction
canPreview
/>
</div>
)}
{!isTextOutput && outputs && Object.keys(outputs).length > 0 && height! > 0 && (
<div className='flex flex-col gap-2'>
<CodeEditor
showFileList
readOnly
title={<div tabIndex={0}>Output</div>}
language={CodeLanguage.json}
value={JSON.stringify(outputs, null, 2)}
isJSONStringifyBeauty
height={height ? (height - 16) / 2 : undefined}
/>
</div>
)}
</div>
)
}
export default OutputPanel

View File

@@ -0,0 +1,177 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import StatusPanel from './status'
import MetaData from './meta'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
import type {
AgentLogItemWithChildren,
NodeTracing,
} from '@/types/workflow'
import { BlockEnum } from '@/app/components/workflow/types'
import { hasRetryNode } from '@/app/components/workflow/utils'
import { IterationLogTrigger } from '@/app/components/workflow/run/iteration-log'
import { LoopLogTrigger } from '@/app/components/workflow/run/loop-log'
import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
import { AgentLogTrigger } from '@/app/components/workflow/run/agent-log'
import LargeDataAlert from '../variable-inspect/large-data-alert'
export type ResultPanelProps = {
nodeInfo?: NodeTracing
inputs?: string
inputs_truncated?: boolean
process_data?: string
process_data_truncated?: boolean
outputs?: string | Record<string, any>
outputs_truncated?: boolean
outputs_full_content?: {
download_url: string
}
status: string
error?: string
elapsed_time?: number
total_tokens?: number
created_at?: number
created_by?: string
finished_at?: number
steps?: number
showSteps?: boolean
exceptionCounts?: number
execution_metadata?: any
isListening?: boolean
handleShowIterationResultList?: (detail: NodeTracing[][], iterDurationMap: any) => void
handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
}
const ResultPanel: FC<ResultPanelProps> = ({
nodeInfo,
inputs,
inputs_truncated,
process_data,
process_data_truncated,
outputs,
outputs_truncated,
outputs_full_content,
status,
error,
elapsed_time,
total_tokens,
created_at,
created_by,
steps,
showSteps,
exceptionCounts,
execution_metadata,
isListening = false,
handleShowIterationResultList,
handleShowLoopResultList,
onShowRetryDetail,
handleShowAgentOrToolLog,
}) => {
const { t } = useTranslation()
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length
const isLoopNode = nodeInfo?.node_type === BlockEnum.Loop && !!nodeInfo?.details?.length
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length
return (
<div className='bg-components-panel-bg py-2'>
<div className='px-4 py-2'>
<StatusPanel
status={status}
time={elapsed_time}
tokens={total_tokens}
error={error}
exceptionCounts={exceptionCounts}
isListening={isListening}
/>
</div>
<div className='px-4'>
{
isIterationNode && handleShowIterationResultList && (
<IterationLogTrigger
nodeInfo={nodeInfo}
onShowIterationResultList={handleShowIterationResultList}
/>
)
}
{
isLoopNode && handleShowLoopResultList && (
<LoopLogTrigger
nodeInfo={nodeInfo}
onShowLoopResultList={handleShowLoopResultList}
/>
)
}
{
isRetryNode && onShowRetryDetail && (
<RetryLogTrigger
nodeInfo={nodeInfo}
onShowRetryResultList={onShowRetryDetail}
/>
)
}
{
(isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
<AgentLogTrigger
nodeInfo={nodeInfo}
onShowAgentOrToolLog={handleShowAgentOrToolLog}
/>
)
}
</div>
<div className='flex flex-col gap-2 px-4 py-2'>
<CodeEditor
readOnly
title={<div>{t('workflow.common.input').toLocaleUpperCase()}</div>}
language={CodeLanguage.json}
value={inputs}
isJSONStringifyBeauty
footer={inputs_truncated && <LargeDataAlert textHasNoExport className='mx-1 mb-1 mt-2 h-7' />}
/>
{process_data && (
<CodeEditor
readOnly
title={<div>{t('workflow.common.processData').toLocaleUpperCase()}</div>}
language={CodeLanguage.json}
value={process_data}
isJSONStringifyBeauty
footer={process_data_truncated && <LargeDataAlert textHasNoExport className='mx-1 mb-1 mt-2 h-7' />}
/>
)}
{(outputs || status === 'running') && (
<CodeEditor
readOnly
title={<div>{t('workflow.common.output').toLocaleUpperCase()}</div>}
language={CodeLanguage.json}
value={outputs}
isJSONStringifyBeauty
tip={<ErrorHandleTip type={execution_metadata?.error_strategy} />}
footer={outputs_truncated && <LargeDataAlert textHasNoExport downloadUrl={outputs_full_content?.download_url} className='mx-1 mb-1 mt-2 h-7' />}
/>
)}
</div>
<div className='px-4 py-2'>
<div className='divider-subtle h-[0.5px]' />
</div>
<div className='px-4 py-2'>
<MetaData
status={status}
executor={created_by}
startTime={created_at}
time={elapsed_time}
tokens={total_tokens}
steps={steps}
showSteps={showSteps}
/>
</div>
</div>
)
}
export default ResultPanel

View File

@@ -0,0 +1,75 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { ImageIndentLeft } from '@/app/components/base/icons/src/vender/line/editor'
import { Markdown } from '@/app/components/base/markdown'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
import StatusContainer from '@/app/components/workflow/run/status-container'
import { FileList } from '@/app/components/base/file-uploader'
type ResultTextProps = {
isRunning?: boolean
outputs?: any
error?: string
onClick?: () => void
allFiles?: any[]
}
const ResultText: FC<ResultTextProps> = ({
isRunning,
outputs,
error,
onClick,
allFiles,
}) => {
const { t } = useTranslation()
return (
<div className='bg-background-section-burn'>
{isRunning && !outputs && (
<div className='pl-[26px] pt-4'>
<LoadingAnim type='text' />
</div>
)}
{!isRunning && error && (
<div className='px-4 py-2'>
<StatusContainer status='failed'>
{error}
</StatusContainer>
</div>
)}
{!isRunning && !outputs && !error && !allFiles?.length && (
<div className='mt-[120px] flex flex-col items-center px-4 py-2 text-[13px] leading-[18px] text-gray-500'>
<ImageIndentLeft className='h-6 w-6 text-gray-400' />
<div className='mr-2'>{t('runLog.resultEmpty.title')}</div>
<div>
{t('runLog.resultEmpty.tipLeft')}
<span onClick={onClick} className='cursor-pointer text-primary-600'>{t('runLog.resultEmpty.link')}</span>
{t('runLog.resultEmpty.tipRight')}
</div>
</div>
)}
{(outputs || !!allFiles?.length) && (
<>
{outputs && (
<div className='px-4 py-2'>
<Markdown content={outputs} />
</div>
)}
{!!allFiles?.length && allFiles.map(item => (
<div key={item.varName} className='system-xs-regular flex flex-col gap-1 px-4 py-2'>
<div className='py-1 text-text-tertiary '>{item.varName}</div>
<FileList
files={item.list}
showDeleteAction={false}
showDownloadAction
canPreview
/>
</div>
))}
</>
)}
</div>
)
}
export default ResultText

View File

@@ -0,0 +1,2 @@
export { default as RetryLogTrigger } from './retry-log-trigger'
export { default as RetryResultPanel } from './retry-result-panel'

View File

@@ -0,0 +1,41 @@
import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiRestartFill,
} from '@remixicon/react'
import Button from '@/app/components/base/button'
import type { NodeTracing } from '@/types/workflow'
type RetryLogTriggerProps = {
nodeInfo: NodeTracing
onShowRetryResultList: (detail: NodeTracing[]) => void
}
const RetryLogTrigger = ({
nodeInfo,
onShowRetryResultList,
}: RetryLogTriggerProps) => {
const { t } = useTranslation()
const { retryDetail } = nodeInfo
const handleShowRetryResultList = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowRetryResultList(retryDetail || [])
}
return (
<Button
className='mb-1 flex w-full items-center justify-between'
variant='tertiary'
onClick={handleShowRetryResultList}
>
<div className='flex items-center'>
<RiRestartFill className='mr-0.5 h-4 w-4 shrink-0 text-components-button-tertiary-text' />
{t('workflow.nodes.common.retry.retries', { num: retryDetail?.length })}
</div>
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
</Button>
)
}
export default RetryLogTrigger

View File

@@ -0,0 +1,46 @@
'use client'
import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowLeftLine,
} from '@remixicon/react'
import TracingPanel from '../tracing-panel'
import type { NodeTracing } from '@/types/workflow'
type Props = {
list: NodeTracing[]
onBack: () => void
}
const RetryResultPanel: FC<Props> = ({
list,
onBack,
}) => {
const { t } = useTranslation()
return (
<div>
<div
className='system-sm-medium flex h-8 cursor-pointer items-center bg-components-panel-bg px-4 text-text-accent-secondary'
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onBack()
}}
>
<RiArrowLeftLine className='mr-1 h-4 w-4' />
{t('workflow.singleRun.back')}
</div>
<TracingPanel
list={list.map((item, index) => ({
...item,
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
}))}
className='bg-background-section-burn'
/>
</div >
)
}
export default memo(RetryResultPanel)

View File

@@ -0,0 +1,98 @@
import { RetryResultPanel } from './retry-log'
import { IterationResultPanel } from './iteration-log'
import { LoopResultPanel } from './loop-log'
import { AgentResultPanel } from './agent-log'
import type {
AgentLogItemWithChildren,
IterationDurationMap,
LoopDurationMap,
LoopVariableMap,
NodeTracing,
} from '@/types/workflow'
export type SpecialResultPanelProps = {
showRetryDetail?: boolean
setShowRetryDetailFalse?: () => void
retryResultList?: NodeTracing[]
showIteratingDetail?: boolean
setShowIteratingDetailFalse?: () => void
iterationResultList?: NodeTracing[][]
iterationResultDurationMap?: IterationDurationMap
showLoopingDetail?: boolean
setShowLoopingDetailFalse?: () => void
loopResultList?: NodeTracing[][]
loopResultDurationMap?: LoopDurationMap
loopResultVariableMap?: LoopVariableMap
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
}
const SpecialResultPanel = ({
showRetryDetail,
setShowRetryDetailFalse,
retryResultList,
showIteratingDetail,
setShowIteratingDetailFalse,
iterationResultList,
iterationResultDurationMap,
showLoopingDetail,
setShowLoopingDetailFalse,
loopResultList,
loopResultDurationMap,
loopResultVariableMap,
agentOrToolLogItemStack,
agentOrToolLogListMap,
handleShowAgentOrToolLog,
}: SpecialResultPanelProps) => {
return (
<div onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}}>
{
!!showRetryDetail && !!retryResultList?.length && setShowRetryDetailFalse && (
<RetryResultPanel
list={retryResultList}
onBack={setShowRetryDetailFalse}
/>
)
}
{
showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && (
<IterationResultPanel
list={iterationResultList}
onBack={setShowIteratingDetailFalse}
iterDurationMap={iterationResultDurationMap}
/>
)
}
{
showLoopingDetail && !!loopResultList?.length && setShowLoopingDetailFalse && (
<LoopResultPanel
list={loopResultList}
onBack={setShowLoopingDetailFalse}
loopDurationMap={loopResultDurationMap}
loopVariableMap={loopResultVariableMap}
/>
)
}
{
!!agentOrToolLogItemStack?.length && agentOrToolLogListMap && handleShowAgentOrToolLog && (
<AgentResultPanel
agentOrToolLogItemStack={agentOrToolLogItemStack}
agentOrToolLogListMap={agentOrToolLogListMap}
onShowAgentOrToolLog={handleShowAgentOrToolLog}
/>
)
}
</div>
)
}
export default SpecialResultPanel

View File

@@ -0,0 +1,52 @@
'use client'
import type { FC } from 'react'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'
import useTheme from '@/hooks/use-theme'
type Props = {
status: string
children?: React.ReactNode
}
const StatusContainer: FC<Props> = ({
status,
children,
}) => {
const { theme } = useTheme()
return (
<div
className={cn(
'system-xs-regular relative break-all rounded-lg border px-3 py-2.5',
status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
status === 'succeeded' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'succeeded' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
status === 'partial-succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
status === 'partial-succeeded' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'partial-succeeded' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] text-text-warning',
status === 'failed' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'failed' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(240,68,56,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
status === 'stopped' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'stopped' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
status === 'exception' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
status === 'exception' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'exception' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
status === 'running' && 'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] text-util-colors-blue-light-blue-light-600',
status === 'running' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'running' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(11,165,236,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
)}
>
<div className={cn(
'absolute left-0 top-0 h-[50px] w-[65%] bg-no-repeat',
theme === Theme.light && 'bg-[url(~@/app/components/workflow/run/assets/highlight.svg)]',
theme === Theme.dark && 'bg-[url(~@/app/components/workflow/run/assets/highlight-dark.svg)]',
)}></div>
{children}
</div>
)
}
export default StatusContainer

View File

@@ -0,0 +1,155 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import Indicator from '@/app/components/header/indicator'
import StatusContainer from '@/app/components/workflow/run/status-container'
import { useDocLink } from '@/context/i18n'
type ResultProps = {
status: string
time?: number
tokens?: number
error?: string
exceptionCounts?: number
isListening?: boolean
}
const StatusPanel: FC<ResultProps> = ({
status,
time,
tokens,
error,
exceptionCounts,
isListening = false,
}) => {
const { t } = useTranslation()
const docLink = useDocLink()
return (
<StatusContainer status={status}>
<div className='flex'>
<div className={cn(
'max-w-[120px] flex-[33%]',
status === 'partial-succeeded' && 'min-w-[140px]',
)}>
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t('runLog.resultPanel.status')}</div>
<div
className={cn(
'system-xs-semibold-uppercase flex items-center gap-1',
status === 'succeeded' && 'text-util-colors-green-green-600',
status === 'partial-succeeded' && 'text-util-colors-green-green-600',
status === 'failed' && 'text-util-colors-red-red-600',
status === 'stopped' && 'text-util-colors-warning-warning-600',
status === 'running' && 'text-util-colors-blue-light-blue-light-600',
)}
>
{status === 'running' && (
<>
<Indicator color={'blue'} />
<span>{isListening ? 'Listening' : 'Running'}</span>
</>
)}
{status === 'succeeded' && (
<>
<Indicator color={'green'} />
<span>SUCCESS</span>
</>
)}
{status === 'partial-succeeded' && (
<>
<Indicator color={'green'} />
<span>PARTIAL SUCCESS</span>
</>
)}
{status === 'exception' && (
<>
<Indicator color={'yellow'} />
<span>EXCEPTION</span>
</>
)}
{status === 'failed' && (
<>
<Indicator color={'red'} />
<span>FAIL</span>
</>
)}
{status === 'stopped' && (
<>
<Indicator color={'yellow'} />
<span>STOP</span>
</>
)}
</div>
</div>
<div className='max-w-[152px] flex-[33%]'>
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t('runLog.resultPanel.time')}</div>
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
{status === 'running' && (
<div className='h-2 w-16 rounded-sm bg-text-quaternary' />
)}
{status !== 'running' && (
<span>{time ? `${time?.toFixed(3)}s` : '-'}</span>
)}
</div>
</div>
<div className='flex-[33%]'>
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t('runLog.resultPanel.tokens')}</div>
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
{status === 'running' && (
<div className='h-2 w-20 rounded-sm bg-text-quaternary' />
)}
{status !== 'running' && (
<span>{`${tokens || 0} Tokens`}</span>
)}
</div>
</div>
</div>
{status === 'failed' && error && (
<>
<div className='my-2 h-[0.5px] bg-divider-subtle'/>
<div className='system-xs-regular whitespace-pre-wrap text-text-destructive'>{error}</div>
{
!!exceptionCounts && (
<>
<div className='my-2 h-[0.5px] bg-divider-subtle'/>
<div className='system-xs-regular text-text-destructive'>
{t('workflow.nodes.common.errorHandle.partialSucceeded.tip', { num: exceptionCounts })}
</div>
</>
)
}
</>
)}
{
status === 'partial-succeeded' && !!exceptionCounts && (
<>
<div className='my-2 h-[0.5px] bg-divider-deep'/>
<div className='system-xs-medium text-text-warning'>
{t('workflow.nodes.common.errorHandle.partialSucceeded.tip', { num: exceptionCounts })}
</div>
</>
)
}
{
status === 'exception' && (
<>
<div className='my-2 h-[0.5px] bg-divider-deep'/>
<div className='system-xs-medium text-text-warning'>
{error}
<a
href={docLink('/guides/workflow/error-handling/error-type')}
target='_blank'
className='text-text-accent'
>
{t('workflow.common.learnMore')}
</a>
</div>
</>
)
}
</StatusContainer>
)
}
export default StatusPanel

View File

@@ -0,0 +1,199 @@
'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 { 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'
type TracingPanelProps = {
list: NodeTracing[]
className?: string
hideNodeInfo?: boolean
hideNodeProcessDetail?: boolean
}
const TracingPanel: FC<TracingPanelProps> = ({
list,
className,
hideNodeInfo = false,
hideNodeProcessDetail = false,
}) => {
const { t } = useTranslation()
const treeNodes = formatNodeList(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 {
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 isCollapsed = collapsedNodes.has(node.id)
const isHovered = hoveredParallel === node.id
return (
<div
key={node.id}
className="relative mb-2 ml-4"
data-parallel-id={node.id}
onMouseEnter={() => handleParallelMouseEnter(node.id)}
onMouseLeave={handleParallelMouseLeave}
>
<div className="mb-1 flex items-center">
<button type="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="h-3 w-3" /> : <RiMenu4Line className="h-3 w-3 text-text-tertiary" />}
</button>
<div className="system-xs-semibold-uppercase flex items-center text-text-secondary">
<span>{parallelDetail.parallelTitle}</span>
</div>
<div
className="mx-2 h-px grow 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={cn(
'absolute bottom-0 left-[5px] top-0 w-[2px]',
isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
)}></div>
{parallelDetail.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>
<NodePanel
nodeInfo={node!}
allExecutions={list}
onShowIterationDetail={handleShowIterationResultList}
onShowLoopDetail={handleShowLoopResultList}
onShowRetryDetail={handleShowRetryResultList}
onShowAgentOrToolLog={handleShowAgentOrToolLog}
hideInfo={hideNodeInfo}
hideProcessDetail={hideNodeProcessDetail}
/>
</div>
)
}
}
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()
}}
>
{treeNodes.map(renderNode)}
</div>
)
}
export default TracingPanel

View File

@@ -0,0 +1,179 @@
import { BlockEnum } from '@/app/components/workflow/types'
export const agentNodeData = (() => {
const node = {
node_type: BlockEnum.Agent,
execution_metadata: {
agent_log: [
{ id: '1', label: 'Root 1' },
{ id: '2', parent_id: '1', label: 'Child 1.2' },
{ id: '3', parent_id: '1', label: 'Child 1.3' },
{ id: '4', parent_id: '2', label: 'Child 2.4' },
{ id: '5', parent_id: '2', label: 'Child 2.5' },
{ id: '6', parent_id: '3', label: 'Child 3.6' },
{ id: '7', parent_id: '4', label: 'Child 4.7' },
{ id: '8', parent_id: '4', label: 'Child 4.8' },
{ id: '9', parent_id: '5', label: 'Child 5.9' },
{ id: '10', parent_id: '5', label: 'Child 5.10' },
{ id: '11', parent_id: '7', label: 'Child 7.11' },
{ id: '12', parent_id: '7', label: 'Child 7.12' },
{ id: '13', parent_id: '9', label: 'Child 9.13' },
{ id: '14', parent_id: '9', label: 'Child 9.14' },
{ id: '15', parent_id: '9', label: 'Child 9.15' },
],
},
}
return {
in: [node],
expect: [{
...node,
agentLog: [
{
id: '1',
label: 'Root 1',
children: [
{
id: '2',
parent_id: '1',
label: 'Child 1.2',
children: [
{
id: '4',
parent_id: '2',
label: 'Child 2.4',
children: [
{
id: '7',
parent_id: '4',
label: 'Child 4.7',
children: [
{ id: '11', parent_id: '7', label: 'Child 7.11' },
{ id: '12', parent_id: '7', label: 'Child 7.12' },
],
},
{ id: '8', parent_id: '4', label: 'Child 4.8' },
],
},
{
id: '5',
parent_id: '2',
label: 'Child 2.5',
children: [
{
id: '9',
parent_id: '5',
label: 'Child 5.9',
children: [
{ id: '13', parent_id: '9', label: 'Child 9.13' },
{ id: '14', parent_id: '9', label: 'Child 9.14' },
{ id: '15', parent_id: '9', label: 'Child 9.15' },
],
},
{ id: '10', parent_id: '5', label: 'Child 5.10' },
],
},
],
},
{
id: '3',
parent_id: '1',
label: 'Child 1.3',
children: [
{ id: '6', parent_id: '3', label: 'Child 3.6' },
],
},
],
},
],
}],
}
})()
export const oneStepCircle = (() => {
const node = {
node_type: BlockEnum.Agent,
execution_metadata: {
agent_log: [
{ id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
],
},
}
return {
in: [node],
expect: [{
...node,
agentLog: [
{
id: '1',
label: 'Node 1',
hasCircle: true,
children: [],
},
],
}],
}
})()
export const multiStepsCircle = (() => {
const node = {
node_type: BlockEnum.Agent,
execution_metadata: {
agent_log: [
// 1 -> [2 -> 4 -> 1, 3]
{ id: '1', label: 'Node 1' },
{ id: '2', parent_id: '1', label: 'Node 2' },
{ id: '3', parent_id: '1', label: 'Node 3' },
{ id: '4', parent_id: '2', label: 'Node 4' },
// Loop
{ id: '1', parent_id: '4', label: 'Node 1' },
{ id: '2', parent_id: '1', label: 'Node 2' },
{ id: '4', parent_id: '2', label: 'Node 4' },
{ id: '1', parent_id: '4', label: 'Node 1' },
{ id: '2', parent_id: '1', label: 'Node 2' },
{ id: '4', parent_id: '2', label: 'Node 4' },
],
},
}
// 1 -> [2(4(1(2(4...)))), 3]
return {
in: [node],
expect: [{
...node,
agentLog: [
{
id: '1',
label: 'Node 1',
children: [
{
id: '2',
parent_id: '1',
label: 'Node 2',
children: [
{
id: '4',
parent_id: '2',
label: 'Node 4',
children: [],
hasCircle: true,
},
],
},
{
id: '3',
parent_id: '1',
label: 'Node 3',
},
],
},
],
}],
}
})()

View File

@@ -0,0 +1,15 @@
import format from '.'
import { agentNodeData, multiStepsCircle, oneStepCircle } from './data'
describe('agent', () => {
test('list should transform to tree', () => {
// console.log(format(agentNodeData.in as any))
expect(format(agentNodeData.in as any)).toEqual(agentNodeData.expect)
})
test('list should remove circle log item', () => {
// format(oneStepCircle.in as any)
expect(format(oneStepCircle.in as any)).toEqual(oneStepCircle.expect)
expect(format(multiStepsCircle.in as any)).toEqual(multiStepsCircle.expect)
})
})

View File

@@ -0,0 +1,149 @@
import { BlockEnum } from '@/app/components/workflow/types'
import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow'
import { cloneDeep } from 'lodash-es'
const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool]
const remove = (node: AgentLogItemWithChildren, removeId: string) => {
let { children } = node
if (!children || children.length === 0)
return
const hasCircle = !!children.find((c) => {
const childId = c.message_id || (c as any).id
return childId === removeId
})
if (hasCircle) {
node.hasCircle = true
node.children = node.children.filter((c) => {
const childId = c.message_id || (c as any).id
return childId !== removeId
})
children = node.children
}
children.forEach((child) => {
remove(child, removeId)
})
}
const removeRepeatedSiblings = (list: AgentLogItemWithChildren[]) => {
if (!list || list.length === 0)
return []
const result: AgentLogItemWithChildren[] = []
const addedItemIds: string[] = []
list.forEach((item) => {
const itemId = item.message_id || (item as any).id
if (itemId && !addedItemIds.includes(itemId)) {
result.push(item)
addedItemIds.push(itemId)
}
})
return result
}
const removeCircleLogItem = (log: AgentLogItemWithChildren) => {
const newLog = cloneDeep(log)
// If no children, return as is
if (!newLog.children || newLog.children.length === 0)
return newLog
newLog.children = removeRepeatedSiblings(newLog.children)
const id = newLog.message_id || (newLog as any).id
let { children } = newLog
// check one step circle
const hasOneStepCircle = !!children.find((c) => {
const childId = c.message_id || (c as any).id
return childId === id
})
if (hasOneStepCircle) {
newLog.hasCircle = true
newLog.children = newLog.children.filter((c) => {
const childId = c.message_id || (c as any).id
return childId !== id
})
children = newLog.children
}
children.forEach((child, index) => {
remove(child, id) // check multi steps circle
children[index] = removeCircleLogItem(child)
})
return newLog
}
const listToTree = (logs: AgentLogItem[]) => {
if (!logs || logs.length === 0)
return []
// First pass: identify all unique items and track parent-child relationships
const itemsById = new Map<string, any>()
const childrenById = new Map<string, any[]>()
logs.forEach((item) => {
const itemId = item.message_id || (item as any).id
// Only add to itemsById if not already there (keep first occurrence)
if (itemId && !itemsById.has(itemId))
itemsById.set(itemId, item)
// Initialize children array for this ID if needed
if (itemId && !childrenById.has(itemId))
childrenById.set(itemId, [])
// If this item has a parent, add it to parent's children list
if (item.parent_id) {
if (!childrenById.has(item.parent_id))
childrenById.set(item.parent_id, [])
childrenById.get(item.parent_id)!.push(item)
}
})
// Second pass: build tree structure
const tree: AgentLogItemWithChildren[] = []
// Find root nodes (items without parents)
itemsById.forEach((item) => {
const hasParent = !!item.parent_id
if (!hasParent) {
const itemId = item.message_id || (item as any).id
const children = childrenById.get(itemId)
if (children && children.length > 0)
item.children = children
tree.push(item as AgentLogItemWithChildren)
}
})
// Add children property to all items that have children
itemsById.forEach((item) => {
const itemId = item.message_id || (item as any).id
const children = childrenById.get(itemId)
if (children && children.length > 0)
item.children = children
})
return tree
}
const format = (list: NodeTracing[]): NodeTracing[] => {
const result: NodeTracing[] = list.map((item) => {
let treeList: AgentLogItemWithChildren[] = []
let removedCircleTree: AgentLogItemWithChildren[] = []
if (supportedAgentLogNodes.includes(item.node_type) && item.execution_metadata?.agent_log && item.execution_metadata?.agent_log.length > 0)
treeList = listToTree(item.execution_metadata.agent_log)
// console.log(JSON.stringify(treeList))
removedCircleTree = treeList.length > 0 ? treeList.map(t => removeCircleLogItem(t)) : []
item.agentLog = removedCircleTree
return item
})
return result
}
export default format

View File

@@ -0,0 +1,138 @@
import parseDSL from './graph-to-log-struct'
describe('parseDSL', () => {
it('should parse plain nodes correctly', () => {
const dsl = 'plainNode1 -> plainNode2'
const result = parseDSL(dsl)
expect(result).toEqual([
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: {}, status: 'succeeded' },
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: {}, status: 'succeeded' },
])
})
it('should parse retry nodes correctly', () => {
const dsl = '(retry, retryNode, 3)'
const result = parseDSL(dsl)
expect(result).toEqual([
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'succeeded' },
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
])
})
it('should parse iteration nodes correctly', () => {
const dsl = '(iteration, iterationNode, plainNode1 -> plainNode2)'
const result = parseDSL(dsl)
expect(result).toEqual([
{ id: 'iterationNode', node_id: 'iterationNode', title: 'iterationNode', node_type: 'iteration', execution_metadata: {}, status: 'succeeded' },
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0 }, status: 'succeeded' },
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0 }, status: 'succeeded' },
])
})
it('should parse loop nodes correctly', () => {
const dsl = '(loop, loopNode, plainNode1 -> plainNode2)'
const result = parseDSL(dsl)
expect(result).toEqual([
{ id: 'loopNode', node_id: 'loopNode', title: 'loopNode', node_type: 'loop', execution_metadata: {}, status: 'succeeded' },
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { loop_id: 'loopNode', loop_index: 0 }, status: 'succeeded' },
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { loop_id: 'loopNode', loop_index: 0 }, status: 'succeeded' },
])
})
it('should parse parallel nodes correctly', () => {
const dsl = '(parallel, parallelNode, nodeA, nodeB -> nodeC)'
const result = parseDSL(dsl)
expect(result).toEqual([
{ id: 'parallelNode', node_id: 'parallelNode', title: 'parallelNode', execution_metadata: { parallel_id: 'parallelNode' }, status: 'succeeded' },
{ id: 'nodeA', node_id: 'nodeA', title: 'nodeA', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeA' }, status: 'succeeded' },
{ id: 'nodeB', node_id: 'nodeB', title: 'nodeB', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeB' }, status: 'succeeded' },
{ id: 'nodeC', node_id: 'nodeC', title: 'nodeC', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeB' }, status: 'succeeded' },
])
})
// TODO
// it('should handle nested parallel nodes', () => {
// const dsl = '(parallel, outerParallel, (parallel, innerParallel, plainNode1 -> plainNode2) -> plainNode3)'
// const result = parseDSL(dsl)
// expect(result).toEqual([
// {
// id: 'outerParallel',
// node_id: 'outerParallel',
// title: 'outerParallel',
// execution_metadata: { parallel_id: 'outerParallel' },
// status: 'succeeded',
// },
// {
// id: 'innerParallel',
// node_id: 'innerParallel',
// title: 'innerParallel',
// execution_metadata: { parallel_id: 'outerParallel', parallel_start_node_id: 'innerParallel' },
// status: 'succeeded',
// },
// {
// id: 'plainNode1',
// node_id: 'plainNode1',
// title: 'plainNode1',
// execution_metadata: {
// parallel_id: 'innerParallel',
// parallel_start_node_id: 'plainNode1',
// parent_parallel_id: 'outerParallel',
// parent_parallel_start_node_id: 'innerParallel',
// },
// status: 'succeeded',
// },
// {
// id: 'plainNode2',
// node_id: 'plainNode2',
// title: 'plainNode2',
// execution_metadata: {
// parallel_id: 'innerParallel',
// parallel_start_node_id: 'plainNode1',
// parent_parallel_id: 'outerParallel',
// parent_parallel_start_node_id: 'innerParallel',
// },
// status: 'succeeded',
// },
// {
// id: 'plainNode3',
// node_id: 'plainNode3',
// title: 'plainNode3',
// execution_metadata: {
// parallel_id: 'outerParallel',
// parallel_start_node_id: 'innerParallel',
// },
// status: 'succeeded',
// },
// ])
// })
// iterations not support nested iterations
// it('should handle nested iterations', () => {
// const dsl = '(iteration, outerIteration, (iteration, innerIteration -> plainNode1 -> plainNode2))'
// const result = parseDSL(dsl)
// expect(result).toEqual([
// { id: 'outerIteration', node_id: 'outerIteration', title: 'outerIteration', node_type: 'iteration', execution_metadata: {}, status: 'succeeded' },
// { id: 'innerIteration', node_id: 'innerIteration', title: 'innerIteration', node_type: 'iteration', execution_metadata: { iteration_id: 'outerIteration', iteration_index: 0 }, status: 'succeeded' },
// { id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'innerIteration', iteration_index: 0 }, status: 'succeeded' },
// { id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'innerIteration', iteration_index: 0 }, status: 'succeeded' },
// ])
// })
// it('should handle nested iterations within parallel nodes', () => {
// const dsl = '(parallel, parallelNode, (iteration, iterationNode, plainNode1, plainNode2))'
// const result = parseDSL(dsl)
// expect(result).toEqual([
// { id: 'parallelNode', node_id: 'parallelNode', title: 'parallelNode', execution_metadata: { parallel_id: 'parallelNode' }, status: 'succeeded' },
// { id: 'iterationNode', node_id: 'iterationNode', title: 'iterationNode', node_type: 'iteration', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
// { id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0, parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
// { id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0, parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
// ])
// })
it('should throw an error for unknown node types', () => {
const dsl = '(unknown, nodeId)'
expect(() => parseDSL(dsl)).toThrowError('Unknown nodeType: unknown')
})
})

View File

@@ -0,0 +1,356 @@
type IterationInfo = { iterationId: string; iterationIndex: number }
type LoopInfo = { loopId: string; loopIndex: number }
type NodePlain = { nodeType: 'plain'; nodeId: string; } & (Partial<IterationInfo> & Partial<LoopInfo>)
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & (Partial<IterationInfo> & Partial<LoopInfo>)) | Node[] | number)[] } & (Partial<IterationInfo> & Partial<LoopInfo>)
type Node = NodePlain | NodeComplex
/**
* Parses a DSL string into an array of node objects.
* @param dsl - The input DSL string.
* @returns An array of parsed nodes.
*/
function parseDSL(dsl: string): NodeData[] {
return convertToNodeData(parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr)))
}
/**
* Splits a top-level flow string by "->", respecting nested structures.
* @param dsl - The DSL string to split.
* @returns An array of top-level segments.
*/
function parseTopLevelFlow(dsl: string): string[] {
const segments: string[] = []
let buffer = ''
let nested = 0
for (let i = 0; i < dsl.length; i++) {
const char = dsl[i]
if (char === '(') nested++
if (char === ')') nested--
if (char === '-' && dsl[i + 1] === '>' && nested === 0) {
segments.push(buffer.trim())
buffer = ''
i++ // Skip the ">" character
}
else {
buffer += char
}
}
if (buffer.trim())
segments.push(buffer.trim())
return segments
}
/**
* Parses a single node string.
* If the node is complex (e.g., has parentheses), it extracts the node type, node ID, and parameters.
* @param nodeStr - The node string to parse.
* @param parentIterationId - The ID of the parent iteration node (if applicable).
* @param parentLoopId - The ID of the parent loop node (if applicable).
* @returns A parsed node object.
*/
function parseNode(nodeStr: string, parentIterationId?: string, parentLoopId?: string): Node {
// Check if the node is a complex node
if (nodeStr.startsWith('(') && nodeStr.endsWith(')')) {
const innerContent = nodeStr.slice(1, -1).trim() // Remove outer parentheses
let nested = 0
let buffer = ''
const parts: string[] = []
// Split the inner content by commas, respecting nested parentheses
for (let i = 0; i < innerContent.length; i++) {
const char = innerContent[i]
if (char === '(') nested++
if (char === ')') nested--
if (char === ',' && nested === 0) {
parts.push(buffer.trim())
buffer = ''
}
else {
buffer += char
}
}
parts.push(buffer.trim())
// Extract nodeType, nodeId, and params
const [nodeType, nodeId, ...paramsRaw] = parts
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId, nodeType === 'loop' ? nodeId.trim() : parentLoopId)
const complexNode = {
nodeType: nodeType.trim(),
nodeId: nodeId.trim(),
params,
}
if (parentIterationId) {
(complexNode as any).iterationId = parentIterationId;
(complexNode as any).iterationIndex = 0 // Fixed as 0
}
if (parentLoopId) {
(complexNode as any).loopId = parentLoopId;
(complexNode as any).loopIndex = 0 // Fixed as 0
}
return complexNode
}
// If it's not a complex node, treat it as a plain node
const plainNode: NodePlain = { nodeType: 'plain', nodeId: nodeStr.trim() }
if (parentIterationId) {
plainNode.iterationId = parentIterationId
plainNode.iterationIndex = 0 // Fixed as 0
}
if (parentLoopId) {
plainNode.loopId = parentLoopId
plainNode.loopIndex = 0 // Fixed as 0
}
return plainNode
}
/**
* Parses parameters of a complex node.
* Supports nested flows and complex sub-nodes.
* Adds iteration-specific metadata recursively.
* @param paramParts - The parameters string split by commas.
* @param parentIterationId - The ID of the parent iteration node (if applicable).
* @param parentLoopId - The ID of the parent loop node (if applicable).
* @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
*/
function parseParams(paramParts: string[], parentIteration?: string, parentLoopId?: string): (Node | Node[] | number)[] {
return paramParts.map((part) => {
if (part.includes('->')) {
// Parse as a flow and return an array of nodes
return parseTopLevelFlow(part).map(node => parseNode(node, parentIteration || undefined, parentLoopId || undefined))
}
else if (part.startsWith('(')) {
// Parse as a nested complex node
return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
}
else if (!Number.isNaN(Number(part.trim()))) {
// Parse as a numeric parameter
return Number(part.trim())
}
else {
// Parse as a plain node
return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
}
})
}
type NodeData = {
id: string;
node_id: string;
title: string;
node_type?: string;
execution_metadata: Record<string, any>;
status: string;
}
/**
* Converts a plain node to node data.
*/
function convertPlainNode(node: Node): NodeData[] {
return [
{
id: node.nodeId,
node_id: node.nodeId,
title: node.nodeId,
execution_metadata: {},
status: 'succeeded',
},
]
}
/**
* Converts a retry node to node data.
*/
function convertRetryNode(node: Node): NodeData[] {
const { nodeId, iterationId, iterationIndex, loopId, loopIndex, params } = node as NodeComplex
const retryCount = params ? Number.parseInt(params[0] as unknown as string, 10) : 0
const result: NodeData[] = [
{
id: nodeId,
node_id: nodeId,
title: nodeId,
execution_metadata: {},
status: 'succeeded',
},
]
for (let i = 0; i < retryCount; i++) {
result.push({
id: nodeId,
node_id: nodeId,
title: nodeId,
execution_metadata: iterationId ? {
iteration_id: iterationId,
iteration_index: iterationIndex || 0,
} : loopId ? {
loop_id: loopId,
loop_index: loopIndex || 0,
} : {},
status: 'retry',
})
}
return result
}
/**
* Converts an iteration node to node data.
*/
function convertIterationNode(node: Node): NodeData[] {
const { nodeId, params } = node as NodeComplex
const result: NodeData[] = [
{
id: nodeId,
node_id: nodeId,
title: nodeId,
node_type: 'iteration',
status: 'succeeded',
execution_metadata: {},
},
]
params?.forEach((param: any) => {
if (Array.isArray(param)) {
param.forEach((childNode: Node) => {
const childData = convertToNodeData([childNode])
childData.forEach((data) => {
data.execution_metadata = {
...data.execution_metadata,
iteration_id: nodeId,
iteration_index: 0,
}
})
result.push(...childData)
})
}
})
return result
}
/**
* Converts an loop node to node data.
*/
function convertLoopNode(node: Node): NodeData[] {
const { nodeId, params } = node as NodeComplex
const result: NodeData[] = [
{
id: nodeId,
node_id: nodeId,
title: nodeId,
node_type: 'loop',
status: 'succeeded',
execution_metadata: {},
},
]
params?.forEach((param: any) => {
if (Array.isArray(param)) {
param.forEach((childNode: Node) => {
const childData = convertToNodeData([childNode])
childData.forEach((data) => {
data.execution_metadata = {
...data.execution_metadata,
loop_id: nodeId,
loop_index: 0,
}
})
result.push(...childData)
})
}
})
return result
}
/**
* Converts a parallel node to node data.
*/
function convertParallelNode(node: Node, parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
const { nodeId, params } = node as NodeComplex
const result: NodeData[] = [
{
id: nodeId,
node_id: nodeId,
title: nodeId,
execution_metadata: {
parallel_id: nodeId,
},
status: 'succeeded',
},
]
params?.forEach((param) => {
if (Array.isArray(param)) {
const startNodeId = param[0]?.nodeId
param.forEach((childNode: Node) => {
const childData = convertToNodeData([childNode])
childData.forEach((data) => {
data.execution_metadata = {
...data.execution_metadata,
parallel_id: nodeId,
parallel_start_node_id: startNodeId,
...(parentParallelId && {
parent_parallel_id: parentParallelId,
parent_parallel_start_node_id: parentStartNodeId,
}),
}
})
result.push(...childData)
})
}
else if (param && typeof param === 'object') {
const startNodeId = param.nodeId
const childData = convertToNodeData([param])
childData.forEach((data) => {
data.execution_metadata = {
...data.execution_metadata,
parallel_id: nodeId,
parallel_start_node_id: startNodeId,
...(parentParallelId && {
parent_parallel_id: parentParallelId,
parent_parallel_start_node_id: parentStartNodeId,
}),
}
})
result.push(...childData)
}
})
return result
}
/**
* Main function to convert nodes to node data.
*/
function convertToNodeData(nodes: Node[], parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
const result: NodeData[] = []
nodes.forEach((node) => {
switch (node.nodeType) {
case 'plain':
result.push(...convertPlainNode(node))
break
case 'retry':
result.push(...convertRetryNode(node))
break
case 'iteration':
result.push(...convertIterationNode(node))
break
case 'loop':
result.push(...convertLoopNode(node))
break
case 'parallel':
result.push(...convertParallelNode(node, parentParallelId, parentStartNodeId))
break
default:
throw new Error(`Unknown nodeType: ${node.nodeType}`)
}
})
return result
}
export default parseDSL

View File

@@ -0,0 +1,98 @@
import type { NodeTracing } from '@/types/workflow'
import { addChildrenToIterationNode } from './iteration'
import { addChildrenToLoopNode } from './loop'
import formatParallelNode from './parallel'
import formatRetryNode from './retry'
import formatAgentNode from './agent'
import { cloneDeep } from 'lodash-es'
import { BlockEnum } from '../../../types'
const formatIterationAndLoopNode = (list: NodeTracing[], t: any) => {
const clonedList = cloneDeep(list)
// Identify all loop and iteration nodes
const loopNodeIds = clonedList
.filter(item => item.node_type === BlockEnum.Loop)
.map(item => item.node_id)
const iterationNodeIds = clonedList
.filter(item => item.node_type === BlockEnum.Iteration)
.map(item => item.node_id)
// Identify all child nodes for both loop and iteration
const loopChildrenNodeIds = clonedList
.filter(item => item.execution_metadata?.loop_id && loopNodeIds.includes(item.execution_metadata.loop_id))
.map(item => item.node_id)
const iterationChildrenNodeIds = clonedList
.filter(item => item.execution_metadata?.iteration_id && iterationNodeIds.includes(item.execution_metadata.iteration_id))
.map(item => item.node_id)
// Filter out child nodes as they will be included in their parent nodes
const result = clonedList
.filter(item => !loopChildrenNodeIds.includes(item.node_id) && !iterationChildrenNodeIds.includes(item.node_id))
.map((item) => {
// Process Loop nodes
if (item.node_type === BlockEnum.Loop) {
const childrenNodes = clonedList.filter(child => child.execution_metadata?.loop_id === item.node_id)
const error = childrenNodes.find(child => child.status === 'failed')
if (error) {
item.status = 'failed'
item.error = error.error
}
const addedChildrenList = addChildrenToLoopNode(item, childrenNodes)
// Handle parallel nodes in loop node
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
addedChildrenList.details = addedChildrenList.details.map((row) => {
return formatParallelNode(row, t)
})
}
return addedChildrenList
}
// Process Iteration nodes
if (item.node_type === BlockEnum.Iteration) {
const childrenNodes = clonedList.filter(child => child.execution_metadata?.iteration_id === item.node_id)
const error = childrenNodes.find(child => child.status === 'failed')
if (error) {
item.status = 'failed'
item.error = error.error
}
const addedChildrenList = addChildrenToIterationNode(item, childrenNodes)
// Handle parallel nodes in iteration node
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
addedChildrenList.details = addedChildrenList.details.map((row) => {
return formatParallelNode(row, t)
})
}
return addedChildrenList
}
return item
})
return result
}
const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
const allItems = cloneDeep([...list]).sort((a, b) => a.index - b.index)
/*
* First handle not change list structure node
* Because Handle struct node will put the node in different
*/
const formattedAgentList = formatAgentNode(allItems)
const formattedRetryList = formatRetryNode(formattedAgentList) // retry one node
// would change the structure of the list. Iteration and parallel can include each other.
const formattedLoopAndIterationList = formatIterationAndLoopNode(formattedRetryList, t)
const formattedParallelList = formatParallelNode(formattedLoopAndIterationList, t)
const result = formattedParallelList
// console.log(allItems)
// console.log(result)
return result
}
export default formatToTracingNodeList

View File

@@ -0,0 +1,23 @@
import format from '.'
import graphToLogStruct from '../graph-to-log-struct'
import { noop } from 'lodash-es'
describe('iteration', () => {
const list = graphToLogStruct('start -> (iteration, iterationNode, plainNode1 -> plainNode2)')
// const [startNode, iterationNode, ...iterations] = list
const result = format(list as any, noop)
test('result should have no nodes in iteration node', () => {
expect((result as any).find((item: any) => !!item.execution_metadata?.iteration_id)).toBeUndefined()
})
// test('iteration should put nodes in details', () => {
// expect(result as any).toEqual([
// startNode,
// {
// ...iterationNode,
// details: [
// [iterations[0], iterations[1]],
// ],
// },
// ])
// })
})

View File

@@ -0,0 +1,56 @@
import { BlockEnum } from '@/app/components/workflow/types'
import type { NodeTracing } from '@/types/workflow'
import formatParallelNode from '../parallel'
export function addChildrenToIterationNode(iterationNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
const details: NodeTracing[][] = []
childrenNodes.forEach((item, index) => {
if (!item.execution_metadata) return
const { iteration_index = 0 } = item.execution_metadata
const runIndex: number = iteration_index !== undefined ? iteration_index : index
if (!details[runIndex])
details[runIndex] = []
details[runIndex].push(item)
})
return {
...iterationNode,
details,
}
}
const format = (list: NodeTracing[], t: any): NodeTracing[] => {
const iterationNodeIds = list
.filter(item => item.node_type === BlockEnum.Iteration)
.map(item => item.node_id)
const iterationChildrenNodeIds = list
.filter(item => item.execution_metadata?.iteration_id && iterationNodeIds.includes(item.execution_metadata.iteration_id))
.map(item => item.node_id)
// move iteration children nodes to iteration node's details field
const result = list
.filter(item => !iterationChildrenNodeIds.includes(item.node_id))
.map((item) => {
if (item.node_type === BlockEnum.Iteration) {
const childrenNodes = list.filter(child => child.execution_metadata?.iteration_id === item.node_id)
const error = childrenNodes.find(child => child.status === 'failed')
if (error) {
item.status = 'failed'
item.error = error.error
}
const addedChildrenList = addChildrenToIterationNode(item, childrenNodes)
// handle parallel node in iteration node
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
addedChildrenList.details = addedChildrenList.details.map((row) => {
return formatParallelNode(row, t)
})
}
return addedChildrenList
}
return item
})
return result
}
export default format

View File

@@ -0,0 +1,23 @@
import format from '.'
import graphToLogStruct from '../graph-to-log-struct'
import { noop } from 'lodash-es'
describe('loop', () => {
const list = graphToLogStruct('start -> (loop, loopNode, plainNode1 -> plainNode2)')
const [startNode, loopNode, ...loops] = list
const result = format(list as any, noop)
test('result should have no nodes in loop node', () => {
expect(result.find(item => !!item.execution_metadata?.loop_id)).toBeUndefined()
})
test('loop should put nodes in details', () => {
expect(result).toEqual([
startNode,
{
...loopNode,
details: [
[loops[0], loops[1]],
],
},
])
})
})

View File

@@ -0,0 +1,56 @@
import { BlockEnum } from '@/app/components/workflow/types'
import type { NodeTracing } from '@/types/workflow'
import formatParallelNode from '../parallel'
export function addChildrenToLoopNode(loopNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
const details: NodeTracing[][] = []
childrenNodes.forEach((item) => {
if (!item.execution_metadata) return
const { parallel_mode_run_id, loop_index = 0 } = item.execution_metadata
const runIndex: number = (parallel_mode_run_id || loop_index) as number
if (!details[runIndex])
details[runIndex] = []
details[runIndex].push(item)
})
return {
...loopNode,
details,
}
}
const format = (list: NodeTracing[], t: any): NodeTracing[] => {
const loopNodeIds = list
.filter(item => item.node_type === BlockEnum.Loop)
.map(item => item.node_id)
const loopChildrenNodeIds = list
.filter(item => item.execution_metadata?.loop_id && loopNodeIds.includes(item.execution_metadata.loop_id))
.map(item => item.node_id)
// move loop children nodes to loop node's details field
const result = list
.filter(item => !loopChildrenNodeIds.includes(item.node_id))
.map((item) => {
if (item.node_type === BlockEnum.Loop) {
const childrenNodes = list.filter(child => child.execution_metadata?.loop_id === item.node_id)
const error = childrenNodes.find(child => child.status === 'failed')
if (error) {
item.status = 'failed'
item.error = error.error
}
const addedChildrenList = addChildrenToLoopNode(item, childrenNodes)
// handle parallel node in loop node
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
addedChildrenList.details = addedChildrenList.details.map((row) => {
return formatParallelNode(row, t)
})
}
return addedChildrenList
}
return item
})
return result
}
export default format

View File

@@ -0,0 +1,175 @@
import { BlockEnum } from '@/app/components/workflow/types'
import type { NodeTracing } from '@/types/workflow'
function printNodeStructure(node: NodeTracing, depth: number) {
const indent = ' '.repeat(depth)
console.log(`${indent}${node.title}`)
if (node.parallelDetail?.children) {
node.parallelDetail.children.forEach((child) => {
printNodeStructure(child, depth + 1)
})
}
}
function addTitle({
list, depth, belongParallelIndexInfo,
}: {
list: NodeTracing[],
depth: number,
belongParallelIndexInfo?: string,
}, t: any) {
let branchIndex = 0
const hasMoreThanOneParallel = list.filter(node => node.parallelDetail?.isParallelStartNode).length > 1
list.forEach((node) => {
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
if (isNotInParallel)
return
const isParallelStartNode = node.parallelDetail?.isParallelStartNode
const parallelIndexLetter = (() => {
if (!isParallelStartNode || !hasMoreThanOneParallel)
return ''
const index = 1 + list.filter(node => node.parallelDetail?.isParallelStartNode).findIndex(item => item.node_id === node.node_id)
return String.fromCharCode(64 + index)
})()
const parallelIndexInfo = `${depth}${parallelIndexLetter}`
if (isParallelStartNode) {
node.parallelDetail!.isParallelStartNode = true
node.parallelDetail!.parallelTitle = `${t('workflow.common.parallel')}-${parallelIndexInfo}`
}
const isBrachStartNode = parallel_start_node_id === node.node_id
if (isBrachStartNode) {
branchIndex++
const branchLetter = String.fromCharCode(64 + branchIndex)
if (!node.parallelDetail) {
node.parallelDetail = {
branchTitle: '',
}
}
node.parallelDetail!.branchTitle = `${t('workflow.common.branch')}-${belongParallelIndexInfo}-${branchLetter}`
}
if (node.parallelDetail?.children && node.parallelDetail.children.length > 0) {
addTitle({
list: node.parallelDetail.children,
depth: depth + 1,
belongParallelIndexInfo: parallelIndexInfo,
}, t)
}
})
}
// list => group by parallel_id(parallel tree).
const format = (list: NodeTracing[], t: any, isPrint?: boolean): NodeTracing[] => {
if (isPrint)
console.log(list)
const result: NodeTracing[] = [...list]
// list to tree by parent_parallel_start_node_id and branch by parallel_start_node_id. Each parallel may has more than one branch.
result.forEach((node) => {
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
const branchStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
const parentParallelBranchStartNodeId = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
if (isNotInParallel)
return
const isParallelStartNode = parallel_start_node_id === node.node_id // in the same parallel has more than one start node
if (isParallelStartNode) {
const selfNode = { ...node, parallelDetail: undefined }
node.parallelDetail = {
isParallelStartNode: true,
children: [selfNode],
}
const isRootLevel = !parent_parallel_id
if (isRootLevel)
return
const parentParallelStartNode = result.find(item => item.node_id === parentParallelBranchStartNodeId)
// append to parent parallel start node and after the same branch
if (parentParallelStartNode) {
if (!parentParallelStartNode?.parallelDetail) {
parentParallelStartNode!.parallelDetail = {
children: [],
}
}
if (parentParallelStartNode!.parallelDetail.children) {
const sameBranchNodesLastIndex = parentParallelStartNode.parallelDetail.children.findLastIndex((node) => {
const currStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
return currStartNodeId === parentParallelBranchStartNodeId
})
if (sameBranchNodesLastIndex !== -1)
parentParallelStartNode!.parallelDetail.children.splice(sameBranchNodesLastIndex + 1, 0, node)
else
parentParallelStartNode!.parallelDetail.children.push(node)
}
}
return
}
// append to parallel start node and after the same branch
const parallelStartNode = result.find(item => parallel_start_node_id === item.node_id)
if (parallelStartNode && parallelStartNode.parallelDetail && parallelStartNode!.parallelDetail!.children) {
const sameBranchNodesLastIndex = parallelStartNode.parallelDetail.children.findLastIndex((node) => {
const currStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
return currStartNodeId === branchStartNodeId
})
if (sameBranchNodesLastIndex !== -1) {
parallelStartNode.parallelDetail.children.splice(sameBranchNodesLastIndex + 1, 0, node)
}
else { // new branch
parallelStartNode.parallelDetail.children.push(node)
}
}
// parallelStartNode!.parallelDetail!.children.push(node)
})
const filteredInParallelSubNodes = result.filter((node) => {
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
if (isNotInParallel)
return true
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
if (parent_parallel_id)
return false
const isParallelStartNode = node.parallelDetail?.isParallelStartNode
if (!isParallelStartNode)
return false
return true
})
// print node structure for debug
if (isPrint) {
filteredInParallelSubNodes.forEach((node) => {
const now = Date.now()
console.log(`----- p: ${now} start -----`)
printNodeStructure(node, 0)
console.log(`----- p: ${now} end -----`)
})
}
addTitle({
list: filteredInParallelSubNodes,
depth: 1,
}, t)
return filteredInParallelSubNodes
}
export default format

View File

@@ -0,0 +1,21 @@
import format from '.'
import graphToLogStruct from '../graph-to-log-struct'
describe('retry', () => {
// retry nodeId:1 3 times.
const steps = graphToLogStruct('start -> (retry, retryNode, 3)')
const [startNode, retryNode, ...retryDetail] = steps
const result = format(steps as any)
test('should have no retry status nodes', () => {
expect(result.find(item => item.status === 'retry')).toBeUndefined()
})
test('should put retry nodes in retryDetail', () => {
expect(result).toEqual([
startNode,
{
...retryNode,
retryDetail,
},
])
})
})

View File

@@ -0,0 +1,43 @@
import type { NodeTracing } from '@/types/workflow'
const format = (list: NodeTracing[]): NodeTracing[] => {
const retryNodes = list.filter((item) => {
return item.status === 'retry'
})
const retryNodeIds = retryNodes.map(item => item.node_id)
// move retry nodes to retryDetail
const result = list.filter((item) => {
return item.status !== 'retry'
}).map((item) => {
const { execution_metadata } = item
const isInIteration = !!execution_metadata?.iteration_id
const isInLoop = !!execution_metadata?.loop_id
const nodeId = item.node_id
const isRetryBelongNode = retryNodeIds.includes(nodeId)
if (isRetryBelongNode) {
return {
...item,
retryDetail: retryNodes.filter((node) => {
if (!isInIteration && !isInLoop)
return node.node_id === nodeId
// retry node in iteration
if (isInIteration)
return node.node_id === nodeId && node.execution_metadata?.iteration_index === execution_metadata?.iteration_index
// retry node in loop
if (isInLoop)
return node.node_id === nodeId && node.execution_metadata?.loop_index === execution_metadata?.loop_index
return false
}),
}
}
return item
})
return result
}
export default format