diff --git a/web/src/pages/agent/canvas/edge/index.tsx b/web/src/pages/agent/canvas/edge/index.tsx index 0fa23b3b9..b82f4617e 100644 --- a/web/src/pages/agent/canvas/edge/index.tsx +++ b/web/src/pages/agent/canvas/edge/index.tsx @@ -30,6 +30,10 @@ function InnerButtonEdge({ sourceHandleId, }: EdgeProps>) { const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById); + const highlightedPlaceholderEdgeId = useGraphStore( + (state) => state.highlightedPlaceholderEdgeId, + ); + const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, @@ -42,6 +46,13 @@ function InnerButtonEdge({ return selected ? { strokeWidth: 1, stroke: 'var(--accent-primary)' } : {}; }, [selected]); + const placeholderHighlightStyle = useMemo(() => { + const isHighlighted = highlightedPlaceholderEdgeId === id; + return isHighlighted + ? { strokeWidth: 2, stroke: 'var(--accent-primary)' } + : {}; + }, [highlightedPlaceholderEdgeId, id]); + const onEdgeClick = () => { deleteEdgeById(id); }; @@ -79,7 +90,12 @@ function InnerButtonEdge({ diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx index 712696b44..b36b9a6c5 100644 --- a/web/src/pages/agent/canvas/index.tsx +++ b/web/src/pages/agent/canvas/index.tsx @@ -182,8 +182,12 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { const { clearActiveDropdown } = useDropdownManager(); - const { removePlaceholderNode, onNodeCreated, setCreatedPlaceholderRef } = - usePlaceholderManager(reactFlowInstance); + const { + removePlaceholderNode, + onNodeCreated, + setCreatedPlaceholderRef, + checkAndRemoveExistingPlaceholder, + } = usePlaceholderManager(reactFlowInstance); const { calculateDropdownPosition } = useDropdownPosition(reactFlowInstance); @@ -204,6 +208,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { calculateDropdownPosition, removePlaceholderNode, clearActiveDropdown, + checkAndRemoveExistingPlaceholder, ); const onPaneClick = useCallback(() => { diff --git a/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx b/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx index 1655630cb..bc7bf4577 100644 --- a/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx +++ b/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx @@ -107,7 +107,7 @@ function OperatorItemList({ )} - +

{t(`flow.${lowerFirst(operator)}Description`)}

@@ -127,7 +127,7 @@ function AccordionOperators({ return ( @@ -249,7 +249,7 @@ export function InnerNextStepDropdown({ style={{ position: 'fixed', left: position.x, - top: position.y + 10, + top: position.y, zIndex: 1000, }} onClick={(e) => e.stopPropagation()} diff --git a/web/src/pages/agent/canvas/node/index.less b/web/src/pages/agent/canvas/node/index.less index 14d7e6077..d6b726f16 100644 --- a/web/src/pages/agent/canvas/node/index.less +++ b/web/src/pages/agent/canvas/node/index.less @@ -283,3 +283,16 @@ transform: translateY(0); } } + +.hideScrollbar { + /* Webkit browsers (Chrome, Safari, Edge) */ + &::-webkit-scrollbar { + display: none; + } + + /* Firefox */ + scrollbar-width: none; + + /* IEå’ŒEdge */ + -ms-overflow-style: none; +} diff --git a/web/src/pages/agent/canvas/node/placeholder-node.tsx b/web/src/pages/agent/canvas/node/placeholder-node.tsx index b828a0ab6..7dc0d0fbd 100644 --- a/web/src/pages/agent/canvas/node/placeholder-node.tsx +++ b/web/src/pages/agent/canvas/node/placeholder-node.tsx @@ -1,18 +1,12 @@ -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 { NodeHandleId } from '../../constant'; 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(); - +function InnerPlaceholderNode({ id, selected }: NodeProps) { return (
- -
- {t(`flow.placeholder`, 'Placeholder')} -
+
-
- -
- - -
+
+
); diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index de21ae9e9..3136f6777 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -965,4 +965,6 @@ 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 DROPDOWN_HORIZONTAL_OFFSET = 28; +export const DROPDOWN_VERTICAL_OFFSET = 74; export const PREVENT_CLOSE_DELAY = 300; diff --git a/web/src/pages/agent/hooks/use-build-dsl.ts b/web/src/pages/agent/hooks/use-build-dsl.ts index eb32b2317..f164c67b4 100644 --- a/web/src/pages/agent/hooks/use-build-dsl.ts +++ b/web/src/pages/agent/hooks/use-build-dsl.ts @@ -1,6 +1,7 @@ import { useFetchAgent } from '@/hooks/use-agent-request'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { useCallback } from 'react'; +import { Operator } from '../constant'; import useGraphStore from '../store'; import { buildDslComponentsByGraph } from '../utils'; @@ -10,15 +11,35 @@ export const useBuildDslData = () => { const buildDslData = useCallback( (currentNodes?: RAGFlowNodeType[]) => { + const nodesToProcess = currentNodes ?? nodes; + + // Filter out placeholder nodes and related edges + const filteredNodes = nodesToProcess.filter( + (node) => node.data?.label !== Operator.Placeholder, + ); + + const filteredEdges = edges.filter((edge) => { + const sourceNode = nodesToProcess.find( + (node) => node.id === edge.source, + ); + const targetNode = nodesToProcess.find( + (node) => node.id === edge.target, + ); + return ( + sourceNode?.data?.label !== Operator.Placeholder && + targetNode?.data?.label !== Operator.Placeholder + ); + }); + const dslComponents = buildDslComponentsByGraph( - currentNodes ?? nodes, - edges, + filteredNodes, + filteredEdges, data.dsl.components, ); return { ...data.dsl, - graph: { nodes: currentNodes ?? nodes, edges }, + graph: { nodes: filteredNodes, edges: filteredEdges }, components: dslComponents, }; }, diff --git a/web/src/pages/agent/hooks/use-connection-drag.ts b/web/src/pages/agent/hooks/use-connection-drag.ts index 19e388c64..48ec99986 100644 --- a/web/src/pages/agent/hooks/use-connection-drag.ts +++ b/web/src/pages/agent/hooks/use-connection-drag.ts @@ -2,6 +2,7 @@ import { Connection, Position } from '@xyflow/react'; import { useCallback, useRef } from 'react'; import { useDropdownManager } from '../canvas/context'; import { Operator, PREVENT_CLOSE_DELAY } from '../constant'; +import useGraphStore from '../store'; import { useAddNode } from './use-add-node'; interface ConnectionStartParams { @@ -26,6 +27,7 @@ export const useConnectionDrag = ( ) => { x: number; y: number }, removePlaceholderNode: () => void, clearActiveDropdown: () => void, + checkAndRemoveExistingPlaceholder: () => void, ) => { // Reference for whether connection is established const isConnectedRef = useRef(false); @@ -38,6 +40,7 @@ export const useConnectionDrag = ( const { addCanvasNode } = useAddNode(reactFlowInstance); const { setActiveDropdown } = useDropdownManager(); + const { setHighlightedPlaceholderEdgeId } = useGraphStore(); /** * Connection start handler function @@ -81,10 +84,17 @@ export const useConnectionDrag = ( } if (isHandleClick) { + removePlaceholderNode(); + hideModal(); + clearActiveDropdown(); connectionStartRef.current = null; mouseStartPosRef.current = null; return; } + + // Check and remove existing placeholder-node before creating new one + checkAndRemoveExistingPlaceholder(); + // Create placeholder node and establish connection const mockEvent = { clientX, clientY }; const contextData = { @@ -101,9 +111,13 @@ export const useConnectionDrag = ( contextData, )(mockEvent); - // Record the created placeholder node ID if (newNodeId) { setCreatedPlaceholderRef(newNodeId); + + if (connectionStartRef.current) { + const edgeId = `xy-edge__${connectionStartRef.current.nodeId}${connectionStartRef.current.handleId}-${newNodeId}end`; + setHighlightedPlaceholderEdgeId(edgeId); + } } // Calculate placeholder node position and display dropdown menu @@ -140,6 +154,11 @@ export const useConnectionDrag = ( calculateDropdownPosition, setActiveDropdown, showModal, + setHighlightedPlaceholderEdgeId, + checkAndRemoveExistingPlaceholder, + removePlaceholderNode, + hideModal, + clearActiveDropdown, ], ); @@ -187,7 +206,13 @@ export const useConnectionDrag = ( removePlaceholderNode(); hideModal(); clearActiveDropdown(); - }, [removePlaceholderNode, hideModal, clearActiveDropdown]); + setHighlightedPlaceholderEdgeId(null); + }, [ + removePlaceholderNode, + hideModal, + clearActiveDropdown, + setHighlightedPlaceholderEdgeId, + ]); return { onConnectStart, diff --git a/web/src/pages/agent/hooks/use-dropdown-position.ts b/web/src/pages/agent/hooks/use-dropdown-position.ts index 38f4d5cea..fc3094c7c 100644 --- a/web/src/pages/agent/hooks/use-dropdown-position.ts +++ b/web/src/pages/agent/hooks/use-dropdown-position.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react'; import { - HALF_PLACEHOLDER_NODE_HEIGHT, + DROPDOWN_HORIZONTAL_OFFSET, + DROPDOWN_VERTICAL_OFFSET, HALF_PLACEHOLDER_NODE_WIDTH, } from '../constant'; @@ -29,8 +30,11 @@ export const useDropdownPosition = (reactFlowInstance: any) => { // 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 + x: + placeholderNodePosition.x + + HALF_PLACEHOLDER_NODE_WIDTH + + DROPDOWN_HORIZONTAL_OFFSET, + y: placeholderNodePosition.y - DROPDOWN_VERTICAL_OFFSET, }; // Convert flow coordinates back to screen coordinates diff --git a/web/src/pages/agent/hooks/use-placeholder-manager.ts b/web/src/pages/agent/hooks/use-placeholder-manager.ts index db8527850..9738cfb4a 100644 --- a/web/src/pages/agent/hooks/use-placeholder-manager.ts +++ b/web/src/pages/agent/hooks/use-placeholder-manager.ts @@ -1,4 +1,5 @@ import { useCallback, useRef } from 'react'; +import { Operator } from '../constant'; import useGraphStore from '../store'; /** @@ -11,6 +12,46 @@ export const usePlaceholderManager = (reactFlowInstance: any) => { // Flag indicating whether user has selected a node const userSelectedNodeRef = useRef(false); + /** + * Check if placeholder node exists and remove it if found + * Ensures only one placeholder can exist on the panel + */ + const checkAndRemoveExistingPlaceholder = useCallback(() => { + const { nodes, edges } = useGraphStore.getState(); + + // Find existing placeholder node + const existingPlaceholder = nodes.find( + (node) => node.data?.label === Operator.Placeholder, + ); + + if (existingPlaceholder && reactFlowInstance) { + // Remove edges related to placeholder + const edgesToRemove = edges.filter( + (edge) => + edge.target === existingPlaceholder.id || + edge.source === existingPlaceholder.id, + ); + + // Remove placeholder node + const nodesToRemove = [existingPlaceholder]; + + if (nodesToRemove.length > 0 || edgesToRemove.length > 0) { + reactFlowInstance.deleteElements({ + nodes: nodesToRemove, + edges: edgesToRemove, + }); + } + + // Clear highlighted placeholder edge + useGraphStore.getState().setHighlightedPlaceholderEdgeId(null); + + // Update ref reference + if (createdPlaceholderRef.current === existingPlaceholder.id) { + createdPlaceholderRef.current = null; + } + } + }, [reactFlowInstance]); + /** * Function to remove placeholder node * Called when user clicks blank area or cancels operation @@ -21,7 +62,8 @@ export const usePlaceholderManager = (reactFlowInstance: any) => { reactFlowInstance && !userSelectedNodeRef.current ) { - const { nodes, edges } = useGraphStore.getState(); + const { nodes, edges, setHighlightedPlaceholderEdgeId } = + useGraphStore.getState(); // Remove edges related to placeholder const edgesToRemove = edges.filter( @@ -42,6 +84,8 @@ export const usePlaceholderManager = (reactFlowInstance: any) => { }); } + setHighlightedPlaceholderEdgeId(null); + createdPlaceholderRef.current = null; } @@ -57,7 +101,13 @@ export const usePlaceholderManager = (reactFlowInstance: any) => { (newNodeId: string) => { // First establish connection between new node and source, then delete placeholder if (createdPlaceholderRef.current && reactFlowInstance) { - const { nodes, edges, addEdge, updateNode } = useGraphStore.getState(); + const { + nodes, + edges, + addEdge, + updateNode, + setHighlightedPlaceholderEdgeId, + } = useGraphStore.getState(); // Find placeholder node to get its position const placeholderNode = nodes.find( @@ -107,6 +157,8 @@ export const usePlaceholderManager = (reactFlowInstance: any) => { edges: edgesToRemove, }); } + + setHighlightedPlaceholderEdgeId(null); } // Mark that user has selected a node @@ -135,6 +187,7 @@ export const usePlaceholderManager = (reactFlowInstance: any) => { onNodeCreated, setCreatedPlaceholderRef, resetUserSelectedFlag, + checkAndRemoveExistingPlaceholder, createdPlaceholderRef: createdPlaceholderRef.current, userSelectedNodeRef: userSelectedNodeRef.current, }; diff --git a/web/src/pages/agent/store.ts b/web/src/pages/agent/store.ts index e163ff8cb..16f341b96 100644 --- a/web/src/pages/agent/store.ts +++ b/web/src/pages/agent/store.ts @@ -39,6 +39,7 @@ export type RFState = { selectedEdgeIds: string[]; clickedNodeId: string; // currently selected node clickedToolId: string; // currently selected tool id + highlightedPlaceholderEdgeId: string | null; onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; onEdgeMouseEnter?: EdgeMouseHandler; @@ -89,6 +90,7 @@ export type RFState = { ) => void; // Deleting a condition of a classification operator will delete the related edge findAgentToolNodeById: (id: string | null) => string | undefined; selectNodeIds: (nodeIds: string[]) => void; + setHighlightedPlaceholderEdgeId: (edgeId: string | null) => void; }; // this is our useStore hook that we can use in our components to get parts of the store and call actions @@ -101,6 +103,7 @@ const useGraphStore = create()( selectedEdgeIds: [] as string[], clickedNodeId: '', clickedToolId: '', + highlightedPlaceholderEdgeId: null, onNodesChange: (changes) => { set({ nodes: applyNodeChanges(changes, get().nodes), @@ -127,8 +130,9 @@ const useGraphStore = create()( }, onConnect: (connection: Connection) => { const { updateFormDataOnConnect } = get(); + const newEdges = addEdge(connection, get().edges); set({ - edges: addEdge(connection, get().edges), + edges: newEdges, }); updateFormDataOnConnect(connection); }, @@ -526,6 +530,9 @@ const useGraphStore = create()( })), ); }, + setHighlightedPlaceholderEdgeId: (edgeId) => { + set({ highlightedPlaceholderEdgeId: edgeId }); + }, })), { name: 'graph', trace: true }, ), diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index c36b1b855..a1b392b0e 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -143,7 +143,7 @@ const buildOperatorParams = (operatorName: string) => // initializeOperatorParams(operatorName), // Final processing, for guarantee ); -const ExcludeOperators = [Operator.Note, Operator.Tool]; +const ExcludeOperators = [Operator.Note, Operator.Tool, Operator.Placeholder]; export function isBottomSubAgent(edges: Edge[], nodeId?: string) { const edge = edges.find(