fix(frontend/builder): automatically frame agent when opening in builder (#11640)

## Summary
- Fixed auto-frame timing in new builder - now calls `fitView` after
nodes are rendered instead of on mount
- Replaced manual viewport calculation in legacy builder with React
Flow's `fitView` for consistency
- Both builders now properly center and frame all blocks when opening an
agent

  ## Test plan
- [x] Open an existing agent with multiple blocks in the new builder -
verify all blocks are visible and centered
- [x] Open an existing agent in the legacy builder - verify all blocks
are visible and centered
  - [x] Verify the manual "Frame" button still works correctly
This commit is contained in:
Bently
2025-12-18 19:07:40 +01:00
committed by GitHub
parent c5e8b0b08f
commit eed07b173a
2 changed files with 44 additions and 23 deletions

View File

@@ -20,6 +20,7 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
export const useFlow = () => {
const [isLocked, setIsLocked] = useState(false);
const [hasAutoFramed, setHasAutoFramed] = useState(false);
const addNodes = useNodeStore(useShallow((state) => state.addNodes));
const addLinks = useEdgeStore(useShallow((state) => state.addLinks));
const updateNodeStatus = useNodeStore(
@@ -187,9 +188,36 @@ export const useFlow = () => {
};
}, []);
const linkCount = graph?.links?.length ?? 0;
useEffect(() => {
fitView({ padding: 0.2, duration: 800, maxZoom: 2 });
}, [fitView]);
if (isGraphLoading || isBlocksLoading) {
setHasAutoFramed(false);
return;
}
if (hasAutoFramed) {
return;
}
const rafId = requestAnimationFrame(() => {
fitView({ padding: 0.2, duration: 800, maxZoom: 1 });
setHasAutoFramed(true);
});
return () => cancelAnimationFrame(rafId);
}, [
fitView,
hasAutoFramed,
customNodes.length,
isBlocksLoading,
isGraphLoading,
linkCount,
]);
useEffect(() => {
setHasAutoFramed(false);
}, [flowID, flowVersion]);
// Drag and drop block from block menu
const onDragOver = useCallback((event: React.DragEvent) => {

View File

@@ -103,6 +103,7 @@ const FlowEditor: React.FC<{
updateNode,
getViewport,
setViewport,
fitView,
screenToFlowPosition,
} = useReactFlow<CustomNode, CustomEdge>();
const [nodeId, setNodeId] = useState<number>(1);
@@ -115,6 +116,7 @@ const FlowEditor: React.FC<{
const [pinBlocksPopover, setPinBlocksPopover] = useState(false);
// State to control if save popover should be pinned open
const [pinSavePopover, setPinSavePopover] = useState(false);
const [hasAutoFramed, setHasAutoFramed] = useState(false);
const {
agentName,
@@ -482,35 +484,26 @@ const FlowEditor: React.FC<{
return uuidv4();
}, []);
// Set the initial view port to center the canvas.
useEffect(() => {
const { x, y } = getViewport();
if (nodes.length <= 0 || x !== 0 || y !== 0) {
if (nodes.length === 0) {
return;
}
const topLeft = { x: Infinity, y: Infinity };
const bottomRight = { x: -Infinity, y: -Infinity };
if (hasAutoFramed) {
return;
}
nodes.forEach((node) => {
const { x, y } = node.position;
topLeft.x = Math.min(topLeft.x, x);
topLeft.y = Math.min(topLeft.y, y);
// Rough estimate of the width and height of the node: 500x400.
bottomRight.x = Math.max(bottomRight.x, x + 500);
bottomRight.y = Math.max(bottomRight.y, y + 400);
const rafId = requestAnimationFrame(() => {
fitView({ padding: 0.2, duration: 800, maxZoom: 1 });
setHasAutoFramed(true);
});
const centerX = (topLeft.x + bottomRight.x) / 2;
const centerY = (topLeft.y + bottomRight.y) / 2;
const zoom = 0.8;
return () => cancelAnimationFrame(rafId);
}, [fitView, hasAutoFramed, nodes.length]);
setViewport({
x: window.innerWidth / 2 - centerX * zoom,
y: window.innerHeight / 2 - centerY * zoom,
zoom: zoom,
});
}, [nodes, getViewport, setViewport]);
useEffect(() => {
setHasAutoFramed(false);
}, [flowID, flowVersion]);
const navigateToNode = useCallback(
(nodeId: string) => {