Feat: Add a child operator node by clicking the operator node anchor point #3221 (#8309)

### 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)
This commit is contained in:
balibabu
2025-06-17 11:57:07 +08:00
committed by GitHub
parent a9532cb9e7
commit 307d5299e7
14 changed files with 375 additions and 177 deletions

View File

@ -149,38 +149,40 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
</marker> </marker>
</defs> </defs>
</svg> </svg>
<ReactFlow <AgentInstanceContext.Provider value={{ addCanvasNode }}>
connectionMode={ConnectionMode.Loose} <ReactFlow
nodes={nodes} connectionMode={ConnectionMode.Loose}
onNodesChange={onNodesChange} nodes={nodes}
edges={edges} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} edges={edges}
fitView onEdgesChange={onEdgesChange}
onConnect={onConnect} fitView
nodeTypes={nodeTypes} onConnect={onConnect}
edgeTypes={edgeTypes} nodeTypes={nodeTypes}
onDrop={onDrop} edgeTypes={edgeTypes}
onDragOver={onDragOver} onDrop={onDrop}
onNodeClick={onNodeClick} onDragOver={onDragOver}
onPaneClick={onPaneClick} onNodeClick={onNodeClick}
onInit={setReactFlowInstance} onPaneClick={onPaneClick}
onSelectionChange={onSelectionChange} onInit={setReactFlowInstance}
nodeOrigin={[0.5, 0]} onSelectionChange={onSelectionChange}
isValidConnection={isValidConnection} nodeOrigin={[0.5, 0]}
defaultEdgeOptions={{ isValidConnection={isValidConnection}
type: 'buttonEdge', defaultEdgeOptions={{
markerEnd: 'logo', type: 'buttonEdge',
style: { markerEnd: 'logo',
strokeWidth: 2, style: {
stroke: 'rgb(202 197 245)', strokeWidth: 2,
}, stroke: 'rgb(202 197 245)',
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 },
}} zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
deleteKeyCode={['Delete', 'Backspace']} }}
onBeforeDelete={handleBeforeDelete} deleteKeyCode={['Delete', 'Backspace']}
> onBeforeDelete={handleBeforeDelete}
<Background /> >
</ReactFlow> <Background />
</ReactFlow>
</AgentInstanceContext.Provider>
{formDrawerVisible && ( {formDrawerVisible && (
<AgentInstanceContext.Provider value={{ addCanvasNode }}> <AgentInstanceContext.Provider value={{ addCanvasNode }}>
<FormSheet <FormSheet

View File

@ -36,6 +36,7 @@ function InnerAgentNode({
position={Position.Left} position={Position.Left}
isConnectable={isConnectable} isConnectable={isConnectable}
style={LeftHandleStyle} style={LeftHandleStyle}
nodeId={id}
></CommonHandle> ></CommonHandle>
<CommonHandle <CommonHandle
type="source" type="source"
@ -44,6 +45,7 @@ function InnerAgentNode({
className={styles.handle} className={styles.handle}
id="b" id="b"
style={RightHandleStyle} style={RightHandleStyle}
nodeId={id}
></CommonHandle> ></CommonHandle>
</> </>
)} )}

View File

@ -17,7 +17,7 @@ import styles from './index.less';
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', []);
@ -29,14 +29,15 @@ function InnerBeginNode({ data }: NodeProps<IBeginNode>) {
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];

View File

@ -24,6 +24,7 @@ export function InnerCategorizeNode({
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>
@ -45,6 +46,7 @@ export function InnerCategorizeNode({
position={Position.Right} position={Position.Right}
isConnectable isConnectable
style={{ ...RightHandleStyle, top: position.top }} style={{ ...RightHandleStyle, top: position.top }}
nodeId={id}
></CommonHandle> ></CommonHandle>
</div> </div>
); );

View File

@ -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>
);
}

View File

@ -1,17 +1,41 @@
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 <HandleContext.Provider value={value}>
{...props} <NextStepDropdown>
className={cn( <Handle
'inline-flex justify-center items-center !bg-background-checked !size-4 !rounded-sm !border-none ', {...props}
className, 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> onClick={(e) => {
e.stopPropagation();
}}
>
<Plus className="size-3 pointer-events-none" />
</Handle>
</NextStepDropdown>
</HandleContext.Provider>
); );
} }

View File

@ -1,11 +1,10 @@
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 { NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
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({
@ -14,36 +13,27 @@ 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 <NodeWrapper>
className={classNames( <CommonHandle
styles.ragNode,
theme === 'dark' ? styles.dark : '',
{
[styles.selectedNode]: selected,
},
)}
>
<Handle
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> nodeId={id}
<Handle ></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>
); );
} }

View File

@ -1,11 +1,11 @@
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 { NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
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,
@ -13,35 +13,28 @@ export function InnerLogicNode({
isConnectable = true, isConnectable = true,
selected, selected,
}: NodeProps<ILogicNode>) { }: NodeProps<ILogicNode>) {
const { theme } = useTheme();
return ( return (
<section <ToolBar selected={selected} id={id} label={data.label}>
className={classNames( <NodeWrapper>
styles.logicNode, <CommonHandle
theme === 'dark' ? styles.dark : '', id="c"
{ type="source"
[styles.selectedNode]: selected, position={Position.Left}
}, isConnectable={isConnectable}
)} style={LeftHandleStyle}
> nodeId={id}
<Handle ></CommonHandle>
id="c" <CommonHandle
type="source" type="source"
position={Position.Left} position={Position.Right}
isConnectable={isConnectable} isConnectable={isConnectable}
className={styles.handle} style={RightHandleStyle}
style={LeftHandleStyle} id="b"
></Handle> nodeId={id}
<Handle ></CommonHandle>
type="source" <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
position={Position.Right} </NodeWrapper>
isConnectable={isConnectable} </ToolBar>
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</section>
); );
} }

