Feat: Add child nodes and their connecting lines by clicking #3221 (#8314)

### 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)
This commit is contained in:
balibabu
2025-06-18 09:42:56 +08:00
committed by GitHub
parent 4a2ff633e0
commit 6ce282d462
11 changed files with 84 additions and 31 deletions

View File

@ -1,7 +1,7 @@
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';
@ -31,21 +31,22 @@ function InnerAgentNode({
{isNotParentAgent && ( {isNotParentAgent && (
<> <>
<CommonHandle <CommonHandle
id="c" type="target"
type="source"
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>
</> </>
)} )}

View File

@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
import { import {
BeginQueryType, BeginQueryType,
BeginQueryTypeIconMap, BeginQueryTypeIconMap,
NodeHandleId,
Operator, Operator,
} from '../../constant'; } from '../../constant';
import { BeginQuery } from '../../interface'; import { BeginQuery } from '../../interface';
@ -27,9 +28,9 @@ function InnerBeginNode({ data, id }: NodeProps<IBeginNode>) {
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">

View File

@ -3,6 +3,7 @@ import { ICategorizeNode } from '@/interfaces/database/flow';
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';
@ -23,7 +24,7 @@ export function InnerCategorizeNode({
type="target" type="target"
position={Position.Left} position={Position.Left}
isConnectable isConnectable
id={'a'} id={NodeHandleId.End}
nodeId={id} nodeId={id}
></CommonHandle> ></CommonHandle>
@ -47,6 +48,7 @@ export function InnerCategorizeNode({
isConnectable isConnectable
style={{ ...RightHandleStyle, top: position.top }} style={{ ...RightHandleStyle, top: position.top }}
nodeId={id} nodeId={id}
isConnectableEnd={false}
></CommonHandle> ></CommonHandle>
</div> </div>
); );

View File

@ -30,8 +30,8 @@ function OperatorItemList({ operators }: OperatorItemProps) {
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, nodeId,
sourceHandle: id, id,
position, position,
})} })}
> >

View File

@ -1,6 +1,7 @@
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';
@ -17,8 +18,8 @@ function InnerRagNode({
<ToolBar selected={selected} id={id} label={data.label}> <ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper> <NodeWrapper>
<CommonHandle <CommonHandle
id="c" id={NodeHandleId.End}
type="source" type="target"
position={Position.Left} position={Position.Left}
isConnectable={isConnectable} isConnectable={isConnectable}
style={LeftHandleStyle} style={LeftHandleStyle}
@ -28,9 +29,10 @@ function InnerRagNode({
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>

View File

@ -4,6 +4,7 @@ import { Flex } from 'antd';
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';
@ -22,20 +23,21 @@ function InnerMessageNode({
<ToolBar selected={selected} id={id} label={data.label}> <ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper> <NodeWrapper>
<CommonHandle <CommonHandle
id="c" type="target"
type="source"
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}

View File

@ -6,6 +6,7 @@ import { Avatar, Flex } from 'antd';
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';
@ -36,8 +37,8 @@ function InnerRetrievalNode({
<ToolBar selected={selected} id={id} label={data.label}> <ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper> <NodeWrapper>
<CommonHandle <CommonHandle
id="c" id={NodeHandleId.End}
type="source" type="target"
position={Position.Left} position={Position.Left}
isConnectable={isConnectable} isConnectable={isConnectable}
className={styles.handle} className={styles.handle}
@ -45,13 +46,14 @@ function InnerRetrievalNode({
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}

View File

@ -3,7 +3,7 @@ import { Card, CardContent } from '@/components/ui/card';
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';
@ -65,8 +65,8 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
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">
@ -96,6 +96,7 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
isConnectable isConnectable
style={{ ...RightHandleStyle, top: position.top }} style={{ ...RightHandleStyle, top: position.top }}
nodeId={id} nodeId={id}
isConnectableEnd={false}
></CommonHandle> ></CommonHandle>
</div> </div>
); );

View File

@ -3008,3 +3008,8 @@ export const NoDebugOperatorsList = [
Operator.Switch, Operator.Switch,
Operator.Iteration, Operator.Iteration,
]; ];
export enum NodeHandleId {
Start = 'start',
End = 'end',
}

View File

@ -112,7 +112,7 @@ const AgentForm = ({ node }: INextOperatorForm) => {
</FormContainer> </FormContainer>
<BlockButton <BlockButton
onClick={addCanvasNode(Operator.Agent, { onClick={addCanvasNode(Operator.Agent, {
id: node?.id, nodeId: node?.id,
position: Position.Bottom, position: Position.Bottom,
})} })}
> >

View File

@ -1,10 +1,11 @@
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,
@ -145,8 +146,8 @@ export function useCalculateNewlyChildPosition() {
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, y: yAxises.length > 0 ? maxY + 150 : parentNode?.position.y || 0,
x: (parentNode?.position.x || 0) + 140, x: (parentNode?.position.x || 0) + 300,
}; };
return position; return position;
@ -157,6 +158,31 @@ export function useCalculateNewlyChildPosition() {
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);
@ -166,18 +192,19 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
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
@ -188,7 +215,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
}); });
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> = {
@ -229,12 +256,15 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
}; };
addNode(newNode); addNode(newNode);
addNode(iterationStartNode); addNode(iterationStartNode);
} else if (type === Operator.Agent) { } else if (
const agentNode = getNode(id); 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
@ -249,9 +279,9 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
}; };
} }
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',
@ -268,11 +298,18 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
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,