### What problem does this PR solve? Feat: Add a child operator node by clicking the operator node anchor point #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| @@ -149,38 +149,40 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { | |||
| </marker> | |||
| </defs> | |||
| </svg> | |||
| <ReactFlow | |||
| connectionMode={ConnectionMode.Loose} | |||
| nodes={nodes} | |||
| onNodesChange={onNodesChange} | |||
| edges={edges} | |||
| onEdgesChange={onEdgesChange} | |||
| fitView | |||
| onConnect={onConnect} | |||
| nodeTypes={nodeTypes} | |||
| edgeTypes={edgeTypes} | |||
| onDrop={onDrop} | |||
| onDragOver={onDragOver} | |||
| onNodeClick={onNodeClick} | |||
| onPaneClick={onPaneClick} | |||
| onInit={setReactFlowInstance} | |||
| onSelectionChange={onSelectionChange} | |||
| nodeOrigin={[0.5, 0]} | |||
| isValidConnection={isValidConnection} | |||
| defaultEdgeOptions={{ | |||
| type: 'buttonEdge', | |||
| markerEnd: 'logo', | |||
| style: { | |||
| strokeWidth: 2, | |||
| stroke: 'rgb(202 197 245)', | |||
| }, | |||
| zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 | |||
| }} | |||
| deleteKeyCode={['Delete', 'Backspace']} | |||
| onBeforeDelete={handleBeforeDelete} | |||
| > | |||
| <Background /> | |||
| </ReactFlow> | |||
| <AgentInstanceContext.Provider value={{ addCanvasNode }}> | |||
| <ReactFlow | |||
| connectionMode={ConnectionMode.Loose} | |||
| nodes={nodes} | |||
| onNodesChange={onNodesChange} | |||
| edges={edges} | |||
| onEdgesChange={onEdgesChange} | |||
| fitView | |||
| onConnect={onConnect} | |||
| nodeTypes={nodeTypes} | |||
| edgeTypes={edgeTypes} | |||
| onDrop={onDrop} | |||
| onDragOver={onDragOver} | |||
| onNodeClick={onNodeClick} | |||
| onPaneClick={onPaneClick} | |||
| onInit={setReactFlowInstance} | |||
| onSelectionChange={onSelectionChange} | |||
| nodeOrigin={[0.5, 0]} | |||
| isValidConnection={isValidConnection} | |||
| defaultEdgeOptions={{ | |||
| type: 'buttonEdge', | |||
| markerEnd: 'logo', | |||
| style: { | |||
| strokeWidth: 2, | |||
| stroke: 'rgb(202 197 245)', | |||
| }, | |||
| zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 | |||
| }} | |||
| deleteKeyCode={['Delete', 'Backspace']} | |||
| onBeforeDelete={handleBeforeDelete} | |||
| > | |||
| <Background /> | |||
| </ReactFlow> | |||
| </AgentInstanceContext.Provider> | |||
| {formDrawerVisible && ( | |||
| <AgentInstanceContext.Provider value={{ addCanvasNode }}> | |||
| <FormSheet | |||
| @@ -36,6 +36,7 @@ function InnerAgentNode({ | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| style={LeftHandleStyle} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| @@ -44,6 +45,7 @@ function InnerAgentNode({ | |||
| className={styles.handle} | |||
| id="b" | |||
| style={RightHandleStyle} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| </> | |||
| )} | |||
| @@ -17,7 +17,7 @@ import styles from './index.less'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| // TODO: do not allow other nodes to connect to this node | |||
| function InnerBeginNode({ data }: NodeProps<IBeginNode>) { | |||
| function InnerBeginNode({ data, id }: NodeProps<IBeginNode>) { | |||
| const { t } = useTranslation(); | |||
| const query: BeginQuery[] = get(data, 'form.query', []); | |||
| @@ -29,14 +29,15 @@ function InnerBeginNode({ data }: NodeProps<IBeginNode>) { | |||
| isConnectable | |||
| className={styles.handle} | |||
| style={RightHandleStyle} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <Flex align="center" justify={'center'} gap={10}> | |||
| <section className="flex items-center justify-center gap-2"> | |||
| <OperatorIcon name={data.label as Operator}></OperatorIcon> | |||
| <div className="truncate text-center font-semibold text-sm"> | |||
| {t(`flow.begin`)} | |||
| </div> | |||
| </Flex> | |||
| </section> | |||
| <Flex gap={8} vertical className={styles.generateParameters}> | |||
| {query.map((x, idx) => { | |||
| const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType]; | |||
| @@ -24,6 +24,7 @@ export function InnerCategorizeNode({ | |||
| position={Position.Left} | |||
| isConnectable | |||
| id={'a'} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| @@ -45,6 +46,7 @@ export function InnerCategorizeNode({ | |||
| position={Position.Right} | |||
| isConnectable | |||
| style={{ ...RightHandleStyle, top: position.top }} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| </div> | |||
| ); | |||
| @@ -0,0 +1,111 @@ | |||
| import { | |||
| Accordion, | |||
| AccordionContent, | |||
| AccordionItem, | |||
| AccordionTrigger, | |||
| } from '@/components/ui/accordion'; | |||
| import { | |||
| DropdownMenu, | |||
| DropdownMenuContent, | |||
| DropdownMenuItem, | |||
| DropdownMenuLabel, | |||
| DropdownMenuTrigger, | |||
| } from '@/components/ui/dropdown-menu'; | |||
| import { Operator } from '@/pages/agent/constant'; | |||
| import { AgentInstanceContext, HandleContext } from '@/pages/agent/context'; | |||
| import OperatorIcon from '@/pages/agent/operator-icon'; | |||
| import { PropsWithChildren, useContext } from 'react'; | |||
| type OperatorItemProps = { operators: Operator[] }; | |||
| function OperatorItemList({ operators }: OperatorItemProps) { | |||
| const { addCanvasNode } = useContext(AgentInstanceContext); | |||
| const { nodeId, id, type, position } = useContext(HandleContext); | |||
| return ( | |||
| <ul className="space-y-2"> | |||
| {operators.map((x) => { | |||
| return ( | |||
| <DropdownMenuItem | |||
| key={x} | |||
| className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start" | |||
| onClick={addCanvasNode(x, { | |||
| id: nodeId, | |||
| sourceHandle: id, | |||
| position, | |||
| })} | |||
| > | |||
| <OperatorIcon name={x}></OperatorIcon> | |||
| {x} | |||
| </DropdownMenuItem> | |||
| ); | |||
| })} | |||
| </ul> | |||
| ); | |||
| } | |||
| function AccordionOperators() { | |||
| return ( | |||
| <Accordion | |||
| type="multiple" | |||
| className="px-2 text-text-title" | |||
| defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']} | |||
| > | |||
| <AccordionItem value="item-1"> | |||
| <AccordionTrigger className="text-xl">AI</AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList | |||
| operators={[Operator.Agent, Operator.Retrieval]} | |||
| ></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| <AccordionItem value="item-2"> | |||
| <AccordionTrigger className="text-xl">Dialogue </AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList operators={[Operator.Message]}></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| <AccordionItem value="item-3"> | |||
| <AccordionTrigger className="text-xl">Flow</AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList | |||
| operators={[ | |||
| Operator.Switch, | |||
| Operator.Iteration, | |||
| Operator.Categorize, | |||
| ]} | |||
| ></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| <AccordionItem value="item-4"> | |||
| <AccordionTrigger className="text-xl"> | |||
| Data Manipulation | |||
| </AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList operators={[Operator.Code]}></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| <AccordionItem value="item-5"> | |||
| <AccordionTrigger className="text-xl">Tools</AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList operators={[]}></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| </Accordion> | |||
| ); | |||
| } | |||
| export function NextStepDropdown({ children }: PropsWithChildren) { | |||
| return ( | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger> | |||
| <DropdownMenuContent | |||
| onClick={(e) => e.stopPropagation()} | |||
| className="w-[300px] font-semibold" | |||
| > | |||
| <DropdownMenuLabel>Next Step</DropdownMenuLabel> | |||
| <AccordionOperators></AccordionOperators> | |||
| </DropdownMenuContent> | |||
| </DropdownMenu> | |||
| ); | |||
| } | |||
| @@ -1,17 +1,41 @@ | |||
| import { cn } from '@/lib/utils'; | |||
| import { Handle, HandleProps } from '@xyflow/react'; | |||
| import { Plus } from 'lucide-react'; | |||
| import { useMemo } from 'react'; | |||
| import { HandleContext } from '../../context'; | |||
| import { NextStepDropdown } from './dropdown/next-step-dropdown'; | |||
| export function CommonHandle({ | |||
| className, | |||
| nodeId, | |||
| ...props | |||
| }: HandleProps & { nodeId: string }) { | |||
| const value = useMemo( | |||
| () => ({ | |||
| nodeId, | |||
| id: props.id, | |||
| type: props.type, | |||
| position: props.position, | |||
| }), | |||
| [nodeId, props.id, props.position, props.type], | |||
| ); | |||
| export function CommonHandle({ className, ...props }: HandleProps) { | |||
| return ( | |||
| <Handle | |||
| {...props} | |||
| className={cn( | |||
| 'inline-flex justify-center items-center !bg-background-checked !size-4 !rounded-sm !border-none ', | |||
| className, | |||
| )} | |||
| > | |||
| <Plus className="size-3 pointer-events-none" /> | |||
| </Handle> | |||
| <HandleContext.Provider value={value}> | |||
| <NextStepDropdown> | |||
| <Handle | |||
| {...props} | |||
| className={cn( | |||
| 'inline-flex justify-center items-center !bg-background-checked !size-4 !rounded-sm !border-none ', | |||
| className, | |||
| )} | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| }} | |||
| > | |||
| <Plus className="size-3 pointer-events-none" /> | |||
| </Handle> | |||
| </NextStepDropdown> | |||
| </HandleContext.Provider> | |||
| ); | |||
| } | |||
| @@ -1,11 +1,10 @@ | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { IRagNode } from '@/interfaces/database/flow'; | |||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||
| import classNames from 'classnames'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { memo } from 'react'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| import NodeHeader from './node-header'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| import { ToolBar } from './toolbar'; | |||
| function InnerRagNode({ | |||
| @@ -14,36 +13,27 @@ function InnerRagNode({ | |||
| isConnectable = true, | |||
| selected, | |||
| }: NodeProps<IRagNode>) { | |||
| const { theme } = useTheme(); | |||
| return ( | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <section | |||
| className={classNames( | |||
| styles.ragNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [styles.selectedNode]: selected, | |||
| }, | |||
| )} | |||
| > | |||
| <Handle | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={LeftHandleStyle} | |||
| ></Handle> | |||
| <Handle | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| id="b" | |||
| style={RightHandleStyle} | |||
| ></Handle> | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| </section> | |||
| </NodeWrapper> | |||
| </ToolBar> | |||
| ); | |||
| } | |||
| @@ -1,11 +1,11 @@ | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { ILogicNode } from '@/interfaces/database/flow'; | |||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||
| import classNames from 'classnames'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { memo } from 'react'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| import NodeHeader from './node-header'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| import { ToolBar } from './toolbar'; | |||
| export function InnerLogicNode({ | |||
| id, | |||
| @@ -13,35 +13,28 @@ export function InnerLogicNode({ | |||
| isConnectable = true, | |||
| selected, | |||
| }: NodeProps<ILogicNode>) { | |||
| const { theme } = useTheme(); | |||
| return ( | |||
| <section | |||
| className={classNames( | |||
| styles.logicNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [styles.selectedNode]: selected, | |||
| }, | |||
| )} | |||
| > | |||
| <Handle | |||
| id="c" | |||
| type="source" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={LeftHandleStyle} | |||
| ></Handle> | |||
| <Handle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={RightHandleStyle} | |||
| id="b" | |||
| ></Handle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| </section> | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| style={LeftHandleStyle} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| style={RightHandleStyle} | |||
| id="b" | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| </NodeWrapper> | |||
| </ToolBar> | |||
| ); | |||
| } | |||
| @@ -27,6 +27,7 @@ function InnerMessageNode({ | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| style={LeftHandleStyle} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| @@ -34,6 +35,7 @@ function InnerMessageNode({ | |||
| isConnectable={isConnectable} | |||
| style={RightHandleStyle} | |||
| id="b" | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <NodeHeader | |||
| id={id} | |||
| @@ -42,6 +42,7 @@ function InnerRetrievalNode({ | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={LeftHandleStyle} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| @@ -50,6 +51,7 @@ function InnerRetrievalNode({ | |||
| className={styles.handle} | |||
| style={RightHandleStyle} | |||
| id="b" | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <NodeHeader | |||
| id={id} | |||
| @@ -66,6 +66,7 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | |||
| position={Position.Left} | |||
| isConnectable | |||
| id={'a'} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| <section className="gap-2.5 flex flex-col"> | |||
| @@ -94,6 +95,7 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | |||
| position={Position.Right} | |||
| isConnectable | |||
| style={{ ...RightHandleStyle, top: position.top }} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| </div> | |||
| ); | |||
| @@ -1,4 +1,5 @@ | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { HandleType, Position } from '@xyflow/react'; | |||
| import { createContext } from 'react'; | |||
| import { useAddNode } from './hooks/use-add-node'; | |||
| import { useCacheChatLog } from './hooks/use-cache-chat-log'; | |||
| @@ -34,3 +35,14 @@ type AgentChatLogContextType = Pick< | |||
| export const AgentChatLogContext = createContext<AgentChatLogContextType>( | |||
| {} as AgentChatLogContextType, | |||
| ); | |||
| export type HandleContextType = { | |||
| nodeId?: string; | |||
| id?: string; | |||
| type: HandleType; | |||
| position: Position; | |||
| }; | |||
| export const HandleContext = createContext<HandleContextType>( | |||
| {} as HandleContextType, | |||
| ); | |||
| @@ -11,6 +11,7 @@ import { | |||
| FormLabel, | |||
| } from '@/components/ui/form'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { Position } from '@xyflow/react'; | |||
| import { useContext, useMemo } from 'react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| @@ -109,7 +110,12 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| )} | |||
| /> | |||
| </FormContainer> | |||
| <BlockButton onClick={addCanvasNode(Operator.Agent, node?.id)}> | |||
| <BlockButton | |||
| onClick={addCanvasNode(Operator.Agent, { | |||
| id: node?.id, | |||
| position: Position.Bottom, | |||
| })} | |||
| > | |||
| Add Agent | |||
| </BlockButton> | |||
| <Output list={outputList}></Output> | |||
| @@ -124,6 +124,39 @@ export const useGetNodeName = () => { | |||
| }; | |||
| }; | |||
| export function useCalculateNewlyChildPosition() { | |||
| const getNode = useGraphStore((state) => state.getNode); | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| const edges = useGraphStore((state) => state.edges); | |||
| const calculateNewlyBackChildPosition = useCallback( | |||
| (id?: string, sourceHandle?: string) => { | |||
| const parentNode = getNode(id); | |||
| // Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes | |||
| const allChildNodeIds = edges | |||
| .filter((x) => x.source === id && x.sourceHandle === sourceHandle) | |||
| .map((x) => x.target); | |||
| const yAxises = nodes | |||
| .filter((x) => allChildNodeIds.some((y) => y === x.id)) | |||
| .map((x) => x.position.y); | |||
| const maxY = Math.max(...yAxises); | |||
| const position = { | |||
| y: yAxises.length > 0 ? maxY + 262 : (parentNode?.position.y || 0) + 82, | |||
| x: (parentNode?.position.x || 0) + 140, | |||
| }; | |||
| return position; | |||
| }, | |||
| [edges, getNode, nodes], | |||
| ); | |||
| return { calculateNewlyBackChildPosition }; | |||
| } | |||
| export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| const addNode = useGraphStore((state) => state.addNode); | |||
| const getNode = useGraphStore((state) => state.getNode); | |||
| @@ -132,95 +165,111 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| const edges = useGraphStore((state) => state.edges); | |||
| const getNodeName = useGetNodeName(); | |||
| const initializeOperatorParams = useInitializeOperatorParams(); | |||
| const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); | |||
| // const [reactFlowInstance, setReactFlowInstance] = | |||
| // useState<ReactFlowInstance<any, any>>(); | |||
| const addCanvasNode = useCallback( | |||
| (type: string, id?: string) => (event: React.MouseEvent<HTMLElement>) => { | |||
| // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition | |||
| // and you don't need to subtract the reactFlowBounds.left/top anymore | |||
| // details: https://@xyflow/react.dev/whats-new/2023-11-10 | |||
| const position = reactFlowInstance?.screenToFlowPosition({ | |||
| x: event.clientX, | |||
| y: event.clientY, | |||
| }); | |||
| const newNode: Node<any> = { | |||
| id: `${type}:${humanId()}`, | |||
| type: NodeMap[type as Operator] || 'ragNode', | |||
| position: position || { | |||
| x: 0, | |||
| y: 0, | |||
| }, | |||
| data: { | |||
| label: `${type}`, | |||
| name: generateNodeNamesWithIncreasingIndex(getNodeName(type), nodes), | |||
| form: initializeOperatorParams(type as Operator), | |||
| }, | |||
| sourcePosition: Position.Right, | |||
| targetPosition: Position.Left, | |||
| dragHandle: getNodeDragHandle(type), | |||
| }; | |||
| ( | |||
| type: string, | |||
| params: { id?: string; position?: Position; sourceHandle?: string } = { | |||
| position: Position.Right, | |||
| }, | |||
| ) => | |||
| (event: React.MouseEvent<HTMLElement>) => { | |||
| const id = params.id; | |||
| if (type === Operator.Iteration) { | |||
| newNode.width = 500; | |||
| newNode.height = 250; | |||
| const iterationStartNode: Node<any> = { | |||
| id: `${Operator.IterationStart}:${humanId()}`, | |||
| type: 'iterationStartNode', | |||
| position: { x: 50, y: 100 }, | |||
| // draggable: false, | |||
| // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition | |||
| // and you don't need to subtract the reactFlowBounds.left/top anymore | |||
| // details: https://@xyflow/react.dev/whats-new/2023-11-10 | |||
| let position = reactFlowInstance?.screenToFlowPosition({ | |||
| x: event.clientX, | |||
| y: event.clientY, | |||
| }); | |||
| if (params.position === Position.Right) { | |||
| position = calculateNewlyBackChildPosition(id, params.sourceHandle); | |||
| } | |||
| const newNode: Node<any> = { | |||
| id: `${type}:${humanId()}`, | |||
| type: NodeMap[type as Operator] || 'ragNode', | |||
| position: position || { | |||
| x: 0, | |||
| y: 0, | |||
| }, | |||
| data: { | |||
| label: Operator.IterationStart, | |||
| name: Operator.IterationStart, | |||
| form: {}, | |||
| label: `${type}`, | |||
| name: generateNodeNamesWithIncreasingIndex( | |||
| getNodeName(type), | |||
| nodes, | |||
| ), | |||
| form: initializeOperatorParams(type as Operator), | |||
| }, | |||
| parentId: newNode.id, | |||
| extent: 'parent', | |||
| sourcePosition: Position.Right, | |||
| targetPosition: Position.Left, | |||
| dragHandle: getNodeDragHandle(type), | |||
| }; | |||
| addNode(newNode); | |||
| addNode(iterationStartNode); | |||
| } else if (type === Operator.Agent) { | |||
| const agentNode = getNode(id); | |||
| if (agentNode) { | |||
| // Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes | |||
| const allChildAgentNodeIds = edges | |||
| .filter((x) => x.source === id && x.sourceHandle === 'e') | |||
| .map((x) => x.target); | |||
| const xAxises = nodes | |||
| .filter((x) => allChildAgentNodeIds.some((y) => y === x.id)) | |||
| .map((x) => x.position.x); | |||
| const maxX = Math.max(...xAxises); | |||
| newNode.position = { | |||
| x: xAxises.length > 0 ? maxX + 262 : agentNode.position.x + 82, | |||
| y: agentNode.position.y + 140, | |||
| if (type === Operator.Iteration) { | |||
| newNode.width = 500; | |||
| newNode.height = 250; | |||
| const iterationStartNode: Node<any> = { | |||
| id: `${Operator.IterationStart}:${humanId()}`, | |||
| type: 'iterationStartNode', | |||
| position: { x: 50, y: 100 }, | |||
| // draggable: false, | |||
| data: { | |||
| label: Operator.IterationStart, | |||
| name: Operator.IterationStart, | |||
| form: {}, | |||
| }, | |||
| parentId: newNode.id, | |||
| extent: 'parent', | |||
| }; | |||
| addNode(newNode); | |||
| addNode(iterationStartNode); | |||
| } else if (type === Operator.Agent) { | |||
| const agentNode = getNode(id); | |||
| if (agentNode) { | |||
| // Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes | |||
| const allChildAgentNodeIds = edges | |||
| .filter((x) => x.source === id && x.sourceHandle === 'e') | |||
| .map((x) => x.target); | |||
| const xAxises = nodes | |||
| .filter((x) => allChildAgentNodeIds.some((y) => y === x.id)) | |||
| .map((x) => x.position.x); | |||
| const maxX = Math.max(...xAxises); | |||
| newNode.position = { | |||
| x: xAxises.length > 0 ? maxX + 262 : agentNode.position.x + 82, | |||
| y: agentNode.position.y + 140, | |||
| }; | |||
| } | |||
| addNode(newNode); | |||
| if (id) { | |||
| addEdge({ | |||
| source: id, | |||
| target: newNode.id, | |||
| sourceHandle: 'e', | |||
| targetHandle: 'f', | |||
| }); | |||
| } | |||
| } else { | |||
| const subNodeOfIteration = getRelativePositionToIterationNode( | |||
| nodes, | |||
| position, | |||
| ); | |||
| if (subNodeOfIteration) { | |||
| newNode.parentId = subNodeOfIteration.parentId; | |||
| newNode.position = subNodeOfIteration.position; | |||
| newNode.extent = 'parent'; | |||
| } | |||
| addNode(newNode); | |||
| } | |||
| addNode(newNode); | |||
| if (id) { | |||
| addEdge({ | |||
| source: id, | |||
| target: newNode.id, | |||
| sourceHandle: 'e', | |||
| targetHandle: 'f', | |||
| }); | |||
| } | |||
| } else { | |||
| const subNodeOfIteration = getRelativePositionToIterationNode( | |||
| nodes, | |||
| position, | |||
| ); | |||
| if (subNodeOfIteration) { | |||
| newNode.parentId = subNodeOfIteration.parentId; | |||
| newNode.position = subNodeOfIteration.position; | |||
| newNode.extent = 'parent'; | |||
| } | |||
| addNode(newNode); | |||
| } | |||
| }, | |||
| }, | |||
| [ | |||
| addEdge, | |||
| addNode, | |||