diff --git a/web/src/components/Chat/ChatContent.tsx b/web/src/components/Chat/ChatContent.tsx index a785ea49..509004b0 100644 --- a/web/src/components/Chat/ChatContent.tsx +++ b/web/src/components/Chat/ChatContent.tsx @@ -8,12 +8,11 @@ import { type FC, useRef, useEffect, useState } from 'react' import clsx from 'clsx' import Markdown from '@/components/Markdown' import type { ChatContentProps } from './types' -import { Spin, Image, Flex, Button } from 'antd' +import { Spin, Flex, Button } from 'antd' import { SoundOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' -import AudioPlayer from './AudioPlayer' -import VideoPlayer from './VideoPlayer' +import MessageFiles from './MessageFiles' const getFileUrl = (file: any) => { return file.thumbUrl || file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : undefined) @@ -149,72 +148,7 @@ const ChatContent: FC = ({ {labelFormat(item)} } - {item?.meta_data?.files && item.meta_data?.files.length > 0 && - {item.meta_data?.files?.map((file) => { - if (file.type.includes('image')) { - return ( -
- {file.name} -
- ) - } - if (file.type.includes('video')) { - return ( -
- {/*
- ) - } - if (file.type.includes('audio')) { - return ( -
- -
- ) - } - - const documentType = (file.file_type || file.type)?.split('/') - return ( - handleDownload(file)} - > -
-
-
{file.name}
-
{documentType?.[documentType.length - 1]} · {file.size}
-
-
- ) - })} -
} + {/* Message bubble */}
+ file.thumbUrl || file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : undefined) + +const DOC_ICONS: [string[], string][] = [ + [['pdf'], "rb:bg-[url('@/assets/images/file/pdf.svg')]"], + [['excel', 'spreadsheetml.sheet', 'xls', 'xlsx'], "rb:bg-[url('@/assets/images/file/excel.svg')]"], + [['csv'], "rb:bg-[url('@/assets/images/file/csv.svg')]"], + [['html'], "rb:bg-[url('@/assets/images/file/html.svg')]"], + [['json'], "rb:bg-[url('@/assets/images/file/json.svg')]"], + [['ppt'], "rb:bg-[url('@/assets/images/file/ppt.svg')]"], + [['markdown'], "rb:bg-[url('@/assets/images/file/md.svg')]"], + [['text'], "rb:bg-[url('@/assets/images/file/txt.svg')]"], + [['doc', 'docx', 'word', 'wordprocessingml.document'], "rb:bg-[url('@/assets/images/file/word.svg')]"], +] + +const getDocIcon = (parts: string[]) => { + const match = DOC_ICONS.find(([keys]) => keys.some(k => parts.includes(k))) + return match ? match[1] : "rb:bg-[url('@/assets/images/file/txt.svg')]" +} + +interface MessageFilesProps { + files: any[] + contentClassNames?: string | Record + onDownload: (file: any) => void +} + +const MessageFiles = ({ files, contentClassNames, onDownload }: MessageFilesProps) => { + if (!files?.length) return null + return ( + + {files.map((file) => { + const key = file.url || file.uid + if (file.type.includes('image')) { + return ( +
+ {file.name} +
+ ) + } + if (file.type.includes('video')) { + return ( +
+ +
+ ) + } + if (file.type.includes('audio')) { + return ( +
+ +
+ ) + } + const documentType = (file.file_type || file.type)?.split('/') ?? [] + return ( + onDownload(file)} + > +
+
+
{file.name}
+
+ {documentType?.[documentType.length - 1]} · {file.size} +
+
+ + ) + })} + + ) +} + +export default MessageFiles diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx index e1d7e596..c0698389 100644 --- a/web/src/components/SiderMenu/index.tsx +++ b/web/src/components/SiderMenu/index.tsx @@ -399,7 +399,7 @@ const Menu: FC<{ className="rb:overflow-y-auto rb:flex-1!" /> {/* Return to space button for superusers */} - {user?.is_superuser && source === 'space' && + {source === 'space' &&
{collapsed ? null : t('common.switchSpace')}
- -
- {collapsed ? null : t('common.returnToSpace')} -
+ {user?.is_superuser && + +
+ {collapsed ? null : t('common.returnToSpace')} +
+ }
} {source === 'manage' && subscription && !collapsed && diff --git a/web/src/views/ToolManagement/components/CustomToolModal.tsx b/web/src/views/ToolManagement/components/CustomToolModal.tsx index cbcddb7c..08e18679 100644 --- a/web/src/views/ToolManagement/components/CustomToolModal.tsx +++ b/web/src/views/ToolManagement/components/CustomToolModal.tsx @@ -101,6 +101,7 @@ const CustomToolModal = forwardRef(({ }); }; const formatSchema = (value: string) => { + if (!value || value.trim() === '') return setParseSchemaData({} as ParseSchemaData) parseSchema({ schema_content: value }) .then(res => { diff --git a/web/src/views/Workflow/components/CanvasToolbar.tsx b/web/src/views/Workflow/components/CanvasToolbar.tsx index 1bbb51f2..8225b65f 100644 --- a/web/src/views/Workflow/components/CanvasToolbar.tsx +++ b/web/src/views/Workflow/components/CanvasToolbar.tsx @@ -57,7 +57,6 @@ const CanvasToolbar: FC = ({ } }} labelRender={(props) => { - console.log('props', props) return `${props.value}%` }} className="rb:w-20 rb:h-4!" diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx index 863825ba..025c2e0b 100644 --- a/web/src/views/Workflow/components/Chat/Chat.tsx +++ b/web/src/views/Workflow/components/Chat/Chat.tsx @@ -66,8 +66,6 @@ const Chat = forwardRef([]) const [message, setMessage] = useState(undefined) - console.log('abortRef', abortRef, chatList) - /** * Opens the chat drawer and loads workflow variables from the start node */ diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx index 3bdb96c0..15f4aa1e 100644 --- a/web/src/views/Workflow/components/Nodes/AddNode.tsx +++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx @@ -18,6 +18,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { // Handle node selection from popover and create new node replacing the add-node placeholder const handleNodeSelect = (selectedNodeType: any) => { + graph.startBatch('add-node'); const parentBBox = node.getBBox(); const cycleId = data.cycle; const horizontalSpacing = 0; @@ -76,6 +77,8 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { } }); + graph.stopBatch('add-node'); + setTimeout(() => { addedEdges.forEach(e => { const src = graph.getCellById(e.getSourceCellId()); diff --git a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx index b431ddd4..74a15c78 100644 --- a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx +++ b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx @@ -99,7 +99,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { {data.type === 'if-else' && {data.config?.cases?.defaultValue.map((item: any, index: number) => ( -
0 ? '' : 'rb:mb-1'}> +
0 ? "space-between" : 'end'} className="rb:mb-1! rb:leading-4"> {item.expressions.length > 0 && CASE{index + 1}} {index === 0 ? 'IF' : `ELIF`} diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx index cffb62dd..ac81a667 100644 --- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx +++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx @@ -1,134 +1,15 @@ -import { useEffect } from 'react'; -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 { useTranslation } from 'react-i18next' -import { graphNodeLibrary, edgeAttrs } from '../../constant'; import NodeTools from './NodeTools' -const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { +const LoopNode: ReactShapeConfig['component'] = ({ node }) => { const data = node.getData() || {}; const { t } = useTranslation() - useEffect(() => { - // 使用setTimeout确保在所有节点都添加完成后再创建连线 - const timer = setTimeout(() => { - initNodes() - checkAndAddAddNode() - }, 50) - - return () => clearTimeout(timer) - }, [graph]) - - const checkAndAddAddNode = () => { - if (!graph) return; - - const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === data.id); - const cycleStartNodes = childNodes.filter((n: any) => n.getData()?.type === 'cycle-start'); - - // 如果只有一个cycle-start节点且没有其他类型的子节点,则添加add-node - if (cycleStartNodes.length === 1 && childNodes.length === 1) { - const cycleStartNode = cycleStartNodes[0]; - const cycleStartBBox = cycleStartNode.getBBox(); - - const addNode = graph.addNode({ - ...graphNodeLibrary.addStart, - x: cycleStartBBox.x + 84, - y: cycleStartBBox.y + 4, - data: { - type: 'add-node', - label: t('workflow.addNode'), - icon: '+', - parentId: node.id, - cycle: data.id, - }, - }); - - node.addChild(addNode); - - // 连接cycle-start和add-node - const sourcePorts = cycleStartNode.getPorts(); - const targetPorts = addNode.getPorts(); - const sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right'; - const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left'; - - // 然后创建连线 - graph.addEdge({ - source: { cell: cycleStartNode.id, port: sourcePort }, - target: { cell: addNode.id, port: targetPort }, - ...edgeAttrs, - }); - - cycleStartNode.toFront() - addNode.toFront() - } - } - - const initNodes = () => { - // 检查是否存在cycle为当前节点ID的子节点,若存在则不调用initNodes,避免重复创建 - const existingCycleNodes = graph.getNodes().filter((n: any) => - n.getData()?.cycle === data.id - ); - if (existingCycleNodes.length > 0) return; - // 添加默认子节点 - const parentBBox = node.getBBox(); - const centerX = parentBBox.x + 24; - const centerY = parentBBox.y + 70; - - const cycleStartNodeId = `cycle_start_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` - const cycleStartNode = graph.addNode({ - ...graphNodeLibrary.cycleStart, - x: centerX, - y: centerY, - id: cycleStartNodeId, - data: { - id: cycleStartNodeId, - type: 'cycle-start', - parentId: node.id, - isDefault: true, // 标记为默认节点,不可删除 - cycle: data.id, - }, - }); - const addNode = graph.addNode({ - ...graphNodeLibrary.addStart, - x: centerX + 84, - y: centerY + 4, - data: { - type: 'add-node', - label: t('workflow.addNode'), - icon: '+', - parentId: node.id, - cycle: data.id, - }, - }); - node.addChild(cycleStartNode) - node.addChild(addNode) - const sourcePorts = cycleStartNode.getPorts() - const targetPorts = addNode.getPorts() - let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right'; - - const edgeConfig = { - source: { - cell: cycleStartNode.id, - port: sourcePort - }, - target: { - cell: addNode.id, - port: targetPorts.find((port: any) => port.group === 'left')?.id || 'left' - }, - ...edgeAttrs - } - graph.addEdge(edgeConfig) - - setTimeout(() => { - - cycleStartNode.toFront() - addNode.toFront() - }, 0) - } - return (
= ({ graph }) => { // Handle node selection from popover menu and create new node with edge connection const handleNodeSelect = (selectedNodeType: any) => { if (!sourceNode || !graph) return; + graph.startBatch('add-node'); const sourceNodeData = sourceNode.getData(); const sourceNodeType = sourceNodeData?.type; @@ -308,6 +309,7 @@ const PortClickHandler: React.FC = ({ graph }) => { if (tgt?.isNode()) tgt.toFront(); }); } + graph.stopBatch('add-node'); }, 50); // Clean up temporary element diff --git a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx index 6e8bd0c0..73a5c087 100644 --- a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx +++ b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx @@ -242,10 +242,11 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({ className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''} > {parameter.type === 'string' && parameter.enum && parameter.enum.length > 0 - ? ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} /> : parameter.type === 'boolean' - ? + ? : = { width: nodeWidth, height: 120, shape: 'notes-node', + }, + output: { + width: nodeWidth, + height: 76, + shape: 'normal-node', + ports: { + groups: { left: defaultPortGroup }, + items: [defaultPortItems[0]], + }, } } diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index a22ee6c0..c81974a4 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -2,10 +2,9 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:17:48 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-24 17:21:09 + * @Last Modified time: 2026-04-27 16:30:30 */ import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, History, type Edge } from '@antv/x6'; -import type { HistoryCommand as Command } from '@antv/x6/lib/plugin/history/type'; import { register } from '@antv/x6-react-shape'; import type { PortMetadata } from '@antv/x6/lib/model/port'; import { App } from 'antd'; @@ -488,8 +487,77 @@ export const useWorkflowGraph = ({ graphRef.current.cleanHistory() } }, 200) + } else { + graphRef.current.enableHistory() + graphRef.current.cleanHistory() } } + + const resizeGroupNodes = (graph: Graph) => { + graph.getNodes().forEach(parentNode => { + const parentType = parentNode.getData()?.type + if (parentType !== 'loop' && parentType !== 'iteration') return + const children = graph.getNodes().filter( + n => n.getData()?.cycle === parentNode.getData()?.id && n.getData()?.type !== 'add-node' + ) + if (!children.length) return + const padding = 24 + const headerHeight = 50 + const childBounds = children.map(c => c.getBBox()) + const minX = Math.min(...childBounds.map(b => b.x)) + const minY = Math.min(...childBounds.map(b => b.y)) + const maxX = Math.max(...childBounds.map(b => b.x + b.width)) + const maxY = Math.max(...childBounds.map(b => b.y + b.height)) + const parentBBox = parentNode.getBBox() + const newWidth = Math.max(parentBBox.width, maxX - minX + padding * 2) + const newHeight = Math.max(parentBBox.height, maxY - minY + padding * 2 + headerHeight) + parentNode.prop('size', { width: newWidth, height: newHeight }) + parentNode.getPorts().forEach(port => { + if (port.group === 'right' && port.args) { + parentNode.portProp(port.id!, 'args/x', newWidth) + } + }) + }) + } + + const syncChildRelationships = () => { + if (!graphRef.current) return + const graph = graphRef.current + // Re-establish parent-child relationships based on cycle data + graph.getNodes().forEach(node => { + const cycleId = node.getData()?.cycle + if (!cycleId) return + const parentNode = graph.getCellById(cycleId) as Node | null + if (!parentNode) return + if (!parentNode.getChildren()?.some(c => c.id === node.id)) { + parentNode.addChild(node) + } + }) + // Remove stale parent-child links (parent exists but child's cycle no longer points to it) + graph.getNodes().forEach(node => { + const children = node.getChildren() + if (!children?.length) return + children.forEach(child => { + const childCycleId = (child as Node).getData?.()?.cycle + if (childCycleId !== node.id && childCycleId !== node.getData?.()?.id) { + node.removeChild(child) + } + }) + }) + // Recalculate group node size based on current children + resizeGroupNodes(graph) + // Bring child edges and nodes to front + graph.getEdges().forEach(edge => { + const src = graph.getCellById(edge.getSourceCellId()) + const tgt = graph.getCellById(edge.getTargetCellId()) + if (src?.getData()?.cycle || tgt?.getData()?.cycle) { + edge.toFront() + } + }) + graph.getNodes().forEach(node => { + if (node.getData()?.cycle) node.toFront() + }) + } /** * Setup X6 graph plugins (MiniMap, Snapline, Clipboard, Keyboard) */ @@ -526,17 +594,18 @@ export const useWorkflowGraph = ({ enabled: false, beforeAddCommand(_event, args: any) { const event = args?.key ? `cell:change:${args.key}` : _event; - if (event.startsWith('cell:change:') && - event !== 'cell:change:position' && - event !== 'cell:change:source' && - event !== 'cell:change:target') return false; + const allowed = ['cell:added', 'cell:removed', 'cell:change:position', 'cell:change:source', 'cell:change:target']; + if (!allowed.includes(event)) return false; }, }), ); - graphRef.current.on('history:change', ({ cmds }: { cmds: Command[] }) => { + graphRef.current.on('history:change', () => { setCanUndo(graphRef.current?.canUndo() ?? false) setCanRedo(graphRef.current?.canRedo() ?? false) }) + + graphRef.current.on('history:undo', syncChildRelationships) + graphRef.current.on('history:redo', syncChildRelationships) }; // 显示/隐藏连接桩 // const showPorts = (show: boolean) => { @@ -753,8 +822,6 @@ export const useWorkflowGraph = ({ // Find corresponding parent node const parentNode = nodes?.find(n => n.id === nodeData.cycle); if (parentNode) { - // Use removeChild method to delete child node - parentNode.removeChild(nodeToDelete); parentNodesToUpdate.push(parentNode); } // Add child node to deletion list @@ -782,42 +849,51 @@ export const useWorkflowGraph = ({ // Delete all collected nodes and edges if (cells.length > 0) { + // Pre-calculate which parents need an add-node restored (before removal changes the graph) + const parentsNeedingAddNode = parentNodesToUpdate + .filter(parentNode => { + const parentShape = parentNode.shape; + if (parentShape !== 'loop-node' && parentShape !== 'iteration-node') return false; + const parentData = parentNode.getData(); + const allChildren = graphRef.current!.getNodes().filter(n => n.getData()?.cycle === parentData.id); + const cycleStartNodes = allChildren.filter(n => n.getData()?.type === 'cycle-start'); + // After deletion, only cycle-start will remain + const nonCycleStartToDelete = cells.filter(c => + c.isNode() && + (c as Node).getData()?.cycle === parentData.id && + (c as Node).getData()?.type !== 'cycle-start' + ); + return cycleStartNodes.length === 1 && (allChildren.length - nonCycleStartToDelete.length) === 1; + }) + .map(parentNode => ({ + parentNode, + cycleStartNode: graphRef.current!.getNodes().find( + n => n.getData()?.cycle === parentNode.getData().id && n.getData()?.type === 'cycle-start' + )! + })) + .filter(({ cycleStartNode }) => !!cycleStartNode); + + graphRef.current?.startBatch('delete'); graphRef.current?.removeCells(cells); - // If parent is iteration/loop and only cycle-start remains, add add-node connected to it - parentNodesToUpdate.forEach(parentNode => { - const parentShape = parentNode.shape; - if (parentShape !== 'loop-node' && parentShape !== 'iteration-node') return; + parentsNeedingAddNode.forEach(({ parentNode, cycleStartNode }) => { const parentData = parentNode.getData(); - const remainingChildren = graphRef.current!.getNodes().filter( - n => n.getData()?.cycle === parentData.id - ); - const cycleStartNodes = remainingChildren.filter(n => n.getData()?.type === 'cycle-start'); - if (cycleStartNodes.length === 1 && remainingChildren.length === 1) { - const cycleStartNode = cycleStartNodes[0]; - const bbox = cycleStartNode.getBBox(); - const addNode = graphRef.current!.addNode({ - ...graphNodeLibrary.addStart, - x: bbox.x + 84, - y: bbox.y + 4, - data: { - type: 'add-node', - parentId: parentNode.id, - cycle: parentData.id, - label: t('workflow.addNode'), - icon: '+', - }, - }); - parentNode.addChild(addNode); - const sourcePort = cycleStartNode.getPorts().find(p => p.group === 'right')?.id || 'right'; - const targetPort = addNode.getPorts().find(p => p.group === 'left')?.id || 'left'; - graphRef.current!.addEdge({ - source: { cell: cycleStartNode.id, port: sourcePort }, - target: { cell: addNode.id, port: targetPort }, - ...edgeAttrs, - }); - } + const bbox = cycleStartNode.getBBox(); + const addNode = graphRef.current!.addNode({ + ...graphNodeLibrary.addStart, + x: bbox.x + 84, + y: bbox.y + 4, + data: { type: 'add-node', parentId: parentNode.id, cycle: parentData.id, label: t('workflow.addNode'), icon: '+' }, + }); + parentNode.addChild(addNode); + graphRef.current!.addEdge({ + source: { cell: cycleStartNode.id, port: cycleStartNode.getPorts().find(p => p.group === 'right')?.id || 'right' }, + target: { cell: addNode.id, port: addNode.getPorts().find(p => p.group === 'left')?.id || 'left' }, + ...edgeAttrs, + }); }); + + graphRef.current?.stopBatch('delete'); } return false; }; @@ -1193,13 +1269,39 @@ export const useWorkflowGraph = ({ }; if (dragData.type === 'loop' || dragData.type === 'iteration') { - graphRef.current.addNode({ + graphRef.current.startBatch('add-group') + const parentNode = graphRef.current.addNode({ ...graphNodeLibrary[dragData.type], x: point.x - 150, y: point.y - 100, id: cleanNodeData.id, data: { ...cleanNodeData, isGroup: true }, }); + const parentBBox = parentNode.getBBox() + const cycleStartId = `cycle_start_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + const cycleStartNode = graphRef.current.addNode({ + ...graphNodeLibrary.cycleStart, + x: parentBBox.x + 24, + y: parentBBox.y + 70, + id: cycleStartId, + data: { id: cycleStartId, type: 'cycle-start', parentId: cleanNodeData.id, isDefault: true, cycle: cleanNodeData.id }, + }) + const addNode = graphRef.current.addNode({ + ...graphNodeLibrary.addStart, + x: parentBBox.x + 24 + 84, + y: parentBBox.y + 70 + 4, + data: { type: 'add-node', label: t('workflow.addNode'), icon: '+', parentId: cleanNodeData.id, cycle: cleanNodeData.id }, + }) + parentNode.addChild(cycleStartNode) + parentNode.addChild(addNode) + graphRef.current.addEdge({ + source: { cell: cycleStartNode.id, port: cycleStartNode.getPorts().find(p => p.group === 'right')?.id || 'right' }, + target: { cell: addNode.id, port: addNode.getPorts().find(p => p.group === 'left')?.id || 'left' }, + ...edgeAttrs, + }) + cycleStartNode.toFront() + addNode.toFront() + graphRef.current.stopBatch('add-group') } else if (dragData.type === 'if-else') { // Create condition node graphRef.current.addNode({ @@ -1488,20 +1590,16 @@ export const useWorkflowGraph = ({ if (!graphRef.current) return; const nodes = graphRef.current.getNodes(); - const lastWithSub = [...chatHistory].reverse().find(item => item.subContent?.length); - // Reset all node execution status first + // Reset all node execution status on every chatHistory change nodes.forEach(node => { const data = node.getData(); - if (typeof data.executionStatus === 'string') { - node.setData({ ...data, executionStatus: undefined }); - } + node.setData({ ...data, executionStatus: '' }); }); - if (!lastWithSub?.subContent) return; - // Build a nodeId -> status map first - const statusMap: Record = {}; - lastWithSub.subContent.forEach(sub => { + + const lastAssistant = [...chatHistory].reverse().find(item => item.role === 'assistant'); + if (!lastAssistant?.subContent?.length) return; + lastAssistant.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 }); diff --git a/web/src/views/Workflow/utils.ts b/web/src/views/Workflow/utils.ts index 74dfca2c..a4035517 100644 --- a/web/src/views/Workflow/utils.ts +++ b/web/src/views/Workflow/utils.ts @@ -17,6 +17,7 @@ export const isSubExprSet = (sub: any) => { * Uses the same per-expression height logic as getConditionNodeCasePortY. */ export const calcConditionNodeTotalHeight = (cases: any[]) => { + if (!cases?.length) return conditionNodeHeight; const casesHeight = cases.reduce((acc: number, c: any) => { const exprs = c?.expressions ?? []; const n = exprs.length;