mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
Feat:Can directly generate an agent node by dragging and dropping the connecting line (#9226) (#9357)
…e connecting line (#9226) ### What problem does this PR solve? Can directly generate an agent node by dragging and dropping the connecting line (#9226) ### Type of change - [ ] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe):
This commit is contained in:
56
web/src/pages/agent/canvas/context.tsx
Normal file
56
web/src/pages/agent/canvas/context.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
interface DropdownContextType {
|
||||||
|
canShowDropdown: () => boolean;
|
||||||
|
setActiveDropdown: (type: 'handle' | 'drag') => void;
|
||||||
|
clearActiveDropdown: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownContext = createContext<DropdownContextType | null>(null);
|
||||||
|
|
||||||
|
export const useDropdownManager = () => {
|
||||||
|
const context = useContext(DropdownContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useDropdownManager must be used within DropdownProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DropdownProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropdownProvider = ({ children }: DropdownProviderProps) => {
|
||||||
|
const activeDropdownRef = useRef<'handle' | 'drag' | null>(null);
|
||||||
|
|
||||||
|
const canShowDropdown = useCallback(() => {
|
||||||
|
const current = activeDropdownRef.current;
|
||||||
|
return !current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setActiveDropdown = useCallback((type: 'handle' | 'drag') => {
|
||||||
|
activeDropdownRef.current = type;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearActiveDropdown = useCallback(() => {
|
||||||
|
activeDropdownRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value: DropdownContextType = {
|
||||||
|
canShowDropdown,
|
||||||
|
setActiveDropdown,
|
||||||
|
clearActiveDropdown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</DropdownContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,17 +4,20 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
|
Connection,
|
||||||
ConnectionMode,
|
ConnectionMode,
|
||||||
ControlButton,
|
ControlButton,
|
||||||
Controls,
|
Controls,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
|
Position,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import { NotebookPen } from 'lucide-react';
|
import { NotebookPen } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChatSheet } from '../chat/chat-sheet';
|
import { ChatSheet } from '../chat/chat-sheet';
|
||||||
import { AgentBackground } from '../components/background';
|
import { AgentBackground } from '../components/background';
|
||||||
@ -22,7 +25,9 @@ import {
|
|||||||
AgentChatContext,
|
AgentChatContext,
|
||||||
AgentChatLogContext,
|
AgentChatLogContext,
|
||||||
AgentInstanceContext,
|
AgentInstanceContext,
|
||||||
|
HandleContext,
|
||||||
} from '../context';
|
} from '../context';
|
||||||
|
|
||||||
import FormSheet from '../form-sheet/next';
|
import FormSheet from '../form-sheet/next';
|
||||||
import {
|
import {
|
||||||
useHandleDrop,
|
useHandleDrop,
|
||||||
@ -33,6 +38,8 @@ import { useAddNode } from '../hooks/use-add-node';
|
|||||||
import { useBeforeDelete } from '../hooks/use-before-delete';
|
import { useBeforeDelete } from '../hooks/use-before-delete';
|
||||||
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
|
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
|
||||||
import { useMoveNote } from '../hooks/use-move-note';
|
import { useMoveNote } from '../hooks/use-move-note';
|
||||||
|
import { useDropdownManager } from './context';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useHideFormSheetOnNodeDeletion,
|
useHideFormSheetOnNodeDeletion,
|
||||||
useShowDrawer,
|
useShowDrawer,
|
||||||
@ -46,6 +53,7 @@ import { RagNode } from './node';
|
|||||||
import { AgentNode } from './node/agent-node';
|
import { AgentNode } from './node/agent-node';
|
||||||
import { BeginNode } from './node/begin-node';
|
import { BeginNode } from './node/begin-node';
|
||||||
import { CategorizeNode } from './node/categorize-node';
|
import { CategorizeNode } from './node/categorize-node';
|
||||||
|
import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
|
||||||
import { GenerateNode } from './node/generate-node';
|
import { GenerateNode } from './node/generate-node';
|
||||||
import { InvokeNode } from './node/invoke-node';
|
import { InvokeNode } from './node/invoke-node';
|
||||||
import { IterationNode, IterationStartNode } from './node/iteration-node';
|
import { IterationNode, IterationStartNode } from './node/iteration-node';
|
||||||
@ -96,7 +104,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
|||||||
const {
|
const {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
onConnect,
|
onConnect: originalOnConnect,
|
||||||
onEdgesChange,
|
onEdgesChange,
|
||||||
onNodesChange,
|
onNodesChange,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
@ -147,14 +155,6 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
|||||||
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
const onPaneClick = useCallback(() => {
|
|
||||||
hideFormDrawer();
|
|
||||||
if (imgVisible) {
|
|
||||||
addNoteNode(mouse);
|
|
||||||
hideImage();
|
|
||||||
}
|
|
||||||
}, [addNoteNode, hideFormDrawer, hideImage, imgVisible, mouse]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chatVisible) {
|
if (!chatVisible) {
|
||||||
clearEventList();
|
clearEventList();
|
||||||
@ -172,6 +172,73 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
|||||||
|
|
||||||
useHideFormSheetOnNodeDeletion({ hideFormDrawer });
|
useHideFormSheetOnNodeDeletion({ hideFormDrawer });
|
||||||
|
|
||||||
|
const { visible, hideModal, showModal } = useSetModalState();
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const isConnectedRef = useRef(false);
|
||||||
|
const connectionStartRef = useRef<{
|
||||||
|
nodeId: string;
|
||||||
|
handleId: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const preventCloseRef = useRef(false);
|
||||||
|
|
||||||
|
const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
|
||||||
|
|
||||||
|
const onPaneClick = useCallback(() => {
|
||||||
|
hideFormDrawer();
|
||||||
|
if (visible && !preventCloseRef.current) {
|
||||||
|
hideModal();
|
||||||
|
clearActiveDropdown();
|
||||||
|
}
|
||||||
|
if (imgVisible) {
|
||||||
|
addNoteNode(mouse);
|
||||||
|
hideImage();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
hideFormDrawer,
|
||||||
|
visible,
|
||||||
|
hideModal,
|
||||||
|
imgVisible,
|
||||||
|
addNoteNode,
|
||||||
|
mouse,
|
||||||
|
hideImage,
|
||||||
|
clearActiveDropdown,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onConnect = (connection: Connection) => {
|
||||||
|
originalOnConnect(connection);
|
||||||
|
isConnectedRef.current = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OnConnectStart = (event: any, params: any) => {
|
||||||
|
isConnectedRef.current = false;
|
||||||
|
|
||||||
|
if (params && params.nodeId && params.handleId) {
|
||||||
|
connectionStartRef.current = {
|
||||||
|
nodeId: params.nodeId,
|
||||||
|
handleId: params.handleId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
connectionStartRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const OnConnectEnd = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if ('clientX' in event && 'clientY' in event) {
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
setDropdownPosition({ x: clientX, y: clientY });
|
||||||
|
if (!isConnectedRef.current) {
|
||||||
|
setActiveDropdown('drag');
|
||||||
|
showModal();
|
||||||
|
preventCloseRef.current = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
preventCloseRef.current = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.canvasWrapper}>
|
<div className={styles.canvasWrapper}>
|
||||||
<svg
|
<svg
|
||||||
@ -206,6 +273,8 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
|||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
|
onConnectStart={OnConnectStart}
|
||||||
|
onConnectEnd={OnConnectEnd}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onPaneClick={onPaneClick}
|
onPaneClick={onPaneClick}
|
||||||
@ -243,6 +312,27 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
|||||||
</ControlButton>
|
</ControlButton>
|
||||||
</Controls>
|
</Controls>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
{visible && (
|
||||||
|
<HandleContext.Provider
|
||||||
|
value={{
|
||||||
|
nodeId: connectionStartRef.current?.nodeId || '',
|
||||||
|
id: connectionStartRef.current?.handleId || '',
|
||||||
|
type: 'source',
|
||||||
|
position: Position.Right,
|
||||||
|
isFromConnectionDrag: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InnerNextStepDropdown
|
||||||
|
hideModal={() => {
|
||||||
|
hideModal();
|
||||||
|
clearActiveDropdown();
|
||||||
|
}}
|
||||||
|
position={dropdownPosition}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</InnerNextStepDropdown>
|
||||||
|
</HandleContext.Provider>
|
||||||
|
)}
|
||||||
</AgentInstanceContext.Provider>
|
</AgentInstanceContext.Provider>
|
||||||
<NotebookPen
|
<NotebookPen
|
||||||
className={cn('hidden absolute size-6', { block: imgVisible })}
|
className={cn('hidden absolute size-6', { block: imgVisible })}
|
||||||
|
|||||||
@ -20,55 +20,111 @@ import { IModalProps } from '@/interfaces/common';
|
|||||||
import { Operator } from '@/pages/agent/constant';
|
import { Operator } from '@/pages/agent/constant';
|
||||||
import { AgentInstanceContext, HandleContext } from '@/pages/agent/context';
|
import { AgentInstanceContext, HandleContext } from '@/pages/agent/context';
|
||||||
import OperatorIcon from '@/pages/agent/operator-icon';
|
import OperatorIcon from '@/pages/agent/operator-icon';
|
||||||
|
import { Position } from '@xyflow/react';
|
||||||
import { lowerFirst } from 'lodash';
|
import { lowerFirst } from 'lodash';
|
||||||
import { PropsWithChildren, createContext, memo, useContext } from 'react';
|
import {
|
||||||
|
PropsWithChildren,
|
||||||
|
createContext,
|
||||||
|
memo,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type OperatorItemProps = { operators: Operator[] };
|
type OperatorItemProps = {
|
||||||
|
operators: Operator[];
|
||||||
|
isCustomDropdown?: boolean;
|
||||||
|
mousePosition?: { x: number; y: number };
|
||||||
|
};
|
||||||
|
|
||||||
const HideModalContext = createContext<IModalProps<any>['showModal']>(() => {});
|
const HideModalContext = createContext<IModalProps<any>['showModal']>(() => {});
|
||||||
|
const OnNodeCreatedContext = createContext<
|
||||||
|
((newNodeId: string) => void) | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
function OperatorItemList({ operators }: OperatorItemProps) {
|
function OperatorItemList({
|
||||||
|
operators,
|
||||||
|
isCustomDropdown = false,
|
||||||
|
mousePosition,
|
||||||
|
}: OperatorItemProps) {
|
||||||
const { addCanvasNode } = useContext(AgentInstanceContext);
|
const { addCanvasNode } = useContext(AgentInstanceContext);
|
||||||
const { nodeId, id, position } = useContext(HandleContext);
|
const handleContext = useContext(HandleContext);
|
||||||
const hideModal = useContext(HideModalContext);
|
const hideModal = useContext(HideModalContext);
|
||||||
|
const onNodeCreated = useContext(OnNodeCreatedContext);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
const handleClick = (operator: Operator) => {
|
||||||
<ul className="space-y-2">
|
const contextData = handleContext || {
|
||||||
{operators.map((x) => {
|
nodeId: '',
|
||||||
return (
|
id: '',
|
||||||
<Tooltip key={x}>
|
type: 'source' as const,
|
||||||
<TooltipTrigger asChild>
|
position: Position.Right,
|
||||||
<DropdownMenuItem
|
isFromConnectionDrag: true,
|
||||||
key={x}
|
};
|
||||||
className="hover:bg-bg-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
|
|
||||||
onClick={addCanvasNode(x, {
|
const mockEvent = mousePosition
|
||||||
nodeId,
|
? {
|
||||||
id,
|
clientX: mousePosition.x,
|
||||||
position,
|
clientY: mousePosition.y,
|
||||||
})}
|
}
|
||||||
onSelect={() => hideModal?.()}
|
: undefined;
|
||||||
>
|
|
||||||
<OperatorIcon name={x}></OperatorIcon>
|
const newNodeId = addCanvasNode(operator, contextData)(mockEvent);
|
||||||
{t(`flow.${lowerFirst(x)}`)}
|
|
||||||
</DropdownMenuItem>
|
if (onNodeCreated && newNodeId) {
|
||||||
</TooltipTrigger>
|
onNodeCreated(newNodeId);
|
||||||
<TooltipContent side="right">
|
}
|
||||||
<p>{t(`flow.${lowerFirst(x)}Description`)}</p>
|
|
||||||
</TooltipContent>
|
hideModal?.();
|
||||||
</Tooltip>
|
};
|
||||||
);
|
|
||||||
})}
|
const renderOperatorItem = (operator: Operator) => {
|
||||||
</ul>
|
const commonContent = (
|
||||||
);
|
<div className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start">
|
||||||
|
<OperatorIcon name={operator} />
|
||||||
|
{t(`flow.${lowerFirst(operator)}`)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={operator}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
{isCustomDropdown ? (
|
||||||
|
<li onClick={() => handleClick(operator)}>{commonContent}</li>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={operator}
|
||||||
|
className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
|
||||||
|
onClick={() => handleClick(operator)}
|
||||||
|
onSelect={() => hideModal?.()}
|
||||||
|
>
|
||||||
|
<OperatorIcon name={operator} />
|
||||||
|
{t(`flow.${lowerFirst(operator)}`)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>{t(`flow.${lowerFirst(operator)}Description`)}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccordionOperators() {
|
function AccordionOperators({
|
||||||
|
isCustomDropdown = false,
|
||||||
|
mousePosition,
|
||||||
|
}: {
|
||||||
|
isCustomDropdown?: boolean;
|
||||||
|
mousePosition?: { x: number; y: number };
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
type="multiple"
|
type="multiple"
|
||||||
className="px-2 text-text-primary max-h-[45vh] overflow-auto"
|
className="px-2 text-text-title max-h-[45vh] overflow-auto"
|
||||||
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
|
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
|
||||||
>
|
>
|
||||||
<AccordionItem value="item-1">
|
<AccordionItem value="item-1">
|
||||||
@ -76,6 +132,8 @@ function AccordionOperators() {
|
|||||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
<OperatorItemList
|
<OperatorItemList
|
||||||
operators={[Operator.Agent, Operator.Retrieval]}
|
operators={[Operator.Agent, Operator.Retrieval]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
></OperatorItemList>
|
></OperatorItemList>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@ -84,6 +142,8 @@ function AccordionOperators() {
|
|||||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
<OperatorItemList
|
<OperatorItemList
|
||||||
operators={[Operator.Message, Operator.UserFillUp]}
|
operators={[Operator.Message, Operator.UserFillUp]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
></OperatorItemList>
|
></OperatorItemList>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@ -96,6 +156,8 @@ function AccordionOperators() {
|
|||||||
Operator.Iteration,
|
Operator.Iteration,
|
||||||
Operator.Categorize,
|
Operator.Categorize,
|
||||||
]}
|
]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
></OperatorItemList>
|
></OperatorItemList>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@ -106,6 +168,8 @@ function AccordionOperators() {
|
|||||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
<OperatorItemList
|
<OperatorItemList
|
||||||
operators={[Operator.Code, Operator.StringTransform]}
|
operators={[Operator.Code, Operator.StringTransform]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
></OperatorItemList>
|
></OperatorItemList>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@ -129,6 +193,8 @@ function AccordionOperators() {
|
|||||||
Operator.Invoke,
|
Operator.Invoke,
|
||||||
Operator.WenCai,
|
Operator.WenCai,
|
||||||
]}
|
]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
></OperatorItemList>
|
></OperatorItemList>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@ -139,9 +205,69 @@ function AccordionOperators() {
|
|||||||
export function InnerNextStepDropdown({
|
export function InnerNextStepDropdown({
|
||||||
children,
|
children,
|
||||||
hideModal,
|
hideModal,
|
||||||
}: PropsWithChildren & IModalProps<any>) {
|
position,
|
||||||
|
onNodeCreated,
|
||||||
|
}: PropsWithChildren &
|
||||||
|
IModalProps<any> & {
|
||||||
|
position?: { x: number; y: number };
|
||||||
|
onNodeCreated?: (newNodeId: string) => void;
|
||||||
|
}) {
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (position && hideModal) {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [position, hideModal]);
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: position.x,
|
||||||
|
top: position.y + 10,
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="w-[300px] font-semibold bg-white border border-border rounded-md shadow-lg">
|
||||||
|
<div className="px-3 py-2 border-b border-border">
|
||||||
|
<div className="text-sm font-medium">Next Step</div>
|
||||||
|
</div>
|
||||||
|
<HideModalContext.Provider value={hideModal}>
|
||||||
|
<OnNodeCreatedContext.Provider value={onNodeCreated}>
|
||||||
|
<AccordionOperators
|
||||||
|
isCustomDropdown={true}
|
||||||
|
mousePosition={position}
|
||||||
|
></AccordionOperators>
|
||||||
|
</OnNodeCreatedContext.Provider>
|
||||||
|
</HideModalContext.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu open onOpenChange={hideModal}>
|
<DropdownMenu
|
||||||
|
open={true}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open && hideModal) {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Handle, HandleProps } from '@xyflow/react';
|
|||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { HandleContext } from '../../context';
|
import { HandleContext } from '../../context';
|
||||||
|
import { useDropdownManager } from '../context';
|
||||||
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
|
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
|
||||||
|
|
||||||
export function CommonHandle({
|
export function CommonHandle({
|
||||||
@ -13,12 +14,16 @@ export function CommonHandle({
|
|||||||
}: HandleProps & { nodeId: string }) {
|
}: HandleProps & { nodeId: string }) {
|
||||||
const { visible, hideModal, showModal } = useSetModalState();
|
const { visible, hideModal, showModal } = useSetModalState();
|
||||||
|
|
||||||
|
const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
|
||||||
|
useDropdownManager();
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
nodeId,
|
nodeId,
|
||||||
id: props.id,
|
id: props.id || undefined,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
position: props.position,
|
position: props.position,
|
||||||
|
isFromConnectionDrag: false,
|
||||||
}),
|
}),
|
||||||
[nodeId, props.id, props.position, props.type],
|
[nodeId, props.id, props.position, props.type],
|
||||||
);
|
);
|
||||||
@ -33,12 +38,23 @@ export function CommonHandle({
|
|||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!canShowDropdown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveDropdown('handle');
|
||||||
showModal();
|
showModal();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="size-3 pointer-events-none text-text-title-invert" />
|
<Plus className="size-3 pointer-events-none text-text-title-invert" />
|
||||||
{visible && (
|
{visible && (
|
||||||
<InnerNextStepDropdown hideModal={hideModal}>
|
<InnerNextStepDropdown
|
||||||
|
hideModal={() => {
|
||||||
|
hideModal();
|
||||||
|
clearActiveDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span></span>
|
<span></span>
|
||||||
</InnerNextStepDropdown>
|
</InnerNextStepDropdown>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export type HandleContextType = {
|
|||||||
id?: string;
|
id?: string;
|
||||||
type: HandleType;
|
type: HandleType;
|
||||||
position: Position;
|
position: Position;
|
||||||
|
isFromConnectionDrag: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HandleContext = createContext<HandleContextType>(
|
export const HandleContext = createContext<HandleContextType>(
|
||||||
|
|||||||
@ -208,7 +208,7 @@ function useAddToolNode() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addToolNode = useCallback(
|
const addToolNode = useCallback(
|
||||||
(newNode: Node<any>, nodeId?: string) => {
|
(newNode: Node<any>, nodeId?: string): boolean => {
|
||||||
const agentNode = getNode(nodeId);
|
const agentNode = getNode(nodeId);
|
||||||
|
|
||||||
if (agentNode) {
|
if (agentNode) {
|
||||||
@ -222,7 +222,7 @@ function useAddToolNode() {
|
|||||||
childToolNodeIds.length > 0 &&
|
childToolNodeIds.length > 0 &&
|
||||||
nodes.some((x) => x.id === childToolNodeIds[0])
|
nodes.some((x) => x.id === childToolNodeIds[0])
|
||||||
) {
|
) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
newNode.position = {
|
newNode.position = {
|
||||||
@ -239,7 +239,9 @@ function useAddToolNode() {
|
|||||||
targetHandle: NodeHandleId.End,
|
targetHandle: NodeHandleId.End,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
[addEdge, addNode, edges, getNode, nodes],
|
[addEdge, addNode, edges, getNode, nodes],
|
||||||
);
|
);
|
||||||
@ -295,13 +297,17 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
|||||||
const addCanvasNode = useCallback(
|
const addCanvasNode = useCallback(
|
||||||
(
|
(
|
||||||
type: string,
|
type: string,
|
||||||
params: { nodeId?: string; position: Position; id?: string } = {
|
params: {
|
||||||
|
nodeId?: string;
|
||||||
|
position: Position;
|
||||||
|
id?: string;
|
||||||
|
isFromConnectionDrag?: boolean;
|
||||||
|
} = {
|
||||||
position: Position.Right,
|
position: Position.Right,
|
||||||
},
|
},
|
||||||
) =>
|
) =>
|
||||||
(event?: CanvasMouseEvent) => {
|
(event?: CanvasMouseEvent): string | undefined => {
|
||||||
const nodeId = params.nodeId;
|
const nodeId = params.nodeId;
|
||||||
|
|
||||||
const node = getNode(nodeId);
|
const node = getNode(nodeId);
|
||||||
|
|
||||||
// reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
|
// reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
|
||||||
@ -312,7 +318,11 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
|||||||
y: event?.clientY || 0,
|
y: event?.clientY || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (params.position === Position.Right && type !== Operator.Note) {
|
if (
|
||||||
|
params.position === Position.Right &&
|
||||||
|
type !== Operator.Note &&
|
||||||
|
!params.isFromConnectionDrag
|
||||||
|
) {
|
||||||
position = calculateNewlyBackChildPosition(nodeId, params.id);
|
position = calculateNewlyBackChildPosition(nodeId, params.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +381,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
|||||||
targetHandle: NodeHandleId.End,
|
targetHandle: NodeHandleId.End,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return newNode.id;
|
||||||
} else if (
|
} else if (
|
||||||
type === Operator.Agent &&
|
type === Operator.Agent &&
|
||||||
params.position === Position.Bottom
|
params.position === Position.Bottom
|
||||||
@ -406,8 +417,10 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
|||||||
targetHandle: NodeHandleId.AgentTop,
|
targetHandle: NodeHandleId.AgentTop,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return newNode.id;
|
||||||
} else if (type === Operator.Tool) {
|
} else if (type === Operator.Tool) {
|
||||||
addToolNode(newNode, params.nodeId);
|
const toolNodeAdded = addToolNode(newNode, params.nodeId);
|
||||||
|
return toolNodeAdded ? newNode.id : undefined;
|
||||||
} else {
|
} else {
|
||||||
addNode(newNode);
|
addNode(newNode);
|
||||||
addChildEdge(params.position, {
|
addChildEdge(params.position, {
|
||||||
@ -416,6 +429,8 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
|||||||
sourceHandle: params.id,
|
sourceHandle: params.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return newNode.id;
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
addChildEdge,
|
addChildEdge,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { ComponentPropsWithoutRef, useCallback } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'umi';
|
import { useParams } from 'umi';
|
||||||
import AgentCanvas from './canvas';
|
import AgentCanvas from './canvas';
|
||||||
|
import { DropdownProvider } from './canvas/context';
|
||||||
import EmbedDialog from './embed-dialog';
|
import EmbedDialog from './embed-dialog';
|
||||||
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
|
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
|
||||||
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
||||||
@ -185,10 +186,12 @@ export default function Agent() {
|
|||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<AgentCanvas
|
<DropdownProvider>
|
||||||
drawerVisible={chatDrawerVisible}
|
<AgentCanvas
|
||||||
hideDrawer={hideChatDrawer}
|
drawerVisible={chatDrawerVisible}
|
||||||
></AgentCanvas>
|
hideDrawer={hideChatDrawer}
|
||||||
|
></AgentCanvas>
|
||||||
|
</DropdownProvider>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
{fileUploadVisible && (
|
{fileUploadVisible && (
|
||||||
<UploadAgentDialog
|
<UploadAgentDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user