diff --git a/web/src/utils/yamlExport.ts b/web/src/utils/yamlExport.ts new file mode 100644 index 00000000..9a8c0c41 --- /dev/null +++ b/web/src/utils/yamlExport.ts @@ -0,0 +1,13 @@ +import yaml from 'js-yaml'; + + +export const exportToYaml = (data: unknown, filename: string = 'export.yaml') => { + const yamlStr = yaml.dump(data); + const blob = new Blob([yamlStr], { type: 'text/yaml' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +}; diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index 9aab1110..28c96ec0 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -221,12 +221,12 @@ const Agent = forwardRef((_props, ref) => { return new Promise((resolve, reject) => { saveAgentConfig(data.app_id, params) - .then(() => { + .then((res) => { if (flag) { message.success(t('common.saveSuccess')) } setIsSave(false) - resolve(true) + resolve(res) }).catch(error => { reject(error) }) diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index 714c1c49..3081aa04 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -58,16 +58,14 @@ const Cluster = forwardRef((_props, ref) => { })) } - console.log('params', params) - return new Promise((resolve, reject) => { form.validateFields().then(() => { saveMultiAgentConfig(id as string, params) - .then(() => { + .then((res) => { if (flag) { message.success(t('common.saveSuccess')) } - resolve(true) + resolve(res) }) .catch(error => { reject(error) diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index 1837b696..94ef0ef7 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -11,7 +11,7 @@ import exportIcon from '@/assets/images/export_hover.svg' import deleteIcon from '@/assets/images/delete_hover.svg' import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types'; import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal' -import type { CopyModalRef, WorkflowRef } from '../types' +import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef } from '../types' import { deleteApplication } from '@/api/application' import CopyModal from './CopyModal' @@ -30,10 +30,11 @@ interface ConfigHeaderProps { handleChangeTab: (key: string) => void; refresh: () => void; workflowRef: React.RefObject + appRef?: React.RefObject } const ConfigHeader: FC = ({ application, activeTab, handleChangeTab, refresh, - workflowRef + workflowRef, }) => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -48,7 +49,7 @@ const ConfigHeader: FC = ({ })) } const formatMenuItems = () => { - const items = ['edit', 'copy', 'delete'].map(key => ({ + const items = ['edit', 'copy', 'export', 'delete'].map(key => ({ key, icon: , label: t(`common.${key}`), @@ -59,7 +60,6 @@ const ConfigHeader: FC = ({ } } const handleClick: MenuProps['onClick'] = ({ key }) => { - console.log('key', key) switch (key) { case 'edit': applicationModalRef.current?.handleOpen(application as Application) diff --git a/web/src/views/ApplicationConfig/index.tsx b/web/src/views/ApplicationConfig/index.tsx index 23f3e434..7d5d5950 100644 --- a/web/src/views/ApplicationConfig/index.tsx +++ b/web/src/views/ApplicationConfig/index.tsx @@ -60,10 +60,11 @@ const ApplicationConfig: React.FC = () => { handleChangeTab={handleChangeTab} application={application as Application} refresh={getApplicationInfo} + appRef={application?.type === 'agent' ? agentRef : application?.type === 'multi_agent' ? clusterRef : application?.type === 'workflow' ? workflowRef : undefined} workflowRef={workflowRef} /> {activeTab === 'arrangement' && application?.type === 'agent' && } - {activeTab === 'arrangement' && application?.type === 'multi_agent' && } + {activeTab === 'arrangement' && application?.type === 'multi_agent' && } {activeTab === 'arrangement' && application?.type === 'workflow' && } {activeTab === 'api' && } {activeTab === 'release' && } diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx index d2d1d15c..f9561a44 100644 --- a/web/src/views/Workflow/components/Nodes/AddNode.tsx +++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Popover } from 'antd'; import clsx from 'clsx'; import type { ReactShapeConfig } from '@antv/x6-react-shape'; -import { nodeLibrary, graphNodeLibrary } from '../../constant'; +import { nodeLibrary, graphNodeLibrary, edgeAttrs } from '../../constant'; import { useTranslation } from 'react-i18next'; const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { @@ -47,7 +47,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { 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(), + ...edgeAttrs }); }); @@ -57,7 +57,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { 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(), + ...edgeAttrs }); }); diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx index 26109d58..97136746 100644 --- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx +++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx @@ -2,9 +2,7 @@ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next' import clsx from 'clsx'; import type { ReactShapeConfig } from '@antv/x6-react-shape'; -import { graphNodeLibrary } from '../../constant'; - -import { edge_color } from '../../hooks/useWorkflowGraph' +import { graphNodeLibrary, edgeAttrs } from '../../constant'; const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { const data = node.getData() || {}; @@ -56,16 +54,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { 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, - }, - }, - }, + ...edgeAttrs, }); } } @@ -122,16 +111,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { cell: addNode.id, port: targetPorts.find((port: any) => port.group === 'left')?.id || 'left' }, - attrs: { - line: { - stroke: edge_color, - strokeWidth: 1, - targetMarker: { - name: 'block', - size: 2, - }, - }, - }, + ...edgeAttrs } graph.addEdge(edgeConfig) } diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index 8d95431c..0a1e3906 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Popover } from 'antd'; import { useTranslation } from 'react-i18next'; -import { nodeLibrary, graphNodeLibrary } from '../constant'; +import { nodeLibrary, graphNodeLibrary, edgeAttrs } from '../constant'; interface PortClickHandlerProps { graph: any; @@ -149,16 +149,7 @@ const PortClickHandler: React.FC = ({ graph }) => { 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, - }, - }, - }, + ...edgeAttrs // zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0 }); diff --git a/web/src/views/Workflow/components/Properties/CaseList/index.tsx b/web/src/views/Workflow/components/Properties/CaseList/index.tsx index 8caa601e..d9521059 100644 --- a/web/src/views/Workflow/components/Properties/CaseList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CaseList/index.tsx @@ -6,6 +6,7 @@ import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectPr import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import VariableSelect from '../VariableSelect' import Editor from '../../Editor' +import { edgeAttrs } from '../../../constant' interface CaseListProps { value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>; @@ -120,16 +121,7 @@ const CaseList: FC = ({ 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, - }, - }, - }, + ...edgeAttrs, }); } graphRef.current?.removeCell(edge); @@ -174,16 +166,7 @@ const CaseList: FC = ({ 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, - }, - }, - }, + ...edgeAttrs }); } } diff --git a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx index ca7f8115..aabc3ad3 100644 --- a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx @@ -5,6 +5,7 @@ import { Graph, Node } from '@antv/x6'; import Editor from '../../Editor'; import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' +import { edgeAttrs } from '../../../constant' interface CategoryListProps { parentName: string; @@ -70,16 +71,7 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe 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, - }, - }, - }, + ...edgeAttrs }); } return; @@ -110,16 +102,7 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe 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, - }, - }, - }, + ...edgeAttrs }); } } diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx index 13e00ea2..9e28c958 100644 --- a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx @@ -1,6 +1,6 @@ import { type FC } from 'react' import { useTranslation } from 'react-i18next'; -import { Form, Select, Input, Button } from 'antd' +import { Form, Select, Input, Button, InputNumber } from 'antd' import VariableSelect from '../VariableSelect' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' @@ -93,6 +93,7 @@ const CycleVarsList: FC = ({ {fields.map(({ key, name }, index) => { + const currentType = value?.[index]?.type; const currentInputType = value?.[index]?.input_type; return ( @@ -131,7 +132,8 @@ const CycleVarsList: FC = ({ - {currentInputType === 'variable' ? ( + {currentInputType === 'variable' + ? ( { @@ -143,7 +145,15 @@ const CycleVarsList: FC = ({ variant="borderless" size="small" /> - ) : ( + ) + : currentType === 'number' + ? form.setFieldValue([name, 'value'], value)} + /> + : ( >; zoomLevel: number; setZoomLevel: React.Dispatch>; - canUndo: boolean; - canRedo: boolean; isHandMode: boolean; setIsHandMode: React.Dispatch>; - onUndo: () => void; - onRedo: () => void; onDrop: (event: React.DragEvent) => void; blankClick: () => void; deleteEvent: () => boolean | void; @@ -39,8 +35,6 @@ export interface UseWorkflowGraphReturn { setChatVariables: React.Dispatch>; } -export const edge_color = '#155EEF'; -const edge_selected_color = '#4DA8FF' export const useWorkflowGraph = ({ containerRef, miniMapRef, @@ -51,9 +45,6 @@ export const useWorkflowGraph = ({ const graphRef = useRef(); const [selectedNode, setSelectedNode] = useState(null); const [zoomLevel, setZoomLevel] = useState(1); - const historyRef = useRef<{ undoStack: string[], redoStack: string[] }>({ undoStack: [], redoStack: [] }); - const [canUndo, setCanUndo] = useState(false); - const [canRedo, setCanRedo] = useState(false); const [isHandMode, setIsHandMode] = useState(true); const [config, setConfig] = useState(null); const [chatVariables, setChatVariables] = useState([]) @@ -338,17 +329,7 @@ export const useWorkflowGraph = ({ port: targetPorts.find((port: any) => port.group === 'left')?.id || 'left' }, connector: { name: 'smooth' }, - attrs: { - line: { - stroke: edge_color, - strokeWidth: 1, - targetMarker: { - name: 'diamond', - width: 4, - height: 4, - }, - }, - }, + ...edgeAttrs // zIndex: loopIterationCount } @@ -368,48 +349,6 @@ export const useWorkflowGraph = ({ }, 200) } } - - const saveState = () => { - if (!graphRef.current) return; - const state = JSON.stringify(graphRef.current.toJSON()); - historyRef.current.undoStack.push(state); - historyRef.current.redoStack = []; - if (historyRef.current.undoStack.length > 50) { - historyRef.current.undoStack.shift(); - } - updateHistoryState(); - }; - - const updateHistoryState = () => { - setCanUndo(historyRef.current.undoStack.length > 1); - setCanRedo(historyRef.current.redoStack.length > 0); - }; - - // 撤销 - const onUndo = () => { - if (!graphRef.current || historyRef.current.undoStack.length === 0) return; - const { undoStack = [], redoStack = [] } = historyRef.current - - const currentState = JSON.stringify(graphRef.current.toJSON()); - const prevState = undoStack[undoStack.length - 2]; - - historyRef.current.redoStack = [...redoStack, currentState] - historyRef.current.undoStack = undoStack.slice(0, undoStack.length - 1) - graphRef.current.fromJSON(JSON.parse(prevState)); - updateHistoryState(); - }; - // 重做 - const onRedo = () => { - if (!graphRef.current || historyRef.current.redoStack.length === 0) return; - const { undoStack = [], redoStack = [] } = historyRef.current - - const nextState = redoStack[redoStack.length - 1]; - - historyRef.current.undoStack = [...undoStack, nextState] - historyRef.current.redoStack = redoStack.slice(0, redoStack.length - 1) - graphRef.current.fromJSON(JSON.parse(nextState)); - updateHistoryState(); - }; // 使用插件 const setupPlugins = () => { if (!graphRef.current || !miniMapRef.current) return; @@ -563,20 +502,6 @@ export const useWorkflowGraph = ({ } return false; }; - // 撤销快捷键事件 - const undoEvent = () => { - if (canUndo) { - onUndo(); - } - return false; - }; - // 重做快捷键事件 - const redoEvent = () => { - if (canRedo) { - onRedo(); - } - return false; - }; // 删除选中的节点和连线事件 const deleteEvent = () => { if (!graphRef.current) return; @@ -748,8 +673,6 @@ export const useWorkflowGraph = ({ background: { color: '#F0F3F8', }, - // width: container.clientWidth || 800, - // height: container.clientHeight || 600, autoResize: true, grid: { visible: true, @@ -765,37 +688,26 @@ export const useWorkflowGraph = ({ enabled: true, }, connecting: { - // router: 'orth', - // router: 'manhattan', connector: { name: 'smooth', args: { radius: 8, }, }, - anchor: 'center', + anchor: 'midSide', connectionPoint: 'anchor', allowBlank: false, + allowLoop: false, allowNode: false, allowEdge: false, + allowPort: true, + allowMulti: true, highlight: true, snap: { radius: 20, }, createEdge() { - return graphRef.current?.createEdge({ - attrs: { - line: { - stroke: edge_color, - strokeWidth: 1, - targetMarker: { - name: 'diamond', - width: 4, - height: 4, - }, - }, - }, - }); + return graphRef.current?.createEdge(edgeAttrs); }, validateConnection({ sourceCell, targetCell, targetMagnet }) { if (!targetMagnet) return false; @@ -901,27 +813,7 @@ export const useWorkflowGraph = ({ // 监听缩放事件 graphRef.current.on('scale', scaleEvent); // 监听节点移动事件 - // graphRef.current.on('node:moved', nodeMoved); - graphRef.current.on('node:change:position', nodeChangePosition); - - // 监听画布变化事件 - const events = [ - 'node:added', - 'node:removed', - 'edge:added', - 'edge:removed', - ]; - events.forEach(event => { - graphRef.current!.on(event, () => { - console.log('event', event); - setTimeout(() => saveState(), 50); - }); - }); - - // 监听撤销键盘事件 - graphRef.current.bindKey(['ctrl+z', 'cmd+z'], undoEvent); - // 监听重做键盘事件 - graphRef.current.bindKey(['ctrl+shift+z', 'cmd+shift+z', 'ctrl+y', 'cmd+y'], redoEvent); + graphRef.current.on('node:moved', nodeMoved); // 监听复制键盘事件 graphRef.current.bindKey(['ctrl+c', 'cmd+c'], copyEvent); // 监听粘贴键盘事件 @@ -929,11 +821,6 @@ export const useWorkflowGraph = ({ // 删除选中的节点和连线 graphRef.current.bindKey(['ctrl+d', 'cmd+d', 'delete', 'backspace'], deleteEvent); - // 保存初始状态 - setTimeout(() => saveState(), 100); - // init window hook - (window as Window & { __x6_instances__?: Graph[] }).__x6_instances__ = []; - (window as Window & { __x6_instances__?: Graph[] }).__x6_instances__?.push(graphRef.current); }; useEffect(() => { @@ -1146,11 +1033,11 @@ export const useWorkflowGraph = ({ }), } saveWorkflowConfig(config.app_id, params as WorkflowConfig) - .then(() => { + .then((res) => { if (flag) { message.success(t('common.saveSuccess')) } - resolve(true) + resolve(res) }).catch(error => { reject(error) }) @@ -1165,12 +1052,8 @@ export const useWorkflowGraph = ({ setSelectedNode, zoomLevel, setZoomLevel, - canUndo, - canRedo, isHandMode, setIsHandMode, - onUndo, - onRedo, onDrop, blankClick, deleteEvent,