Feature/agent UI style optimization (#10385)

### 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

<img width="1323" height="509" alt="Image"
src="https://github.com/user-attachments/assets/0d366f7f-477d-4c00-bb58-d5d58b3a745f"
/>

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 <leonlai@futurefab.ai>
This commit is contained in:
FatMii 2025-10-09 11:12:12 +08:00 committed by GitHub
parent 4585edc20e
commit f341dc03b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 174 additions and 38 deletions

View File

@ -30,6 +30,10 @@ function InnerButtonEdge({
sourceHandleId,
}: EdgeProps<Edge<{ isHovered: boolean }>>) {
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({
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{ ...style, ...selectedStyle, ...showHighlight }}
style={{
...style,
...selectedStyle,
...showHighlight,
...placeholderHighlightStyle,
}}
className={cn('text-text-secondary')}
/>

View File

@ -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(() => {

View File

@ -107,7 +107,7 @@ function OperatorItemList({
</DropdownMenuItem>
)}
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side="right" sideOffset={24}>
<p>{t(`flow.${lowerFirst(operator)}Description`)}</p>
</TooltipContent>
</Tooltip>
@ -127,7 +127,7 @@ function AccordionOperators({
return (
<Accordion
type="multiple"
className="px-2 text-text-title max-h-[45vh] overflow-auto"
className="px-2 text-text-title max-h-[45vh] overflow-auto scrollbar-none"
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
>
<AccordionItem value="item-1">
@ -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()}

View File

@ -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;
}

View File

@ -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 (
<NodeWrapper selected={selected}>
<CommonHandle
@ -25,20 +19,16 @@ function InnerPlaceholderNode({ data, id, selected }: NodeProps) {
></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>
<Skeleton.Avatar
active
size={24}
shape="square"
style={{ backgroundColor: 'rgba(255,255,255,0.05)' }}
/>
</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 className={'flex gap-2 flex-col'} style={{ marginTop: 10 }}>
<Skeleton.Input active style={{ width: '100%', height: 30 }} />
</section>
</NodeWrapper>
);

View File

@ -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;

View File

@ -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,
};
},

View File

@ -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,

View File

@ -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

View File

@ -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,
};

View File

@ -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<RAGFlowNodeType>;
onEdgesChange: OnEdgesChange;
onEdgeMouseEnter?: EdgeMouseHandler<Edge>;
@ -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<RFState>()(
selectedEdgeIds: [] as string[],
clickedNodeId: '',
clickedToolId: '',
highlightedPlaceholderEdgeId: null,
onNodesChange: (changes) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
@ -127,8 +130,9 @@ const useGraphStore = create<RFState>()(
},
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<RFState>()(
})),
);
},
setHighlightedPlaceholderEdgeId: (edgeId) => {
set({ highlightedPlaceholderEdgeId: edgeId });
},
})),
{ name: 'graph', trace: true },
),

View File

@ -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(