Merge #107 into develop_web from feature/20251219_zy
feat(web): memory-read、memory-write、iteration、assigner、tool node * feature/20251219_zy: (4 commits) feat(web): add question classifier node feat(web): memory insight feat(web): add loop node; add chat variable; feat(web): memory-read、memory-write、iteration、assigner、tool 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/107
This commit is contained in:
@@ -27,5 +27,10 @@ export const execute = (data: ExecuteData) => {
|
|||||||
}
|
}
|
||||||
export const parseSchema = (data: Record<string, any>) => {
|
export const parseSchema = (data: Record<string, any>) => {
|
||||||
return request.post(`/tools/parse_schema`, data)
|
return request.post(`/tools/parse_schema`, data)
|
||||||
|
}
|
||||||
|
export const getToolDetail = (tool_id: string) => {
|
||||||
|
return request.get(`/tools/${tool_id}`)
|
||||||
|
}
|
||||||
|
export const getToolMethods = (tool_id: string) => {
|
||||||
|
return request.get(`/tools/${tool_id}/methods`)
|
||||||
}
|
}
|
||||||
BIN
web/src/assets/images/workflow/assigner.png
Normal file
BIN
web/src/assets/images/workflow/assigner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 588 B |
BIN
web/src/assets/images/workflow/break.png
Normal file
BIN
web/src/assets/images/workflow/break.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 866 B |
BIN
web/src/assets/images/workflow/question-classifier.png
Normal file
BIN
web/src/assets/images/workflow/question-classifier.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 853 B |
@@ -1117,7 +1117,11 @@ export const en = {
|
|||||||
memoryContent: 'Memory Content',
|
memoryContent: 'Memory Content',
|
||||||
created_at: 'Created At',
|
created_at: 'Created At',
|
||||||
|
|
||||||
memoryWindow: "{{name}}'s Window of Memory"
|
memoryWindow: "{{name}}'s Window of Memory",
|
||||||
|
memory_insight: 'Overall Overview',
|
||||||
|
key_findings: 'Key Findings',
|
||||||
|
behavior_pattern: 'Behavior Pattern',
|
||||||
|
growth_trajectory: 'Growth Trajectory',
|
||||||
},
|
},
|
||||||
space: {
|
space: {
|
||||||
createSpace: 'Create Space',
|
createSpace: 'Create Space',
|
||||||
@@ -1580,16 +1584,22 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
'parameter-extractor': 'Parameter Extraction',
|
'parameter-extractor': 'Parameter Extraction',
|
||||||
flowControl: 'Flow Control',
|
flowControl: 'Flow Control',
|
||||||
'if-else': 'Conditional Branch',
|
'if-else': 'Conditional Branch',
|
||||||
|
'question-classifier': 'Question Classifier',
|
||||||
iteration: 'Iteration',
|
iteration: 'Iteration',
|
||||||
loop: 'Loop',
|
loop: 'Loop',
|
||||||
|
'cycle-start': '',
|
||||||
|
break: 'Break Loop',
|
||||||
|
assigner: 'Variable Assignment',
|
||||||
parallel: 'Parallel Execution',
|
parallel: 'Parallel Execution',
|
||||||
'var-aggregator': 'Variable Aggregator',
|
'var-aggregator': 'Variable Aggregator',
|
||||||
externalInteraction: 'External Interaction',
|
externalInteraction: 'External Interaction',
|
||||||
"http-request": 'HTTP Request',
|
"http-request": 'HTTP Request',
|
||||||
tools: 'Tools',
|
tool: 'Tools',
|
||||||
code_execution: 'Code Execution',
|
code_execution: 'Code Execution',
|
||||||
"jinja-render": 'Template Rendering',
|
"jinja-render": 'Template Rendering',
|
||||||
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
||||||
|
'memory-read': 'Memory Retrieval',
|
||||||
|
'memory-write': 'Memory Storage',
|
||||||
task_planning: 'Task Planning',
|
task_planning: 'Task Planning',
|
||||||
reasoning_control: 'Reasoning Control',
|
reasoning_control: 'Reasoning Control',
|
||||||
self_reflection: 'Self Reflection',
|
self_reflection: 'Self Reflection',
|
||||||
@@ -1607,9 +1617,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
|
|
||||||
clickToConfigure: 'Click to configure node parameters',
|
clickToConfigure: 'Click to configure node parameters',
|
||||||
nodeProperties: 'Node Properties',
|
nodeProperties: 'Node Properties',
|
||||||
empty: "Emmm... The box is empty, nothing here~",
|
empty: "Emmm…The box is empty, there's nothing here~",
|
||||||
nodeName: 'Node Name',
|
nodeName: 'Node Name',
|
||||||
|
addvariable: 'Chat Variables',
|
||||||
|
addChatVariable: 'Add Chat Variable',
|
||||||
|
editChatVariable: 'Edit Chat Variable',
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
llm: {
|
llm: {
|
||||||
@@ -1631,7 +1643,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
editVariable: 'Edit Variable',
|
editVariable: 'Edit Variable',
|
||||||
variableType: 'Variable Type',
|
variableType: 'Variable Type',
|
||||||
variableName: 'Variable Name',
|
variableName: 'Variable Name',
|
||||||
invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores',
|
invalidVariableName: 'Variable name can only start with English letters and contain English letters, numbers, and underscores',
|
||||||
description: 'Display Name',
|
description: 'Display Name',
|
||||||
default: 'Default Value',
|
default: 'Default Value',
|
||||||
required: 'Required',
|
required: 'Required',
|
||||||
@@ -1658,10 +1670,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
editParam: 'Edit Extract Parameter',
|
editParam: 'Edit Extract Parameter',
|
||||||
|
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
invalidParamName: 'Parameter name must start with a letter and contain only letters, numbers, and underscores',
|
invalidParamName: 'Extract parameter name can only start with English letters and contain English letters, numbers, and underscores',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
desc: 'Description',
|
desc: 'Description',
|
||||||
required: 'Required',
|
required: 'Required',
|
||||||
|
default: 'Default Value',
|
||||||
|
|
||||||
'string': 'String',
|
'string': 'String',
|
||||||
'number': 'Number',
|
'number': 'Number',
|
||||||
@@ -1673,7 +1686,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
},
|
},
|
||||||
'var-aggregator': {
|
'var-aggregator': {
|
||||||
group: 'Aggregation Group',
|
group: 'Aggregation Group',
|
||||||
invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores',
|
invalidVariableName: 'Variable name can only start with English letters and contain English letters, numbers, and underscores',
|
||||||
addGroup: 'Add Group',
|
addGroup: 'Add Group',
|
||||||
variable: 'Variable Assignment'
|
variable: 'Variable Assignment'
|
||||||
},
|
},
|
||||||
@@ -1691,7 +1704,76 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
"gt": '>',
|
"gt": '>',
|
||||||
"ge": '>=',
|
"ge": '>=',
|
||||||
else_desc: 'Used to define the logic that should be executed when the if condition is not met.'
|
else_desc: 'Used to define the logic that should be executed when the if condition is not met.'
|
||||||
}
|
},
|
||||||
|
'http-request': {
|
||||||
|
auth: 'Authentication',
|
||||||
|
authType: 'Auth Type',
|
||||||
|
apiKey: 'API Key',
|
||||||
|
basic: 'Basic',
|
||||||
|
bearer: 'Bearer',
|
||||||
|
custom: 'Custom',
|
||||||
|
header: 'Header',
|
||||||
|
api_key: 'API Key',
|
||||||
|
timeouts: 'Timeout Settings',
|
||||||
|
"connect_timeout": 'Connection Timeout (seconds)',
|
||||||
|
"read_timeout": 'Read Timeout (seconds)',
|
||||||
|
"write_timeout": 'Write Timeout (seconds)',
|
||||||
|
retry: 'Retry on Failure',
|
||||||
|
error_handle: 'Error Handling',
|
||||||
|
verify_ssl: 'Verify SSL Certificate',
|
||||||
|
none: 'None',
|
||||||
|
default: 'Default Value',
|
||||||
|
branch: 'Error Branch',
|
||||||
|
status_code: 'Status Code',
|
||||||
|
max_attempts: 'Max Retry Attempts',
|
||||||
|
retry_interval: 'Retry Interval',
|
||||||
|
},
|
||||||
|
'jinja-render': {
|
||||||
|
template: 'Code',
|
||||||
|
mapping: 'Input Variables'
|
||||||
|
},
|
||||||
|
'question-classifier': {
|
||||||
|
model_id: 'Model',
|
||||||
|
input_variable: 'Input Variable',
|
||||||
|
categories: 'Categories',
|
||||||
|
user_supplement_prompt: 'Instruction',
|
||||||
|
class_name: 'Classification',
|
||||||
|
addClassName: 'Add Classification'
|
||||||
|
},
|
||||||
|
loop: {
|
||||||
|
cycle_vars: 'Loop Variables',
|
||||||
|
condition: 'Loop Termination Condition',
|
||||||
|
},
|
||||||
|
assigner: {
|
||||||
|
assignments: 'Variables',
|
||||||
|
cover: 'Overwrite',
|
||||||
|
assign: 'Set',
|
||||||
|
clear: 'Clear'
|
||||||
|
},
|
||||||
|
iteration: {
|
||||||
|
input: 'Input Variable',
|
||||||
|
output: 'Output Variable',
|
||||||
|
parallel: 'Parallel Mode',
|
||||||
|
parallel_count: 'Max Parallelism',
|
||||||
|
flatten: 'Flatten Output',
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
tool_id: 'Tool',
|
||||||
|
},
|
||||||
|
'memory-read': {
|
||||||
|
message: 'Message',
|
||||||
|
config_id: 'Memory Configuration',
|
||||||
|
search_switch: 'Search Mode',
|
||||||
|
},
|
||||||
|
'memory-write': {
|
||||||
|
message: 'Message',
|
||||||
|
config_id: 'Memory Configuration',
|
||||||
|
search_switch: 'Search Mode',
|
||||||
|
},
|
||||||
|
name: 'Key',
|
||||||
|
type: 'Type',
|
||||||
|
value: 'Value',
|
||||||
|
addCase: 'Add Condition',
|
||||||
},
|
},
|
||||||
|
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
|
|||||||
@@ -1198,7 +1198,11 @@ export const zh = {
|
|||||||
updated_at: '最后更新时间',
|
updated_at: '最后更新时间',
|
||||||
fullScreen: '全屏',
|
fullScreen: '全屏',
|
||||||
|
|
||||||
memoryWindow: "{{name}}的记忆之窗"
|
memoryWindow: "{{name}}的记忆之窗",
|
||||||
|
memory_insight: '总体概述',
|
||||||
|
key_findings: '关键发现',
|
||||||
|
behavior_pattern: '行为模式',
|
||||||
|
growth_trajectory: '成长轨迹',
|
||||||
},
|
},
|
||||||
space: {
|
space: {
|
||||||
createSpace: '创建空间',
|
createSpace: '创建空间',
|
||||||
@@ -1680,16 +1684,22 @@ export const zh = {
|
|||||||
'parameter-extractor': '参数提取',
|
'parameter-extractor': '参数提取',
|
||||||
flowControl: '流程控制',
|
flowControl: '流程控制',
|
||||||
'if-else': '条件分支',
|
'if-else': '条件分支',
|
||||||
|
'question-classifier': '问题分类器',
|
||||||
iteration: '迭代 (Iteration)',
|
iteration: '迭代 (Iteration)',
|
||||||
loop: '循环 (Loop)',
|
loop: '循环 (Loop)',
|
||||||
|
'cycle-start': '',
|
||||||
|
break: '退出循环',
|
||||||
|
assigner: '变量赋值',
|
||||||
parallel: '并行执行',
|
parallel: '并行执行',
|
||||||
'var-aggregator': '变量聚合器',
|
'var-aggregator': '变量聚合器',
|
||||||
externalInteraction: '外部交互',
|
externalInteraction: '外部交互',
|
||||||
"http-request": 'HTTP请求',
|
"http-request": 'HTTP请求',
|
||||||
tools: '工具 (Tools)',
|
tool: '工具 (Tool)',
|
||||||
code_execution: '代码执行',
|
code_execution: '代码执行',
|
||||||
"jinja-render": '模板渲染',
|
"jinja-render": '模板渲染',
|
||||||
cognitiveUpgrading: '认知升级(创新)',
|
cognitiveUpgrading: '认知升级(创新)',
|
||||||
|
'memory-read': '记忆提取',
|
||||||
|
'memory-write': '记忆储存',
|
||||||
task_planning: '任务规划',
|
task_planning: '任务规划',
|
||||||
reasoning_control: '推理控制',
|
reasoning_control: '推理控制',
|
||||||
self_reflection: '自我反思',
|
self_reflection: '自我反思',
|
||||||
@@ -1709,7 +1719,9 @@ export const zh = {
|
|||||||
nodeProperties: '节点属性',
|
nodeProperties: '节点属性',
|
||||||
empty: "Emmm…盒子是空的,这里什么都没有~",
|
empty: "Emmm…盒子是空的,这里什么都没有~",
|
||||||
nodeName: '节点名称',
|
nodeName: '节点名称',
|
||||||
|
addvariable: '会话变量',
|
||||||
|
addChatVariable: '添加会话变量',
|
||||||
|
editChatVariable: '编辑会话变量',
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
llm: {
|
llm: {
|
||||||
@@ -1762,6 +1774,7 @@ export const zh = {
|
|||||||
type: '类型',
|
type: '类型',
|
||||||
desc: '描述',
|
desc: '描述',
|
||||||
required: '必填',
|
required: '必填',
|
||||||
|
default: '默认值',
|
||||||
|
|
||||||
'string': 'String',
|
'string': 'String',
|
||||||
'number': 'Number',
|
'number': 'Number',
|
||||||
@@ -1819,9 +1832,48 @@ export const zh = {
|
|||||||
template: '代码',
|
template: '代码',
|
||||||
mapping: '输入变量'
|
mapping: '输入变量'
|
||||||
},
|
},
|
||||||
|
'question-classifier': {
|
||||||
|
model_id: '模型',
|
||||||
|
input_variable: '输入变量',
|
||||||
|
categories: '分类',
|
||||||
|
user_supplement_prompt: '指令',
|
||||||
|
class_name: '分类',
|
||||||
|
addClassName: '添加分类'
|
||||||
|
},
|
||||||
|
loop: {
|
||||||
|
cycle_vars: '循环变量',
|
||||||
|
condition: '循环终止条件',
|
||||||
|
},
|
||||||
|
assigner: {
|
||||||
|
assignments: '变量',
|
||||||
|
cover: '覆盖',
|
||||||
|
assign: '设置',
|
||||||
|
clear: '清空'
|
||||||
|
},
|
||||||
|
iteration: {
|
||||||
|
input: '输入变量',
|
||||||
|
output: '输出变量',
|
||||||
|
parallel: '并行模式',
|
||||||
|
parallel_count: '最大并行度',
|
||||||
|
flatten: '扁平化输出',
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
tool_id: '工具',
|
||||||
|
},
|
||||||
|
'memory-read': {
|
||||||
|
message: '消息',
|
||||||
|
config_id: '记忆配置',
|
||||||
|
search_switch: '检索模式',
|
||||||
|
},
|
||||||
|
'memory-write': {
|
||||||
|
message: '消息',
|
||||||
|
config_id: '记忆配置',
|
||||||
|
search_switch: '检索模式',
|
||||||
|
},
|
||||||
name: '键',
|
name: '键',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
value: '值',
|
value: '值',
|
||||||
|
addCase: '添加条件',
|
||||||
},
|
},
|
||||||
|
|
||||||
clear: '清空',
|
clear: '清空',
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
const clear = () => {
|
const clear = () => {
|
||||||
workflowRef?.current?.graphRef?.current?.clearCells()
|
workflowRef?.current?.graphRef?.current?.clearCells()
|
||||||
}
|
}
|
||||||
|
const addvariable = () => {
|
||||||
|
workflowRef?.current?.addVariable()
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
|
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
|
||||||
@@ -132,6 +135,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
{application?.type === 'workflow'
|
{application?.type === 'workflow'
|
||||||
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
|
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
|
||||||
<Button onClick={clear}>{t('workflow.clear')}</Button>
|
<Button onClick={clear}>{t('workflow.clear')}</Button>
|
||||||
|
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
|
||||||
<Button onClick={run}>{t('workflow.run')}</Button>
|
<Button onClick={run}>{t('workflow.run')}</Button>
|
||||||
<Button type="primary" onClick={save}>{t('workflow.save')}</Button>
|
<Button type="primary" onClick={save}>{t('workflow.save')}</Button>
|
||||||
{/* <Button type="primary">{t('workflow.export')}</Button> */}
|
{/* <Button type="primary">{t('workflow.export')}</Button> */}
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ export interface ClusterRef {
|
|||||||
export interface WorkflowRef {
|
export interface WorkflowRef {
|
||||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||||
handleRun: () => void;
|
handleRun: () => void;
|
||||||
graphRef: GraphRef
|
graphRef: GraphRef;
|
||||||
|
addVariable: () => void;
|
||||||
}
|
}
|
||||||
export interface ApplicationModalRef {
|
export interface ApplicationModalRef {
|
||||||
handleOpen: (application?: Config) => void;
|
handleOpen: (application?: Config) => void;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
type ToolType = 'mcp' | 'builtin' | 'custom'
|
export type ToolType = 'mcp' | 'builtin' | 'custom'
|
||||||
export interface Query {
|
export interface Query {
|
||||||
name?: string;
|
name?: string;
|
||||||
tool_type: ToolType
|
tool_type: ToolType
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { type FC, useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||||
|
import clsx from 'clsx'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { Skeleton } from 'antd';
|
import { Skeleton, Space } from 'antd';
|
||||||
import RbCard from '@/components/RbCard/Card'
|
import RbCard from '@/components/RbCard/Card'
|
||||||
import Empty from '@/components/Empty';
|
import Empty from '@/components/Empty';
|
||||||
import {
|
import {
|
||||||
@@ -9,11 +10,20 @@ import {
|
|||||||
} from '@/api/memory'
|
} from '@/api/memory'
|
||||||
import type { MemoryInsightRef } from '../types'
|
import type { MemoryInsightRef } from '../types'
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
memory_insight?: string;
|
||||||
|
behavior_pattern?: string;
|
||||||
|
key_findings?: string[];
|
||||||
|
growth_trajectory?: string;
|
||||||
|
updated_at?: number;
|
||||||
|
is_cached: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [report, setReport] = useState<string | null>(null)
|
const [data, setData] = useState<Data>({} as Data)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -25,7 +35,7 @@ const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
|||||||
if (!id) return
|
if (!id) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
getMemoryInsightReport(id).then((res) => {
|
getMemoryInsightReport(id).then((res) => {
|
||||||
setReport((res as { report?: string }).report || null)
|
setData((res as Data) || {})
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -43,10 +53,35 @@ const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
|||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
? <Skeleton />
|
? <Skeleton />
|
||||||
: report
|
: Object.keys(data).length > 0
|
||||||
? <div className="rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3 rb:px-4 rb:text-[#5B6167] rb:leading-5">
|
? <Space size={16} direction="vertical" className="rb:w-full">
|
||||||
{report || '-'}
|
{['memory_insight', 'key_findings', 'behavior_pattern', 'growth_trajectory'].map(key => {
|
||||||
</div>
|
if (data[key as keyof Data]) {
|
||||||
|
return (
|
||||||
|
<div key={key} className="rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3 rb:text-[#5B6167] rb:leading-5">
|
||||||
|
<div className={clsx(`rb:relative rb:before:content-[''] rb:before:block rb:before:h-4 rb:before:absolute rb:before:top-0.5 rb:before:left-0 rb:before:w-1 rb:pl-4 rb:mb-2 rb:font-medium rb:leading-5`, {
|
||||||
|
'rb:before:bg-[#155EEF]': key === 'memory_insight',
|
||||||
|
'rb:before:bg-[#369F21]': key !== 'memory_insight'
|
||||||
|
})}>{t(`userMemory.${key}`)}</div>
|
||||||
|
<div className="rb:px-4">
|
||||||
|
{Array.isArray(data[key as keyof Data])
|
||||||
|
? <>
|
||||||
|
{(data[key as keyof Data] as string[])?.map((item: string, index: number) => (
|
||||||
|
<div key={index}>
|
||||||
|
- {item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
: data[key as keyof Data] as string
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
|
||||||
|
</Space>
|
||||||
: <Empty size={80} />
|
: <Empty size={80} />
|
||||||
}
|
}
|
||||||
</RbCard>
|
</RbCard>
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
|
import { Form, Input, Select, Checkbox, InputNumber } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { ChatVariableModalRef } from './types'
|
||||||
|
import type { ChatVariable } from '../../types';
|
||||||
|
import RbModal from '@/components/RbModal'
|
||||||
|
|
||||||
|
const FormItem = Form.Item;
|
||||||
|
|
||||||
|
interface ChatVariableModalProps {
|
||||||
|
refresh: (value: ChatVariable, editIndex?: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = [
|
||||||
|
'string',
|
||||||
|
'number',
|
||||||
|
'boolean',
|
||||||
|
]
|
||||||
|
|
||||||
|
const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProps>(({
|
||||||
|
refresh
|
||||||
|
}, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [form] = Form.useForm<ChatVariable>();
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [editIndex, setEditIndex] = useState<number | undefined>(undefined)
|
||||||
|
const typeValue = Form.useWatch('type', form);
|
||||||
|
|
||||||
|
// 封装取消方法,添加关闭弹窗逻辑
|
||||||
|
const handleClose = () => {
|
||||||
|
setVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
setLoading(false)
|
||||||
|
setEditIndex(undefined)
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpen = (variable?: ChatVariable, 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
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RbModal
|
||||||
|
title={editIndex !== undefined ? t('workflow.editChatVariable') : t('workflow.addChatVariable')}
|
||||||
|
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')}
|
||||||
|
onChange={() => form.setFieldValue('default', undefined)}
|
||||||
|
options={types.map(key => ({
|
||||||
|
value: key,
|
||||||
|
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
name="default"
|
||||||
|
label={t('workflow.config.parameter-extractor.default')}
|
||||||
|
>
|
||||||
|
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
const type = getFieldValue('type');
|
||||||
|
if (type === 'number') {
|
||||||
|
return <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />;
|
||||||
|
}
|
||||||
|
if (type === 'boolean') {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={[
|
||||||
|
{ value: true, label: 'true' },
|
||||||
|
{ value: false, label: 'false' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Input placeholder={t('common.enter')} />;
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
name="description"
|
||||||
|
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 ChatVariableModal;
|
||||||
113
web/src/views/Workflow/components/AddChatVariable/index.tsx
Normal file
113
web/src/views/Workflow/components/AddChatVariable/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import React, { useState, useImperativeHandle, forwardRef, useRef } from 'react';
|
||||||
|
import { Button, Input, Space, Typography, Tooltip, message, List } from 'antd';
|
||||||
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { ChatVariable, AddChatVariableRef } from '../../types';
|
||||||
|
import type { ChatVariableModalRef } from './types'
|
||||||
|
import RbDrawer from '@/components/RbDrawer';
|
||||||
|
import Empty from '@/components/Empty';
|
||||||
|
import ChatVariableModal from './ChatVariableModal';
|
||||||
|
|
||||||
|
interface AddChatVariableProps {
|
||||||
|
variables?: ChatVariable[];
|
||||||
|
onChange?: (variables: ChatVariable[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
maxVariables?: number;
|
||||||
|
}
|
||||||
|
const AddChatVariable = forwardRef<AddChatVariableRef, AddChatVariableProps>(({
|
||||||
|
variables = [],
|
||||||
|
onChange,
|
||||||
|
}, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const chatVariableRef = useRef<ChatVariableModalRef>(null);
|
||||||
|
|
||||||
|
const handleAddVariable = () => {
|
||||||
|
chatVariableRef.current?.handleOpen()
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (index: number) => {
|
||||||
|
chatVariableRef.current?.handleOpen(variables[index], index)
|
||||||
|
}
|
||||||
|
const handleDelete = (index: number) => {
|
||||||
|
const list = [...variables]
|
||||||
|
list.splice(index, 1)
|
||||||
|
onChange && onChange(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
const handleSave = (value: ChatVariable, index?: number) => {
|
||||||
|
const list = [...variables]
|
||||||
|
if (index && index > -1) {
|
||||||
|
list[index] = value
|
||||||
|
} else {
|
||||||
|
list.push(value)
|
||||||
|
}
|
||||||
|
onChange && onChange(list)
|
||||||
|
}
|
||||||
|
// 暴露给父组件的方法
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
handleOpen,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RbDrawer
|
||||||
|
title={t('workflow.addvariable')}
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className="rb:mb-3"
|
||||||
|
onClick={handleAddVariable}
|
||||||
|
>
|
||||||
|
+ {t('workflow.addChatVariable')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{variables.length === 0
|
||||||
|
? <Empty size={88} />
|
||||||
|
:
|
||||||
|
<List
|
||||||
|
grid={{ gutter: 12, column: 1 }}
|
||||||
|
dataSource={variables}
|
||||||
|
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.description}</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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChatVariableModal
|
||||||
|
ref={chatVariableRef}
|
||||||
|
refresh={handleSave}
|
||||||
|
/>
|
||||||
|
</RbDrawer>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AddChatVariable;
|
||||||
24
web/src/views/Workflow/components/AddChatVariable/types.ts
Normal file
24
web/src/views/Workflow/components/AddChatVariable/types.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { ChatVariable } from '../../types'
|
||||||
|
|
||||||
|
export interface AddChatVariableProps {
|
||||||
|
variables?: ChatVariable[];
|
||||||
|
onChange?: (variables: ChatVariable[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
maxVariables?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VariableFormData {
|
||||||
|
name: string;
|
||||||
|
type: ChatVariable['type'];
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatVariableModalRef {
|
||||||
|
handleOpen: (value?: ChatVariable, index?: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatVariableModalRef {
|
||||||
|
handleOpen: (vo?: ChatVariable, index?: number) => void;
|
||||||
|
}
|
||||||
@@ -44,14 +44,14 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
|||||||
>
|
>
|
||||||
{data.isContext ? (
|
{data.isContext ? (
|
||||||
<span style={{ fontSize: '12px', marginRight: '4px' }}>📄</span>
|
<span style={{ fontSize: '12px', marginRight: '4px' }}>📄</span>
|
||||||
) : (
|
) : data.group !== 'CONVERSATION' ? (
|
||||||
<img
|
<img
|
||||||
src={data.nodeData?.icon}
|
src={data.nodeData?.icon}
|
||||||
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
{!data.isContext && (
|
{!data.isContext && data.group !== 'CONVERSATION' && (
|
||||||
<>
|
<>
|
||||||
<span className="rb:wrap-break-word rb:line-clamp-1">{data.nodeData?.name}</span>
|
<span className="rb:wrap-break-word rb:line-clamp-1">{data.nodeData?.name}</span>
|
||||||
<span style={{ color: '#DFE4ED', margin: '0 2px' }}>/</span>
|
<span style={{ color: '#DFE4ED', margin: '0 2px' }}>/</span>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface Suggestion {
|
|||||||
type: string;
|
type: string;
|
||||||
dataType: string;
|
dataType: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
group?: string
|
||||||
nodeData: NodeProperties;
|
nodeData: NodeProperties;
|
||||||
isContext?: boolean; // 标记是否为context变量
|
isContext?: boolean; // 标记是否为context变量
|
||||||
disabled?: boolean; // 标记是否禁用
|
disabled?: boolean; // 标记是否禁用
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ const NodeLibrary: FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Space size={8} direction="vertical" className="rb:w-full">
|
<Space size={8} direction="vertical" className="rb:w-full">
|
||||||
{category.nodes.map((node, nodeIndex) => (
|
{category.nodes
|
||||||
|
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||||
|
.map((node, nodeIndex) => (
|
||||||
<div
|
<div
|
||||||
key={nodeIndex}
|
key={nodeIndex}
|
||||||
className="rb:bg-white rb:rounded-lg rb:p-2 rb:border rb:border-[#DFE4ED] rb:cursor-pointer rb:flex rb:items-center rb:gap-2 rb:hover:border-[#155EEF] rb:hover:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.15)]"
|
className="rb:bg-white rb:rounded-lg rb:p-2 rb:border rb:border-[#DFE4ED] rb:cursor-pointer rb:flex rb:items-center rb:gap-2 rb:hover:border-[#155EEF] rb:hover:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.15)]"
|
||||||
|
|||||||
@@ -1,18 +1,167 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Popover } from 'antd';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||||
|
import { nodeLibrary, graphNodeLibrary } from '../../constant';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const AddNode: ReactShapeConfig['component'] = ({ node }) => {
|
const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||||
const data = node?.getData() || {}
|
const data = node?.getData() || {};
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleNodeSelect = (selectedNodeType: any) => {
|
||||||
|
const parentBBox = node.getBBox();
|
||||||
|
const cycleId = data.cycle;
|
||||||
|
|
||||||
|
const newNode = graph.addNode({
|
||||||
|
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
|
||||||
|
x: parentBBox.x,
|
||||||
|
y: parentBBox.y,
|
||||||
|
data: {
|
||||||
|
id: `${selectedNodeType.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
type: selectedNodeType.type,
|
||||||
|
icon: selectedNodeType.icon,
|
||||||
|
name: t(`workflow.${selectedNodeType.type}`),
|
||||||
|
cycle: cycleId,
|
||||||
|
parentId: data.parentId,
|
||||||
|
config: selectedNodeType.config || {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将新节点添加为父节点的子节点
|
||||||
|
if (cycleId) {
|
||||||
|
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
|
||||||
|
if (parentNode) {
|
||||||
|
parentNode.addChild(newNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const incomingEdges = graph.getIncomingEdges(node);
|
||||||
|
const outgoingEdges = graph.getOutgoingEdges(node);
|
||||||
|
|
||||||
|
incomingEdges?.forEach(edge => {
|
||||||
|
graph.addEdge({
|
||||||
|
source: { cell: edge.getSourceCellId(), port: edge.getSourcePortId() },
|
||||||
|
target: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'left')?.id || 'left' },
|
||||||
|
attrs: edge.getAttrs()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
outgoingEdges?.forEach(edge => {
|
||||||
|
const targetCell = graph.getCellById(edge.getTargetCellId()) as any;
|
||||||
|
const targetPortId = targetCell?.getPorts?.()?.find((port: any) => port.group === 'left')?.id || edge.getTargetPortId();
|
||||||
|
graph.addEdge({
|
||||||
|
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
|
||||||
|
target: { cell: edge.getTargetCellId(), port: targetPortId },
|
||||||
|
attrs: edge.getAttrs()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除所有add-node类型的节点
|
||||||
|
graph.getNodes().forEach((n: any) => {
|
||||||
|
if (n.getData()?.type === 'add-node' && n.getData()?.cycle === cycleId) {
|
||||||
|
n.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自动调整循环节点大小
|
||||||
|
const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
|
||||||
|
if (loopNode) {
|
||||||
|
const adjustLoopSize = () => {
|
||||||
|
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
|
||||||
|
if (childNodes.length > 0) {
|
||||||
|
const bounds = childNodes.reduce((acc, child) => {
|
||||||
|
const bbox = child.getBBox();
|
||||||
|
return {
|
||||||
|
minX: Math.min(acc.minX, bbox.x),
|
||||||
|
minY: Math.min(acc.minY, bbox.y),
|
||||||
|
maxX: Math.max(acc.maxX, bbox.x + bbox.width),
|
||||||
|
maxY: Math.max(acc.maxY, bbox.y + bbox.height)
|
||||||
|
};
|
||||||
|
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||||
|
|
||||||
|
const padding = 20;
|
||||||
|
const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2);
|
||||||
|
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2);
|
||||||
|
|
||||||
|
loopNode.prop('size', { width: newWidth, height: newHeight });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
adjustLoopSize();
|
||||||
|
|
||||||
|
// 监听子节点移动事件
|
||||||
|
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
|
||||||
|
childNodes.forEach((childNode: any) => {
|
||||||
|
childNode.on('change:position', adjustLoopSize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}>
|
||||||
|
{nodeLibrary.map((category, categoryIndex) => {
|
||||||
|
const filteredNodes = category.nodes.filter(nodeType =>
|
||||||
|
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredNodes.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category.category}>
|
||||||
|
{categoryIndex > 0 && <div style={{ height: '1px', background: '#f0f0f0', margin: '4px 0' }} />}
|
||||||
|
<div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}>
|
||||||
|
{t(`workflow.${category.category}`)}
|
||||||
|
</div>
|
||||||
|
{filteredNodes.map((nodeType) => (
|
||||||
|
<div
|
||||||
|
key={nodeType.type}
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
}}
|
||||||
|
onClick={() => handleNodeSelect(nodeType)}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f0f8ff';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'white';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={nodeType.icon} className="rb:w-4 rb:h-4" />
|
||||||
|
<span style={{ fontSize: '14px' }}>{t(`workflow.${nodeType.type}`)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('rb:group rb:relative rb:h-10 rb:w-30 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border', {
|
<Popover
|
||||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
content={content}
|
||||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
trigger="click"
|
||||||
})}>
|
open={open}
|
||||||
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
onOpenChange={setOpen}
|
||||||
{data.icon} {data.label}
|
placement="bottomLeft"
|
||||||
</span>
|
>
|
||||||
</div>
|
<div
|
||||||
|
className={clsx('rb:group rb:relative rb:h-11 rb:w-22 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border rb:cursor-pointer', {
|
||||||
|
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
||||||
|
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
||||||
|
{data.icon} {data.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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)]', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full 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-[#155EEF]': data.isSelected,
|
||||||
'rb:border-[#DFE4ED]': !data.isSelected
|
'rb:border-[#DFE4ED]': !data.isSelected
|
||||||
})}>
|
})}>
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||||
|
import startIcon from '@/assets/images/workflow/start.png';
|
||||||
|
|
||||||
const GroupStartNode: ReactShapeConfig['component'] = ({ node }) => {
|
const GroupStartNode: ReactShapeConfig['component'] = () => {
|
||||||
const data = node?.getData() || {}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('rb:group rb:relative rb:h-10 rb:w-20 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full 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-[#DFE4ED]')}>
|
||||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
<img src={startIcon} className="rb:w-6 rb:h-6" />
|
||||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
|
||||||
})}>
|
|
||||||
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
|
||||||
{data.icon} {data.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { Dropdown } from 'antd';
|
|
||||||
import { SmallDashOutlined } from '@ant-design/icons';
|
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
|
||||||
import { graphNodeLibrary } from '../../constant';
|
|
||||||
|
|
||||||
interface NodeData {
|
|
||||||
isSelected?: boolean;
|
|
||||||
type?: string;
|
|
||||||
label?: string;
|
|
||||||
icon?: string;
|
|
||||||
parentId?: string;
|
|
||||||
isGroup?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IterationNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|
||||||
const data = node.getData() as NodeData;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initNodes()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const initNodes = () => {
|
|
||||||
// 添加默认子节点
|
|
||||||
const parentBBox = node.getBBox();
|
|
||||||
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
|
||||||
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
|
||||||
|
|
||||||
const childNode1 = graph.addNode({
|
|
||||||
...graphNodeLibrary.groupStart,
|
|
||||||
x: centerX,
|
|
||||||
y: centerY,
|
|
||||||
data: {
|
|
||||||
type: 'default',
|
|
||||||
label: '开始',
|
|
||||||
// icon: '📌',
|
|
||||||
parentId: node.id,
|
|
||||||
isDefault: true // 标记为默认节点,不可删除
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const childNode2 = graph.addNode({
|
|
||||||
...graphNodeLibrary.addStart,
|
|
||||||
x: centerX + 150,
|
|
||||||
y: centerY,
|
|
||||||
data: {
|
|
||||||
type: 'default',
|
|
||||||
label: '添加节点',
|
|
||||||
icon: '+',
|
|
||||||
parentId: node.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
node.addChild(childNode1)
|
|
||||||
node.addChild(childNode2)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={clsx('rb:group rb:border-2 rb:border-dashed rb:rounded-xl rb:relative rb:min-w-75 rb:min-h-50 rb:p-4', {
|
|
||||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data?.isSelected,
|
|
||||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data?.isSelected
|
|
||||||
})}>
|
|
||||||
{/* 标题区域 */}
|
|
||||||
<div className="rb:absolute rb:-top-3 rb:left-4 rb:bg-[#10b981] rb:rounded-[20px] rb:p-[8px_16px] rb:flex rb:items-center rb:gap-2 rb:text-white rb:text-[14px] rb:font-bold rb:z-10">
|
|
||||||
<div className="rb:w-5 rb:h-5 rb:bg-[#FFFFFF] rb:rounded-sm rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:text-[#10b981]">
|
|
||||||
🔁
|
|
||||||
</div>
|
|
||||||
迭代
|
|
||||||
</div>
|
|
||||||
<Dropdown
|
|
||||||
menu={{items: [
|
|
||||||
{
|
|
||||||
key: '1',
|
|
||||||
label: '删除',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '2',
|
|
||||||
label: '复制',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '3',
|
|
||||||
label: '删除',
|
|
||||||
}
|
|
||||||
]}}
|
|
||||||
>
|
|
||||||
<SmallDashOutlined
|
|
||||||
className={clsx("rb:cursor-pointer rb:right-1 rb:top-1 rb:invisible rb:absolute rb:group-hover:visible", {
|
|
||||||
'rb:visible': data.isSelected
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
{/* 画布内容区域 */}
|
|
||||||
<div className="rb:mt-6 rb:min-h-37.5 rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IterationNode;
|
|
||||||
@@ -1,19 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Dropdown } from 'antd';
|
|
||||||
import { SmallDashOutlined } from '@ant-design/icons';
|
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||||
import { graphNodeLibrary } from '../../constant';
|
import { graphNodeLibrary } from '../../constant';
|
||||||
|
|
||||||
interface NodeData {
|
import { edge_color } from '../../hooks/useWorkflowGraph'
|
||||||
isSelected?: boolean;
|
|
||||||
type?: string;
|
|
||||||
label?: string;
|
|
||||||
icon?: string;
|
|
||||||
parentId?: string;
|
|
||||||
isGroup?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||||
const data = node.getData() || {};
|
const data = node.getData() || {};
|
||||||
@@ -21,63 +12,145 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initNodes()
|
initNodes()
|
||||||
|
// 检查是否需要添加add-node
|
||||||
|
checkAndAddAddNode()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const checkAndAddAddNode = () => {
|
||||||
|
if (!graph) return;
|
||||||
|
|
||||||
|
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === data.id);
|
||||||
|
const cycleStartNodes = childNodes.filter((n: any) => n.getData()?.type === 'cycle-start');
|
||||||
|
|
||||||
|
// 如果只有一个cycle-start节点且没有其他类型的子节点,则添加add-node
|
||||||
|
if (cycleStartNodes.length === 1 && childNodes.length === 1) {
|
||||||
|
const cycleStartNode = cycleStartNodes[0];
|
||||||
|
const cycleStartBBox = cycleStartNode.getBBox();
|
||||||
|
|
||||||
|
const addNode = graph.addNode({
|
||||||
|
...graphNodeLibrary.addStart,
|
||||||
|
x: cycleStartBBox.x + 64,
|
||||||
|
y: cycleStartBBox.y,
|
||||||
|
data: {
|
||||||
|
type: 'add-node',
|
||||||
|
label: '添加节点',
|
||||||
|
icon: '+',
|
||||||
|
parentId: node.id,
|
||||||
|
cycle: data.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
node.addChild(addNode);
|
||||||
|
|
||||||
|
// 连接cycle-start和add-node
|
||||||
|
const sourcePorts = cycleStartNode.getPorts();
|
||||||
|
const targetPorts = addNode.getPorts();
|
||||||
|
const sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
|
||||||
|
const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
|
||||||
|
|
||||||
|
graph.addEdge({
|
||||||
|
source: { cell: cycleStartNode.id, port: sourcePort },
|
||||||
|
target: { cell: addNode.id, port: targetPort },
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: edge_color,
|
||||||
|
strokeWidth: 1,
|
||||||
|
targetMarker: {
|
||||||
|
name: 'block',
|
||||||
|
size: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const initNodes = () => {
|
const initNodes = () => {
|
||||||
|
// 检查是否存在cycle为当前节点ID的子节点,若存在则不调用initNodes,避免重复创建
|
||||||
|
const existingCycleNodes = graph.getNodes().filter((n: any) =>
|
||||||
|
n.getData()?.cycle === data.id
|
||||||
|
);
|
||||||
|
if (existingCycleNodes.length > 0) return;
|
||||||
// 添加默认子节点
|
// 添加默认子节点
|
||||||
const parentBBox = node.getBBox();
|
const parentBBox = node.getBBox();
|
||||||
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
||||||
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
||||||
|
|
||||||
const childNode1 = graph.addNode({
|
const cycleStartNode = graph.addNode({
|
||||||
...graphNodeLibrary.groupStart,
|
...graphNodeLibrary.cycleStart,
|
||||||
x: centerX,
|
x: centerX,
|
||||||
y: centerY,
|
y: centerY,
|
||||||
data: {
|
data: {
|
||||||
type: 'default',
|
type: 'cycle-start',
|
||||||
label: '开始',
|
|
||||||
// icon: '📌',
|
|
||||||
parentId: node.id,
|
parentId: node.id,
|
||||||
isDefault: true // 标记为默认节点,不可删除
|
isDefault: true, // 标记为默认节点,不可删除
|
||||||
|
cycle: data.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const childNode2 = graph.addNode({
|
const addNode = graph.addNode({
|
||||||
...graphNodeLibrary.addStart,
|
...graphNodeLibrary.addStart,
|
||||||
x: centerX + 150,
|
x: centerX + 64,
|
||||||
y: centerY,
|
y: centerY,
|
||||||
data: {
|
data: {
|
||||||
type: 'default',
|
type: 'add-node',
|
||||||
label: '添加节点',
|
label: '添加节点',
|
||||||
icon: '+',
|
icon: '+',
|
||||||
parentId: node.id,
|
parentId: node.id,
|
||||||
|
cycle: data.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
node.addChild(childNode1)
|
node.addChild(cycleStartNode)
|
||||||
node.addChild(childNode2)
|
node.addChild(addNode)
|
||||||
|
const sourcePorts = cycleStartNode.getPorts()
|
||||||
|
const targetPorts = addNode.getPorts()
|
||||||
|
let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
|
||||||
|
|
||||||
|
const edgeConfig = {
|
||||||
|
source: {
|
||||||
|
cell: cycleStartNode.id,
|
||||||
|
port: sourcePort
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
cell: addNode.id,
|
||||||
|
port: targetPorts.find((port: any) => port.group === 'left')?.id || 'left'
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: edge_color,
|
||||||
|
strokeWidth: 1,
|
||||||
|
targetMarker: {
|
||||||
|
name: 'block',
|
||||||
|
size: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.addEdge(edgeConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-16 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)]', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full 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-[#155EEF]': data.isSelected,
|
||||||
'rb:border-[#DFE4ED]': !data.isSelected
|
'rb:border-[#DFE4ED]': !data.isSelected
|
||||||
})}>
|
})}>
|
||||||
<div className="rb:flex rb:items-center rb:justify-between">
|
<div className="rb:flex rb:items-center rb:justify-between">
|
||||||
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
||||||
<img src={data.icon} className="rb:w-5 rb:h-5" />
|
<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 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>
|
||||||
<div className="rb:mt-6 rb:min-h-37.5 rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></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>
|
||||||
);
|
<div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoopNode;
|
export default LoopNode;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-16 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)]', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full 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-[#155EEF]': data.isSelected,
|
||||||
'rb:border-[#DFE4ED]': !data.isSelected
|
'rb:border-[#DFE4ED]': !data.isSelected
|
||||||
})}>
|
})}>
|
||||||
|
|||||||
222
web/src/views/Workflow/components/PortClickHandler.tsx
Normal file
222
web/src/views/Workflow/components/PortClickHandler.tsx
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Popover } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { nodeLibrary, graphNodeLibrary } from '../constant';
|
||||||
|
|
||||||
|
interface PortClickHandlerProps {
|
||||||
|
graph: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||||
|
const [popoverPosition, setPopoverPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [sourceNode, setSourceNode] = useState<any>(null);
|
||||||
|
const [sourcePort, setSourcePort] = useState<string>('');
|
||||||
|
const [tempElement, setTempElement] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePortClick = (event: CustomEvent) => {
|
||||||
|
const { node, port, element, rect } = event.detail;
|
||||||
|
setSourceNode(node);
|
||||||
|
setSourcePort(port);
|
||||||
|
setTempElement(element);
|
||||||
|
setPopoverPosition({ x: rect.left, y: rect.top });
|
||||||
|
setPopoverVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('port:click', handlePortClick as EventListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('port:click', handlePortClick as EventListener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleNodeSelect = (selectedNodeType: any) => {
|
||||||
|
if (!sourceNode || !graph) return;
|
||||||
|
|
||||||
|
const sourceNodeData = sourceNode.getData();
|
||||||
|
|
||||||
|
// 计算新节点位置(在源节点右侧)
|
||||||
|
const sourceBBox = sourceNode.getBBox();
|
||||||
|
const newX = sourceBBox.x + sourceBBox.width + 50;
|
||||||
|
const newY = sourceBBox.y;
|
||||||
|
|
||||||
|
// 创建新节点
|
||||||
|
const newNode = graph.addNode({
|
||||||
|
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
data: {
|
||||||
|
id: `${selectedNodeType.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
type: selectedNodeType.type,
|
||||||
|
icon: selectedNodeType.icon,
|
||||||
|
name: t(`workflow.${selectedNodeType.type}`),
|
||||||
|
cycle: sourceNodeData.cycle, // 继承源节点的cycle
|
||||||
|
config: selectedNodeType.config || {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将新节点添加为父节点的子节点
|
||||||
|
if (sourceNodeData.cycle) {
|
||||||
|
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle);
|
||||||
|
if (parentNode) {
|
||||||
|
parentNode.addChild(newNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建连线
|
||||||
|
setTimeout(() => {
|
||||||
|
const targetPorts = newNode.getPorts();
|
||||||
|
const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
|
||||||
|
|
||||||
|
graph.addEdge({
|
||||||
|
source: { cell: sourceNode.id, port: sourcePort },
|
||||||
|
target: { cell: newNode.id, port: targetPort },
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: '#155EEF',
|
||||||
|
strokeWidth: 1,
|
||||||
|
targetMarker: {
|
||||||
|
name: 'block',
|
||||||
|
size: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 循环节点内子节点通过连接桩添加时,调整循环节点大小
|
||||||
|
const cycleId = sourceNodeData.cycle;
|
||||||
|
if (cycleId) {
|
||||||
|
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
|
||||||
|
|
||||||
|
if (parentNode) {
|
||||||
|
const adjustLoopSize = () => {
|
||||||
|
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
|
||||||
|
if (childNodes.length > 0) {
|
||||||
|
const bounds = childNodes.reduce((acc: any, child: any) => {
|
||||||
|
const bbox = child.getBBox();
|
||||||
|
return {
|
||||||
|
minX: Math.min(acc.minX, bbox.x),
|
||||||
|
minY: Math.min(acc.minY, bbox.y),
|
||||||
|
maxX: Math.max(acc.maxX, bbox.x + bbox.width),
|
||||||
|
maxY: Math.max(acc.maxY, bbox.y + bbox.height)
|
||||||
|
};
|
||||||
|
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||||
|
|
||||||
|
const padding = 20;
|
||||||
|
const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2);
|
||||||
|
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2);
|
||||||
|
|
||||||
|
parentNode.prop('size', { width: newWidth, height: newHeight });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
adjustLoopSize();
|
||||||
|
|
||||||
|
// 监听子节点移动事件
|
||||||
|
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
|
||||||
|
childNodes.forEach((childNode: any) => {
|
||||||
|
childNode.on('change:position', adjustLoopSize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
// 清理临时元素
|
||||||
|
if (tempElement) {
|
||||||
|
document.body.removeChild(tempElement);
|
||||||
|
setTempElement(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPopoverVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePopoverClose = () => {
|
||||||
|
setPopoverVisible(false);
|
||||||
|
if (tempElement) {
|
||||||
|
document.body.removeChild(tempElement);
|
||||||
|
setTempElement(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}>
|
||||||
|
{nodeLibrary.map((category, categoryIndex) => {
|
||||||
|
const sourceNodeData = sourceNode?.getData();
|
||||||
|
const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop');
|
||||||
|
const isChildOfIteration = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'iteration');
|
||||||
|
|
||||||
|
let filteredNodes;
|
||||||
|
if (isChildOfLoop) {
|
||||||
|
// Use same filtering as AddNode for child nodes of loop
|
||||||
|
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
|
||||||
|
} else if (isChildOfIteration) {
|
||||||
|
// Filter out loop and iteration nodes for children of iteration nodes
|
||||||
|
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'break', 'cycle-start', 'iteration'].includes(nodeType.type));
|
||||||
|
} else {
|
||||||
|
// Original filtering for non-loop child nodes
|
||||||
|
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type));
|
||||||
|
filteredNodes = category.nodes.filter(nodeType =>
|
||||||
|
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredNodes.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category.category}>
|
||||||
|
{categoryIndex > 0 && <div style={{ height: '1px', background: '#f0f0f0', margin: '4px 0' }} />}
|
||||||
|
<div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}>
|
||||||
|
{t(`workflow.${category.category}`)}
|
||||||
|
</div>
|
||||||
|
{filteredNodes.map((nodeType) => (
|
||||||
|
<div
|
||||||
|
key={nodeType.type}
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
}}
|
||||||
|
onClick={() => handleNodeSelect(nodeType)}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f0f8ff';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'white';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={nodeType.icon} className="rb:w-4 rb:h-4" />
|
||||||
|
<span style={{ fontSize: '14px' }}>{t(`workflow.${nodeType.type}`)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tempElement) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={content}
|
||||||
|
open={popoverVisible}
|
||||||
|
onOpenChange={(visible) => {
|
||||||
|
if (!visible) handlePopoverClose();
|
||||||
|
}}
|
||||||
|
placement="right"
|
||||||
|
overlayStyle={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: popoverPosition.x + 10,
|
||||||
|
top: popoverPosition.y - 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PortClickHandler;
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Form, Input, Button, Row, Col, Select } from 'antd'
|
||||||
|
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
|
import VariableSelect from '../VariableSelect'
|
||||||
|
|
||||||
|
interface AssignmentListProps {
|
||||||
|
value?: Array<{ variable_selector: string; operation: string[]; value: string;}>;
|
||||||
|
parentName: string;
|
||||||
|
options: Suggestion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssignmentList: FC<AssignmentListProps> = ({
|
||||||
|
parentName,
|
||||||
|
options = [],
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.List name={parentName}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
<div className="rb:flex rb:justify-between">
|
||||||
|
{t(`workflow.config.assigner.${parentName}`)}
|
||||||
|
<PlusOutlined onClick={() => add({ operation: 'cover'})} />
|
||||||
|
</div>
|
||||||
|
{fields.map(({ key, name, ...restField }) => {
|
||||||
|
return (
|
||||||
|
<div key={key} className="rb:mb-4">
|
||||||
|
<Row gutter={12} className="rb:mb-2!">
|
||||||
|
<Col span={14}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'variable_selector']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<VariableSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={options}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'operation']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'cover', label: t('workflow.config.assigner.cover') },
|
||||||
|
{ value: 'clear', label: t('workflow.config.assigner.clear') },
|
||||||
|
{ value: 'assign', label: t('workflow.config.assigner.assign') },
|
||||||
|
]}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
onChange={() => {
|
||||||
|
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={2} className="rb:flex! rb:items-center rb:justify-end">
|
||||||
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item shouldUpdate noStyle>
|
||||||
|
{(form) => {
|
||||||
|
const operation = form.getFieldValue([parentName, name, 'operation']);
|
||||||
|
if (operation === 'clear') return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'value']}
|
||||||
|
noStyle
|
||||||
|
rules={[{ required: true, message: 'Missing last name' }]}
|
||||||
|
>
|
||||||
|
{operation === 'assign' ? (
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder={t('common.pleaseEnter')}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<VariableSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={options}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AssignmentList
|
||||||
@@ -9,8 +9,8 @@ import VariableSelect from '../VariableSelect'
|
|||||||
import Editor from '../../Editor'
|
import Editor from '../../Editor'
|
||||||
|
|
||||||
interface CaseListProps {
|
interface CaseListProps {
|
||||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>;
|
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; comparison_operator: string; right: string; }[] }>;
|
||||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>) => void;
|
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; comparison_operator: string; right: string; }[] }>) => void;
|
||||||
options: Suggestion[];
|
options: Suggestion[];
|
||||||
name: string;
|
name: string;
|
||||||
selectedNode?: any;
|
selectedNode?: any;
|
||||||
@@ -221,7 +221,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
onClick={() => addCondition()}
|
onClick={() => addCondition()}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
+ 添加条件
|
+ {t('workflow.config.addCase')}
|
||||||
</Button>
|
</Button>
|
||||||
{caseFields.length > 1 && <DeleteOutlined
|
{caseFields.length > 1 && <DeleteOutlined
|
||||||
className="rb:text-[12px]"
|
className="rb:text-[12px]"
|
||||||
@@ -229,7 +229,8 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
/>}
|
/>}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
{conditionFields?.length > 1 && <>
|
{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: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%)]]">
|
<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 >
|
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||||
@@ -238,50 +239,56 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{conditionFields.map((conditionField, conditionIndex) => (
|
{conditionFields.map((conditionField, conditionIndex) => {
|
||||||
<div key={conditionField.key} className={clsx({
|
const currentOperator = value?.[caseIndex]?.expressions?.[conditionIndex]?.comparison_operator;
|
||||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||||
})}>
|
|
||||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
return (
|
||||||
<Row gutter={12} className="rb:mb-1">
|
<div key={conditionField.key} className={clsx({
|
||||||
<Col span={14}>
|
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
})}>
|
||||||
<VariableSelect
|
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||||
placeholder={t('common.pleaseSelect')}
|
<Row gutter={12} className="rb:mb-1">
|
||||||
options={options}
|
<Col span={14}>
|
||||||
size="small"
|
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||||
allowClear={false}
|
<VariableSelect
|
||||||
popupMatchSelectWidth={false}
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={options}
|
||||||
|
size="small"
|
||||||
|
allowClear={false}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item name={[conditionField.name, 'comparison_operator']} noStyle>
|
||||||
|
<Select
|
||||||
|
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>
|
||||||
|
|
||||||
|
{!hideRightField && (
|
||||||
|
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||||
|
<Editor options={options} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
)}
|
||||||
<Col span={8}>
|
</div>
|
||||||
<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>
|
)
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { type FC } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Input, Button, Form, Space } from 'antd';
|
||||||
|
import { PlusOutlined, CopyOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons';
|
||||||
|
import { Graph, Node } from '@antv/x6';
|
||||||
|
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||||
|
|
||||||
|
interface CategoryListProps {
|
||||||
|
parentName: string;
|
||||||
|
selectedNode?: Node | null;
|
||||||
|
graphRef?: React.MutableRefObject<Graph | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRef }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const formValues = Form.useWatch([parentName], form);
|
||||||
|
|
||||||
|
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 < baseHeight ? baseHeight : newHeight })
|
||||||
|
|
||||||
|
// 添加 分类 端口
|
||||||
|
for (let i = 0; i < caseCount; i++) {
|
||||||
|
selectedNode.addPort({
|
||||||
|
id: `CASE${i + 1}`,
|
||||||
|
group: 'right',
|
||||||
|
args: i === 0 ? { dy: 24 } : undefined,
|
||||||
|
attrs: { text: { text: `分类${i + 1}`, fontSize: 12, fill: '#5B6167' } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 恢复连线
|
||||||
|
setTimeout(() => {
|
||||||
|
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
|
||||||
|
graphRef.current?.removeCell(edge);
|
||||||
|
|
||||||
|
// 如果是进入连线(左侧端口),直接恢复
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理右侧端口连线
|
||||||
|
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
|
||||||
|
|
||||||
|
// 如果是被删除的端口,不重新创建连线
|
||||||
|
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newPortId = sourcePortId;
|
||||||
|
|
||||||
|
// 如果删除了某个端口,需要重新映射后续端口的ID
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCategory = (addFunc: Function) => {
|
||||||
|
addFunc({});
|
||||||
|
setTimeout(() => {
|
||||||
|
updateNodePorts((formValues?.length || 0) + 1);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveCategory = (removeFunc: Function, fieldName: number, categoryIndex: number) => {
|
||||||
|
removeFunc(fieldName);
|
||||||
|
setTimeout(() => {
|
||||||
|
updateNodePorts((formValues?.length || 1) - 1, categoryIndex);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('formValues', formValues)
|
||||||
|
return (
|
||||||
|
<Form.List name={parentName}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<Space direction="vertical" size={12} className="rb:w-full">
|
||||||
|
{fields.map(({ key, name, ...restField }, index) => {
|
||||||
|
const currentItem = formValues?.[key] || {};
|
||||||
|
const contentLength = (currentItem.class_name || '').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-[#F8F9FB]">
|
||||||
|
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||||
|
<div>{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||||
|
<div className="rb:flex rb:items-center rb:gap-1">
|
||||||
|
<span className="rb:text-xs rb:text-gray-500">{contentLength}</span>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleRemoveCategory(remove, name, index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'class_name']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder={t('common.pleaseEnter')}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)})}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => handleAddCategory(add)}
|
||||||
|
className="rb:w-full"
|
||||||
|
>
|
||||||
|
+ {t('workflow.config.question-classifier.addClassName')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryList;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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 Case {
|
||||||
|
logical_operator: 'and' | 'or';
|
||||||
|
expressions: Array<{ left: string; comparison_operator: string; right: string; }>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CaseListProps {
|
||||||
|
value?: Case;
|
||||||
|
onChange?: (value: Case) => void;
|
||||||
|
options: Suggestion[];
|
||||||
|
parentName: string;
|
||||||
|
selectedNode?: any;
|
||||||
|
graphRef?: any;
|
||||||
|
addBtnText?: string;
|
||||||
|
}
|
||||||
|
const operatorList = [
|
||||||
|
"empty",
|
||||||
|
"not_empty",
|
||||||
|
"contains",
|
||||||
|
"not_contains",
|
||||||
|
"startwith",
|
||||||
|
"endwith",
|
||||||
|
"eq",
|
||||||
|
"ne",
|
||||||
|
"lt",
|
||||||
|
"le",
|
||||||
|
"gt",
|
||||||
|
"ge"
|
||||||
|
]
|
||||||
|
|
||||||
|
const ConditionList: FC<CaseListProps> = ({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
parentName,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleChangeLogicalOperator = () => {
|
||||||
|
if (!value) return;
|
||||||
|
onChange && onChange({
|
||||||
|
logical_operator: value.logical_operator === 'and' ? 'or' : 'and',
|
||||||
|
expressions: value.expressions || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.List name={[parentName, 'expressions']}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<div>
|
||||||
|
<div className="rb:relative">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const currentOperator = value?.expressions?.[index]?.comparison_operator;
|
||||||
|
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.key} className="rb:mb-3">
|
||||||
|
{index > 0 && (<>
|
||||||
|
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-3.75 rb:bottom-3.75 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={[parentName, 'logical_operator']} noStyle >
|
||||||
|
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{value?.logical_operator}</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white rb:ml-6">
|
||||||
|
<Row gutter={8} align="middle">
|
||||||
|
<Col span={14}>
|
||||||
|
<Form.Item name={[field.name, 'left']} noStyle>
|
||||||
|
<VariableSelect
|
||||||
|
options={options}
|
||||||
|
size="small"
|
||||||
|
allowClear={false}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
||||||
|
<Select
|
||||||
|
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-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||||
|
onClick={() => remove(field.name)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{!hideRightField && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Form.Item name={[field.name, 'right']} noStyle>
|
||||||
|
<Editor options={options} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => add({ left: '', comparison_operator: '', right: '' })}
|
||||||
|
className="rb:w-full rb:ml-6 rb:mt-2"
|
||||||
|
icon={<span>+</span>}
|
||||||
|
>
|
||||||
|
添加条件
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConditionList
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Form, Button, Select, Row, Col, Input } from 'antd'
|
||||||
|
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import VariableSelect from '../VariableSelect'
|
||||||
|
|
||||||
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
|
|
||||||
|
interface CycleVar {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
input_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CycleVarsListProps {
|
||||||
|
value?: CycleVar[];
|
||||||
|
onChange?: (value: CycleVar[]) => void;
|
||||||
|
options: Suggestion[];
|
||||||
|
parentName: string;
|
||||||
|
selectedNode?: any;
|
||||||
|
graphRef?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = [
|
||||||
|
'string',
|
||||||
|
'number',
|
||||||
|
'boolean',
|
||||||
|
'array[string]',
|
||||||
|
'array[number]',
|
||||||
|
'array[boolean]',
|
||||||
|
'array[object]'
|
||||||
|
]
|
||||||
|
|
||||||
|
const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||||
|
value = [],
|
||||||
|
options,
|
||||||
|
parentName,
|
||||||
|
onChange,
|
||||||
|
selectedNode,
|
||||||
|
graphRef
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
// 获取循环节点的子节点变量
|
||||||
|
const getChildNodeVariables = () => {
|
||||||
|
if (!selectedNode || !graphRef?.current || selectedNode.getData()?.type !== 'loop') {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loopNodeId = selectedNode.getData()?.id;
|
||||||
|
const childNodes = graphRef.current.getNodes().filter((node: any) =>
|
||||||
|
node.getData()?.cycle === loopNodeId
|
||||||
|
);
|
||||||
|
|
||||||
|
const childVariables: Suggestion[] = [];
|
||||||
|
childNodes.forEach((childNode: any) => {
|
||||||
|
const childData = childNode.getData();
|
||||||
|
if (childData?.config) {
|
||||||
|
Object.keys(childData.config).forEach(key => {
|
||||||
|
if (childData.config[key]?.defaultValue) {
|
||||||
|
childVariables.push({
|
||||||
|
key: `${childData.id}.${key}`,
|
||||||
|
label: `${childData.name || childData.type}.${key}`,
|
||||||
|
type: 'output',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `{{${childData.id}.${key}}}`,
|
||||||
|
nodeData: childData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...options, ...childVariables];
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableOptions = getChildNodeVariables();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<Form.List name={parentName}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||||
|
<span className="rb:text-sm rb:font-medium">循环变量</span>
|
||||||
|
<PlusOutlined className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-blue-500" onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })} />
|
||||||
|
</div>
|
||||||
|
{fields.map(({ key, name, ...field }, index) => {
|
||||||
|
const currentInputType = value?.[index]?.input_type;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} className="rb:mb-3 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white">
|
||||||
|
<Row gutter={8} align="middle" className="rb:mb-2">
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item name={[name, 'name']} noStyle>
|
||||||
|
<Input size="small" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item name={[name, 'type']} noStyle>
|
||||||
|
<Select
|
||||||
|
options={types.map(key => ({
|
||||||
|
value: key,
|
||||||
|
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||||
|
}))}
|
||||||
|
size="small"
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item name={[name, 'input_type']} noStyle>
|
||||||
|
<Select
|
||||||
|
placeholder="Constant"
|
||||||
|
options={[
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' }
|
||||||
|
]}
|
||||||
|
size="small"
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
onChange={() => {
|
||||||
|
// 重置 value 字段
|
||||||
|
form.setFieldValue([parentName, index, 'value'], undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={2}>
|
||||||
|
<DeleteOutlined
|
||||||
|
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||||
|
onClick={() => remove(name)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item name={[name, 'value']} noStyle>
|
||||||
|
{currentInputType === 'variable' ? (
|
||||||
|
<VariableSelect
|
||||||
|
placeholder="选择变量"
|
||||||
|
options={availableOptions}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="输入值"
|
||||||
|
rows={3}
|
||||||
|
className="rb:w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CycleVarsList
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import { type FC, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps } from 'antd'
|
||||||
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
|
import { getToolMethods, getToolDetail, getTools } from '@/api/tools'
|
||||||
|
import type { ToolType, ToolItem } from '@/views/ToolManagement/types'
|
||||||
|
import Editor from "../../Editor";
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
value?: string | number | null;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
children?: Option[];
|
||||||
|
isLeaf?: boolean;
|
||||||
|
method_id?: string;
|
||||||
|
parameters?: Parameter[];
|
||||||
|
}
|
||||||
|
interface Parameter {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
required: boolean;
|
||||||
|
default: any;
|
||||||
|
enum: null | string[];
|
||||||
|
minimum: number;
|
||||||
|
maximum: number;
|
||||||
|
pattern: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||||
|
options,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const values = Form.useWatch([], form) || {}
|
||||||
|
const [optionList, setOptionList] = useState<Option[]>([
|
||||||
|
{ value: 'mcp', label: t('tool.mcp'), isLeaf: false },
|
||||||
|
{ value: 'builtin', label: t('tool.inner'), isLeaf: false },
|
||||||
|
{ value: 'custom', label: t('tool.custom'), isLeaf: false },
|
||||||
|
])
|
||||||
|
const [parameters, setParameters] = useState<Parameter[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (values.tool_id) {
|
||||||
|
getToolDetail(values.tool_id)
|
||||||
|
.then(res => {
|
||||||
|
const detail = res as { tool_type: ToolType; }
|
||||||
|
|
||||||
|
getTools({ tool_type: detail.tool_type })
|
||||||
|
.then(toolsRes => {
|
||||||
|
const tools = toolsRes as ToolItem[]
|
||||||
|
|
||||||
|
getToolMethods(values.tool_id)
|
||||||
|
.then(methodsRes => {
|
||||||
|
const response = methodsRes as Array<{ method_id: string; name: string; parameters: Parameter[] }>
|
||||||
|
|
||||||
|
setOptionList(prevList => {
|
||||||
|
return prevList.map(item => {
|
||||||
|
if (item.value === detail.tool_type) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children: tools.map((vo: ToolItem) => ({
|
||||||
|
value: vo.id,
|
||||||
|
label: vo.name,
|
||||||
|
isLeaf: false,
|
||||||
|
children: vo.id === values.tool_id ? response.map(method => ({
|
||||||
|
value: method.name,
|
||||||
|
label: method.name,
|
||||||
|
isLeaf: true,
|
||||||
|
method_id: method.method_id,
|
||||||
|
parameters: method.parameters
|
||||||
|
})) : undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.length > 1) {
|
||||||
|
const filterTarget = response.find(vo => vo.name === values.tool_parameters?.operation)
|
||||||
|
if (filterTarget) {
|
||||||
|
setParameters([...filterTarget.parameters])
|
||||||
|
} else {
|
||||||
|
setParameters([])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setParameters([...response[0].parameters])
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setFieldValue('tools', [detail.tool_type, values.tool_id, values.tool_parameters?.operation ?? response[0].name])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [values.tool_id, values.tool_parameters?.operation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (values.tools && values.tools.length === 3) {
|
||||||
|
const [toolType, toolId, operation] = values.tools
|
||||||
|
|
||||||
|
// 从 optionList 中查找对应的参数
|
||||||
|
const typeOption = optionList.find(opt => opt.value === toolType)
|
||||||
|
if (typeOption?.children) {
|
||||||
|
const toolOption = typeOption.children.find(opt => opt.value === toolId)
|
||||||
|
if (toolOption?.children) {
|
||||||
|
const methodOption = toolOption.children.find(opt => opt.value === operation)
|
||||||
|
if (methodOption?.parameters) {
|
||||||
|
setParameters([...methodOption.parameters])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [values.tools])
|
||||||
|
|
||||||
|
const loadData = (selectedOptions: Option[]) => {
|
||||||
|
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||||
|
if (selectedOptions.length === 1) {
|
||||||
|
getTools({ tool_type: targetOption.value as ToolType })
|
||||||
|
.then(res => {
|
||||||
|
const response = res as ToolItem[]
|
||||||
|
targetOption.children = response.map((vo: any) => {
|
||||||
|
return {
|
||||||
|
value: vo.id,
|
||||||
|
label: vo.name,
|
||||||
|
isLeaf: response.length === 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setOptionList([...optionList])
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
getToolMethods(targetOption.value as string)
|
||||||
|
.then(res => {
|
||||||
|
const response = res as Array<{ method_id: string; name: string }>
|
||||||
|
targetOption.children = response.map((vo: any) => {
|
||||||
|
return {
|
||||||
|
value: vo.name,
|
||||||
|
label: vo.name,
|
||||||
|
isLeaf: true,
|
||||||
|
method_id: vo.method_id,
|
||||||
|
parameters: vo.parameters
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setOptionList([...optionList])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange: CascaderProps<Option>['onChange'] = (value, selectedOptions) => {
|
||||||
|
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||||
|
const curParameters = [...(targetOption.parameters ?? [])]
|
||||||
|
setParameters([...curParameters])
|
||||||
|
const inititalValue: any = { tool_id: selectedOptions[1].value, tool_parameters: {} }
|
||||||
|
|
||||||
|
if (value[0] === 'mcp' || (value[0] === 'builtin' && selectedOptions[1]?.children && selectedOptions[1].children.length > 1)) {
|
||||||
|
inititalValue.tool_parameters.operation = value?.[2]
|
||||||
|
} else if (value[0] === 'custom') {
|
||||||
|
inititalValue.tool_parameters.operation = selectedOptions?.[2].method_id
|
||||||
|
}
|
||||||
|
curParameters.forEach(vo => {
|
||||||
|
inititalValue.tool_parameters[vo.name] = vo.default
|
||||||
|
})
|
||||||
|
|
||||||
|
form.setFieldsValue(inititalValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name="tools"
|
||||||
|
label={t('workflow.config.tool.tool_id')}
|
||||||
|
>
|
||||||
|
<Cascader
|
||||||
|
options={optionList}
|
||||||
|
loadData={loadData}
|
||||||
|
onChange={handleChange}
|
||||||
|
changeOnSelect={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="tool_id" hidden />
|
||||||
|
<Form.Item name={['tool_parameters', 'operation']} hidden />
|
||||||
|
{parameters.map((parameter) => {
|
||||||
|
return (
|
||||||
|
<div key={parameter.name}>
|
||||||
|
<Form.Item
|
||||||
|
name={['tool_parameters', parameter.name]}
|
||||||
|
label={parameter.name}
|
||||||
|
extra={parameter.type === 'boolean' ? undefined : parameter.description}
|
||||||
|
rules={[
|
||||||
|
{ required: parameter.required, message: t('workflow.config.tool.required') }
|
||||||
|
]}
|
||||||
|
layout={parameter.type === 'boolean' ? 'horizontal' : 'vertical'}
|
||||||
|
className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''}
|
||||||
|
>
|
||||||
|
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
|
||||||
|
? <Select options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||||
|
: parameter.type === 'boolean'
|
||||||
|
? <Switch />
|
||||||
|
: parameter.type === 'integer' || parameter.type === 'number'
|
||||||
|
? <InputNumber min={parameter.minimum} max={parameter.maximum} step={parameter.type === 'integer' ? 1 : 0.01} placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||||
|
: <Editor
|
||||||
|
height={32}
|
||||||
|
variant="outlined"
|
||||||
|
options={options}
|
||||||
|
placeholder={t('common.pleaseEnter')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
{parameter.type === 'boolean' && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 rb:mb-6">{parameter.description}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ToolConfig;
|
||||||
@@ -26,7 +26,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
}
|
}
|
||||||
const labelRender: LabelRender = (props) => {
|
const labelRender: LabelRender = (props) => {
|
||||||
const { value } = props
|
const { value } = props
|
||||||
const filterOption = options.find(vo => vo.value === value)
|
const filterOption = options.find(vo => `{{${vo.value}}}` === value)
|
||||||
|
|
||||||
if (filterOption) {
|
if (filterOption) {
|
||||||
return (
|
return (
|
||||||
@@ -37,13 +37,17 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
})}
|
})}
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
>
|
>
|
||||||
<img
|
{filterOption.nodeData?.icon && filterOption.nodeData?.name && (
|
||||||
src={filterOption.nodeData?.icon}
|
<>
|
||||||
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
<img
|
||||||
alt=""
|
src={filterOption.nodeData.icon}
|
||||||
/>
|
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
||||||
{filterOption.nodeData?.name}
|
alt=""
|
||||||
<span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>
|
/>
|
||||||
|
{filterOption.nodeData.name}
|
||||||
|
<span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<span className="rb:text-[#155EEF]">{filterOption.label}</span>
|
<span className="rb:text-[#155EEF]">{filterOption.label}</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -62,8 +66,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
|
|
||||||
const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({
|
const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({
|
||||||
label: suggestions[0].nodeData.name,
|
label: suggestions[0].nodeData.name,
|
||||||
options: suggestions.map(s => ({ label: s.label, value: s.value }))
|
options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` }))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log('groupedOptions', groupedOptions)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import GroupVariableList from './GroupVariableList'
|
|||||||
import CaseList from './CaseList'
|
import CaseList from './CaseList'
|
||||||
import HttpRequest from './HttpRequest';
|
import HttpRequest from './HttpRequest';
|
||||||
import MappingList from './MappingList'
|
import MappingList from './MappingList'
|
||||||
|
import CategoryList from './CategoryList'
|
||||||
|
import ConditionList from './ConditionList'
|
||||||
|
import CycleVarsList from './CycleVarsList'
|
||||||
|
import AssignmentList from './AssignmentList'
|
||||||
|
import ToolConfig from './ToolConfig'
|
||||||
|
|
||||||
interface PropertiesProps {
|
interface PropertiesProps {
|
||||||
selectedNode?: Node | null;
|
selectedNode?: Node | null;
|
||||||
@@ -26,10 +31,12 @@ interface PropertiesProps {
|
|||||||
deleteEvent: () => void;
|
deleteEvent: () => void;
|
||||||
copyEvent: () => void;
|
copyEvent: () => void;
|
||||||
parseEvent: () => void;
|
parseEvent: () => void;
|
||||||
|
config?: any;
|
||||||
}
|
}
|
||||||
const Properties: FC<PropertiesProps> = ({
|
const Properties: FC<PropertiesProps> = ({
|
||||||
selectedNode,
|
selectedNode,
|
||||||
graphRef,
|
graphRef,
|
||||||
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { modal } = App.useApp()
|
const { modal } = App.useApp()
|
||||||
@@ -178,9 +185,19 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
return allPrevious;
|
return allPrevious;
|
||||||
};
|
};
|
||||||
|
|
||||||
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
// Find child nodes (nodes whose cycle field equals current node's ID)
|
||||||
|
const getChildNodes = (nodeId: string): string[] => {
|
||||||
|
return nodes
|
||||||
|
.filter(node => node.getData()?.cycle === nodeId)
|
||||||
|
.map(node => node.id);
|
||||||
|
};
|
||||||
|
|
||||||
allPreviousNodeIds.forEach(nodeId => {
|
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
||||||
|
const childNodeIds = getChildNodes(selectedNode.id);
|
||||||
|
console.log('childNodeIds', childNodeIds)
|
||||||
|
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
|
||||||
|
|
||||||
|
allRelevantNodeIds.forEach(nodeId => {
|
||||||
const node = nodes.find(n => n.id === nodeId);
|
const node = nodes.find(n => n.id === nodeId);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
@@ -245,7 +262,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
key: knowledgeKey,
|
key: knowledgeKey,
|
||||||
label: 'message',
|
label: 'message',
|
||||||
type: 'variable',
|
type: 'variable',
|
||||||
dataType: 'String',
|
dataType: 'array[object]',
|
||||||
value: `${nodeId}.message`,
|
value: `${nodeId}.message`,
|
||||||
nodeData: nodeData,
|
nodeData: nodeData,
|
||||||
});
|
});
|
||||||
@@ -254,10 +271,30 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add conversation variables from global config
|
||||||
|
const conversationVariables = config?.variables || [];
|
||||||
|
|
||||||
|
conversationVariables.forEach((variable: any) => {
|
||||||
|
const key = `CONVERSATION_${variable.name}`;
|
||||||
|
if (!addedKeys.has(key)) {
|
||||||
|
addedKeys.add(key);
|
||||||
|
variableList.push({
|
||||||
|
key,
|
||||||
|
label: variable.name,
|
||||||
|
type: 'variable',
|
||||||
|
dataType: variable.type,
|
||||||
|
value: `conversation.${variable.name}`,
|
||||||
|
nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' },
|
||||||
|
group: 'CONVERSATION'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return variableList;
|
return variableList;
|
||||||
}, [selectedNode, graphRef]);
|
}, [selectedNode, graphRef]);
|
||||||
|
|
||||||
console.log('values', values)
|
console.log('values', values)
|
||||||
|
console.log('variableList', variableList, selectedNode?.data)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
|
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
|
||||||
@@ -278,12 +315,18 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{selectedNode?.data?.type === 'http-request'
|
{selectedNode?.data?.type === 'http-request'
|
||||||
? <HttpRequest
|
? <HttpRequest
|
||||||
options={variableList}
|
options={variableList}
|
||||||
/>
|
/>
|
||||||
|
: selectedNode?.data?.type === 'tool'
|
||||||
|
? <ToolConfig options={variableList} />
|
||||||
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||||
const config = configs[key] || {}
|
const config = configs[key] || {}
|
||||||
|
|
||||||
|
if (config.dependsOn && (values as any)?.[config.dependsOn as string] !== config.dependsOnValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
|
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
@@ -409,14 +452,13 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
<GroupVariableList
|
<GroupVariableList
|
||||||
name={key}
|
name={key}
|
||||||
options={variableList}
|
options={variableList}
|
||||||
isCanAdd={!!values?.group}
|
isCanAdd={!!(values as any)?.group}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (config.type === 'caseList') {
|
if (config.type === 'caseList') {
|
||||||
console.log('key', key)
|
|
||||||
return (
|
return (
|
||||||
<Form.Item key={key} name={key}>
|
<Form.Item key={key} name={key}>
|
||||||
<CaseList
|
<CaseList
|
||||||
@@ -439,6 +481,94 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (config.type === 'cycleVarsList') {
|
||||||
|
return (
|
||||||
|
<Form.Item key={key} name={key}>
|
||||||
|
<CycleVarsList
|
||||||
|
parentName={key}
|
||||||
|
options={variableList}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (config.type === 'assignmentList') {
|
||||||
|
return (
|
||||||
|
<Form.Item key={key} name={key}>
|
||||||
|
<AssignmentList
|
||||||
|
parentName={key}
|
||||||
|
options={(() => {
|
||||||
|
if (config.filterLoopIterationVars) {
|
||||||
|
// Add loop cycle variables and iteration item/index variables
|
||||||
|
const loopIterationVars: Suggestion[] = [];
|
||||||
|
const graph = graphRef.current;
|
||||||
|
if (graph && selectedNode) {
|
||||||
|
const nodes = graph.getNodes();
|
||||||
|
|
||||||
|
// Find parent loop/iteration nodes
|
||||||
|
const findParentLoopIteration = (nodeId: string): string[] => {
|
||||||
|
const node = nodes.find(n => n.id === nodeId);
|
||||||
|
if (!node) return [];
|
||||||
|
|
||||||
|
const nodeData = node.getData();
|
||||||
|
const cycle = nodeData?.cycle;
|
||||||
|
|
||||||
|
if (cycle) {
|
||||||
|
const parentNode = nodes.find(n => n.getData().id === cycle);
|
||||||
|
if (parentNode) {
|
||||||
|
const parentData = parentNode.getData();
|
||||||
|
if (parentData?.type === 'loop') {
|
||||||
|
console.log('parentData', parentData)
|
||||||
|
// Add cycle variables from loop node
|
||||||
|
const cycleVars = parentData.cycle_vars || [];
|
||||||
|
cycleVars.forEach((cycleVar: any) => {
|
||||||
|
loopIterationVars.push({
|
||||||
|
key: `${cycle}_cycle_${cycleVar.name}`,
|
||||||
|
label: cycleVar.name,
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'String',
|
||||||
|
value: `${cycle}.${cycleVar.name}`,
|
||||||
|
nodeData: parentData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (parentData?.type === 'iteration') {
|
||||||
|
// Add item and index variables from iteration node
|
||||||
|
loopIterationVars.push(
|
||||||
|
{
|
||||||
|
key: `${cycle}_item`,
|
||||||
|
label: 'item',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'Object',
|
||||||
|
value: `${cycle}.item`,
|
||||||
|
nodeData: parentData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: `${cycle}_index`,
|
||||||
|
label: 'index',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'Number',
|
||||||
|
value: `${cycle}.index`,
|
||||||
|
nodeData: parentData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [cycle, ...findParentLoopIteration(cycle)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
findParentLoopIteration(selectedNode.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...variableList, ...loopIterationVars];
|
||||||
|
}
|
||||||
|
return variableList;
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -453,7 +583,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||||
: config.type === 'select'
|
: config.type === 'select'
|
||||||
? <Select
|
? <Select
|
||||||
options={config.options}
|
options={config.needTranslation ? config.options?.map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
/>
|
/>
|
||||||
: config.type === 'inputNumber'
|
: config.type === 'inputNumber'
|
||||||
@@ -472,10 +602,50 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
: config.type === 'variableList'
|
: config.type === 'variableList'
|
||||||
? <VariableSelect
|
? <VariableSelect
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
options={variableList}
|
options={(() => {
|
||||||
|
// Apply filtering if specified in config
|
||||||
|
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||||
|
return variableList.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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 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 variableList.filter(variable =>
|
||||||
|
childNodes.some(node => node.id === variable.nodeData?.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return variableList;
|
||||||
|
})()
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
: config.type === 'switch'
|
: config.type === 'switch'
|
||||||
? <Switch />
|
? <Switch />
|
||||||
|
: config.type === 'categoryList'
|
||||||
|
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||||
|
: config.type === 'conditionList'
|
||||||
|
? <ConditionList
|
||||||
|
parentName={key}
|
||||||
|
options={variableList}
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
graphRef={graphRef}
|
||||||
|
addBtnText={t('workflow.config.addCase')}
|
||||||
|
/>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import LoopNode from './components/Nodes/LoopNode';
|
import LoopNode from './components/Nodes/LoopNode';
|
||||||
import IterationNode from './components/Nodes/IterationNode';
|
|
||||||
import NormalNode from './components/Nodes/NormalNode';
|
import NormalNode from './components/Nodes/NormalNode';
|
||||||
import ConditionNode from './components/Nodes/ConditionNode';
|
import ConditionNode from './components/Nodes/ConditionNode';
|
||||||
import GroupStartNode from './components/Nodes/GroupStartNode';
|
import GroupStartNode from './components/Nodes/GroupStartNode';
|
||||||
@@ -37,6 +36,10 @@ import sensitiveDetectionIcon from '@/assets/images/workflow/sensitive_detection
|
|||||||
import outputAuditIcon from '@/assets/images/workflow/output_audit.png';
|
import outputAuditIcon from '@/assets/images/workflow/output_audit.png';
|
||||||
import selfOptimizationIcon from '@/assets/images/workflow/self_optimization.png';
|
import selfOptimizationIcon from '@/assets/images/workflow/self_optimization.png';
|
||||||
import processEvolutionIcon from '@/assets/images/workflow/process_evolution.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 { memoryConfigListUrl } from '@/api/memory'
|
||||||
|
|
||||||
import { getModelListUrl } from '@/api/models'
|
import { getModelListUrl } from '@/api/models'
|
||||||
import type { NodeLibrary } from './types'
|
import type { NodeLibrary } from './types'
|
||||||
@@ -168,15 +171,49 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// category: "cognitiveUpgrading",
|
category: "cognitiveUpgrading",
|
||||||
// nodes: [
|
nodes: [
|
||||||
// { type: "task_planning", icon: taskPlanningIcon },
|
{
|
||||||
// { type: "reasoning_control", icon: reasoningControlIcon },
|
type: "memory-read", icon: memoryEnhancementIcon,
|
||||||
// { type: "self_reflection", icon: selfReflectionIcon },
|
config: {
|
||||||
// { type: "memory_enhancement", icon: memoryEnhancementIcon }
|
message: {
|
||||||
// ]
|
type: 'messageEditor',
|
||||||
// },
|
isArray: false
|
||||||
|
},
|
||||||
|
config_id: {
|
||||||
|
type: 'customSelect',
|
||||||
|
url: memoryConfigListUrl,
|
||||||
|
valueKey: 'config_id',
|
||||||
|
labelKey: 'config_name'
|
||||||
|
},
|
||||||
|
search_switch: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ value: '0', label: 'memoryConversation.deepThinking' },
|
||||||
|
{ value: '1', label: 'memoryConversation.normalReply' },
|
||||||
|
{ value: '2', label: 'memoryConversation.quickReply' },
|
||||||
|
],
|
||||||
|
needTranslation: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: "memory-write", icon: memoryEnhancementIcon,
|
||||||
|
config: {
|
||||||
|
message: {
|
||||||
|
type: 'messageEditor',
|
||||||
|
isArray: false
|
||||||
|
},
|
||||||
|
config_id: {
|
||||||
|
type: 'customSelect',
|
||||||
|
url: memoryConfigListUrl,
|
||||||
|
valueKey: 'config_id',
|
||||||
|
labelKey: 'config_name'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// category: "agentCollaborationNode",
|
// category: "agentCollaborationNode",
|
||||||
// nodes: [
|
// nodes: [
|
||||||
@@ -201,8 +238,76 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// { type: "iteration", icon: iterationIcon },
|
{ type: "question-classifier", icon: questionClassifierIcon,
|
||||||
// { type: "loop", icon: loopIcon },
|
config: {
|
||||||
|
model_id: {
|
||||||
|
type: 'customSelect',
|
||||||
|
url: getModelListUrl,
|
||||||
|
params: { type: 'llm,chat' }, // llm/chat
|
||||||
|
valueKey: 'id',
|
||||||
|
labelKey: 'name',
|
||||||
|
},
|
||||||
|
input_variable: {
|
||||||
|
type: 'variableList',
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
type: 'categoryList',
|
||||||
|
defaultValue: [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
user_supplement_prompt: {
|
||||||
|
type: 'messageEditor',
|
||||||
|
isArray: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: "iteration", icon: iterationIcon,
|
||||||
|
config: {
|
||||||
|
input: {
|
||||||
|
type: 'variableList',
|
||||||
|
filterNodeTypes: ['knowledge-retrieval'],
|
||||||
|
filterVariableNames: ['message']
|
||||||
|
},
|
||||||
|
parallel: {
|
||||||
|
type: 'switch',
|
||||||
|
},
|
||||||
|
parallel_count: {
|
||||||
|
type: 'slider',
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
defaultValue: 10,
|
||||||
|
dependsOn: 'parallel',
|
||||||
|
dependsOnValue: true
|
||||||
|
},
|
||||||
|
flatten: { // 扁平化输出
|
||||||
|
type: 'switch',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
type: 'variableList',
|
||||||
|
filterChildNodes: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: "loop", icon: loopIcon,
|
||||||
|
config: {
|
||||||
|
cycle_vars: {
|
||||||
|
type: 'cycleVarsList',
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
type: 'conditionList',
|
||||||
|
showLabel: true,
|
||||||
|
defaultValue: {
|
||||||
|
logical_operator: 'and',
|
||||||
|
expressions: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: "cycle-start", icon: loopIcon },
|
||||||
|
{ type: "break", icon: breakIcon },
|
||||||
// { type: "parallel", icon: parallelIcon },
|
// { type: "parallel", icon: parallelIcon },
|
||||||
{ type: "var-aggregator", icon: aggregatorIcon,
|
{ type: "var-aggregator", icon: aggregatorIcon,
|
||||||
config: {
|
config: {
|
||||||
@@ -215,7 +320,16 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
defaultValue: [{ key: 'Group1', value: []}]
|
defaultValue: [{ key: 'Group1', value: []}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
type: "assigner", icon: assignerIcon,
|
||||||
|
config: {
|
||||||
|
assignments: {
|
||||||
|
type: 'assignmentList',
|
||||||
|
filterLoopIterationVars: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -278,7 +392,16 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// { type: "tools", icon: toolsIcon },
|
{ type: "tool", icon: toolsIcon,
|
||||||
|
config: {
|
||||||
|
tool_id: {
|
||||||
|
type: 'cascader'
|
||||||
|
},
|
||||||
|
tool_parameters: {
|
||||||
|
type: 'define'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
// { type: "code_execution", icon: codeExecutionIcon },
|
// { type: "code_execution", icon: codeExecutionIcon },
|
||||||
{ type: "jinja-render", icon: templateRenderingIcon,
|
{ type: "jinja-render", icon: templateRenderingIcon,
|
||||||
config: {
|
config: {
|
||||||
@@ -315,14 +438,14 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
|||||||
{
|
{
|
||||||
shape: 'loop-node',
|
shape: 'loop-node',
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 80,
|
height: 120,
|
||||||
component: LoopNode,
|
component: LoopNode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shape: 'iteration-node',
|
shape: 'iteration-node',
|
||||||
width: 200,
|
width: 240,
|
||||||
height: 200,
|
height: 120,
|
||||||
component: IterationNode,
|
component: LoopNode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shape: 'normal-node',
|
shape: 'normal-node',
|
||||||
@@ -337,15 +460,15 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
|||||||
component: ConditionNode,
|
component: ConditionNode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shape: 'group-start-node',
|
shape: 'cycle-start',
|
||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
component: GroupStartNode,
|
component: GroupStartNode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shape: 'add-node',
|
shape: 'add-node',
|
||||||
width: 120,
|
width: 88,
|
||||||
height: 40,
|
height: 44,
|
||||||
component: AddNode,
|
component: AddNode,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -382,7 +505,7 @@ const defaultPortItems = [
|
|||||||
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||||
iteration: {
|
iteration: {
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 200,
|
height: 120,
|
||||||
shape: 'iteration-node',
|
shape: 'iteration-node',
|
||||||
ports: {
|
ports: {
|
||||||
groups: defaultPortGroups,
|
groups: defaultPortGroups,
|
||||||
@@ -411,6 +534,19 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'question-classifier': {
|
||||||
|
width: 240,
|
||||||
|
height: 88,
|
||||||
|
shape: 'condition-node',
|
||||||
|
ports: {
|
||||||
|
groups: defaultPortGroups,
|
||||||
|
items: [
|
||||||
|
{ group: 'left' },
|
||||||
|
{ group: 'right', id: 'CASE1', args: { dy: 24 }, attrs: { text: { text: '分类1', fontSize: 12, color: '#5B6167' } } },
|
||||||
|
{ group: 'right', id: 'CASE2', attrs: { text: { text: '分类2', fontSize: 12, color: '#5B6167' } } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
start: {
|
start: {
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 64,
|
height: 64,
|
||||||
@@ -429,6 +565,24 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
|||||||
items: [{ group: 'left' }],
|
items: [{ group: 'left' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'cycle-start': {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
shape: 'cycle-start',
|
||||||
|
ports: {
|
||||||
|
groups: {right: { position: 'right', attrs: portAttrs }},
|
||||||
|
items: [{ group: 'right' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'add-node': {
|
||||||
|
width: 88,
|
||||||
|
height: 44,
|
||||||
|
shape: 'add-node',
|
||||||
|
ports: {
|
||||||
|
groups: {left: { position: 'left', attrs: portAttrs }},
|
||||||
|
items: [{ group: 'left' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
default: {
|
default: {
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 64,
|
height: 64,
|
||||||
@@ -438,18 +592,18 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
|||||||
items: defaultPortItems,
|
items: defaultPortItems,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
groupStart: {
|
cycleStart: {
|
||||||
width: 80,
|
width: 44,
|
||||||
height: 40,
|
height: 44,
|
||||||
shape: 'group-start-node',
|
shape: 'cycle-start',
|
||||||
ports: {
|
ports: {
|
||||||
groups: {right: { position: 'right', attrs: portAttrs }},
|
groups: {right: { position: 'right', attrs: portAttrs }},
|
||||||
items: [{ group: 'right' }],
|
items: [{ group: 'right' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
addStart: {
|
addStart: {
|
||||||
width: 80,
|
width: 88,
|
||||||
height: 40,
|
height: 44,
|
||||||
shape: 'add-node',
|
shape: 'add-node',
|
||||||
ports: {
|
ports: {
|
||||||
groups: {left: { position: 'left', attrs: portAttrs }},
|
groups: {left: { position: 'left', attrs: portAttrs }},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface UseWorkflowGraphProps {
|
|||||||
|
|
||||||
export interface UseWorkflowGraphReturn {
|
export interface UseWorkflowGraphReturn {
|
||||||
config: WorkflowConfig | null;
|
config: WorkflowConfig | null;
|
||||||
|
setConfig: React.Dispatch<React.SetStateAction<WorkflowConfig | null>>;
|
||||||
graphRef: React.MutableRefObject<Graph | undefined>;
|
graphRef: React.MutableRefObject<Graph | undefined>;
|
||||||
selectedNode: Node | null;
|
selectedNode: Node | null;
|
||||||
setSelectedNode: React.Dispatch<React.SetStateAction<Node | null>>;
|
setSelectedNode: React.Dispatch<React.SetStateAction<Node | null>>;
|
||||||
@@ -155,9 +156,99 @@ export const useWorkflowGraph = ({
|
|||||||
nodeConfig.height = newHeight;
|
nodeConfig.height = newHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是question-classifier节点,根据categories动态生成端口
|
||||||
|
if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) {
|
||||||
|
const categoryCount = config.categories.length;
|
||||||
|
const baseHeight = 88;
|
||||||
|
const newHeight = baseHeight + (categoryCount - 1) * 30;
|
||||||
|
|
||||||
|
const portAttrs = {
|
||||||
|
circle: {
|
||||||
|
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const portItems: PortMetadata[] = [
|
||||||
|
{ group: 'left' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 添加分类端口
|
||||||
|
config.categories.forEach((category: any, index: number) => {
|
||||||
|
portItems.push({
|
||||||
|
group: 'right',
|
||||||
|
id: `CASE${index + 1}`,
|
||||||
|
args: index === 0 ? { dy: 24 } : undefined,
|
||||||
|
attrs: { text: { text: category.class_name || `分类${index + 1}`, fontSize: 12, fill: '#5B6167' }}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeConfig.ports = {
|
||||||
|
groups: {
|
||||||
|
right: { position: 'right', attrs: portAttrs },
|
||||||
|
left: { position: 'left', attrs: portAttrs },
|
||||||
|
},
|
||||||
|
items: portItems
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeConfig.height = newHeight;
|
||||||
|
}
|
||||||
|
|
||||||
return nodeConfig
|
return nodeConfig
|
||||||
})
|
})
|
||||||
graphRef.current?.addNodes(nodeList)
|
|
||||||
|
// 分离父节点和子节点
|
||||||
|
const parentNodes = nodeList.filter(node => !node.data.cycle)
|
||||||
|
const childNodes = nodeList.filter(node => node.data.cycle)
|
||||||
|
|
||||||
|
// 先添加父节点
|
||||||
|
graphRef.current?.addNodes(parentNodes)
|
||||||
|
|
||||||
|
// 然后处理子节点,使用addChild添加到对应的父节点
|
||||||
|
childNodes.forEach(childNode => {
|
||||||
|
const cycleId = childNode.data.cycle
|
||||||
|
if (cycleId) {
|
||||||
|
const parentNode = graphRef.current?.getCellById(cycleId)
|
||||||
|
if (parentNode) {
|
||||||
|
const addedChild = graphRef.current?.addNode(childNode)
|
||||||
|
if (addedChild) {
|
||||||
|
parentNode.addChild(addedChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调整父节点大小以适应子节点
|
||||||
|
setTimeout(() => {
|
||||||
|
const parentNodesWithChildren = parentNodes.filter(parentNode => {
|
||||||
|
const parentId = parentNode.data.id
|
||||||
|
return childNodes.some(child => child.data.cycle === parentId)
|
||||||
|
})
|
||||||
|
|
||||||
|
parentNodesWithChildren.forEach(parentNodeConfig => {
|
||||||
|
const parentNode = graphRef.current?.getCellById(parentNodeConfig.data.id)
|
||||||
|
if (parentNode) {
|
||||||
|
const children = parentNode.getChildren()
|
||||||
|
if (children && children.length > 0) {
|
||||||
|
const childBounds = children.map(child => child.getBBox())
|
||||||
|
const minX = Math.min(...childBounds.map(b => b.x))
|
||||||
|
const minY = Math.min(...childBounds.map(b => b.y))
|
||||||
|
const maxX = Math.max(...childBounds.map(b => b.x + b.width))
|
||||||
|
const maxY = Math.max(...childBounds.map(b => b.y + b.height))
|
||||||
|
|
||||||
|
const padding = 24
|
||||||
|
const headerHeight = 50
|
||||||
|
const parentBBox = parentNode.getBBox()
|
||||||
|
|
||||||
|
const newWidth = Math.max(parentBBox.width, maxX - minX + padding * 2)
|
||||||
|
const newHeight = Math.max(parentBBox.height, maxY - minY + padding * 2 + headerHeight)
|
||||||
|
|
||||||
|
console.log('newWidth', newHeight, newWidth)
|
||||||
|
|
||||||
|
parentNode.prop('size', { width: newWidth, height: newHeight })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
if (edges.length) {
|
if (edges.length) {
|
||||||
// 去重处理:相同节点之间的连线仅连一次
|
// 去重处理:相同节点之间的连线仅连一次
|
||||||
@@ -185,6 +276,14 @@ export const useWorkflowGraph = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是question-classifier节点且有label,根据label匹配对应的端口
|
||||||
|
if (sourceCell.getData()?.type === 'question-classifier' && label) {
|
||||||
|
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||||
|
if (matchingPort) {
|
||||||
|
sourcePort = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const edgeConfig = {
|
const edgeConfig = {
|
||||||
source: {
|
source: {
|
||||||
cell: sourceCell.id,
|
cell: sourceCell.id,
|
||||||
@@ -304,6 +403,12 @@ export const useWorkflowGraph = ({
|
|||||||
};
|
};
|
||||||
// 节点选择事件
|
// 节点选择事件
|
||||||
const nodeClick = ({ node }: { node: Node }) => {
|
const nodeClick = ({ node }: { node: Node }) => {
|
||||||
|
// 忽略 add-node 类型的节点点击
|
||||||
|
if (node.getData()?.type === 'add-node' || node.getData().type === 'break' || node.getData().type === 'cycle-start') {
|
||||||
|
setSelectedNode(null)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nodes = graphRef.current?.getNodes();
|
const nodes = graphRef.current?.getNodes();
|
||||||
|
|
||||||
nodes?.forEach(vo => {
|
nodes?.forEach(vo => {
|
||||||
@@ -360,9 +465,9 @@ export const useWorkflowGraph = ({
|
|||||||
};
|
};
|
||||||
// 节点移动事件
|
// 节点移动事件
|
||||||
const nodeMoved = ({ node }: { node: Node }) => {
|
const nodeMoved = ({ node }: { node: Node }) => {
|
||||||
const parentId = node.getData()?.parentId;
|
const cycle = node.getData()?.cycle;
|
||||||
if (parentId) {
|
if (cycle) {
|
||||||
const parentNode = graphRef.current!.getNodes().find(n => n.id === parentId);
|
const parentNode = graphRef.current!.getNodes().find(n => n.id === cycle);
|
||||||
if (parentNode?.getData()?.isGroup) {
|
if (parentNode?.getData()?.isGroup) {
|
||||||
// 获取父节点和子节点的边界框
|
// 获取父节点和子节点的边界框
|
||||||
const parentBBox = parentNode.getBBox();
|
const parentBBox = parentNode.getBBox();
|
||||||
@@ -465,21 +570,23 @@ export const useWorkflowGraph = ({
|
|||||||
nodesToDelete.forEach(nodeToDelete => {
|
nodesToDelete.forEach(nodeToDelete => {
|
||||||
// 检查是否为子节点
|
// 检查是否为子节点
|
||||||
const nodeData = nodeToDelete.getData();
|
const nodeData = nodeToDelete.getData();
|
||||||
if (nodeData.parentId) {
|
if (nodeData.cycle) {
|
||||||
// 找到对应的父节点
|
// 找到对应的父节点
|
||||||
const parentNode = nodes?.find(n => n.id === nodeData.parentId);
|
const parentNode = nodes?.find(n => n.id === nodeData.cycle);
|
||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
// 使用removeChild方法删除子节点
|
// 使用removeChild方法删除子节点
|
||||||
parentNode.removeChild(nodeToDelete);
|
parentNode.removeChild(nodeToDelete);
|
||||||
parentNodesToUpdate.push(parentNode);
|
parentNodesToUpdate.push(parentNode);
|
||||||
}
|
}
|
||||||
|
// 将子节点添加到删除列表
|
||||||
|
cells.push(nodeToDelete);
|
||||||
}
|
}
|
||||||
// 检查是否为 LoopNode、IterationNode 或 SubGraphNode
|
// 检查是否为 LoopNode、IterationNode 或 SubGraphNode
|
||||||
else if (nodeToDelete.shape === 'loop-node' || nodeToDelete.shape === 'iteration-node' || nodeToDelete.shape === 'subgraph-node') {
|
else if (nodeToDelete.shape === 'loop-node' || nodeToDelete.shape === 'iteration-node' || nodeToDelete.shape === 'subgraph-node') {
|
||||||
// 查找所有 parentId 为当前节点 id 的子节点
|
// 查找所有 cycle 为当前节点 id 的子节点
|
||||||
nodes?.forEach(node => {
|
nodes?.forEach(node => {
|
||||||
const data = node.getData();
|
const data = node.getData();
|
||||||
if (data.parentId === nodeToDelete.id) {
|
if (data.cycle === nodeToDelete.id || data.cycle === nodeToDelete.getData()?.id) {
|
||||||
cells.push(node);
|
cells.push(node);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -582,13 +689,14 @@ export const useWorkflowGraph = ({
|
|||||||
if (sourceType === 'end') return false;
|
if (sourceType === 'end') return false;
|
||||||
|
|
||||||
// 获取源节点和目标节点的父节点ID
|
// 获取源节点和目标节点的父节点ID
|
||||||
const sourceParentId = sourceCell?.getData()?.parentId;
|
const sourceParentId = sourceCell?.getData()?.cycle;
|
||||||
const targetParentId = targetCell?.getData()?.parentId;
|
const targetParentId = targetCell?.getData()?.cycle;
|
||||||
|
|
||||||
// 验证父子节点关系:
|
// 验证父子节点关系:
|
||||||
// 1. 如果两个节点都有父节点ID,必须相同才能连线
|
// 1. 如果两个节点都有父节点ID,必须相同才能连线
|
||||||
// 2. 如果一个有父节点ID,另一个没有,不能连线
|
// 2. 如果两个都没有父节点ID,可以正常连线
|
||||||
// 3. 如果两个都没有父节点ID,可以正常连线
|
// 3. 如果一个有父节点,一个没有,不能连线
|
||||||
|
console.log('sourceParentId', sourceParentId, targetParentId)
|
||||||
if (sourceParentId && targetParentId) {
|
if (sourceParentId && targetParentId) {
|
||||||
// 同一父节点下的子节点可以互相连线
|
// 同一父节点下的子节点可以互相连线
|
||||||
return sourceParentId === targetParentId;
|
return sourceParentId === targetParentId;
|
||||||
@@ -635,6 +743,28 @@ export const useWorkflowGraph = ({
|
|||||||
graphRef.current.on('node:click', nodeClick);
|
graphRef.current.on('node:click', nodeClick);
|
||||||
// 监听连线选择事件
|
// 监听连线选择事件
|
||||||
graphRef.current.on('edge:click', edgeClick);
|
graphRef.current.on('edge:click', edgeClick);
|
||||||
|
// 监听连接桩点击事件
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 创建临时的popover触发元素
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 触发自定义事件来显示节点选择popover
|
||||||
|
const customEvent = new CustomEvent('port:click', {
|
||||||
|
detail: { node, port, element: tempDiv, rect }
|
||||||
|
});
|
||||||
|
window.dispatchEvent(customEvent);
|
||||||
|
});
|
||||||
// 监听画布点击事件,取消选择
|
// 监听画布点击事件,取消选择
|
||||||
graphRef.current.on('blank:click', blankClick);
|
graphRef.current.on('blank:click', blankClick);
|
||||||
// 监听缩放事件
|
// 监听缩放事件
|
||||||
@@ -723,36 +853,23 @@ export const useWorkflowGraph = ({
|
|||||||
data: { ...cleanNodeData },
|
data: { ...cleanNodeData },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 检查是否放置在群组内
|
// 普通节点创建,不支持拖拽到循环节点内
|
||||||
const groups = graphRef.current.getNodes().filter(node => {
|
graphRef.current.addNode({
|
||||||
const shape = node.shape;
|
|
||||||
return shape === 'loop-node' || shape === 'iteration-node' || shape === 'subgraph-node';
|
|
||||||
});
|
|
||||||
let parentGroup = null;
|
|
||||||
|
|
||||||
for (const group of groups) {
|
|
||||||
const bbox = group.getBBox();
|
|
||||||
if (point.x >= bbox.x && point.x <= bbox.x + bbox.width &&
|
|
||||||
point.y >= bbox.y && point.y <= bbox.y + bbox.height) {
|
|
||||||
parentGroup = group;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const childNode = graphRef.current.addNode({
|
|
||||||
...(graphNodeLibrary[dragData.type] || graphNodeLibrary.default),
|
...(graphNodeLibrary[dragData.type] || graphNodeLibrary.default),
|
||||||
x: point.x - 60,
|
x: point.x - 60,
|
||||||
y: point.y - 20,
|
y: point.y - 20,
|
||||||
data: { ...cleanNodeData, parentId: parentGroup?.id },
|
data: { ...cleanNodeData },
|
||||||
});
|
});
|
||||||
parentGroup?.addChild(childNode);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 保存workflow配置
|
// 保存workflow配置
|
||||||
const handleSave = (flag = true) => {
|
const handleSave = (flag = true) => {
|
||||||
if (!graphRef.current || !config) return Promise.resolve()
|
if (!graphRef.current || !config) return Promise.resolve()
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const nodes = graphRef.current?.getNodes() || [];
|
const nodes = graphRef.current?.getNodes().filter((node: Node) => {
|
||||||
|
const nodeData = node.getData();
|
||||||
|
return nodeData?.type !== 'add-node';
|
||||||
|
}) || [];
|
||||||
const edges = graphRef.current?.getEdges() || []
|
const edges = graphRef.current?.getEdges() || []
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
@@ -771,7 +888,7 @@ export const useWorkflowGraph = ({
|
|||||||
itemConfig = {
|
itemConfig = {
|
||||||
...itemConfig,
|
...itemConfig,
|
||||||
...data.config[key].defaultValue,
|
...data.config[key].defaultValue,
|
||||||
knowledge_bases: knowledge_bases.map((vo: any) => ({ kb_id: vo.id, ...vo.config }))
|
knowledge_bases: knowledge_bases?.map((vo: any) => ({ kb_id: vo.id, ...vo.config }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -781,6 +898,7 @@ export const useWorkflowGraph = ({
|
|||||||
id: data.id || node.id,
|
id: data.id || node.id,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
cycle: data.cycle, // 保存cycle参数
|
||||||
position: {
|
position: {
|
||||||
x: position.x,
|
x: position.x,
|
||||||
y: position.y,
|
y: position.y,
|
||||||
@@ -793,8 +911,9 @@ export const useWorkflowGraph = ({
|
|||||||
const targetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
const targetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||||
const sourcePortId = edge.getSourcePortId();
|
const sourcePortId = edge.getSourcePortId();
|
||||||
|
|
||||||
// 过滤无效连线:源节点或目标节点不存在
|
// 过滤无效连线:源节点或目标节点不存在,或者是add-node类型
|
||||||
if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id) {
|
if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id ||
|
||||||
|
sourceCell?.getData()?.type === 'add-node' || targetCell?.getData()?.type === 'add-node') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,6 +926,15 @@ export const useWorkflowGraph = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是question-classifier节点的右侧端口连线,添加label
|
||||||
|
if (sourceCell?.getData()?.type === 'question-classifier' && sourcePortId?.startsWith('CASE')) {
|
||||||
|
return {
|
||||||
|
source: sourceCell.getData().id,
|
||||||
|
target: targetCell?.getData().id,
|
||||||
|
label: sourcePortId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: sourceCell?.getData().id,
|
source: sourceCell?.getData().id,
|
||||||
target: targetCell?.getData().id,
|
target: targetCell?.getData().id,
|
||||||
@@ -832,6 +960,7 @@ export const useWorkflowGraph = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
|
setConfig,
|
||||||
graphRef,
|
graphRef,
|
||||||
selectedNode,
|
selectedNode,
|
||||||
setSelectedNode,
|
setSelectedNode,
|
||||||
@@ -848,6 +977,6 @@ export const useWorkflowGraph = ({
|
|||||||
deleteEvent,
|
deleteEvent,
|
||||||
copyEvent,
|
copyEvent,
|
||||||
parseEvent,
|
parseEvent,
|
||||||
handleSave
|
handleSave,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,20 +4,24 @@ import clsx from 'clsx';
|
|||||||
import NodeLibrary from './components/NodeLibrary'
|
import NodeLibrary from './components/NodeLibrary'
|
||||||
import Properties from './components/Properties';
|
import Properties from './components/Properties';
|
||||||
import CanvasToolbar from './components/CanvasToolbar';
|
import CanvasToolbar from './components/CanvasToolbar';
|
||||||
|
import PortClickHandler from './components/PortClickHandler';
|
||||||
import { useWorkflowGraph } from './hooks/useWorkflowGraph';
|
import { useWorkflowGraph } from './hooks/useWorkflowGraph';
|
||||||
import type { WorkflowRef } from '@/views/ApplicationConfig/types'
|
import type { WorkflowRef } from '@/views/ApplicationConfig/types'
|
||||||
import Chat from './components/Chat/Chat';
|
import Chat from './components/Chat/Chat';
|
||||||
import type { ChatRef } from './types'
|
import type { ChatRef, AddChatVariableRef, ChatVariable } from './types'
|
||||||
import arrowIcon from '@/assets/images/workflow/arrow.png'
|
import arrowIcon from '@/assets/images/workflow/arrow.png'
|
||||||
|
import AddChatVariable from './components/AddChatVariable';
|
||||||
|
|
||||||
const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const miniMapRef = useRef<HTMLDivElement>(null);
|
const miniMapRef = useRef<HTMLDivElement>(null);
|
||||||
|
const addChatVariableRef = useRef<AddChatVariableRef>(null)
|
||||||
const chatRef = useRef<ChatRef>(null)
|
const chatRef = useRef<ChatRef>(null)
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
// 使用自定义Hook初始化工作流图
|
// 使用自定义Hook初始化工作流图
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
|
setConfig,
|
||||||
graphRef,
|
graphRef,
|
||||||
selectedNode,
|
selectedNode,
|
||||||
setSelectedNode,
|
setSelectedNode,
|
||||||
@@ -33,7 +37,7 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
|||||||
deleteEvent,
|
deleteEvent,
|
||||||
copyEvent,
|
copyEvent,
|
||||||
parseEvent,
|
parseEvent,
|
||||||
handleSave
|
handleSave,
|
||||||
} = useWorkflowGraph({ containerRef, miniMapRef });
|
} = useWorkflowGraph({ containerRef, miniMapRef });
|
||||||
|
|
||||||
const onDragOver = (event: React.DragEvent) => {
|
const onDragOver = (event: React.DragEvent) => {
|
||||||
@@ -45,11 +49,24 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
|||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
setCollapsed(prev => !prev)
|
setCollapsed(prev => !prev)
|
||||||
}
|
}
|
||||||
|
const addVariable = () => {
|
||||||
|
addChatVariableRef.current?.handleOpen()
|
||||||
|
}
|
||||||
|
const handleUpdateChatVariable = (variables: ChatVariable[]) => {
|
||||||
|
setConfig(prev => {
|
||||||
|
if (!prev) return null
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
variables
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleSave,
|
handleSave,
|
||||||
handleRun,
|
handleRun,
|
||||||
graphRef
|
graphRef,
|
||||||
|
addVariable
|
||||||
}))
|
}))
|
||||||
return (
|
return (
|
||||||
<div className="rb:h-[calc(100vh-64px)] rb:relative">
|
<div className="rb:h-[calc(100vh-64px)] rb:relative">
|
||||||
@@ -97,12 +114,20 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
|||||||
deleteEvent={deleteEvent}
|
deleteEvent={deleteEvent}
|
||||||
copyEvent={copyEvent}
|
copyEvent={copyEvent}
|
||||||
parseEvent={parseEvent}
|
parseEvent={parseEvent}
|
||||||
|
config={config}
|
||||||
/>
|
/>
|
||||||
<Chat
|
<Chat
|
||||||
ref={chatRef}
|
ref={chatRef}
|
||||||
graphRef={graphRef}
|
graphRef={graphRef}
|
||||||
appId={config?.app_id as string}
|
appId={config?.app_id as string}
|
||||||
/>
|
/>
|
||||||
|
<PortClickHandler graph={graphRef.current} />
|
||||||
|
|
||||||
|
<AddChatVariable
|
||||||
|
ref={addChatVariableRef}
|
||||||
|
variables={config?.variables}
|
||||||
|
onChange={handleUpdateChatVariable}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export interface NodeConfig {
|
|||||||
|
|
||||||
knowledge_retrieval?: KnowledgeConfig;
|
knowledge_retrieval?: KnowledgeConfig;
|
||||||
|
|
||||||
group_names?: Array<{key: string, value: string[]}>
|
group_names?: Array<{ key: string, value: string[] }>
|
||||||
|
cycle?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export interface NodeProperties {
|
|||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
config?: Record<string, NodeConfig>;
|
config?: Record<string, NodeConfig>;
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeLibrary {
|
export interface NodeLibrary {
|
||||||
@@ -53,6 +55,8 @@ export interface NodeItem {
|
|||||||
config: {
|
config: {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cycle?: string;
|
||||||
}
|
}
|
||||||
export interface EdgesItem {
|
export interface EdgesItem {
|
||||||
source: string;
|
source: string;
|
||||||
@@ -102,4 +106,15 @@ export interface ChatRef {
|
|||||||
export type GraphRef = React.MutableRefObject<Graph | undefined>
|
export type GraphRef = React.MutableRefObject<Graph | undefined>
|
||||||
export interface VariableConfigModalRef {
|
export interface VariableConfigModalRef {
|
||||||
handleOpen: (values: StartVariableItem[]) => void;
|
handleOpen: (values: StartVariableItem[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatVariable {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
required: boolean;
|
||||||
|
description: string;
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
export interface AddChatVariableRef {
|
||||||
|
handleOpen: (value?: ChatVariable) => void;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user