Introduce Plugins (#13836)

Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Signed-off-by: -LAN- <laipz8200@outlook.com>
Signed-off-by: xhe <xw897002528@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: takatost <takatost@gmail.com>
Co-authored-by: kurokobo <kuro664@gmail.com>
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: AkaraChen <akarachen@outlook.com>
Co-authored-by: Yi <yxiaoisme@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com>
Co-authored-by: AkaraChen <85140972+AkaraChen@users.noreply.github.com>
Co-authored-by: NFish <douxc512@gmail.com>
Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: Novice <857526207@qq.com>
Co-authored-by: Hiroki Nagai <82458324+nagaihiroki-git@users.noreply.github.com>
Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com>
Co-authored-by: eux <euxuuu@gmail.com>
Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com>
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
Co-authored-by: lotsik <lotsik@mail.ru>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com>
Co-authored-by: CN-P5 <heibai2006@gmail.com>
Co-authored-by: CN-P5 <heibai2006@qq.com>
Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com>
Co-authored-by: yihong <zouzou0208@gmail.com>
Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Boris Feld <lothiraldan@gmail.com>
Co-authored-by: mbo <himabo@gmail.com>
Co-authored-by: mabo <mabo@aeyes.ai>
Co-authored-by: Warren Chen <warren.chen830@gmail.com>
Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com>
Co-authored-by: jiandanfeng <chenjh3@wangsu.com>
Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com>
Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com>
Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com>
Co-authored-by: Xu Song <xusong.vip@gmail.com>
Co-authored-by: rayshaw001 <396301947@163.com>
Co-authored-by: Ding Jiatong <dingjiatong@gmail.com>
Co-authored-by: Bowen Liang <liangbowen@gf.com.cn>
Co-authored-by: JasonVV <jasonwangiii@outlook.com>
Co-authored-by: le0zh <newlight@qq.com>
Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com>
Co-authored-by: k-zaku <zaku99@outlook.jp>
Co-authored-by: luckylhb90 <luckylhb90@gmail.com>
Co-authored-by: hobo.l <hobo.l@binance.com>
Co-authored-by: jiangbo721 <365065261@qq.com>
Co-authored-by: 刘江波 <jiangbo721@163.com>
Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com>
Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: sino <sino2322@gmail.com>
Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com>
Co-authored-by: lowell <lowell.hu@zkteco.in>
Co-authored-by: Boris Polonsky <BorisPolonsky@users.noreply.github.com>
Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com>
Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com>
Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com>
Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com>
Co-authored-by: Jason <ggbbddjm@gmail.com>
Co-authored-by: Xin Zhang <sjhpzx@gmail.com>
Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com>
Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com>
Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com>
Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com>
Co-authored-by: Yingchun Lai <laiyingchun@apache.org>
Co-authored-by: Hash Brown <hi@xzd.me>
Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com>
Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com>
Co-authored-by: aplio <ryo.091219@gmail.com>
Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com>
Co-authored-by: Nam Vu <zuzoovn@gmail.com>
Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com>
Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com>
Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com>
Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com>
Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp>
Co-authored-by: HQidea <HQidea@users.noreply.github.com>
Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com>
Co-authored-by: xhe <xw897002528@gmail.com>
Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com>
Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com>
Co-authored-by: engchina <12236799+engchina@users.noreply.github.com>
Co-authored-by: engchina <atjapan2015@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 呆萌闷油瓶 <253605712@qq.com>
Co-authored-by: Kemal <kemalmeler@outlook.com>
Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com>
Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com>
Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com>
Co-authored-by: steven <sunzwj@digitalchina.com>
Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com>
Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com>
Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com>
Co-authored-by: 胡春东 <gycm520@gmail.com>
Co-authored-by: Junjie.M <118170653@qq.com>
Co-authored-by: MuYu <mr.muzea@gmail.com>
Co-authored-by: Naoki Takashima <39912547+takatea@users.noreply.github.com>
Co-authored-by: Summer-Gu <37869445+gubinjie@users.noreply.github.com>
Co-authored-by: Fei He <droxer.he@gmail.com>
Co-authored-by: ybalbert001 <120714773+ybalbert001@users.noreply.github.com>
Co-authored-by: Yuanbo Li <ybalbert@amazon.com>
Co-authored-by: douxc <7553076+douxc@users.noreply.github.com>
Co-authored-by: liuzhenghua <1090179900@qq.com>
Co-authored-by: Wu Jiayang <62842862+Wu-Jiayang@users.noreply.github.com>
Co-authored-by: Your Name <you@example.com>
Co-authored-by: kimjion <45935338+kimjion@users.noreply.github.com>
Co-authored-by: AugNSo <song.tiankai@icloud.com>
Co-authored-by: llinvokerl <38915183+llinvokerl@users.noreply.github.com>
Co-authored-by: liusurong.lsr <liusurong.lsr@alibaba-inc.com>
Co-authored-by: Vasu Negi <vasu-negi@users.noreply.github.com>
Co-authored-by: Hundredwz <1808096180@qq.com>
Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
This commit is contained in:
Yeuoly
2025-02-17 17:05:13 +08:00
committed by GitHub
parent 222df44d21
commit 403e2d58b9
3272 changed files with 66339 additions and 281594 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='bg-background-default border-[0.5px] border-components-panel-border rounded-[10px]'>
<div
className={cn(
'flex items-center pl-1.5 pt-2 pr-3 pb-2 cursor-pointer',
expanded && 'pb-1',
)}
onClick={() => setExpanded(!expanded)}
>
{
expanded
? <RiArrowRightSLine className='shrink-0 w-4 h-4 rotate-90 text-text-quaternary' />
: <RiArrowRightSLine className='shrink-0 w-4 h-4 text-text-quaternary' />
}
<BlockIcon
className='shrink-0 mr-1.5'
type={toolIcon ? BlockEnum.Tool : BlockEnum.Agent}
toolIcon={toolIcon}
/>
<div
className='grow system-sm-semibold-uppercase text-text-secondary truncate'
title={label}
>
{label}
</div>
{
metadata?.elapsed_time && (
<div className='shrink-0 mr-2 system-xs-regular 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='flex items-center justify-between mb-1 w-full'
variant='tertiary'
onClick={() => onShowAgentOrToolLog(item)}
>
<div className='flex items-center'>
<RiListView className='mr-1 w-4 h-4 text-components-button-tertiary-text shrink-0' />
{`${children.length} Action Logs`}
</div>
<div className='flex'>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
</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: { id: string; label: string }[]
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='w-6 h-6'
variant='ghost-accent'
>
<RiMoreLine className='w-4 h-4' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<div className='p-1 w-[136px] bg-components-panel-bg-blur border-[0.5px] border-components-panel-border rounded-xl shadow-lg'>
{
options.map(option => (
<div
key={option.id}
className='flex items-center px-2 h-8 rounded-lg system-md-regular text-text-secondary hover:bg-state-base-hover cursor-pointer'
onClick={() => {
onShowAgentOrToolLog(option as AgentLogItemWithChildren)
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 items-center p-1 pr-3 h-8 bg-components-panel-bg'>
<Button
className='shrink-0 px-[5px]'
size='small'
variant='ghost-accent'
onClick={() => {
onShowAgentOrToolLog()
}}
>
<RiArrowLeftLine className='mr-1 w-3.5 h-3.5' />
AGENT
</Button>
<div className='shrink-0 mx-0.5 system-xs-regular 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='flex items-center px-[5px] system-xs-medium-uppercase text-text-tertiary'>
{t('workflow.nodes.agent.strategy.label')}
</div>
)
}
{
!!mid.length && (
<>
<div className='shrink-0 mx-0.5 system-xs-regular text-divider-deep'>/</div>
<AgentLogNavMore
options={mid}
onShowAgentOrToolLog={onShowAgentOrToolLog}
/>
</>
)
}
{
!!end && agentOrToolLogItemStackLength > 1 && (
<>
<div className='shrink-0 mx-0.5 system-xs-regular text-divider-deep'>/</div>
<div className='flex items-center px-[5px] system-xs-medium-uppercase 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='bg-components-button-tertiary-bg rounded-[10px] cursor-pointer'
onClick={() => {
onShowAgentOrToolLog({ id: nodeInfo.id, children: agentLog || [] } as AgentLogItemWithChildren)
}}
>
<div className='flex items-center px-3 pt-2 system-2xs-medium-uppercase text-text-tertiary'>
{t('workflow.nodes.agent.strategy.label')}
</div>
<div className='flex items-center pl-3 pt-1 pr-2 pb-1.5'>
{
agentStrategy && (
<div className='grow system-xs-medium text-text-secondary'>
{agentStrategy}
</div>
)
}
<div
className='shrink-0 flex items-center px-[1px] system-xs-regular-uppercase text-text-tertiary cursor-pointer'
>
{t('runLog.detail')}
<RiArrowRightLine className='ml-0.5 w-3.5 h-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.id]
return (
<div className='bg-background-section overflow-y-auto'>
<AgentLogNav
agentOrToolLogItemStack={agentOrToolLogItemStack}
onShowAgentOrToolLog={onShowAgentOrToolLog}
/>
{
<div className='p-2 space-y-1'>
{
list.map(item => (
<AgentLogItem
key={item.id}
item={item}
onShowAgentOrToolLog={onShowAgentOrToolLog}
/>
))
}
</div>
}
{
top.hasCircle && (
<div className='flex items-center mt-1 rounded-xl px-3 pr-2 border border-components-panel-border bg-components-panel-bg-blur shadow-md'>
<div
className='absolute inset-0 opacity-[0.4] rounded-xl'
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 w-4 h-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,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,88 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { useBoolean } from 'ahooks'
import type {
AgentLogItemWithChildren,
IterationDurationMap,
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 [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 { id, children } = detail
let currentAgentOrToolLogItemStack = agentOrToolLogItemStackRef.current.slice()
const index = currentAgentOrToolLogItemStack.findIndex(logItem => logItem.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 || !!agentOrToolLogItemStack.length,
showRetryDetail,
setShowRetryDetailTrue,
setShowRetryDetailFalse,
retryResultList,
setRetryResultList,
handleShowRetryResultList,
showIteratingDetail,
setShowIteratingDetailTrue,
setShowIteratingDetailFalse,
iterationResultList,
setIterationResultList,
iterationResultDurationMap,
setIterationResultDurationMap,
handleShowIterationResultList,
agentOrToolLogItemStack,
agentOrToolLogListMap,
handleShowAgentOrToolLog,
}
}

View File

@@ -3,21 +3,16 @@ 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 { useBoolean } from 'ahooks'
import { BlockEnum } from '../types'
import OutputPanel from './output-panel'
import ResultPanel from './result-panel'
import TracingPanel from './tracing-panel'
import IterationResultPanel from './iteration-result-panel'
import RetryResultPanel from './retry-result-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 { IterationDurationMap, NodeTracing } from '@/types/workflow'
import type { NodeTracing } from '@/types/workflow'
import type { WorkflowRunDetailResponse } from '@/models/log'
import { useStore as useAppStore } from '@/app/components/app/store'
export type RunProps = {
hideResult?: boolean
activeTab?: 'RESULT' | 'DETAIL' | 'TRACING'
@@ -60,124 +55,12 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
}
}, [notify, getResultCallback])
const formatNodeList = useCallback((list: NodeTracing[]) => {
const allItems = [...list].reverse()
const result: NodeTracing[] = []
const nodeGroupMap = new Map<string, Map<string, NodeTracing[]>>()
const processIterationNode = (item: NodeTracing) => {
result.push({
...item,
details: [],
})
}
const updateParallelModeGroup = (runId: string, item: NodeTracing, iterationNode: NodeTracing) => {
if (!nodeGroupMap.has(iterationNode.node_id))
nodeGroupMap.set(iterationNode.node_id, new Map())
const groupMap = nodeGroupMap.get(iterationNode.node_id)!
if (!groupMap.has(runId)) {
groupMap.set(runId, [item])
}
else {
if (item.status === 'retry') {
const retryNode = groupMap.get(runId)!.find(node => node.node_id === item.node_id)
if (retryNode) {
if (retryNode?.retryDetail)
retryNode.retryDetail.push(item)
else
retryNode.retryDetail = [item]
}
}
else {
groupMap.get(runId)!.push(item)
}
}
if (item.status === 'failed') {
iterationNode.status = 'failed'
iterationNode.error = item.error
}
iterationNode.details = Array.from(groupMap.values())
}
const updateSequentialModeGroup = (index: number, item: NodeTracing, iterationNode: NodeTracing) => {
const { details } = iterationNode
if (details) {
if (!details[index]) {
details[index] = [item]
}
else {
if (item.status === 'retry') {
const retryNode = details[index].find(node => node.node_id === item.node_id)
if (retryNode) {
if (retryNode?.retryDetail)
retryNode.retryDetail.push(item)
else
retryNode.retryDetail = [item]
}
}
else {
details[index].push(item)
}
}
}
if (item.status === 'failed') {
iterationNode.status = 'failed'
iterationNode.error = item.error
}
}
const processNonIterationNode = (item: NodeTracing) => {
const { execution_metadata } = item
if (!execution_metadata?.iteration_id) {
if (item.status === 'retry') {
const retryNode = result.find(node => node.node_id === item.node_id)
if (retryNode) {
if (retryNode?.retryDetail)
retryNode.retryDetail.push(item)
else
retryNode.retryDetail = [item]
}
return
}
result.push(item)
return
}
const iterationNode = result.find(node => node.node_id === execution_metadata.iteration_id)
if (!iterationNode || !Array.isArray(iterationNode.details))
return
const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata
if (parallel_mode_run_id)
updateParallelModeGroup(parallel_mode_run_id, item, iterationNode)
else
updateSequentialModeGroup(iteration_index, item, iterationNode)
}
allItems.forEach((item) => {
item.node_type === BlockEnum.Iteration
? processIterationNode(item)
: processNonIterationNode(item)
})
return result
}, [])
const getTracingList = useCallback(async (appID: string, runID: string) => {
try {
const { data: nodeList } = await fetchTracingList({
url: `/apps/${appID}/workflow-runs/${runID}/node-executions`,
})
setList(formatNodeList(nodeList))
setList(nodeList)
}
catch (err) {
notify({
@@ -219,42 +102,6 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
adjustResultHeight()
}, [loading])
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({})
const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([])
const [isShowIterationDetail, {
setTrue: doShowIterationDetail,
setFalse: doHideIterationDetail,
}] = useBoolean(false)
const [isShowRetryDetail, {
setTrue: doShowRetryDetail,
setFalse: doHideRetryDetail,
}] = useBoolean(false)
const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => {
setIterationRunResult(detail)
doShowIterationDetail()
setIterDurationMap(iterDurationMap)
}, [doShowIterationDetail, setIterationRunResult, setIterDurationMap])
const handleShowRetryDetail = useCallback((detail: NodeTracing[]) => {
setRetryRunResult(detail)
doShowRetryDetail()
}, [doShowRetryDetail, setRetryRunResult])
if (isShowIterationDetail) {
return (
<div className='grow relative flex flex-col'>
<IterationResultPanel
list={iterationRunResult}
onHide={doHideIterationDetail}
onBack={doHideIterationDetail}
iterDurationMap={iterDurationMap}
/>
</div>
)
}
return (
<div className='grow relative flex flex-col'>
{/* tab */}
@@ -284,7 +131,7 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
>{t('runLog.tracing')}</div>
</div>
{/* panel detail */}
<div ref={ref} className={cn('grow bg-components-panel-bg h-0 overflow-y-auto rounded-b-2xl', currentTab !== 'DETAIL' && '!bg-background-section-burn')}>
<div ref={ref} className={cn('relative grow bg-components-panel-bg h-0 overflow-y-auto rounded-b-2xl')}>
{loading && (
<div className='flex h-full items-center justify-center bg-components-panel-bg'>
<Loading />
@@ -311,22 +158,12 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
exceptionCounts={runDetail.exceptions_count}
/>
)}
{!loading && currentTab === 'TRACING' && !isShowRetryDetail && (
{!loading && currentTab === 'TRACING' && (
<TracingPanel
className='bg-background-section-burn'
list={list}
onShowIterationDetail={handleShowIterationDetail}
onShowRetryDetail={handleShowRetryDetail}
/>
)}
{
!loading && currentTab === 'TRACING' && isShowRetryDetail && (
<RetryResultPanel
list={retryRunResult}
onBack={doHideRetryDetail}
/>
)
}
</div>
</div>
)

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,57 @@
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 { Iteration } from '@/app/components/base/icons/src/vender/workflow'
type IterationLogTriggerProps = {
nodeInfo: NodeTracing
onShowIterationResultList: (iterationResultList: NodeTracing[][], iterationResultDurationMap: IterationDurationMap) => void
}
const IterationLogTrigger = ({
nodeInfo,
onShowIterationResultList,
}: IterationLogTriggerProps) => {
const { t } = useTranslation()
const getErrorCount = (details: NodeTracing[][] | undefined) => {
if (!details || details.length === 0)
return 0
return details.reduce((acc, iteration) => {
if (iteration.some(item => item.status === 'failed'))
acc++
return acc
}, 0)
}
const getCount = (iteration_curr_length: number | undefined, iteration_length: number) => {
if ((iteration_curr_length && iteration_curr_length < iteration_length) || !iteration_length)
return iteration_curr_length
return iteration_length
}
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowIterationResultList(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {})
}
return (
<Button
className='flex items-center w-full self-stretch gap-2 px-3 py-2 bg-components-button-tertiary-bg-hover hover:bg-components-button-tertiary-bg-hover rounded-lg cursor-pointer border-none'
onClick={handleOnShowIterationDetail}
>
<Iteration className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
<div className='flex-1 text-left system-sm-medium text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}{getErrorCount(nodeInfo.details) > 0 && (
<>
{t('workflow.nodes.iteration.comma')}
{t('workflow.nodes.iteration.error', { count: getErrorCount(nodeInfo.details) })}
</>
)}</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
</Button>
)
}
export default IterationLogTrigger

View File

@@ -3,15 +3,13 @@ import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowLeftLine,
RiArrowRightSLine,
RiCloseLine,
RiErrorWarningLine,
RiLoader2Line,
} from '@remixicon/react'
import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
import { NodeRunningStatus } from '../types'
import TracingPanel from './tracing-panel'
import RetryResultPanel from './retry-result-panel'
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'
@@ -19,17 +17,13 @@ const i18nPrefix = 'workflow.singleRun'
type Props = {
list: NodeTracing[][]
onHide: () => void
onBack: () => void
noWrap?: boolean
iterDurationMap?: IterationDurationMap
}
const IterationResultPanel: FC<Props> = ({
list,
onHide,
onBack,
noWrap,
iterDurationMap,
}) => {
const { t } = useTranslation()
@@ -75,29 +69,22 @@ const IterationResultPanel: FC<Props> = ({
</>
)
}
const [retryRunResult, setRetryRunResult] = useState<Record<string, NodeTracing[]> | undefined>()
const handleRetryDetail = (v: number, detail?: NodeTracing[]) => {
setRetryRunResult({ ...retryRunResult, [v]: detail })
}
const main = (
<>
<div className={cn(!noWrap && 'shrink-0 ', 'px-4 pt-3')}>
<div className='shrink-0 flex justify-between items-center h-8'>
<div className='system-xl-semibold text-text-primary truncate'>
{t(`${i18nPrefix}.testRunIteration`)}
</div>
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}>
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</div>
</div>
<div className='flex items-center py-2 space-x-1 text-text-accent-secondary cursor-pointer' onClick={onBack}>
<ArrowNarrowLeft className='w-4 h-4' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
</div>
return (
<div className='bg-components-panel-bg'>
<div
className='flex items-center px-4 h-8 text-text-accent-secondary cursor-pointer border-b-[0.5px] border-b-divider-regular'
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onBack()
}}
>
<RiArrowLeftLine className='mr-1 w-4 h-4' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
</div>
{/* List */}
<div className={cn(!noWrap ? 'flex-grow overflow-auto' : 'max-h-full', 'p-2 bg-components-panel-bg')}>
<div className='p-2 bg-components-panel-bg'>
{list.map((iteration, index) => (
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl bg-background-section-burn border-none')}>
<div
@@ -109,66 +96,31 @@ const IterationResultPanel: FC<Props> = ({
onClick={() => toggleIteration(index)}
>
<div className={cn('flex items-center gap-2 flex-grow')}>
<div className='flex items-center justify-center w-4 h-4 rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500 flex-shrink-0'>
<div className='flex items-center justify-center w-4 h-4 rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500 shrink-0'>
<Iteration className='w-3 h-3 text-text-primary-on-surface' />
</div>
<span className='system-sm-semibold-uppercase text-text-primary flex-grow'>
<span className='system-sm-semibold-uppercase text-text-primary grow'>
{t(`${i18nPrefix}.iteration`)} {index + 1}
</span>
{iterationStatusShow(index, iteration, iterDurationMap)}
</div>
</div>
{expandedIterations[index] && <div
className="flex-grow h-px bg-divider-subtle"
className="grow h-px bg-divider-subtle"
></div>}
{
!retryRunResult?.[index] && (
<div className={cn(
'overflow-hidden transition-all duration-200',
expandedIterations[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
)}>
<TracingPanel
list={iteration}
className='bg-background-section-burn'
onShowRetryDetail={v => handleRetryDetail(index, v)}
/>
</div>
)
}
{
retryRunResult?.[index] && (
<RetryResultPanel
list={retryRunResult[index]}
onBack={() => handleRetryDetail(index, undefined)}
/>
)
}
<div className={cn(
'overflow-hidden transition-all duration-200',
expandedIterations[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
)}>
<TracingPanel
list={iteration}
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='h-full rounded-2xl bg-components-panel-bg flex flex-col'>
{main}
</div>
</div >
</div>
)
}
export default React.memo(IterationResultPanel)

View File

@@ -8,18 +8,21 @@ import {
RiCheckboxCircleFill,
RiErrorWarningLine,
RiLoader2Line,
RiRestartFill,
} from '@remixicon/react'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import Split from '../nodes/_base/components/split'
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
import { RetryLogTrigger } from './retry-log'
import { IterationLogTrigger } from './iteration-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 Button from '@/app/components/base/button'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
import type {
AgentLogItemWithChildren,
IterationDurationMap,
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'
@@ -31,9 +34,8 @@ type Props = {
hideProcessDetail?: boolean
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
notShowIterationNav?: boolean
justShowIterationNavArrow?: boolean
justShowRetryNavArrow?: boolean
}
const NodePanel: FC<Props> = ({
@@ -44,8 +46,8 @@ const NodePanel: FC<Props> = ({
hideProcessDetail,
onShowIterationDetail,
onShowRetryDetail,
onShowAgentOrToolLog,
notShowIterationNav,
justShowIterationNavArrow,
}) => {
const [collapseState, doSetCollapseState] = useState<boolean>(true)
const setCollapseState = useCallback((state: boolean) => {
@@ -59,7 +61,7 @@ const NodePanel: FC<Props> = ({
if (time < 1)
return `${(time * 1000).toFixed(3)} ms`
if (time > 60)
return `${parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s`
return `${Number.parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s`
return `${time.toFixed(3)} s`
}
@@ -67,43 +69,20 @@ const NodePanel: FC<Props> = ({
if (tokens < 1000)
return tokens
if (tokens >= 1000 && tokens < 1000000)
return `${parseFloat((tokens / 1000).toFixed(3))}K`
return `${Number.parseFloat((tokens / 1000).toFixed(3))}K`
if (tokens >= 1000000)
return `${parseFloat((tokens / 1000000).toFixed(3))}M`
return `${Number.parseFloat((tokens / 1000000).toFixed(3))}M`
}
const getCount = (iteration_curr_length: number | undefined, iteration_length: number) => {
if ((iteration_curr_length && iteration_curr_length < iteration_length) || !iteration_length)
return iteration_curr_length
return iteration_length
}
const getErrorCount = (details: NodeTracing[][] | undefined) => {
if (!details || details.length === 0)
return 0
return details.reduce((acc, iteration) => {
if (iteration.some(item => item.status === 'failed'))
acc++
return acc
}, 0)
}
useEffect(() => {
setCollapseState(!nodeInfo.expand)
}, [nodeInfo.expand, setCollapseState])
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration
const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowIterationDetail?.(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {})
}
const handleOnShowRetryDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowRetryDetail?.(nodeInfo.retryDetail || [])
}
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!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={cn('px-2 py-1', className)}>
<div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadow-xs hover:shadow-md'>
@@ -153,46 +132,26 @@ const NodePanel: FC<Props> = ({
{!collapseState && !hideProcessDetail && (
<div className='px-1 pb-1'>
{/* The nav to the iteration detail */}
{isIterationNode && !notShowIterationNav && (
<div className='mt-2 mb-1 !px-2'>
<Button
className='flex items-center w-full self-stretch gap-2 px-3 py-2 bg-components-button-tertiary-bg-hover hover:bg-components-button-tertiary-bg-hover rounded-lg cursor-pointer border-none'
onClick={handleOnShowIterationDetail}
>
<Iteration className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
<div className='flex-1 text-left system-sm-medium text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}{getErrorCount(nodeInfo.details) > 0 && (
<>
{t('workflow.nodes.iteration.comma')}
{t('workflow.nodes.iteration.error', { count: getErrorCount(nodeInfo.details) })}
</>
)}</div>
{justShowIterationNavArrow
? (
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
)
: (
<div className='flex items-center space-x-1 text-[#155EEF]'>
<div className='text-[13px] font-normal '>{t('workflow.common.viewDetailInTracingPanel')}</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
</div>
)}
</Button>
<Split className='mt-2' />
</div>
{isIterationNode && !notShowIterationNav && onShowIterationDetail && (
<IterationLogTrigger
nodeInfo={nodeInfo}
onShowIterationResultList={onShowIterationDetail}
/>
)}
{isRetryNode && (
<Button
className='flex items-center justify-between mb-1 w-full'
variant='tertiary'
onClick={handleOnShowRetryDetail}
>
<div className='flex items-center'>
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
{t('workflow.nodes.common.retry.retries', { num: nodeInfo.retryDetail?.length })}
</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
</Button>
{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'>

View File

@@ -54,7 +54,7 @@ const OutputPanel: FC<OutputPanelProps> = ({
return getProcessedFilesFromResponse(fileList)
}, [outputs])
return (
<div className='py-2'>
<div className='p-2'>
{isRunning && (
<div className='pt-4 pl-[26px]'>
<LoadingAnim type='text' />

View File

@@ -1,19 +1,23 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiRestartFill,
} from '@remixicon/react'
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 { NodeTracing } from '@/types/workflow'
import Button from '@/app/components/base/button'
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 { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
import { AgentLogTrigger } from '@/app/components/workflow/run/agent-log'
type ResultPanelProps = {
nodeInfo?: NodeTracing
inputs?: string
process_data?: string
outputs?: string
@@ -28,11 +32,13 @@ type ResultPanelProps = {
showSteps?: boolean
exceptionCounts?: number
execution_metadata?: any
retry_events?: NodeTracing[]
onShowRetryDetail?: (retries: NodeTracing[]) => void
handleShowIterationResultList?: (detail: NodeTracing[][], iterDurationMap: any) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
}
const ResultPanel: FC<ResultPanelProps> = ({
nodeInfo,
inputs,
process_data,
outputs,
@@ -46,10 +52,15 @@ const ResultPanel: FC<ResultPanelProps> = ({
showSteps,
exceptionCounts,
execution_metadata,
retry_events,
handleShowIterationResultList,
onShowRetryDetail,
handleShowAgentOrToolLog,
}) => {
const { t } = useTranslation()
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!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'>
@@ -62,23 +73,32 @@ const ResultPanel: FC<ResultPanelProps> = ({
exceptionCounts={exceptionCounts}
/>
</div>
{
retry_events?.length && onShowRetryDetail && (
<div className='px-4'>
<Button
className='flex items-center justify-between w-full'
variant='tertiary'
onClick={() => onShowRetryDetail(retry_events)}
>
<div className='flex items-center'>
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
{t('workflow.nodes.common.retry.retries', { num: retry_events?.length })}
</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
</Button>
</div>
)
}
<div className='px-4'>
{
isIterationNode && handleShowIterationResultList && (
<IterationLogTrigger
nodeInfo={nodeInfo}
onShowIterationResultList={handleShowIterationResultList}
/>
)
}
{
isRetryNode && onShowRetryDetail && (
<RetryLogTrigger
nodeInfo={nodeInfo}
onShowRetryResultList={onShowRetryDetail}
/>
)
}
{
(isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
<AgentLogTrigger
nodeInfo={nodeInfo}
onShowAgentOrToolLog={handleShowAgentOrToolLog}
/>
)
}
</div>
<div className='px-4 py-2 flex flex-col gap-2'>
<CodeEditor
readOnly

View File

@@ -7,7 +7,7 @@ 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 = {
interface ResultTextProps {
isRunning?: boolean
outputs?: any
error?: string

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='flex items-center justify-between mb-1 w-full'
variant='tertiary'
onClick={handleShowRetryResultList}
>
<div className='flex items-center'>
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text shrink-0' />
{t('workflow.nodes.common.retry.retries', { num: retryDetail?.length })}
</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
</Button>
)
}
export default RetryLogTrigger

View File

@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
import {
RiArrowLeftLine,
} from '@remixicon/react'
import TracingPanel from './tracing-panel'
import TracingPanel from '../tracing-panel'
import type { NodeTracing } from '@/types/workflow'
type Props = {

View File

@@ -0,0 +1,73 @@
import { RetryResultPanel } from './retry-log'
import { IterationResultPanel } from './iteration-log'
import { AgentResultPanel } from './agent-log'
import type {
AgentLogItemWithChildren,
IterationDurationMap,
NodeTracing,
} from '@/types/workflow'
export type SpecialResultPanelProps = {
showRetryDetail?: boolean
setShowRetryDetailFalse?: () => void
retryResultList?: NodeTracing[]
showIteratingDetail?: boolean
setShowIteratingDetailFalse?: () => void
iterationResultList?: NodeTracing[][]
iterationResultDurationMap?: IterationDurationMap
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
}
const SpecialResultPanel = ({
showRetryDetail,
setShowRetryDetailFalse,
retryResultList,
showIteratingDetail,
setShowIteratingDetailFalse,
iterationResultList,
iterationResultDurationMap,
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}
/>
)
}
{
!!agentOrToolLogItemStack?.length && agentOrToolLogListMap && handleShowAgentOrToolLog && (
<AgentResultPanel
agentOrToolLogItemStack={agentOrToolLogItemStack}
agentOrToolLogListMap={agentOrToolLogListMap}
onShowAgentOrToolLog={handleShowAgentOrToolLog}
/>
)
}
</div>
)
}
export default SpecialResultPanel

View File

@@ -1,5 +1,7 @@
'use client'
import type { FC } from 'react'
import { useAppContext } from '@/context/app-context'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'
type Props = {
@@ -11,19 +13,36 @@ const StatusContainer: FC<Props> = ({
status,
children,
}) => {
const { theme } = useAppContext()
return (
<div
className={cn(
'relative px-3 py-2.5 rounded-lg border system-xs-regular break-all',
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)] 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)] text-text-success',
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)] 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)] text-text-success',
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)] 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)] text-text-warning',
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)] 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)] text-text-destructive',
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)] 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)] text-text-destructive',
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)] 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)] text-util-colors-blue-light-blue-light-600',
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='absolute top-0 left-0 w-[65%] h-[50px] bg-[url(~@/app/components/workflow/run/assets/highlight.svg)]'></div>
<div className={cn(
'absolute top-0 left-0 w-[65%] h-[50px] 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>
)

View File

@@ -12,162 +12,27 @@ import {
RiMenu4Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useLogs } from './hooks'
import NodePanel from './node'
import {
BlockEnum,
} from '@/app/components/workflow/types'
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
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[]
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
className?: string
hideNodeInfo?: boolean
hideNodeProcessDetail?: boolean
}
type TracingNodeProps = {
id: string
uniqueId: string
isParallel: boolean
data: NodeTracing | null
children: TracingNodeProps[]
parallelTitle?: string
branchTitle?: string
hideNodeInfo?: boolean
hideNodeProcessDetail?: boolean
}
function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): TracingNodeProps[] {
const rootNodes: TracingNodeProps[] = []
const parallelStacks: { [key: string]: TracingNodeProps } = {}
const levelCounts: { [key: string]: number } = {}
const parallelChildCounts: { [key: string]: Set<string> } = {}
let uniqueIdCounter = 0
const getUniqueId = () => {
uniqueIdCounter++
return `unique-${uniqueIdCounter}`
}
const getParallelTitle = (parentId: string | null): string => {
const levelKey = parentId || 'root'
if (!levelCounts[levelKey])
levelCounts[levelKey] = 0
levelCounts[levelKey]++
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
return `${t('workflow.common.parallel')}-${levelNumber}${letter}`
}
const getBranchTitle = (parentId: string | null, branchNum: number): string => {
const levelKey = parentId || 'root'
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
const branchLetter = String.fromCharCode(64 + branchNum)
return `${t('workflow.common.branch')}-${levelNumber}${letter}-${branchLetter}`
}
// Count parallel children (for figuring out if we need to use letters)
for (const node of nodes) {
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
if (parallel_id) {
const parentKey = parent_parallel_id || 'root'
if (!parallelChildCounts[parentKey])
parallelChildCounts[parentKey] = new Set()
parallelChildCounts[parentKey].add(parallel_id)
}
}
for (const node of nodes) {
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
if (!parallel_id || node.node_type === BlockEnum.End) {
rootNodes.push({
id: node.id,
uniqueId: getUniqueId(),
isParallel: false,
data: node,
children: [],
})
}
else {
if (!parallelStacks[parallel_id]) {
const newParallelGroup: TracingNodeProps = {
id: parallel_id,
uniqueId: getUniqueId(),
isParallel: true,
data: null,
children: [],
parallelTitle: '',
}
parallelStacks[parallel_id] = newParallelGroup
if (parent_parallel_id && parallelStacks[parent_parallel_id]) {
const sameBranchIndex = parallelStacks[parent_parallel_id].children.findLastIndex(c =>
c.data?.execution_metadata?.parallel_start_node_id === parent_parallel_start_node_id || c.data?.parallel_start_node_id === parent_parallel_start_node_id,
)
parallelStacks[parent_parallel_id].children.splice(sameBranchIndex + 1, 0, newParallelGroup)
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
}
else {
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
rootNodes.push(newParallelGroup)
}
}
const branchTitle = parallel_start_node_id === node.node_id ? getBranchTitle(parent_parallel_id, parallelStacks[parallel_id].children.length + 1) : ''
if (branchTitle) {
parallelStacks[parallel_id].children.push({
id: node.id,
uniqueId: getUniqueId(),
isParallel: false,
data: node,
children: [],
branchTitle,
})
}
else {
let sameBranchIndex = parallelStacks[parallel_id].children.findLastIndex(c =>
c.data?.execution_metadata?.parallel_start_node_id === parallel_start_node_id || c.data?.parallel_start_node_id === parallel_start_node_id,
)
if (parallelStacks[parallel_id].children[sameBranchIndex + 1]?.isParallel)
sameBranchIndex++
parallelStacks[parallel_id].children.splice(sameBranchIndex + 1, 0, {
id: node.id,
uniqueId: getUniqueId(),
isParallel: false,
data: node,
children: [],
branchTitle,
})
}
}
}
return rootNodes
}
const TracingPanel: FC<TracingPanelProps> = ({
list,
onShowIterationDetail,
onShowRetryDetail,
className,
hideNodeInfo = false,
hideNodeProcessDetail = false,
}) => {
const { t } = useTranslation()
const treeNodes = buildLogTree(list, t)
const treeNodes = formatNodeList(list, t)
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
@@ -203,13 +68,34 @@ const TracingPanel: FC<TracingPanelProps> = ({
}
}, [])
const renderNode = (node: TracingNodeProps) => {
if (node.isParallel) {
const {
showSpecialResultPanel,
showRetryDetail,
setShowRetryDetailFalse,
retryResultList,
handleShowRetryResultList,
showIteratingDetail,
setShowIteratingDetailFalse,
iterationResultList,
iterationResultDurationMap,
handleShowIterationResultList,
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.uniqueId}
key={node.id}
className="ml-4 mb-2 relative"
data-parallel-id={node.id}
onMouseEnter={() => handleParallelMouseEnter(node.id)}
@@ -226,10 +112,10 @@ const TracingPanel: FC<TracingPanelProps> = ({
{isHovered ? <RiArrowDownSLine className="w-3 h-3" /> : <RiMenu4Line className="w-3 h-3 text-text-tertiary" />}
</button>
<div className="system-xs-semibold-uppercase text-text-secondary flex items-center">
<span>{node.parallelTitle}</span>
<span>{parallelDetail.parallelTitle}</span>
</div>
<div
className="mx-2 flex-grow h-px bg-divider-subtle"
className="mx-2 grow h-px bg-divider-subtle"
style={{ background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08), rgba(255, 255, 255, 0)' }}
></div>
</div>
@@ -238,7 +124,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
'absolute top-0 bottom-0 left-[5px] w-[2px]',
isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
)}></div>
{node.children.map(renderNode)}
{parallelDetail.children!.map(renderNode)}
</div>
</div>
)
@@ -246,16 +132,15 @@ const TracingPanel: FC<TracingPanelProps> = ({
else {
const isHovered = hoveredParallel === node.id
return (
<div key={node.uniqueId}>
<div key={node.id}>
<div className={cn('pl-4 -mb-1.5 system-2xs-medium-uppercase', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}>
{node.branchTitle}
{node?.parallelDetail?.branchTitle}
</div>
<NodePanel
nodeInfo={node.data!}
onShowIterationDetail={onShowIterationDetail}
onShowRetryDetail={onShowRetryDetail}
justShowIterationNavArrow={true}
justShowRetryNavArrow={true}
nodeInfo={node!}
onShowIterationDetail={handleShowIterationResultList}
onShowRetryDetail={handleShowRetryResultList}
onShowAgentOrToolLog={handleShowAgentOrToolLog}
hideInfo={hideNodeInfo}
hideProcessDetail={hideNodeProcessDetail}
/>
@@ -264,8 +149,33 @@ const TracingPanel: FC<TracingPanelProps> = ({
}
}
if (showSpecialResultPanel) {
return (
<SpecialResultPanel
showRetryDetail={showRetryDetail}
setShowRetryDetailFalse={setShowRetryDetailFalse}
retryResultList={retryResultList}
showIteratingDetail={showIteratingDetail}
setShowIteratingDetailFalse={setShowIteratingDetailFalse}
iterationResultList={iterationResultList}
iterationResultDurationMap={iterationResultDurationMap}
agentOrToolLogItemStack={agentOrToolLogItemStack}
agentOrToolLogListMap={agentOrToolLogListMap}
handleShowAgentOrToolLog={handleShowAgentOrToolLog}
/>
)
}
return (
<div className={cn(className || 'bg-components-panel-bg', 'py-2')}>
<div
className={cn(className || 'bg-components-panel-bg', 'py-2')}
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}}
>
{treeNodes.map(renderNode)}
</div>
)

View File

@@ -0,0 +1,182 @@
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',
},
],
},
],
}],
}
})()
export const CircleNestCircle = (() => {
})()

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,99 @@
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 => c.id === removeId)
if (hasCircle) {
node.hasCircle = true
node.children = node.children.filter(c => c.id !== 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) => {
if (!addedItemIds.includes(item.id)) {
result.push(item)
addedItemIds.push(item.id)
}
})
return result
}
const removeCircleLogItem = (log: AgentLogItemWithChildren) => {
const newLog = cloneDeep(log)
newLog.children = removeRepeatedSiblings(newLog.children)
let { id, children } = newLog
if (!children || children.length === 0)
return log
// check one step circle
const hasOneStepCircle = !!children.find(c => c.id === id)
if (hasOneStepCircle) {
newLog.hasCircle = true
newLog.children = newLog.children.filter(c => c.id !== 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 []
const tree: AgentLogItemWithChildren[] = []
logs.forEach((log) => {
const hasParent = !!log.parent_id
if (hasParent) {
const parent = logs.find(item => item.id === log.parent_id) as AgentLogItemWithChildren
if (parent) {
if (!parent.children)
parent.children = []
parent.children.push(log as AgentLogItemWithChildren)
}
}
else {
tree.push(log as AgentLogItemWithChildren)
}
})
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,128 @@
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 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: 'plainNode3',
},
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,304 @@
type IterationInfo = { iterationId: string; iterationIndex: number }
type NodePlain = { nodeType: 'plain'; nodeId: string; } & Partial<IterationInfo>
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & Partial<IterationInfo>) | Node[] | number)[] } & Partial<IterationInfo>
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).
* @returns A parsed node object.
*/
function parseNode(nodeStr: string, parentIterationId?: 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)
const complexNode = {
nodeType: nodeType.trim(),
nodeId: nodeId.trim(),
params,
}
if (parentIterationId) {
(complexNode as any).iterationId = parentIterationId;
(complexNode as any).iterationIndex = 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
}
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 iterationId - The ID of the iteration node, if applicable.
* @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
*/
function parseParams(paramParts: string[], iterationId?: 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, iterationId))
}
else if (part.startsWith('(')) {
// Parse as a nested complex node
return parseNode(part, iterationId)
}
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, iterationId)
}
})
}
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, 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,
} : {},
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 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 '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,10 @@
import formatToTracingNodeList from '.'
import { simpleIterationData } from './iteration/data'
import { simpleRetryData } from './retry/data'
describe('format api data to tracing panel data', () => {
test('integration', () => {
expect(formatToTracingNodeList(simpleIterationData.in.reverse() as any)).toEqual(simpleIterationData.expect)
expect(formatToTracingNodeList(simpleRetryData.in.reverse() as any)).toEqual(simpleRetryData.expect)
})
})

