瀏覽代碼

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)
tags/v0.19.1
balibabu 4 月之前
父節點
當前提交
307d5299e7
沒有連結到貢獻者的電子郵件帳戶。

+ 34
- 32
web/src/pages/agent/canvas/index.tsx 查看文件

@@ -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

+ 2
- 0
web/src/pages/agent/canvas/node/agent-node.tsx 查看文件

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

+ 4
- 3
web/src/pages/agent/canvas/node/begin-node.tsx 查看文件

@@ -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];

+ 2
- 0
web/src/pages/agent/canvas/node/categorize-node.tsx 查看文件

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

+ 111
- 0
web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx 查看文件

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

+ 34
- 10
web/src/pages/agent/canvas/node/handle.tsx 查看文件

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

+ 11
- 21
web/src/pages/agent/canvas/node/index.tsx 查看文件

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

+ 25
- 32
web/src/pages/agent/canvas/node/logic-node.tsx 查看文件

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


+ 2
- 0
web/src/pages/agent/canvas/node/message-node.tsx 查看文件

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

+ 2
- 0
web/src/pages/agent/canvas/node/retrieval-node.tsx 查看文件

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

+ 2
- 0
web/src/pages/agent/canvas/node/switch-node.tsx 查看文件

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

+ 12
- 0
web/src/pages/agent/context.ts 查看文件

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

+ 7
- 1
web/src/pages/agent/form/agent-form/index.tsx 查看文件

@@ -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>

+ 128
- 79
web/src/pages/agent/hooks/use-add-node.ts 查看文件

@@ -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,

Loading…
取消
儲存