From 091c480547e772873de77d7838edc07bac03d539 Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Sat, 28 Jun 2025 15:33:25 +0800 Subject: [PATCH 1/2] feat(workflow): add node position indicator and prevent event propagation Add node position indicator to workflow run panel when not in publish mode. Also prevent event propagation when clicking the position indicator to avoid unintended panel toggling. --- .../nodes/_base/components/node-position.tsx | 3 +- web/app/components/workflow/run/node.tsx | 560 +++++++++--------- 2 files changed, 295 insertions(+), 268 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/node-position.tsx b/web/app/components/workflow/nodes/_base/components/node-position.tsx index 404648dfa6..6f84be989b 100644 --- a/web/app/components/workflow/nodes/_base/components/node-position.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-position.tsx @@ -36,7 +36,8 @@ const NodePosition = ({ >
{ + onClick={(e) => { + e.stopPropagation() setViewport({ x: (clientWidth - 400 - nodeWidth * zoom) / 2 - nodePosition.x * zoom, y: (clientHeight - nodeHeight * zoom) / 2 - nodePosition.y * zoom, diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index a4df5f4c74..bebceb68ed 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -1,267 +1,293 @@ -'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' - -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 = ({ - className, - nodeInfo, - allExecutions, - inMessage = false, - hideInfo = false, - hideProcessDetail, - onShowIterationDetail, - onShowLoopDetail, - onShowRetryDetail, - onShowAgentOrToolLog, - notShowIterationNav, - notShowLoopNav, -}) => { - const [collapseState, doSetCollapseState] = useState(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 `${Number.parseInt(Math.round(time / 60).toString())} 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 ( -
-
-
setCollapseState(!collapseState)} - > - {!hideProcessDetail && ( - - )} - - {nodeInfo.title}
- } - > -
{nodeInfo.title}
- - {nodeInfo.status !== 'running' && !hideInfo && ( -
{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}
- )} - {nodeInfo.status === 'succeeded' && ( - - )} - {nodeInfo.status === 'failed' && ( - - )} - {nodeInfo.status === 'stopped' && ( - - )} - {nodeInfo.status === 'exception' && ( - - )} - {nodeInfo.status === 'running' && ( -
- Running - -
- )} -
- {!collapseState && !hideProcessDetail && ( -
- {/* The nav to the iteration detail */} - {isIterationNode && !notShowIterationNav && onShowIterationDetail && ( - - )} - {/* The nav to the Loop detail */} - {isLoopNode && !notShowLoopNav && onShowLoopDetail && ( - - )} - {isRetryNode && onShowRetryDetail && ( - - )} - { - (isAgentNode || isToolNode) && onShowAgentOrToolLog && ( - - ) - } -
- {(nodeInfo.status === 'stopped') && ( - - {t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })} - - )} - {(nodeInfo.status === 'exception') && ( - - {nodeInfo.error} - - {t('workflow.common.learnMore')} - - - )} - {nodeInfo.status === 'failed' && ( - - {nodeInfo.error} - - )} - {nodeInfo.status === 'retry' && ( - - {nodeInfo.error} - - )} -
- {nodeInfo.inputs && ( -
- {inputsTitle}
} - language={CodeLanguage.json} - value={nodeInfo.inputs} - isJSONStringifyBeauty - /> -
- )} - {nodeInfo.process_data && ( -
- {processDataTitle}
} - language={CodeLanguage.json} - value={nodeInfo.process_data} - isJSONStringifyBeauty - /> -
- )} - {nodeInfo.outputs && ( -
- {outputTitle}
} - language={CodeLanguage.json} - value={nodeInfo.outputs} - isJSONStringifyBeauty - tip={} - /> -
- )} - - )} - - - ) -} - -export default NodePanel +'use client' +import { useTranslation } from 'react-i18next' +import type { FC } from 'react' +import { useCallback, useContext, 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 NodePosition from '@/app/components/workflow/nodes/_base/components/node-position' +import type { XYPosition } from 'reactflow' +import { WorkflowHistoryStoreContext } from '@/app/components/workflow/workflow-history-store' + +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 = ({ + className, + nodeInfo, + allExecutions, + inMessage = false, + hideInfo = false, + hideProcessDetail, + onShowIterationDetail, + onShowLoopDetail, + onShowRetryDetail, + onShowAgentOrToolLog, + notShowIterationNav, + notShowLoopNav, +}) => { + const { store } = useContext(WorkflowHistoryStoreContext) + let inPublishMode = true + let hasNode = false + let nodePosition: XYPosition = { x: 0, y: 0 } + let nodeWidth = 0 + let nodeHeight = 0 + + if (store) { + inPublishMode = false + const nodes = store.getState().nodes + const currentNodeIndex = nodes.findIndex(node => node.id === nodeInfo.node_id) + const currentNode = nodes[currentNodeIndex] + nodePosition = currentNode?.position ?? { x: -1, y: -1 } + nodeWidth = currentNode?.width ?? -1 + nodeHeight = currentNode?.height ?? -1 + hasNode = !!currentNode + } + + const [collapseState, doSetCollapseState] = useState(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 `${Number.parseInt(Math.round(time / 60).toString())} 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 ( +
+
+
setCollapseState(!collapseState)} + > + {!hideProcessDetail && ( + + )} + + {nodeInfo.title}
+ } + > +
{nodeInfo.title}
+ + {nodeInfo.status !== 'running' && !hideInfo && ( +
{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}
+ )} + {nodeInfo.status === 'succeeded' && ( + + )} + {nodeInfo.status === 'failed' && ( + + )} + {nodeInfo.status === 'stopped' && ( + + )} + {nodeInfo.status === 'exception' && ( + + )} + {nodeInfo.status === 'running' && ( +
+ Running + +
+ )} + {!inPublishMode && ( +
+ +
+ )} +
+ {!collapseState && !hideProcessDetail && ( +
+ {/* The nav to the iteration detail */} + {isIterationNode && !notShowIterationNav && onShowIterationDetail && ( + + )} + {/* The nav to the Loop detail */} + {isLoopNode && !notShowLoopNav && onShowLoopDetail && ( + + )} + {isRetryNode && onShowRetryDetail && ( + + )} + { + (isAgentNode || isToolNode) && onShowAgentOrToolLog && ( + + ) + } +
+ {(nodeInfo.status === 'stopped') && ( + + {t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })} + + )} + {(nodeInfo.status === 'exception') && ( + + {nodeInfo.error} + + {t('workflow.common.learnMore')} + + + )} + {nodeInfo.status === 'failed' && ( + + {nodeInfo.error} + + )} + {nodeInfo.status === 'retry' && ( + + {nodeInfo.error} + + )} +
+ {nodeInfo.inputs && ( +
+ {inputsTitle}
} + language={CodeLanguage.json} + value={nodeInfo.inputs} + isJSONStringifyBeauty + /> +
+ )} + {nodeInfo.process_data && ( +
+ {processDataTitle}
} + language={CodeLanguage.json} + value={nodeInfo.process_data} + isJSONStringifyBeauty + /> +
+ )} + {nodeInfo.outputs && ( +
+ {outputTitle}
} + language={CodeLanguage.json} + value={nodeInfo.outputs} + isJSONStringifyBeauty + tip={} + /> + + )} + + )} + + + ) +} + +export default NodePanel From b5960bf525ddcb49c79997ccd7e462941ea3364b Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Sat, 28 Jun 2025 16:19:32 +0800 Subject: [PATCH 2/2] style(workflow): fix line endings and formatting in node panel component --- web/app/components/workflow/run/node.tsx | 586 +++++++++++------------ 1 file changed, 293 insertions(+), 293 deletions(-) diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index bebceb68ed..f99c4bfd44 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -1,293 +1,293 @@ -'use client' -import { useTranslation } from 'react-i18next' -import type { FC } from 'react' -import { useCallback, useContext, 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 NodePosition from '@/app/components/workflow/nodes/_base/components/node-position' -import type { XYPosition } from 'reactflow' -import { WorkflowHistoryStoreContext } from '@/app/components/workflow/workflow-history-store' - -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 = ({ - className, - nodeInfo, - allExecutions, - inMessage = false, - hideInfo = false, - hideProcessDetail, - onShowIterationDetail, - onShowLoopDetail, - onShowRetryDetail, - onShowAgentOrToolLog, - notShowIterationNav, - notShowLoopNav, -}) => { - const { store } = useContext(WorkflowHistoryStoreContext) - let inPublishMode = true - let hasNode = false - let nodePosition: XYPosition = { x: 0, y: 0 } - let nodeWidth = 0 - let nodeHeight = 0 - - if (store) { - inPublishMode = false - const nodes = store.getState().nodes - const currentNodeIndex = nodes.findIndex(node => node.id === nodeInfo.node_id) - const currentNode = nodes[currentNodeIndex] - nodePosition = currentNode?.position ?? { x: -1, y: -1 } - nodeWidth = currentNode?.width ?? -1 - nodeHeight = currentNode?.height ?? -1 - hasNode = !!currentNode - } - - const [collapseState, doSetCollapseState] = useState(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 `${Number.parseInt(Math.round(time / 60).toString())} 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 ( -
-
-
setCollapseState(!collapseState)} - > - {!hideProcessDetail && ( - - )} - - {nodeInfo.title}
- } - > -
{nodeInfo.title}
- - {nodeInfo.status !== 'running' && !hideInfo && ( -
{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}
- )} - {nodeInfo.status === 'succeeded' && ( - - )} - {nodeInfo.status === 'failed' && ( - - )} - {nodeInfo.status === 'stopped' && ( - - )} - {nodeInfo.status === 'exception' && ( - - )} - {nodeInfo.status === 'running' && ( -
- Running - -
- )} - {!inPublishMode && ( -
- -
- )} -
- {!collapseState && !hideProcessDetail && ( -
- {/* The nav to the iteration detail */} - {isIterationNode && !notShowIterationNav && onShowIterationDetail && ( - - )} - {/* The nav to the Loop detail */} - {isLoopNode && !notShowLoopNav && onShowLoopDetail && ( - - )} - {isRetryNode && onShowRetryDetail && ( - - )} - { - (isAgentNode || isToolNode) && onShowAgentOrToolLog && ( - - ) - } -
- {(nodeInfo.status === 'stopped') && ( - - {t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })} - - )} - {(nodeInfo.status === 'exception') && ( - - {nodeInfo.error} - - {t('workflow.common.learnMore')} - - - )} - {nodeInfo.status === 'failed' && ( - - {nodeInfo.error} - - )} - {nodeInfo.status === 'retry' && ( - - {nodeInfo.error} - - )} -
- {nodeInfo.inputs && ( -
- {inputsTitle}
} - language={CodeLanguage.json} - value={nodeInfo.inputs} - isJSONStringifyBeauty - /> -
- )} - {nodeInfo.process_data && ( -
- {processDataTitle}
} - language={CodeLanguage.json} - value={nodeInfo.process_data} - isJSONStringifyBeauty - /> -
- )} - {nodeInfo.outputs && ( -
- {outputTitle}
} - language={CodeLanguage.json} - value={nodeInfo.outputs} - isJSONStringifyBeauty - tip={} - /> - - )} - - )} - - - ) -} - -export default NodePanel +'use client' +import { useTranslation } from 'react-i18next' +import type { FC } from 'react' +import { useCallback, useContext, 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 NodePosition from '@/app/components/workflow/nodes/_base/components/node-position' +import type { XYPosition } from 'reactflow' +import { WorkflowHistoryStoreContext } from '@/app/components/workflow/workflow-history-store' + +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 = ({ + className, + nodeInfo, + allExecutions, + inMessage = false, + hideInfo = false, + hideProcessDetail, + onShowIterationDetail, + onShowLoopDetail, + onShowRetryDetail, + onShowAgentOrToolLog, + notShowIterationNav, + notShowLoopNav, +}) => { + const { store } = useContext(WorkflowHistoryStoreContext) + let inPublishMode = true + let hasNode = false + let nodePosition: XYPosition = { x: 0, y: 0 } + let nodeWidth = 0 + let nodeHeight = 0 + + if (store) { + inPublishMode = false + const nodes = store.getState().nodes + const currentNodeIndex = nodes.findIndex(node => node.id === nodeInfo.node_id) + const currentNode = nodes[currentNodeIndex] + nodePosition = currentNode?.position ?? { x: -1, y: -1 } + nodeWidth = currentNode?.width ?? -1 + nodeHeight = currentNode?.height ?? -1 + hasNode = !!currentNode + } + + const [collapseState, doSetCollapseState] = useState(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 `${Number.parseInt(Math.round(time / 60).toString())} 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 ( +
+
+
setCollapseState(!collapseState)} + > + {!hideProcessDetail && ( + + )} + + {nodeInfo.title}
+ } + > +
{nodeInfo.title}
+ + {nodeInfo.status !== 'running' && !hideInfo && ( +
{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}
+ )} + {nodeInfo.status === 'succeeded' && ( + + )} + {nodeInfo.status === 'failed' && ( + + )} + {nodeInfo.status === 'stopped' && ( + + )} + {nodeInfo.status === 'exception' && ( + + )} + {nodeInfo.status === 'running' && ( +
+ Running + +
+ )} + {!inPublishMode && ( +
+ +
+ )} +
+ {!collapseState && !hideProcessDetail && ( +
+ {/* The nav to the iteration detail */} + {isIterationNode && !notShowIterationNav && onShowIterationDetail && ( + + )} + {/* The nav to the Loop detail */} + {isLoopNode && !notShowLoopNav && onShowLoopDetail && ( + + )} + {isRetryNode && onShowRetryDetail && ( + + )} + { + (isAgentNode || isToolNode) && onShowAgentOrToolLog && ( + + ) + } +
+ {(nodeInfo.status === 'stopped') && ( + + {t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })} + + )} + {(nodeInfo.status === 'exception') && ( + + {nodeInfo.error} + + {t('workflow.common.learnMore')} + + + )} + {nodeInfo.status === 'failed' && ( + + {nodeInfo.error} + + )} + {nodeInfo.status === 'retry' && ( + + {nodeInfo.error} + + )} +
+ {nodeInfo.inputs && ( +
+ {inputsTitle}
} + language={CodeLanguage.json} + value={nodeInfo.inputs} + isJSONStringifyBeauty + /> +
+ )} + {nodeInfo.process_data && ( +
+ {processDataTitle}
} + language={CodeLanguage.json} + value={nodeInfo.process_data} + isJSONStringifyBeauty + /> +
+ )} + {nodeInfo.outputs && ( +
+ {outputTitle}
} + language={CodeLanguage.json} + value={nodeInfo.outputs} + isJSONStringifyBeauty + tip={} + /> + + )} + + )} + + + ) +} + +export default NodePanel