From f341dc03b8cf989c2777524f0d86d34ae800a3f3 Mon Sep 17 00:00:00 2001 From: FatMii <39074672+FatMii@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:12:12 +0800 Subject: [PATCH] Feature/agent UI style optimization (#10385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What problem does this PR solve? Hi team, @ZhenhangTung @KevinHuSh @cike8899 About #10384 , I've completed the UI optimization adjustments for the Agent page according to our previous discussions and the design draft sketches provided by @Naomi. The main modifications include: 1. Adjusted the style and content of placeholder-node. 2. Adjusted the location of the dropdown (to the right of the placeholder-node) . 3. Adjusted the tooltip position spacing when the mouse hovers in the dropdown menu. 4. Hides the thick scroll bar on the dropdown component. 5. Highlight the connection line when dragging to generate a placeholder-node Image Please review the related code modifications when you have time. Let me know if further adjustments are needed! Thanks! ### Type of change - [x] Other (please describe): UI Enhancement --------- Co-authored-by: leonlai --- web/src/pages/agent/canvas/edge/index.tsx | 18 +++++- web/src/pages/agent/canvas/index.tsx | 9 ++- .../node/dropdown/next-step-dropdown.tsx | 6 +- web/src/pages/agent/canvas/node/index.less | 13 +++++ .../agent/canvas/node/placeholder-node.tsx | 30 ++++------ web/src/pages/agent/constant.tsx | 2 + web/src/pages/agent/hooks/use-build-dsl.ts | 27 ++++++++- .../pages/agent/hooks/use-connection-drag.ts | 29 +++++++++- .../agent/hooks/use-dropdown-position.ts | 10 +++- .../agent/hooks/use-placeholder-manager.ts | 57 ++++++++++++++++++- web/src/pages/agent/store.ts | 9 ++- web/src/pages/agent/utils.ts | 2 +- 12 files changed, 174 insertions(+), 38 deletions(-) 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(