diff --git a/service-web/client/src/components/flow/FlowEditor.tsx b/service-web/client/src/components/flow/FlowEditor.tsx index acc631b..5ec1872 100644 --- a/service-web/client/src/components/flow/FlowEditor.tsx +++ b/service-web/client/src/components/flow/FlowEditor.tsx @@ -1,19 +1,20 @@ -import {PlusCircleFilled, RollbackOutlined, SaveFilled} from '@ant-design/icons' +import {RollbackOutlined, SaveFilled} from '@ant-design/icons' import {Background, BackgroundVariant, Controls, MiniMap, Panel, ReactFlow} from '@xyflow/react' -import {Button, Dropdown, message, Popconfirm, Space} from 'antd' -import {arrToMap, isEqual, randomId, unique} from 'licia' +import {Button, message, Popconfirm, Space} from 'antd' +import {arrToMap} from 'licia' import {useEffect} from 'react' import {useNavigate} from 'react-router' import styled from 'styled-components' import '@xyflow/react/dist/style.css' import {commonInfo} from '../../util/amis.tsx' -import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx' +import {checkAddConnection, checkSave} from './FlowChecker.tsx' import {useNodeDrag} from './Helper.tsx' -import {NodeRegistry, NodeRegistryMap} from './NodeRegistry.tsx' +import {NodeRegistryMap} from './NodeRegistry.tsx' import {useContextStore} from './store/ContextStore.ts' import {useDataStore} from './store/DataStore.ts' import {useFlowStore} from './store/FlowStore.ts' import {flowDotColor, type FlowEditorProps} from './types.ts' +import AddNodeButton from './component/AddNodeButton.tsx' const FlowableDiv = styled.div` .react-flow__node.selectable { @@ -111,53 +112,7 @@ function FlowEditor(props: FlowEditorProps) { > - i.group)) - .map(group => ({ - type: 'group', - label: group, - children: NodeRegistry.filter(i => isEqual(group, i.group)) - .map(i => ({key: i.key, label: i.name, icon: i.icon})), - })), - onClick: ({key}) => { - try { - if (commonInfo.debug) { - console.info('Add', key, JSON.stringify({nodes, edges, data})) - } - checkAddNode(key, nodes, edges) - - let nodeId = randomId(10, 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM') - let define = NodeRegistryMap[key] - - setDataById( - nodeId, - { - node: { - name: define.name, - description: define.description, - }, - }, - ) - - addNode({ - id: nodeId, - type: key, - position: {x: 100, y: 100}, - data: {}, - }) - } catch (e) { - // @ts-ignore - messageApi.error(e.toString()) - } - }, - }} - > - - + { if (parentNode) { let newPosition = { x: max(min(node.position.x, (parentNode.measured?.width ?? 0) - (node.measured?.width ?? 0) - 28), 28), - y: max(min(node.position.y, (parentNode.measured?.height ?? 0) - (node.measured?.height ?? 0) - 28), 90), + y: max(min(node.position.y, (parentNode.measured?.height ?? 0) - (node.measured?.height ?? 0) - 28), 130), } setNode({ ...node, diff --git a/service-web/client/src/components/flow/component/AddNodeButton.tsx b/service-web/client/src/components/flow/component/AddNodeButton.tsx new file mode 100644 index 0000000..fc27538 --- /dev/null +++ b/service-web/client/src/components/flow/component/AddNodeButton.tsx @@ -0,0 +1,77 @@ +import {isEqual, randomId, unique} from 'licia' +import {NodeRegistry, NodeRegistryMap} from '../NodeRegistry.tsx' +import {commonInfo} from '../../../util/amis.tsx' +import {checkAddNode} from '../FlowChecker.tsx' +import {Button, Dropdown} from 'antd' +import {PlusCircleFilled} from '@ant-design/icons' +import {useDataStore} from '../store/DataStore.ts' +import {useFlowStore} from '../store/FlowStore.ts' +import type {ButtonProps} from 'antd/lib' + +export type AddNodeButtonProps = { + parentId?: string +} & ButtonProps + +const AddNodeButton = (props: AddNodeButtonProps) => { + const {data, setDataById} = useDataStore() + const {nodes, addNode, edges,} = useFlowStore() + return ( + i.group)) + .map(group => ({ + type: 'group', + label: group, + children: NodeRegistry + .filter(i => isEqual(group, i.group)) + // 循环节点里不能再嵌套循环节点 + .filter(i => !props.parentId || (props.parentId && !isEqual(i.key, 'loop-node'))) + .map(i => ({key: i.key, label: i.name, icon: i.icon})), + })), + onClick: ({key}) => { + try { + if (commonInfo.debug) { + console.info('Add', key, JSON.stringify({nodes, edges, data})) + } + checkAddNode(key, nodes, edges) + + let nodeId = randomId(10, 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM') + let define = NodeRegistryMap[key] + + setDataById( + nodeId, + { + node: { + name: define.name, + description: define.description, + }, + }, + ) + + addNode({ + id: nodeId, + type: key, + position: {x: 100, y: 130}, + data: {}, + // 如果是循环节点就将节点加入到循环节点中 + ...(props.parentId ? { + parentId: props.parentId, + extent: 'parent', + } : {}) + }) + } catch (e) { + // @ts-ignore + messageApi.error(e.toString()) + } + }, + }} + > + + + ) +} + +export default AddNodeButton diff --git a/service-web/client/src/components/flow/node/LoopNode.tsx b/service-web/client/src/components/flow/node/LoopNode.tsx index 8095e82..1611b95 100644 --- a/service-web/client/src/components/flow/node/LoopNode.tsx +++ b/service-web/client/src/components/flow/node/LoopNode.tsx @@ -3,14 +3,15 @@ import {classnames} from 'amis' import React from 'react' import {flowBackgroundColor, flowDotColor} from '../types.ts' import AmisNode, {nodeClassName, NormalNodeHandler} from './AmisNode.tsx' +import AddNodeButton from '../component/AddNodeButton.tsx' const LoopNode = (props: NodeProps) => { return ( { color={flowDotColor} bgColor={flowBackgroundColor} /> + } handler={} resize={{ - minWidth: 256, - minHeight: 208, + minWidth: 350, + minHeight: 290, }} /> ) diff --git a/service-web/client/src/pages/Test.tsx b/service-web/client/src/pages/Test.tsx index 12f6f49..dd5e7d1 100644 --- a/service-web/client/src/pages/Test.tsx +++ b/service-web/client/src/pages/Test.tsx @@ -4,7 +4,7 @@ import type {GraphData} from '../components/flow/types.ts' function Test() { // language=JSON - const [graphData] = useState(JSON.parse('{\n "nodes": [\n {\n "id": "QxNrkChBWQ",\n "type": "loop-node",\n "position": {\n "x": 742,\n "y": 119\n },\n "data": {},\n "measured": {\n "width": 458,\n "height": 368\n },\n "selected": true,\n "dragging": false,\n "width": 458,\n "height": 368,\n "resizing": false\n },\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 92\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false,\n "extent": "parent",\n "parentId": "QxNrkChBWQ"\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "string"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n },\n "QxNrkChBWQ": {\n "node": {\n "name": "循环",\n "description": "实现循环执行流程"\n },\n "finished": true\n }\n }\n}')) + const [graphData] = useState(JSON.parse('{\n "nodes": [\n {\n "id": "QxNrkChBWQ",\n "type": "loop-node",\n "position": {\n "x": 742,\n "y": 119\n },\n "data": {},\n "measured": {\n "width": 458,\n "height": 368\n },\n "selected": true,\n "dragging": false,\n "width": 458,\n "height": 368,\n "resizing": false\n },\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 135\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false,\n "extent": "parent",\n "parentId": "QxNrkChBWQ"\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "string"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n },\n "QxNrkChBWQ": {\n "node": {\n "name": "循环",\n "description": "实现循环执行流程"\n },\n "finished": true\n }\n }\n}')) return (