mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-26 17:16:52 +08:00
### 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)
This commit is contained in:
@ -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
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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
web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx
Normal file
111
web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user