From 643a3fbe094e97d44b6ceb23b8f70b5e67ace039 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 15 Apr 2026 16:09:38 +0800 Subject: [PATCH] feat(web): node run status --- web/src/components/CodeMirrorEditor/index.tsx | 6 +-- web/src/store/workflow.ts | 8 ++++ .../views/Workflow/components/Chat/Chat.tsx | 17 +++++-- .../views/Workflow/components/NodeLibrary.tsx | 45 +++++++++---------- .../components/Nodes/ConditionNode.tsx | 17 +++++-- .../Workflow/components/Nodes/LoopNode.tsx | 15 ++++++- .../Workflow/components/Nodes/NormalNode.tsx | 17 +++++-- .../views/Workflow/hooks/useWorkflowGraph.ts | 30 ++++++++++++- 8 files changed, 115 insertions(+), 40 deletions(-) diff --git a/web/src/components/CodeMirrorEditor/index.tsx b/web/src/components/CodeMirrorEditor/index.tsx index ec2a6780..8671992a 100644 --- a/web/src/components/CodeMirrorEditor/index.tsx +++ b/web/src/components/CodeMirrorEditor/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-04 17:20:52 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-04 17:20:52 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-14 18:24:29 */ import { useEffect, useRef, useMemo } from 'react'; import { EditorView, basicSetup } from 'codemirror'; @@ -156,7 +156,7 @@ const CodeMirrorEditor = ({
); }; diff --git a/web/src/store/workflow.ts b/web/src/store/workflow.ts index 0999d35a..382d9255 100644 --- a/web/src/store/workflow.ts +++ b/web/src/store/workflow.ts @@ -6,11 +6,15 @@ */ import { create } from 'zustand' import type { NodeCheckResult } from '@/views/Workflow/components/CheckList' +import type { ChatItem } from '@/components/Chat/types' interface WorkflowState { checkResults: Record setCheckResults: (appId: string, results: NodeCheckResult[]) => void getCheckResults: (appId: string) => NodeCheckResult[] + chatHistoryMap: Record + setChatHistory: (conversationId: string, history: ChatItem[]) => void + getChatHistory: (conversationId: string) => ChatItem[] } export const useWorkflowStore = create((set, get) => ({ @@ -18,4 +22,8 @@ export const useWorkflowStore = create((set, get) => ({ setCheckResults: (appId, results) => set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })), getCheckResults: (appId) => get().checkResults[appId] ?? [], + chatHistoryMap: {}, + setChatHistory: (conversationId, history) => + set(state => ({ chatHistoryMap: { ...state.chatHistoryMap, [conversationId]: history } })), + getChatHistory: (conversationId) => get().chatHistoryMap[conversationId] ?? [], })) diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx index e1a0ad95..19b06a0d 100644 --- a/web/src/views/Workflow/components/Chat/Chat.tsx +++ b/web/src/views/Workflow/components/Chat/Chat.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:10:56 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-07 18:07:38 + * @Last Modified time: 2026-04-15 15:57:35 */ /** * Workflow Chat Component @@ -41,12 +41,15 @@ import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar' import Runtime from './Runtime'; import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'; import { replaceVariables } from '@/views/ApplicationConfig/Agent'; +import { useWorkflowStore } from '@/store/workflow'; -const Chat = forwardRef(({ +const Chat = forwardRef(({ // eslint-disable-line appId, graphRef, features }, ref) => { const { t } = useTranslation() const { message: messageApi } = App.useApp() + const { setChatHistory } = useWorkflowStore() + const conversationIdRef = useRef('draft') const toolbarRef = useRef(null) const [toolbarReady, setToolbarReady] = useState(false) const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => { @@ -118,6 +121,7 @@ const Chat = forwardRef; - status?: 'completed' | 'failed', + status?: 'completed' | 'failed' | 'running', citations?: { document_id: string; file_name: string; @@ -231,6 +235,7 @@ const Chat = forwardRef { + setChatHistory(conversationIdRef.current, chatList) + }, [chatList]) + return ( diff --git a/web/src/views/Workflow/components/NodeLibrary.tsx b/web/src/views/Workflow/components/NodeLibrary.tsx index e6190adb..525c09ae 100644 --- a/web/src/views/Workflow/components/NodeLibrary.tsx +++ b/web/src/views/Workflow/components/NodeLibrary.tsx @@ -34,29 +34,24 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col > {collapsed - ? <> - {nodeLibrary.map(category => ( - <> - {category.nodes - .filter(node => node.type !== 'cycle-start' && node.type !== 'break') - .map((node, nodeIndex) => ( - -
{ - e.dataTransfer.setData('application/reactflow', node.type); - e.dataTransfer.setData('application/json', JSON.stringify(node)); - }} - > -
-
- - )) - } - - ))} - + ? nodeLibrary.flatMap(category => + category.nodes + .filter(node => node.type !== 'cycle-start' && node.type !== 'break') + .map(node => ( + +
{ + e.dataTransfer.setData('application/reactflow', node.type); + e.dataTransfer.setData('application/json', JSON.stringify(node)); + }} + > +
+
+ + )) + ) : nodeLibrary.map(category => (
void }> = ({ col {category.nodes .filter(node => node.type !== 'cycle-start' && node.type !== 'break') - .map((node, nodeIndex) => ( + .map((node) => ( { return (
-
{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,