From 6d53d9178c7e939d22d4478d47d63e8b8ee0a133 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Sat, 7 Mar 2026 14:55:04 +0800 Subject: [PATCH] feat(web): workflow ui upgrade --- .../components/AddChatVariable/index.tsx | 13 +- .../Workflow/components/CanvasToolbar.tsx | 137 +------- .../views/Workflow/components/Chat/Chat.tsx | 8 +- .../Workflow/components/Chat/chat.module.css | 5 +- .../Workflow/components/Editor/index.tsx | 24 +- .../components/Editor/nodes/VariableNode.tsx | 4 +- .../Editor/plugin/Jinja2HighlightPlugin.tsx | 2 +- .../views/Workflow/components/NodeLibrary.tsx | 118 +++++-- .../Workflow/components/Nodes/AddNode.tsx | 38 ++- .../components/Nodes/ConditionNode.tsx | 67 +++- .../components/Nodes/GroupStartNode.tsx | 5 +- .../Workflow/components/Nodes/LoopNode.tsx | 36 +- .../Workflow/components/Nodes/NodeTools.tsx | 43 +++ .../Workflow/components/Nodes/NormalNode.tsx | 28 +- .../Workflow/components/PortClickHandler.tsx | 29 +- .../Properties/AssignmentList/index.tsx | 48 ++- .../components/Properties/CaseList/index.tsx | 320 +++++++++--------- .../Properties/CategoryList/index.tsx | 74 ++-- .../Properties/CodeExecution/OutputList.tsx | 16 +- .../Properties/CodeExecution/index.module.css | 3 + .../Properties/CodeExecution/index.tsx | 47 ++- .../Properties/ConditionList/index.tsx | 251 ++++++++------ .../Properties/CycleVarsList/index.module.css | 3 + .../Properties/CycleVarsList/index.tsx | 191 ++++++----- .../Properties/GroupVariableList/index.tsx | 31 +- .../Properties/HttpRequest/EditableTable.tsx | 38 +-- .../Properties/HttpRequest/index.tsx | 92 +++-- .../Properties/JinjaRender/index.tsx | 3 +- .../Properties/Knowledge/Knowledge.tsx | 34 +- .../Knowledge/KnowledgeConfigModal.tsx | 10 +- .../Knowledge/KnowledgeGlobalConfigModal.tsx | 6 +- .../Knowledge/KnowledgeListModal.tsx | 47 +-- .../Properties/MappingList/index.tsx | 76 +++-- .../Properties/MemoryConfig/index.tsx | 48 +-- .../components/Properties/MessageEditor.tsx | 31 +- .../Properties/ModelConfig/index.tsx | 74 ++++ .../Properties/ParamsList/index.tsx | 23 +- .../components/Properties/RadioGroupBtn.tsx | 105 ++++++ .../Properties/ToolConfig/index.tsx | 10 +- .../VariableList/VariableEditModal.tsx | 6 +- .../Properties/VariableList/index.tsx | 31 +- .../components/Properties/VariableSelect.tsx | 9 +- .../Workflow/components/Properties/index.tsx | 311 +++++++++-------- .../Properties/properties.module.css | 140 +++++--- web/src/views/Workflow/constant.ts | 241 ++++++------- .../views/Workflow/hooks/useWorkflowGraph.ts | 99 +++--- web/src/views/Workflow/index.tsx | 48 +-- web/tailwind.config.js | 14 +- 48 files changed, 1702 insertions(+), 1335 deletions(-) create mode 100644 web/src/views/Workflow/components/Nodes/NodeTools.tsx create mode 100644 web/src/views/Workflow/components/Properties/CodeExecution/index.module.css create mode 100644 web/src/views/Workflow/components/Properties/CycleVarsList/index.module.css create mode 100644 web/src/views/Workflow/components/Properties/ModelConfig/index.tsx create mode 100644 web/src/views/Workflow/components/Properties/RadioGroupBtn.tsx diff --git a/web/src/views/Workflow/components/AddChatVariable/index.tsx b/web/src/views/Workflow/components/AddChatVariable/index.tsx index d6741bf9..71507466 100644 --- a/web/src/views/Workflow/components/AddChatVariable/index.tsx +++ b/web/src/views/Workflow/components/AddChatVariable/index.tsx @@ -1,6 +1,7 @@ import { useState, useImperativeHandle, forwardRef, useRef } from 'react'; -import { Button, Space, List } from 'antd'; +import { Button, List, Flex } from 'antd'; import { useTranslation } from 'react-i18next'; + import type { ChatVariable, AddChatVariableRef } from '../../types'; import type { ChatVariableModalRef } from './types' import RbDrawer from '@/components/RbDrawer'; @@ -74,15 +75,15 @@ const AddChatVariable = forwardRef(({ dataSource={variables} renderItem={(item, index) => ( -
-
+
+
{item.name} ({t(`workflow.config.parameter-extractor.${item.type}`)})
-
+
{item.description}
- +
handleEdit(index)} @@ -91,7 +92,7 @@ const AddChatVariable = forwardRef(({ className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]" onClick={() => handleDelete(index)} >
-
+
)} diff --git a/web/src/views/Workflow/components/CanvasToolbar.tsx b/web/src/views/Workflow/components/CanvasToolbar.tsx index 8ca272e1..084d231b 100644 --- a/web/src/views/Workflow/components/CanvasToolbar.tsx +++ b/web/src/views/Workflow/components/CanvasToolbar.tsx @@ -2,7 +2,6 @@ import type { FC } from 'react'; import { Select } from 'antd'; // import { Node } from '@antv/x6'; import type { GraphRef } from '../types' -import { PlusOutlined, MinusOutlined } from '@ant-design/icons' interface CanvasToolbarProps { miniMapRef: React.RefObject; @@ -10,150 +9,20 @@ interface CanvasToolbarProps { isHandMode: boolean; setIsHandMode: React.Dispatch>; zoomLevel: number; - canUndo: boolean; - canRedo: boolean; - onUndo: () => void; - onRedo: () => void; } const CanvasToolbar: FC = ({ miniMapRef, graphRef, - // isHandMode, - // setIsHandMode, zoomLevel, - // canUndo, - // canRedo, - // onUndo, - // onRedo, }) => { - // 整理布局函数 - /* - const handleLayout = () => { - if (!graphRef.current) return; - const nodes = graphRef.current.getNodes(); - const edges = graphRef.current.getEdges(); - - // 如果没有连线,使用垂直布局避免节点重叠 - if (edges.length === 0) { - nodes.forEach((node, index) => { - const nodeData = node.getData(); - const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else'; - const nodeHeight = isSpecialNode ? 220 : 50; - const xPosition = 100; - const yPosition = index * (nodeHeight + 100) + 100; - node.setPosition(xPosition, yPosition); - }); - return; - } - - // 简单的树布局算法 - const nodeMap = new Map(); - const children = new Map(); - const roots: string[] = []; - - // 初始化节点映射 - nodes.forEach(node => { - nodeMap.set(node.id, node); - children.set(node.id, []); - }); - - // 构建父子关系 - edges.forEach(edge => { - const sourceId = edge.getSourceCellId(); - const targetId = edge.getTargetCellId(); - if (sourceId && targetId) { - children.get(sourceId)?.push(targetId); - } - }); - - // 找到根节点 - const hasParent = new Set(); - edges.forEach(edge => { - const targetId = edge.getTargetCellId(); - if (targetId) hasParent.add(targetId); - }); - - nodes.forEach(node => { - if (!hasParent.has(node.id)) { - roots.push(node.id); - } - }); - - // 布局参数 - const levelWidths: number[] = []; - const baseNodeSpacing = 120; - let currentY = 100; - - // 计算每层的最大宽度 - const calculateLevelWidths = (nodeId: string, level: number) => { - const node = nodeMap.get(nodeId); - if (!node) return; - - const nodeData = node.getData(); - const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else'; - const nodeWidth = isSpecialNode ? 400 : 160; - const gap = isSpecialNode ? 150 : 100; - - levelWidths[level] = Math.max(levelWidths[level] || 0, nodeWidth + gap); - - const childIds = children.get(nodeId) || []; - childIds.forEach((childId: string) => calculateLevelWidths(childId, level + 1)); - }; - - roots.forEach(rootId => calculateLevelWidths(rootId, 0)); - - // 递归布局函数 - const layoutNode = (nodeId: string, level: number, parentY: number): number => { - const node = nodeMap.get(nodeId); - if (!node) return parentY; - - const nodeData = node.getData(); - const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else'; - const nodeHeight = isSpecialNode ? 220 : 50; - const verticalGap = isSpecialNode ? 80 : 40; - const spacing = baseNodeSpacing + nodeHeight + verticalGap; - - const xPosition = levelWidths.slice(0, level).reduce((sum, width) => sum + width, 100); - - const childIds = children.get(nodeId) || []; - - if (childIds.length === 0) { - // 叶子节点 - node.setPosition(xPosition, currentY); - currentY += spacing; - return currentY - spacing; - } else { - // 非叶子节点,先布局子节点 - const childPositions: number[] = []; - childIds.forEach((childId: string) => { - const childY = layoutNode(childId, level + 1, currentY); - childPositions.push(childY); - }); - - // 父节点居中,确保有足够间隙 - const minY = Math.min(...childPositions); - const maxY = Math.max(...childPositions); - const centerY = (minY + maxY) / 2; - node.setPosition(xPosition, centerY); - return centerY; - } - }; - - // 布局所有根节点 - roots.forEach(rootId => { - layoutNode(rootId, 0, currentY); - currentY += 300; // 不同树之间的间距 - }); - }; - */ return ( <> {/* 小地图 */}
{/* 缩放控制按钮 */} -
- graphRef.current?.zoom(-0.1)} /> +
+
graphRef.current?.zoom(-0.1)}>
({ + ...vo, + label: t(String(vo?.label || '')) + }))} + size="small" + popupMatchSelectWidth={false} + placeholder={t('common.pleaseSelect')} + variant="borderless" + className="rb:w-full!" + /> + + + + + {!hideRightField && ( +
+ {leftFieldType === 'number' + ? + + ({ - ...vo, - label: t(String(vo?.label || '')) - }))} - size="small" - popupMatchSelectWidth={false} - placeholder={t('common.pleaseSelect')} - className="rb:bg-white! rb:w-22!" - /> - -
- - {!hideRightField &&
- {leftFieldType === 'number' - ?
- - = ({ label, name, extra }) => { }))} size="small" popupMatchSelectWidth={false} - className="rb:w-22!" + className="rb:w-27!" />
remove(name)} >
-
+ ))} )} diff --git a/web/src/views/Workflow/components/Properties/CodeExecution/index.module.css b/web/src/views/Workflow/components/Properties/CodeExecution/index.module.css new file mode 100644 index 00000000..4960ff28 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/CodeExecution/index.module.css @@ -0,0 +1,3 @@ +.editor:global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector) { + padding-left: 0 !important; +} \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/CodeExecution/index.tsx b/web/src/views/Workflow/components/Properties/CodeExecution/index.tsx index b9c2c881..2a976bf0 100644 --- a/web/src/views/Workflow/components/Properties/CodeExecution/index.tsx +++ b/web/src/views/Workflow/components/Properties/CodeExecution/index.tsx @@ -1,12 +1,13 @@ import { type FC } from 'react' import { useTranslation } from 'react-i18next' -import { Form, Select, Space, Row, Col, Divider, Button, Tooltip } from 'antd' +import { Form, Select, Flex, Tooltip } from 'antd' import { Node } from '@antv/x6' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import MappingList from '../MappingList' import OutputList from './OutputList' import CodeMirrorEditor from '@/components/CodeMirrorEditor'; +import styles from './index.module.css' interface MappingItem { name?: string @@ -73,40 +74,31 @@ const CodeExecution: FC = ({ options }) => { return ( <> - + - +
} />
- - - - - + prev.language !== curr.language}> {() => ( @@ -117,9 +109,8 @@ const CodeExecution: FC = ({ options }) => { )} - - - + + = ({ {(fields, { add, remove }) => { const logicalOperator = form.getFieldValue([parentName, 'logical_operator']); return ( -
-
-
- {t('workflow.config.loop.condition')} -
+ <> +
+ +
+ {t('workflow.config.loop.condition')} +
- +
+
1 + })} > - + {t('workflow.config.loop.addCondition')} - -
- {fields?.length > 1 &&
-
-
- - - -
-
-
} - {fields.map((field, index) => { - const expressions = form.getFieldValue([parentName, 'expressions']) || []; - const currentExpression = expressions[index] || {}; - const currentOperator = currentExpression.operator; - const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty'; - const leftFieldValue = currentExpression.left; - const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue); - const leftFieldType = leftFieldOption?.dataType; - const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || []; - const inputType = leftFieldType === 'number' ? currentExpression.input_type?.toLocaleLowerCase() : undefined; - console.log('inputType', inputType) - - return ( -
-
-
- - - vo.value.includes('sys.') || - vo.value.includes('conv.') || - vo.nodeData.type === 'loop' || - (vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id) - )} - size="small" - allowClear={false} - popupMatchSelectWidth={false} - placeholder={t('common.pleaseSelect')} - onChange={(val) => handleLeftFieldChange(index, val)} - /> - - - handleInputTypeChange(index)} - /> - - - - {inputType === 'variable' - ? +
+
+ )} + {fields.map((field, index) => { + const expressions = form.getFieldValue([parentName, 'expressions']) || []; + const currentExpression = expressions[index] || {}; + const currentOperator = currentExpression.operator; + const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty'; + const leftFieldValue = currentExpression.left; + const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue); + const leftFieldType = leftFieldOption?.dataType; + const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || []; + const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined; + return ( + +
+ + + vo.dataType === 'number')} + options={options.filter(vo => + vo.value.includes('sys.') || + vo.value.includes('conv.') || + vo.nodeData.type === 'loop' || + (vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id) + )} + size="small" allowClear={false} popupMatchSelectWidth={false} + placeholder={t('common.pleaseSelect')} + onChange={(val) => handleLeftFieldChange(index, val)} variant="borderless" className="rb:w-full!" /> - : + + + + handleInputTypeChange(index)} + /> + + + + {inputType === 'Variable' + ? ( + vo.dataType === 'number')} + allowClear={false} + popupMatchSelectWidth={false} + variant="borderless" + size="small" + /> + ) + : ( + form.setFieldValue([parentName, 'expressions', index, 'right'], value)} + /> + ) + } + + + ) + : ( + + {leftFieldType === 'boolean' + ? + : + } + + ) } - -
- : - {leftFieldType === 'boolean' - ? - True - False - - : - } - - } -
} -
-
remove(field.name)} - >
-
- ) - })} -
+
+ )} +
+
remove(field.name)} + >
+ + ) + })} +
+
+ ) }} diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.module.css b/web/src/views/Workflow/components/Properties/CycleVarsList/index.module.css new file mode 100644 index 00000000..a6691a84 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.module.css @@ -0,0 +1,3 @@ +.select:global(.ant-select-single.ant-select-sm .ant-select-selector) { + background: #F6F6F6; +} \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx index dfa82f0a..77ca0e61 100644 --- a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx @@ -1,9 +1,10 @@ import { type FC } from 'react' import { useTranslation } from 'react-i18next'; -import { Form, Select, Input, Button, InputNumber, Radio } from 'antd' -import VariableSelect from '../VariableSelect' +import { Form, Select, Input, Button, InputNumber, Flex } from 'antd' +import VariableSelect from '../VariableSelect' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' +import RadioGroupBtn from '../RadioGroupBtn' interface CycleVar { name: string; @@ -81,101 +82,113 @@ const CycleVarsList: FC = ({ return ( {(fields, { add, remove }) => ( - <> -
+ + {t('workflow.config.loop.cycle_vars')} -
- {fields.map(({ key, name }, index) => { - const currentType = value?.[index]?.type; - const currentInputType = value?.[index]?.input_type; - - return ( -
-
-
- - - - - { - form.setFieldValue([parentName, index, 'value'], undefined); - }} - className="rb:w-18!" - /> - -
- - - {currentInputType === 'variable' - ? ( - { - const currentType = value?.[index]?.type; - if (!currentType) return true; + + + {fields.map(({ key, name }, index) => { + const currentType = value?.[index]?.type; + const currentInputType = value?.[index]?.input_type; + + return ( + + + + + + + + { + form.setFieldValue([parentName, index, 'value'], undefined); + }} + className={`rb:w-25! select`} + variant="borderless" + /> + + + + + {currentInputType === 'variable' + ? ( + { + const currentType = value?.[index]?.type; + if (!currentType) return true; - return option.dataType === currentType - })} - variant="borderless" - size="small" - /> - ) - : currentType === 'number' - ? form.setFieldValue([name, 'value'], value)} - /> - : currentType === 'boolean' - ? - True - False - - : ( - - )} - -
-
remove(name)} - >
-
- ) - })} - + return option.dataType === currentType + })} + variant="borderless" + size="small" + className="select" + /> + ) + : currentType === 'number' + ? form.setFieldValue([name, 'value'], value)} + /> + : currentType === 'boolean' + ? + : ( + + ) + } + + +
remove(name)} + >
+ + ) + })} + + )}
) diff --git a/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx b/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx index a9b67dac..3f215bca 100644 --- a/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx +++ b/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx @@ -1,12 +1,12 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 15:17:39 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 15:17:39 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-03 10:54:15 */ import { useEffect, type FC } from 'react' import { useTranslation } from 'react-i18next'; -import { Form, Input, Button, Row, Col } from 'antd' +import { Form, Input, Button, Flex } from 'antd' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import VariableSelect from '../VariableSelect' @@ -126,8 +126,9 @@ const GroupVariableList: FC = ({ {fields.map(({ key, name, ...restField }) => { return (
- - + + +
= ({ ]} noStyle > - {isCanAdd ? : t('workflow.config.var-aggregator.variable')} + {isCanAdd + ? + : t('workflow.config.var-aggregator.variable') + } - +
- {isCanAdd && + {isCanAdd && (
remove(name)} >
- } -
+ )} + = ({ const getColumns = (remove: (index: number) => void): TableProps['columns'] => { const hasType = typeOptions.length > 0; - const cellClassName="rb:p-1!" - const contentClassName ="rb:w-[108px]! rb:text-[12px]! rb:overflow-hidden!" + const contentClassName = hasType ? 'rb:w-[120px]!' : "rb:w-[154px]!" + const formClassName = 'rb:mb-0! rb:bg-[#F6F6F6] rb:rounded-[8px] rb:py-[2px]! rb:px-[6px]!' return [ { title: t('workflow.config.name'), dataIndex: 'name', - className: cellClassName, render: (_: any, __: TableRow, index: number) => ( - + !option.dataType.includes('file'))} type="input" className={contentClassName} size={size} - height={16} + variant="borderless" /> ) @@ -72,7 +70,6 @@ const EditableTable: FC = ({ title: t('workflow.config.type'), dataIndex: 'type', width: '20%', - className: cellClassName, render: (_: any, __: TableRow, index: number) => ( {(form) => ( @@ -86,6 +83,8 @@ const EditableTable: FC = ({ form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined); }} size={size} + variant="borderless" + className="rb:w-17! select" /> )} @@ -95,7 +94,6 @@ const EditableTable: FC = ({ { title: t('workflow.config.value'), dataIndex: 'value', - className: cellClassName, render: (_: any, __: TableRow, index: number) => ( { @@ -107,18 +105,18 @@ const EditableTable: FC = ({ > {(form) => { const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']); - const filteredOptions = currentType === 'file' + const filteredOptions = currentType === 'file' ? booleanFilterOptions.filter(option => option.dataType.includes('file')) : booleanFilterOptions.filter(option => !option.dataType.includes('file')); return ( - + ); @@ -129,10 +127,9 @@ const EditableTable: FC = ({ { title: '', dataIndex: 'actions', - className: cellClassName, render: (_: any, __: TableRow, index: number) => (
remove(index)} >
) @@ -146,27 +143,26 @@ const EditableTable: FC = ({ {(fields, { add, remove }) => { const AddButton = ({ block = false }: { block?: boolean }) => ( ); return ( <> {title && ( -
+
{title}
-
+ )} - bordered + bordered={false} dataSource={fields.map((field) => ({ key: String(field.key), name: undefined, @@ -176,9 +172,7 @@ const EditableTable: FC = ({ columns={getColumns(remove)} pagination={false} size="small" - rowClassName="rb:p-0! rb:bg-[#F6F8FC]!" locale={{ emptyText: }} - style={{ width: '274px' }} /> {!title && } diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx index 840bff9a..440b4b8a 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx @@ -2,11 +2,11 @@ * @Author: ZhaoYing * @Date: 2026-02-09 18:35:43 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-02 17:24:51 + * @Last Modified time: 2026-03-04 15:20:32 */ import { type FC, useRef, useState } from "react"; import { useTranslation } from 'react-i18next' -import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd' +import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input, Flex, Radio } from 'antd' import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons'; import Editor from '../../Editor' @@ -15,7 +15,7 @@ import AuthConfigModal from './AuthConfigModal' import type { AuthConfigModalRef, HttpRequestConfigForm } from './types' import MessageEditor from '../MessageEditor' import EditableTable from './EditableTable' -import { portTextAttrs } from '../../../constant' +import { portTextAttrs, nodeWidth, portItemArgsY } from '../../../constant' const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({ options, @@ -35,8 +35,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an form.setFieldsValue({ auth }) } - const handleChangeBodyContentType = () => { - form.setFieldValue(['body', 'data'], undefined) + const handleChangeBodyContentType = (e: any) => { + const value = e.target.value || e.target.value + form.setFieldValue(['body', 'data'], ['form-data', 'x-www-form-urlencoded'].includes(value) ? [{}] : undefined) } // Handle error handling method change and update node ports accordingly @@ -61,6 +62,10 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an selectedNode.addPort({ id: 'ERROR', group: 'right', + args: { + x: nodeWidth, + y: portItemArgsY + portItemArgsY, + }, attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }} }); } else if (method !== 'branch' && errorPort) { @@ -81,7 +86,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an return ( <> -
+
API
-
+ @@ -113,6 +118,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an variant="outlined" type="input" size="small" + height={28} /> @@ -139,9 +145,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
- - - + +
{t('workflow.config.http-request.error_handle')}
+ + status_code number} + label={<> + status_code + number + } + className="rb:my-2!" > headers object} + label={<> + headers + object + } + className="rb:my-2!" > } + = ({ selectedNode, options, templateOpti return ( <> - + @@ -188,6 +188,7 @@ const JinjaRender: FC = ({ selectedNode, options, templateOpti options={templateOptions} titleVariant="borderless" size="small" + className="rb:bg-[#F6F6F6] rb:border-[#F6F6F6]! rb:hover:bg-white rb:hover:border-[#171719]!" /> diff --git a/web/src/views/Workflow/components/Properties/Knowledge/Knowledge.tsx b/web/src/views/Workflow/components/Properties/Knowledge/Knowledge.tsx index 3cd7efcd..54a1e7ed 100644 --- a/web/src/views/Workflow/components/Properties/Knowledge/Knowledge.tsx +++ b/web/src/views/Workflow/components/Properties/Knowledge/Knowledge.tsx @@ -1,7 +1,7 @@ import { type FC, useRef, useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Space, Button } from 'antd' -import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg' +import { Space, Button, Flex } from 'antd' + import type { KnowledgeConfigForm, KnowledgeConfig, @@ -11,7 +11,6 @@ import type { KnowledgeConfigModalRef, KnowledgeGlobalConfigModalRef, } from './types' -import Empty from '@/components/Empty' import KnowledgeListModal from './KnowledgeListModal' import KnowledgeConfigModal from './KnowledgeConfigModal' import KnowledgeGlobalConfigModal from './KnowledgeGlobalConfigModal' @@ -113,7 +112,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi } return (
-
+
{t('application.knowledgeBaseAssociation')}
@@ -122,15 +121,16 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi onClick={handleKnowledgeConfig} className="rb:py-0! rb:px-1! rb:text-[12px]! rb:group rb:gap-0.5!" size="small" + disabled={knowledgeList.length === 0} >
{t('application.globalConfig')} -
+ - +
{/* 结果重排 */} -
+
{t('application.rerankModel')}
{t('application.rerankModelDesc')}
@@ -87,7 +87,7 @@ const KnowledgeGlobalConfigModal = forwardRef -
+
{values?.rerank_model && <> (({ const [visible, setVisible] = useState(false); const [list, setList] = useState([]) const [filterList, setFilterList] = useState([]) - const [query, setQuery] = useState<{keywords?: string}>({}) const [selectedIds, setSelectedIds] = useState([]) const [selectedRows, setSelectedRows] = useState([]) + const [form] = Form.useForm() + const query = Form.useWatch([], form) + // 封装取消方法,添加关闭弹窗逻辑 const handleClose = () => { setVisible(false); - setQuery({}) + form.resetFields() setSelectedIds([]) setSelectedRows([]) }; const handleOpen = () => { setVisible(true); - setQuery({}) + form.resetFields() setSelectedIds([]) setSelectedRows([]) }; @@ -45,7 +48,7 @@ const KnowledgeListModal = forwardRef(({ if (visible) { getList() } - }, [query.keywords, visible]) + }, [query?.keywords, visible]) const getList = () => { getKnowledgeBaseList(undefined, { ...query, @@ -77,11 +80,6 @@ const KnowledgeListModal = forwardRef(({ handleOpen, handleClose })); - const handleSearch = (value?: string) => { - setQuery({keywords: value}) - setSelectedIds([]) - setSelectedRows([]) - } const handleSelect = (item: KnowledgeBase) => { const index = selectedIds.indexOf(item.id) if (index === -1) { @@ -112,22 +110,27 @@ const KnowledgeListModal = forwardRef(({ onOk={handleSave} width={1000} > - - + +
+ + + +
{filterList.length === 0 ? : ( - -
+ handleSelect(item)}>
@@ -135,12 +138,12 @@ const KnowledgeListModal = forwardRef(({
{t('application.contains', {include_count: item.doc_num})}
{formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')}
-
+
)} /> } -
+ ); diff --git a/web/src/views/Workflow/components/Properties/MappingList/index.tsx b/web/src/views/Workflow/components/Properties/MappingList/index.tsx index d0f56e1c..d4395736 100644 --- a/web/src/views/Workflow/components/Properties/MappingList/index.tsx +++ b/web/src/views/Workflow/components/Properties/MappingList/index.tsx @@ -1,6 +1,7 @@ import { type FC, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next' -import { Button, Form, Input, Divider, Space } from 'antd'; +import { Button, Form, Input, Space, Flex } from 'antd'; + import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import VariableSelect from '../VariableSelect' @@ -18,7 +19,7 @@ const MappingList: FC = ({ label, name, options, extra, valueK {(fields, { add, remove }) => ( <> -
+
{label}
@@ -27,49 +28,50 @@ const MappingList: FC = ({ label, name, options, extra, valueK {extra} -
- {fields.map(({ key, name, ...restField }) => ( -
- - - - - - -
remove(name)} - >
-
- ))} + + + {fields.map(({ key, name, ...restField }) => ( + + + + + + + +
remove(name)} + >
+
+ ))} +
)}
- ) }; diff --git a/web/src/views/Workflow/components/Properties/MemoryConfig/index.tsx b/web/src/views/Workflow/components/Properties/MemoryConfig/index.tsx index 9e33e73e..edac612d 100644 --- a/web/src/views/Workflow/components/Properties/MemoryConfig/index.tsx +++ b/web/src/views/Workflow/components/Properties/MemoryConfig/index.tsx @@ -1,8 +1,10 @@ import { type FC } from "react"; import { useTranslation } from 'react-i18next' -import { Form, Row, Col, Divider, Switch, Slider } from 'antd' +import { Form, Switch, Flex } from 'antd' + import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import MessageEditor from '../MessageEditor' +import RbSlider from "@/components/RbSlider"; const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({ options, @@ -29,12 +31,15 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({ return ( <> + + + {values?.memory?.enable && <> -
+ {t('workflow.config.llm.memory')} {t('workflow.config.llm.inner')} -
- + + = ({ size="small" /> - - - } - - - - {values?.memory?.enable && <> - - - - - - {t('workflow.config.llm.enable_window')} - - - - - - - +
+ + + + + + +
} ); diff --git a/web/src/views/Workflow/components/Properties/MessageEditor.tsx b/web/src/views/Workflow/components/Properties/MessageEditor.tsx index 83aa289c..0aa0dc77 100644 --- a/web/src/views/Workflow/components/Properties/MessageEditor.tsx +++ b/web/src/views/Workflow/components/Properties/MessageEditor.tsx @@ -1,7 +1,8 @@ import { type FC, type ReactNode, useMemo } from 'react'; import clsx from 'clsx' import { useTranslation } from 'react-i18next' -import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd'; +import { Input, Form, Button, Row, Col, Select, type FormListOperation, Flex } from 'antd'; + import Editor, { type LexicalEditorProps } from '../Editor' import type { Suggestion } from '../Editor/plugin/AutocompletePlugin' @@ -16,7 +17,8 @@ interface MessageEditor { value?: string; language?: LexicalEditorProps['language']; onChange?: (value?: string) => void; - size?: 'small' | 'default' + size?: 'small' | 'default'; + className?: string; } const roleOptions = [ // { label: 'SYSTEM', value: 'SYSTEM' }, @@ -31,7 +33,8 @@ const MessageEditor: FC = ({ placeholder, options = [], language, - size = 'default' + size = 'default', + className }) => { const { t } = useTranslation() const form = Form.useFormInstance(); @@ -78,12 +81,12 @@ const MessageEditor: FC = ({ if (!isArray) { return ( - + {typeof title === 'string' - ?
{title ?? t('workflow.answerDesc')}
: title} @@ -91,14 +94,14 @@ const MessageEditor: FC = ({ -
+ ); } return ( {(fields, { add, remove }) => ( - + {fields.map(({ key, name, ...restField }) => { const fieldValue = Array.isArray(parentName) ? parentName.reduce((obj, key) => obj?.[key], values) @@ -107,12 +110,12 @@ const MessageEditor: FC = ({ const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase(); return ( - + {currentRole === 'SYSTEM' ? ( - + ) : ( = ({ : selectedNode?.data?.type === 'http-request' - ? + ? : selectedNode?.data?.type === 'tool' ? : selectedNode?.data?.type === 'jinja-render' @@ -485,7 +509,7 @@ const Properties: FC = ({ if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') { return ( - + = ({ ) } + if (key === 'model_id' && selectedNode?.data?.type === 'llm') { + return + } if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') { // 为llm节点且isArray=true时添加context变量支持 let contextVariableList = [...getFilteredVariableList('llm')]; const isArrayMode = config.isArray !== false; // 默认为true - + if (isArrayMode) { const contextKey = `${selectedNode.id}_context`; const hasContextVariable = contextVariableList.some(v => v.key === contextKey); - + if (!hasContextVariable) { contextVariableList.unshift({ key: contextKey, @@ -520,7 +547,7 @@ const Properties: FC = ({ variable.nodeData?.type !== 'knowledge-retrieval')} + options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')} parentName={key} placeholder={t(config.placeholder || 'common.pleaseSelect')} size="small" @@ -548,10 +575,11 @@ const Properties: FC = ({ if (config.type === 'messageEditor') { return ( - - + = ({ label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)} /> - + ) } if (config.type === 'groupVariableList') { @@ -586,7 +614,7 @@ const Properties: FC = ({ } if (config.type === 'caseList') { return ( - + = ({ options={(() => { if (config.filterLoopIterationVars) { const loopIterationVars: Suggestion[] = []; - + return [...getFilteredVariableList(selectedNode?.data?.type, key), ...loopIterationVars]; } return getFilteredVariableList(selectedNode?.data?.type, key); })() - } - /> + } + /> ) } @@ -674,103 +702,117 @@ const Properties: FC = ({ } return ( - {t(`workflow.config.${selectedNode?.data?.type}.${key}`)} - : t(`workflow.config.${selectedNode?.data?.type}.${key}`) + ? {t(`workflow.config.${selectedNode?.data?.type}.${key}`)} + : t(`workflow.config.${selectedNode?.data?.type}.${key}`) } layout={config.type === 'switch' ? 'horizontal' : 'vertical'} - className={key === 'parallel_count' ? 'rb:-mt-3! rb:leading-3.5!' : ''} + className={ + key === 'parallel' && values?.parallel + ? 'rb:mb-1!' + : key === 'vision' && values?.vision + ? 'rb:mb-2!' + : key === 'group' && values?.group + ? 'rb:mb-3!' + : '' + } hidden={Boolean(config.hidden)} > {config.type === 'input' ? : config.type === 'textarea' - ? - : config.type === 'select' - ? ({ ...vo, label: t(vo.label) })) : config.options} + placeholder={t('common.pleaseSelect')} + /> + : config.type === 'inputNumber' + ? form.setFieldValue(key, value)} + /> + : config.type === 'slider' + ? + : config.type === 'customSelect' + ? + : config.type === 'variableList' + ? { + const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key); + // Apply filtering if specified in config + if (config.filterNodeTypes || config.filterVariableNames) { + return baseVariableList.filter(variable => { + const nodeTypeMatch = !config.filterNodeTypes || + (Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type)); + const variableNameMatch = !config.filterVariableNames || + (Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label)); + return nodeTypeMatch || variableNameMatch; + }); + } + if (config.onFilterVariableNames) { + return baseVariableList.filter(variable => Array.isArray(config.onFilterVariableNames) && config.onFilterVariableNames.includes(variable.label)); + } + // Filter child nodes for iteration output + if (config.filterChildNodes && selectedNode) { + const graph = graphRef.current; + if (!graph) return []; + + const nodes = graph.getNodes(); + + // Find child nodes whose cycle field equals parent node's ID + const childNodes = nodes.filter(node => { + const nodeData = node.getData(); + return nodeData?.cycle === selectedNode.id; + }); + + return baseVariableList.filter(variable => + childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.') + ); + } + return baseVariableList; + })()} + onChange={(value, option) => handleChangeVariableList(value, option, key)} + size="small" + /> + : config.type === 'switch' + ? { form.setFieldValue('group_variables', []) } + : key === 'vision' + ? () => { form.setFieldValue('vision_input', undefined) } + : undefined + } /> + : config.type === 'categoryList' + ? + : config.type === 'editor' + ? + : null } ) @@ -779,23 +821,26 @@ const Properties: FC = ({ {currentNodeVariables.length > 0 && !(!values?.group && selectedNode.getData().type === 'var-aggregator') && -
- - -
+
+ + {t('workflow.config.output')} - {outputCollapsed ? : } -
+
+ {!outputCollapsed && currentNodeVariables.map(vo => ( -
+ {vo.label} - {vo.dataType} -
+ {vo.dataType} + ))} - +
} -
} +
); }; diff --git a/web/src/views/Workflow/components/Properties/properties.module.css b/web/src/views/Workflow/components/Properties/properties.module.css index 4820788f..fdce9376 100644 --- a/web/src/views/Workflow/components/Properties/properties.module.css +++ b/web/src/views/Workflow/components/Properties/properties.module.css @@ -7,51 +7,46 @@ } .properties :global(.ant-input-outlined.ant-input-disabled), .properties :global(.ant-input-outlined[disabled]) { - background-color: #F6F8FC; + background-color: #F6F6F6; } -.properties :global(.ant-select-single.ant-select-sm){ +.properties :global(.ant-select-single.ant-select-sm) { + height: 28px; +} +.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selector) { + min-height: 28px; +} +.properties :global(:not(.select).ant-select-single.ant-select-sm.ant-select-borderless) { + height: 22px; +} +.properties :global(.select.ant-select-single.ant-select-sm.ant-select-borderless) { height: 28px; } .properties :global(.ant-table-wrapper .ant-table-thead>tr>th), .properties :global(.ant-table-wrapper .ant-table-thead>tr>td), -.properties :global(.ant-table-wrapper .ant-table) { - background-color: #F6F8FC; -} -.properties :global(.ant-table-wrapper .ant-table), -.properties :global(.ant-table-container), -.properties :global(.ant-table-wrapper table) { - border-radius: 6px; -} -.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:first-child) { - border-start-start-radius: 6px; -} -.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:last-child) { - border-start-end-radius: 6px; -} -.properties :global(.ant-table-row:last-child .ant-table-cell:first-child) { - border-bottom-left-radius: 6px; -} -.properties :global(.ant-table-row:last-child .ant-table-cell:last-child) { - border-bottom-right-radius: 6px; -} -.properties :global(.ant-table-wrapper .ant-table) { - background: transparent; -} -.properties :global(.ant-table-wrapper .ant-table-container) { - border-start-start-radius: 6px; - border-start-end-radius: 6px; -} -.properties :global(.ant-table-container) { - /* border-left: none; - border-top: none; - border-bottom: none; */ - border: none; -} +.properties :global(.ant-table-wrapper .ant-table-tbody>tr>td), .properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>th), .properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>td), .properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder), -.properties :global(.ant-table-wrapper .ant-table) { - background-color: #F6F8FC; +.properties :global(.ant-table-wrapper .ant-table-wrapper .ant-table-thead>tr>th), +.properties :global(.ant-table-wrapper .ant-table-thead>tr>td) +.properties :global(.ant-table-wrapper .ant-table), +.properties :global(.ant-table-container), +.properties :global(.ant-table-wrapper .ant-table-tbody .ant-table-row>.ant-table-cell-row-hover) { + background-color: transparent; + border: none; +} +.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-title), +.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-footer), +.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-cell), +.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-thead>tr>th), +.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-tbody>tr>th), +.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-tbody>tr>td), +.properties :global(.ant-table-wrapper .ant-table.ant-table-small tfoot>tr>th), +.properties :global(.ant-table-wrapper .ant-table.ant-table-small tfoot>tr>td) { + padding: 0; +} +.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-tbody>tr>td) { + padding: 4px 4px 0 0; } .properties :global(.ant-form-item-horizontal.ant-form-item .ant-form-item-control-input-content:has(> .ant-switch:only-child, > .ant-rate:only-child)) { display: flex; @@ -62,12 +57,15 @@ } .properties :global(.ant-form-item) { margin-bottom: 16px; + line-height: 17px; } .properties :global(.ant-form-item .ant-form-item-label>label) { font-weight: 500; font-size: 12px; + height: 17px; } .properties :global(.ant-select-single.ant-select-sm .ant-select-selector), +.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selector), .properties :global(.ant-select-dropdown .ant-select-item),.properties :global(.ant-input-number-sm) { font-size: 12px; } @@ -77,7 +75,7 @@ .properties :global(.ant-slider-horizontal .ant-slider-step) { height: 6px; } -.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector) { +.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector){ padding: 0 4px 0 6px ; } .properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input).ant-select-show-arrow .ant-select-selection-item), @@ -88,6 +86,72 @@ font-size: 10px; inset-inline-end: 6px; } +.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selector) { + padding-block: 2px; +} .properties :global(.ant-input-sm) { padding: 3.6px 7px; +} +.properties :global(.ant-divider) { + border-block-start: 1px solid #EBEBEB; +} +.properties :global(.ant-switch.ant-switch-small) { + height: 16px; + line-height: 16px; +} +.properties :global(.ant-switch.ant-switch-small .ant-switch-handle) { + width: 13.6px; + height: 13.6px; + top: 1.5px; + inset-inline-start: 1.5px; +} +.properties :global(.ant-switch.ant-switch-small.ant-switch-checked .ant-switch-handle) { + inset-inline-start: calc(100% - 15px); +} +.properties :global(.ant-select .ant-select-selection-item) { + font-weight: 500; +} +.properties :global(.ant-input-number-sm) { + padding-top: 3px; + padding-bottom: 3px; +} + +.properties :global(.select.ant-select-borderless .ant-select-selector) { + background: #F6F6F6 !important; +} +.properties :global(.ant-radio-group) { + display: grid; + grid-template-columns: 48px 73px 172px; + gap: 8px 18px; +} +.properties :global(.ant-radio-wrapper) { + font-size: 12px; + line-height: 18px; + margin-inline-end: 0; +} +.properties :global(.ant-radio-wrapper .ant-radio-inner) { + width: 12px; + height: 12px; + border-color: #A8A9AA; +} +.properties :global(.ant-radio-wrapper .ant-radio-inner::after) { + width: 12px; + height: 12px; + margin-block-start: -6px; + margin-inline-start: -6px; +} +.properties :global(.ant-radio-wrapper .ant-radio-checked .ant-radio-inner) { + border-color: #171719; +} +.properties :global(.ant-radio-wrapper .ant-radio-checked::after) { + border: 4px solid #171719; +} +.properties :global(.ant-radio-wrapper span.ant-radio+*) { + padding-inline-start: 3px; + padding-inline-end: 0; +} +.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selection-overflow .ant-select-selection-item) { + padding-inline-start: 0px; + border-radius: 4px; + margin-block: 0px; } \ No newline at end of file diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index e7d2177a..2a132dc7 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:06:18 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-11 12:07:20 + * @Last Modified time: 2026-03-06 14:52:02 */ import LoopNode from './components/Nodes/LoopNode'; import NormalNode from './components/Nodes/NormalNode'; @@ -13,40 +13,24 @@ import type { PortMetadata, GroupMetadata } from '@antv/x6/lib/model/port'; import type { ReactShapeConfig } from '@antv/x6-react-shape'; // Import workflow icons -import startIcon from '@/assets/images/workflow/start.png'; -import endIcon from '@/assets/images/workflow/end.png'; -// import answerIcon from '@/assets/images/workflow/answer.png'; -import llmIcon from '@/assets/images/workflow/llm.png'; -// import modelSelectionIcon from '@/assets/images/workflow/model_selection.png'; -// import modelVotingIcon from '@/assets/images/workflow/model_voting.png'; -import ragIcon from '@/assets/images/workflow/rag.png'; -// import classificationIcon from '@/assets/images/workflow/classification.png'; -import parameterExtractionIcon from '@/assets/images/workflow/parameter_extraction.png'; -// import taskPlanningIcon from '@/assets/images/workflow/task_planning.png'; -// import reasoningControlIcon from '@/assets/images/workflow/reasoning_control.png'; -// import selfReflectionIcon from '@/assets/images/workflow/self_reflection.png'; -// import memoryEnhancementIcon from '@/assets/images/workflow/memory_enhancement.png'; -// import agentSchedulingIcon from '@/assets/images/workflow/agent_scheduling.png'; -// import agentCollaborationIcon from '@/assets/images/workflow/agent_collaboration.png'; -// import agentArbitrationIcon from '@/assets/images/workflow/agent_arbitration.png'; -import conditionIcon from '@/assets/images/workflow/condition.png'; -import iterationIcon from '@/assets/images/workflow/iteration.png'; -import loopIcon from '@/assets/images/workflow/loop.png'; -// import parallelIcon from '@/assets/images/workflow/parallel.png'; -import aggregatorIcon from '@/assets/images/workflow/aggregator.png'; -import httpRequestIcon from '@/assets/images/workflow/http_request.png'; -import toolsIcon from '@/assets/images/workflow/tools.png'; -import codeExecutionIcon from '@/assets/images/workflow/code_execution.png'; -import templateRenderingIcon from '@/assets/images/workflow/template_rendering.png'; -// import sensitiveDetectionIcon from '@/assets/images/workflow/sensitive_detection.png'; -// import outputAuditIcon from '@/assets/images/workflow/output_audit.png'; -// import selfOptimizationIcon from '@/assets/images/workflow/self_optimization.png'; -// import processEvolutionIcon from '@/assets/images/workflow/process_evolution.png'; -import questionClassifierIcon from '@/assets/images/workflow/question-classifier.png' -import breakIcon from '@/assets/images/workflow/break.png' -import assignerIcon from '@/assets/images/workflow/assigner.png' -import memoryReadIcon from '@/assets/images/workflow/memory-read.png' -import memoryWriteIcon from '@/assets/images/workflow/memory-write.png' +import startIcon from '@/assets/images/workflow/start.svg'; +import endIcon from '@/assets/images/workflow/end.svg'; +import llmIcon from '@/assets/images/workflow/llm.svg'; +import ragIcon from '@/assets/images/workflow/rag.svg'; +import parameterExtractionIcon from '@/assets/images/workflow/parameter_extraction.svg'; +import conditionIcon from '@/assets/images/workflow/condition.svg'; +import iterationIcon from '@/assets/images/workflow/iteration.svg'; +import loopIcon from '@/assets/images/workflow/loop.svg'; +import aggregatorIcon from '@/assets/images/workflow/aggregator.svg'; +import httpRequestIcon from '@/assets/images/workflow/http_request.svg'; +import toolsIcon from '@/assets/images/workflow/tools.svg'; +import codeExecutionIcon from '@/assets/images/workflow/code_execution.svg'; +import templateRenderingIcon from '@/assets/images/workflow/template_rendering.svg'; +import questionClassifierIcon from '@/assets/images/workflow/question-classifier.svg' +import breakIcon from '@/assets/images/workflow/break.svg' +import assignerIcon from '@/assets/images/workflow/assigner.svg' +import memoryReadIcon from '@/assets/images/workflow/memory-read.svg' +import memoryWriteIcon from '@/assets/images/workflow/memory-write.svg' import unknownIcon from '@/assets/images/workflow/unknown.svg' import { memoryConfigListUrl } from '@/api/memory' @@ -119,21 +103,21 @@ export const nodeLibrary: NodeLibrary[] = [ { type: "llm", icon: llmIcon, config: { model_id: { - type: 'customSelect', + type: 'define', url: getModelListUrl, params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat valueKey: 'id', labelKey: 'name', }, temperature: { - type: 'slider', + type: 'define', max: 2, min: 0, step: 0.1, defaultValue: 0.7 }, max_tokens: { - type: 'slider', + type: 'define', max: 32000, min: 256, step: 1, @@ -171,8 +155,6 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - // { type: "model_selection", icon: modelSelectionIcon }, - // { type: "model_voting", icon: modelVotingIcon }, { type: "knowledge-retrieval", icon: ragIcon, config: { query: { @@ -183,7 +165,6 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - // { type: "classification", icon: classificationIcon }, { type: "parameter-extractor", icon: parameterExtractionIcon, config: { model_id: { @@ -260,14 +241,6 @@ export const nodeLibrary: NodeLibrary[] = [ }, ] }, - // { - // category: "agentCollaborationNode", - // nodes: [ - // { type: "agent_scheduling", icon: agentSchedulingIcon }, - // { type: "agent_collaboration", icon: agentCollaborationIcon }, - // { type: "agent_arbitration", icon: agentArbitrationIcon } - // ] - // }, { category: "flowControl", nodes: [ @@ -306,7 +279,8 @@ export const nodeLibrary: NodeLibrary[] = [ user_supplement_prompt: { type: 'messageEditor', isArray: false, - titleVariant: 'borderless' + titleVariant: 'borderless', + placeholder: 'common.pleaseEnter' } } }, @@ -366,9 +340,8 @@ export const nodeLibrary: NodeLibrary[] = [ }, } }, - { type: "cycle-start", icon: loopIcon }, + { type: "cycle-start", icon: startIcon }, { type: "break", icon: breakIcon }, - // { type: "parallel", icon: parallelIcon }, { type: "var-aggregator", icon: aggregatorIcon, config: { group: { @@ -510,20 +483,6 @@ export const nodeLibrary: NodeLibrary[] = [ }, ] }, - // { - // category: "safetyAndCompliance", - // nodes: [ - // { type: "sensitive_detection", icon: sensitiveDetectionIcon }, - // { type: "output_audit", icon: outputAuditIcon } - // ] - // }, - // { - // category: "evolutionAndGovernance", - // nodes: [ - // { type: "self_optimization", icon: selfOptimizationIcon }, - // { type: "process_evolution", icon: processEvolutionIcon } - // ] - // }, ]; export const unknownNode = { type: 'unknown', @@ -531,6 +490,10 @@ export const unknownNode = { } export const nodeWidth = 240; + +export const conditionNodePortItemArgsY = 60; +export const conditionNodeItemHeight = 26; +export const conditionNodeHeight = 110; /** * Node registration library for X6 graph * Maps node shapes to their React components @@ -557,19 +520,19 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [ { shape: 'condition-node', width: nodeWidth, - height: 88, + height: conditionNodeHeight, component: ConditionNode, }, { shape: 'cycle-start', - width: 44, - height: 44, + width: 36, + height: 36, component: GroupStartNode, }, { shape: 'add-node', - width: 88, - height: 44, + width: 100, + height: 28, component: AddNode, }, ]; @@ -599,10 +562,12 @@ interface NodeConfig { } /** Edge color for normal state */ -export const edge_color = '#155EEF'; +export const edge_color = '#D4D5D9'; /** Edge color for selected state */ -export const edge_selected_color = '#4DA8FF' - +export const edge_selected_color = '#171719' +export const edge_width = 2; +/** Port color */ +export const port_color = '#171719' /** * Unified port markup configuration * Defines SVG elements for port rendering @@ -626,9 +591,9 @@ export const portAttrs = { body: { r: 6, magnet: true, - stroke: edge_color, - strokeWidth: 2, - fill: edge_color, + stroke: port_color, + strokeWidth: edge_width, + fill: port_color, }, label: { text: '+', @@ -641,36 +606,33 @@ export const portAttrs = { }, } export const portTextAttrs = { fontSize: 12, fill: '#5B6167' } +/** + * Port position arguments + */ +export const portItemArgsY = 26; +export const portArgs = { x: nodeWidth, y: portItemArgsY } + +const defaultPortGroup = { + position: { name: 'absolute' }, + markup: portMarkup, + attrs: portAttrs +} /** * Unified port group configuration * Defines port positions and attributes for different sides */ -const defaultPortGroups = { - // top: { position: 'top', markup: portMarkup, attrs: portAttrs }, - right: { position: 'right', markup: portMarkup, attrs: portAttrs }, - // bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs }, - left: { position: 'left', markup: portMarkup, attrs: portAttrs }, -} export const defaultAbsolutePortGroups = { - // top: { position: 'top', markup: portMarkup, attrs: portAttrs }, - right: { position: { name: 'absolute' }, markup: portMarkup, attrs: portAttrs }, - // bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs }, - left: { position: 'left', markup: portMarkup, attrs: portAttrs }, + right: defaultPortGroup, + left: defaultPortGroup, } /** * Default port items for standard nodes */ -const defaultPortItems = [ - // { group: 'top' }, - { group: 'right' }, - // { group: 'bottom' }, - { group: 'left' } +export const defaultPortItems = [ + { group: 'left', args: { x: 0, y: portItemArgsY }, }, + { group: 'right', args: { x: nodeWidth, y: portItemArgsY }, }, ]; -/** - * Port position arguments - */ -export const portArgs = { x: nodeWidth, y: 42 } /** * Graph node library configuration @@ -679,125 +641,132 @@ export const portArgs = { x: nodeWidth, y: 42 } export const graphNodeLibrary: Record = { iteration: { width: nodeWidth, - height: 120, + height: 140, shape: 'iteration-node', ports: { - groups: defaultPortGroups, + groups: defaultAbsolutePortGroups, items: defaultPortItems, }, }, loop: { width: nodeWidth, - height: 120, + height: 140, shape: 'loop-node', ports: { - groups: defaultPortGroups, + groups: defaultAbsolutePortGroups, items: defaultPortItems, }, }, 'if-else': { width: nodeWidth, - height: 88, + height: conditionNodeHeight, shape: 'condition-node', ports: { groups: defaultAbsolutePortGroups, items: [ - { group: 'left' }, - ...(['IF', 'ELSE'].map((text, index) => ({ + defaultPortItems[0], + ...(['IF', 'ELSE'].map((_, index) => ({ group: 'right', id: `CASE${index}`, args: { ...portArgs, - y: 30 * index + 42, + y: portItemArgsY * index + conditionNodePortItemArgsY, }, - attrs: { text: { text: text, ...portTextAttrs } } }))), ], }, }, 'question-classifier': { width: nodeWidth, - height: 88, + height: conditionNodeHeight, shape: 'condition-node', ports: { groups: defaultAbsolutePortGroups, items: [ - { group: 'left' }, - ...(['分类1', '分类2'].map((text, index) => ({ + defaultPortItems[0], + ...(['分类1', '分类2'].map((_text, index) => ({ group: 'right', id: `CASE${index}`, args: { ...portArgs, - y: 30 * index + 42, + y: portItemArgsY * index + conditionNodePortItemArgsY, }, - attrs: { text: { text: text, ...portTextAttrs } } }))), ], }, }, start: { width: nodeWidth, - height: 64, + height: 76, shape: 'normal-node', ports: { - groups: {right: { position: 'right', markup: portMarkup, attrs: portAttrs }}, - items: [{ group: 'right' }], + groups: { right: defaultPortGroup}, + items: [defaultPortItems[1]], }, }, end: { width: nodeWidth, - height: 64, + height: 76, shape: 'normal-node', ports: { - groups: {left: { position: 'left', markup: portMarkup, attrs: portAttrs }}, - items: [{ group: 'left' }], + groups: { left: defaultPortGroup}, + items: [defaultPortItems[0]], }, }, 'cycle-start': { - width: 44, - height: 44, + width: 36, + height: 36, shape: 'cycle-start', ports: { - groups: {right: { position: 'right', markup: portMarkup, attrs: portAttrs }}, - items: [{ group: 'right' }], + groups: { right: defaultPortGroup }, + items: [{ group: 'right', args: { x: 36, y: 18 } }], }, }, 'add-node': { - width: 88, - height: 44, + width: 100, + height: 28, shape: 'add-node', ports: { - groups: {left: { position: 'left', markup: portMarkup, attrs: portAttrs }}, - items: [{ group: 'left' }], + groups: { left: defaultPortGroup }, + items: [{ group: 'left', args: { x: 0, y: 18 }}], }, }, default: { width: nodeWidth, - height: 64, + height: 76, shape: 'normal-node', ports: { - groups: defaultPortGroups, + groups: defaultAbsolutePortGroups, items: defaultPortItems, }, }, cycleStart: { - width: 44, - height: 44, + width: 36, + height: 36, shape: 'cycle-start', ports: { - groups: {right: { position: 'right', markup: portMarkup, attrs: portAttrs }}, - items: [{ group: 'right' }], + groups: { right: defaultPortGroup }, + items: [{ group: 'right', args: { x: 36, y: 18 }}], }, }, addStart: { - width: 88, - height: 44, + width: 100, + height: 28, shape: 'add-node', ports: { - groups: {left: { position: 'left', markup: portMarkup, attrs: portAttrs }}, - items: [{ group: 'left' }], + groups: { left: defaultPortGroup }, + items: [{ group: 'left', args: { x: 0, y: 14 } }], }, - } + }, + break: { + width: nodeWidth, + height: 76, + shape: 'normal-node', + ports: { + groups: { left: defaultPortGroup }, + items: [defaultPortItems[0]], + }, + }, } @@ -926,7 +895,7 @@ export const edgeAttrs = { attrs: { line: { stroke: edge_color, - strokeWidth: 1, + strokeWidth: edge_width, targetMarker: { name: 'block', width: 4, diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 2d8d1939..8aac9a8c 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-02-28 17:59:34 + * @Last Modified time: 2026-03-06 14:49:17 */ import { useRef, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; @@ -12,7 +12,7 @@ import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from ' import { register } from '@antv/x6-react-shape'; import type { PortMetadata } from '@antv/x6/lib/model/port'; -import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth, unknownNode } from '../constant'; +import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth, unknownNode, defaultPortItems, portItemArgsY, edge_width, conditionNodePortItemArgsY, conditionNodeItemHeight, conditionNodeHeight } from '../constant'; import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types'; import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application' @@ -130,8 +130,8 @@ export const useWorkflowGraph = ({ const { id, type, name, position, config = {} } = node let nodeLibraryConfig = [...nodeLibrary, { nodes: [unknownNode] }] .flatMap(category => category.nodes) - .find(n => n.type === type) - nodeLibraryConfig = JSON.parse(JSON.stringify({ config: {}, ...nodeLibraryConfig })) as NodeProperties + .find(n => n.type === type) as NodeProperties + nodeLibraryConfig = JSON.parse(JSON.stringify({ ...nodeLibraryConfig, config: nodeLibraryConfig.config || {} })) if (nodeLibraryConfig?.config) { Object.keys(nodeLibraryConfig.config).forEach(key => { @@ -201,11 +201,10 @@ export const useWorkflowGraph = ({ // Generate ports dynamically for if-else node based on cases if (type === 'if-else' && config.cases && Array.isArray(config.cases)) { const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE - const baseHeight = 88; - const newHeight = baseHeight + (totalPorts - 2) * 30; + const newHeight = conditionNodeHeight + (totalPorts - 2) * conditionNodeItemHeight; const portItems: PortMetadata[] = [ - { group: 'left' }, + defaultPortItems[0], ]; // Add IF/ELIF/ELSE ports for (let i = 0; i < totalPorts; i++) { @@ -214,9 +213,8 @@ export const useWorkflowGraph = ({ id: `CASE${i + 1}`, args: { x: nodeWidth, - y: 30 * i + 42, + y: portItemArgsY * i + conditionNodePortItemArgsY, }, - attrs: { text: { text: i === 0 ? 'IF' : i === totalPorts - 1 ? 'ELSE' : 'ELIF', ...portTextAttrs } } }); } @@ -231,11 +229,10 @@ export const useWorkflowGraph = ({ // Generate ports dynamically for question-classifier node based on categories if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) { const categoryCount = config.categories.length; - const baseHeight = 88; - const newHeight = baseHeight + (categoryCount - 2) * 30; + const newHeight = conditionNodeHeight + (categoryCount - 2) * conditionNodeItemHeight; const portItems: PortMetadata[] = [ - { group: 'left' } + defaultPortItems[0] ]; // Add category ports @@ -245,9 +242,8 @@ export const useWorkflowGraph = ({ id: `CASE${index + 1}`, args: { x: nodeWidth, - y: 30 * index + 42, + y: portItemArgsY * index + conditionNodePortItemArgsY, }, - attrs: { text: { text: `分类${index + 1}`, ...portTextAttrs }} }); }); @@ -260,16 +256,22 @@ export const useWorkflowGraph = ({ } // Check error_handle.method config for http-request node - if (type === 'http-request' && (config as any).error_handle?.method === 'branch') { + if (type === 'http-request' && (nodeConfig as any).error_handle?.method === 'branch') { nodeConfig.ports = { groups: { right: { position: 'right', markup: portMarkup, attrs: portAttrs }, left: { position: 'left', markup: portMarkup, attrs: portAttrs }, }, items: [ - { group: 'left' }, - { group: 'right', id: 'right' }, - { group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}} + defaultPortItems[0], + { ...defaultPortItems[1], id: 'right' }, + { + ...defaultPortItems[1], + args: { + x: nodeWidth, + y: portItemArgsY + portItemArgsY, + }, + id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}} ] }; } @@ -326,6 +328,14 @@ export const useWorkflowGraph = ({ console.log('newWidth', newHeight, newWidth) parentNode.prop('size', { width: newWidth, height: newHeight }) + + // Update x position of right group ports + const ports = (parentNode as Node).getPorts() + ports.forEach(port => { + if (port.group === 'right' && port.args) { + (parentNode as Node).portProp(port.id!, 'args/x', newWidth) + } + }) } } }) @@ -482,6 +492,7 @@ export const useWorkflowGraph = ({ isSelected: true, }); setSelectedNode(node); + clearEdgeSelect() }; /** * Handle edge click event @@ -514,7 +525,7 @@ export const useWorkflowGraph = ({ const clearEdgeSelect = () => { graphRef.current?.getEdges().forEach(e => { e.setAttrByPath('line/stroke', edge_color); - e.setAttrByPath('line/strokeWidth', 1); + e.setAttrByPath('line/strokeWidth', edge_width); }); }; /** @@ -524,6 +535,7 @@ export const useWorkflowGraph = ({ clearNodeSelect(); clearEdgeSelect(); graphRef.current?.cleanSelection(); + setSelectedNode(null); }; /** * Handle canvas scale/zoom event @@ -675,6 +687,28 @@ export const useWorkflowGraph = ({ } return false; }; + const nodePortClickEvent = ({ e, node, port }: { e: MouseEvent, node: Node, port: string }) => { + e.stopPropagation(); + const portElement = e.target as HTMLElement; + const rect = portElement.getBoundingClientRect(); + + // Create temporary popover trigger element + const tempDiv = document.createElement('div'); + tempDiv.style.position = 'fixed'; + tempDiv.style.left = rect.left + 'px'; + tempDiv.style.top = rect.top + 'px'; + tempDiv.style.width = '1px'; + tempDiv.style.height = '1px'; + tempDiv.style.zIndex = '9999'; + document.body.appendChild(tempDiv); + + // Trigger custom event to show node selection popover + const customEvent = new CustomEvent('port:click', { + detail: { node, port, element: tempDiv, rect } + }); + window.dispatchEvent(customEvent); + clearNodeSelect(); + } /** * Handle window resize event @@ -808,7 +842,7 @@ export const useWorkflowGraph = ({ graphRef.current.on('edge:mouseleave', ({ edge }: { edge: Edge }) => { if (edge.getAttrByPath('line/stroke') !== edge_selected_color) { edge.setAttrByPath('line/stroke', edge_color); - edge.setAttrByPath('line/strokeWidth', 1); + edge.setAttrByPath('line/strokeWidth', edge_width); } }); // Listen to node selection event @@ -816,33 +850,14 @@ export const useWorkflowGraph = ({ // Listen to edge selection event graphRef.current.on('edge:click', edgeClick); // Listen to port click event - graphRef.current.on('node:port:click', ({ e, node, port }: { e: MouseEvent, node: Node, port: string }) => { - e.stopPropagation(); - const portElement = e.target as HTMLElement; - const rect = portElement.getBoundingClientRect(); - - // Create temporary popover trigger element - const tempDiv = document.createElement('div'); - tempDiv.style.position = 'fixed'; - tempDiv.style.left = rect.left + 'px'; - tempDiv.style.top = rect.top + 'px'; - tempDiv.style.width = '1px'; - tempDiv.style.height = '1px'; - tempDiv.style.zIndex = '9999'; - document.body.appendChild(tempDiv); - - // Trigger custom event to show node selection popover - const customEvent = new CustomEvent('port:click', { - detail: { node, port, element: tempDiv, rect } - }); - window.dispatchEvent(customEvent); - }); + graphRef.current.on('node:port:click', nodePortClickEvent); // Listen to canvas click event, cancel selection graphRef.current.on('blank:click', blankClick); // Listen to zoom event graphRef.current.on('scale', scaleEvent); // Listen to node move event graphRef.current.on('node:moved', nodeMoved); + graphRef.current.on('node:removed', blankClick) // Listen to copy keyboard event graphRef.current.bindKey(['ctrl+c', 'cmd+c'], copyEvent); // Listen to paste keyboard event @@ -889,7 +904,7 @@ export const useWorkflowGraph = ({ name: t(`workflow.${dragData.type}`), ...nodeLibraryConfig }; - + if (dragData.type === 'loop' || dragData.type === 'iteration') { graphRef.current.addNode({ ...graphNodeLibrary[dragData.type], diff --git a/web/src/views/Workflow/index.tsx b/web/src/views/Workflow/index.tsx index 31e3d4df..3b7f16f4 100644 --- a/web/src/views/Workflow/index.tsx +++ b/web/src/views/Workflow/index.tsx @@ -9,7 +9,6 @@ import { useWorkflowGraph } from './hooks/useWorkflowGraph'; import type { WorkflowRef } from '@/views/ApplicationConfig/types' import Chat from './components/Chat/Chat'; import type { ChatRef, AddChatVariableRef } from './types' -import arrowIcon from '@/assets/images/workflow/arrow.png' import AddChatVariable from './components/AddChatVariable'; const Workflow = forwardRef((_props, ref) => { @@ -23,14 +22,9 @@ const Workflow = forwardRef((_props, ref) => { config, graphRef, selectedNode, - setSelectedNode, zoomLevel, - canUndo, - canRedo, isHandMode, setIsHandMode, - onUndo, - onRedo, onDrop, blankClick, deleteEvent, @@ -64,22 +58,11 @@ const Workflow = forwardRef((_props, ref) => { return (
{/* 左侧节点面板 */} - {!collapsed && } - + {/* 右侧画布区域 */}
@@ -91,25 +74,22 @@ const Workflow = forwardRef((_props, ref) => { isHandMode={isHandMode} setIsHandMode={setIsHandMode} zoomLevel={zoomLevel} - canUndo={canUndo} - canRedo={canRedo} - onUndo={onUndo} - onRedo={onRedo} />
{/* 右侧属性面板 */} - + {selectedNode && + + }