/* * @Author: ZhaoYing * @Date: 2026-02-24 17:57:08 * @Last Modified by: ZhaoYing * @Last Modified time: 2026-04-24 18:04:31 */ /* * Runtime Component * * This component displays the execution runtime details of workflow nodes in a chat interface. * It provides a hierarchical view of workflow execution with support for: * - Node execution status (completed, failed, running) * - Nested loop and iteration cycles * - Input/output data visualization * - Error messages for failed nodes * - Elapsed time tracking */ import { type FC, useState } from 'react' import { useTranslation } from 'react-i18next' import clsx from 'clsx' import { App, Button, Collapse, Flex } from 'antd' import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined, RightOutlined, ArrowLeftOutlined } from '@ant-design/icons' import copy from 'copy-to-clipboard' import styles from './chat.module.css' import type { ChatItem } from '@/components/Chat/types' import Markdown from '@/components/Markdown' import CodeBlock from '@/components/Markdown/CodeBlock' import RbAlert from '@/components/RbAlert' /** * Runtime component props * @param item - Chat item containing workflow execution data * @param index - Index of the chat item in the list */ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ item, index }) => { const { t } = useTranslation() const { message } = App.useApp() // Stores the currently selected detail view (for nested loop/iteration exploration) const [detail, setDetail] = useState(null) // Tracks whether the current detail view is for a loop (true) or iteration (false) const [loop, setLoop] = useState(null) const [expanded, setExpanded] = useState(false) /** * Handles navigation into nested loop/iteration details * @param vo - The node object containing subContent to display * @param isLoop - Whether this is a loop node (true) or iteration node (false) */ const handleViewDetail = (vo: any, isLoop: boolean) => { setDetail(vo) setLoop(isLoop) } /** * Returns CSS class for status-based text color * @param status - Node execution status: 'completed', 'failed', or other * @returns Tailwind CSS class for appropriate color */ const getStatus = (status?: string) => { return status === 'completed' ? 'rb:text-[#369F21]!' : status === 'failed' ? 'rb:text-[#FF5D34]!' : 'rb:text-[#5B6167]!' } /** * Renders child nodes grouped by cycle index (for loop/iteration nodes) * Groups nodes by their cycle_idx and displays them in separate collapsible sections * @param list - Array of child node execution data */ const renderDetailChild = (list: any) => { // Group nodes by cycle_idx to organize loop/iteration cycles const groupedByCycle = list.reduce((acc: any, item: any) => { const idx = item.cycle_idx ?? 0 if (!acc[idx]) acc[idx] = [] acc[idx].push(item) return acc }, {}) return ( {Object.entries(groupedByCycle).map(([cycleIdx, items]: [string, any]) => { return ( {t(`workflow.runtime.${loop ? 'loop' : 'iteration'}`)} {Number(cycleIdx) + 1} , className: styles.collapseItem, children: renderChild(items) }]} /> ) })} ) } /** * Renders detailed view of child nodes with their execution information * Displays node status, input/output data, errors, and nested cycles * @param list - Array of node execution data or error message string */ const renderChild = (list: any) => { if (Array.isArray(list)) { return {list?.map(vo => { const isLoop = vo.node_type === 'loop'; // Render cycle variables for loop nodes without node_name if (typeof vo.cycle_idx === 'number' && isLoop && !vo.node_name) { return
{t(`workflow.config.loop.cycle_vars`)}
} // Skip rendering if no node_name is present if (!vo.node_name) return null // Render collapsible node with status, timing, and execution details return ( {vo.icon &&
}
{vo.node_name}
{typeof vo.elapsed_time == 'number' && <>{vo.elapsed_time?.toFixed(3)}ms} {vo.status === 'completed' ? : vo.status === 'failed' ? : }
, className: styles.collapseItem, children: ( {/* Display error message for failed nodes */} {vo.content?.error && vo.content?.error !== '' && {t(`workflow.error`)} } {/* Display navigation to nested cycles if subContent exists */} {vo.subContent?.length > 0 && ( handleViewDetail(vo, vo.node_type === 'loop')}> {Math.max(...vo.subContent.map((itemVo: any) => itemVo.cycle_idx + 1))} {t(`workflow.${isLoop ? 'loopNum' : 'iterationNum'}`)} )} {/* Display input and output data as JSON code blocks */} {['input', 'process', 'output'].map(key => { if (vo.node_type !== 'http-request' && key === 'process') return null return (
{isLoop ? t(`workflow.runtime.${key}_cycle_vars`) : t(`workflow.${key}_result`)}
) })}
) }]} /> ) })}
} return
} /** Copy value to clipboard and show success message */ const handleCopy = (value: string) => { copy(value) message.success(t('common.copySuccess')) } return (
{item.status === 'completed' ? : item.status === 'failed' ? : } {t('application.workflow')} { setExpanded(v => !v); setDetail(null) }} >
{expanded && ( detail ? (
{renderDetailChild(detail.subContent)}
) :
{item.error && item.error !== '' && } {renderChild(item.subContent)}
)}
) } export default Runtime