diff --git a/web/app/components/workflow/run/index.tsx b/web/app/components/workflow/run/index.tsx index a501dd184b..441002c86c 100644 --- a/web/app/components/workflow/run/index.tsx +++ b/web/app/components/workflow/run/index.tsx @@ -2,7 +2,6 @@ import type { FC } from 'react' import type { WorkflowRunDetailResponse } from '@/models/log' import type { NodeTracing } from '@/types/workflow' -import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' diff --git a/web/app/components/workflow/run/utils/format-log/human-input/index.ts b/web/app/components/workflow/run/utils/format-log/human-input/index.ts new file mode 100644 index 0000000000..f8fbe7974c --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/human-input/index.ts @@ -0,0 +1,59 @@ +import type { NodeTracing } from '@/types/workflow' +import { BlockEnum } from '@/app/components/workflow/types' + +/** + * Format human-input nodes to ensure only the latest status is kept for each node. + * Human-input nodes can have multiple log entries as their status changes + * (e.g., running -> paused -> succeeded/failed). + * This function keeps only the entry with the latest index for each unique node_id. + */ +const formatHumanInputNode = (list: NodeTracing[]): NodeTracing[] => { + // Group human-input nodes by node_id + const humanInputNodeMap = new Map() + + // Track which node_ids are human-input type + const humanInputNodeIds = new Set() + + // First pass: identify human-input nodes and keep the one with the highest index + list.forEach((item) => { + if (item.node_type === BlockEnum.HumanInput) { + humanInputNodeIds.add(item.node_id) + + const existingNode = humanInputNodeMap.get(item.node_id) + if (!existingNode || item.index > existingNode.index) { + humanInputNodeMap.set(item.node_id, item) + } + } + }) + + // If no human-input nodes, return the list as is + if (humanInputNodeIds.size === 0) + return list + + // Second pass: filter the list to remove duplicate human-input nodes + // and keep only the latest one for each node_id + const result: NodeTracing[] = [] + const addedHumanInputNodeIds = new Set() + + list.forEach((item) => { + if (item.node_type === BlockEnum.HumanInput) { + // Only add the human-input node with the highest index + if (!addedHumanInputNodeIds.has(item.node_id)) { + const latestNode = humanInputNodeMap.get(item.node_id) + if (latestNode) { + result.push(latestNode) + addedHumanInputNodeIds.add(item.node_id) + } + } + // Skip duplicate human-input nodes + } + else { + // Keep all non-human-input nodes + result.push(item) + } + }) + + return result +} + +export default formatHumanInputNode diff --git a/web/app/components/workflow/run/utils/format-log/index.ts b/web/app/components/workflow/run/utils/format-log/index.ts index c152a5156a..2393f9837f 100644 --- a/web/app/components/workflow/run/utils/format-log/index.ts +++ b/web/app/components/workflow/run/utils/format-log/index.ts @@ -2,6 +2,7 @@ import type { NodeTracing } from '@/types/workflow' import { cloneDeep } from 'es-toolkit/object' import { BlockEnum } from '../../../types' import formatAgentNode from './agent' +import formatHumanInputNode from './human-input' import { addChildrenToIterationNode } from './iteration' import { addChildrenToLoopNode } from './loop' import formatParallelNode from './parallel' @@ -83,7 +84,8 @@ const formatToTracingNodeList = (list: NodeTracing[], t: any) => { * Because Handle struct node will put the node in different */ const formattedAgentList = formatAgentNode(allItems) - const formattedRetryList = formatRetryNode(formattedAgentList) // retry one node + const formattedHumanInputList = formatHumanInputNode(formattedAgentList) // Keep only latest status for human-input nodes + const formattedRetryList = formatRetryNode(formattedHumanInputList) // 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)