- {data.name ?? t(`workflow.${data.type}`)}
+ {data.name ?? t(`workflow.${data.type}`)}
+ {data.executionStatus === 'completed'
+ ?
+ : data.executionStatus === 'failed'
+ ?
+ : data.executionStatus === 'running'
+ ?
+ : null
+ }
{data.type === 'question-classifier' &&
diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx
index ca0eaeff..c540db76 100644
--- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import clsx from 'clsx';
import type { ReactShapeConfig } from '@antv/x6-react-shape';
import { Flex } from 'antd';
+import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
import { graphNodeLibrary, edgeAttrs } from '../../constant';
import NodeTools from './NodeTools'
@@ -131,12 +132,22 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
return (
- {data.name ?? t(`workflow.${data.type}`)}
+ {data.name ?? t(`workflow.${data.type}`)}
+ {data.executionStatus === 'completed'
+ ?
+ : data.executionStatus === 'failed'
+ ?
+ : data.executionStatus === 'running'
+ ?
+ : null
+ }
diff --git a/web/src/views/Workflow/components/Nodes/NormalNode.tsx b/web/src/views/Workflow/components/Nodes/NormalNode.tsx
index f947d004..ce936be9 100644
--- a/web/src/views/Workflow/components/Nodes/NormalNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/NormalNode.tsx
@@ -2,6 +2,7 @@ import clsx from 'clsx';
import { useTranslation } from 'react-i18next'
import type { ReactShapeConfig } from '@antv/x6-react-shape';
import { Flex } from 'antd';
+import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
import NodeTools from './NodeTools'
@@ -11,13 +12,23 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
return (
- {data.name ?? t(`workflow.${data.type}`)}
+ {data.name ?? t(`workflow.${data.type}`)}
+ {data.executionStatus === 'completed'
+ ?
+ : data.executionStatus === 'failed'
+ ?
+ : data.executionStatus === 'running'
+ ?
+ : null
+ }
{t('workflow.clickToConfigure')}
diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
index f385acf3..516bc24c 100644
--- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts
+++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:17:48
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-04-07 23:17:50
+ * @Last Modified time: 2026-04-15 16:02:49
*/
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, type Edge } from '@antv/x6';
import { register } from '@antv/x6-react-shape';
@@ -18,6 +18,7 @@ import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
import { conditionNodeHeight, conditionNodeItemHeight, conditionNodePortItemArgsY, defaultAbsolutePortGroups, defaultPortItems, edgeAttrs, edgeHoverTool, edge_color, edge_selected_color, edge_width, graphNodeLibrary, nodeLibrary, nodeRegisterLibrary, nodeWidth, notesConfig, portAttrs, portItemArgsY, portMarkup, portTextAttrs, unknownNode } from '../constant';
import type { ChatVariable, NodeProperties, WorkflowConfig } from '../types';
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils';
+import { useWorkflowStore } from '@/store/workflow';
/**
* Props for useWorkflowGraph hook
@@ -94,6 +95,8 @@ export const useWorkflowGraph = ({
const { message } = App.useApp();
const { t } = useTranslation()
const { user } = useUser();
+ const { chatHistoryMap } = useWorkflowStore()
+ const chatHistory = Object.values(chatHistoryMap).at(-1) ?? []
// Refs
const graphRef = useRef
();
@@ -1425,6 +1428,31 @@ export const useWorkflowGraph = ({
}
}
}
+ useEffect(() => {
+ if (!graphRef.current) return;
+ const nodes = graphRef.current.getNodes();
+
+ const lastWithSub = [...chatHistory].reverse().find(item => item.subContent?.length);
+ // Reset all node execution status first
+ nodes.forEach(node => {
+ const data = node.getData();
+ if (typeof data.status === 'string') {
+ node.setData({ ...data, executionStatus: undefined });
+ }
+ });
+ if (!lastWithSub?.subContent) return;
+ // Build a nodeId -> status map first
+ const statusMap: Record = {};
+ lastWithSub.subContent.forEach(sub => {
+ if (typeof sub.status === 'string') {
+ statusMap[sub.node_id] = sub.status;
+ const node = nodes.find(n => n.getData()?.id === sub.node_id);
+ if (node) {
+ node.setData({ ...node.getData(), executionStatus: sub.status });
+ }
+ }
+ });
+ }, [chatHistory, graphRef.current]);
return {
config,