### 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
| </marker> | </marker> | ||||
| </defs> | </defs> | ||||
| </svg> | </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 && ( | {formDrawerVisible && ( | ||||
| <AgentInstanceContext.Provider value={{ addCanvasNode }}> | <AgentInstanceContext.Provider value={{ addCanvasNode }}> | ||||
| <FormSheet | <FormSheet |
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| style={LeftHandleStyle} | style={LeftHandleStyle} | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <CommonHandle | <CommonHandle | ||||
| type="source" | type="source" | ||||
| className={styles.handle} | className={styles.handle} | ||||
| id="b" | id="b" | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| </> | </> | ||||
| )} | )} |
| import { NodeWrapper } from './node-wrapper'; | import { NodeWrapper } from './node-wrapper'; | ||||
| // TODO: do not allow other nodes to connect to this node | // 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 { t } = useTranslation(); | ||||
| const query: BeginQuery[] = get(data, 'form.query', []); | const query: BeginQuery[] = get(data, 'form.query', []); | ||||
| isConnectable | isConnectable | ||||
| className={styles.handle} | className={styles.handle} | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <Flex align="center" justify={'center'} gap={10}> | |||||
| <section className="flex items-center justify-center gap-2"> | |||||
| <OperatorIcon name={data.label as Operator}></OperatorIcon> | <OperatorIcon name={data.label as Operator}></OperatorIcon> | ||||
| <div className="truncate text-center font-semibold text-sm"> | <div className="truncate text-center font-semibold text-sm"> | ||||
| {t(`flow.begin`)} | {t(`flow.begin`)} | ||||
| </div> | </div> | ||||
| </Flex> | |||||
| </section> | |||||
| <Flex gap={8} vertical className={styles.generateParameters}> | <Flex gap={8} vertical className={styles.generateParameters}> | ||||
| {query.map((x, idx) => { | {query.map((x, idx) => { | ||||
| const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType]; | const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType]; |
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable | isConnectable | ||||
| id={'a'} | id={'a'} | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable | isConnectable | ||||
| style={{ ...RightHandleStyle, top: position.top }} | style={{ ...RightHandleStyle, top: position.top }} | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| </div> | </div> | ||||
| ); | ); |
| 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> | |||||
| ); | |||||
| } |
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| import { Handle, HandleProps } from '@xyflow/react'; | import { Handle, HandleProps } from '@xyflow/react'; | ||||
| import { Plus } from 'lucide-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 ( | 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> | |||||
| ); | ); | ||||
| } | } |
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { IRagNode } from '@/interfaces/database/flow'; | 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 { memo } from 'react'; | ||||
| import { CommonHandle } from './handle'; | |||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | |||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| import { NodeWrapper } from './node-wrapper'; | |||||
| import { ToolBar } from './toolbar'; | import { ToolBar } from './toolbar'; | ||||
| function InnerRagNode({ | function InnerRagNode({ | ||||
| isConnectable = true, | isConnectable = true, | ||||
| selected, | selected, | ||||
| }: NodeProps<IRagNode>) { | }: NodeProps<IRagNode>) { | ||||
| const { theme } = useTheme(); | |||||
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <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" | id="c" | ||||
| type="source" | type="source" | ||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| className={styles.handle} | |||||
| style={LeftHandleStyle} | style={LeftHandleStyle} | ||||
| ></Handle> | |||||
| <Handle | |||||
| nodeId={id} | |||||
| ></CommonHandle> | |||||
| <CommonHandle | |||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| className={styles.handle} | |||||
| id="b" | id="b" | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| ></Handle> | |||||
| nodeId={id} | |||||
| ></CommonHandle> | |||||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | ||||
| </section> | |||||
| </NodeWrapper> | |||||
| </ToolBar> | </ToolBar> | ||||
| ); | ); | ||||
| } | } |
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { ILogicNode } from '@/interfaces/database/flow'; | 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 { memo } from 'react'; | ||||
| import { CommonHandle } from './handle'; | |||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | |||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| import { NodeWrapper } from './node-wrapper'; | |||||
| import { ToolBar } from './toolbar'; | |||||
| export function InnerLogicNode({ | export function InnerLogicNode({ | ||||
| id, | id, | ||||
| isConnectable = true, | isConnectable = true, | ||||
| selected, | selected, | ||||
| }: NodeProps<ILogicNode>) { | }: NodeProps<ILogicNode>) { | ||||
| const { theme } = useTheme(); | |||||
| return ( | 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> | |||||
| ); | ); | ||||
| } | } | ||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| style={LeftHandleStyle} | style={LeftHandleStyle} | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <CommonHandle | <CommonHandle | ||||
| type="source" | type="source" | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| id="b" | id="b" | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <NodeHeader | <NodeHeader | ||||
| id={id} | id={id} |
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| className={styles.handle} | className={styles.handle} | ||||
| style={LeftHandleStyle} | style={LeftHandleStyle} | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <CommonHandle | <CommonHandle | ||||
| type="source" | type="source" | ||||
| className={styles.handle} | className={styles.handle} | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| id="b" | id="b" | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| <NodeHeader | <NodeHeader | ||||
| id={id} | id={id} |
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable | isConnectable | ||||
| id={'a'} | id={'a'} | ||||
| nodeId={id} | |||||
| ></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"> | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable | isConnectable | ||||
| style={{ ...RightHandleStyle, top: position.top }} | style={{ ...RightHandleStyle, top: position.top }} | ||||
| nodeId={id} | |||||
| ></CommonHandle> | ></CommonHandle> | ||||
| </div> | </div> | ||||
| ); | ); |
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | import { RAGFlowNodeType } from '@/interfaces/database/flow'; | ||||
| import { HandleType, Position } from '@xyflow/react'; | |||||
| import { createContext } from 'react'; | import { createContext } from 'react'; | ||||
| import { useAddNode } from './hooks/use-add-node'; | import { useAddNode } from './hooks/use-add-node'; | ||||
| import { useCacheChatLog } from './hooks/use-cache-chat-log'; | import { useCacheChatLog } from './hooks/use-cache-chat-log'; | ||||
| export const AgentChatLogContext = createContext<AgentChatLogContextType>( | export const AgentChatLogContext = createContext<AgentChatLogContextType>( | ||||
| {} as AgentChatLogContextType, | {} as AgentChatLogContextType, | ||||
| ); | ); | ||||
| export type HandleContextType = { | |||||
| nodeId?: string; | |||||
| id?: string; | |||||
| type: HandleType; | |||||
| position: Position; | |||||
| }; | |||||
| export const HandleContext = createContext<HandleContextType>( | |||||
| {} as HandleContextType, | |||||
| ); |
| FormLabel, | FormLabel, | ||||
| } from '@/components/ui/form'; | } from '@/components/ui/form'; | ||||
| import { zodResolver } from '@hookform/resolvers/zod'; | import { zodResolver } from '@hookform/resolvers/zod'; | ||||
| import { Position } from '@xyflow/react'; | |||||
| import { useContext, useMemo } from 'react'; | import { useContext, useMemo } from 'react'; | ||||
| import { useForm } from 'react-hook-form'; | import { useForm } from 'react-hook-form'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| )} | )} | ||||
| /> | /> | ||||
| </FormContainer> | </FormContainer> | ||||
| <BlockButton onClick={addCanvasNode(Operator.Agent, node?.id)}> | |||||
| <BlockButton | |||||
| onClick={addCanvasNode(Operator.Agent, { | |||||
| id: node?.id, | |||||
| position: Position.Bottom, | |||||
| })} | |||||
| > | |||||
| Add Agent | Add Agent | ||||
| </BlockButton> | </BlockButton> | ||||
| <Output list={outputList}></Output> | <Output list={outputList}></Output> |
| }; | }; | ||||
| }; | }; | ||||
| 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>) { | 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 edges = useGraphStore((state) => state.edges); | const edges = useGraphStore((state) => state.edges); | ||||
| const getNodeName = useGetNodeName(); | const getNodeName = useGetNodeName(); | ||||
| const initializeOperatorParams = useInitializeOperatorParams(); | const initializeOperatorParams = useInitializeOperatorParams(); | ||||
| const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); | |||||
| // const [reactFlowInstance, setReactFlowInstance] = | // const [reactFlowInstance, setReactFlowInstance] = | ||||
| // useState<ReactFlowInstance<any, any>>(); | // useState<ReactFlowInstance<any, any>>(); | ||||
| const addCanvasNode = useCallback( | 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: { | 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, | addEdge, | ||||
| addNode, | addNode, |