diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx index 21279da0b..7833b8763 100644 --- a/web/src/pages/agent/canvas/index.tsx +++ b/web/src/pages/agent/canvas/index.tsx @@ -149,38 +149,40 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { - - - + + + + + {formDrawerVisible && ( )} diff --git a/web/src/pages/agent/canvas/node/begin-node.tsx b/web/src/pages/agent/canvas/node/begin-node.tsx index c13db7c6f..d4f92e116 100644 --- a/web/src/pages/agent/canvas/node/begin-node.tsx +++ b/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) { +function InnerBeginNode({ data, id }: NodeProps) { const { t } = useTranslation(); const query: BeginQuery[] = get(data, 'form.query', []); @@ -29,14 +29,15 @@ function InnerBeginNode({ data }: NodeProps) { isConnectable className={styles.handle} style={RightHandleStyle} + nodeId={id} > - +
{t(`flow.begin`)}
- +
{query.map((x, idx) => { const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType]; diff --git a/web/src/pages/agent/canvas/node/categorize-node.tsx b/web/src/pages/agent/canvas/node/categorize-node.tsx index 397ed9eee..7460d1d99 100644 --- a/web/src/pages/agent/canvas/node/categorize-node.tsx +++ b/web/src/pages/agent/canvas/node/categorize-node.tsx @@ -24,6 +24,7 @@ export function InnerCategorizeNode({ position={Position.Left} isConnectable id={'a'} + nodeId={id} > @@ -45,6 +46,7 @@ export function InnerCategorizeNode({ position={Position.Right} isConnectable style={{ ...RightHandleStyle, top: position.top }} + nodeId={id} > ); diff --git a/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx b/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx new file mode 100644 index 000000000..434a5bb3b --- /dev/null +++ b/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 ( +
    + {operators.map((x) => { + return ( + + + {x} + + ); + })} +
+ ); +} + +function AccordionOperators() { + return ( + + + AI + + + + + + Dialogue + + + + + + Flow + + + + + + + Data Manipulation + + + + + + + Tools + + + + + + ); +} + +export function NextStepDropdown({ children }: PropsWithChildren) { + return ( + + {children} + e.stopPropagation()} + className="w-[300px] font-semibold" + > + Next Step + + + + ); +} diff --git a/web/src/pages/agent/canvas/node/handle.tsx b/web/src/pages/agent/canvas/node/handle.tsx index 212208e60..ad76a87eb 100644 --- a/web/src/pages/agent/canvas/node/handle.tsx +++ b/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 ( - - - + + + { + e.stopPropagation(); + }} + > + + + + ); } diff --git a/web/src/pages/agent/canvas/node/index.tsx b/web/src/pages/agent/canvas/node/index.tsx index d67ec040c..4afad2d2d 100644 --- a/web/src/pages/agent/canvas/node/index.tsx +++ b/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) { - const { theme } = useTheme(); return ( -
- + - + + nodeId={id} + > -
+
); } diff --git a/web/src/pages/agent/canvas/node/logic-node.tsx b/web/src/pages/agent/canvas/node/logic-node.tsx index 3e43954ae..a98efc371 100644 --- a/web/src/pages/agent/canvas/node/logic-node.tsx +++ b/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) { - const { theme } = useTheme(); return ( -
- - - -
+ + + + + + + ); } diff --git a/web/src/pages/agent/canvas/node/message-node.tsx b/web/src/pages/agent/canvas/node/message-node.tsx index 5c76df9a2..7dd4f77e1 100644 --- a/web/src/pages/agent/canvas/node/message-node.tsx +++ b/web/src/pages/agent/canvas/node/message-node.tsx @@ -27,6 +27,7 @@ function InnerMessageNode({ position={Position.Left} isConnectable={isConnectable} style={LeftHandleStyle} + nodeId={id} > ) { position={Position.Left} isConnectable id={'a'} + nodeId={id} >
@@ -94,6 +95,7 @@ function InnerSwitchNode({ id, data, selected }: NodeProps) { position={Position.Right} isConnectable style={{ ...RightHandleStyle, top: position.top }} + nodeId={id} > ); diff --git a/web/src/pages/agent/context.ts b/web/src/pages/agent/context.ts index cca63defb..eda8c50ee 100644 --- a/web/src/pages/agent/context.ts +++ b/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( {} as AgentChatLogContextType, ); + +export type HandleContextType = { + nodeId?: string; + id?: string; + type: HandleType; + position: Position; +}; + +export const HandleContext = createContext( + {} as HandleContextType, +); diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index 82eb2cd31..bf2a1a6d3 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/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) => { )} /> - + Add Agent diff --git a/web/src/pages/agent/hooks/use-add-node.ts b/web/src/pages/agent/hooks/use-add-node.ts index b234805d8..445850a30 100644 --- a/web/src/pages/agent/hooks/use-add-node.ts +++ b/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) { const addNode = useGraphStore((state) => state.addNode); const getNode = useGraphStore((state) => state.getNode); @@ -132,95 +165,111 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance) { const edges = useGraphStore((state) => state.edges); const getNodeName = useGetNodeName(); const initializeOperatorParams = useInitializeOperatorParams(); + const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); // const [reactFlowInstance, setReactFlowInstance] = // useState>(); const addCanvasNode = useCallback( - (type: string, id?: string) => (event: React.MouseEvent) => { - // 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, - }); + ( + type: string, + params: { id?: string; position?: Position; sourceHandle?: string } = { + position: Position.Right, + }, + ) => + (event: React.MouseEvent) => { + const id = params.id; - const newNode: Node = { - 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), - }; + // 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 (type === Operator.Iteration) { - newNode.width = 500; - newNode.height = 250; - const iterationStartNode: Node = { - id: `${Operator.IterationStart}:${humanId()}`, - type: 'iterationStartNode', - position: { x: 50, y: 100 }, - // draggable: false, - data: { - label: Operator.IterationStart, - name: Operator.IterationStart, - form: {}, + if (params.position === Position.Right) { + position = calculateNewlyBackChildPosition(id, params.sourceHandle); + } + + const newNode: Node = { + id: `${type}:${humanId()}`, + type: NodeMap[type as Operator] || 'ragNode', + position: position || { + x: 0, + y: 0, }, - parentId: newNode.id, - extent: 'parent', + data: { + 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 - .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 = { + 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,