diff --git a/web/src/api/tools.ts b/web/src/api/tools.ts index 142d4c41..b14905f8 100644 --- a/web/src/api/tools.ts +++ b/web/src/api/tools.ts @@ -27,5 +27,10 @@ export const execute = (data: ExecuteData) => { } export const parseSchema = (data: Record) => { 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`) } \ No newline at end of file diff --git a/web/src/assets/images/workflow/assigner.png b/web/src/assets/images/workflow/assigner.png new file mode 100644 index 00000000..4370bfdd Binary files /dev/null and b/web/src/assets/images/workflow/assigner.png differ diff --git a/web/src/assets/images/workflow/break.png b/web/src/assets/images/workflow/break.png new file mode 100644 index 00000000..473ab068 Binary files /dev/null and b/web/src/assets/images/workflow/break.png differ diff --git a/web/src/assets/images/workflow/question-classifier.png b/web/src/assets/images/workflow/question-classifier.png new file mode 100644 index 00000000..754a0a62 Binary files /dev/null and b/web/src/assets/images/workflow/question-classifier.png differ diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index ee6400f0..353962c7 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1117,7 +1117,11 @@ export const en = { memoryContent: 'Memory Content', 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: { createSpace: 'Create Space', @@ -1580,16 +1584,22 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re 'parameter-extractor': 'Parameter Extraction', flowControl: 'Flow Control', 'if-else': 'Conditional Branch', + 'question-classifier': 'Question Classifier', iteration: 'Iteration', loop: 'Loop', + 'cycle-start': '', + break: 'Break Loop', + assigner: 'Variable Assignment', parallel: 'Parallel Execution', 'var-aggregator': 'Variable Aggregator', externalInteraction: 'External Interaction', "http-request": 'HTTP Request', - tools: 'Tools', + tool: 'Tools', code_execution: 'Code Execution', "jinja-render": 'Template Rendering', cognitiveUpgrading: 'Cognitive Upgrading (Innovation)', + 'memory-read': 'Memory Retrieval', + 'memory-write': 'Memory Storage', task_planning: 'Task Planning', reasoning_control: 'Reasoning Control', 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', nodeProperties: 'Node Properties', - empty: "Emmm... The box is empty, nothing here~", + empty: "Emmm…The box is empty, there's nothing here~", nodeName: 'Node Name', - + addvariable: 'Chat Variables', + addChatVariable: 'Add Chat Variable', + editChatVariable: 'Edit Chat Variable', config: { llm: { @@ -1631,7 +1643,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re editVariable: 'Edit Variable', variableType: 'Variable Type', variableName: 'Variable Name', - invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores', + invalidVariableName: 'Variable name can only start with English letters and contain English letters, numbers, and underscores', description: 'Display Name', default: 'Default Value', required: 'Required', @@ -1658,10 +1670,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re editParam: 'Edit Extract Parameter', 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', desc: 'Description', required: 'Required', + default: 'Default Value', 'string': 'String', 'number': 'Number', @@ -1673,7 +1686,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re }, 'var-aggregator': { 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', variable: 'Variable Assignment' }, @@ -1691,7 +1704,76 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re "gt": '>', "ge": '>=', 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', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 38fc04d6..7709729e 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1198,7 +1198,11 @@ export const zh = { updated_at: '最后更新时间', fullScreen: '全屏', - memoryWindow: "{{name}}的记忆之窗" + memoryWindow: "{{name}}的记忆之窗", + memory_insight: '总体概述', + key_findings: '关键发现', + behavior_pattern: '行为模式', + growth_trajectory: '成长轨迹', }, space: { createSpace: '创建空间', @@ -1680,16 +1684,22 @@ export const zh = { 'parameter-extractor': '参数提取', flowControl: '流程控制', 'if-else': '条件分支', + 'question-classifier': '问题分类器', iteration: '迭代 (Iteration)', loop: '循环 (Loop)', + 'cycle-start': '', + break: '退出循环', + assigner: '变量赋值', parallel: '并行执行', 'var-aggregator': '变量聚合器', externalInteraction: '外部交互', "http-request": 'HTTP请求', - tools: '工具 (Tools)', + tool: '工具 (Tool)', code_execution: '代码执行', "jinja-render": '模板渲染', cognitiveUpgrading: '认知升级(创新)', + 'memory-read': '记忆提取', + 'memory-write': '记忆储存', task_planning: '任务规划', reasoning_control: '推理控制', self_reflection: '自我反思', @@ -1709,7 +1719,9 @@ export const zh = { nodeProperties: '节点属性', empty: "Emmm…盒子是空的,这里什么都没有~", nodeName: '节点名称', - + addvariable: '会话变量', + addChatVariable: '添加会话变量', + editChatVariable: '编辑会话变量', config: { llm: { @@ -1762,6 +1774,7 @@ export const zh = { type: '类型', desc: '描述', required: '必填', + default: '默认值', 'string': 'String', 'number': 'Number', @@ -1819,9 +1832,48 @@ export const zh = { template: '代码', 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: '键', type: '类型', value: '值', + addCase: '添加条件', }, clear: '清空', diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index ec899a32..37a56c80 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -101,6 +101,9 @@ const ConfigHeader: FC = ({ const clear = () => { workflowRef?.current?.graphRef?.current?.clearCells() } + const addvariable = () => { + workflowRef?.current?.addVariable() + } return ( <>
@@ -132,6 +135,7 @@ const ConfigHeader: FC = ({ {application?.type === 'workflow' ?
+ {/* */} diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index abe7a008..3a1c262c 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -121,7 +121,8 @@ export interface ClusterRef { export interface WorkflowRef { handleSave: (flag?: boolean) => Promise; handleRun: () => void; - graphRef: GraphRef + graphRef: GraphRef; + addVariable: () => void; } export interface ApplicationModalRef { handleOpen: (application?: Config) => void; diff --git a/web/src/views/ToolManagement/types.ts b/web/src/views/ToolManagement/types.ts index 4fa07740..6fd4e439 100644 --- a/web/src/views/ToolManagement/types.ts +++ b/web/src/views/ToolManagement/types.ts @@ -1,4 +1,4 @@ -type ToolType = 'mcp' | 'builtin' | 'custom' +export type ToolType = 'mcp' | 'builtin' | 'custom' export interface Query { name?: string; tool_type: ToolType diff --git a/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx b/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx index 0c0751f3..608ad01d 100644 --- a/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx +++ b/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx @@ -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 { useParams } from 'react-router-dom' -import { Skeleton } from 'antd'; +import { Skeleton, Space } from 'antd'; import RbCard from '@/components/RbCard/Card' import Empty from '@/components/Empty'; import { @@ -9,11 +10,20 @@ import { } from '@/api/memory' 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((_props, ref) => { const { t } = useTranslation() const { id } = useParams() const [loading, setLoading] = useState(false) - const [report, setReport] = useState(null) + const [data, setData] = useState({} as Data) useEffect(() => { if (!id) return @@ -25,7 +35,7 @@ const MemoryInsight = forwardRef((_props, ref) => { if (!id) return setLoading(true) getMemoryInsightReport(id).then((res) => { - setReport((res as { report?: string }).report || null) + setData((res as Data) || {}) setLoading(false) }) .finally(() => { @@ -43,10 +53,35 @@ const MemoryInsight = forwardRef((_props, ref) => { > {loading ? - : report - ?
- {report || '-'} -
+ : Object.keys(data).length > 0 + ? + {['memory_insight', 'key_findings', 'behavior_pattern', 'growth_trajectory'].map(key => { + if (data[key as keyof Data]) { + return ( +
+
{t(`userMemory.${key}`)}
+
+ {Array.isArray(data[key as keyof Data]) + ? <> + {(data[key as keyof Data] as string[])?.map((item: string, index: number) => ( +
+ - {item} +
+ ))} + + : data[key as keyof Data] as string + } +
+
+ ) + } + return null + })} + +
: } diff --git a/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx b/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx new file mode 100644 index 00000000..571f1e4e --- /dev/null +++ b/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx @@ -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(({ + refresh +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const [editIndex, setEditIndex] = useState(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 ( + +
+ + + + + + ); + } + return ; + }} + + + + + + + + + {t('workflow.config.parameter-extractor.required')} + +
+
+ ); +}); + +export default ChatVariableModal; \ No newline at end of file diff --git a/web/src/views/Workflow/components/AddChatVariable/index.tsx b/web/src/views/Workflow/components/AddChatVariable/index.tsx new file mode 100644 index 00000000..f765b5eb --- /dev/null +++ b/web/src/views/Workflow/components/AddChatVariable/index.tsx @@ -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(({ + variables = [], + onChange, +}, ref) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const chatVariableRef = useRef(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 ( + setOpen(false)} + > +
+ + + {variables.length === 0 + ? + : + ( + +
+
+
+ {item.name} + ({t(`workflow.config.parameter-extractor.${item.type}`)}) +
+ {item.required ? t('workflow.config.parameter-extractor.required') : ''} + +
+
{item.description}
+ +
handleEdit(index)} + >
+
handleDelete(index)} + >
+
+
+
+ )} + /> + } +
+ + +
+ ); +}); + +export default AddChatVariable; \ No newline at end of file diff --git a/web/src/views/Workflow/components/AddChatVariable/types.ts b/web/src/views/Workflow/components/AddChatVariable/types.ts new file mode 100644 index 00000000..ab00ae69 --- /dev/null +++ b/web/src/views/Workflow/components/AddChatVariable/types.ts @@ -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; +} \ No newline at end of file diff --git a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx index bf810bec..13d12ee1 100644 --- a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx +++ b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx @@ -44,14 +44,14 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({ > {data.isContext ? ( 📄 - ) : ( + ) : data.group !== 'CONVERSATION' ? ( - )} - {!data.isContext && ( + ) : null} + {!data.isContext && data.group !== 'CONVERSATION' && ( <> {data.nodeData?.name} / diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index a35096a4..5c5d3956 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -11,6 +11,7 @@ export interface Suggestion { type: string; dataType: string; value: string; + group?: string nodeData: NodeProperties; isContext?: boolean; // 标记是否为context变量 disabled?: boolean; // 标记是否禁用 diff --git a/web/src/views/Workflow/components/NodeLibrary.tsx b/web/src/views/Workflow/components/NodeLibrary.tsx index a43c65eb..ecc4d5f4 100644 --- a/web/src/views/Workflow/components/NodeLibrary.tsx +++ b/web/src/views/Workflow/components/NodeLibrary.tsx @@ -23,7 +23,9 @@ const NodeLibrary: FC = () => { }} > - {category.nodes.map((node, nodeIndex) => ( + {category.nodes + .filter(node => node.type !== 'cycle-start' && node.type !== 'break') + .map((node, nodeIndex) => (
{ - const data = node?.getData() || {} +const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { + 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 = ( +
+ {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 ( +
+ {categoryIndex > 0 &&
} +
+ {t(`workflow.${category.category}`)} +
+ {filteredNodes.map((nodeType) => ( +
handleNodeSelect(nodeType)} + onMouseEnter={(e) => { + e.currentTarget.style.background = '#f0f8ff'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'white'; + }} + > + + {t(`workflow.${nodeType.type}`)} +
+ ))} +
+ ); + })} +
+ ); return ( -
- - {data.icon} {data.label} - -
+ +
+ + {data.icon} {data.label} + +
+
); }; diff --git a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx index de6fcb1c..48b926dc 100644 --- a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx +++ b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx @@ -7,7 +7,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { const { t } = useTranslation() return ( -
diff --git a/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx b/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx index cd8a9c50..7fce262e 100644 --- a/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx +++ b/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx @@ -1,17 +1,11 @@ import clsx from 'clsx'; import type { ReactShapeConfig } from '@antv/x6-react-shape'; +import startIcon from '@/assets/images/workflow/start.png'; -const GroupStartNode: ReactShapeConfig['component'] = ({ node }) => { - const data = node?.getData() || {} - +const GroupStartNode: ReactShapeConfig['component'] = () => { return ( -
- - {data.icon} {data.label} - +
+
); }; diff --git a/web/src/views/Workflow/components/Nodes/IterationNode.tsx b/web/src/views/Workflow/components/Nodes/IterationNode.tsx deleted file mode 100644 index a6c55138..00000000 --- a/web/src/views/Workflow/components/Nodes/IterationNode.tsx +++ /dev/null @@ -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 ( -
- {/* 标题区域 */} -
-
- 🔁 -
- 迭代 -
- - - - - {/* 画布内容区域 */} -
-
- ); -}; - -export default IterationNode; diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx index da73e4ce..b0b8d4ce 100644 --- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx +++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx @@ -1,19 +1,10 @@ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next' 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; -} +import { edge_color } from '../../hooks/useWorkflowGraph' const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { const data = node.getData() || {}; @@ -21,63 +12,145 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { useEffect(() => { 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 = () => { + // 检查是否存在cycle为当前节点ID的子节点,若存在则不调用initNodes,避免重复创建 + const existingCycleNodes = graph.getNodes().filter((n: any) => + n.getData()?.cycle === data.id + ); + if (existingCycleNodes.length > 0) return; // 添加默认子节点 const parentBBox = node.getBBox(); const centerX = parentBBox.x + 24; // 默认节点宽度的一半 const centerY = parentBBox.y + 50; // 默认节点高度的一半 - const childNode1 = graph.addNode({ - ...graphNodeLibrary.groupStart, + const cycleStartNode = graph.addNode({ + ...graphNodeLibrary.cycleStart, x: centerX, y: centerY, data: { - type: 'default', - label: '开始', - // icon: '📌', + type: 'cycle-start', parentId: node.id, - isDefault: true // 标记为默认节点,不可删除 + isDefault: true, // 标记为默认节点,不可删除 + cycle: data.id, }, }); - const childNode2 = graph.addNode({ + const addNode = graph.addNode({ ...graphNodeLibrary.addStart, - x: centerX + 150, + x: centerX + 64, y: centerY, data: { - type: 'default', + type: 'add-node', label: '添加节点', icon: '+', parentId: node.id, + cycle: data.id, }, }); - node.addChild(childNode1) - node.addChild(childNode2) + node.addChild(cycleStartNode) + node.addChild(addNode) + const sourcePorts = cycleStartNode.getPorts() + const targetPorts = addNode.getPorts() + let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right'; + + const edgeConfig = { + source: { + cell: cycleStartNode.id, + port: sourcePort + }, + target: { + cell: addNode.id, + port: targetPorts.find((port: any) => port.group === 'left')?.id || 'left' + }, + attrs: { + line: { + stroke: edge_color, + strokeWidth: 1, + targetMarker: { + name: 'block', + size: 8, + }, + }, + }, + } + + graph.addEdge(edgeConfig) } - return ( -
-
-
- -
{data.name ?? t(`workflow.${data.type}`)}
-
- -
{ - e.stopPropagation() - node.remove() - }} - >
+ return ( +
+
+
+ +
{data.name ?? t(`workflow.${data.type}`)}
-
+ +
{ + e.stopPropagation() + node.remove() + }} + >
- ); +
+
+ ); }; export default LoopNode; diff --git a/web/src/views/Workflow/components/Nodes/NormalNode.tsx b/web/src/views/Workflow/components/Nodes/NormalNode.tsx index 17c55494..b910ee68 100644 --- a/web/src/views/Workflow/components/Nodes/NormalNode.tsx +++ b/web/src/views/Workflow/components/Nodes/NormalNode.tsx @@ -7,7 +7,7 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => { const { t } = useTranslation() return ( -
diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx new file mode 100644 index 00000000..0be6fba1 --- /dev/null +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -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 = ({ graph }) => { + const { t } = useTranslation(); + const [popoverVisible, setPopoverVisible] = useState(false); + const [popoverPosition, setPopoverPosition] = useState({ x: 0, y: 0 }); + const [sourceNode, setSourceNode] = useState(null); + const [sourcePort, setSourcePort] = useState(''); + const [tempElement, setTempElement] = useState(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 = ( +
+ {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 ( +
+ {categoryIndex > 0 &&
} +
+ {t(`workflow.${category.category}`)} +
+ {filteredNodes.map((nodeType) => ( +
handleNodeSelect(nodeType)} + onMouseEnter={(e) => { + e.currentTarget.style.background = '#f0f8ff'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'white'; + }} + > + + {t(`workflow.${nodeType.type}`)} +
+ ))} +
+ ); + })} +
+ ); + + if (!tempElement) return null; + + return ( + { + if (!visible) handlePopoverClose(); + }} + placement="right" + overlayStyle={{ + position: 'fixed', + left: popoverPosition.x + 10, + top: popoverPosition.y - 10, + }} + > +
+ + ); +}; + +export default PortClickHandler; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx new file mode 100644 index 00000000..34c133c7 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx @@ -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 = ({ + parentName, + options = [], +}) => { + const { t } = useTranslation(); + const form = Form.useFormInstance(); + + return ( + + {(fields, { add, remove }) => ( + <> +
+ {t(`workflow.config.assigner.${parentName}`)} + add({ operation: 'cover'})} /> +
+ {fields.map(({ key, name, ...restField }) => { + return ( +
+ + + + + + + + + ({ + value: key, + label: t(`workflow.config.if-else.${key}`) + }))} + size="small" + popupMatchSelectWidth={false} + /> + + + + removeCondition(conditionField.name)} /> + + + + {!hideRightField && ( + + - - - - ({ + value: key, + label: t(`workflow.config.if-else.${key}`) + }))} + size="small" + popupMatchSelectWidth={false} + /> + + + + remove(field.name)} + /> + + + {!hideRightField && ( + + + + + + )} + + +
+
+ ) + })} +
+ + +
+ )} + + + ) +} + +export default ConditionList \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx new file mode 100644 index 00000000..367e8483 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx @@ -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 = ({ + 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 ( +
+ + + {(fields, { add, remove }) => ( + <> +
+ 循环变量 + add({ name: '', type: 'string', input_type: 'constant', value: '' })} /> +
+ {fields.map(({ key, name, ...field }, index) => { + const currentInputType = value?.[index]?.input_type; + + return ( +
+ + + + + + + + + { + // 重置 value 字段 + form.setFieldValue([parentName, index, 'value'], undefined); + }} + /> + + + + remove(name)} + /> + + + + + {currentInputType === 'variable' ? ( + + ) : ( + + )} + +
+ ) + })} + + )} +
+
+ ) +} + +export default CycleVarsList \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx new file mode 100644 index 00000000..0cba5596 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx @@ -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([ + { 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([]) + + 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