mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): Improve added block positioning logic to handle collisions and dynamic dimensions size (#8406)
This commit is contained in:
@@ -26,8 +26,12 @@ import {
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { CustomNode } from "./CustomNode";
|
||||
import "./flow.css";
|
||||
import { Link } from "@/lib/autogpt-server-api";
|
||||
import { getTypeColor, filterBlocksByType } from "@/lib/utils";
|
||||
import { BlockUIType, Link } from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
getTypeColor,
|
||||
filterBlocksByType,
|
||||
findNewlyAddedBlockCoordinates,
|
||||
} from "@/lib/utils";
|
||||
import { history } from "./history";
|
||||
import { CustomEdge } from "./CustomEdge";
|
||||
import ConnectionLine from "./ConnectionLine";
|
||||
@@ -57,6 +61,15 @@ type FlowContextType = {
|
||||
getNextNodeId: () => string;
|
||||
};
|
||||
|
||||
export type NodeDimension = {
|
||||
[nodeId: string]: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const FlowContext = createContext<FlowContextType | null>(null);
|
||||
|
||||
const FlowEditor: React.FC<{
|
||||
@@ -64,8 +77,14 @@ const FlowEditor: React.FC<{
|
||||
template?: boolean;
|
||||
className?: string;
|
||||
}> = ({ flowID, template, className }) => {
|
||||
const { addNodes, addEdges, getNode, deleteElements, updateNode } =
|
||||
useReactFlow<CustomNode, CustomEdge>();
|
||||
const {
|
||||
addNodes,
|
||||
addEdges,
|
||||
getNode,
|
||||
deleteElements,
|
||||
updateNode,
|
||||
setViewport,
|
||||
} = useReactFlow<CustomNode, CustomEdge>();
|
||||
const [nodeId, setNodeId] = useState<number>(1);
|
||||
const [copiedNodes, setCopiedNodes] = useState<CustomNode[]>([]);
|
||||
const [copiedEdges, setCopiedEdges] = useState<CustomEdge[]>([]);
|
||||
@@ -110,6 +129,9 @@ const FlowEditor: React.FC<{
|
||||
|
||||
const TUTORIAL_STORAGE_KEY = "shepherd-tour";
|
||||
|
||||
// It stores the dimension of all nodes with position as well
|
||||
const [nodeDimensions, setNodeDimensions] = useState<NodeDimension>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (params.get("resetTutorial") === "true") {
|
||||
localStorage.removeItem(TUTORIAL_STORAGE_KEY);
|
||||
@@ -402,16 +424,36 @@ const FlowEditor: React.FC<{
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the center of the viewport considering zoom
|
||||
const viewportCenter = {
|
||||
x: (window.innerWidth / 2 - x) / zoom,
|
||||
y: (window.innerHeight / 2 - y) / zoom,
|
||||
};
|
||||
/*
|
||||
Calculate a position to the right of the newly added block, allowing for some margin.
|
||||
If adding to the right side causes the new block to collide with an existing block, attempt to place it at the bottom or left.
|
||||
Why not the top? Because the height of the new block is unknown.
|
||||
If it still collides, run a loop to find the best position where it does not collide.
|
||||
Then, adjust the canvas to center on the newly added block.
|
||||
Note: The width is known, e.g., w = 300px for a note and w = 500px for others, but the height is dynamic.
|
||||
*/
|
||||
|
||||
// Alternative: We could also use D3 force, Intersection for this (React flow Pro examples)
|
||||
|
||||
const viewportCoordinates =
|
||||
nodeDimensions && Object.keys(nodeDimensions).length > 0
|
||||
? // we will get all the dimension of nodes, then store
|
||||
findNewlyAddedBlockCoordinates(
|
||||
nodeDimensions,
|
||||
(nodeSchema.uiType == BlockUIType.NOTE ? 300 : 500) / zoom,
|
||||
60 / zoom,
|
||||
zoom,
|
||||
)
|
||||
: // we will get all the dimension of nodes, then store
|
||||
{
|
||||
x: (window.innerWidth / 2 - x) / zoom,
|
||||
y: (window.innerHeight / 2 - y) / zoom,
|
||||
};
|
||||
|
||||
const newNode: CustomNode = {
|
||||
id: nodeId.toString(),
|
||||
type: "custom",
|
||||
position: viewportCenter, // Set the position to the calculated viewport center
|
||||
position: viewportCoordinates, // Set the position to the calculated viewport center
|
||||
data: {
|
||||
blockType: nodeType,
|
||||
blockCosts: nodeSchema.costs,
|
||||
@@ -433,6 +475,15 @@ const FlowEditor: React.FC<{
|
||||
setNodeId((prevId) => prevId + 1);
|
||||
clearNodesStatusAndOutput(); // Clear status and output when a new node is added
|
||||
|
||||
setViewport(
|
||||
{
|
||||
x: -viewportCoordinates.x * zoom + window.innerWidth / 2,
|
||||
y: -viewportCoordinates.y * zoom + window.innerHeight / 2 - 100,
|
||||
zoom: 0.8,
|
||||
},
|
||||
{ duration: 500 },
|
||||
);
|
||||
|
||||
history.push({
|
||||
type: "ADD_NODE",
|
||||
payload: { node: { ...newNode, ...newNode.data } },
|
||||
@@ -442,8 +493,10 @@ const FlowEditor: React.FC<{
|
||||
},
|
||||
[
|
||||
nodeId,
|
||||
setViewport,
|
||||
availableNodes,
|
||||
addNodes,
|
||||
nodeDimensions,
|
||||
deleteElements,
|
||||
clearNodesStatusAndOutput,
|
||||
x,
|
||||
@@ -452,6 +505,38 @@ const FlowEditor: React.FC<{
|
||||
],
|
||||
);
|
||||
|
||||
const findNodeDimensions = useCallback(() => {
|
||||
const newNodeDimensions: NodeDimension = nodes.reduce((acc, node) => {
|
||||
const nodeElement = document.querySelector(
|
||||
`[data-id="custom-node-${node.id}"]`,
|
||||
);
|
||||
if (nodeElement) {
|
||||
const rect = nodeElement.getBoundingClientRect();
|
||||
const { left, top, width, height } = rect;
|
||||
|
||||
// Convert screen coordinates to flow coordinates
|
||||
const flowX = (left - x) / zoom;
|
||||
const flowY = (top - y) / zoom;
|
||||
const flowWidth = width / zoom;
|
||||
const flowHeight = height / zoom;
|
||||
|
||||
acc[node.id] = {
|
||||
x: flowX,
|
||||
y: flowY,
|
||||
width: flowWidth,
|
||||
height: flowHeight,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {} as NodeDimension);
|
||||
|
||||
setNodeDimensions(newNodeDimensions);
|
||||
}, [nodes, x, y, zoom]);
|
||||
|
||||
useEffect(() => {
|
||||
findNodeDimensions();
|
||||
}, [nodes, findNodeDimensions]);
|
||||
|
||||
const handleUndo = () => {
|
||||
history.undo();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Category } from "./autogpt-server-api/types";
|
||||
import { NodeDimension } from "@/components/Flow";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -213,3 +214,78 @@ export function getBehaveAs(): BehaveAs {
|
||||
? BehaveAs.CLOUD
|
||||
: BehaveAs.LOCAL;
|
||||
}
|
||||
|
||||
function rectanglesOverlap(
|
||||
rect1: { x: number; y: number; width: number; height?: number },
|
||||
rect2: { x: number; y: number; width: number; height?: number },
|
||||
): boolean {
|
||||
const x1 = rect1.x,
|
||||
y1 = rect1.y,
|
||||
w1 = rect1.width,
|
||||
h1 = rect1.height ?? 100;
|
||||
const x2 = rect2.x,
|
||||
y2 = rect2.y,
|
||||
w2 = rect2.width,
|
||||
h2 = rect2.height ?? 100;
|
||||
|
||||
// Check if the rectangles do not overlap
|
||||
return !(x1 + w1 <= x2 || x1 >= x2 + w2 || y1 + h1 <= y2 || y1 >= y2 + h2);
|
||||
}
|
||||
|
||||
export function findNewlyAddedBlockCoordinates(
|
||||
nodeDimensions: NodeDimension,
|
||||
newWidth: number,
|
||||
margin: number,
|
||||
zoom: number,
|
||||
) {
|
||||
const nodeDimensionArray = Object.values(nodeDimensions);
|
||||
|
||||
for (let i = nodeDimensionArray.length - 1; i >= 0; i--) {
|
||||
const lastNode = nodeDimensionArray[i];
|
||||
const lastNodeHeight = lastNode.height ?? 100;
|
||||
|
||||
// Right of the last node
|
||||
let newX = lastNode.x + lastNode.width + margin;
|
||||
let newY = lastNode.y;
|
||||
let newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };
|
||||
|
||||
const collisionRight = nodeDimensionArray.some((node) =>
|
||||
rectanglesOverlap(newRect, node),
|
||||
);
|
||||
|
||||
if (!collisionRight) {
|
||||
return { x: newX, y: newY };
|
||||
}
|
||||
|
||||
// Left of the last node
|
||||
newX = lastNode.x - newWidth - margin;
|
||||
newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };
|
||||
|
||||
const collisionLeft = nodeDimensionArray.some((node) =>
|
||||
rectanglesOverlap(newRect, node),
|
||||
);
|
||||
|
||||
if (!collisionLeft) {
|
||||
return { x: newX, y: newY };
|
||||
}
|
||||
|
||||
// Below the last node
|
||||
newX = lastNode.x;
|
||||
newY = lastNode.y + lastNodeHeight + margin;
|
||||
newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };
|
||||
|
||||
const collisionBelow = nodeDimensionArray.some((node) =>
|
||||
rectanglesOverlap(newRect, node),
|
||||
);
|
||||
|
||||
if (!collisionBelow) {
|
||||
return { x: newX, y: newY };
|
||||
}
|
||||
}
|
||||
|
||||
// Default position if no space is found
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user