mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 07:08:09 -05:00
feat(frontend): add copy paste functionality in new builder (#11339)
In the new builder, I’ve added a copy-paste functionality using the keyboard. https://github.com/user-attachments/assets/3106ae86-3f47-4807-a598-9c0b166eaae9 ### Changes 🏗️ - Added useCopyPasteKeyboard hook for handling keyboard shortcuts - Created new copyPasteStore for state management - Implemented performance optimizations (memo on CustomEdge) - Updated nodeStore and edgeStore to support the functionality ### 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] The copy-paste functionality is working correctly as you can see in the video.
This commit is contained in:
@@ -12,6 +12,7 @@ import { GraphLoadingBox } from "./components/GraphLoadingBox";
|
||||
import { BuilderActions } from "../../BuilderActions/BuilderActions";
|
||||
import { RunningBackground } from "./components/RunningBackground";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
import { useCopyPasteKeyboard } from "../../../hooks/useCopyPasteKeyboard";
|
||||
|
||||
export const Flow = () => {
|
||||
const nodes = useNodeStore(useShallow((state) => state.nodes));
|
||||
@@ -27,6 +28,8 @@ export const Flow = () => {
|
||||
// This hook is used for websocket realtime updates.
|
||||
useFlowRealtime();
|
||||
|
||||
useCopyPasteKeyboard();
|
||||
|
||||
const { isFlowContentLoading } = useFlow();
|
||||
const { isGraphRunning } = useGraphStore();
|
||||
return (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { XIcon } from "@phosphor-icons/react";
|
||||
import { memo } from "react";
|
||||
|
||||
const CustomEdge = ({
|
||||
id,
|
||||
@@ -56,4 +57,4 @@ const CustomEdge = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEdge;
|
||||
export default memo(CustomEdge);
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export const useCustomEdge = () => {
|
||||
const connections = useEdgeStore((s) => s.connections);
|
||||
const connections = useEdgeStore(useShallow((s) => s.connections));
|
||||
const addConnection = useEdgeStore((s) => s.addConnection);
|
||||
const removeConnection = useEdgeStore((s) => s.removeConnection);
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect } from "react";
|
||||
import { useCopyPasteStore } from "../stores/copyPasteStore";
|
||||
|
||||
export function useCopyPasteKeyboard() {
|
||||
const { copySelectedNodes, pasteNodes } = useCopyPasteStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const activeElement = document.activeElement;
|
||||
const isInputField =
|
||||
activeElement?.tagName === "INPUT" ||
|
||||
activeElement?.tagName === "TEXTAREA" ||
|
||||
activeElement?.getAttribute("contenteditable") === "true";
|
||||
|
||||
if (isInputField) return;
|
||||
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
(event.key === "c" || event.key === "C")
|
||||
) {
|
||||
event.preventDefault();
|
||||
copySelectedNodes();
|
||||
}
|
||||
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
(event.key === "v" || event.key === "V")
|
||||
) {
|
||||
event.preventDefault();
|
||||
pasteNodes();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [copySelectedNodes, pasteNodes]);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { create } from "zustand";
|
||||
import { CustomNode } from "../components/FlowEditor/nodes/CustomNode/CustomNode";
|
||||
import { Connection, useEdgeStore } from "./edgeStore";
|
||||
import { Key, storage } from "@/services/storage/local-storage";
|
||||
import { useNodeStore } from "./nodeStore";
|
||||
|
||||
interface CopyableData {
|
||||
nodes: CustomNode[];
|
||||
connections: Connection[];
|
||||
}
|
||||
|
||||
type CopyPasteStore = {
|
||||
copySelectedNodes: () => void;
|
||||
pasteNodes: () => void;
|
||||
};
|
||||
|
||||
export const useCopyPasteStore = create<CopyPasteStore>(() => ({
|
||||
copySelectedNodes: () => {
|
||||
const { nodes } = useNodeStore.getState();
|
||||
const { connections } = useEdgeStore.getState();
|
||||
|
||||
const selectedNodes = nodes.filter((node) => node.selected);
|
||||
const selectedNodeIds = new Set(selectedNodes.map((node) => node.id));
|
||||
|
||||
const selectedConnections = connections.filter(
|
||||
(conn) =>
|
||||
selectedNodeIds.has(conn.source) && selectedNodeIds.has(conn.target),
|
||||
);
|
||||
|
||||
const copiedData: CopyableData = {
|
||||
nodes: selectedNodes.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
},
|
||||
})),
|
||||
connections: selectedConnections,
|
||||
};
|
||||
|
||||
storage.set(Key.COPIED_FLOW_DATA, JSON.stringify(copiedData));
|
||||
},
|
||||
|
||||
pasteNodes: () => {
|
||||
const copiedDataString = storage.get(Key.COPIED_FLOW_DATA);
|
||||
if (!copiedDataString) return;
|
||||
|
||||
const copiedData = JSON.parse(copiedDataString) as CopyableData;
|
||||
const { addNode } = useNodeStore.getState();
|
||||
const { addConnection } = useEdgeStore.getState();
|
||||
|
||||
const oldToNewIdMap: Record<string, string> = {};
|
||||
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
copiedData.nodes.forEach((node) => {
|
||||
minX = Math.min(minX, node.position.x);
|
||||
minY = Math.min(minY, node.position.y);
|
||||
maxX = Math.max(maxX, node.position.x);
|
||||
maxY = Math.max(maxY, node.position.y);
|
||||
});
|
||||
|
||||
const offsetX = 50;
|
||||
const offsetY = 50;
|
||||
|
||||
useNodeStore.setState((state) => ({
|
||||
nodes: state.nodes.map((node) => ({ ...node, selected: false })),
|
||||
}));
|
||||
|
||||
copiedData.nodes.forEach((node) => {
|
||||
const { incrementNodeCounter, nodeCounter } = useNodeStore.getState();
|
||||
incrementNodeCounter();
|
||||
oldToNewIdMap[node.id] = (nodeCounter + 1).toString();
|
||||
|
||||
addNode({
|
||||
...node,
|
||||
id: (nodeCounter + 1).toString(),
|
||||
position: {
|
||||
x: node.position.x + offsetX,
|
||||
y: node.position.y + offsetY,
|
||||
},
|
||||
selected: true,
|
||||
});
|
||||
});
|
||||
|
||||
copiedData.connections.forEach((conn) => {
|
||||
const newSourceId = oldToNewIdMap[conn.source] ?? conn.source;
|
||||
const newTargetId = oldToNewIdMap[conn.target] ?? conn.target;
|
||||
|
||||
addConnection({
|
||||
source: newSourceId,
|
||||
target: newTargetId,
|
||||
sourceHandle: conn.sourceHandle ?? "",
|
||||
targetHandle: conn.targetHandle ?? "",
|
||||
});
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -16,7 +16,7 @@ type EdgeStore = {
|
||||
setConnections: (connections: Connection[]) => void;
|
||||
addConnection: (
|
||||
conn: Omit<Connection, "edge_id"> & { edge_id?: string },
|
||||
) => Connection;
|
||||
) => void;
|
||||
removeConnection: (edge_id: string) => void;
|
||||
upsertMany: (conns: Connection[]) => void;
|
||||
|
||||
|
||||
@@ -66,10 +66,11 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
addNode: (node) =>
|
||||
addNode: (node) => {
|
||||
set((state) => ({
|
||||
nodes: [...state.nodes, node],
|
||||
})),
|
||||
}));
|
||||
},
|
||||
addBlock: (block: BlockInfo) => {
|
||||
const customNodeData = convertBlockInfoIntoCustomNodeData(block);
|
||||
get().incrementNodeCounter();
|
||||
|
||||
Reference in New Issue
Block a user