feat(web): node run status

This commit is contained in:
zhaoying
2026-04-15 16:09:38 +08:00
parent 3e48d620b2
commit 643a3fbe09
8 changed files with 115 additions and 40 deletions

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-04 17:20:52 * @Date: 2026-02-04 17:20:52
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 17:20:52 * @Last Modified time: 2026-04-14 18:24:29
*/ */
import { useEffect, useRef, useMemo } from 'react'; import { useEffect, useRef, useMemo } from 'react';
import { EditorView, basicSetup } from 'codemirror'; import { EditorView, basicSetup } from 'codemirror';
@@ -156,7 +156,7 @@ const CodeMirrorEditor = ({
<div <div
ref={editorRef} ref={editorRef}
style={{ minHeight, fontSize, lineHeight }} style={{ minHeight, fontSize, lineHeight }}
className={variant === 'borderless' ? '' : 'rb-border rb:rounded-[8px]'} className={variant === 'borderless' ? '' : 'rb-border rb:rounded-lg'}
/> />
); );
}; };

View File

@@ -6,11 +6,15 @@
*/ */
import { create } from 'zustand' import { create } from 'zustand'
import type { NodeCheckResult } from '@/views/Workflow/components/CheckList' import type { NodeCheckResult } from '@/views/Workflow/components/CheckList'
import type { ChatItem } from '@/components/Chat/types'
interface WorkflowState { interface WorkflowState {
checkResults: Record<string, NodeCheckResult[]> checkResults: Record<string, NodeCheckResult[]>
setCheckResults: (appId: string, results: NodeCheckResult[]) => void setCheckResults: (appId: string, results: NodeCheckResult[]) => void
getCheckResults: (appId: string) => NodeCheckResult[] getCheckResults: (appId: string) => NodeCheckResult[]
chatHistoryMap: Record<string, ChatItem[]>
setChatHistory: (conversationId: string, history: ChatItem[]) => void
getChatHistory: (conversationId: string) => ChatItem[]
} }
export const useWorkflowStore = create<WorkflowState>((set, get) => ({ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
@@ -18,4 +22,8 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
setCheckResults: (appId, results) => setCheckResults: (appId, results) =>
set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })), set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })),
getCheckResults: (appId) => get().checkResults[appId] ?? [], getCheckResults: (appId) => get().checkResults[appId] ?? [],
chatHistoryMap: {},
setChatHistory: (conversationId, history) =>
set(state => ({ chatHistoryMap: { ...state.chatHistoryMap, [conversationId]: history } })),
getChatHistory: (conversationId) => get().chatHistoryMap[conversationId] ?? [],
})) }))

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-06 21:10:56 * @Date: 2026-02-06 21:10:56
* @Last Modified by: ZhaoYing * @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 * Workflow Chat Component
@@ -41,12 +41,15 @@ import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
import Runtime from './Runtime'; import Runtime from './Runtime';
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'; import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
import { replaceVariables } from '@/views/ApplicationConfig/Agent'; import { replaceVariables } from '@/views/ApplicationConfig/Agent';
import { useWorkflowStore } from '@/store/workflow';
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({ // eslint-disable-line
appId, graphRef, features appId, graphRef, features
}, ref) => { }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { message: messageApi } = App.useApp() const { message: messageApi } = App.useApp()
const { setChatHistory } = useWorkflowStore()
const conversationIdRef = useRef<string>('draft')
const toolbarRef = useRef<ChatToolbarRef>(null) const toolbarRef = useRef<ChatToolbarRef>(null)
const [toolbarReady, setToolbarReady] = useState(false) const [toolbarReady, setToolbarReady] = useState(false)
const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => { const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => {
@@ -118,6 +121,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
setChatList([]) setChatList([])
setVariables([]) setVariables([])
setConversationId(null) setConversationId(null)
conversationIdRef.current = 'draft'
setMessage(undefined) setMessage(undefined)
toolbarRef.current?.setFiles([]) toolbarRef.current?.setFiles([])
toolbarRef.current?.setVariables([]) toolbarRef.current?.setVariables([])
@@ -189,7 +193,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
elapsed_time?: string; elapsed_time?: string;
error?: any; error?: any;
state: Record<string, any>; state: Record<string, any>;
status?: 'completed' | 'failed', status?: 'completed' | 'failed' | 'running',
citations?: { citations?: {
document_id: string; document_id: string;
file_name: string; file_name: string;
@@ -231,6 +235,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
node_name: name, node_name: name,
node_type: type, node_type: type,
icon, icon,
status: 'running',
content: {}, content: {},
} }
} else { } else {
@@ -240,6 +245,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
node_name: name, node_name: name,
node_type: type, node_type: type,
icon, icon,
status: 'running',
content: {}, content: {},
}) })
} }
@@ -344,6 +350,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
} }
if (conversation_id && conversationId !== conversation_id) { if (conversation_id && conversationId !== conversation_id) {
conversationIdRef.current = conversation_id
setConversationId(conversation_id) setConversationId(conversation_id)
} }
}) })
@@ -440,6 +447,10 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
} }
}, [chatList.length, features?.opening_statement, variables]) }, [chatList.length, features?.opening_statement, variables])
useEffect(() => {
setChatHistory(conversationIdRef.current, chatList)
}, [chatList])
return ( return (
<RbDrawer <RbDrawer
title={<Flex align="center" gap={10}> title={<Flex align="center" gap={10}>

View File

@@ -34,29 +34,24 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
> >
<Flex vertical align={collapsed ? 'center' : undefined} gap={collapsed ? 8 : 16}> <Flex vertical align={collapsed ? 'center' : undefined} gap={collapsed ? 8 : 16}>
{collapsed {collapsed
? <> ? nodeLibrary.flatMap(category =>
{nodeLibrary.map(category => ( category.nodes
<> .filter(node => node.type !== 'cycle-start' && node.type !== 'break')
{category.nodes .map(node => (
.filter(node => node.type !== 'cycle-start' && node.type !== 'break') <Tooltip key={node.type} title={t(`workflow.${node.type}`)} placement="right">
.map((node, nodeIndex) => ( <div
<Tooltip key={nodeIndex} title={t(`workflow.${node.type}`)} placement="right"> className="rb:p-2 rb:rounded-lg rb:hover:bg-[rgba(33,35,50,0.08)]"
<div draggable
className="rb:p-2 rb:rounded-lg rb:hover:bg-[rgba(33,35,50,0.08)]" onDragStart={(e) => {
draggable e.dataTransfer.setData('application/reactflow', node.type);
onDragStart={(e) => { e.dataTransfer.setData('application/json', JSON.stringify(node));
e.dataTransfer.setData('application/reactflow', node.type); }}
e.dataTransfer.setData('application/json', JSON.stringify(node)); >
}} <div className={`rb:size-6 rb:cursor-pointer rb:bg-cover ${node.icon}`} />
> </div>
<div className={`rb:size-6 rb:cursor-pointer rb:bg-cover ${node.icon}`} /> </Tooltip>
</div> ))
</Tooltip> )
))
}
</>
))}
</>
: nodeLibrary.map(category => ( : nodeLibrary.map(category => (
<div <div
key={category.category} key={category.category}
@@ -65,9 +60,9 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
<Flex gap={6} vertical> <Flex gap={6} vertical>
{category.nodes {category.nodes
.filter(node => node.type !== 'cycle-start' && node.type !== 'break') .filter(node => node.type !== 'cycle-start' && node.type !== 'break')
.map((node, nodeIndex) => ( .map((node) => (
<Flex <Flex
key={nodeIndex} key={node.type}
align="center" align="center"
gap={8} gap={8}
className="rb:rounded-xl rb:p-2! rb:border rb:border-[#EBEBEB] rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!" className="rb:rounded-xl rb:p-2! rb:border rb:border-[#EBEBEB] rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!"

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import clsx from 'clsx'; import clsx from 'clsx';
import type { ReactShapeConfig } from '@antv/x6-react-shape'; import type { ReactShapeConfig } from '@antv/x6-react-shape';
import { Flex } from 'antd'; import { Flex } from 'antd';
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
import NodeTools from './NodeTools' import NodeTools from './NodeTools'
import { useVariableList } from '../Properties/hooks/useVariableList' import { useVariableList } from '../Properties/hooks/useVariableList'
@@ -47,13 +48,23 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
return ( return (
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', { <div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
'rb:border-[#171719]': data.isSelected, 'rb:border-[#171719]!': data.isSelected,
'rb:border-[#FCFCFD]': !data.isSelected 'rb:border-[#FCFCFD]': !data.isSelected,
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
})}> })}>
<NodeTools node={node} /> <NodeTools node={node} />
<Flex align="center" gap={8} className="rb:flex-1"> <Flex align="center" gap={8} className="rb:flex-1">
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} /> <div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div> <div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
{data.executionStatus === 'completed'
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
: data.executionStatus === 'failed'
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
: data.executionStatus === 'running'
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
: null
}
</Flex> </Flex>
{data.type === 'question-classifier' && {data.type === 'question-classifier' &&

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import clsx from 'clsx'; import clsx from 'clsx';
import type { ReactShapeConfig } from '@antv/x6-react-shape'; import type { ReactShapeConfig } from '@antv/x6-react-shape';
import { Flex } from 'antd'; import { Flex } from 'antd';
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
import { graphNodeLibrary, edgeAttrs } from '../../constant'; import { graphNodeLibrary, edgeAttrs } from '../../constant';
import NodeTools from './NodeTools' import NodeTools from './NodeTools'
@@ -131,12 +132,22 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
return ( return (
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', { <div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
'rb:border-[#171719]': data.isSelected, 'rb:border-[#171719]': data.isSelected,
'rb:border-[#FCFCFD]': !data.isSelected 'rb:border-[#FCFCFD]': !data.isSelected,
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
})}> })}>
<NodeTools node={node} /> <NodeTools node={node} />
<Flex align="center" gap={8} className="rb:flex-1"> <Flex align="center" gap={8} className="rb:flex-1">
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} /> <div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div> <div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
{data.executionStatus === 'completed'
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
: data.executionStatus === 'failed'
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
: data.executionStatus === 'running'
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
: null
}
</Flex> </Flex>
<div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#939AB1_1px,#F0F3F8_1px)] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-[10px] rb:bg-size-[12px_12px]"></div> <div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#939AB1_1px,#F0F3F8_1px)] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-[10px] rb:bg-size-[12px_12px]"></div>
</div> </div>

View File

@@ -2,6 +2,7 @@ import clsx from 'clsx';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ReactShapeConfig } from '@antv/x6-react-shape'; import type { ReactShapeConfig } from '@antv/x6-react-shape';
import { Flex } from 'antd'; import { Flex } from 'antd';
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
import NodeTools from './NodeTools' import NodeTools from './NodeTools'
@@ -11,13 +12,23 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
return ( return (
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', { <div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
'rb:border-[#171719]': data.isSelected, 'rb:border-[#171719]!': data.isSelected,
'rb:border-[#FCFCFD]': !data.isSelected 'rb:border-[#FCFCFD]': !data.isSelected,
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
})}> })}>
<NodeTools node={node} /> <NodeTools node={node} />
<Flex align="center" gap={8} className="rb:flex-1"> <Flex align="center" gap={8} className="rb:flex-1">
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} /> <div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div> <div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
{data.executionStatus === 'completed'
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
: data.executionStatus === 'failed'
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
: data.executionStatus === 'running'
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
: null
}
</Flex> </Flex>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-3">{t('workflow.clickToConfigure')}</div> <div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-3">{t('workflow.clickToConfigure')}</div>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 15:17:48 * @Date: 2026-02-03 15:17:48
* @Last Modified by: ZhaoYing * @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 { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, type Edge } from '@antv/x6';
import { register } from '@antv/x6-react-shape'; 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 { 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 type { ChatVariable, NodeProperties, WorkflowConfig } from '../types';
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils'; import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils';
import { useWorkflowStore } from '@/store/workflow';
/** /**
* Props for useWorkflowGraph hook * Props for useWorkflowGraph hook
@@ -94,6 +95,8 @@ export const useWorkflowGraph = ({
const { message } = App.useApp(); const { message } = App.useApp();
const { t } = useTranslation() const { t } = useTranslation()
const { user } = useUser(); const { user } = useUser();
const { chatHistoryMap } = useWorkflowStore()
const chatHistory = Object.values(chatHistoryMap).at(-1) ?? []
// Refs // Refs
const graphRef = useRef<Graph>(); const graphRef = useRef<Graph>();
@@ -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<string, string> = {};
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 { return {
config, config,