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:
FatMii
2025-08-14 17:48:02 +08:00
committed by GitHub
parent 762aa4b8c4
commit 618d6bc924
7 changed files with 366 additions and 59 deletions

View File

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

View File

@ -4,6 +4,7 @@ import { Handle, HandleProps } from '@xyflow/react';
import { Plus } from 'lucide-react';
import { useMemo } from 'react';
import { HandleContext } from '../../context';
import { useDropdownManager } from '../context';
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
export function CommonHandle({
@ -13,12 +14,16 @@ export function CommonHandle({
}: HandleProps & { nodeId: string }) {
const { visible, hideModal, showModal } = useSetModalState();
const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
useDropdownManager();
const value = useMemo(
() => ({
nodeId,
id: props.id,
id: props.id || undefined,
type: props.type,
position: props.position,
isFromConnectionDrag: false,
}),
[nodeId, props.id, props.position, props.type],
);
@ -33,12 +38,23 @@ export function CommonHandle({
)}
onClick={(e) => {
e.stopPropagation();
if (!canShowDropdown()) {
return;
}
setActiveDropdown('handle');
showModal();
}}
>
<Plus className="size-3 pointer-events-none text-text-title-invert" />
{visible && (
<InnerNextStepDropdown hideModal={hideModal}>
<InnerNextStepDropdown
hideModal={() => {
hideModal();
clearActiveDropdown();
}}
>
<span></span>
</InnerNextStepDropdown>
)}