Skip to content
109 changes: 48 additions & 61 deletions web/src/pages/agent/canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import { useSetModalState } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import {
Connection,
ConnectionMode,
ControlButton,
Controls,
Expand All @@ -17,7 +16,7 @@ import {
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { NotebookPen } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChatSheet } from '../chat/chat-sheet';
import { AgentBackground } from '../components/background';
Expand All @@ -37,7 +36,10 @@ import {
import { useAddNode } from '../hooks/use-add-node';
import { useBeforeDelete } from '../hooks/use-before-delete';
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
import { useConnectionDrag } from '../hooks/use-connection-drag';
import { useDropdownPosition } from '../hooks/use-dropdown-position';
import { useMoveNote } from '../hooks/use-move-note';
import { usePlaceholderManager } from '../hooks/use-placeholder-manager';
import { useDropdownManager } from './context';

import Spotlight from '@/components/spotlight';
Expand All @@ -62,6 +64,7 @@ import { KeywordNode } from './node/keyword-node';
import { LogicNode } from './node/logic-node';
import { MessageNode } from './node/message-node';
import NoteNode from './node/note-node';
import { PlaceholderNode } from './node/placeholder-node';
import { RelevantNode } from './node/relevant-node';
import { RetrievalNode } from './node/retrieval-node';
import { RewriteNode } from './node/rewrite-node';
Expand All @@ -73,6 +76,7 @@ export const nodeTypes: NodeTypes = {
ragNode: RagNode,
categorizeNode: CategorizeNode,
beginNode: BeginNode,
placeholderNode: PlaceholderNode,
relevantNode: RelevantNode,
logicNode: LogicNode,
noteNode: NoteNode,
Expand Down Expand Up @@ -176,19 +180,36 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
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 { clearActiveDropdown } = useDropdownManager();

const preventCloseRef = useRef(false);
const { removePlaceholderNode, onNodeCreated, setCreatedPlaceholderRef } =
usePlaceholderManager(reactFlowInstance);

const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
const { calculateDropdownPosition } = useDropdownPosition(reactFlowInstance);

const {
onConnectStart,
onConnectEnd,
handleConnect,
getConnectionStartContext,
shouldPreventClose,
onMove,
} = useConnectionDrag(
reactFlowInstance,
originalOnConnect,
showModal,
hideModal,
setDropdownPosition,
setCreatedPlaceholderRef,
calculateDropdownPosition,
removePlaceholderNode,
clearActiveDropdown,
);

const onPaneClick = useCallback(() => {
hideFormDrawer();
if (visible && !preventCloseRef.current) {
if (visible && !shouldPreventClose()) {
removePlaceholderNode();
hideModal();
clearActiveDropdown();
}
Expand All @@ -199,55 +220,16 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
}, [
hideFormDrawer,
visible,
shouldPreventClose,
hideModal,
imgVisible,
addNoteNode,
mouse,
hideImage,
clearActiveDropdown,
removePlaceholderNode,
]);

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) => {
const target = event.target as HTMLElement;
// Clicking Handle will also trigger OnConnectEnd.
// To solve the problem that the operator on the right side added by clicking Handle will overlap with the original operator, this event is blocked here.
// TODO: However, a better way is to add both operators in the same way as OnConnectEnd.
if (target?.classList.contains('react-flow__handle')) {
return;
}

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 (
<div className={styles.canvasWrapper}>
<svg
Expand Down Expand Up @@ -278,12 +260,13 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
edges={edges}
onEdgesChange={onEdgesChange}
fitView
onConnect={onConnect}
onConnect={handleConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onDrop={onDrop}
onConnectStart={OnConnectStart}
onConnectEnd={OnConnectEnd}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onMove={onMove}
onDragOver={onDragOver}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
Expand Down Expand Up @@ -324,20 +307,24 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
</ReactFlow>
{visible && (
<HandleContext.Provider
value={{
nodeId: connectionStartRef.current?.nodeId || '',
id: connectionStartRef.current?.handleId || '',
type: 'source',
position: Position.Right,
isFromConnectionDrag: true,
}}
value={
getConnectionStartContext() || {
nodeId: '',
id: '',
type: 'source',
position: Position.Right,
isFromConnectionDrag: true,
}
}
>
<InnerNextStepDropdown
hideModal={() => {
removePlaceholderNode();
hideModal();
clearActiveDropdown();
}}
position={dropdownPosition}
onNodeCreated={onNodeCreated}
>
<span></span>
</InnerNextStepDropdown>
Expand Down
47 changes: 47 additions & 0 deletions web/src/pages/agent/canvas/node/placeholder-node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { cn } from '@/lib/utils';
import { NodeProps, Position } from '@xyflow/react';
import { Skeleton } from 'antd';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { NodeHandleId, Operator } from '../../constant';
import OperatorIcon from '../../operator-icon';
import { CommonHandle } from './handle';
import { LeftHandleStyle } from './handle-icon';
import styles from './index.less';
import { NodeWrapper } from './node-wrapper';

function InnerPlaceholderNode({ data, id, selected }: NodeProps) {
const { t } = useTranslation();

return (
<NodeWrapper selected={selected}>
<CommonHandle
type="target"
position={Position.Left}
isConnectable
style={LeftHandleStyle}
nodeId={id}
id={NodeHandleId.End}
></CommonHandle>

<section className="flex items-center gap-2">
<OperatorIcon name={data.label as Operator}></OperatorIcon>
<div className="truncate text-center font-semibold text-sm">
{t(`flow.placeholder`, 'Placeholder')}
</div>
</section>

<section
className={cn(styles.generateParameters, 'flex gap-2 flex-col mt-2')}
>
<Skeleton active paragraph={{ rows: 2 }} title={false} />
<div className="flex gap-2">
<Skeleton.Button active size="small" />
<Skeleton.Button active size="small" />
</div>
</section>
</NodeWrapper>
);
}

export const PlaceholderNode = memo(InnerPlaceholderNode);
16 changes: 16 additions & 0 deletions web/src/pages/agent/constant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export enum Operator {
UserFillUp = 'UserFillUp',
StringTransform = 'StringTransform',
SearXNG = 'SearXNG',
Placeholder = 'Placeholder',
}

export const SwitchLogicOperatorOptions = ['and', 'or'];
Expand Down Expand Up @@ -780,6 +781,11 @@ export const initialTavilyExtractValues = {
},
};

export const initialPlaceholderValues = {
// Placeholder node doesn't need any specific form values
// It's just a visual placeholder
};

export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
Expand Down Expand Up @@ -900,6 +906,7 @@ export const NodeMap = {
[Operator.UserFillUp]: 'ragNode',
[Operator.StringTransform]: 'ragNode',
[Operator.TavilyExtract]: 'ragNode',
[Operator.Placeholder]: 'placeholderNode',
};

export enum BeginQueryType {
Expand Down Expand Up @@ -950,3 +957,12 @@ export enum AgentExceptionMethod {
Comment = 'comment',
Goto = 'goto',
}

export const PLACEHOLDER_NODE_WIDTH = 200;
export const PLACEHOLDER_NODE_HEIGHT = 60;
export const DROPDOWN_SPACING = 25;
export const DROPDOWN_ADDITIONAL_OFFSET = 50;
export const HALF_PLACEHOLDER_NODE_WIDTH = PLACEHOLDER_NODE_WIDTH / 2;
export const HALF_PLACEHOLDER_NODE_HEIGHT =
PLACEHOLDER_NODE_HEIGHT + DROPDOWN_SPACING + DROPDOWN_ADDITIONAL_OFFSET;
export const PREVENT_CLOSE_DELAY = 300;
1 change: 1 addition & 0 deletions web/src/pages/agent/hooks/use-add-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
x: 0,
y: 0,
},
draggable: type === Operator.Placeholder ? false : undefined,
data: {
label: `${type}`,
name: generateNodeNamesWithIncreasingIndex(
Expand Down
Loading