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:
@ -20,55 +20,111 @@ import { IModalProps } from '@/interfaces/common';
|
||||
import { Operator } from '@/pages/agent/constant';
|
||||
import { AgentInstanceContext, HandleContext } from '@/pages/agent/context';
|
||||
import OperatorIcon from '@/pages/agent/operator-icon';
|
||||
import { Position } from '@xyflow/react';
|
||||
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';
|
||||
|
||||
type OperatorItemProps = { operators: Operator[] };
|
||||
type OperatorItemProps = {
|
||||
operators: Operator[];
|
||||
isCustomDropdown?: boolean;
|
||||
mousePosition?: { x: number; y: number };
|
||||
};
|
||||
|
||||
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 { nodeId, id, position } = useContext(HandleContext);
|
||||
const handleContext = useContext(HandleContext);
|
||||
const hideModal = useContext(HideModalContext);
|
||||
const onNodeCreated = useContext(OnNodeCreatedContext);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ul className="space-y-2">
|
||||
{operators.map((x) => {
|
||||
return (
|
||||
<Tooltip key={x}>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
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, {
|
||||
nodeId,
|
||||
id,
|
||||
position,
|
||||
})}
|
||||
onSelect={() => hideModal?.()}
|
||||
>
|
||||
<OperatorIcon name={x}></OperatorIcon>
|
||||
{t(`flow.${lowerFirst(x)}`)}
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{t(`flow.${lowerFirst(x)}Description`)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
const handleClick = (operator: Operator) => {
|
||||
const contextData = handleContext || {
|
||||
nodeId: '',
|
||||
id: '',
|
||||
type: 'source' as const,
|
||||
position: Position.Right,
|
||||
isFromConnectionDrag: true,
|
||||
};
|
||||
|
||||
const mockEvent = mousePosition
|
||||
? {
|
||||
clientX: mousePosition.x,
|
||||
clientY: mousePosition.y,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const newNodeId = addCanvasNode(operator, contextData)(mockEvent);
|
||||
|
||||
if (onNodeCreated && newNodeId) {
|
||||
onNodeCreated(newNodeId);
|
||||
}
|
||||
|
||||
hideModal?.();
|
||||
};
|
||||
|
||||
const renderOperatorItem = (operator: Operator) => {
|
||||
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 (
|
||||
<Accordion
|
||||
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']}
|
||||
>
|
||||
<AccordionItem value="item-1">
|
||||
@ -76,6 +132,8 @@ function AccordionOperators() {
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorItemList
|
||||
operators={[Operator.Agent, Operator.Retrieval]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@ -84,6 +142,8 @@ function AccordionOperators() {
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorItemList
|
||||
operators={[Operator.Message, Operator.UserFillUp]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@ -96,6 +156,8 @@ function AccordionOperators() {
|
||||
Operator.Iteration,
|
||||
Operator.Categorize,
|
||||
]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@ -106,6 +168,8 @@ function AccordionOperators() {
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorItemList
|
||||
operators={[Operator.Code, Operator.StringTransform]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@ -129,6 +193,8 @@ function AccordionOperators() {
|
||||
Operator.Invoke,
|
||||
Operator.WenCai,
|
||||
]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@ -139,9 +205,69 @@ function AccordionOperators() {
|
||||
export function InnerNextStepDropdown({
|
||||
children,
|
||||
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 (
|
||||
<DropdownMenu open onOpenChange={hideModal}>
|
||||
<DropdownMenu
|
||||
open={true}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && hideModal) {
|
||||
hideModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
Reference in New Issue
Block a user