Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions autogpt_platform/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"cmdk": "1.1.1",
"cookie": "1.0.2",
"date-fns": "4.1.0",
"dexie": "4.2.1",
"dotenv": "17.2.3",
"elliptic": "6.6.1",
"embla-carousel-react": "8.6.0",
Expand Down
8 changes: 8 additions & 0 deletions autogpt_platform/frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"use client";

import { Button } from "@/components/atoms/Button/Button";
import { ClockCounterClockwiseIcon, XIcon } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import { formatTimeAgo } from "@/lib/utils/time";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { useDraftRecoveryPopup } from "./useDraftRecoveryPopup";
import { Text } from "@/components/atoms/Text/Text";
import { AnimatePresence, motion } from "framer-motion";

interface DraftRecoveryPopupProps {
isInitialLoadComplete: boolean;
}

export function DraftRecoveryPopup({
isInitialLoadComplete,
}: DraftRecoveryPopupProps) {
const { isOpen, popupRef, nodeCount, edgeCount, savedAt, onLoad, onDiscard } =
useDraftRecoveryPopup(isInitialLoadComplete);

return (
<AnimatePresence>
{isOpen && (
<motion.div
ref={popupRef}
className={cn("absolute left-1/2 top-4 z-50")}
initial={{
opacity: 0,
x: "-50%",
y: "-150%",
scale: 0.5,
filter: "blur(20px)",
}}
animate={{
opacity: 1,
x: "-50%",
y: "0%",
scale: 1,
filter: "blur(0px)",
}}
exit={{
opacity: 0,
y: "-150%",
scale: 0.5,
filter: "blur(20px)",
transition: { duration: 0.4, type: "spring", bounce: 0.2 },
}}
transition={{ duration: 0.2, type: "spring", bounce: 0.2 }}
>
<div
className={cn(
"flex items-center gap-3 rounded-xlarge border border-amber-200 bg-amber-50 px-4 py-3 shadow-lg",
)}
>
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
<ClockCounterClockwiseIcon className="h-5 w-5" weight="fill" />
</div>

<div className="flex flex-col">
<Text
variant="small-medium"
className="text-amber-900 dark:text-amber-100"
>
Unsaved changes found
</Text>
<Text
variant="small"
className="text-amber-700 dark:text-amber-400"
>
{nodeCount} block{nodeCount !== 1 ? "s" : ""}, {edgeCount}{" "}
connection
{edgeCount !== 1 ? "s" : ""} •{" "}
{formatTimeAgo(new Date(savedAt).toISOString())}
</Text>
</div>

<div className="ml-2 flex items-center gap-2">
<Tooltip delayDuration={10}>
<TooltipTrigger asChild>
<Button
variant="primary"
size="small"
onClick={onLoad}
className="aspect-square min-w-0 p-1.5"
>
<ClockCounterClockwiseIcon size={20} weight="fill" />
<span className="sr-only">Restore changes</span>
</Button>
</TooltipTrigger>
<TooltipContent>Restore changes</TooltipContent>
</Tooltip>
<Tooltip delayDuration={10}>
<TooltipTrigger asChild>
<Button
variant="destructive"
size="icon"
onClick={onDiscard}
aria-label="Discard changes"
className="aspect-square min-w-0 p-1.5"
>
<XIcon size={20} />
<span className="sr-only">Discard changes</span>
</Button>
</TooltipTrigger>
<TooltipContent>Discard changes</TooltipContent>
</Tooltip>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useRef } from "react";
import { useDraftManager } from "../FlowEditor/Flow/useDraftManager";

export const useDraftRecoveryPopup = (isInitialLoadComplete: boolean) => {
const popupRef = useRef<HTMLDivElement>(null);

const {
isRecoveryOpen: isOpen,
savedAt,
nodeCount,
edgeCount,
loadDraft: onLoad,
discardDraft: onDiscard,
} = useDraftManager(isInitialLoadComplete);

useEffect(() => {
if (!isOpen) return;

const handleClickOutside = (event: MouseEvent) => {
if (
popupRef.current &&
!popupRef.current.contains(event.target as Node)
) {
onDiscard();
}
};

const timeoutId = setTimeout(() => {
document.addEventListener("mousedown", handleClickOutside);
}, 100);

return () => {
clearTimeout(timeoutId);
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen, onDiscard]);

useEffect(() => {
if (!isOpen) return;

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onDiscard();
}
};

document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [isOpen, onDiscard]);
return {
popupRef,
isOpen,
nodeCount,
edgeCount,
savedAt,
onLoad,
onDiscard,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CustomEdge from "../edges/CustomEdge";
import { useFlow } from "./useFlow";
import { useShallow } from "zustand/react/shallow";
import { useNodeStore } from "../../../stores/nodeStore";
import { useMemo, useEffect, useCallback } from "react";
import { useMemo, useCallback } from "react";
import { CustomNode } from "../nodes/CustomNode/CustomNode";
import { useCustomEdge } from "../edges/useCustomEdge";
import { useFlowRealtime } from "./useFlowRealtime";
Expand All @@ -21,6 +21,7 @@ import { okData } from "@/app/api/helpers";
import { TriggerAgentBanner } from "./components/TriggerAgentBanner";
import { resolveCollisions } from "./helpers/resolve-collision";
import { FloatingSafeModeToggle } from "../../FloatingSafeModeToogle";
import { DraftRecoveryPopup } from "../../DraftRecoveryDialog/DraftRecoveryPopup";

export const Flow = () => {
const [{ flowID, flowExecutionID }] = useQueryStates({
Expand Down Expand Up @@ -60,26 +61,22 @@ export const Flow = () => {
}, [setNodes, nodes]);
const { edges, onConnect, onEdgesChange } = useCustomEdge();

// We use this hook to load the graph and convert them into custom nodes and edges.
const { onDragOver, onDrop, isFlowContentLoading, isLocked, setIsLocked } =
useFlow();
// for loading purpose
const {
onDragOver,
onDrop,
isFlowContentLoading,
isInitialLoadComplete,
isLocked,
setIsLocked,
} = useFlow();

// This hook is used for websocket realtime updates.
useFlowRealtime();

// Copy/paste functionality
const handleCopyPaste = useCopyPaste();
useCopyPaste();

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
handleCopyPaste(event);
};

window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [handleCopyPaste]);
const isGraphRunning = useGraphStore(
useShallow((state) => state.isGraphRunning),
);
Expand Down Expand Up @@ -115,6 +112,7 @@ export const Flow = () => {
className="right-2 top-32 p-2"
/>
)}
<DraftRecoveryPopup isInitialLoadComplete={isInitialLoadComplete} />
</ReactFlow>
</div>
{/* TODO: Need to update it in future - also do not send executionId as prop - rather use useQueryState inside the component */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback, useEffect } from "react";
import { useReactFlow } from "@xyflow/react";
import { v4 as uuidv4 } from "uuid";
import { useNodeStore } from "../../../stores/nodeStore";
Expand Down Expand Up @@ -151,5 +151,16 @@ export function useCopyPaste() {
[getViewport, toast],
);

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
handleCopyPaste(event);
};

window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [handleCopyPaste]);

return handleCopyPaste;
}
Loading
Loading