dify
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiListView,
|
||||
} from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { AgentLogItemWithChildren } from '@/types/workflow'
|
||||
import NodeStatusIcon from '@/app/components/workflow/nodes/_base/components/node-status-icon'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
|
||||
type AgentLogItemProps = {
|
||||
item: AgentLogItemWithChildren
|
||||
onShowAgentOrToolLog: (detail: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentLogItem = ({
|
||||
item,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentLogItemProps) => {
|
||||
const {
|
||||
label,
|
||||
status,
|
||||
children,
|
||||
data,
|
||||
metadata,
|
||||
} = item
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const toolIcon = useMemo(() => {
|
||||
const icon = metadata?.icon
|
||||
|
||||
if (icon) {
|
||||
if (icon.includes('http'))
|
||||
return icon
|
||||
|
||||
return getIconUrl(icon)
|
||||
}
|
||||
|
||||
return ''
|
||||
}, [getIconUrl, metadata?.icon])
|
||||
|
||||
const mergeStatus = useMemo(() => {
|
||||
if (status === 'start')
|
||||
return 'running'
|
||||
|
||||
return status
|
||||
}, [status])
|
||||
|
||||
return (
|
||||
<div className='rounded-[10px] border-[0.5px] border-components-panel-border bg-background-default'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center pb-2 pl-1.5 pr-3 pt-2',
|
||||
expanded && 'pb-1',
|
||||
)}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{
|
||||
expanded
|
||||
? <RiArrowRightSLine className='h-4 w-4 shrink-0 rotate-90 text-text-quaternary' />
|
||||
: <RiArrowRightSLine className='h-4 w-4 shrink-0 text-text-quaternary' />
|
||||
}
|
||||
<BlockIcon
|
||||
className='mr-1.5 shrink-0'
|
||||
type={toolIcon ? BlockEnum.Tool : BlockEnum.Agent}
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
<div
|
||||
className='system-sm-semibold-uppercase grow truncate text-text-secondary'
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{
|
||||
metadata?.elapsed_time && (
|
||||
<div className='system-xs-regular mr-2 shrink-0 text-text-tertiary'>{metadata?.elapsed_time?.toFixed(3)}s</div>
|
||||
)
|
||||
}
|
||||
<NodeStatusIcon status={mergeStatus} />
|
||||
</div>
|
||||
{
|
||||
expanded && (
|
||||
<div className='p-1 pt-0'>
|
||||
{
|
||||
!!children?.length && (
|
||||
<Button
|
||||
className='mb-1 flex w-full items-center justify-between'
|
||||
variant='tertiary'
|
||||
onClick={() => onShowAgentOrToolLog(item)}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<RiListView className='mr-1 h-4 w-4 shrink-0 text-components-button-tertiary-text' />
|
||||
{`${children.length} Action Logs`}
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
data && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{'data'.toLocaleUpperCase()}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={data}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogItem
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState } from 'react'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { AgentLogItemWithChildren } from '@/types/workflow'
|
||||
|
||||
type AgentLogNavMoreProps = {
|
||||
options: AgentLogItemWithChildren[]
|
||||
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentLogNavMore = ({
|
||||
options,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentLogNavMoreProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 2,
|
||||
crossAxis: -54,
|
||||
}}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger>
|
||||
<Button
|
||||
className='h-6 w-6'
|
||||
variant='ghost-accent'
|
||||
>
|
||||
<RiMoreLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<div className='w-[136px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.message_id}
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onShowAgentOrToolLog(option)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogNavMore
|
||||
@@ -0,0 +1,78 @@
|
||||
import { RiArrowLeftLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AgentLogNavMore from './agent-log-nav-more'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { AgentLogItemWithChildren } from '@/types/workflow'
|
||||
|
||||
type AgentLogNavProps = {
|
||||
agentOrToolLogItemStack: AgentLogItemWithChildren[]
|
||||
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentLogNav = ({
|
||||
agentOrToolLogItemStack,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentLogNavProps) => {
|
||||
const { t } = useTranslation()
|
||||
const agentOrToolLogItemStackLength = agentOrToolLogItemStack.length
|
||||
const first = agentOrToolLogItemStack[0]
|
||||
const mid = agentOrToolLogItemStack.slice(1, -1)
|
||||
const end = agentOrToolLogItemStack.at(-1)
|
||||
|
||||
return (
|
||||
<div className='flex h-8 items-center bg-components-panel-bg p-1 pr-3'>
|
||||
<Button
|
||||
className='shrink-0 px-[5px]'
|
||||
size='small'
|
||||
variant='ghost-accent'
|
||||
onClick={() => {
|
||||
onShowAgentOrToolLog()
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine className='mr-1 h-3.5 w-3.5' />
|
||||
AGENT
|
||||
</Button>
|
||||
<div className='system-xs-regular mx-0.5 shrink-0 text-divider-deep'>/</div>
|
||||
{
|
||||
agentOrToolLogItemStackLength > 1
|
||||
? (
|
||||
<Button
|
||||
className='shrink-0 px-[5px]'
|
||||
size='small'
|
||||
variant='ghost-accent'
|
||||
onClick={() => onShowAgentOrToolLog(first)}
|
||||
>
|
||||
{t('workflow.nodes.agent.strategy.label')}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<div className='system-xs-medium-uppercase flex items-center px-[5px] text-text-tertiary'>
|
||||
{t('workflow.nodes.agent.strategy.label')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!mid.length && (
|
||||
<>
|
||||
<div className='system-xs-regular mx-0.5 shrink-0 text-divider-deep'>/</div>
|
||||
<AgentLogNavMore
|
||||
options={mid}
|
||||
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!end && agentOrToolLogItemStackLength > 1 && (
|
||||
<>
|
||||
<div className='system-xs-regular mx-0.5 shrink-0 text-divider-deep'>/</div>
|
||||
<div className='system-xs-medium-uppercase flex items-center px-[5px] text-text-tertiary'>
|
||||
{end.label}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogNav
|
||||
@@ -0,0 +1,49 @@
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
type AgentLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentLogTrigger = ({
|
||||
nodeInfo,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { agentLog, execution_metadata } = nodeInfo
|
||||
const agentStrategy = execution_metadata?.tool_info?.agent_strategy
|
||||
|
||||
return (
|
||||
<div
|
||||
className='cursor-pointer rounded-[10px] bg-components-button-tertiary-bg'
|
||||
onClick={() => {
|
||||
onShowAgentOrToolLog({ message_id: nodeInfo.id, children: agentLog || [] } as AgentLogItemWithChildren)
|
||||
}}
|
||||
>
|
||||
<div className='system-2xs-medium-uppercase flex items-center px-3 pt-2 text-text-tertiary'>
|
||||
{t('workflow.nodes.agent.strategy.label')}
|
||||
</div>
|
||||
<div className='flex items-center pb-1.5 pl-3 pr-2 pt-1'>
|
||||
{
|
||||
agentStrategy && (
|
||||
<div className='system-xs-medium grow text-text-secondary'>
|
||||
{agentStrategy}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className='system-xs-regular-uppercase flex shrink-0 cursor-pointer items-center px-[1px] text-text-tertiary'
|
||||
>
|
||||
{t('runLog.detail')}
|
||||
<RiArrowRightLine className='ml-0.5 h-3.5 w-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogTrigger
|
||||
@@ -0,0 +1,60 @@
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AgentLogItem from './agent-log-item'
|
||||
import AgentLogNav from './agent-log-nav'
|
||||
import type { AgentLogItemWithChildren } from '@/types/workflow'
|
||||
|
||||
type AgentResultPanelProps = {
|
||||
agentOrToolLogItemStack: AgentLogItemWithChildren[]
|
||||
agentOrToolLogListMap: Record<string, AgentLogItemWithChildren[]>
|
||||
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentResultPanel = ({
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentResultPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const top = agentOrToolLogItemStack[agentOrToolLogItemStack.length - 1]
|
||||
const list = agentOrToolLogListMap[top.message_id]
|
||||
|
||||
return (
|
||||
<div className='overflow-y-auto bg-background-section'>
|
||||
<AgentLogNav
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||
/>
|
||||
{
|
||||
<div className='space-y-1 p-2'>
|
||||
{
|
||||
list.map(item => (
|
||||
<AgentLogItem
|
||||
key={item.message_id}
|
||||
item={item}
|
||||
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
top.hasCircle && (
|
||||
<div className='mt-1 flex items-center rounded-xl border border-components-panel-border bg-components-panel-bg-blur px-3 pr-2 shadow-md'>
|
||||
<div
|
||||
className='absolute inset-0 rounded-xl opacity-[0.4]'
|
||||
style={{
|
||||
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
|
||||
}}
|
||||
></div>
|
||||
<RiAlertFill className='mr-1.5 h-4 w-4 text-text-warning-secondary' />
|
||||
<div className='system-xs-medium text-text-primary'>
|
||||
{t('runLog.circularInvocationTip')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentResultPanel
|
||||
2
dify/web/app/components/workflow/run/agent-log/index.tsx
Normal file
2
dify/web/app/components/workflow/run/agent-log/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AgentLogTrigger } from './agent-log-trigger'
|
||||
export { default as AgentResultPanel } from './agent-result-panel'
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 0H368M0 2H368M0 4H368M0 6H368M0 8H368M0 10H368M0 12H368M0 14H368M0 16H368M0 18H368M0 20H368M0 22H368M0 24H368M0 26H368M0 28H368M0 30H368M0 32H368M0 34H368M0 36H368M0 38H368M0 40H368M0 42H368M0 44H368M0 46H368M0 48H368M0 50H368M0 52H368M0 54H368M0 56H368M0 58H368M0 60H368M0 62H368M0 64H368M0 66H368M0 68H368M0 70H368M0 72H368M0 74H368M0 76H368M0 78H368M0 80H368M0 82H368M0 84H368M0 86H368M0 88H368M0 90H368M0 92H368M0 94H368M0 96H368M0 98H368M0 100H368M0 102H368M0 104H368M0 106H368M0 108H368M0 110H368M0 112H368M0 114H368M0 116H368M0 118H368M0 120H368M0 122H368M0 124H368M0 126H368M0 128H368M0 130H368M0 132H368M0 134H368M0 136H368M0 138H368M0 140H368M0 142H368M0 144H368M0 146H368M0 148H368M0 150H368M0 152H368M0 154H368M0 156H368M0 158H368M0 160H368M0 162H368M0 164H368M0 166H368M0 168H368M0 170H368M0 172H368M0 174H368M0 176H368M0 178H368M0 180H368M0 182H368M0 184H368M0 186H368M0 188H368M0 190H368M0 192H368M0 194H368M0 196H368M0 198H368M0 200H368M0 202H368M0 204H368M0 206H368M0 208H368M0 210H368M0 212H368M0 214H368M0 216H368M0 218H368M0 220H368M0 222H368M0 224H368M0 226H368" stroke="#F04438" stroke-opacity="0.3" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 0.5H368M0 2.5H368M0 4.5H368M0 6.5H368M0 8.5H368M0 10.5H368M0 12.5H368M0 14.5H368M0 16.5H368M0 18.5H368M0 20.5H368M0 22.5H368M0 24.5H368M0 26.5H368M0 28.5H368M0 30.5H368M0 32.5H368M0 34.5H368M0 36.5H368M0 38.5H368M0 40.5H368M0 42.5H368M0 44.5H368M0 46.5H368M0 48.5H368M0 50.5H368M0 52.5H368M0 54.5H368M0 56.5H368M0 58.5H368M0 60.5H368M0 62.5H368M0 64.5H368M0 66.5H368M0 68.5H368M0 70.5H368M0 72.5H368M0 74.5H368M0 76.5H368M0 78.5H368M0 80.5H368M0 82.5H368M0 84.5H368M0 86.5H368M0 88.5H368M0 90.5H368M0 92.5H368M0 94.5H368M0 96.5H368M0 98.5H368M0 100.5H368M0 102.5H368M0 104.5H368M0 106.5H368M0 108.5H368M0 110.5H368M0 112.5H368M0 114.5H368M0 116.5H368M0 118.5H368M0 120.5H368M0 122.5H368M0 124.5H368M0 126.5H368M0 128.5H368M0 130.5H368M0 132.5H368M0 134.5H368M0 136.5H368M0 138.5H368M0 140.5H368M0 142.5H368M0 144.5H368M0 146.5H368M0 148.5H368M0 150.5H368M0 152.5H368M0 154.5H368M0 156.5H368M0 158.5H368M0 160.5H368M0 162.5H368M0 164.5H368M0 166.5H368M0 168.5H368M0 170.5H368M0 172.5H368M0 174.5H368M0 176.5H368M0 178.5H368M0 180.5H368M0 182.5H368M0 184.5H368M0 186.5H368M0 188.5H368M0 190.5H368M0 192.5H368M0 194.5H368M0 196.5H368M0 198.5H368M0 200.5H368M0 202.5H368M0 204.5H368M0 206.5H368M0 208.5H368M0 210.5H368M0 212.5H368M0 214.5H368M0 216.5H368M0 218.5H368M0 220.5H368M0 222.5H368M0 224.5H368M0 226.5H368" stroke="#0BA5EC" stroke-opacity="0.3" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 0H368M0 2H368M0 4H368M0 6H368M0 8H368M0 10H368M0 12H368M0 14H368M0 16H368M0 18H368M0 20H368M0 22H368M0 24H368M0 26H368M0 28H368M0 30H368M0 32H368M0 34H368M0 36H368M0 38H368M0 40H368M0 42H368M0 44H368M0 46H368M0 48H368M0 50H368M0 52H368M0 54H368M0 56H368M0 58H368M0 60H368M0 62H368M0 64H368M0 66H368M0 68H368M0 70H368M0 72H368M0 74H368M0 76H368M0 78H368M0 80H368M0 82H368M0 84H368M0 86H368M0 88H368M0 90H368M0 92H368M0 94H368M0 96H368M0 98H368M0 100H368M0 102H368M0 104H368M0 106H368M0 108H368M0 110H368M0 112H368M0 114H368M0 116H368M0 118H368M0 120H368M0 122H368M0 124H368M0 126H368M0 128H368M0 130H368M0 132H368M0 134H368M0 136H368M0 138H368M0 140H368M0 142H368M0 144H368M0 146H368M0 148H368M0 150H368M0 152H368M0 154H368M0 156H368M0 158H368M0 160H368M0 162H368M0 164H368M0 166H368M0 168H368M0 170H368M0 172H368M0 174H368M0 176H368M0 178H368M0 180H368M0 182H368M0 184H368M0 186H368M0 188H368M0 190H368M0 192H368M0 194H368M0 196H368M0 198H368M0 200H368M0 202H368M0 204H368M0 206H368M0 208H368M0 210H368M0 212H368M0 214H368M0 216H368M0 218H368M0 220H368M0 222H368M0 224H368M0 226H368" stroke="#17B26A" stroke-opacity="0.3" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 0.5H368M0 2.5H368M0 4.5H368M0 6.5H368M0 8.5H368M0 10.5H368M0 12.5H368M0 14.5H368M0 16.5H368M0 18.5H368M0 20.5H368M0 22.5H368M0 24.5H368M0 26.5H368M0 28.5H368M0 30.5H368M0 32.5H368M0 34.5H368M0 36.5H368M0 38.5H368M0 40.5H368M0 42.5H368M0 44.5H368M0 46.5H368M0 48.5H368M0 50.5H368M0 52.5H368M0 54.5H368M0 56.5H368M0 58.5H368M0 60.5H368M0 62.5H368M0 64.5H368M0 66.5H368M0 68.5H368M0 70.5H368M0 72.5H368M0 74.5H368M0 76.5H368M0 78.5H368M0 80.5H368M0 82.5H368M0 84.5H368M0 86.5H368M0 88.5H368M0 90.5H368M0 92.5H368M0 94.5H368M0 96.5H368M0 98.5H368M0 100.5H368M0 102.5H368M0 104.5H368M0 106.5H368M0 108.5H368M0 110.5H368M0 112.5H368M0 114.5H368M0 116.5H368M0 118.5H368M0 120.5H368M0 122.5H368M0 124.5H368M0 126.5H368M0 128.5H368M0 130.5H368M0 132.5H368M0 134.5H368M0 136.5H368M0 138.5H368M0 140.5H368M0 142.5H368M0 144.5H368M0 146.5H368M0 148.5H368M0 150.5H368M0 152.5H368M0 154.5H368M0 156.5H368M0 158.5H368M0 160.5H368M0 162.5H368M0 164.5H368M0 166.5H368M0 168.5H368M0 170.5H368M0 172.5H368M0 174.5H368M0 176.5H368M0 178.5H368M0 180.5H368M0 182.5H368M0 184.5H368M0 186.5H368M0 188.5H368M0 190.5H368M0 192.5H368M0 194.5H368M0 196.5H368M0 198.5H368M0 200.5H368M0 202.5H368M0 204.5H368M0 206.5H368M0 208.5H368M0 210.5H368M0 212.5H368M0 214.5H368M0 216.5H368M0 218.5H368M0 220.5H368M0 222.5H368M0 224.5H368M0 226.5H368" stroke="#F79009" stroke-opacity="0.3" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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 |
@@ -0,0 +1,9 @@
|
||||
<svg width="237" height="50" viewBox="0 0 237 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 8C0 3.58172 3.58172 0 8 0H237L215.033 50H8C3.58172 50 0 46.4183 0 42V8Z" fill="url(#paint0_linear_3552_29170)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3552_29170" x1="-4.89158e-08" y1="4.62963" x2="168.013" y2="23.1752" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.12"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 516 B |
115
dify/web/app/components/workflow/run/hooks.ts
Normal file
115
dify/web/app/components/workflow/run/hooks.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
export const useLogs = () => {
|
||||
const [showRetryDetail, {
|
||||
setTrue: setShowRetryDetailTrue,
|
||||
setFalse: setShowRetryDetailFalse,
|
||||
}] = useBoolean(false)
|
||||
const [retryResultList, setRetryResultList] = useState<NodeTracing[]>([])
|
||||
const handleShowRetryResultList = useCallback((detail: NodeTracing[]) => {
|
||||
setShowRetryDetailTrue()
|
||||
setRetryResultList(detail)
|
||||
}, [setShowRetryDetailTrue, setRetryResultList])
|
||||
|
||||
const [showIteratingDetail, {
|
||||
setTrue: setShowIteratingDetailTrue,
|
||||
setFalse: setShowIteratingDetailFalse,
|
||||
}] = useBoolean(false)
|
||||
const [iterationResultList, setIterationResultList] = useState<NodeTracing[][]>([])
|
||||
const [iterationResultDurationMap, setIterationResultDurationMap] = useState<IterationDurationMap>({})
|
||||
const handleShowIterationResultList = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => {
|
||||
setShowIteratingDetailTrue()
|
||||
setIterationResultList(detail)
|
||||
setIterationResultDurationMap(iterDurationMap)
|
||||
}, [setShowIteratingDetailTrue, setIterationResultList, setIterationResultDurationMap])
|
||||
|
||||
const [showLoopingDetail, {
|
||||
setTrue: setShowLoopingDetailTrue,
|
||||
setFalse: setShowLoopingDetailFalse,
|
||||
}] = useBoolean(false)
|
||||
const [loopResultList, setLoopResultList] = useState<NodeTracing[][]>([])
|
||||
const [loopResultDurationMap, setLoopResultDurationMap] = useState<LoopDurationMap>({})
|
||||
const [loopResultVariableMap, setLoopResultVariableMap] = useState<Record<string, any>>({})
|
||||
const handleShowLoopResultList = useCallback((detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => {
|
||||
setShowLoopingDetailTrue()
|
||||
setLoopResultList(detail)
|
||||
setLoopResultDurationMap(loopDurationMap)
|
||||
setLoopResultVariableMap(loopVariableMap)
|
||||
}, [setShowLoopingDetailTrue, setLoopResultList, setLoopResultDurationMap])
|
||||
|
||||
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
|
||||
const agentOrToolLogItemStackRef = useRef(agentOrToolLogItemStack)
|
||||
const [agentOrToolLogListMap, setAgentOrToolLogListMap] = useState<Record<string, AgentLogItemWithChildren[]>>({})
|
||||
const agentOrToolLogListMapRef = useRef(agentOrToolLogListMap)
|
||||
const handleShowAgentOrToolLog = useCallback((detail?: AgentLogItemWithChildren) => {
|
||||
if (!detail) {
|
||||
setAgentOrToolLogItemStack([])
|
||||
agentOrToolLogItemStackRef.current = []
|
||||
return
|
||||
}
|
||||
const { message_id: id, children } = detail
|
||||
let currentAgentOrToolLogItemStack = agentOrToolLogItemStackRef.current.slice()
|
||||
const index = currentAgentOrToolLogItemStack.findIndex(logItem => logItem.message_id === id)
|
||||
|
||||
if (index > -1)
|
||||
currentAgentOrToolLogItemStack = currentAgentOrToolLogItemStack.slice(0, index + 1)
|
||||
else
|
||||
currentAgentOrToolLogItemStack = [...currentAgentOrToolLogItemStack.slice(), detail]
|
||||
|
||||
setAgentOrToolLogItemStack(currentAgentOrToolLogItemStack)
|
||||
agentOrToolLogItemStackRef.current = currentAgentOrToolLogItemStack
|
||||
|
||||
if (children) {
|
||||
setAgentOrToolLogListMap({
|
||||
...agentOrToolLogListMapRef.current,
|
||||
[id]: children,
|
||||
})
|
||||
}
|
||||
}, [setAgentOrToolLogItemStack, setAgentOrToolLogListMap])
|
||||
|
||||
return {
|
||||
showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length,
|
||||
showRetryDetail,
|
||||
setShowRetryDetailTrue,
|
||||
setShowRetryDetailFalse,
|
||||
retryResultList,
|
||||
setRetryResultList,
|
||||
handleShowRetryResultList,
|
||||
|
||||
showIteratingDetail,
|
||||
setShowIteratingDetailTrue,
|
||||
setShowIteratingDetailFalse,
|
||||
iterationResultList,
|
||||
setIterationResultList,
|
||||
iterationResultDurationMap,
|
||||
setIterationResultDurationMap,
|
||||
handleShowIterationResultList,
|
||||
|
||||
showLoopingDetail,
|
||||
setShowLoopingDetailTrue,
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
setLoopResultList,
|
||||
loopResultDurationMap,
|
||||
setLoopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
setLoopResultVariableMap,
|
||||
handleShowLoopResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
}
|
||||
}
|
||||
197
dify/web/app/components/workflow/run/index.tsx
Normal file
197
dify/web/app/components/workflow/run/index.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OutputPanel from './output-panel'
|
||||
import ResultPanel from './result-panel'
|
||||
import StatusPanel from './status'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import TracingPanel from './tracing-panel'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { fetchRunDetail, fetchTracingList } from '@/service/log'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import type { WorkflowRunDetailResponse } from '@/models/log'
|
||||
import { useStore } from '../store'
|
||||
|
||||
export type RunProps = {
|
||||
hideResult?: boolean
|
||||
activeTab?: 'RESULT' | 'DETAIL' | 'TRACING'
|
||||
getResultCallback?: (result: WorkflowRunDetailResponse) => void
|
||||
runDetailUrl: string
|
||||
tracingListUrl: string
|
||||
}
|
||||
|
||||
const RunPanel: FC<RunProps> = ({
|
||||
hideResult,
|
||||
activeTab = 'RESULT',
|
||||
getResultCallback,
|
||||
runDetailUrl,
|
||||
tracingListUrl,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [currentTab, setCurrentTab] = useState<string>(activeTab)
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [runDetail, setRunDetail] = useState<WorkflowRunDetailResponse>()
|
||||
const [list, setList] = useState<NodeTracing[]>([])
|
||||
const isListening = useStore(s => s.isListening)
|
||||
|
||||
const executor = useMemo(() => {
|
||||
if (runDetail?.created_by_role === 'account')
|
||||
return runDetail.created_by_account?.name || ''
|
||||
if (runDetail?.created_by_role === 'end_user')
|
||||
return runDetail.created_by_end_user?.session_id || ''
|
||||
return 'N/A'
|
||||
}, [runDetail])
|
||||
|
||||
const getResult = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchRunDetail(runDetailUrl)
|
||||
setRunDetail(res)
|
||||
if (getResultCallback)
|
||||
getResultCallback(res)
|
||||
}
|
||||
catch (err) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `${err}`,
|
||||
})
|
||||
}
|
||||
}, [notify, getResultCallback, runDetailUrl])
|
||||
|
||||
const getTracingList = useCallback(async () => {
|
||||
try {
|
||||
const { data: nodeList } = await fetchTracingList({
|
||||
url: tracingListUrl,
|
||||
})
|
||||
setList(nodeList)
|
||||
}
|
||||
catch (err) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `${err}`,
|
||||
})
|
||||
}
|
||||
}, [notify, tracingListUrl])
|
||||
|
||||
const getData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
await getResult()
|
||||
await getTracingList()
|
||||
setLoading(false)
|
||||
}, [getResult, getTracingList])
|
||||
|
||||
const switchTab = async (tab: string) => {
|
||||
setCurrentTab(tab)
|
||||
if (tab === 'RESULT') {
|
||||
if (runDetailUrl)
|
||||
await getResult()
|
||||
}
|
||||
if (tracingListUrl)
|
||||
await getTracingList()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isListening)
|
||||
setCurrentTab('DETAIL')
|
||||
}, [isListening])
|
||||
|
||||
useEffect(() => {
|
||||
// fetch data
|
||||
if (runDetailUrl && tracingListUrl)
|
||||
getData()
|
||||
}, [runDetailUrl, tracingListUrl])
|
||||
|
||||
const [height, setHeight] = useState(0)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const adjustResultHeight = () => {
|
||||
if (ref.current)
|
||||
setHeight(ref.current?.clientHeight - 16 - 16 - 2 - 1)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
adjustResultHeight()
|
||||
}, [loading])
|
||||
|
||||
return (
|
||||
<div className='relative flex grow flex-col'>
|
||||
{/* tab */}
|
||||
<div className='flex shrink-0 items-center border-b-[0.5px] border-divider-subtle px-4'>
|
||||
{!hideResult && (
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
|
||||
currentTab === 'RESULT' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
|
||||
)}
|
||||
onClick={() => switchTab('RESULT')}
|
||||
>{t('runLog.result')}</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
|
||||
currentTab === 'DETAIL' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
|
||||
)}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>{t('runLog.detail')}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
|
||||
currentTab === 'TRACING' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
|
||||
)}
|
||||
onClick={() => switchTab('TRACING')}
|
||||
>{t('runLog.tracing')}</div>
|
||||
</div>
|
||||
{/* panel detail */}
|
||||
<div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-components-panel-bg')}>
|
||||
{loading && (
|
||||
<div className='flex h-full items-center justify-center bg-components-panel-bg'>
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
{!loading && currentTab === 'RESULT' && runDetail && (
|
||||
<OutputPanel
|
||||
outputs={runDetail.outputs}
|
||||
error={runDetail.error}
|
||||
height={height}
|
||||
/>
|
||||
)}
|
||||
{!loading && currentTab === 'DETAIL' && runDetail && (
|
||||
<ResultPanel
|
||||
inputs={runDetail.inputs}
|
||||
inputs_truncated={runDetail.inputs_truncated}
|
||||
outputs={runDetail.outputs}
|
||||
outputs_truncated={runDetail.outputs_truncated}
|
||||
outputs_full_content={runDetail.outputs_full_content}
|
||||
status={runDetail.status}
|
||||
error={runDetail.error}
|
||||
elapsed_time={runDetail.elapsed_time}
|
||||
total_tokens={runDetail.total_tokens}
|
||||
created_at={runDetail.created_at}
|
||||
created_by={executor}
|
||||
steps={runDetail.total_steps}
|
||||
exceptionCounts={runDetail.exceptions_count}
|
||||
isListening={isListening}
|
||||
/>
|
||||
)}
|
||||
{!loading && currentTab === 'DETAIL' && !runDetail && isListening && (
|
||||
<StatusPanel
|
||||
status={WorkflowRunningStatus.Running}
|
||||
isListening={true}
|
||||
/>
|
||||
)}
|
||||
{!loading && currentTab === 'TRACING' && (
|
||||
<TracingPanel
|
||||
className='bg-background-section-burn'
|
||||
list={list}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RunPanel
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as IterationLogTrigger } from './iteration-log-trigger'
|
||||
export { default as IterationResultPanel } from './iteration-result-panel'
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type {
|
||||
IterationDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type IterationLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
allExecutions?: NodeTracing[]
|
||||
onShowIterationResultList: (iterationResultList: NodeTracing[][], iterationResultDurationMap: IterationDurationMap) => void
|
||||
}
|
||||
const IterationLogTrigger = ({
|
||||
nodeInfo,
|
||||
allExecutions,
|
||||
onShowIterationResultList,
|
||||
}: IterationLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filterNodesForInstance = (key: string): NodeTracing[] => {
|
||||
if (!allExecutions) return []
|
||||
|
||||
const parallelNodes = allExecutions.filter(exec =>
|
||||
exec.execution_metadata?.parallel_mode_run_id === key,
|
||||
)
|
||||
if (parallelNodes.length > 0)
|
||||
return parallelNodes
|
||||
|
||||
const serialIndex = Number.parseInt(key, 10)
|
||||
if (!isNaN(serialIndex)) {
|
||||
const serialNodes = allExecutions.filter(exec =>
|
||||
exec.execution_metadata?.iteration_id === nodeInfo.node_id
|
||||
&& exec.execution_metadata?.iteration_index === serialIndex,
|
||||
)
|
||||
if (serialNodes.length > 0)
|
||||
return serialNodes
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
|
||||
const iterationNodeMeta = nodeInfo.execution_metadata
|
||||
const iterDurationMap = nodeInfo?.iterDurationMap || iterationNodeMeta?.iteration_duration_map || {}
|
||||
|
||||
let structuredList: NodeTracing[][] = []
|
||||
if (iterationNodeMeta?.iteration_duration_map) {
|
||||
const instanceKeys = Object.keys(iterationNodeMeta.iteration_duration_map)
|
||||
structuredList = instanceKeys
|
||||
.map(key => filterNodesForInstance(key))
|
||||
.filter(branchNodes => branchNodes.length > 0)
|
||||
|
||||
// Also include failed iterations that might not be in duration map
|
||||
if (allExecutions && nodeInfo.details?.length) {
|
||||
const existingIterationIndices = new Set<number>()
|
||||
structuredList.forEach((iteration) => {
|
||||
iteration.forEach((node) => {
|
||||
if (node.execution_metadata?.iteration_index !== undefined)
|
||||
existingIterationIndices.add(node.execution_metadata.iteration_index)
|
||||
})
|
||||
})
|
||||
|
||||
// Find failed iterations that are not in the structured list
|
||||
nodeInfo.details.forEach((iteration, index) => {
|
||||
if (!existingIterationIndices.has(index) && iteration.some(node => node.status === NodeRunningStatus.Failed))
|
||||
structuredList.push(iteration)
|
||||
})
|
||||
|
||||
// Sort by iteration index to maintain order
|
||||
structuredList.sort((a, b) => {
|
||||
const aIndex = a[0]?.execution_metadata?.iteration_index ?? 0
|
||||
const bIndex = b[0]?.execution_metadata?.iteration_index ?? 0
|
||||
return aIndex - bIndex
|
||||
})
|
||||
}
|
||||
}
|
||||
else if (nodeInfo.details?.length) {
|
||||
structuredList = nodeInfo.details
|
||||
}
|
||||
|
||||
onShowIterationResultList(structuredList, iterDurationMap)
|
||||
}
|
||||
|
||||
let displayIterationCount = 0
|
||||
const iterMap = nodeInfo.execution_metadata?.iteration_duration_map
|
||||
if (iterMap)
|
||||
displayIterationCount = Object.keys(iterMap).length
|
||||
else if (nodeInfo.details?.length)
|
||||
displayIterationCount = nodeInfo.details.length
|
||||
else if (nodeInfo.metadata?.iterator_length)
|
||||
displayIterationCount = nodeInfo.metadata.iterator_length
|
||||
|
||||
const getErrorCount = (details: NodeTracing[][] | undefined, iterationNodeMeta?: any) => {
|
||||
if (!details || details.length === 0)
|
||||
return 0
|
||||
|
||||
// Use Set to track failed iteration indices to avoid duplicate counting
|
||||
const failedIterationIndices = new Set<number>()
|
||||
|
||||
// Collect failed iteration indices from details
|
||||
details.forEach((iteration, index) => {
|
||||
if (iteration.some(item => item.status === NodeRunningStatus.Failed)) {
|
||||
// Try to get iteration index from first node, fallback to array index
|
||||
const iterationIndex = iteration[0]?.execution_metadata?.iteration_index ?? index
|
||||
failedIterationIndices.add(iterationIndex)
|
||||
}
|
||||
})
|
||||
|
||||
// If allExecutions exists, check for additional failed iterations
|
||||
if (iterationNodeMeta?.iteration_duration_map && allExecutions) {
|
||||
// Find all failed iteration nodes
|
||||
allExecutions.forEach((exec) => {
|
||||
if (exec.execution_metadata?.iteration_id === nodeInfo.node_id
|
||||
&& exec.status === NodeRunningStatus.Failed
|
||||
&& exec.execution_metadata?.iteration_index !== undefined)
|
||||
failedIterationIndices.add(exec.execution_metadata.iteration_index)
|
||||
})
|
||||
}
|
||||
|
||||
return failedIterationIndices.size
|
||||
}
|
||||
const errorCount = getErrorCount(nodeInfo.details, nodeInfo.execution_metadata)
|
||||
|
||||
return (
|
||||
<Button
|
||||
className='flex w-full cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-components-button-tertiary-bg-hover px-3 py-2 hover:bg-components-button-tertiary-bg-hover'
|
||||
onClick={handleOnShowIterationDetail}
|
||||
>
|
||||
<Iteration className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
|
||||
<div className='system-sm-medium flex-1 text-left text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: displayIterationCount })}{errorCount > 0 && (
|
||||
<>
|
||||
{t('workflow.nodes.iteration.comma')}
|
||||
{t('workflow.nodes.iteration.error', { count: errorCount })}
|
||||
</>
|
||||
)}</div>
|
||||
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default IterationLogTrigger
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiArrowRightSLine,
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
|
||||
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[][]
|
||||
onBack: () => void
|
||||
iterDurationMap?: IterationDurationMap
|
||||
}
|
||||
|
||||
const IterationResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onBack,
|
||||
iterDurationMap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedIterations, setExpandedIterations] = useState<Record<number, boolean>>({})
|
||||
|
||||
const toggleIteration = useCallback((index: number) => {
|
||||
setExpandedIterations(prev => ({
|
||||
...prev,
|
||||
[index]: !prev[index],
|
||||
}))
|
||||
}, [])
|
||||
const countIterDuration = (iteration: NodeTracing[], iterDurationMap: IterationDurationMap): string => {
|
||||
const IterRunIndex = iteration[0]?.execution_metadata?.iteration_index as number
|
||||
const iterRunId = iteration[0]?.execution_metadata?.parallel_mode_run_id
|
||||
const iterItem = iterDurationMap[iterRunId || IterRunIndex]
|
||||
const duration = iterItem
|
||||
return `${(duration && duration > 0.01) ? duration.toFixed(2) : 0.01}s`
|
||||
}
|
||||
const iterationStatusShow = (index: number, iteration: NodeTracing[], iterDurationMap?: IterationDurationMap) => {
|
||||
const hasFailed = iteration.some(item => item.status === NodeRunningStatus.Failed)
|
||||
const isRunning = iteration.some(item => item.status === NodeRunningStatus.Running)
|
||||
const hasDurationMap = iterDurationMap && Object.keys(iterDurationMap).length !== 0
|
||||
|
||||
if (hasFailed)
|
||||
return <RiErrorWarningLine className='h-4 w-4 text-text-destructive' />
|
||||
|
||||
if (isRunning)
|
||||
return <RiLoader2Line className='h-3.5 w-3.5 animate-spin text-primary-600' />
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasDurationMap && (
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{countIterDuration(iteration, iterDurationMap)}
|
||||
</div>
|
||||
)}
|
||||
<RiArrowRightSLine
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200',
|
||||
expandedIterations[index] && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-components-panel-bg'>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center border-b-[0.5px] border-b-divider-regular px-4 text-text-accent-secondary'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onBack()
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine className='mr-1 h-4 w-4' />
|
||||
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
|
||||
</div>
|
||||
{/* List */}
|
||||
<div className='bg-components-panel-bg p-2'>
|
||||
{list.map((iteration, index) => (
|
||||
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl border-none bg-background-section-burn')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-center justify-between px-3',
|
||||
expandedIterations[index] ? 'pb-2 pt-3' : 'py-3',
|
||||
'rounded-xl text-left',
|
||||
)}
|
||||
onClick={() => toggleIteration(index)}
|
||||
>
|
||||
<div className={cn('flex grow items-center gap-2')}>
|
||||
<div className='flex h-4 w-4 shrink-0 items-center justify-center rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500'>
|
||||
<Iteration className='h-3 w-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<span className='system-sm-semibold-uppercase grow text-text-primary'>
|
||||
{t(`${i18nPrefix}.iteration`)} {index + 1}
|
||||
</span>
|
||||
{iterationStatusShow(index, iteration, iterDurationMap)}
|
||||
</div>
|
||||
</div>
|
||||
{expandedIterations[index] && <div
|
||||
className="h-px grow bg-divider-subtle"
|
||||
></div>}
|
||||
<div className={cn(
|
||||
'transition-all duration-200',
|
||||
expandedIterations[index]
|
||||
? 'opacity-100'
|
||||
: 'max-h-0 overflow-hidden opacity-0',
|
||||
)}>
|
||||
<TracingPanel
|
||||
list={iteration}
|
||||
className='bg-background-section-burn'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(IterationResultPanel)
|
||||
2
dify/web/app/components/workflow/run/loop-log/index.tsx
Normal file
2
dify/web/app/components/workflow/run/loop-log/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as LoopLogTrigger } from './loop-log-trigger'
|
||||
export { default as LoopResultPanel } from './loop-result-panel'
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type {
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type LoopLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
allExecutions?: NodeTracing[]
|
||||
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
|
||||
}
|
||||
const LoopLogTrigger = ({
|
||||
nodeInfo,
|
||||
allExecutions,
|
||||
onShowLoopResultList,
|
||||
}: LoopLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filterNodesForInstance = (key: string): NodeTracing[] => {
|
||||
if (!allExecutions) return []
|
||||
|
||||
const parallelNodes = allExecutions.filter(exec =>
|
||||
exec.execution_metadata?.parallel_mode_run_id === key,
|
||||
)
|
||||
if (parallelNodes.length > 0)
|
||||
return parallelNodes
|
||||
|
||||
const serialIndex = Number.parseInt(key, 10)
|
||||
if (!isNaN(serialIndex)) {
|
||||
const serialNodes = allExecutions.filter(exec =>
|
||||
exec.execution_metadata?.loop_id === nodeInfo.node_id
|
||||
&& exec.execution_metadata?.loop_index === serialIndex,
|
||||
)
|
||||
if (serialNodes.length > 0)
|
||||
return serialNodes
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const handleOnShowLoopDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
|
||||
const loopNodeMeta = nodeInfo.execution_metadata
|
||||
const loopDurMap = nodeInfo?.loopDurationMap || loopNodeMeta?.loop_duration_map || {}
|
||||
const loopVarMap = loopNodeMeta?.loop_variable_map || {}
|
||||
|
||||
let structuredList: NodeTracing[][] = []
|
||||
if (nodeInfo.details?.length) {
|
||||
structuredList = nodeInfo.details
|
||||
}
|
||||
else if (loopNodeMeta?.loop_duration_map) {
|
||||
const instanceKeys = Object.keys(loopNodeMeta.loop_duration_map)
|
||||
structuredList = instanceKeys
|
||||
.map(key => filterNodesForInstance(key))
|
||||
.filter(branchNodes => branchNodes.length > 0)
|
||||
}
|
||||
|
||||
onShowLoopResultList(
|
||||
structuredList,
|
||||
loopDurMap,
|
||||
loopVarMap,
|
||||
)
|
||||
}
|
||||
|
||||
let displayLoopCount = 0
|
||||
const loopMap = nodeInfo.execution_metadata?.loop_duration_map
|
||||
if (loopMap)
|
||||
displayLoopCount = Object.keys(loopMap).length
|
||||
else if (nodeInfo.details?.length)
|
||||
displayLoopCount = nodeInfo.details.length
|
||||
else if (nodeInfo.metadata?.loop_length)
|
||||
displayLoopCount = nodeInfo.metadata.loop_length
|
||||
|
||||
const getErrorCount = (details: NodeTracing[][] | undefined) => {
|
||||
if (!details || details.length === 0)
|
||||
return 0
|
||||
return details.reduce((acc, loop) => {
|
||||
if (loop.some(item => item.status === 'failed'))
|
||||
acc++
|
||||
return acc
|
||||
}, 0)
|
||||
}
|
||||
const errorCount = getErrorCount(nodeInfo.details)
|
||||
|
||||
return (
|
||||
<Button
|
||||
className='flex w-full cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-components-button-tertiary-bg-hover px-3 py-2 hover:bg-components-button-tertiary-bg-hover'
|
||||
onClick={handleOnShowLoopDetail}
|
||||
>
|
||||
<Loop className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
|
||||
<div className='system-sm-medium flex-1 text-left text-components-button-tertiary-text'>{t('workflow.nodes.loop.loop', { count: displayLoopCount })}{errorCount > 0 && (
|
||||
<>
|
||||
{t('workflow.nodes.loop.comma')}
|
||||
{t('workflow.nodes.loop.error', { count: errorCount })}
|
||||
</>
|
||||
)}</div>
|
||||
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoopLogTrigger
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiArrowRightSLine,
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { LoopDurationMap, LoopVariableMap, NodeTracing } from '@/types/workflow'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[][]
|
||||
onBack: () => void
|
||||
loopDurationMap?: LoopDurationMap
|
||||
loopVariableMap?: LoopVariableMap
|
||||
}
|
||||
|
||||
const LoopResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onBack,
|
||||
loopDurationMap,
|
||||
loopVariableMap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>({})
|
||||
|
||||
const toggleLoop = useCallback((index: number) => {
|
||||
setExpandedLoops(prev => ({
|
||||
...prev,
|
||||
[index]: !prev[index],
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const countLoopDuration = (loop: NodeTracing[], loopDurationMap: LoopDurationMap): string => {
|
||||
const loopRunIndex = loop[0]?.execution_metadata?.loop_index as number
|
||||
const loopRunId = loop[0]?.execution_metadata?.parallel_mode_run_id
|
||||
const loopItem = loopDurationMap[loopRunId || loopRunIndex]
|
||||
const duration = loopItem
|
||||
return `${(duration && duration > 0.01) ? duration.toFixed(2) : 0.01}s`
|
||||
}
|
||||
|
||||
const loopStatusShow = (index: number, loop: NodeTracing[], loopDurationMap?: LoopDurationMap) => {
|
||||
const hasFailed = loop.some(item => item.status === NodeRunningStatus.Failed)
|
||||
const isRunning = loop.some(item => item.status === NodeRunningStatus.Running)
|
||||
const hasDurationMap = loopDurationMap && Object.keys(loopDurationMap).length !== 0
|
||||
|
||||
if (hasFailed)
|
||||
return <RiErrorWarningLine className='h-4 w-4 text-text-destructive' />
|
||||
|
||||
if (isRunning)
|
||||
return <RiLoader2Line className='h-3.5 w-3.5 animate-spin text-primary-600' />
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasDurationMap && (
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{countLoopDuration(loop, loopDurationMap)}
|
||||
</div>
|
||||
)}
|
||||
<RiArrowRightSLine
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200',
|
||||
expandedLoops[index] && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-components-panel-bg'>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center border-b-[0.5px] border-b-divider-regular px-4 text-text-accent-secondary'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onBack()
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine className='mr-1 h-4 w-4' />
|
||||
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
|
||||
</div>
|
||||
{/* List */}
|
||||
<div className='bg-components-panel-bg p-2'>
|
||||
{list.map((loop, index) => (
|
||||
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl border-none bg-background-section-burn')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-center justify-between px-3',
|
||||
expandedLoops[index] ? 'pb-2 pt-3' : 'py-3',
|
||||
'rounded-xl text-left',
|
||||
)}
|
||||
onClick={() => toggleLoop(index)}
|
||||
>
|
||||
<div className={cn('flex grow items-center gap-2')}>
|
||||
<div className='flex h-4 w-4 shrink-0 items-center justify-center rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500'>
|
||||
<Loop className='h-3 w-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<span className='system-sm-semibold-uppercase grow text-text-primary'>
|
||||
{t(`${i18nPrefix}.loop`)} {index + 1}
|
||||
</span>
|
||||
{loopStatusShow(index, loop, loopDurationMap)}
|
||||
</div>
|
||||
</div>
|
||||
{expandedLoops[index] && <div
|
||||
className="h-px grow bg-divider-subtle"
|
||||
></div>}
|
||||
<div className={cn(
|
||||
'transition-all duration-200',
|
||||
expandedLoops[index]
|
||||
? 'opacity-100'
|
||||
: 'max-h-0 overflow-hidden opacity-0',
|
||||
)}>
|
||||
{
|
||||
loopVariableMap?.[index] && (
|
||||
<div className='p-2 pb-0'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.nodes.loop.loopVariables').toLocaleUpperCase()}</div>}
|
||||
language={CodeLanguage.json}
|
||||
height={112}
|
||||
value={loopVariableMap[index]}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<TracingPanel
|
||||
list={loop}
|
||||
className='bg-background-section-burn'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LoopResultPanel)
|
||||
124
dify/web/app/components/workflow/run/loop-result-panel.tsx
Normal file
124
dify/web/app/components/workflow/run/loop-result-panel.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
|
||||
import TracingPanel from './tracing-panel'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[][]
|
||||
onHide: () => void
|
||||
onBack: () => void
|
||||
noWrap?: boolean
|
||||
}
|
||||
|
||||
const LoopResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onHide,
|
||||
onBack,
|
||||
noWrap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>([])
|
||||
|
||||
const toggleLoop = useCallback((index: number) => {
|
||||
setExpandedLoops(prev => ({
|
||||
...prev,
|
||||
[index]: !prev[index],
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const main = (
|
||||
<>
|
||||
<div className={cn(!noWrap && 'shrink-0 ', 'px-4 pt-3')}>
|
||||
<div className='flex h-8 shrink-0 items-center justify-between'>
|
||||
<div className='system-xl-semibold truncate text-text-primary'>
|
||||
{t(`${i18nPrefix}.testRunLoop`)}
|
||||
</div>
|
||||
<div className='ml-2 shrink-0 cursor-pointer p-1' onClick={onHide}>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex cursor-pointer items-center space-x-1 py-2 text-text-accent-secondary' onClick={onBack}>
|
||||
<ArrowNarrowLeft className='h-4 w-4' />
|
||||
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* List */}
|
||||
<div className={cn(!noWrap ? 'grow overflow-auto' : 'max-h-full', 'bg-components-panel-bg p-2')}>
|
||||
{list.map((loop, index) => (
|
||||
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl border-none bg-background-section-burn')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-center justify-between px-3',
|
||||
expandedLoops[index] ? 'pb-2 pt-3' : 'py-3',
|
||||
'rounded-xl text-left',
|
||||
)}
|
||||
onClick={() => toggleLoop(index)}
|
||||
>
|
||||
<div className={cn('flex grow items-center gap-2')}>
|
||||
<div className='flex h-4 w-4 shrink-0 items-center justify-center rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500'>
|
||||
<Loop className='h-3 w-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<span className='system-sm-semibold-uppercase grow text-text-primary'>
|
||||
{t(`${i18nPrefix}.loop`)} {index + 1}
|
||||
</span>
|
||||
<RiArrowRightSLine className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200',
|
||||
expandedLoops[index] && 'rotate-90',
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
{expandedLoops[index] && <div
|
||||
className="h-px grow bg-divider-subtle"
|
||||
></div>}
|
||||
<div className={cn(
|
||||
'transition-all duration-200',
|
||||
expandedLoops[index]
|
||||
? 'opacity-100'
|
||||
: 'max-h-0 overflow-hidden opacity-0',
|
||||
)}>
|
||||
<TracingPanel
|
||||
list={loop}
|
||||
className='bg-background-section-burn'
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
const handleNotBubble = useCallback((e: React.MouseEvent) => {
|
||||
// if not do this, it will trigger the message log modal disappear(useClickAway)
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}, [])
|
||||
|
||||
if (noWrap)
|
||||
return main
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute inset-0 z-10 rounded-2xl pt-10'
|
||||
style={{
|
||||
backgroundColor: 'rgba(16, 24, 40, 0.20)',
|
||||
}}
|
||||
onClick={handleNotBubble}
|
||||
>
|
||||
<div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'>
|
||||
{main}
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(LoopResultPanel)
|
||||
117
dify/web/app/components/workflow/run/meta.tsx
Normal file
117
dify/web/app/components/workflow/run/meta.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
|
||||
type Props = {
|
||||
status: string
|
||||
executor?: string
|
||||
startTime?: number
|
||||
time?: number
|
||||
tokens?: number
|
||||
steps?: number
|
||||
showSteps?: boolean
|
||||
}
|
||||
|
||||
const MetaData: FC<Props> = ({
|
||||
status,
|
||||
executor,
|
||||
startTime,
|
||||
time,
|
||||
tokens,
|
||||
steps = 1,
|
||||
showSteps = true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTime } = useTimestamp()
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className='system-xs-medium-uppercase h-6 py-1 text-text-tertiary'>{t('runLog.meta.title')}</div>
|
||||
<div className='py-1'>
|
||||
<div className='flex'>
|
||||
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.status')}</div>
|
||||
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
|
||||
{status === 'running' && (
|
||||
<div className='my-1 h-2 w-16 rounded-sm bg-text-quaternary'/>
|
||||
)}
|
||||
{status === 'succeeded' && (
|
||||
<span>SUCCESS</span>
|
||||
)}
|
||||
{status === 'partial-succeeded' && (
|
||||
<span>PARTIAL SUCCESS</span>
|
||||
)}
|
||||
{status === 'exception' && (
|
||||
<span>EXCEPTION</span>
|
||||
)}
|
||||
{status === 'failed' && (
|
||||
<span>FAIL</span>
|
||||
)}
|
||||
{status === 'stopped' && (
|
||||
<span>STOP</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.executor')}</div>
|
||||
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
|
||||
{status === 'running' && (
|
||||
<div className='my-1 h-2 w-[88px] rounded-sm bg-text-quaternary'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{executor || 'N/A'}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.startTime')}</div>
|
||||
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
|
||||
{status === 'running' && (
|
||||
<div className='my-1 h-2 w-[72px] rounded-sm bg-text-quaternary'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{startTime ? formatTime(startTime, t('appLog.dateTimeFormat') as string) : '-'}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.time')}</div>
|
||||
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
|
||||
{status === 'running' && (
|
||||
<div className='my-1 h-2 w-[72px] rounded-sm bg-text-quaternary'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{time ? `${time.toFixed(3)}s` : '-'}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.tokens')}</div>
|
||||
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
|
||||
{status === 'running' && (
|
||||
<div className='my-1 h-2 w-[48px] rounded-sm bg-text-quaternary'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{`${tokens || 0} Tokens`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showSteps && (
|
||||
<div className='flex'>
|
||||
<div className='system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary'>{t('runLog.meta.steps')}</div>
|
||||
<div className='system-xs-regular grow px-2 py-1.5 text-text-secondary'>
|
||||
{status === 'running' && (
|
||||
<div className='my-1 h-2 w-[24px] rounded-sm bg-text-quaternary'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{steps}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MetaData
|
||||
270
dify/web/app/components/workflow/run/node.tsx
Normal file
270
dify/web/app/components/workflow/run/node.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiArrowRightSLine,
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import { RetryLogTrigger } from './retry-log'
|
||||
import { IterationLogTrigger } from './iteration-log'
|
||||
import { LoopLogTrigger } from './loop-log'
|
||||
import { AgentLogTrigger } from './agent-log'
|
||||
import cn from '@/utils/classnames'
|
||||
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||
import { hasRetryNode } from '@/app/components/workflow/utils'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import LargeDataAlert from '../variable-inspect/large-data-alert'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
nodeInfo: NodeTracing
|
||||
allExecutions?: NodeTracing[]
|
||||
inMessage?: boolean
|
||||
hideInfo?: boolean
|
||||
hideProcessDetail?: boolean
|
||||
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
|
||||
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
notShowIterationNav?: boolean
|
||||
notShowLoopNav?: boolean
|
||||
}
|
||||
|
||||
const NodePanel: FC<Props> = ({
|
||||
className,
|
||||
nodeInfo,
|
||||
allExecutions,
|
||||
inMessage = false,
|
||||
hideInfo = false,
|
||||
hideProcessDetail,
|
||||
onShowIterationDetail,
|
||||
onShowLoopDetail,
|
||||
onShowRetryDetail,
|
||||
onShowAgentOrToolLog,
|
||||
notShowIterationNav,
|
||||
notShowLoopNav,
|
||||
}) => {
|
||||
const [collapseState, doSetCollapseState] = useState<boolean>(true)
|
||||
const setCollapseState = useCallback((state: boolean) => {
|
||||
if (hideProcessDetail)
|
||||
return
|
||||
doSetCollapseState(state)
|
||||
}, [hideProcessDetail])
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
const getTime = (time: number) => {
|
||||
if (time < 1)
|
||||
return `${(time * 1000).toFixed(3)} ms`
|
||||
if (time > 60)
|
||||
return `${Math.floor(time / 60)} m ${(time % 60).toFixed(3)} s`
|
||||
return `${time.toFixed(3)} s`
|
||||
}
|
||||
|
||||
const getTokenCount = (tokens: number) => {
|
||||
if (tokens < 1000)
|
||||
return tokens
|
||||
if (tokens >= 1000 && tokens < 1000000)
|
||||
return `${Number.parseFloat((tokens / 1000).toFixed(3))}K`
|
||||
if (tokens >= 1000000)
|
||||
return `${Number.parseFloat((tokens / 1000000).toFixed(3))}M`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCollapseState(!nodeInfo.expand)
|
||||
}, [nodeInfo.expand, setCollapseState])
|
||||
|
||||
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!nodeInfo.details?.length
|
||||
const isLoopNode = nodeInfo.node_type === BlockEnum.Loop && !!nodeInfo.details?.length
|
||||
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
|
||||
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
|
||||
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
|
||||
|
||||
const inputsTitle = useMemo(() => {
|
||||
let text = t('workflow.common.input')
|
||||
if (nodeInfo.node_type === BlockEnum.Loop)
|
||||
text = t('workflow.nodes.loop.initialLoopVariables')
|
||||
return text.toLocaleUpperCase()
|
||||
}, [nodeInfo.node_type, t])
|
||||
const processDataTitle = t('workflow.common.processData').toLocaleUpperCase()
|
||||
const outputTitle = useMemo(() => {
|
||||
let text = t('workflow.common.output')
|
||||
if (nodeInfo.node_type === BlockEnum.Loop)
|
||||
text = t('workflow.nodes.loop.finalLoopVariables')
|
||||
return text.toLocaleUpperCase()
|
||||
}, [nodeInfo.node_type, t])
|
||||
|
||||
return (
|
||||
<div className={cn('px-2 py-1', className)}>
|
||||
<div className='group rounded-[10px] border border-components-panel-border bg-background-default shadow-xs transition-all hover:shadow-md'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center pl-1 pr-3',
|
||||
hideInfo ? 'py-2 pl-2' : 'py-1.5',
|
||||
!collapseState && (hideInfo ? '!pb-1' : '!pb-1.5'),
|
||||
)}
|
||||
onClick={() => setCollapseState(!collapseState)}
|
||||
>
|
||||
{!hideProcessDetail && (
|
||||
<RiArrowRightSLine
|
||||
className={cn(
|
||||
'mr-1 h-4 w-4 shrink-0 text-text-quaternary transition-all group-hover:text-text-tertiary',
|
||||
!collapseState && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<BlockIcon size={inMessage ? 'xs' : 'sm'} className={cn('mr-2 shrink-0', inMessage && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='max-w-xs'>{nodeInfo.title}</div>
|
||||
}
|
||||
>
|
||||
<div className={cn(
|
||||
'system-xs-semibold-uppercase grow truncate text-text-secondary',
|
||||
hideInfo && '!text-xs',
|
||||
)}>{nodeInfo.title}</div>
|
||||
</Tooltip>
|
||||
{nodeInfo.status !== 'running' && !hideInfo && (
|
||||
<div className='system-xs-regular shrink-0 text-text-tertiary'>{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}</div>
|
||||
)}
|
||||
{nodeInfo.status === 'succeeded' && (
|
||||
<RiCheckboxCircleFill className='ml-2 h-3.5 w-3.5 shrink-0 text-text-success' />
|
||||
)}
|
||||
{nodeInfo.status === 'failed' && (
|
||||
<RiErrorWarningLine className='ml-2 h-3.5 w-3.5 shrink-0 text-text-warning' />
|
||||
)}
|
||||
{nodeInfo.status === 'stopped' && (
|
||||
<RiAlertFill className={cn('ml-2 h-4 w-4 shrink-0 text-text-warning-secondary', inMessage && 'h-3.5 w-3.5')} />
|
||||
)}
|
||||
{nodeInfo.status === 'exception' && (
|
||||
<RiAlertFill className={cn('ml-2 h-4 w-4 shrink-0 text-text-warning-secondary', inMessage && 'h-3.5 w-3.5')} />
|
||||
)}
|
||||
{nodeInfo.status === 'running' && (
|
||||
<div className='flex shrink-0 items-center text-[13px] font-medium leading-[16px] text-text-accent'>
|
||||
<span className='mr-2 text-xs font-normal'>Running</span>
|
||||
<RiLoader2Line className='h-3.5 w-3.5 animate-spin' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!collapseState && !hideProcessDetail && (
|
||||
<div className='px-1 pb-1'>
|
||||
{/* The nav to the iteration detail */}
|
||||
{isIterationNode && !notShowIterationNav && onShowIterationDetail && (
|
||||
<IterationLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
allExecutions={allExecutions}
|
||||
onShowIterationResultList={onShowIterationDetail}
|
||||
/>
|
||||
)}
|
||||
{/* The nav to the Loop detail */}
|
||||
{isLoopNode && !notShowLoopNav && onShowLoopDetail && (
|
||||
<LoopLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
allExecutions={allExecutions}
|
||||
onShowLoopResultList={onShowLoopDetail}
|
||||
/>
|
||||
)}
|
||||
{isRetryNode && onShowRetryDetail && (
|
||||
<RetryLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowRetryResultList={onShowRetryDetail}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
(isAgentNode || isToolNode) && onShowAgentOrToolLog && (
|
||||
<AgentLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}>
|
||||
{(nodeInfo.status === 'stopped') && (
|
||||
<StatusContainer status='stopped'>
|
||||
{t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}
|
||||
</StatusContainer>
|
||||
)}
|
||||
{(nodeInfo.status === 'exception') && (
|
||||
<StatusContainer status='stopped'>
|
||||
{nodeInfo.error}
|
||||
<a
|
||||
href={docLink('/guides/workflow/error-handling/error-type')}
|
||||
target='_blank'
|
||||
className='text-text-accent'
|
||||
>
|
||||
{t('workflow.common.learnMore')}
|
||||
</a>
|
||||
</StatusContainer>
|
||||
)}
|
||||
{nodeInfo.status === 'failed' && (
|
||||
<StatusContainer status='failed'>
|
||||
{nodeInfo.error}
|
||||
</StatusContainer>
|
||||
)}
|
||||
{nodeInfo.status === 'retry' && (
|
||||
<StatusContainer status='failed'>
|
||||
{nodeInfo.error}
|
||||
</StatusContainer>
|
||||
)}
|
||||
</div>
|
||||
{nodeInfo.inputs && (
|
||||
<div className={cn('mb-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{inputsTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.inputs}
|
||||
isJSONStringifyBeauty
|
||||
footer={nodeInfo.inputs_truncated && <LargeDataAlert textHasNoExport className='mx-1 mb-1 mt-2 h-7' />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{nodeInfo.process_data && (
|
||||
<div className={cn('mb-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{processDataTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.process_data}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{nodeInfo.outputs && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{outputTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.outputs}
|
||||
isJSONStringifyBeauty
|
||||
tip={<ErrorHandleTip type={nodeInfo.execution_metadata?.error_strategy} />}
|
||||
footer={nodeInfo.outputs_truncated && <LargeDataAlert textHasNoExport downloadUrl={nodeInfo.outputs_full_content?.download_url} className='mx-1 mb-1 mt-2 h-7' />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NodePanel
|
||||
111
dify/web/app/components/workflow/run/output-panel.tsx
Normal file
111
dify/web/app/components/workflow/run/output-panel.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import { FileList } from '@/app/components/base/file-uploader'
|
||||
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
|
||||
type OutputPanelProps = {
|
||||
isRunning?: boolean
|
||||
outputs?: any
|
||||
error?: string
|
||||
height?: number
|
||||
}
|
||||
|
||||
const OutputPanel: FC<OutputPanelProps> = ({
|
||||
isRunning,
|
||||
outputs,
|
||||
error,
|
||||
height,
|
||||
}) => {
|
||||
const isTextOutput = useMemo(() => {
|
||||
if (!outputs || typeof outputs !== 'object')
|
||||
return false
|
||||
const keys = Object.keys(outputs)
|
||||
const value = outputs[keys[0]]
|
||||
return keys.length === 1 && (
|
||||
typeof value === 'string'
|
||||
|| (Array.isArray(value) && value.every(item => typeof item === 'string'))
|
||||
)
|
||||
}, [outputs])
|
||||
|
||||
const fileList = useMemo(() => {
|
||||
const fileList: any[] = []
|
||||
if (!outputs)
|
||||
return fileList
|
||||
if (Object.keys(outputs).length > 1)
|
||||
return fileList
|
||||
for (const key in outputs) {
|
||||
if (Array.isArray(outputs[key])) {
|
||||
outputs[key].map((output: any) => {
|
||||
if (output?.dify_model_identity === '__dify__file__')
|
||||
fileList.push(output)
|
||||
return null
|
||||
})
|
||||
}
|
||||
else if (outputs[key]?.dify_model_identity === '__dify__file__') {
|
||||
fileList.push(outputs[key])
|
||||
}
|
||||
}
|
||||
return getProcessedFilesFromResponse(fileList)
|
||||
}, [outputs])
|
||||
return (
|
||||
<div className='p-2'>
|
||||
{isRunning && (
|
||||
<div className='pl-[26px] pt-4'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
)}
|
||||
{!isRunning && error && (
|
||||
<div className='px-4'>
|
||||
<StatusContainer status='failed'>{error}</StatusContainer>
|
||||
</div>
|
||||
)}
|
||||
{!isRunning && !outputs && (
|
||||
<div className='px-4 py-2'>
|
||||
<Markdown content='No Output' />
|
||||
</div>
|
||||
)}
|
||||
{isTextOutput && (
|
||||
<div className='px-4 py-2'>
|
||||
<Markdown
|
||||
content={
|
||||
Array.isArray(outputs[Object.keys(outputs)[0]])
|
||||
? outputs[Object.keys(outputs)[0]].join('\n')
|
||||
: (outputs[Object.keys(outputs)[0]] || '')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{fileList.length > 0 && (
|
||||
<div className='px-4 py-2'>
|
||||
<FileList
|
||||
files={fileList}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
canPreview
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isTextOutput && outputs && Object.keys(outputs).length > 0 && height! > 0 && (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<CodeEditor
|
||||
showFileList
|
||||
readOnly
|
||||
title={<div tabIndex={0}>Output</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={JSON.stringify(outputs, null, 2)}
|
||||
isJSONStringifyBeauty
|
||||
height={height ? (height - 16) / 2 : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OutputPanel
|
||||
177
dify/web/app/components/workflow/run/result-panel.tsx
Normal file
177
dify/web/app/components/workflow/run/result-panel.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import StatusPanel from './status'
|
||||
import MetaData from './meta'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { hasRetryNode } from '@/app/components/workflow/utils'
|
||||
import { IterationLogTrigger } from '@/app/components/workflow/run/iteration-log'
|
||||
import { LoopLogTrigger } from '@/app/components/workflow/run/loop-log'
|
||||
import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
|
||||
import { AgentLogTrigger } from '@/app/components/workflow/run/agent-log'
|
||||
import LargeDataAlert from '../variable-inspect/large-data-alert'
|
||||
|
||||
export type ResultPanelProps = {
|
||||
nodeInfo?: NodeTracing
|
||||
inputs?: string
|
||||
inputs_truncated?: boolean
|
||||
process_data?: string
|
||||
process_data_truncated?: boolean
|
||||
outputs?: string | Record<string, any>
|
||||
outputs_truncated?: boolean
|
||||
outputs_full_content?: {
|
||||
download_url: string
|
||||
}
|
||||
status: string
|
||||
error?: string
|
||||
elapsed_time?: number
|
||||
total_tokens?: number
|
||||
created_at?: number
|
||||
created_by?: string
|
||||
finished_at?: number
|
||||
steps?: number
|
||||
showSteps?: boolean
|
||||
exceptionCounts?: number
|
||||
execution_metadata?: any
|
||||
isListening?: boolean
|
||||
handleShowIterationResultList?: (detail: NodeTracing[][], iterDurationMap: any) => void
|
||||
handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
|
||||
const ResultPanel: FC<ResultPanelProps> = ({
|
||||
nodeInfo,
|
||||
inputs,
|
||||
inputs_truncated,
|
||||
process_data,
|
||||
process_data_truncated,
|
||||
outputs,
|
||||
outputs_truncated,
|
||||
outputs_full_content,
|
||||
status,
|
||||
error,
|
||||
elapsed_time,
|
||||
total_tokens,
|
||||
created_at,
|
||||
created_by,
|
||||
steps,
|
||||
showSteps,
|
||||
exceptionCounts,
|
||||
execution_metadata,
|
||||
isListening = false,
|
||||
handleShowIterationResultList,
|
||||
handleShowLoopResultList,
|
||||
onShowRetryDetail,
|
||||
handleShowAgentOrToolLog,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length
|
||||
const isLoopNode = nodeInfo?.node_type === BlockEnum.Loop && !!nodeInfo?.details?.length
|
||||
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length
|
||||
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length
|
||||
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length
|
||||
|
||||
return (
|
||||
<div className='bg-components-panel-bg py-2'>
|
||||
<div className='px-4 py-2'>
|
||||
<StatusPanel
|
||||
status={status}
|
||||
time={elapsed_time}
|
||||
tokens={total_tokens}
|
||||
error={error}
|
||||
exceptionCounts={exceptionCounts}
|
||||
isListening={isListening}
|
||||
/>
|
||||
</div>
|
||||
<div className='px-4'>
|
||||
{
|
||||
isIterationNode && handleShowIterationResultList && (
|
||||
<IterationLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowIterationResultList={handleShowIterationResultList}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isLoopNode && handleShowLoopResultList && (
|
||||
<LoopLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowLoopResultList={handleShowLoopResultList}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isRetryNode && onShowRetryDetail && (
|
||||
<RetryLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowRetryResultList={onShowRetryDetail}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
(isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
|
||||
<AgentLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='flex flex-col gap-2 px-4 py-2'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.input').toLocaleUpperCase()}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={inputs}
|
||||
isJSONStringifyBeauty
|
||||
footer={inputs_truncated && <LargeDataAlert textHasNoExport className='mx-1 mb-1 mt-2 h-7' />}
|
||||
/>
|
||||
{process_data && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.processData').toLocaleUpperCase()}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={process_data}
|
||||
isJSONStringifyBeauty
|
||||
footer={process_data_truncated && <LargeDataAlert textHasNoExport className='mx-1 mb-1 mt-2 h-7' />}
|
||||
/>
|
||||
)}
|
||||
{(outputs || status === 'running') && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.output').toLocaleUpperCase()}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={outputs}
|
||||
isJSONStringifyBeauty
|
||||
tip={<ErrorHandleTip type={execution_metadata?.error_strategy} />}
|
||||
footer={outputs_truncated && <LargeDataAlert textHasNoExport downloadUrl={outputs_full_content?.download_url} className='mx-1 mb-1 mt-2 h-7' />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<div className='divider-subtle h-[0.5px]' />
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<MetaData
|
||||
status={status}
|
||||
executor={created_by}
|
||||
startTime={created_at}
|
||||
time={elapsed_time}
|
||||
tokens={total_tokens}
|
||||
steps={steps}
|
||||
showSteps={showSteps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResultPanel
|
||||
75
dify/web/app/components/workflow/run/result-text.tsx
Normal file
75
dify/web/app/components/workflow/run/result-text.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ImageIndentLeft } from '@/app/components/base/icons/src/vender/line/editor'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||
import { FileList } from '@/app/components/base/file-uploader'
|
||||
|
||||
type ResultTextProps = {
|
||||
isRunning?: boolean
|
||||
outputs?: any
|
||||
error?: string
|
||||
onClick?: () => void
|
||||
allFiles?: any[]
|
||||
}
|
||||
|
||||
const ResultText: FC<ResultTextProps> = ({
|
||||
isRunning,
|
||||
outputs,
|
||||
error,
|
||||
onClick,
|
||||
allFiles,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='bg-background-section-burn'>
|
||||
{isRunning && !outputs && (
|
||||
<div className='pl-[26px] pt-4'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
)}
|
||||
{!isRunning && error && (
|
||||
<div className='px-4 py-2'>
|
||||
<StatusContainer status='failed'>
|
||||
{error}
|
||||
</StatusContainer>
|
||||
</div>
|
||||
)}
|
||||
{!isRunning && !outputs && !error && !allFiles?.length && (
|
||||
<div className='mt-[120px] flex flex-col items-center px-4 py-2 text-[13px] leading-[18px] text-gray-500'>
|
||||
<ImageIndentLeft className='h-6 w-6 text-gray-400' />
|
||||
<div className='mr-2'>{t('runLog.resultEmpty.title')}</div>
|
||||
<div>
|
||||
{t('runLog.resultEmpty.tipLeft')}
|
||||
<span onClick={onClick} className='cursor-pointer text-primary-600'>{t('runLog.resultEmpty.link')}</span>
|
||||
{t('runLog.resultEmpty.tipRight')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(outputs || !!allFiles?.length) && (
|
||||
<>
|
||||
{outputs && (
|
||||
<div className='px-4 py-2'>
|
||||
<Markdown content={outputs} />
|
||||
</div>
|
||||
)}
|
||||
{!!allFiles?.length && allFiles.map(item => (
|
||||
<div key={item.varName} className='system-xs-regular flex flex-col gap-1 px-4 py-2'>
|
||||
<div className='py-1 text-text-tertiary '>{item.varName}</div>
|
||||
<FileList
|
||||
files={item.list}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
canPreview
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResultText
|
||||
2
dify/web/app/components/workflow/run/retry-log/index.tsx
Normal file
2
dify/web/app/components/workflow/run/retry-log/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as RetryLogTrigger } from './retry-log-trigger'
|
||||
export { default as RetryResultPanel } from './retry-result-panel'
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiRestartFill,
|
||||
} from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type RetryLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowRetryResultList: (detail: NodeTracing[]) => void
|
||||
}
|
||||
const RetryLogTrigger = ({
|
||||
nodeInfo,
|
||||
onShowRetryResultList,
|
||||
}: RetryLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { retryDetail } = nodeInfo
|
||||
|
||||
const handleShowRetryResultList = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onShowRetryResultList(retryDetail || [])
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className='mb-1 flex w-full items-center justify-between'
|
||||
variant='tertiary'
|
||||
onClick={handleShowRetryResultList}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<RiRestartFill className='mr-0.5 h-4 w-4 shrink-0 text-components-button-tertiary-text' />
|
||||
{t('workflow.nodes.common.retry.retries', { num: retryDetail?.length })}
|
||||
</div>
|
||||
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default RetryLogTrigger
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import TracingPanel from '../tracing-panel'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[]
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const RetryResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onBack,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className='system-sm-medium flex h-8 cursor-pointer items-center bg-components-panel-bg px-4 text-text-accent-secondary'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onBack()
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine className='mr-1 h-4 w-4' />
|
||||
{t('workflow.singleRun.back')}
|
||||
</div>
|
||||
<TracingPanel
|
||||
list={list.map((item, index) => ({
|
||||
...item,
|
||||
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
|
||||
}))}
|
||||
className='bg-background-section-burn'
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default memo(RetryResultPanel)
|
||||
@@ -0,0 +1,98 @@
|
||||
import { RetryResultPanel } from './retry-log'
|
||||
import { IterationResultPanel } from './iteration-log'
|
||||
import { LoopResultPanel } from './loop-log'
|
||||
import { AgentResultPanel } from './agent-log'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
export type SpecialResultPanelProps = {
|
||||
showRetryDetail?: boolean
|
||||
setShowRetryDetailFalse?: () => void
|
||||
retryResultList?: NodeTracing[]
|
||||
|
||||
showIteratingDetail?: boolean
|
||||
setShowIteratingDetailFalse?: () => void
|
||||
iterationResultList?: NodeTracing[][]
|
||||
iterationResultDurationMap?: IterationDurationMap
|
||||
|
||||
showLoopingDetail?: boolean
|
||||
setShowLoopingDetailFalse?: () => void
|
||||
loopResultList?: NodeTracing[][]
|
||||
loopResultDurationMap?: LoopDurationMap
|
||||
loopResultVariableMap?: LoopVariableMap
|
||||
|
||||
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
|
||||
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
|
||||
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const SpecialResultPanel = ({
|
||||
showRetryDetail,
|
||||
setShowRetryDetailFalse,
|
||||
retryResultList,
|
||||
|
||||
showIteratingDetail,
|
||||
setShowIteratingDetailFalse,
|
||||
iterationResultList,
|
||||
iterationResultDurationMap,
|
||||
|
||||
showLoopingDetail,
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
loopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
}: SpecialResultPanelProps) => {
|
||||
return (
|
||||
<div onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}}>
|
||||
{
|
||||
!!showRetryDetail && !!retryResultList?.length && setShowRetryDetailFalse && (
|
||||
<RetryResultPanel
|
||||
list={retryResultList}
|
||||
onBack={setShowRetryDetailFalse}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && (
|
||||
<IterationResultPanel
|
||||
list={iterationResultList}
|
||||
onBack={setShowIteratingDetailFalse}
|
||||
iterDurationMap={iterationResultDurationMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showLoopingDetail && !!loopResultList?.length && setShowLoopingDetailFalse && (
|
||||
<LoopResultPanel
|
||||
list={loopResultList}
|
||||
onBack={setShowLoopingDetailFalse}
|
||||
loopDurationMap={loopResultDurationMap}
|
||||
loopVariableMap={loopResultVariableMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!agentOrToolLogItemStack?.length && agentOrToolLogListMap && handleShowAgentOrToolLog && (
|
||||
<AgentResultPanel
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
agentOrToolLogListMap={agentOrToolLogListMap}
|
||||
onShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpecialResultPanel
|
||||
52
dify/web/app/components/workflow/run/status-container.tsx
Normal file
52
dify/web/app/components/workflow/run/status-container.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Theme } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
|
||||
type Props = {
|
||||
status: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const StatusContainer: FC<Props> = ({
|
||||
status,
|
||||
children,
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-regular relative break-all rounded-lg border px-3 py-2.5',
|
||||
status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
|
||||
status === 'succeeded' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'succeeded' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'partial-succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
|
||||
status === 'partial-succeeded' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'partial-succeeded' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] text-text-warning',
|
||||
status === 'failed' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'failed' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(240,68,56,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
|
||||
status === 'stopped' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'stopped' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'exception' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
|
||||
status === 'exception' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'exception' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'running' && 'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] text-util-colors-blue-light-blue-light-600',
|
||||
status === 'running' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'running' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(11,165,236,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
'absolute left-0 top-0 h-[50px] w-[65%] bg-no-repeat',
|
||||
theme === Theme.light && 'bg-[url(~@/app/components/workflow/run/assets/highlight.svg)]',
|
||||
theme === Theme.dark && 'bg-[url(~@/app/components/workflow/run/assets/highlight-dark.svg)]',
|
||||
)}></div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusContainer
|
||||
155
dify/web/app/components/workflow/run/status.tsx
Normal file
155
dify/web/app/components/workflow/run/status.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
type ResultProps = {
|
||||
status: string
|
||||
time?: number
|
||||
tokens?: number
|
||||
error?: string
|
||||
exceptionCounts?: number
|
||||
isListening?: boolean
|
||||
}
|
||||
|
||||
const StatusPanel: FC<ResultProps> = ({
|
||||
status,
|
||||
time,
|
||||
tokens,
|
||||
error,
|
||||
exceptionCounts,
|
||||
isListening = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
return (
|
||||
<StatusContainer status={status}>
|
||||
<div className='flex'>
|
||||
<div className={cn(
|
||||
'max-w-[120px] flex-[33%]',
|
||||
status === 'partial-succeeded' && 'min-w-[140px]',
|
||||
)}>
|
||||
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t('runLog.resultPanel.status')}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-semibold-uppercase flex items-center gap-1',
|
||||
status === 'succeeded' && 'text-util-colors-green-green-600',
|
||||
status === 'partial-succeeded' && 'text-util-colors-green-green-600',
|
||||
status === 'failed' && 'text-util-colors-red-red-600',
|
||||
status === 'stopped' && 'text-util-colors-warning-warning-600',
|
||||
status === 'running' && 'text-util-colors-blue-light-blue-light-600',
|
||||
)}
|
||||
>
|
||||
{status === 'running' && (
|
||||
<>
|
||||
<Indicator color={'blue'} />
|
||||
<span>{isListening ? 'Listening' : 'Running'}</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'succeeded' && (
|
||||
<>
|
||||
<Indicator color={'green'} />
|
||||
<span>SUCCESS</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'partial-succeeded' && (
|
||||
<>
|
||||
<Indicator color={'green'} />
|
||||
<span>PARTIAL SUCCESS</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'exception' && (
|
||||
<>
|
||||
<Indicator color={'yellow'} />
|
||||
<span>EXCEPTION</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'failed' && (
|
||||
<>
|
||||
<Indicator color={'red'} />
|
||||
<span>FAIL</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'stopped' && (
|
||||
<>
|
||||
<Indicator color={'yellow'} />
|
||||
<span>STOP</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='max-w-[152px] flex-[33%]'>
|
||||
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t('runLog.resultPanel.time')}</div>
|
||||
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
|
||||
{status === 'running' && (
|
||||
<div className='h-2 w-16 rounded-sm bg-text-quaternary' />
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{time ? `${time?.toFixed(3)}s` : '-'}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-[33%]'>
|
||||
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t('runLog.resultPanel.tokens')}</div>
|
||||
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
|
||||
{status === 'running' && (
|
||||
<div className='h-2 w-20 rounded-sm bg-text-quaternary' />
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{`${tokens || 0} Tokens`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{status === 'failed' && error && (
|
||||
<>
|
||||
<div className='my-2 h-[0.5px] bg-divider-subtle'/>
|
||||
<div className='system-xs-regular whitespace-pre-wrap text-text-destructive'>{error}</div>
|
||||
{
|
||||
!!exceptionCounts && (
|
||||
<>
|
||||
<div className='my-2 h-[0.5px] bg-divider-subtle'/>
|
||||
<div className='system-xs-regular text-text-destructive'>
|
||||
{t('workflow.nodes.common.errorHandle.partialSucceeded.tip', { num: exceptionCounts })}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
{
|
||||
status === 'partial-succeeded' && !!exceptionCounts && (
|
||||
<>
|
||||
<div className='my-2 h-[0.5px] bg-divider-deep'/>
|
||||
<div className='system-xs-medium text-text-warning'>
|
||||
{t('workflow.nodes.common.errorHandle.partialSucceeded.tip', { num: exceptionCounts })}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
status === 'exception' && (
|
||||
<>
|
||||
<div className='my-2 h-[0.5px] bg-divider-deep'/>
|
||||
<div className='system-xs-medium text-text-warning'>
|
||||
{error}
|
||||
<a
|
||||
href={docLink('/guides/workflow/error-handling/error-type')}
|
||||
target='_blank'
|
||||
className='text-text-accent'
|
||||
>
|
||||
{t('workflow.common.learnMore')}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</StatusContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusPanel
|
||||
199
dify/web/app/components/workflow/run/tracing-panel.tsx
Normal file
199
dify/web/app/components/workflow/run/tracing-panel.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import
|
||||
React,
|
||||
{
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiMenu4Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLogs } from './hooks'
|
||||
import NodePanel from './node'
|
||||
import SpecialResultPanel from './special-result-panel'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatNodeList from '@/app/components/workflow/run/utils/format-log'
|
||||
|
||||
type TracingPanelProps = {
|
||||
list: NodeTracing[]
|
||||
className?: string
|
||||
hideNodeInfo?: boolean
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
const TracingPanel: FC<TracingPanelProps> = ({
|
||||
list,
|
||||
className,
|
||||
hideNodeInfo = false,
|
||||
hideNodeProcessDetail = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const treeNodes = formatNodeList(list, t)
|
||||
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(() => new Set())
|
||||
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
|
||||
|
||||
const toggleCollapse = (id: string) => {
|
||||
setCollapsedNodes((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(id))
|
||||
newSet.delete(id)
|
||||
|
||||
else
|
||||
newSet.add(id)
|
||||
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const handleParallelMouseEnter = useCallback((id: string) => {
|
||||
setHoveredParallel(id)
|
||||
}, [])
|
||||
|
||||
const handleParallelMouseLeave = useCallback((e: React.MouseEvent) => {
|
||||
const relatedTarget = e.relatedTarget as Element | null
|
||||
if (relatedTarget && 'closest' in relatedTarget) {
|
||||
const closestParallel = relatedTarget.closest('[data-parallel-id]')
|
||||
if (closestParallel)
|
||||
setHoveredParallel(closestParallel.getAttribute('data-parallel-id'))
|
||||
|
||||
else
|
||||
setHoveredParallel(null)
|
||||
}
|
||||
else {
|
||||
setHoveredParallel(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const {
|
||||
showSpecialResultPanel,
|
||||
|
||||
showRetryDetail,
|
||||
setShowRetryDetailFalse,
|
||||
retryResultList,
|
||||
handleShowRetryResultList,
|
||||
|
||||
showIteratingDetail,
|
||||
setShowIteratingDetailFalse,
|
||||
iterationResultList,
|
||||
iterationResultDurationMap,
|
||||
handleShowIterationResultList,
|
||||
|
||||
showLoopingDetail,
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
loopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
handleShowLoopResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
} = useLogs()
|
||||
|
||||
const renderNode = (node: NodeTracing) => {
|
||||
const isParallelFirstNode = !!node.parallelDetail?.isParallelStartNode
|
||||
if (isParallelFirstNode) {
|
||||
const parallelDetail = node.parallelDetail!
|
||||
const isCollapsed = collapsedNodes.has(node.id)
|
||||
const isHovered = hoveredParallel === node.id
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
className="relative mb-2 ml-4"
|
||||
data-parallel-id={node.id}
|
||||
onMouseEnter={() => handleParallelMouseEnter(node.id)}
|
||||
onMouseLeave={handleParallelMouseLeave}
|
||||
>
|
||||
<div className="mb-1 flex items-center">
|
||||
<button type="button"
|
||||
onClick={() => toggleCollapse(node.id)}
|
||||
className={cn(
|
||||
'mr-2 transition-colors',
|
||||
isHovered ? 'rounded border-components-button-primary-border bg-components-button-primary-bg text-text-primary-on-surface' : 'text-text-secondary hover:text-text-primary',
|
||||
)}
|
||||
>
|
||||
{isHovered ? <RiArrowDownSLine className="h-3 w-3" /> : <RiMenu4Line className="h-3 w-3 text-text-tertiary" />}
|
||||
</button>
|
||||
<div className="system-xs-semibold-uppercase flex items-center text-text-secondary">
|
||||
<span>{parallelDetail.parallelTitle}</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx-2 h-px grow bg-divider-subtle"
|
||||
style={{ background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08), rgba(255, 255, 255, 0)' }}
|
||||
></div>
|
||||
</div>
|
||||
<div className={`relative pl-2 ${isCollapsed ? 'hidden' : ''}`}>
|
||||
<div className={cn(
|
||||
'absolute bottom-0 left-[5px] top-0 w-[2px]',
|
||||
isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
|
||||
)}></div>
|
||||
{parallelDetail.children!.map(renderNode)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
const isHovered = hoveredParallel === node.id
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<div className={cn('system-2xs-medium-uppercase -mb-1.5 pl-4', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}>
|
||||
{node?.parallelDetail?.branchTitle}
|
||||
</div>
|
||||
<NodePanel
|
||||
nodeInfo={node!}
|
||||
allExecutions={list}
|
||||
onShowIterationDetail={handleShowIterationResultList}
|
||||
onShowLoopDetail={handleShowLoopResultList}
|
||||
onShowRetryDetail={handleShowRetryResultList}
|
||||
onShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
hideInfo={hideNodeInfo}
|
||||
hideProcessDetail={hideNodeProcessDetail}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSpecialResultPanel) {
|
||||
return (
|
||||
<SpecialResultPanel
|
||||
showRetryDetail={showRetryDetail}
|
||||
setShowRetryDetailFalse={setShowRetryDetailFalse}
|
||||
retryResultList={retryResultList}
|
||||
|
||||
showIteratingDetail={showIteratingDetail}
|
||||
setShowIteratingDetailFalse={setShowIteratingDetailFalse}
|
||||
iterationResultList={iterationResultList}
|
||||
iterationResultDurationMap={iterationResultDurationMap}
|
||||
|
||||
showLoopingDetail={showLoopingDetail}
|
||||
setShowLoopingDetailFalse={setShowLoopingDetailFalse}
|
||||
loopResultList={loopResultList}
|
||||
loopResultDurationMap={loopResultDurationMap}
|
||||
loopResultVariableMap={loopResultVariableMap}
|
||||
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
agentOrToolLogListMap={agentOrToolLogListMap}
|
||||
handleShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('py-2', className)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}}
|
||||
>
|
||||
{treeNodes.map(renderNode)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TracingPanel
|
||||
@@ -0,0 +1,179 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
export const agentNodeData = (() => {
|
||||
const node = {
|
||||
node_type: BlockEnum.Agent,
|
||||
execution_metadata: {
|
||||
agent_log: [
|
||||
{ id: '1', label: 'Root 1' },
|
||||
{ id: '2', parent_id: '1', label: 'Child 1.2' },
|
||||
{ id: '3', parent_id: '1', label: 'Child 1.3' },
|
||||
{ id: '4', parent_id: '2', label: 'Child 2.4' },
|
||||
{ id: '5', parent_id: '2', label: 'Child 2.5' },
|
||||
{ id: '6', parent_id: '3', label: 'Child 3.6' },
|
||||
{ id: '7', parent_id: '4', label: 'Child 4.7' },
|
||||
{ id: '8', parent_id: '4', label: 'Child 4.8' },
|
||||
{ id: '9', parent_id: '5', label: 'Child 5.9' },
|
||||
{ id: '10', parent_id: '5', label: 'Child 5.10' },
|
||||
{ id: '11', parent_id: '7', label: 'Child 7.11' },
|
||||
{ id: '12', parent_id: '7', label: 'Child 7.12' },
|
||||
{ id: '13', parent_id: '9', label: 'Child 9.13' },
|
||||
{ id: '14', parent_id: '9', label: 'Child 9.14' },
|
||||
{ id: '15', parent_id: '9', label: 'Child 9.15' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
in: [node],
|
||||
expect: [{
|
||||
...node,
|
||||
agentLog: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Root 1',
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
parent_id: '1',
|
||||
label: 'Child 1.2',
|
||||
children: [
|
||||
{
|
||||
id: '4',
|
||||
parent_id: '2',
|
||||
label: 'Child 2.4',
|
||||
children: [
|
||||
{
|
||||
id: '7',
|
||||
parent_id: '4',
|
||||
label: 'Child 4.7',
|
||||
children: [
|
||||
{ id: '11', parent_id: '7', label: 'Child 7.11' },
|
||||
{ id: '12', parent_id: '7', label: 'Child 7.12' },
|
||||
],
|
||||
},
|
||||
{ id: '8', parent_id: '4', label: 'Child 4.8' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
parent_id: '2',
|
||||
label: 'Child 2.5',
|
||||
children: [
|
||||
{
|
||||
id: '9',
|
||||
parent_id: '5',
|
||||
label: 'Child 5.9',
|
||||
children: [
|
||||
{ id: '13', parent_id: '9', label: 'Child 9.13' },
|
||||
{ id: '14', parent_id: '9', label: 'Child 9.14' },
|
||||
{ id: '15', parent_id: '9', label: 'Child 9.15' },
|
||||
],
|
||||
},
|
||||
{ id: '10', parent_id: '5', label: 'Child 5.10' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
parent_id: '1',
|
||||
label: 'Child 1.3',
|
||||
children: [
|
||||
{ id: '6', parent_id: '3', label: 'Child 3.6' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
})()
|
||||
|
||||
export const oneStepCircle = (() => {
|
||||
const node = {
|
||||
node_type: BlockEnum.Agent,
|
||||
execution_metadata: {
|
||||
agent_log: [
|
||||
{ id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
in: [node],
|
||||
expect: [{
|
||||
...node,
|
||||
agentLog: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Node 1',
|
||||
hasCircle: true,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
})()
|
||||
|
||||
export const multiStepsCircle = (() => {
|
||||
const node = {
|
||||
node_type: BlockEnum.Agent,
|
||||
execution_metadata: {
|
||||
agent_log: [
|
||||
// 1 -> [2 -> 4 -> 1, 3]
|
||||
{ id: '1', label: 'Node 1' },
|
||||
{ id: '2', parent_id: '1', label: 'Node 2' },
|
||||
{ id: '3', parent_id: '1', label: 'Node 3' },
|
||||
{ id: '4', parent_id: '2', label: 'Node 4' },
|
||||
|
||||
// Loop
|
||||
{ id: '1', parent_id: '4', label: 'Node 1' },
|
||||
{ id: '2', parent_id: '1', label: 'Node 2' },
|
||||
{ id: '4', parent_id: '2', label: 'Node 4' },
|
||||
{ id: '1', parent_id: '4', label: 'Node 1' },
|
||||
{ id: '2', parent_id: '1', label: 'Node 2' },
|
||||
{ id: '4', parent_id: '2', label: 'Node 4' },
|
||||
],
|
||||
},
|
||||
}
|
||||
// 1 -> [2(4(1(2(4...)))), 3]
|
||||
return {
|
||||
in: [node],
|
||||
expect: [{
|
||||
...node,
|
||||
agentLog: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Node 1',
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
parent_id: '1',
|
||||
label: 'Node 2',
|
||||
children: [
|
||||
{
|
||||
id: '4',
|
||||
parent_id: '2',
|
||||
label: 'Node 4',
|
||||
children: [],
|
||||
hasCircle: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
parent_id: '1',
|
||||
label: 'Node 3',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
})()
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,149 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool]
|
||||
|
||||
const remove = (node: AgentLogItemWithChildren, removeId: string) => {
|
||||
let { children } = node
|
||||
if (!children || children.length === 0)
|
||||
return
|
||||
|
||||
const hasCircle = !!children.find((c) => {
|
||||
const childId = c.message_id || (c as any).id
|
||||
return childId === removeId
|
||||
})
|
||||
if (hasCircle) {
|
||||
node.hasCircle = true
|
||||
node.children = node.children.filter((c) => {
|
||||
const childId = c.message_id || (c as any).id
|
||||
return childId !== removeId
|
||||
})
|
||||
children = node.children
|
||||
}
|
||||
|
||||
children.forEach((child) => {
|
||||
remove(child, removeId)
|
||||
})
|
||||
}
|
||||
|
||||
const removeRepeatedSiblings = (list: AgentLogItemWithChildren[]) => {
|
||||
if (!list || list.length === 0)
|
||||
return []
|
||||
|
||||
const result: AgentLogItemWithChildren[] = []
|
||||
const addedItemIds: string[] = []
|
||||
list.forEach((item) => {
|
||||
const itemId = item.message_id || (item as any).id
|
||||
if (itemId && !addedItemIds.includes(itemId)) {
|
||||
result.push(item)
|
||||
addedItemIds.push(itemId)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const removeCircleLogItem = (log: AgentLogItemWithChildren) => {
|
||||
const newLog = cloneDeep(log)
|
||||
|
||||
// If no children, return as is
|
||||
if (!newLog.children || newLog.children.length === 0)
|
||||
return newLog
|
||||
|
||||
newLog.children = removeRepeatedSiblings(newLog.children)
|
||||
const id = newLog.message_id || (newLog as any).id
|
||||
let { children } = newLog
|
||||
|
||||
// check one step circle
|
||||
const hasOneStepCircle = !!children.find((c) => {
|
||||
const childId = c.message_id || (c as any).id
|
||||
return childId === id
|
||||
})
|
||||
if (hasOneStepCircle) {
|
||||
newLog.hasCircle = true
|
||||
newLog.children = newLog.children.filter((c) => {
|
||||
const childId = c.message_id || (c as any).id
|
||||
return childId !== id
|
||||
})
|
||||
children = newLog.children
|
||||
}
|
||||
|
||||
children.forEach((child, index) => {
|
||||
remove(child, id) // check multi steps circle
|
||||
children[index] = removeCircleLogItem(child)
|
||||
})
|
||||
return newLog
|
||||
}
|
||||
|
||||
const listToTree = (logs: AgentLogItem[]) => {
|
||||
if (!logs || logs.length === 0)
|
||||
return []
|
||||
|
||||
// First pass: identify all unique items and track parent-child relationships
|
||||
const itemsById = new Map<string, any>()
|
||||
const childrenById = new Map<string, any[]>()
|
||||
|
||||
logs.forEach((item) => {
|
||||
const itemId = item.message_id || (item as any).id
|
||||
|
||||
// Only add to itemsById if not already there (keep first occurrence)
|
||||
if (itemId && !itemsById.has(itemId))
|
||||
itemsById.set(itemId, item)
|
||||
|
||||
// Initialize children array for this ID if needed
|
||||
if (itemId && !childrenById.has(itemId))
|
||||
childrenById.set(itemId, [])
|
||||
|
||||
// If this item has a parent, add it to parent's children list
|
||||
if (item.parent_id) {
|
||||
if (!childrenById.has(item.parent_id))
|
||||
childrenById.set(item.parent_id, [])
|
||||
|
||||
childrenById.get(item.parent_id)!.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
// Second pass: build tree structure
|
||||
const tree: AgentLogItemWithChildren[] = []
|
||||
|
||||
// Find root nodes (items without parents)
|
||||
itemsById.forEach((item) => {
|
||||
const hasParent = !!item.parent_id
|
||||
if (!hasParent) {
|
||||
const itemId = item.message_id || (item as any).id
|
||||
const children = childrenById.get(itemId)
|
||||
if (children && children.length > 0)
|
||||
item.children = children
|
||||
|
||||
tree.push(item as AgentLogItemWithChildren)
|
||||
}
|
||||
})
|
||||
|
||||
// Add children property to all items that have children
|
||||
itemsById.forEach((item) => {
|
||||
const itemId = item.message_id || (item as any).id
|
||||
const children = childrenById.get(itemId)
|
||||
if (children && children.length > 0)
|
||||
item.children = children
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
const format = (list: NodeTracing[]): NodeTracing[] => {
|
||||
const result: NodeTracing[] = list.map((item) => {
|
||||
let treeList: AgentLogItemWithChildren[] = []
|
||||
let removedCircleTree: AgentLogItemWithChildren[] = []
|
||||
if (supportedAgentLogNodes.includes(item.node_type) && item.execution_metadata?.agent_log && item.execution_metadata?.agent_log.length > 0)
|
||||
treeList = listToTree(item.execution_metadata.agent_log)
|
||||
// console.log(JSON.stringify(treeList))
|
||||
removedCircleTree = treeList.length > 0 ? treeList.map(t => removeCircleLogItem(t)) : []
|
||||
item.agentLog = removedCircleTree
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default format
|
||||
@@ -0,0 +1,138 @@
|
||||
import parseDSL from './graph-to-log-struct'
|
||||
|
||||
describe('parseDSL', () => {
|
||||
it('should parse plain nodes correctly', () => {
|
||||
const dsl = 'plainNode1 -> plainNode2'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: {}, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse retry nodes correctly', () => {
|
||||
const dsl = '(retry, retryNode, 3)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse iteration nodes correctly', () => {
|
||||
const dsl = '(iteration, iterationNode, plainNode1 -> plainNode2)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'iterationNode', node_id: 'iterationNode', title: 'iterationNode', node_type: 'iteration', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0 }, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0 }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse loop nodes correctly', () => {
|
||||
const dsl = '(loop, loopNode, plainNode1 -> plainNode2)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'loopNode', node_id: 'loopNode', title: 'loopNode', node_type: 'loop', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { loop_id: 'loopNode', loop_index: 0 }, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { loop_id: 'loopNode', loop_index: 0 }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse parallel nodes correctly', () => {
|
||||
const dsl = '(parallel, parallelNode, nodeA, nodeB -> nodeC)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'parallelNode', node_id: 'parallelNode', title: 'parallelNode', execution_metadata: { parallel_id: 'parallelNode' }, status: 'succeeded' },
|
||||
{ id: 'nodeA', node_id: 'nodeA', title: 'nodeA', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeA' }, status: 'succeeded' },
|
||||
{ id: 'nodeB', node_id: 'nodeB', title: 'nodeB', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeB' }, status: 'succeeded' },
|
||||
{ id: 'nodeC', node_id: 'nodeC', title: 'nodeC', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeB' }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
// TODO
|
||||
// it('should handle nested parallel nodes', () => {
|
||||
// const dsl = '(parallel, outerParallel, (parallel, innerParallel, plainNode1 -> plainNode2) -> plainNode3)'
|
||||
// const result = parseDSL(dsl)
|
||||
// expect(result).toEqual([
|
||||
// {
|
||||
// id: 'outerParallel',
|
||||
// node_id: 'outerParallel',
|
||||
// title: 'outerParallel',
|
||||
// execution_metadata: { parallel_id: 'outerParallel' },
|
||||
// status: 'succeeded',
|
||||
// },
|
||||
// {
|
||||
// id: 'innerParallel',
|
||||
// node_id: 'innerParallel',
|
||||
// title: 'innerParallel',
|
||||
// execution_metadata: { parallel_id: 'outerParallel', parallel_start_node_id: 'innerParallel' },
|
||||
// status: 'succeeded',
|
||||
// },
|
||||
// {
|
||||
// id: 'plainNode1',
|
||||
// node_id: 'plainNode1',
|
||||
// title: 'plainNode1',
|
||||
// execution_metadata: {
|
||||
// parallel_id: 'innerParallel',
|
||||
// parallel_start_node_id: 'plainNode1',
|
||||
// parent_parallel_id: 'outerParallel',
|
||||
// parent_parallel_start_node_id: 'innerParallel',
|
||||
// },
|
||||
// status: 'succeeded',
|
||||
// },
|
||||
// {
|
||||
// id: 'plainNode2',
|
||||
// node_id: 'plainNode2',
|
||||
// title: 'plainNode2',
|
||||
// execution_metadata: {
|
||||
// parallel_id: 'innerParallel',
|
||||
// parallel_start_node_id: 'plainNode1',
|
||||
// parent_parallel_id: 'outerParallel',
|
||||
// parent_parallel_start_node_id: 'innerParallel',
|
||||
// },
|
||||
// status: 'succeeded',
|
||||
// },
|
||||
// {
|
||||
// id: 'plainNode3',
|
||||
// node_id: 'plainNode3',
|
||||
// title: 'plainNode3',
|
||||
// execution_metadata: {
|
||||
// parallel_id: 'outerParallel',
|
||||
// parallel_start_node_id: 'innerParallel',
|
||||
// },
|
||||
// status: 'succeeded',
|
||||
// },
|
||||
// ])
|
||||
// })
|
||||
|
||||
// iterations not support nested iterations
|
||||
// it('should handle nested iterations', () => {
|
||||
// const dsl = '(iteration, outerIteration, (iteration, innerIteration -> plainNode1 -> plainNode2))'
|
||||
// const result = parseDSL(dsl)
|
||||
// expect(result).toEqual([
|
||||
// { id: 'outerIteration', node_id: 'outerIteration', title: 'outerIteration', node_type: 'iteration', execution_metadata: {}, status: 'succeeded' },
|
||||
// { id: 'innerIteration', node_id: 'innerIteration', title: 'innerIteration', node_type: 'iteration', execution_metadata: { iteration_id: 'outerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// { id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'innerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// { id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'innerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// ])
|
||||
// })
|
||||
|
||||
// it('should handle nested iterations within parallel nodes', () => {
|
||||
// const dsl = '(parallel, parallelNode, (iteration, iterationNode, plainNode1, plainNode2))'
|
||||
// const result = parseDSL(dsl)
|
||||
// expect(result).toEqual([
|
||||
// { id: 'parallelNode', node_id: 'parallelNode', title: 'parallelNode', execution_metadata: { parallel_id: 'parallelNode' }, status: 'succeeded' },
|
||||
// { id: 'iterationNode', node_id: 'iterationNode', title: 'iterationNode', node_type: 'iteration', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
// { id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0, parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
// { id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0, parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
// ])
|
||||
// })
|
||||
|
||||
it('should throw an error for unknown node types', () => {
|
||||
const dsl = '(unknown, nodeId)'
|
||||
expect(() => parseDSL(dsl)).toThrowError('Unknown nodeType: unknown')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,356 @@
|
||||
type IterationInfo = { iterationId: string; iterationIndex: number }
|
||||
type LoopInfo = { loopId: string; loopIndex: number }
|
||||
type NodePlain = { nodeType: 'plain'; nodeId: string; } & (Partial<IterationInfo> & Partial<LoopInfo>)
|
||||
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & (Partial<IterationInfo> & Partial<LoopInfo>)) | Node[] | number)[] } & (Partial<IterationInfo> & Partial<LoopInfo>)
|
||||
type Node = NodePlain | NodeComplex
|
||||
|
||||
/**
|
||||
* Parses a DSL string into an array of node objects.
|
||||
* @param dsl - The input DSL string.
|
||||
* @returns An array of parsed nodes.
|
||||
*/
|
||||
function parseDSL(dsl: string): NodeData[] {
|
||||
return convertToNodeData(parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a top-level flow string by "->", respecting nested structures.
|
||||
* @param dsl - The DSL string to split.
|
||||
* @returns An array of top-level segments.
|
||||
*/
|
||||
function parseTopLevelFlow(dsl: string): string[] {
|
||||
const segments: string[] = []
|
||||
let buffer = ''
|
||||
let nested = 0
|
||||
|
||||
for (let i = 0; i < dsl.length; i++) {
|
||||
const char = dsl[i]
|
||||
if (char === '(') nested++
|
||||
if (char === ')') nested--
|
||||
if (char === '-' && dsl[i + 1] === '>' && nested === 0) {
|
||||
segments.push(buffer.trim())
|
||||
buffer = ''
|
||||
i++ // Skip the ">" character
|
||||
}
|
||||
else {
|
||||
buffer += char
|
||||
}
|
||||
}
|
||||
if (buffer.trim())
|
||||
segments.push(buffer.trim())
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single node string.
|
||||
* If the node is complex (e.g., has parentheses), it extracts the node type, node ID, and parameters.
|
||||
* @param nodeStr - The node string to parse.
|
||||
* @param parentIterationId - The ID of the parent iteration node (if applicable).
|
||||
* @param parentLoopId - The ID of the parent loop node (if applicable).
|
||||
* @returns A parsed node object.
|
||||
*/
|
||||
function parseNode(nodeStr: string, parentIterationId?: string, parentLoopId?: string): Node {
|
||||
// Check if the node is a complex node
|
||||
if (nodeStr.startsWith('(') && nodeStr.endsWith(')')) {
|
||||
const innerContent = nodeStr.slice(1, -1).trim() // Remove outer parentheses
|
||||
let nested = 0
|
||||
let buffer = ''
|
||||
const parts: string[] = []
|
||||
|
||||
// Split the inner content by commas, respecting nested parentheses
|
||||
for (let i = 0; i < innerContent.length; i++) {
|
||||
const char = innerContent[i]
|
||||
if (char === '(') nested++
|
||||
if (char === ')') nested--
|
||||
|
||||
if (char === ',' && nested === 0) {
|
||||
parts.push(buffer.trim())
|
||||
buffer = ''
|
||||
}
|
||||
else {
|
||||
buffer += char
|
||||
}
|
||||
}
|
||||
parts.push(buffer.trim())
|
||||
|
||||
// Extract nodeType, nodeId, and params
|
||||
const [nodeType, nodeId, ...paramsRaw] = parts
|
||||
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId, nodeType === 'loop' ? nodeId.trim() : parentLoopId)
|
||||
const complexNode = {
|
||||
nodeType: nodeType.trim(),
|
||||
nodeId: nodeId.trim(),
|
||||
params,
|
||||
}
|
||||
if (parentIterationId) {
|
||||
(complexNode as any).iterationId = parentIterationId;
|
||||
(complexNode as any).iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
if (parentLoopId) {
|
||||
(complexNode as any).loopId = parentLoopId;
|
||||
(complexNode as any).loopIndex = 0 // Fixed as 0
|
||||
}
|
||||
return complexNode
|
||||
}
|
||||
|
||||
// If it's not a complex node, treat it as a plain node
|
||||
const plainNode: NodePlain = { nodeType: 'plain', nodeId: nodeStr.trim() }
|
||||
if (parentIterationId) {
|
||||
plainNode.iterationId = parentIterationId
|
||||
plainNode.iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
if (parentLoopId) {
|
||||
plainNode.loopId = parentLoopId
|
||||
plainNode.loopIndex = 0 // Fixed as 0
|
||||
}
|
||||
return plainNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses parameters of a complex node.
|
||||
* Supports nested flows and complex sub-nodes.
|
||||
* Adds iteration-specific metadata recursively.
|
||||
* @param paramParts - The parameters string split by commas.
|
||||
* @param parentIterationId - The ID of the parent iteration node (if applicable).
|
||||
* @param parentLoopId - The ID of the parent loop node (if applicable).
|
||||
* @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
|
||||
*/
|
||||
function parseParams(paramParts: string[], parentIteration?: string, parentLoopId?: string): (Node | Node[] | number)[] {
|
||||
return paramParts.map((part) => {
|
||||
if (part.includes('->')) {
|
||||
// Parse as a flow and return an array of nodes
|
||||
return parseTopLevelFlow(part).map(node => parseNode(node, parentIteration || undefined, parentLoopId || undefined))
|
||||
}
|
||||
else if (part.startsWith('(')) {
|
||||
// Parse as a nested complex node
|
||||
return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
|
||||
}
|
||||
else if (!Number.isNaN(Number(part.trim()))) {
|
||||
// Parse as a numeric parameter
|
||||
return Number(part.trim())
|
||||
}
|
||||
else {
|
||||
// Parse as a plain node
|
||||
return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type NodeData = {
|
||||
id: string;
|
||||
node_id: string;
|
||||
title: string;
|
||||
node_type?: string;
|
||||
execution_metadata: Record<string, any>;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a plain node to node data.
|
||||
*/
|
||||
function convertPlainNode(node: Node): NodeData[] {
|
||||
return [
|
||||
{
|
||||
id: node.nodeId,
|
||||
node_id: node.nodeId,
|
||||
title: node.nodeId,
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a retry node to node data.
|
||||
*/
|
||||
function convertRetryNode(node: Node): NodeData[] {
|
||||
const { nodeId, iterationId, iterationIndex, loopId, loopIndex, params } = node as NodeComplex
|
||||
const retryCount = params ? Number.parseInt(params[0] as unknown as string, 10) : 0
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
|
||||
for (let i = 0; i < retryCount; i++) {
|
||||
result.push({
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: iterationId ? {
|
||||
iteration_id: iterationId,
|
||||
iteration_index: iterationIndex || 0,
|
||||
} : loopId ? {
|
||||
loop_id: loopId,
|
||||
loop_index: loopIndex || 0,
|
||||
} : {},
|
||||
status: 'retry',
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an iteration node to node data.
|
||||
*/
|
||||
function convertIterationNode(node: Node): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
node_type: 'iteration',
|
||||
status: 'succeeded',
|
||||
execution_metadata: {},
|
||||
},
|
||||
]
|
||||
|
||||
params?.forEach((param: any) => {
|
||||
if (Array.isArray(param)) {
|
||||
param.forEach((childNode: Node) => {
|
||||
const childData = convertToNodeData([childNode])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
iteration_id: nodeId,
|
||||
iteration_index: 0,
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an loop node to node data.
|
||||
*/
|
||||
function convertLoopNode(node: Node): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
node_type: 'loop',
|
||||
status: 'succeeded',
|
||||
execution_metadata: {},
|
||||
},
|
||||
]
|
||||
|
||||
params?.forEach((param: any) => {
|
||||
if (Array.isArray(param)) {
|
||||
param.forEach((childNode: Node) => {
|
||||
const childData = convertToNodeData([childNode])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
loop_id: nodeId,
|
||||
loop_index: 0,
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parallel node to node data.
|
||||
*/
|
||||
function convertParallelNode(node: Node, parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: {
|
||||
parallel_id: nodeId,
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
|
||||
params?.forEach((param) => {
|
||||
if (Array.isArray(param)) {
|
||||
const startNodeId = param[0]?.nodeId
|
||||
param.forEach((childNode: Node) => {
|
||||
const childData = convertToNodeData([childNode])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
parallel_id: nodeId,
|
||||
parallel_start_node_id: startNodeId,
|
||||
...(parentParallelId && {
|
||||
parent_parallel_id: parentParallelId,
|
||||
parent_parallel_start_node_id: parentStartNodeId,
|
||||
}),
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
else if (param && typeof param === 'object') {
|
||||
const startNodeId = param.nodeId
|
||||
const childData = convertToNodeData([param])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
parallel_id: nodeId,
|
||||
parallel_start_node_id: startNodeId,
|
||||
...(parentParallelId && {
|
||||
parent_parallel_id: parentParallelId,
|
||||
parent_parallel_start_node_id: parentStartNodeId,
|
||||
}),
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to convert nodes to node data.
|
||||
*/
|
||||
function convertToNodeData(nodes: Node[], parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
|
||||
const result: NodeData[] = []
|
||||
|
||||
nodes.forEach((node) => {
|
||||
switch (node.nodeType) {
|
||||
case 'plain':
|
||||
result.push(...convertPlainNode(node))
|
||||
break
|
||||
case 'retry':
|
||||
result.push(...convertRetryNode(node))
|
||||
break
|
||||
case 'iteration':
|
||||
result.push(...convertIterationNode(node))
|
||||
break
|
||||
case 'loop':
|
||||
result.push(...convertLoopNode(node))
|
||||
break
|
||||
case 'parallel':
|
||||
result.push(...convertParallelNode(node, parentParallelId, parentStartNodeId))
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown nodeType: ${node.nodeType}`)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default parseDSL
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { addChildrenToIterationNode } from './iteration'
|
||||
import { addChildrenToLoopNode } from './loop'
|
||||
import formatParallelNode from './parallel'
|
||||
import formatRetryNode from './retry'
|
||||
import formatAgentNode from './agent'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { BlockEnum } from '../../../types'
|
||||
|
||||
const formatIterationAndLoopNode = (list: NodeTracing[], t: any) => {
|
||||
const clonedList = cloneDeep(list)
|
||||
|
||||
// Identify all loop and iteration nodes
|
||||
const loopNodeIds = clonedList
|
||||
.filter(item => item.node_type === BlockEnum.Loop)
|
||||
.map(item => item.node_id)
|
||||
|
||||
const iterationNodeIds = clonedList
|
||||
.filter(item => item.node_type === BlockEnum.Iteration)
|
||||
.map(item => item.node_id)
|
||||
|
||||
// Identify all child nodes for both loop and iteration
|
||||
const loopChildrenNodeIds = clonedList
|
||||
.filter(item => item.execution_metadata?.loop_id && loopNodeIds.includes(item.execution_metadata.loop_id))
|
||||
.map(item => item.node_id)
|
||||
|
||||
const iterationChildrenNodeIds = clonedList
|
||||
.filter(item => item.execution_metadata?.iteration_id && iterationNodeIds.includes(item.execution_metadata.iteration_id))
|
||||
.map(item => item.node_id)
|
||||
|
||||
// Filter out child nodes as they will be included in their parent nodes
|
||||
const result = clonedList
|
||||
.filter(item => !loopChildrenNodeIds.includes(item.node_id) && !iterationChildrenNodeIds.includes(item.node_id))
|
||||
.map((item) => {
|
||||
// Process Loop nodes
|
||||
if (item.node_type === BlockEnum.Loop) {
|
||||
const childrenNodes = clonedList.filter(child => child.execution_metadata?.loop_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToLoopNode(item, childrenNodes)
|
||||
|
||||
// Handle parallel nodes in loop node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
// Process Iteration nodes
|
||||
if (item.node_type === BlockEnum.Iteration) {
|
||||
const childrenNodes = clonedList.filter(child => child.execution_metadata?.iteration_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToIterationNode(item, childrenNodes)
|
||||
|
||||
// Handle parallel nodes in iteration node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
|
||||
const allItems = cloneDeep([...list]).sort((a, b) => a.index - b.index)
|
||||
/*
|
||||
* First handle not change list structure node
|
||||
* Because Handle struct node will put the node in different
|
||||
*/
|
||||
const formattedAgentList = formatAgentNode(allItems)
|
||||
const formattedRetryList = formatRetryNode(formattedAgentList) // retry one node
|
||||
// would change the structure of the list. Iteration and parallel can include each other.
|
||||
const formattedLoopAndIterationList = formatIterationAndLoopNode(formattedRetryList, t)
|
||||
const formattedParallelList = formatParallelNode(formattedLoopAndIterationList, t)
|
||||
|
||||
const result = formattedParallelList
|
||||
// console.log(allItems)
|
||||
// console.log(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default formatToTracingNodeList
|
||||
@@ -0,0 +1,23 @@
|
||||
import format from '.'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
describe('iteration', () => {
|
||||
const list = graphToLogStruct('start -> (iteration, iterationNode, plainNode1 -> plainNode2)')
|
||||
// const [startNode, iterationNode, ...iterations] = list
|
||||
const result = format(list as any, noop)
|
||||
test('result should have no nodes in iteration node', () => {
|
||||
expect((result as any).find((item: any) => !!item.execution_metadata?.iteration_id)).toBeUndefined()
|
||||
})
|
||||
// test('iteration should put nodes in details', () => {
|
||||
// expect(result as any).toEqual([
|
||||
// startNode,
|
||||
// {
|
||||
// ...iterationNode,
|
||||
// details: [
|
||||
// [iterations[0], iterations[1]],
|
||||
// ],
|
||||
// },
|
||||
// ])
|
||||
// })
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatParallelNode from '../parallel'
|
||||
|
||||
export function addChildrenToIterationNode(iterationNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
const details: NodeTracing[][] = []
|
||||
childrenNodes.forEach((item, index) => {
|
||||
if (!item.execution_metadata) return
|
||||
const { iteration_index = 0 } = item.execution_metadata
|
||||
const runIndex: number = iteration_index !== undefined ? iteration_index : index
|
||||
if (!details[runIndex])
|
||||
details[runIndex] = []
|
||||
|
||||
details[runIndex].push(item)
|
||||
})
|
||||
return {
|
||||
...iterationNode,
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
||||
const format = (list: NodeTracing[], t: any): NodeTracing[] => {
|
||||
const iterationNodeIds = list
|
||||
.filter(item => item.node_type === BlockEnum.Iteration)
|
||||
.map(item => item.node_id)
|
||||
const iterationChildrenNodeIds = list
|
||||
.filter(item => item.execution_metadata?.iteration_id && iterationNodeIds.includes(item.execution_metadata.iteration_id))
|
||||
.map(item => item.node_id)
|
||||
// move iteration children nodes to iteration node's details field
|
||||
const result = list
|
||||
.filter(item => !iterationChildrenNodeIds.includes(item.node_id))
|
||||
.map((item) => {
|
||||
if (item.node_type === BlockEnum.Iteration) {
|
||||
const childrenNodes = list.filter(child => child.execution_metadata?.iteration_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToIterationNode(item, childrenNodes)
|
||||
// handle parallel node in iteration node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default format
|
||||
@@ -0,0 +1,23 @@
|
||||
import format from '.'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
describe('loop', () => {
|
||||
const list = graphToLogStruct('start -> (loop, loopNode, plainNode1 -> plainNode2)')
|
||||
const [startNode, loopNode, ...loops] = list
|
||||
const result = format(list as any, noop)
|
||||
test('result should have no nodes in loop node', () => {
|
||||
expect(result.find(item => !!item.execution_metadata?.loop_id)).toBeUndefined()
|
||||
})
|
||||
test('loop should put nodes in details', () => {
|
||||
expect(result).toEqual([
|
||||
startNode,
|
||||
{
|
||||
...loopNode,
|
||||
details: [
|
||||
[loops[0], loops[1]],
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatParallelNode from '../parallel'
|
||||
|
||||
export function addChildrenToLoopNode(loopNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
const details: NodeTracing[][] = []
|
||||
childrenNodes.forEach((item) => {
|
||||
if (!item.execution_metadata) return
|
||||
const { parallel_mode_run_id, loop_index = 0 } = item.execution_metadata
|
||||
const runIndex: number = (parallel_mode_run_id || loop_index) as number
|
||||
if (!details[runIndex])
|
||||
details[runIndex] = []
|
||||
|
||||
details[runIndex].push(item)
|
||||
})
|
||||
return {
|
||||
...loopNode,
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
||||
const format = (list: NodeTracing[], t: any): NodeTracing[] => {
|
||||
const loopNodeIds = list
|
||||
.filter(item => item.node_type === BlockEnum.Loop)
|
||||
.map(item => item.node_id)
|
||||
const loopChildrenNodeIds = list
|
||||
.filter(item => item.execution_metadata?.loop_id && loopNodeIds.includes(item.execution_metadata.loop_id))
|
||||
.map(item => item.node_id)
|
||||
// move loop children nodes to loop node's details field
|
||||
const result = list
|
||||
.filter(item => !loopChildrenNodeIds.includes(item.node_id))
|
||||
.map((item) => {
|
||||
if (item.node_type === BlockEnum.Loop) {
|
||||
const childrenNodes = list.filter(child => child.execution_metadata?.loop_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToLoopNode(item, childrenNodes)
|
||||
// handle parallel node in loop node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default format
|
||||
@@ -0,0 +1,175 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
function printNodeStructure(node: NodeTracing, depth: number) {
|
||||
const indent = ' '.repeat(depth)
|
||||
console.log(`${indent}${node.title}`)
|
||||
if (node.parallelDetail?.children) {
|
||||
node.parallelDetail.children.forEach((child) => {
|
||||
printNodeStructure(child, depth + 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addTitle({
|
||||
list, depth, belongParallelIndexInfo,
|
||||
}: {
|
||||
list: NodeTracing[],
|
||||
depth: number,
|
||||
belongParallelIndexInfo?: string,
|
||||
}, t: any) {
|
||||
let branchIndex = 0
|
||||
const hasMoreThanOneParallel = list.filter(node => node.parallelDetail?.isParallelStartNode).length > 1
|
||||
list.forEach((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return
|
||||
|
||||
const isParallelStartNode = node.parallelDetail?.isParallelStartNode
|
||||
|
||||
const parallelIndexLetter = (() => {
|
||||
if (!isParallelStartNode || !hasMoreThanOneParallel)
|
||||
return ''
|
||||
|
||||
const index = 1 + list.filter(node => node.parallelDetail?.isParallelStartNode).findIndex(item => item.node_id === node.node_id)
|
||||
return String.fromCharCode(64 + index)
|
||||
})()
|
||||
|
||||
const parallelIndexInfo = `${depth}${parallelIndexLetter}`
|
||||
|
||||
if (isParallelStartNode) {
|
||||
node.parallelDetail!.isParallelStartNode = true
|
||||
node.parallelDetail!.parallelTitle = `${t('workflow.common.parallel')}-${parallelIndexInfo}`
|
||||
}
|
||||
|
||||
const isBrachStartNode = parallel_start_node_id === node.node_id
|
||||
if (isBrachStartNode) {
|
||||
branchIndex++
|
||||
const branchLetter = String.fromCharCode(64 + branchIndex)
|
||||
if (!node.parallelDetail) {
|
||||
node.parallelDetail = {
|
||||
branchTitle: '',
|
||||
}
|
||||
}
|
||||
|
||||
node.parallelDetail!.branchTitle = `${t('workflow.common.branch')}-${belongParallelIndexInfo}-${branchLetter}`
|
||||
}
|
||||
|
||||
if (node.parallelDetail?.children && node.parallelDetail.children.length > 0) {
|
||||
addTitle({
|
||||
list: node.parallelDetail.children,
|
||||
depth: depth + 1,
|
||||
belongParallelIndexInfo: parallelIndexInfo,
|
||||
}, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// list => group by parallel_id(parallel tree).
|
||||
const format = (list: NodeTracing[], t: any, isPrint?: boolean): NodeTracing[] => {
|
||||
if (isPrint)
|
||||
console.log(list)
|
||||
|
||||
const result: NodeTracing[] = [...list]
|
||||
// list to tree by parent_parallel_start_node_id and branch by parallel_start_node_id. Each parallel may has more than one branch.
|
||||
result.forEach((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const branchStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
const parentParallelBranchStartNodeId = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return
|
||||
|
||||
const isParallelStartNode = parallel_start_node_id === node.node_id // in the same parallel has more than one start node
|
||||
if (isParallelStartNode) {
|
||||
const selfNode = { ...node, parallelDetail: undefined }
|
||||
node.parallelDetail = {
|
||||
isParallelStartNode: true,
|
||||
children: [selfNode],
|
||||
}
|
||||
const isRootLevel = !parent_parallel_id
|
||||
if (isRootLevel)
|
||||
return
|
||||
|
||||
const parentParallelStartNode = result.find(item => item.node_id === parentParallelBranchStartNodeId)
|
||||
// append to parent parallel start node and after the same branch
|
||||
if (parentParallelStartNode) {
|
||||
if (!parentParallelStartNode?.parallelDetail) {
|
||||
parentParallelStartNode!.parallelDetail = {
|
||||
children: [],
|
||||
}
|
||||
}
|
||||
if (parentParallelStartNode!.parallelDetail.children) {
|
||||
const sameBranchNodesLastIndex = parentParallelStartNode.parallelDetail.children.findLastIndex((node) => {
|
||||
const currStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
return currStartNodeId === parentParallelBranchStartNodeId
|
||||
})
|
||||
if (sameBranchNodesLastIndex !== -1)
|
||||
parentParallelStartNode!.parallelDetail.children.splice(sameBranchNodesLastIndex + 1, 0, node)
|
||||
else
|
||||
parentParallelStartNode!.parallelDetail.children.push(node)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// append to parallel start node and after the same branch
|
||||
const parallelStartNode = result.find(item => parallel_start_node_id === item.node_id)
|
||||
|
||||
if (parallelStartNode && parallelStartNode.parallelDetail && parallelStartNode!.parallelDetail!.children) {
|
||||
const sameBranchNodesLastIndex = parallelStartNode.parallelDetail.children.findLastIndex((node) => {
|
||||
const currStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
return currStartNodeId === branchStartNodeId
|
||||
})
|
||||
if (sameBranchNodesLastIndex !== -1) {
|
||||
parallelStartNode.parallelDetail.children.splice(sameBranchNodesLastIndex + 1, 0, node)
|
||||
}
|
||||
else { // new branch
|
||||
parallelStartNode.parallelDetail.children.push(node)
|
||||
}
|
||||
}
|
||||
// parallelStartNode!.parallelDetail!.children.push(node)
|
||||
})
|
||||
|
||||
const filteredInParallelSubNodes = result.filter((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return true
|
||||
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
|
||||
if (parent_parallel_id)
|
||||
return false
|
||||
|
||||
const isParallelStartNode = node.parallelDetail?.isParallelStartNode
|
||||
|
||||
if (!isParallelStartNode)
|
||||
return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// print node structure for debug
|
||||
if (isPrint) {
|
||||
filteredInParallelSubNodes.forEach((node) => {
|
||||
const now = Date.now()
|
||||
console.log(`----- p: ${now} start -----`)
|
||||
printNodeStructure(node, 0)
|
||||
console.log(`----- p: ${now} end -----`)
|
||||
})
|
||||
}
|
||||
|
||||
addTitle({
|
||||
list: filteredInParallelSubNodes,
|
||||
depth: 1,
|
||||
}, t)
|
||||
|
||||
return filteredInParallelSubNodes
|
||||
}
|
||||
export default format
|
||||
@@ -0,0 +1,21 @@
|
||||
import format from '.'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
|
||||
describe('retry', () => {
|
||||
// retry nodeId:1 3 times.
|
||||
const steps = graphToLogStruct('start -> (retry, retryNode, 3)')
|
||||
const [startNode, retryNode, ...retryDetail] = steps
|
||||
const result = format(steps as any)
|
||||
test('should have no retry status nodes', () => {
|
||||
expect(result.find(item => item.status === 'retry')).toBeUndefined()
|
||||
})
|
||||
test('should put retry nodes in retryDetail', () => {
|
||||
expect(result).toEqual([
|
||||
startNode,
|
||||
{
|
||||
...retryNode,
|
||||
retryDetail,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
const format = (list: NodeTracing[]): NodeTracing[] => {
|
||||
const retryNodes = list.filter((item) => {
|
||||
return item.status === 'retry'
|
||||
})
|
||||
|
||||
const retryNodeIds = retryNodes.map(item => item.node_id)
|
||||
// move retry nodes to retryDetail
|
||||
const result = list.filter((item) => {
|
||||
return item.status !== 'retry'
|
||||
}).map((item) => {
|
||||
const { execution_metadata } = item
|
||||
const isInIteration = !!execution_metadata?.iteration_id
|
||||
const isInLoop = !!execution_metadata?.loop_id
|
||||
const nodeId = item.node_id
|
||||
const isRetryBelongNode = retryNodeIds.includes(nodeId)
|
||||
|
||||
if (isRetryBelongNode) {
|
||||
return {
|
||||
...item,
|
||||
retryDetail: retryNodes.filter((node) => {
|
||||
if (!isInIteration && !isInLoop)
|
||||
return node.node_id === nodeId
|
||||
|
||||
// retry node in iteration
|
||||
if (isInIteration)
|
||||
return node.node_id === nodeId && node.execution_metadata?.iteration_index === execution_metadata?.iteration_index
|
||||
|
||||
// retry node in loop
|
||||
if (isInLoop)
|
||||
return node.node_id === nodeId && node.execution_metadata?.loop_index === execution_metadata?.loop_index
|
||||
|
||||
return false
|
||||
}),
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export default format
|
||||
Reference in New Issue
Block a user