From 8426cbbd0287048472821d7f2c248037bc510972 Mon Sep 17 00:00:00 2001
From: FatMii <39074672+FatMii@users.noreply.github.com>
Date: Mon, 29 Sep 2025 10:28:19 +0800
Subject: [PATCH] =?UTF-8?q?Feat:=20Keep=20connection=20status=20during=20g?=
=?UTF-8?q?enerating=20agent=20by=20drag=20and=20drop=20=E2=80=A6=20(#1014?=
=?UTF-8?q?1)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
### What problem does this PR solve?
About issue #10140
In version 0.20.1, we implemented the generation of new node through
mouse drag and drop. If we could create a workflow module like in Coze,
where there is not only a dropdown menu but also an intermediate node
(placeholder node) after the drag and drop is completed, this could
improve the user experience.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
---
web/src/pages/agent/canvas/index.tsx | 109 +++++-----
.../agent/canvas/node/placeholder-node.tsx | 47 ++++
web/src/pages/agent/constant.tsx | 16 ++
web/src/pages/agent/hooks/use-add-node.ts | 1 +
.../pages/agent/hooks/use-connection-drag.ts | 200 ++++++++++++++++++
.../agent/hooks/use-dropdown-position.ts | 106 ++++++++++
.../agent/hooks/use-placeholder-manager.ts | 141 ++++++++++++
web/src/pages/agent/hooks/use-show-drawer.tsx | 2 +-
8 files changed, 560 insertions(+), 62 deletions(-)
create mode 100644 web/src/pages/agent/canvas/node/placeholder-node.tsx
create mode 100644 web/src/pages/agent/hooks/use-connection-drag.ts
create mode 100644 web/src/pages/agent/hooks/use-dropdown-position.ts
create mode 100644 web/src/pages/agent/hooks/use-placeholder-manager.ts
diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx
index 74e6d146a..712696b44 100644
--- a/web/src/pages/agent/canvas/index.tsx
+++ b/web/src/pages/agent/canvas/index.tsx
@@ -7,7 +7,6 @@ import {
import { useSetModalState } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import {
- Connection,
ConnectionMode,
ControlButton,
Controls,
@@ -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';
@@ -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';
@@ -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';
@@ -73,6 +76,7 @@ export const nodeTypes: NodeTypes = {
ragNode: RagNode,
categorizeNode: CategorizeNode,
beginNode: BeginNode,
+ placeholderNode: PlaceholderNode,
relevantNode: RelevantNode,
logicNode: LogicNode,
noteNode: NoteNode,
@@ -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();
}
@@ -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 (