View File

@ -27,6 +27,7 @@ function InnerMessageNode({
position={Position.Left} position={Position.Left}
isConnectable={isConnectable} isConnectable={isConnectable}
style={LeftHandleStyle} style={LeftHandleStyle}
nodeId={id}
></CommonHandle> ></CommonHandle>
<CommonHandle <CommonHandle
type="source" type="source"
@ -34,6 +35,7 @@ function InnerMessageNode({
isConnectable={isConnectable} isConnectable={isConnectable}
style={RightHandleStyle} style={RightHandleStyle}
id="b" id="b"
nodeId={id}
></CommonHandle> ></CommonHandle>
<NodeHeader <NodeHeader
id={id} id={id}

View File

@ -42,6 +42,7 @@ function InnerRetrievalNode({
isConnectable={isConnectable} isConnectable={isConnectable}
className={styles.handle} className={styles.handle}
style={LeftHandleStyle} style={LeftHandleStyle}
nodeId={id}
></CommonHandle> ></CommonHandle>
<CommonHandle <CommonHandle
type="source" type="source"
@ -50,6 +51,7 @@ function InnerRetrievalNode({
className={styles.handle} className={styles.handle}
style={RightHandleStyle} style={RightHandleStyle}
id="b" id="b"
nodeId={id}
></CommonHandle> ></CommonHandle>
<NodeHeader <NodeHeader
id={id} id={id}

View File

@ -66,6 +66,7 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
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">
@ -94,6 +95,7 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
position={Position.Right} position={Position.Right}
isConnectable isConnectable
style={{ ...RightHandleStyle, top: position.top }} style={{ ...RightHandleStyle, top: position.top }}
nodeId={id}
></CommonHandle> ></CommonHandle>
</div> </div>
); );

View File

@ -1,4 +1,5 @@
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';
@ -34,3 +35,14 @@ type AgentChatLogContextType = Pick<
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,
);

View File

@ -11,6 +11,7 @@ import {
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';
@ -109,7 +110,12 @@ const AgentForm = ({ node }: INextOperatorForm) => {
)} )}
/> />
</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>

View File

@ -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>) { 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);
@ -132,95 +165,111 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
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 type: string,
// and you don't need to subtract the reactFlowBounds.left/top anymore params: { id?: string; position?: Position; sourceHandle?: string } = {
// details: https://@xyflow/react.dev/whats-new/2023-11-10 position: Position.Right,
const position = reactFlowInstance?.screenToFlowPosition({ },
x: event.clientX, ) =>
y: event.clientY, (event: React.MouseEvent<HTMLElement>) => {
}); const id = params.id;
const newNode: Node<any> = { // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
id: `${type}:${humanId()}`, // and you don't need to subtract the reactFlowBounds.left/top anymore
type: NodeMap[type as Operator] || 'ragNode', // details: https://@xyflow/react.dev/whats-new/2023-11-10
position: position || { let position = reactFlowInstance?.screenToFlowPosition({
x: 0, x: event.clientX,
y: 0, y: event.clientY,
}, });
data: {
label: `${type}`,
name: generateNodeNamesWithIncreasingIndex(getNodeName(type), nodes),
form: initializeOperatorParams(type as Operator),
},
sourcePosition: Position.Right,
targetPosition: Position.Left,
dragHandle: getNodeDragHandle(type),
};
if (type === Operator.Iteration) { if (params.position === Position.Right) {
newNode.width = 500; position = calculateNewlyBackChildPosition(id, params.sourceHandle);
newNode.height = 250; }
const iterationStartNode: Node<any> = {
id: `${Operator.IterationStart}:${humanId()}`, const newNode: Node<any> = {
type: 'iterationStartNode', id: `${type}:${humanId()}`,
position: { x: 50, y: 100 }, type: NodeMap[type as Operator] || 'ragNode',
// draggable: false, position: position || {
data: { x: 0,
label: Operator.IterationStart, y: 0,
name: Operator.IterationStart,
form: {},
}, },
parentId: newNode.id, data: {
extent: 'parent', label: `${type}`,
name: generateNodeNamesWithIncreasingIndex(
getNodeName(type),
nodes,
),
form: initializeOperatorParams(type as Operator),
},
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 if (type === Operator.Iteration) {
.filter((x) => allChildAgentNodeIds.some((y) => y === x.id)) newNode.width = 500;
.map((x) => x.position.x); newNode.height = 250;
const iterationStartNode: Node<any> = {
const maxX = Math.max(...xAxises); id: `${Operator.IterationStart}:${humanId()}`,
type: 'iterationStartNode',
newNode.position = { position: { x: 50, y: 100 },
x: xAxises.length > 0 ? maxX + 262 : agentNode.position.x + 82, // draggable: false,
y: agentNode.position.y + 140, 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,