Merge #83 into develop_web from feature/20251219_zy
feat(web): add parameter-extractor、if-else、var-aggregator Node * feature/20251219_zy: (4 commits) doc: update version doc: update zh.ts fix(web): fix neo4j user memory refresh feat(web): add parameter-extractor、if-else、var-aggregator Node Signed-off-by: zhaoying <zhaoying@redbearai.com> Merged-by: zhaoying <zhaoying@redbearai.com> CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/83
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "memory-bear-font-end",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1552,17 +1552,17 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
answer: 'Answer',
|
||||
aiAndCognitiveProcessing: 'AI & Cognitive Processing',
|
||||
llm: 'Large Language Model (LLM)',
|
||||
model_selection: 'Model Selection',
|
||||
model_voting: 'Model Voting',
|
||||
rag: 'Knowledge Retrieval (RAG)',
|
||||
classification: 'Smart Classification',
|
||||
model_selection: 'Multi-Model Selection',
|
||||
model_voting: 'Multi-Model Voting',
|
||||
'knowledge-retrieval': 'Knowledge Retrieval (RAG)',
|
||||
classification: 'Intelligent Classification',
|
||||
'parameter-extractor': 'Parameter Extraction',
|
||||
flowControl: 'Flow Control',
|
||||
condition: 'Conditional Branch',
|
||||
'if-else': 'Conditional Branch',
|
||||
iteration: 'Iteration',
|
||||
loop: 'Loop',
|
||||
parallel: 'Parallel Execution',
|
||||
aggregator: 'Aggregator',
|
||||
'var-aggregator': 'Variable Aggregator',
|
||||
externalInteraction: 'External Interaction',
|
||||
http_request: 'HTTP Request',
|
||||
tools: 'Tools',
|
||||
@@ -1586,7 +1586,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
|
||||
clickToConfigure: 'Click to configure node parameters',
|
||||
nodeProperties: 'Node Properties',
|
||||
empty: "Emmm... The box is empty, there's nothing here~",
|
||||
empty: "Emmm... The box is empty, nothing here~",
|
||||
nodeName: 'Node Name',
|
||||
|
||||
|
||||
@@ -1609,16 +1609,66 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
editVariable: 'Edit Variable',
|
||||
variableType: 'Variable Type',
|
||||
variableName: 'Variable Name',
|
||||
invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores',
|
||||
description: 'Display Name',
|
||||
default: 'Default Value',
|
||||
required: 'Required',
|
||||
max_length: 'Max Length',
|
||||
defaultChecked: 'Checked',
|
||||
notDefaultChecked: 'Not Checked',
|
||||
notDefaultChecked: 'Unchecked',
|
||||
options: 'Options',
|
||||
},
|
||||
end: {
|
||||
output: 'Reply'
|
||||
},
|
||||
'knowledge-retrieval': {
|
||||
query: 'Query Variable',
|
||||
knowledge_retrieval: 'Knowledge Base',
|
||||
recallConfig: 'Recall Test',
|
||||
},
|
||||
'parameter-extractor': {
|
||||
model_id: 'Model',
|
||||
text: 'Input Variable',
|
||||
params: 'Extract Parameters',
|
||||
prompt: 'Instruction',
|
||||
|
||||
addParam: 'Add Extract Parameter',
|
||||
editParam: 'Edit Extract Parameter',
|
||||
|
||||
name: 'Name',
|
||||
invalidParamName: 'Parameter name must start with a letter and contain only letters, numbers, and underscores',
|
||||
type: 'Type',
|
||||
desc: 'Description',
|
||||
required: 'Required',
|
||||
|
||||
'string': 'String',
|
||||
'number': 'Number',
|
||||
'boolean': 'Boolean',
|
||||
'array[string]': 'Array[String]',
|
||||
'array[number]': 'Array[Number]',
|
||||
'array[boolean]': 'Array[Boolean]',
|
||||
'array[object]': 'Array[Object]',
|
||||
},
|
||||
'var-aggregator': {
|
||||
group: 'Aggregation Group',
|
||||
invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores',
|
||||
addGroup: 'Add Group',
|
||||
variable: 'Variable Assignment'
|
||||
},
|
||||
'if-else': {
|
||||
"empty": 'Is Empty',
|
||||
"not_empty": 'Is Not Empty',
|
||||
"contains": 'Contains',
|
||||
"not_contains": 'Does Not Contain',
|
||||
"startwith": 'Starts With',
|
||||
"endwith": 'Ends With',
|
||||
"eq": '==',
|
||||
"ne": '!=',
|
||||
"lt": '<',
|
||||
"le": '<=',
|
||||
"gt": '>',
|
||||
"ge": '>=',
|
||||
else_desc: 'Used to define the logic that should be executed when the if condition is not met.'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1627,7 +1677,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
save: 'Save',
|
||||
export: 'Export',
|
||||
variableConfig: 'Variable Configuration',
|
||||
variableRequired: 'required',
|
||||
variableRequired: 'Required',
|
||||
addMessage: 'Add Message',
|
||||
answerDesc: 'Reply'
|
||||
},
|
||||
|
||||
@@ -1188,7 +1188,9 @@ export const zh = {
|
||||
memoryContent: '记忆内容',
|
||||
created_at: '创建时间',
|
||||
updated_at: '最后更新时间',
|
||||
fullScreen: '全屏'
|
||||
fullScreen: '全屏',
|
||||
|
||||
memoryWindow: "{{name}}的记忆之窗"
|
||||
},
|
||||
space: {
|
||||
createSpace: '创建空间',
|
||||
@@ -1778,7 +1780,8 @@ export const zh = {
|
||||
"lt": '<',
|
||||
"le": '<=',
|
||||
"gt": '>',
|
||||
"ge": '>='
|
||||
"ge": '>=',
|
||||
else_desc: '用于定义当 if 条件不满足时应执行的逻辑。'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const Neo4j: FC = () => {
|
||||
memoryInsightRef.current?.getData()
|
||||
}
|
||||
if (response.summary_success) {
|
||||
memoryInsightRef.current?.getData()
|
||||
aboutMeRef.current?.getData()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -36,7 +36,7 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
||||
if (edges.length === 0) {
|
||||
nodes.forEach((node, index) => {
|
||||
const nodeData = node.getData();
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'condition';
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
||||
const nodeHeight = isSpecialNode ? 220 : 50;
|
||||
const xPosition = 100;
|
||||
const yPosition = index * (nodeHeight + 100) + 100;
|
||||
@@ -89,7 +89,7 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
||||
if (!node) return;
|
||||
|
||||
const nodeData = node.getData();
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'condition';
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
||||
const nodeWidth = isSpecialNode ? 400 : 160;
|
||||
const gap = isSpecialNode ? 150 : 100;
|
||||
|
||||
@@ -107,7 +107,7 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
||||
if (!node) return parentY;
|
||||
|
||||
const nodeData = node.getData();
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'condition';
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
||||
const nodeHeight = isSpecialNode ? 220 : 50;
|
||||
const verticalGap = isSpecialNode ? 80 : 40;
|
||||
const spacing = baseNodeSpacing + nodeHeight + verticalGap;
|
||||
|
||||
@@ -20,7 +20,7 @@ interface LexicalEditorProps {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
suggestions: Suggestion[];
|
||||
options: Suggestion[];
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -35,7 +35,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder = "请输入内容...",
|
||||
value = "",
|
||||
onChange,
|
||||
suggestions,
|
||||
options,
|
||||
}) => {
|
||||
const [_count, setCount] = useState(0);
|
||||
const initialConfig = {
|
||||
@@ -91,9 +91,9 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<CommandPlugin />
|
||||
<AutocompletePlugin suggestions={suggestions} />
|
||||
<AutocompletePlugin options={options} />
|
||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||
<InitialValuePlugin value={value} suggestions={suggestions} />
|
||||
<InitialValuePlugin value={value} options={options} />
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface Suggestion {
|
||||
nodeData: NodeProperties
|
||||
}
|
||||
|
||||
const AutocompletePlugin: FC<{ suggestions: Suggestion[] }> = ({ suggestions }) => {
|
||||
const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@@ -73,8 +73,8 @@ const AutocompletePlugin: FC<{ suggestions: Suggestion[] }> = ({ suggestions })
|
||||
|
||||
if (!showSuggestions) return null;
|
||||
|
||||
// Group suggestions by node id
|
||||
const groupedSuggestions = suggestions.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||
// Group options by node id
|
||||
const groupedSuggestions = options.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||
const { nodeData } = suggestion
|
||||
const nodeId = nodeData.id as string;
|
||||
if (!groups[nodeId]) {
|
||||
@@ -84,6 +84,9 @@ const AutocompletePlugin: FC<{ suggestions: Suggestion[] }> = ({ suggestions })
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
if (Object.entries(groupedSuggestions).length === 0) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -7,10 +7,10 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
|
||||
|
||||
interface InitialValuePluginProps {
|
||||
value: string;
|
||||
suggestions?: Suggestion[];
|
||||
options?: Suggestion[];
|
||||
}
|
||||
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, suggestions = [] }) => {
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
@@ -29,7 +29,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, suggesti
|
||||
if (match) {
|
||||
const [_, nodeId, label] = match;
|
||||
|
||||
const suggestion = suggestions.find(s => {
|
||||
const suggestion = options.find(s => {
|
||||
if (nodeId === 'sys') {
|
||||
return s.nodeData.type === 'start' && s.label === `sys.${label}`
|
||||
}
|
||||
@@ -51,7 +51,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, suggesti
|
||||
|
||||
initializedRef.current = true;
|
||||
}
|
||||
}, [suggestions]);
|
||||
}, [options]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,153 +1,32 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx';
|
||||
import { Button } from 'antd'
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
|
||||
const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {};
|
||||
|
||||
const addPort = (e: React.MouseEvent) => {
|
||||
if (!node || !node.addPort) return;
|
||||
e.stopPropagation();
|
||||
|
||||
const currentPorts = node.getPorts();
|
||||
const totalPorts = currentPorts.length;
|
||||
|
||||
// 如果没有端口,添加第一个端口和ELSE端口
|
||||
if (totalPorts === 0) {
|
||||
// 添加第一个ELIF端口
|
||||
node.addPort({
|
||||
id: 'elif_1',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELIF 1',
|
||||
},
|
||||
},
|
||||
});
|
||||
// 添加ELSE端口
|
||||
node.addPort({
|
||||
id: 'else',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELSE',
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果只有一个端口,确保它是ELSE,然后在之前添加ELIF
|
||||
if (totalPorts === 1) {
|
||||
const existingPort = currentPorts[0];
|
||||
|
||||
// 如果现有端口不是ELSE,先移除它
|
||||
if (node.removePort && existingPort.id !== 'else') {
|
||||
node.removePort(existingPort.id as string);
|
||||
|
||||
// 添加ELIF端口
|
||||
node.addPort({
|
||||
id: 'elif_1',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELIF 1',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 添加或确保存在ELSE端口
|
||||
if (existingPort.id !== 'else') {
|
||||
node.addPort({
|
||||
id: 'else',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELSE',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取最后一个端口,确保它是ELSE
|
||||
let lastPort = currentPorts[totalPorts - 1];
|
||||
|
||||
// 如果最后一个端口不是ELSE,先移除它
|
||||
if (node.removePort && lastPort.id !== 'else') {
|
||||
node.removePort(lastPort.id as string);
|
||||
|
||||
// 添加ELSE端口作为最后一个
|
||||
node.addPort({
|
||||
id: 'else',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELSE',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 更新currentPorts和totalPorts
|
||||
const updatedPorts = node.getPorts();
|
||||
const updatedTotal = updatedPorts.length;
|
||||
lastPort = updatedPorts[updatedTotal - 1];
|
||||
}
|
||||
|
||||
// 计算新的ELIF端口数量(最后一个是ELSE,不算在内)
|
||||
const elifCount = totalPorts - 1;
|
||||
const newElifCount = elifCount + 1;
|
||||
|
||||
// 如果有removePort方法,先移除最后一个端口(ELSE),添加新的ELIF端口,再添加回ELSE端口
|
||||
if (node.removePort) {
|
||||
// 移除最后一个端口(ELSE)
|
||||
node.removePort(lastPort.id as string);
|
||||
|
||||
// 添加新的ELIF端口在倒数第二个位置
|
||||
node.addPort({
|
||||
id: `elif_${newElifCount}`,
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: `ELIF ${newElifCount}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 添加回ELSE端口
|
||||
node.addPort({
|
||||
id: 'else',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELSE',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// const removeElif = (e: React.MouseEvent) => {
|
||||
// e.stopPropagation();
|
||||
// };
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={clsx(`rb:border rb:rounded-[12px] rb:relative rb:min-w-[200px] rb:min-h-[120px] rb:p-2`, {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-orange-50 rb:text-gray-700': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-[#FFFFFF] rb:text-[#374151]': !data.isSelected
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-60 rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
|
||||
<Button onClick={addPort}>+ 添加 ELIF</Button>
|
||||
{/* 标题区域 */}
|
||||
<div className="rb:absolute rb:-top-3 rb:left-2 rb:bg-blue-500 rb:rounded-2xl rb:px-3 rb:py-1 rb:flex rb:items-center rb:gap-1.5 rb:text-white rb:text-xs rb:font-bold rb:z-10">
|
||||
<div className="rb:w-4 rb:h-4 rb:bg-white rb:rounded rb:flex rb:items-center rb:justify-center rb:text-blue-500 rb:text-[10px]">
|
||||
🔀
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
||||
<img src={data.icon} className="rb:w-5 rb:h-5" />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
</div>
|
||||
条件分支
|
||||
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
node.remove()
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-7">{t('workflow.clickToConfigure')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
309
web/src/views/Workflow/components/Properties/CaseList/index.tsx
Normal file
309
web/src/views/Workflow/components/Properties/CaseList/index.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Row, Col, Divider } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
|
||||
interface CaseListProps {
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>;
|
||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>) => void;
|
||||
options: Suggestion[];
|
||||
name: string;
|
||||
selectedNode?: any;
|
||||
graphRef?: any;
|
||||
}
|
||||
const operatorList = [
|
||||
"empty",
|
||||
"not_empty",
|
||||
"contains",
|
||||
"not_contains",
|
||||
"startwith",
|
||||
"endwith",
|
||||
"eq",
|
||||
"ne",
|
||||
"lt",
|
||||
"le",
|
||||
"gt",
|
||||
"ge"
|
||||
]
|
||||
|
||||
const CaseList: FC<CaseListProps> = ({
|
||||
value = [],
|
||||
options,
|
||||
name,
|
||||
onChange,
|
||||
selectedNode,
|
||||
graphRef
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
|
||||
if (!selectedNode || !graphRef?.current) return;
|
||||
|
||||
// 保存现有连线信息(包括左侧端口连线)
|
||||
const existingEdges = graphRef.current.getEdges().filter((edge: any) =>
|
||||
edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id
|
||||
);
|
||||
const edgeConnections = existingEdges.map((edge: any) => ({
|
||||
edge,
|
||||
sourcePortId: edge.getSourcePortId(),
|
||||
targetCellId: edge.getTargetCellId(),
|
||||
targetPortId: edge.getTargetPortId(),
|
||||
sourceCellId: edge.getSourceCellId(),
|
||||
isIncoming: edge.getTargetCellId() === selectedNode.id
|
||||
}));
|
||||
|
||||
// 移除所有现有的右侧端口
|
||||
const existingPorts = selectedNode.getPorts();
|
||||
existingPorts.forEach((port: any) => {
|
||||
if (port.group === 'right') {
|
||||
selectedNode.removePort(port.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 计算新的节点高度:基础高度88px + 每个额外port增加30px
|
||||
const baseHeight = 88;
|
||||
const totalPorts = caseCount + 1; // IF/ELIF + ELSE
|
||||
const newHeight = baseHeight + (totalPorts - 2) * 30;
|
||||
|
||||
selectedNode.prop('size', { width: 240, height: newHeight })
|
||||
|
||||
// 添加 IF 端口
|
||||
selectedNode.addPort({
|
||||
id: 'CASE1',
|
||||
group: 'right',
|
||||
args: { dy: 24 },
|
||||
attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }}
|
||||
});
|
||||
|
||||
// 添加 ELIF 端口
|
||||
for (let i = 1; i < caseCount; i++) {
|
||||
selectedNode.addPort({
|
||||
id: `CASE${i + 1}`,
|
||||
group: 'right',
|
||||
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加 ELSE 端口
|
||||
selectedNode.addPort({
|
||||
id: `CASE${caseCount + 1}`,
|
||||
group: 'right',
|
||||
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }}
|
||||
});
|
||||
|
||||
// 恢复仍然存在的端口连线
|
||||
setTimeout(() => {
|
||||
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
|
||||
// 如果是进入连线(左侧端口),直接恢复
|
||||
if (isIncoming) {
|
||||
const sourceCell = graphRef.current?.getCellById(sourceCellId);
|
||||
if (sourceCell) {
|
||||
graphRef.current?.addEdge({
|
||||
source: { cell: sourceCellId, port: sourcePortId },
|
||||
target: { cell: selectedNode.id, port: targetPortId },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#155EEF',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
graphRef.current?.removeCell(edge);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理右侧端口连线
|
||||
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
|
||||
|
||||
// 如果是被删除的端口,不重新创建连线
|
||||
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
|
||||
graphRef.current?.removeCell(edge);
|
||||
return;
|
||||
}
|
||||
|
||||
let newPortId = sourcePortId;
|
||||
|
||||
// 如果是原来的ELSE端口,重新映射到新的ELSE端口
|
||||
const maxOriginalCaseNumber = Math.max(...edgeConnections
|
||||
.filter(({ isIncoming }: any) => !isIncoming)
|
||||
.map(({ sourcePortId }: any) => {
|
||||
const match = sourcePortId.match(/CASE(\d+)/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
}));
|
||||
|
||||
if (originalCaseNumber === maxOriginalCaseNumber) {
|
||||
newPortId = `CASE${caseCount + 1}`; // 新的ELSE端口
|
||||
} else if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) {
|
||||
// 如果是被删除端口之后的端口,编号向前移动
|
||||
newPortId = `CASE${originalCaseNumber - 1}`;
|
||||
}
|
||||
|
||||
const newPorts = selectedNode.getPorts();
|
||||
const matchingPort = newPorts.find((port: any) => port.id === newPortId);
|
||||
|
||||
if (matchingPort) {
|
||||
const targetCell = graphRef.current?.getCellById(targetCellId);
|
||||
if (targetCell) {
|
||||
graphRef.current?.addEdge({
|
||||
source: { cell: selectedNode.id, port: newPortId },
|
||||
target: { cell: targetCellId, port: targetPortId },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#155EEF',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
graphRef.current?.removeCell(edge);
|
||||
});
|
||||
}, 50);
|
||||
};
|
||||
const handleChangeLogicalOperator = (index: number) => {
|
||||
const newValue = [...value]
|
||||
newValue[index] = {
|
||||
...newValue[index],
|
||||
logical_operator: newValue[index].logical_operator === 'and' ? 'or' : 'and'
|
||||
}
|
||||
onChange && onChange(newValue)
|
||||
}
|
||||
|
||||
const handleAddCase = (addCaseFunc: Function) => {
|
||||
addCaseFunc({ logical_operator: 'and', expressions: [] });
|
||||
setTimeout(() => {
|
||||
updateNodePorts((value?.length || 0) + 1);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleRemoveCase = (removeCaseFunc: Function, fieldName: number, caseIndex: number) => {
|
||||
removeCaseFunc(fieldName);
|
||||
setTimeout(() => {
|
||||
updateNodePorts((value?.length || 1) - 1, caseIndex);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.List name={name}>
|
||||
{(caseFields, { add: addCase, remove: removeCase }) => (
|
||||
<>
|
||||
{caseFields.map((caseField, caseIndex) => (
|
||||
<div key={caseField.key}>
|
||||
<Form.List name={[caseField.name, 'expressions']}>
|
||||
{(conditionFields, { add: addCondition, remove: removeCondition }) => {
|
||||
return (
|
||||
<div className={clsx("rb:relative rb:mb-4 rb:border rb:border-gray-200 rb:rounded rb:p-3 rb:pl-5")}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:font-medium">
|
||||
{caseIndex === 0 ? 'IF' : 'ELIF'}<br/>
|
||||
{caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]">{`CASE ${caseIndex + 1}`}</span>}
|
||||
</span>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => addCondition()}
|
||||
size="small"
|
||||
>
|
||||
+ 添加条件
|
||||
</Button>
|
||||
{caseFields.length > 1 && <DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
|
||||
/>}
|
||||
</Space>
|
||||
</div>
|
||||
{conditionFields?.length > 1 && <>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-15 rb:bottom-6 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={() => handleChangeLogicalOperator(caseIndex)}>{value?.[caseIndex].logical_operator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{conditionFields.map((conditionField, conditionIndex) => (
|
||||
<div key={conditionField.key} className={clsx({
|
||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||
})}>
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row gutter={12} className="rb:mb-1">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
placeholder="包含"
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form.List>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
onClick={() => handleAddCase(addCase)}
|
||||
>
|
||||
+ ELIF
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Divider />
|
||||
<div className="rb:font-medium">ELSE</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] ">{t('workflow.config.if-else.else_desc')}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CaseList
|
||||
@@ -0,0 +1,101 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Button, Row, Col } from 'antd'
|
||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
interface GroupVariableListProps {
|
||||
value?: Array<{ key: string; value: string[]; }>;
|
||||
name: string;
|
||||
options: Suggestion[];
|
||||
isCanAdd: boolean
|
||||
}
|
||||
|
||||
const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
name,
|
||||
options = [],
|
||||
isCanAdd = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isCanAdd) {
|
||||
return (
|
||||
<div className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name={[name,0, 'key']}
|
||||
noStyle
|
||||
>
|
||||
{t('workflow.config.var-aggregator.variable')}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name={[name, 0, 'value']}
|
||||
noStyle
|
||||
rules={[{ required: true, message: 'Missing last name' }]}
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
mode="multiple"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
return (
|
||||
<div key={key} className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={isCanAdd ? [name, 'key'] : undefined}
|
||||
rules={[
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('workflow.config.var-aggregator.invalidVariableName') },
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
{isCanAdd ? <Input placeholder={t('common.pleaseEnter')} /> : t('workflow.config.var-aggregator.variable')}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{isCanAdd && <Col span={12} className="rb:flex! rb:items-center rb:justify-end">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>}
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
rules={[{ required: true, message: 'Missing last name' }]}
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
mode="multiple"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{isCanAdd && <Form.Item noStyle>
|
||||
<Button type="dashed" onClick={() => add({ key: `Group${fields.length + 1}` })} block>
|
||||
+ {t('workflow.config.var-aggregator.addGroup')}
|
||||
</Button>
|
||||
</Form.Item>}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupVariableList
|
||||
@@ -1,20 +1,19 @@
|
||||
import { type FC, useMemo } from 'react';
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import Editor from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface TextareaProps {
|
||||
options: Suggestion[];
|
||||
title?: string
|
||||
isArray?: boolean;
|
||||
parentName?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value?: string) => void;
|
||||
selectedNode?: Node | null;
|
||||
graphRef?: React.MutableRefObject<Graph | undefined>;
|
||||
}
|
||||
const roleOptions = [
|
||||
// { label: 'SYSTEM', value: 'SYSTEM' },
|
||||
@@ -22,92 +21,15 @@ const roleOptions = [
|
||||
{ label: 'ASSISTANT', value: 'ASSISTANT' },
|
||||
]
|
||||
const MessageEditor: FC<TextareaProps> = ({
|
||||
title,
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
selectedNode,
|
||||
graphRef,
|
||||
options,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
const values = form.getFieldsValue()
|
||||
|
||||
const suggestions = useMemo(() => {
|
||||
if (!selectedNode || !graphRef?.current) return [];
|
||||
|
||||
const suggestions: Suggestion[] = [];
|
||||
const graph = graphRef.current;
|
||||
const edges = graph.getEdges();
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find all connected previous nodes (recursive)
|
||||
const getAllPreviousNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
||||
if (visited.has(nodeId)) return [];
|
||||
visited.add(nodeId);
|
||||
|
||||
const directPrevious = edges
|
||||
.filter(edge => edge.getTargetCellId() === nodeId)
|
||||
.map(edge => edge.getSourceCellId());
|
||||
|
||||
const allPrevious = [...directPrevious];
|
||||
directPrevious.forEach(prevNodeId => {
|
||||
allPrevious.push(...getAllPreviousNodes(prevNodeId, visited));
|
||||
});
|
||||
|
||||
return allPrevious;
|
||||
};
|
||||
|
||||
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
||||
console.log('allPreviousNodeIds', allPreviousNodeIds)
|
||||
|
||||
allPreviousNodeIds.forEach(nodeId => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const nodeData = node.getData();
|
||||
|
||||
switch(nodeData.type) {
|
||||
case 'start':
|
||||
const list = [
|
||||
...(nodeData.config?.variables?.defaultValue ?? []),
|
||||
...(nodeData.config?.variables?.value ?? [])
|
||||
]
|
||||
list.forEach((variable: any) => {
|
||||
suggestions.push({
|
||||
key: `${nodeId}_${variable.name}`,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `${nodeId}.${variable.name}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
});
|
||||
nodeData.config?.variables?.sys.forEach((variable: any) => {
|
||||
suggestions.push({
|
||||
key: `${nodeId}_${variable.name}`,
|
||||
label: `sys.${variable.name}`,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `sys.${variable.name}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
});
|
||||
break
|
||||
case 'llm':
|
||||
suggestions.push({
|
||||
key: `${nodeId}_output`,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
value: `${nodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
break
|
||||
}
|
||||
});
|
||||
|
||||
return suggestions;
|
||||
}, [selectedNode, graphRef]);
|
||||
|
||||
const handleAdd = (add: FormListOperation['add']) => {
|
||||
const list = values[parentName];
|
||||
@@ -158,7 +80,7 @@ const MessageEditor: FC<TextareaProps> = ({
|
||||
name={[name, 'content']}
|
||||
noStyle
|
||||
>
|
||||
<Editor placeholder={placeholder} suggestions={suggestions} />
|
||||
<Editor placeholder={placeholder} options={options} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
)
|
||||
@@ -175,14 +97,14 @@ const MessageEditor: FC<TextareaProps> = ({
|
||||
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
{t('workflow.answerDesc')}
|
||||
{title ?? t('workflow.answerDesc')}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
name={parentName}
|
||||
noStyle
|
||||
>
|
||||
<Editor placeholder={placeholder} suggestions={suggestions} />
|
||||
<Editor placeholder={placeholder} options={options} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, Checkbox } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ParamItem, ParamEditModalRef } from './types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ParamEditModalProps {
|
||||
refresh: (values: ParamItem, editIndex?: number) => void;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'array[string]',
|
||||
'array[number]',
|
||||
'array[boolean]',
|
||||
'array[object]'
|
||||
]
|
||||
|
||||
const ParamEditModal = forwardRef<ParamEditModalRef, ParamEditModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ParamItem>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setEditIndex(undefined)
|
||||
};
|
||||
|
||||
const handleOpen = (variable?: ParamItem, index?: number) => {
|
||||
setVisible(true);
|
||||
if (variable) {
|
||||
form.setFieldsValue(variable)
|
||||
setEditIndex(index)
|
||||
} else {
|
||||
form.resetFields();
|
||||
setEditIndex(undefined)
|
||||
}
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields().then((values) => {
|
||||
refresh({ ...values }, editIndex)
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editIndex !== undefined ? t('workflow.config.parameter-extractor.editParam') : t('workflow.config.parameter-extractor.addParam')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('workflow.config.parameter-extractor.name')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('workflow.config.parameter-extractor.invalidParamName') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="type"
|
||||
label={t('workflow.config.parameter-extractor.type')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="desc"
|
||||
label={t('workflow.config.parameter-extractor.desc')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="required"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>{t('workflow.config.parameter-extractor.required')}</Checkbox>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ParamEditModal;
|
||||
@@ -0,0 +1,94 @@
|
||||
import { type FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Space, List } from 'antd'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
import type { ParamItem, ParamEditModalRef } from './types'
|
||||
import ParamEditModal from './ParamEditModal'
|
||||
interface ParamsListProps {
|
||||
label: string;
|
||||
value?: ParamItem[];
|
||||
onChange?: (value: ParamItem[]) => void
|
||||
}
|
||||
|
||||
const ParamsList: FC<ParamsListProps> = ({
|
||||
label,
|
||||
value = [],
|
||||
onChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const paramEditModalRef = useRef<ParamEditModalRef>(null)
|
||||
|
||||
const handleAdd = () => {
|
||||
paramEditModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEdit = (index: number) => {
|
||||
paramEditModalRef.current?.handleOpen(value[index], index)
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
const list = [...value]
|
||||
list.splice(index, 1)
|
||||
onChange && onChange(list)
|
||||
}
|
||||
const handleSave = (vo: ParamItem, index?: number) => {
|
||||
if (index !== undefined) {
|
||||
const list = [...value]
|
||||
list[index] = vo
|
||||
onChange && onChange(list)
|
||||
} else {
|
||||
onChange && onChange([...value, vo])
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div>{label}</div>
|
||||
|
||||
<Space>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAdd}>+</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{value?.length === 0
|
||||
? <Empty size={88} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={value}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div key={index} className="rb:group rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:leading-4">
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)})</span>
|
||||
</div>
|
||||
<span className="rb:block rb:group-hover:hidden rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
|
||||
</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{item.desc}</div>
|
||||
<Space size={12} className="rb:hidden! rb:group-hover:flex! rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
></div>
|
||||
<div
|
||||
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)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
<ParamEditModal
|
||||
ref={paramEditModalRef}
|
||||
refresh={handleSave}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParamsList
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface ParamItem {
|
||||
name: string;
|
||||
type: string;
|
||||
desc: string;
|
||||
required: boolean
|
||||
}
|
||||
|
||||
export interface ParamEditModalRef {
|
||||
handleOpen: (vo?: ParamItem, index?: number) => void;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx';
|
||||
import { Select, type SelectProps } from 'antd'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
type LabelRender = SelectProps['labelRender'];
|
||||
@@ -7,13 +8,17 @@ interface VariableSelectProps extends SelectProps {
|
||||
options: Suggestion[];
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
allowClear?: boolean;
|
||||
}
|
||||
|
||||
const VariableSelect: FC<VariableSelectProps> = ({
|
||||
placeholder,
|
||||
options,
|
||||
value,
|
||||
allowClear = true,
|
||||
onChange,
|
||||
size,
|
||||
...resetPorps
|
||||
}) => {
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
@@ -26,7 +31,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
if (filterOption) {
|
||||
return (
|
||||
<span
|
||||
className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:bg-white rb:leading-5.5! rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer"
|
||||
className={clsx("rb:w-full rb:wrap-break-word rb:line-clamp-1 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer", {
|
||||
'rb:leading-5.5!': size !== 'small',
|
||||
'rb:leading-4!': size === 'small'
|
||||
})}
|
||||
contentEditable={false}
|
||||
>
|
||||
<img
|
||||
@@ -59,6 +67,8 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...resetPorps}
|
||||
size={size}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
style={{ width: '100%' }}
|
||||
@@ -66,6 +76,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
labelRender={labelRender}
|
||||
onChange={handleChange}
|
||||
showSearch
|
||||
allowClear={allowClear}
|
||||
filterOption={(input, option) => {
|
||||
if (option?.options) {
|
||||
return option.label?.toLowerCase().includes(input.toLowerCase()) ||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC, useEffect, useState, useRef, useMemo } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import { Form, Input, Button, Select, InputNumber, Slider, Space, Divider, App } from 'antd'
|
||||
import { Form, Input, Button, Select, InputNumber, Slider, Space, Divider, App, Switch } from 'antd'
|
||||
|
||||
import type { NodeConfig, NodeProperties, StartVariableItem, VariableEditModalRef } from '../../types'
|
||||
import Empty from '@/components/Empty';
|
||||
@@ -12,6 +12,9 @@ import MessageEditor from './MessageEditor'
|
||||
import Knowledge from './Knowledge/Knowledge';
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from './VariableSelect';
|
||||
import ParamsList from './ParamsList';
|
||||
import GroupVariableList from './GroupVariableList'
|
||||
import CaseList from './CaseList'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -70,12 +73,24 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (values && selectedNode) {
|
||||
const { id, knowledge_retrieval, ...rest } = values
|
||||
const { knowledge_bases, ...restKnowledgeConfig } = knowledge_retrieval || {}
|
||||
const { id, knowledge_retrieval, group, group_names, ...rest } = values
|
||||
const { knowledge_bases = [], ...restKnowledgeConfig } = (knowledge_retrieval as any) || {}
|
||||
|
||||
let groupNames: Record<string, string[]> | string[] = {}
|
||||
|
||||
if (group && group_names?.length) {
|
||||
group_names.forEach(vo => {
|
||||
(groupNames as Record<string, string[]>)[vo.key] = vo.value
|
||||
})
|
||||
} else if (!group) {
|
||||
groupNames = group_names?.[0]?.value || []
|
||||
}
|
||||
let allRest = {
|
||||
...rest,
|
||||
...restKnowledgeConfig,
|
||||
knowledge_bases: knowledge_bases?.map(vo => ({
|
||||
}
|
||||
if (knowledge_bases?.length) {
|
||||
allRest.knowledge_bases = knowledge_bases?.map((vo: any) => ({
|
||||
id: vo.id,
|
||||
...vo.config
|
||||
}))
|
||||
@@ -134,7 +149,6 @@ const Properties: FC<PropertiesProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const variableList = useMemo(() => {
|
||||
if (!selectedNode || !graphRef?.current) return [];
|
||||
|
||||
@@ -142,6 +156,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
const graph = graphRef.current;
|
||||
const edges = graph.getEdges();
|
||||
const nodes = graph.getNodes();
|
||||
const addedKeys = new Set<string>();
|
||||
|
||||
// Find all connected previous nodes (recursive)
|
||||
const getAllPreviousNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
||||
@@ -175,35 +190,47 @@ const Properties: FC<PropertiesProps> = ({
|
||||
...(nodeData.config?.variables?.value ?? [])
|
||||
]
|
||||
list.forEach((variable: any) => {
|
||||
variableList.push({
|
||||
key: `${nodeId}_${variable.name}`,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `{{${nodeId}.${variable.name}}}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
const key = `${nodeId}_${variable.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `{{${nodeId}.${variable.name}}}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
});
|
||||
nodeData.config?.variables?.sys.forEach((variable: any) => {
|
||||
variableList.push({
|
||||
key: `${nodeId}_${variable.name}`,
|
||||
label: `sys.${variable.name}`,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `{{sys.${variable.name}}}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
const key = `${nodeId}_sys_${variable.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: `sys.${variable.name}`,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `sys.${variable.name}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
});
|
||||
break
|
||||
case 'llm':
|
||||
variableList.push({
|
||||
key: `${nodeId}_output`,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
value: `${nodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
const llmKey = `${nodeId}_output`;
|
||||
if (!addedKeys.has(llmKey)) {
|
||||
addedKeys.add(llmKey);
|
||||
variableList.push({
|
||||
key: llmKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
value: `${nodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
}
|
||||
});
|
||||
@@ -279,14 +306,14 @@ const Properties: FC<PropertiesProps> = ({
|
||||
if (selectedNode.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor selectedNode={selectedNode} graphRef={graphRef} />
|
||||
<MessageEditor options={variableList} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
if (selectedNode.data?.type === 'end' && key === 'output') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor isArray={false} parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||
<MessageEditor isArray={false} parentName={key} options={variableList} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
@@ -306,11 +333,61 @@ const Properties: FC<PropertiesProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (config.type === 'messageEditor') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor
|
||||
title={t(`workflow.config.${selectedNode.data.type}.${key}`)}
|
||||
isArray={!!config.isArray}
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
if (config.type === 'paramList') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<ParamsList
|
||||
label={t(`workflow.config.${selectedNode.data.type}.${key}`)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
)
|
||||
}
|
||||
if (config.type === 'groupVariableList') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<GroupVariableList
|
||||
name={key}
|
||||
options={variableList}
|
||||
isCanAdd={!!values?.group}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
)
|
||||
}
|
||||
if (config.type === 'caseList') {
|
||||
console.log('key', key)
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<CaseList
|
||||
name={key}
|
||||
options={variableList}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={key}
|
||||
label={t(`workflow.config.${selectedNode.data.type}.${key}`)}
|
||||
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
|
||||
>
|
||||
{config.type === 'input'
|
||||
? <Input placeholder={t('common.pleaseEnter')} />
|
||||
@@ -339,6 +416,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={variableList}
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch />
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
@@ -142,61 +142,102 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
},
|
||||
// { type: "classification", icon: classificationIcon },
|
||||
// { type: "parameter_extraction", icon: parameterExtractionIcon }
|
||||
]
|
||||
},
|
||||
/*
|
||||
{
|
||||
category: "cognitiveUpgrading",
|
||||
nodes: [
|
||||
{ type: "task_planning", icon: taskPlanningIcon },
|
||||
{ type: "reasoning_control", icon: reasoningControlIcon },
|
||||
{ type: "self_reflection", icon: selfReflectionIcon },
|
||||
{ type: "memory_enhancement", icon: memoryEnhancementIcon }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: "agentCollaborationNode",
|
||||
nodes: [
|
||||
{ type: "agent_scheduling", icon: agentSchedulingIcon },
|
||||
{ type: "agent_collaboration", icon: agentCollaborationIcon },
|
||||
{ type: "agent_arbitration", icon: agentArbitrationIcon }
|
||||
{ type: "parameter-extractor", icon: parameterExtractionIcon,
|
||||
config: {
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat' }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
text: {
|
||||
type: 'variableList',
|
||||
},
|
||||
params: {
|
||||
type: 'paramList',
|
||||
},
|
||||
prompt: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// category: "cognitiveUpgrading",
|
||||
// nodes: [
|
||||
// { type: "task_planning", icon: taskPlanningIcon },
|
||||
// { type: "reasoning_control", icon: reasoningControlIcon },
|
||||
// { type: "self_reflection", icon: selfReflectionIcon },
|
||||
// { type: "memory_enhancement", icon: memoryEnhancementIcon }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// category: "agentCollaborationNode",
|
||||
// nodes: [
|
||||
// { type: "agent_scheduling", icon: agentSchedulingIcon },
|
||||
// { type: "agent_collaboration", icon: agentCollaborationIcon },
|
||||
// { type: "agent_arbitration", icon: agentArbitrationIcon }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
category: "flowControl",
|
||||
nodes: [
|
||||
{ type: "condition", icon: conditionIcon },
|
||||
{ type: "iteration", icon: iterationIcon },
|
||||
{ type: "loop", icon: loopIcon },
|
||||
{ type: "parallel", icon: parallelIcon },
|
||||
{ type: "aggregator", icon: aggregatorIcon }
|
||||
{ type: "if-else", icon: conditionIcon,
|
||||
config: {
|
||||
cases: {
|
||||
type: 'caseList',
|
||||
defaultValue: [
|
||||
{
|
||||
logical_operator: 'and',
|
||||
expressions: []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// { type: "iteration", icon: iterationIcon },
|
||||
// { type: "loop", icon: loopIcon },
|
||||
// { type: "parallel", icon: parallelIcon },
|
||||
{ type: "var-aggregator", icon: aggregatorIcon,
|
||||
config: {
|
||||
group: {
|
||||
type: 'switch',
|
||||
defaultValue: false
|
||||
},
|
||||
group_names: {
|
||||
type: 'groupVariableList',
|
||||
defaultValue: [{ key: 'Group1', value: []}]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: "externalInteraction",
|
||||
nodes: [
|
||||
{ type: "http_request", icon: httpRequestIcon },
|
||||
{ type: "tools", icon: toolsIcon },
|
||||
{ type: "code_execution", icon: codeExecutionIcon },
|
||||
{ type: "template_rendering", icon: templateRenderingIcon }
|
||||
]
|
||||
},
|
||||
{
|
||||
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 }
|
||||
]
|
||||
},
|
||||
*/
|
||||
// {
|
||||
// category: "externalInteraction",
|
||||
// nodes: [
|
||||
// { type: "http_request", icon: httpRequestIcon },
|
||||
// { type: "tools", icon: toolsIcon },
|
||||
// { type: "code_execution", icon: codeExecutionIcon },
|
||||
// { type: "template_rendering", icon: templateRenderingIcon }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// 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 }
|
||||
// ]
|
||||
// },
|
||||
];
|
||||
|
||||
// 节点注册库
|
||||
@@ -221,8 +262,8 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
||||
},
|
||||
{
|
||||
shape: 'condition-node',
|
||||
width: 200,
|
||||
height: 100,
|
||||
width: 240,
|
||||
height: 88,
|
||||
component: ConditionNode,
|
||||
},
|
||||
{
|
||||
@@ -253,7 +294,7 @@ interface NodeConfig {
|
||||
|
||||
const portAttrs = {
|
||||
circle: {
|
||||
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF',
|
||||
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 }
|
||||
},
|
||||
}
|
||||
const defaultPortGroups = {
|
||||
@@ -287,16 +328,16 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
items: defaultPortItems,
|
||||
},
|
||||
},
|
||||
condition: {
|
||||
'if-else': {
|
||||
width: 240,
|
||||
height: 200,
|
||||
height: 88,
|
||||
shape: 'condition-node',
|
||||
ports: {
|
||||
groups: defaultPortGroups,
|
||||
items: [
|
||||
{ group: 'left' },
|
||||
{ group: 'right', id: 'if_1', attrs: {text: { text: 'IF' }} },
|
||||
{ group: 'right', id: 'else_2', attrs: {text: { text: 'ELSE' }} }
|
||||
{ group: 'right', id: 'CASE1', args: { dy: 24 }, attrs: { text: { text: 'IF', fontSize: 12, color: '#5B6167' }} },
|
||||
{ group: 'right', id: 'CASE2', attrs: { text: { text: 'ELSE', fontSize: 12, color: '#5B6167' }} }
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { register } from '@antv/x6-react-shape';
|
||||
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary } from '../constant';
|
||||
import type { WorkflowConfig, NodeProperties } from '../types';
|
||||
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
|
||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||
|
||||
export interface UseWorkflowGraphProps {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
@@ -35,7 +36,7 @@ export interface UseWorkflowGraphReturn {
|
||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||
}
|
||||
|
||||
const edge_color = '#155EEF';
|
||||
export const edge_color = '#155EEF';
|
||||
const edge_selected_color = '#4DA8FF'
|
||||
|
||||
export const useWorkflowGraph = ({
|
||||
@@ -88,7 +89,13 @@ export const useWorkflowGraph = ({
|
||||
nodeLibraryConfig.config[key].defaultValue = {
|
||||
...rest
|
||||
}
|
||||
console.log(type, config, nodeLibraryConfig)
|
||||
} else if (key === 'group_names' && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
const { group_names, group } = config
|
||||
nodeLibraryConfig.config[key].defaultValue = group
|
||||
? Object.entries(group_names as Record<string, any>).map(([key, value]) => ({ key, value }))
|
||||
: [{ key: 'Group1', value: group_names }]
|
||||
|
||||
console.log('group_names', nodeLibraryConfig.config)
|
||||
} else if (nodeLibraryConfig.config && nodeLibraryConfig.config[key] && config[key]) {
|
||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||
}
|
||||
@@ -102,13 +109,59 @@ export const useWorkflowGraph = ({
|
||||
data: { ...node, ...nodeLibraryConfig},
|
||||
...position,
|
||||
}
|
||||
|
||||
// 如果是if-else节点,根据cases动态生成端口
|
||||
if (type === 'if-else' && config.cases && Array.isArray(config.cases)) {
|
||||
const caseCount = config.cases.length;
|
||||
const totalPorts = caseCount + 1; // IF/ELIF + ELSE
|
||||
const baseHeight = 88;
|
||||
const newHeight = baseHeight + (totalPorts - 2) * 30;
|
||||
|
||||
const portAttrs = {
|
||||
circle: {
|
||||
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 }
|
||||
},
|
||||
};
|
||||
|
||||
const portItems: PortMetadata[] = [
|
||||
{ group: 'left' },
|
||||
{ group: 'right', id: 'CASE1', args: { dy: 24 }, attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }} }
|
||||
];
|
||||
|
||||
// 添加 ELIF 端口
|
||||
for (let i = 1; i < caseCount; i++) {
|
||||
portItems.push({
|
||||
group: 'right',
|
||||
id: `CASE${i + 1}`,
|
||||
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加 ELSE 端口
|
||||
portItems.push({
|
||||
group: 'right',
|
||||
id: `CASE${caseCount + 1}`,
|
||||
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }}
|
||||
});
|
||||
|
||||
nodeConfig.ports = {
|
||||
groups: {
|
||||
right: { position: 'right', attrs: portAttrs },
|
||||
left: { position: 'left', attrs: portAttrs },
|
||||
},
|
||||
items: portItems
|
||||
};
|
||||
|
||||
nodeConfig.height = newHeight;
|
||||
}
|
||||
|
||||
return nodeConfig
|
||||
})
|
||||
graphRef.current?.addNodes(nodeList)
|
||||
}
|
||||
if (edges.length) {
|
||||
const edgeList = edges.map(edge => {
|
||||
const { source, target } = edge
|
||||
const { source, target, label } = edge
|
||||
const sourceCell = graphRef.current?.getCellById(source)
|
||||
const targetCell = graphRef.current?.getCellById(target)
|
||||
|
||||
@@ -116,16 +169,22 @@ export const useWorkflowGraph = ({
|
||||
const sourcePorts = (sourceCell as Node).getPorts()
|
||||
const targetPorts = (targetCell as Node).getPorts()
|
||||
|
||||
let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
|
||||
|
||||
// 如果是if-else节点且有label,使用label作为源端口
|
||||
if (sourceCell.getData()?.type === 'if-else' && label) {
|
||||
sourcePort = label;
|
||||
}
|
||||
|
||||
const edgeConfig = {
|
||||
source: {
|
||||
cell: sourceCell.id,
|
||||
port: sourcePorts.find((port: any) => port.group === 'right')?.id || 'right'
|
||||
port: sourcePort
|
||||
},
|
||||
target: {
|
||||
cell: targetCell.id,
|
||||
port: targetPorts.find((port: any) => port.group === 'left')?.id || 'left'
|
||||
},
|
||||
// label,
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: edge_color,
|
||||
@@ -646,13 +705,13 @@ export const useWorkflowGraph = ({
|
||||
y: point.y - 100,
|
||||
data: { ...cleanNodeData, isGroup: true },
|
||||
});
|
||||
} else if (dragData.type === 'condition') {
|
||||
} else if (dragData.type === 'if-else') {
|
||||
// 创建条件节点
|
||||
graphRef.current.addNode({
|
||||
...graphNodeLibrary[dragData.type],
|
||||
x: point.x - 100,
|
||||
y: point.y - 60,
|
||||
data: { ...cleanNodeData, elifCount: 0 },
|
||||
data: { ...cleanNodeData },
|
||||
});
|
||||
} else {
|
||||
// 检查是否放置在群组内
|
||||
@@ -719,10 +778,21 @@ export const useWorkflowGraph = ({
|
||||
};
|
||||
}),
|
||||
edges: edges.map((edge: Edge) => {
|
||||
const sourceCell = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||
const sourcePortId = edge.getSourcePortId();
|
||||
|
||||
// 如果是if-else节点的右侧端口连线,添加label
|
||||
if (sourceCell?.getData()?.type === 'if-else' && sourcePortId?.startsWith('CASE')) {
|
||||
return {
|
||||
source: edge.getSourceCellId(),
|
||||
target: edge.getTargetCellId(),
|
||||
label: sourcePortId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
source: edge.getSourceCellId(),
|
||||
target: edge.getTargetCellId(),
|
||||
// label: edge.getAttrs()?.label?.text,
|
||||
};
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
import { Graph } from '@antv/x6';
|
||||
import type { KnowledgeConfig } from './components/Properties/Knowledge/types'
|
||||
export interface NodeConfig {
|
||||
type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList';
|
||||
type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList' | string;
|
||||
options?: { label: string; value: string }[];
|
||||
|
||||
max?: number;
|
||||
@@ -20,6 +21,10 @@ export interface NodeConfig {
|
||||
type: string;
|
||||
readonly: boolean;
|
||||
}>
|
||||
|
||||
knowledge_retrieval?: KnowledgeConfig;
|
||||
|
||||
group_names?: Array<{key: string, value: string[]}>
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user