mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(frontend): integrate drag-and-drop functionality in new builder (#11341)
In this PR, I’ve added drag-and-drop functionality to the new builder using built-in HTML drag-and-drop. https://github.com/user-attachments/assets/b27c281e-6216-4131-9a89-e10b0dd56a8f ### Changes - Added ReactFlowProvider to manage flow state in BuilderPage and Flow components. - Implemented drag-and-drop support for blocks in the NewControlPanel, allowing users to drag blocks from the menu and drop them onto the canvas. - Enhanced the Block component to handle drag events and provide visual feedback during dragging. - Updated useFlow hook to include onDragOver and onDrop handlers for managing block placement. - Adjusted nodeStore to accept position parameters for added blocks, improving placement accuracy. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] I’ve tried dragging and dropping multiple blocks, and it works perfectly as shown in the video.
This commit is contained in:
@@ -23,7 +23,7 @@ export const Flow = () => {
|
||||
const { edges, onConnect, onEdgesChange } = useCustomEdge();
|
||||
|
||||
// We use this hook to load the graph and convert them into custom nodes and edges.
|
||||
useFlow();
|
||||
const { onDragOver, onDrop } = useFlow();
|
||||
|
||||
// This hook is used for websocket realtime updates.
|
||||
useFlowRealtime();
|
||||
@@ -45,6 +45,8 @@ export const Flow = () => {
|
||||
edgeTypes={{ custom: CustomEdge }}
|
||||
maxZoom={2}
|
||||
minZoom={0.1}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useGetV2GetSpecificBlocks } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import {
|
||||
useGetV1GetExecutionDetails,
|
||||
@@ -8,12 +9,13 @@ import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { convertNodesPlusBlockInfoIntoCustomNodes } from "../../helper";
|
||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||
import { GetV1GetExecutionDetails200 } from "@/app/api/__generated__/models/getV1GetExecutionDetails200";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
|
||||
export const useFlow = () => {
|
||||
const addNodes = useNodeStore(useShallow((state) => state.addNodes));
|
||||
@@ -30,6 +32,11 @@ export const useFlow = () => {
|
||||
const setGraphSchemas = useGraphStore(
|
||||
useShallow((state) => state.setGraphSchemas),
|
||||
);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const addBlock = useNodeStore(useShallow((state) => state.addBlock));
|
||||
const setBlockMenuOpen = useControlPanelStore(
|
||||
useShallow((state) => state.setBlockMenuOpen),
|
||||
);
|
||||
const [{ flowID, flowVersion, flowExecutionID }] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
@@ -144,5 +151,36 @@ export const useFlow = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isFlowContentLoading: isGraphLoading || isBlocksLoading };
|
||||
// Drag and drop block from block menu
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}, []);
|
||||
|
||||
const onDrop = async (event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
const blockDataString = event.dataTransfer.getData("application/reactflow");
|
||||
if (!blockDataString) return;
|
||||
|
||||
try {
|
||||
const blockData = JSON.parse(blockDataString) as BlockInfo;
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
addBlock(blockData, position);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
setBlockMenuOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to drop block:", error);
|
||||
setBlockMenuOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isFlowContentLoading: isGraphLoading || isBlocksLoading,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -75,6 +75,7 @@ export const AllBlocksContent = () => {
|
||||
title={block.name as string}
|
||||
description={block.name as string}
|
||||
onClick={() => addBlock(block)}
|
||||
blockData={block}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -4,10 +4,14 @@ import { beautifyString, cn } from "@/lib/utils";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { highlightText } from "./helpers";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
import { blockDragPreviewStyle } from "./style";
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
title?: string;
|
||||
description?: string;
|
||||
highlightedText?: string;
|
||||
blockData: BlockInfo;
|
||||
}
|
||||
|
||||
interface BlockComponent extends React.FC<Props> {
|
||||
@@ -19,15 +23,38 @@ export const Block: BlockComponent = ({
|
||||
description,
|
||||
highlightedText,
|
||||
className,
|
||||
blockData,
|
||||
...rest
|
||||
}) => {
|
||||
const setBlockMenuOpen = useControlPanelStore(
|
||||
(state) => state.setBlockMenuOpen,
|
||||
);
|
||||
const handleDragStart = (e: React.DragEvent<HTMLButtonElement>) => {
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
e.dataTransfer.setData("application/reactflow", JSON.stringify(blockData));
|
||||
|
||||
setBlockMenuOpen(false);
|
||||
|
||||
// preview when user drags it
|
||||
const dragPreview = document.createElement("div");
|
||||
dragPreview.style.cssText = blockDragPreviewStyle;
|
||||
dragPreview.textContent = beautifyString(title || "");
|
||||
|
||||
document.body.appendChild(dragPreview);
|
||||
e.dataTransfer.setDragImage(dragPreview, 0, 0);
|
||||
|
||||
setTimeout(() => document.body.removeChild(dragPreview), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
draggable={true}
|
||||
className={cn(
|
||||
"group flex h-16 w-full min-w-[7.5rem] items-center justify-start space-x-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 px-[0.875rem] py-[0.625rem] text-start shadow-none",
|
||||
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
onDragStart={handleDragStart}
|
||||
{...rest}
|
||||
>
|
||||
<div className="flex flex-1 flex-col items-start gap-0.5">
|
||||
|
||||
@@ -29,6 +29,7 @@ export const BlocksList: React.FC<BlocksListProps> = ({
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
onClick={() => addBlock(block)}
|
||||
blockData={block}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export const BlockMenu = () => {
|
||||
const { blockMenuOpen, setBlockMenuOpen } = useControlPanelStore();
|
||||
return (
|
||||
// pinBlocksPopover ? true : open
|
||||
<Popover onOpenChange={setBlockMenuOpen}>
|
||||
<Popover onOpenChange={setBlockMenuOpen} open={blockMenuOpen}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger className="hover:cursor-pointer">
|
||||
|
||||
@@ -76,6 +76,7 @@ export const BlockMenuSearch = () => {
|
||||
highlightedText={searchQuery}
|
||||
description={data.description}
|
||||
onClick={() => addBlock(data)}
|
||||
blockData={data}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ export const SuggestionContent = () => {
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
onClick={() => addBlock(block)}
|
||||
blockData={block}
|
||||
/>
|
||||
))
|
||||
: Array(3)
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
export const blockMenuContainerStyle =
|
||||
"scrollbar-thin scrollbar-thumb-zinc-300 scrollbar-track-transparent w-full px-4 pb-4 space-y-3 h-full overflow-y-auto pt-4 transition-all duration-200";
|
||||
|
||||
export const blockDragPreviewStyle = `
|
||||
padding: 0.625rem 0.875rem;
|
||||
background: #f4f4f5;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #27272a;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
`;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useEffect } from "react";
|
||||
import { Flow } from "./components/FlowEditor/Flow/Flow";
|
||||
import { BuilderViewTabs } from "./components/BuilderViewTabs/BuilderViewTabs";
|
||||
import { useBuilderView } from "./components/BuilderViewTabs/useBuilderViewTabs";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
|
||||
function BuilderContent() {
|
||||
const query = useSearchParams();
|
||||
@@ -42,10 +43,22 @@ export default function BuilderPage() {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<BuilderViewTabs value={selectedView} onChange={setSelectedView} />
|
||||
{selectedView === "new" ? <Flow /> : <BuilderContent />}
|
||||
{selectedView === "new" ? (
|
||||
<ReactFlowProvider>
|
||||
<Flow />
|
||||
</ReactFlowProvider>
|
||||
) : (
|
||||
<BuilderContent />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return isNewFlowEditorEnabled ? <Flow /> : <BuilderContent />;
|
||||
return isNewFlowEditorEnabled ? (
|
||||
<ReactFlowProvider>
|
||||
<Flow />
|
||||
</ReactFlowProvider>
|
||||
) : (
|
||||
<BuilderContent />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import { NodeChange, applyNodeChanges } from "@xyflow/react";
|
||||
import { NodeChange, XYPosition, applyNodeChanges } from "@xyflow/react";
|
||||
import { CustomNode } from "../components/FlowEditor/nodes/CustomNode/CustomNode";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { convertBlockInfoIntoCustomNodeData } from "../components/helper";
|
||||
@@ -16,7 +16,7 @@ type NodeStore = {
|
||||
setNodes: (nodes: CustomNode[]) => void;
|
||||
onNodesChange: (changes: NodeChange<CustomNode>[]) => void;
|
||||
addNode: (node: CustomNode) => void;
|
||||
addBlock: (block: BlockInfo) => void;
|
||||
addBlock: (block: BlockInfo, position?: XYPosition) => void;
|
||||
incrementNodeCounter: () => void;
|
||||
updateNodeData: (nodeId: string, data: Partial<CustomNode["data"]>) => void;
|
||||
toggleAdvanced: (nodeId: string) => void;
|
||||
@@ -71,7 +71,7 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
nodes: [...state.nodes, node],
|
||||
}));
|
||||
},
|
||||
addBlock: (block: BlockInfo) => {
|
||||
addBlock: (block: BlockInfo, position?: XYPosition) => {
|
||||
const customNodeData = convertBlockInfoIntoCustomNodeData(block);
|
||||
get().incrementNodeCounter();
|
||||
const nodeNumber = get().nodeCounter;
|
||||
@@ -79,7 +79,7 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
id: nodeNumber.toString(),
|
||||
data: customNodeData,
|
||||
type: "custom",
|
||||
position: { x: 0, y: 0 },
|
||||
position: position || ({ x: 0, y: 0 } as XYPosition),
|
||||
};
|
||||
set((state) => ({
|
||||
nodes: [...state.nodes, customNode],
|
||||
|
||||
Reference in New Issue
Block a user