### What problem does this PR solve? Feat: Add child nodes and their connecting lines by clicking #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| import { IAgentNode } from '@/interfaces/database/flow'; | import { IAgentNode } from '@/interfaces/database/flow'; | ||||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | import { Handle, NodeProps, Position } from '@xyflow/react'; | ||||
| import { memo, useMemo } from 'react'; | import { memo, useMemo } from 'react'; | ||||
| import { Operator } from '../../constant'; | |||||
| import { NodeHandleId, Operator } from '../../constant'; | |||||
| import useGraphStore from '../../store'; | import useGraphStore from '../../store'; | ||||
| import { CommonHandle } from './handle'; | import { CommonHandle } from './handle'; | ||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| {isNotParentAgent && ( | {isNotParentAgent && ( | ||||
| <> | <> | ||||
| <CommonHandle | <CommonHandle | ||||
| id="c" | |||||
| type="source" | |||||
| type="target" | |||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| style={LeftHandleStyle} | style={LeftHandleStyle} | ||||
| nodeId={id} | nodeId={id} | ||||
| id={NodeHandleId.End} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <CommonHandle | <CommonHandle | ||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| className={styles.handle} | className={styles.handle} | ||||
| id="b" | |||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| nodeId={id} | nodeId={id} | ||||
| id={NodeHandleId.Start} | |||||
| isConnectableEnd={false} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| </> | </> | ||||
| )} | )} |
| import { | import { | ||||
| BeginQueryType, | BeginQueryType, | ||||
| BeginQueryTypeIconMap, | BeginQueryTypeIconMap, | ||||
| NodeHandleId, | |||||
| Operator, | Operator, | ||||
| } from '../../constant'; | } from '../../constant'; | ||||
| import { BeginQuery } from '../../interface'; | import { BeginQuery } from '../../interface'; | ||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable | isConnectable | ||||
| className={styles.handle} | |||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| nodeId={id} | nodeId={id} | ||||
| id={NodeHandleId.Start} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <section className="flex items-center justify-center gap-2"> | <section className="flex items-center justify-center gap-2"> |
| import { NodeProps, Position } from '@xyflow/react'; | import { NodeProps, Position } from '@xyflow/react'; | ||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import { memo } from 'react'; | import { memo } from 'react'; | ||||
| import { NodeHandleId } from '../../constant'; | |||||
| import { CommonHandle } from './handle'; | import { CommonHandle } from './handle'; | ||||
| import { RightHandleStyle } from './handle-icon'; | import { RightHandleStyle } from './handle-icon'; | ||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| type="target" | type="target" | ||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable | isConnectable | ||||
| id={'a'} | |||||
| id={NodeHandleId.End} | |||||
| nodeId={id} | nodeId={id} | ||||
| ></CommonHandle> | ></CommonHandle> | ||||
| isConnectable | isConnectable | ||||
| style={{ ...RightHandleStyle, top: position.top }} | style={{ ...RightHandleStyle, top: position.top }} | ||||
| nodeId={id} | nodeId={id} | ||||
| isConnectableEnd={false} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| </div> | </div> | ||||
| ); | ); |
| key={x} | key={x} | ||||
| className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start" | className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start" | ||||
| onClick={addCanvasNode(x, { | onClick={addCanvasNode(x, { | ||||
| id: nodeId, | |||||
| sourceHandle: id, | |||||
| nodeId, | |||||
| id, | |||||
| position, | position, | ||||
| })} | })} | ||||
| > | > |
| import { IRagNode } from '@/interfaces/database/flow'; | import { IRagNode } from '@/interfaces/database/flow'; | ||||
| import { NodeProps, Position } from '@xyflow/react'; | import { NodeProps, Position } from '@xyflow/react'; | ||||
| import { memo } from 'react'; | import { memo } from 'react'; | ||||
| import { NodeHandleId } from '../../constant'; | |||||
| import { CommonHandle } from './handle'; | import { CommonHandle } from './handle'; | ||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | <NodeWrapper> | ||||
| <CommonHandle | <CommonHandle | ||||
| id="c" | |||||
| type="source" | |||||
| id={NodeHandleId.End} | |||||
| type="target" | |||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| style={LeftHandleStyle} | style={LeftHandleStyle} | ||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| id="b" | |||||
| id={NodeHandleId.Start} | |||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| nodeId={id} | nodeId={id} | ||||
| isConnectableEnd={false} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | ||||
| </NodeWrapper> | </NodeWrapper> |
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import { memo } from 'react'; | import { memo } from 'react'; | ||||
| import { NodeHandleId } from '../../constant'; | |||||
| import { CommonHandle } from './handle'; | import { CommonHandle } from './handle'; | ||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | <NodeWrapper> | ||||
| <CommonHandle | <CommonHandle | ||||
| id="c" | |||||
| type="source" | |||||
| type="target" | |||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| style={LeftHandleStyle} | style={LeftHandleStyle} | ||||
| nodeId={id} | nodeId={id} | ||||
| id={NodeHandleId.End} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <CommonHandle | <CommonHandle | ||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| id="b" | |||||
| id={NodeHandleId.Start} | |||||
| nodeId={id} | nodeId={id} | ||||
| isConnectableEnd={false} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <NodeHeader | <NodeHeader | ||||
| id={id} | id={id} |
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import { memo, useMemo } from 'react'; | import { memo, useMemo } from 'react'; | ||||
| import { NodeHandleId } from '../../constant'; | |||||
| import { CommonHandle } from './handle'; | import { CommonHandle } from './handle'; | ||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | <NodeWrapper> | ||||
| <CommonHandle | <CommonHandle | ||||
| id="c" | |||||
| type="source" | |||||
| id={NodeHandleId.End} | |||||
| type="target" | |||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| className={styles.handle} | className={styles.handle} | ||||
| nodeId={id} | nodeId={id} | ||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <CommonHandle | <CommonHandle | ||||
| id={NodeHandleId.Start} | |||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| className={styles.handle} | className={styles.handle} | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| id="b" | |||||
| nodeId={id} | nodeId={id} | ||||
| isConnectableEnd={false} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <NodeHeader | <NodeHeader | ||||
| id={id} | id={id} |
| import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow'; | import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow'; | ||||
| import { NodeProps, Position } from '@xyflow/react'; | import { NodeProps, Position } from '@xyflow/react'; | ||||
| import { memo, useCallback } from 'react'; | import { memo, useCallback } from 'react'; | ||||
| import { SwitchOperatorOptions } from '../../constant'; | |||||
| import { NodeHandleId, SwitchOperatorOptions } from '../../constant'; | |||||
| import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; | import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; | ||||
| import { CommonHandle } from './handle'; | import { CommonHandle } from './handle'; | ||||
| import { RightHandleStyle } from './handle-icon'; | import { RightHandleStyle } from './handle-icon'; | ||||
| type="target" | type="target" | ||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable | isConnectable | ||||
| id={'a'} | |||||
| nodeId={id} | nodeId={id} | ||||
| id={NodeHandleId.End} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | ||||
| <section className="gap-2.5 flex flex-col"> | <section className="gap-2.5 flex flex-col"> | ||||
| isConnectable | isConnectable | ||||
| style={{ ...RightHandleStyle, top: position.top }} | style={{ ...RightHandleStyle, top: position.top }} | ||||
| nodeId={id} | nodeId={id} | ||||
| isConnectableEnd={false} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| </div> | </div> | ||||
| ); | ); |
| Operator.Switch, | Operator.Switch, | ||||
| Operator.Iteration, | Operator.Iteration, | ||||
| ]; | ]; | ||||
| export enum NodeHandleId { | |||||
| Start = 'start', | |||||
| End = 'end', | |||||
| } |
| </FormContainer> | </FormContainer> | ||||
| <BlockButton | <BlockButton | ||||
| onClick={addCanvasNode(Operator.Agent, { | onClick={addCanvasNode(Operator.Agent, { | ||||
| id: node?.id, | |||||
| nodeId: node?.id, | |||||
| position: Position.Bottom, | position: Position.Bottom, | ||||
| })} | })} | ||||
| > | > |
| import { useFetchModelId } from '@/hooks/logic-hooks'; | import { useFetchModelId } from '@/hooks/logic-hooks'; | ||||
| import { Node, Position, ReactFlowInstance } from '@xyflow/react'; | |||||
| import { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react'; | |||||
| import humanId from 'human-id'; | import humanId from 'human-id'; | ||||
| import { lowerFirst } from 'lodash'; | import { lowerFirst } from 'lodash'; | ||||
| import { useCallback, useMemo } from 'react'; | import { useCallback, useMemo } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { | import { | ||||
| NodeHandleId, | |||||
| NodeMap, | NodeMap, | ||||
| Operator, | Operator, | ||||
| initialAgentValues, | initialAgentValues, | ||||
| const maxY = Math.max(...yAxises); | const maxY = Math.max(...yAxises); | ||||
| const position = { | const position = { | ||||
| y: yAxises.length > 0 ? maxY + 262 : (parentNode?.position.y || 0) + 82, | |||||
| x: (parentNode?.position.x || 0) + 140, | |||||
| y: yAxises.length > 0 ? maxY + 150 : parentNode?.position.y || 0, | |||||
| x: (parentNode?.position.x || 0) + 300, | |||||
| }; | }; | ||||
| return position; | return position; | ||||
| return { calculateNewlyBackChildPosition }; | return { calculateNewlyBackChildPosition }; | ||||
| } | } | ||||
| function useAddChildEdge() { | |||||
| const addEdge = useGraphStore((state) => state.addEdge); | |||||
| const addChildEdge = useCallback( | |||||
| (position: Position = Position.Right, edge: Partial<Connection>) => { | |||||
| if ( | |||||
| position === Position.Right && | |||||
| edge.source && | |||||
| edge.target && | |||||
| edge.sourceHandle | |||||
| ) { | |||||
| addEdge({ | |||||
| source: edge.source, | |||||
| target: edge.target, | |||||
| sourceHandle: edge.sourceHandle, | |||||
| targetHandle: NodeHandleId.End, | |||||
| }); | |||||
| } | |||||
| }, | |||||
| [addEdge], | |||||
| ); | |||||
| return { addChildEdge }; | |||||
| } | |||||
| export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | ||||
| const addNode = useGraphStore((state) => state.addNode); | const addNode = useGraphStore((state) => state.addNode); | ||||
| const getNode = useGraphStore((state) => state.getNode); | const getNode = useGraphStore((state) => state.getNode); | ||||
| const getNodeName = useGetNodeName(); | const getNodeName = useGetNodeName(); | ||||
| const initializeOperatorParams = useInitializeOperatorParams(); | const initializeOperatorParams = useInitializeOperatorParams(); | ||||
| const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); | const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); | ||||
| const { addChildEdge } = useAddChildEdge(); | |||||
| // const [reactFlowInstance, setReactFlowInstance] = | // const [reactFlowInstance, setReactFlowInstance] = | ||||
| // useState<ReactFlowInstance<any, any>>(); | // useState<ReactFlowInstance<any, any>>(); | ||||
| const addCanvasNode = useCallback( | const addCanvasNode = useCallback( | ||||
| ( | ( | ||||
| type: string, | type: string, | ||||
| params: { id?: string; position?: Position; sourceHandle?: string } = { | |||||
| params: { nodeId?: string; position: Position; id?: string } = { | |||||
| position: Position.Right, | position: Position.Right, | ||||
| }, | }, | ||||
| ) => | ) => | ||||
| (event: React.MouseEvent<HTMLElement>) => { | (event: React.MouseEvent<HTMLElement>) => { | ||||
| const id = params.id; | |||||
| const nodeId = params.nodeId; | |||||
| // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition | // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition | ||||
| // and you don't need to subtract the reactFlowBounds.left/top anymore | // and you don't need to subtract the reactFlowBounds.left/top anymore | ||||
| }); | }); | ||||
| if (params.position === Position.Right) { | if (params.position === Position.Right) { | ||||
| position = calculateNewlyBackChildPosition(id, params.sourceHandle); | |||||
| position = calculateNewlyBackChildPosition(nodeId, params.id); | |||||
| } | } | ||||
| const newNode: Node<any> = { | const newNode: Node<any> = { | ||||
| }; | }; | ||||
| addNode(newNode); | addNode(newNode); | ||||
| addNode(iterationStartNode); | addNode(iterationStartNode); | ||||
| } else if (type === Operator.Agent) { | |||||
| const agentNode = getNode(id); | |||||
| } else if ( | |||||
| type === Operator.Agent && | |||||
| params.position === Position.Bottom | |||||
| ) { | |||||
| const agentNode = getNode(nodeId); | |||||
| if (agentNode) { | if (agentNode) { | ||||
| // Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes | // Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes | ||||
| const allChildAgentNodeIds = edges | const allChildAgentNodeIds = edges | ||||
| .filter((x) => x.source === id && x.sourceHandle === 'e') | |||||
| .filter((x) => x.source === nodeId && x.sourceHandle === 'e') | |||||
| .map((x) => x.target); | .map((x) => x.target); | ||||
| const xAxises = nodes | const xAxises = nodes | ||||
| }; | }; | ||||
| } | } | ||||
| addNode(newNode); | addNode(newNode); | ||||
| if (id) { | |||||
| if (nodeId) { | |||||
| addEdge({ | addEdge({ | ||||
| source: id, | |||||
| source: nodeId, | |||||
| target: newNode.id, | target: newNode.id, | ||||
| sourceHandle: 'e', | sourceHandle: 'e', | ||||
| targetHandle: 'f', | targetHandle: 'f', | ||||
| newNode.extent = 'parent'; | newNode.extent = 'parent'; | ||||
| } | } | ||||
| addNode(newNode); | addNode(newNode); | ||||
| addChildEdge(params.position, { | |||||
| source: params.nodeId, | |||||
| target: newNode.id, | |||||
| sourceHandle: params.id, | |||||
| }); | |||||
| } | } | ||||
| }, | }, | ||||
| [ | [ | ||||
| addChildEdge, | |||||
| addEdge, | addEdge, | ||||
| addNode, | addNode, | ||||
| calculateNewlyBackChildPosition, | |||||
| edges, | edges, | ||||
| getNode, | getNode, | ||||
| getNodeName, | getNodeName, |