mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-26 17:16:52 +08:00
Feat: Keep connection status during generating agent by drag and drop … (#10141)
### 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)
This commit is contained in:
@ -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(
|
||||
|
||||
200
web/src/pages/agent/hooks/use-connection-drag.ts
Normal file
200
web/src/pages/agent/hooks/use-connection-drag.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { Connection, Position } from '@xyflow/react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useDropdownManager } from '../canvas/context';
|
||||
import { Operator, PREVENT_CLOSE_DELAY } from '../constant';
|
||||
import { useAddNode } from './use-add-node';
|
||||
|
||||
interface ConnectionStartParams {
|
||||
nodeId: string;
|
||||
handleId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection drag management Hook
|
||||
* Responsible for handling connection drag start and end logic
|
||||
*/
|
||||
export const useConnectionDrag = (
|
||||
reactFlowInstance: any,
|
||||
onConnect: (connection: Connection) => void,
|
||||
showModal: () => void,
|
||||
hideModal: () => void,
|
||||
setDropdownPosition: (position: { x: number; y: number }) => void,
|
||||
setCreatedPlaceholderRef: (nodeId: string | null) => void,
|
||||
calculateDropdownPosition: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
) => { x: number; y: number },
|
||||
removePlaceholderNode: () => void,
|
||||
clearActiveDropdown: () => void,
|
||||
) => {
|
||||
// Reference for whether connection is established
|
||||
const isConnectedRef = useRef(false);
|
||||
// Reference for connection start parameters
|
||||
const connectionStartRef = useRef<ConnectionStartParams | null>(null);
|
||||
// Reference to prevent immediate close
|
||||
const preventCloseRef = useRef(false);
|
||||
// Reference to track mouse position for click detection
|
||||
const mouseStartPosRef = useRef<{ x: number; y: number } | null>(null);
|
||||
|
||||
const { addCanvasNode } = useAddNode(reactFlowInstance);
|
||||
const { setActiveDropdown } = useDropdownManager();
|
||||
|
||||
/**
|
||||
* Connection start handler function
|
||||
*/
|
||||
const onConnectStart = useCallback((event: any, params: any) => {
|
||||
isConnectedRef.current = false;
|
||||
|
||||
// Record mouse start position to detect click vs drag
|
||||
if ('clientX' in event && 'clientY' in event) {
|
||||
mouseStartPosRef.current = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
|
||||
if (params && params.nodeId && params.handleId) {
|
||||
connectionStartRef.current = {
|
||||
nodeId: params.nodeId,
|
||||
handleId: params.handleId,
|
||||
};
|
||||
} else {
|
||||
connectionStartRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Connection end handler function
|
||||
*/
|
||||
const onConnectEnd = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
if ('clientX' in event && 'clientY' in event) {
|
||||
const { clientX, clientY } = event;
|
||||
setDropdownPosition({ x: clientX, y: clientY });
|
||||
|
||||
if (!isConnectedRef.current && connectionStartRef.current) {
|
||||
// Check mouse movement distance to distinguish click from drag
|
||||
let isHandleClick = false;
|
||||
if (mouseStartPosRef.current) {
|
||||
const movementDistance = Math.sqrt(
|
||||
Math.pow(clientX - mouseStartPosRef.current.x, 2) +
|
||||
Math.pow(clientY - mouseStartPosRef.current.y, 2),
|
||||
);
|
||||
isHandleClick = movementDistance < 5; // Consider clicks within 5px as handle clicks
|
||||
}
|
||||
|
||||
if (isHandleClick) {
|
||||
connectionStartRef.current = null;
|
||||
mouseStartPosRef.current = null;
|
||||
return;
|
||||
}
|
||||
// Create placeholder node and establish connection
|
||||
const mockEvent = { clientX, clientY };
|
||||
const contextData = {
|
||||
nodeId: connectionStartRef.current.nodeId,
|
||||
id: connectionStartRef.current.handleId,
|
||||
type: 'source' as const,
|
||||
position: Position.Right,
|
||||
isFromConnectionDrag: true,
|
||||
};
|
||||
|
||||
// Use Placeholder operator to create node
|
||||
const newNodeId = addCanvasNode(
|
||||
Operator.Placeholder,
|
||||
contextData,
|
||||
)(mockEvent);
|
||||
|
||||
// Record the created placeholder node ID
|
||||
if (newNodeId) {
|
||||
setCreatedPlaceholderRef(newNodeId);
|
||||
}
|
||||
|
||||
// Calculate placeholder node position and display dropdown menu
|
||||
if (newNodeId && reactFlowInstance) {
|
||||
const dropdownScreenPosition = calculateDropdownPosition(
|
||||
clientX,
|
||||
clientY,
|
||||
);
|
||||
|
||||
setDropdownPosition({
|
||||
x: dropdownScreenPosition.x,
|
||||
y: dropdownScreenPosition.y,
|
||||
});
|
||||
|
||||
setActiveDropdown('drag');
|
||||
showModal();
|
||||
preventCloseRef.current = true;
|
||||
setTimeout(() => {
|
||||
preventCloseRef.current = false;
|
||||
}, PREVENT_CLOSE_DELAY);
|
||||
}
|
||||
|
||||
// Reset connection state
|
||||
connectionStartRef.current = null;
|
||||
mouseStartPosRef.current = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
setDropdownPosition,
|
||||
addCanvasNode,
|
||||
setCreatedPlaceholderRef,
|
||||
reactFlowInstance,
|
||||
calculateDropdownPosition,
|
||||
setActiveDropdown,
|
||||
showModal,
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Connection establishment handler function
|
||||
*/
|
||||
const handleConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
onConnect(connection);
|
||||
isConnectedRef.current = true;
|
||||
},
|
||||
[onConnect],
|
||||
);
|
||||
|
||||
/**
|
||||
* Get connection start context data
|
||||
*/
|
||||
const getConnectionStartContext = useCallback(() => {
|
||||
if (!connectionStartRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
nodeId: connectionStartRef.current.nodeId,
|
||||
id: connectionStartRef.current.handleId,
|
||||
type: 'source' as const,
|
||||
position: Position.Right,
|
||||
isFromConnectionDrag: true,
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Check if close should be prevented
|
||||
*/
|
||||
const shouldPreventClose = useCallback(() => {
|
||||
return preventCloseRef.current;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle canvas move/zoom events
|
||||
* Hide dropdown and remove placeholder when user scrolls or moves canvas
|
||||
*/
|
||||
const onMove = useCallback(() => {
|
||||
// Clean up placeholder and dropdown when canvas moves/zooms
|
||||
removePlaceholderNode();
|
||||
hideModal();
|
||||
clearActiveDropdown();
|
||||
}, [removePlaceholderNode, hideModal, clearActiveDropdown]);
|
||||
|
||||
return {
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
handleConnect,
|
||||
getConnectionStartContext,
|
||||
shouldPreventClose,
|
||||
onMove,
|
||||
};
|
||||
};
|
||||
106
web/src/pages/agent/hooks/use-dropdown-position.ts
Normal file
106
web/src/pages/agent/hooks/use-dropdown-position.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
HALF_PLACEHOLDER_NODE_HEIGHT,
|
||||
HALF_PLACEHOLDER_NODE_WIDTH,
|
||||
} from '../constant';
|
||||
|
||||
/**
|
||||
* Dropdown position calculation Hook
|
||||
* Responsible for calculating dropdown menu position relative to placeholder node
|
||||
*/
|
||||
export const useDropdownPosition = (reactFlowInstance: any) => {
|
||||
/**
|
||||
* Calculate dropdown menu position
|
||||
* @param clientX Mouse click screen X coordinate
|
||||
* @param clientY Mouse click screen Y coordinate
|
||||
* @returns Dropdown menu screen coordinates
|
||||
*/
|
||||
const calculateDropdownPosition = useCallback(
|
||||
(clientX: number, clientY: number) => {
|
||||
if (!reactFlowInstance) {
|
||||
return { x: clientX, y: clientY };
|
||||
}
|
||||
|
||||
// Convert screen coordinates to flow coordinates
|
||||
const placeholderNodePosition = reactFlowInstance.screenToFlowPosition({
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
});
|
||||
|
||||
// Calculate dropdown position in flow coordinate system
|
||||
const dropdownFlowPosition = {
|
||||
x: placeholderNodePosition.x - HALF_PLACEHOLDER_NODE_WIDTH, // Placeholder node left-aligned offset
|
||||
y: placeholderNodePosition.y + HALF_PLACEHOLDER_NODE_HEIGHT, // Placeholder node height plus spacing
|
||||
};
|
||||
|
||||
// Convert flow coordinates back to screen coordinates
|
||||
const dropdownScreenPosition =
|
||||
reactFlowInstance.flowToScreenPosition(dropdownFlowPosition);
|
||||
|
||||
return {
|
||||
x: dropdownScreenPosition.x,
|
||||
y: dropdownScreenPosition.y,
|
||||
};
|
||||
},
|
||||
[reactFlowInstance],
|
||||
);
|
||||
|
||||
/**
|
||||
* Calculate placeholder node flow coordinate position
|
||||
* @param clientX Mouse click screen X coordinate
|
||||
* @param clientY Mouse click screen Y coordinate
|
||||
* @returns Placeholder node flow coordinates
|
||||
*/
|
||||
const getPlaceholderNodePosition = useCallback(
|
||||
(clientX: number, clientY: number) => {
|
||||
if (!reactFlowInstance) {
|
||||
return { x: clientX, y: clientY };
|
||||
}
|
||||
|
||||
return reactFlowInstance.screenToFlowPosition({
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
});
|
||||
},
|
||||
[reactFlowInstance],
|
||||
);
|
||||
|
||||
/**
|
||||
* Convert flow coordinates to screen coordinates
|
||||
* @param flowPosition Flow coordinates
|
||||
* @returns Screen coordinates
|
||||
*/
|
||||
const flowToScreenPosition = useCallback(
|
||||
(flowPosition: { x: number; y: number }) => {
|
||||
if (!reactFlowInstance) {
|
||||
return flowPosition;
|
||||
}
|
||||
|
||||
return reactFlowInstance.flowToScreenPosition(flowPosition);
|
||||
},
|
||||
[reactFlowInstance],
|
||||
);
|
||||
|
||||
/**
|
||||
* Convert screen coordinates to flow coordinates
|
||||
* @param screenPosition Screen coordinates
|
||||
* @returns Flow coordinates
|
||||
*/
|
||||
const screenToFlowPosition = useCallback(
|
||||
(screenPosition: { x: number; y: number }) => {
|
||||
if (!reactFlowInstance) {
|
||||
return screenPosition;
|
||||
}
|
||||
|
||||
return reactFlowInstance.screenToFlowPosition(screenPosition);
|
||||
},
|
||||
[reactFlowInstance],
|
||||
);
|
||||
|
||||
return {
|
||||
calculateDropdownPosition,
|
||||
getPlaceholderNodePosition,
|
||||
flowToScreenPosition,
|
||||
screenToFlowPosition,
|
||||
};
|
||||
};
|
||||
141
web/src/pages/agent/hooks/use-placeholder-manager.ts
Normal file
141
web/src/pages/agent/hooks/use-placeholder-manager.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import useGraphStore from '../store';
|
||||
|
||||
/**
|
||||
* Placeholder node management Hook
|
||||
* Responsible for managing placeholder node creation, deletion, and state tracking
|
||||
*/
|
||||
export const usePlaceholderManager = (reactFlowInstance: any) => {
|
||||
// Reference to the created placeholder node ID
|
||||
const createdPlaceholderRef = useRef<string | null>(null);
|
||||
// Flag indicating whether user has selected a node
|
||||
const userSelectedNodeRef = useRef(false);
|
||||
|
||||
/**
|
||||
* Function to remove placeholder node
|
||||
* Called when user clicks blank area or cancels operation
|
||||
*/
|
||||
const removePlaceholderNode = useCallback(() => {
|
||||
if (
|
||||
createdPlaceholderRef.current &&
|
||||
reactFlowInstance &&
|
||||
!userSelectedNodeRef.current
|
||||
) {
|
||||
const { nodes, edges } = useGraphStore.getState();
|
||||
|
||||
// Remove edges related to placeholder
|
||||
const edgesToRemove = edges.filter(
|
||||
(edge) =>
|
||||
edge.target === createdPlaceholderRef.current ||
|
||||
edge.source === createdPlaceholderRef.current,
|
||||
);
|
||||
|
||||
// Remove placeholder node
|
||||
const nodesToRemove = nodes.filter(
|
||||
(node) => node.id === createdPlaceholderRef.current,
|
||||
);
|
||||
|
||||
if (nodesToRemove.length > 0 || edgesToRemove.length > 0) {
|
||||
reactFlowInstance.deleteElements({
|
||||
nodes: nodesToRemove,
|
||||
edges: edgesToRemove,
|
||||
});
|
||||
}
|
||||
|
||||
createdPlaceholderRef.current = null;
|
||||
}
|
||||
|
||||
// Reset user selection flag
|
||||
userSelectedNodeRef.current = false;
|
||||
}, [reactFlowInstance]);
|
||||
|
||||
/**
|
||||
* User node selection callback
|
||||
* Called when user selects a node type from dropdown menu
|
||||
*/
|
||||
const onNodeCreated = useCallback(
|
||||
(newNodeId: string) => {
|
||||
// First establish connection between new node and source, then delete placeholder
|
||||
if (createdPlaceholderRef.current && reactFlowInstance) {
|
||||
const { nodes, edges, addEdge, updateNode } = useGraphStore.getState();
|
||||
|
||||
// Find placeholder node to get its position
|
||||
const placeholderNode = nodes.find(
|
||||
(node) => node.id === createdPlaceholderRef.current,
|
||||
);
|
||||
|
||||
// Find placeholder-related connection and get source node info
|
||||
const placeholderEdge = edges.find(
|
||||
(edge) => edge.target === createdPlaceholderRef.current,
|
||||
);
|
||||
|
||||
// Update new node position to match placeholder position
|
||||
if (placeholderNode) {
|
||||
const newNode = nodes.find((node) => node.id === newNodeId);
|
||||
if (newNode) {
|
||||
updateNode({
|
||||
...newNode,
|
||||
position: placeholderNode.position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (placeholderEdge) {
|
||||
// Establish connection between new node and source node
|
||||
addEdge({
|
||||
source: placeholderEdge.source,
|
||||
target: newNodeId,
|
||||
sourceHandle: placeholderEdge.sourceHandle || null,
|
||||
targetHandle: placeholderEdge.targetHandle || null,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove placeholder node and related connections
|
||||
const edgesToRemove = edges.filter(
|
||||
(edge) =>
|
||||
edge.target === createdPlaceholderRef.current ||
|
||||
edge.source === createdPlaceholderRef.current,
|
||||
);
|
||||
|
||||
const nodesToRemove = nodes.filter(
|
||||
(node) => node.id === createdPlaceholderRef.current,
|
||||
);
|
||||
|
||||
if (nodesToRemove.length > 0 || edgesToRemove.length > 0) {
|
||||
reactFlowInstance.deleteElements({
|
||||
nodes: nodesToRemove,
|
||||
edges: edgesToRemove,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mark that user has selected a node
|
||||
userSelectedNodeRef.current = true;
|
||||
createdPlaceholderRef.current = null;
|
||||
},
|
||||
[reactFlowInstance],
|
||||
);
|
||||
|
||||
/**
|
||||
* Set the created placeholder node ID
|
||||
*/
|
||||
const setCreatedPlaceholderRef = useCallback((nodeId: string | null) => {
|
||||
createdPlaceholderRef.current = nodeId;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Reset user selection flag
|
||||
*/
|
||||
const resetUserSelectedFlag = useCallback(() => {
|
||||
userSelectedNodeRef.current = false;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
removePlaceholderNode,
|
||||
onNodeCreated,
|
||||
setCreatedPlaceholderRef,
|
||||
resetUserSelectedFlag,
|
||||
createdPlaceholderRef: createdPlaceholderRef.current,
|
||||
userSelectedNodeRef: userSelectedNodeRef.current,
|
||||
};
|
||||
};
|
||||
@ -61,7 +61,7 @@ export const useShowSingleDebugDrawer = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const ExcludedNodes = [Operator.Note];
|
||||
const ExcludedNodes = [Operator.Note, Operator.Placeholder];
|
||||
|
||||
export function useShowDrawer({
|
||||
drawerVisible,
|
||||
|
||||
Reference in New Issue
Block a user