View File

@@ -0,0 +1,27 @@
import type { NodeTracing } from '@/types/workflow'
import formatIterationNode from './iteration'
import formatParallelNode from './parallel'
import formatRetryNode from './retry'
import formatAgentNode from './agent'
import { cloneDeep } from 'lodash-es'
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 formattedIterationList = formatIterationNode(formattedRetryList, t)
const formattedParallelList = formatParallelNode(formattedIterationList, t)
const result = formattedParallelList
// console.log(allItems)
// console.log(result)
return result
}
export default formatToTracingNodeList

View File

@@ -0,0 +1,22 @@
import format from '.'
import graphToLogStruct from '../graph-to-log-struct'
describe('iteration', () => {
const list = graphToLogStruct('start -> (iteration, iterationNode, plainNode1 -> plainNode2)')
const [startNode, iterationNode, ...iterations] = list
const result = format(list as any, () => { })
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,55 @@
import { BlockEnum } from '@/app/components/workflow/types'
import type { NodeTracing } from '@/types/workflow'
import formatParallelNode from '../parallel'
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 || 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,39 @@
import { cloneDeep } from 'lodash-es'
import format from '.'
import graphToLogStruct from '../graph-to-log-struct'
describe('parallel', () => {
const list = graphToLogStruct('(parallel, parallelNode, nodeA, nodeB -> nodeC)')
const [parallelNode, ...parallelDetail] = list
const parallelI18n = 'PARALLEL'
// format will change the list...
const result = format(cloneDeep(list) as any, () => parallelI18n)
test('parallel should put nodes in details', () => {
expect(result as any).toEqual([
{
...parallelNode,
parallelDetail: {
isParallelStartNode: true,
parallelTitle: `${parallelI18n}-1`,
children: [
parallelNode,
{
...parallelDetail[0],
parallelDetail: {
branchTitle: `${parallelI18n}-1-A`,
},
},
{
...parallelDetail[1],
parallelDetail: {
branchTitle: `${parallelI18n}-1-B`,
},
},
parallelDetail[2],
],
},
},
])
})
})

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]
const parallelFirstNodeMap: Record<string, string> = {}
// 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 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 = !parallelFirstNodeMap[parallel_id]
if (isParallelStartNode) {
const selfNode = { ...node, parallelDetail: undefined }
node.parallelDetail = {
isParallelStartNode: true,
children: [selfNode],
}
parallelFirstNodeMap[parallel_id] = node.node_id
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 => item.node_id === parallelFirstNodeMap[parallel_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 as any).status === 'retry')).toBeUndefined()
})
test('should put retry nodes in retryDetail', () => {
expect(result).toEqual([
startNode,
{
...retryNode,
retryDetail,
},
])
})
})

View File

@@ -0,0 +1,35 @@
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 nodeId = item.node_id
const isRetryBelongNode = retryNodeIds.includes(nodeId)
if (isRetryBelongNode) {
return {
...item,
retryDetail: retryNodes.filter((node) => {
if (!isInIteration)
return node.node_id === nodeId
// retry node in iteration
return node.node_id === nodeId && node.execution_metadata?.iteration_index === execution_metadata?.iteration_index
}),
}
}
return item
})
return result
}
export default format