diff --git a/skyvern-frontend/src/components/AnimatedWave.tsx b/skyvern-frontend/src/components/AnimatedWave.tsx new file mode 100644 index 00000000..5229aca7 --- /dev/null +++ b/skyvern-frontend/src/components/AnimatedWave.tsx @@ -0,0 +1,42 @@ +interface AnimatedWaveProps { + text: string; + className?: string; +} + +export function AnimatedWave({ text, className = "" }: AnimatedWaveProps) { + const characters = text.split(""); + + return ( + <> + + + {characters.map((char, index) => ( + + {char === " " ? "\u00A0" : char} + + ))} + + + ); +} diff --git a/skyvern-frontend/src/components/FloatingWindow.tsx b/skyvern-frontend/src/components/FloatingWindow.tsx index 42235ca9..37988744 100644 --- a/skyvern-frontend/src/components/FloatingWindow.tsx +++ b/skyvern-frontend/src/components/FloatingWindow.tsx @@ -18,7 +18,14 @@ import { import { flushSync } from "react-dom"; import Draggable from "react-draggable"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/util/utils"; +import { PowerIcon } from "./icons/PowerIcon"; type OS = "Windows" | "macOS" | "Linux" | "Unknown"; @@ -70,11 +77,41 @@ function WindowsButton(props: { ); } +function PowerButton(props: { onClick: () => void }) { + return ( + + + + + + Cycle (New Browser) + + + ); +} + function ReloadButton(props: { isReloading: boolean; onClick: () => void }) { return ( - + + + + + + Reconnect + + ); } @@ -114,10 +151,12 @@ function FloatingWindow({ showCloseButton, showMaximizeButton, showMinimizeButton, + showPowerButton, showReloadButton = false, title, zIndex, // -- + onCycle, onInteract, }: { bounded?: boolean; @@ -128,10 +167,12 @@ function FloatingWindow({ showCloseButton?: boolean; showMaximizeButton?: boolean; showMinimizeButton?: boolean; + showPowerButton?: boolean; showReloadButton?: boolean; title: string; zIndex?: string; // -- + onCycle?: () => void; onInteract?: () => void; }) { const [reloadKey, setReloadKey] = useState(0); @@ -420,6 +461,10 @@ function FloatingWindow({ }, 1000); }; + const cycle = () => { + onCycle?.(); + }; + /** * If maximized, need to retain max size during parent resizing. */ @@ -582,6 +627,7 @@ function FloatingWindow({ onClick={toggleMaximized} /> )} + {showPowerButton && cycle()} />}
{title}
{showReloadButton && ( @@ -601,6 +647,7 @@ function FloatingWindow({ )}
{title}
+ {showPowerButton && cycle()} />} {showMinimizeButton && ( + + + + ); +} + +export { PowerIcon }; diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx index ab0bfe62..cb33d8ee 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx @@ -1,12 +1,30 @@ -import { ReactFlowProvider } from "@xyflow/react"; -import { useParams } from "react-router-dom"; -import { useQueryClient } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import { ReloadIcon } from "@radix-ui/react-icons"; import { useEffect, useRef, useState } from "react"; +import { useParams } from "react-router-dom"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ReactFlowProvider } from "@xyflow/react"; +import { getClient } from "@/api/AxiosClient"; +import { DebugSessionApiResponse } from "@/api/types"; +import { AnimatedWave } from "@/components/AnimatedWave"; import { BrowserStream } from "@/components/BrowserStream"; import { FloatingWindow } from "@/components/FloatingWindow"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogClose, +} from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; +import { toast } from "@/components/ui/use-toast"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useMountEffect } from "@/hooks/useMountEffect"; +import { useUser } from "@/hooks/useUser"; import { statusIsFinalized } from "@/routes/tasks/types.ts"; import { useSidebarStore } from "@/store/SidebarStore"; import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; @@ -19,9 +37,16 @@ import { getElements } from "./workflowEditorUtils"; import { getInitialParameters } from "./utils"; function WorkflowDebugger() { - const { workflowPermanentId } = useParams(); + const { blockLabel, workflowPermanentId } = useParams(); + const [openDialogue, setOpenDialogue] = useState(false); + const [activeDebugSession, setActiveDebugSession] = + useState(null); + const credentialGetter = useCredentialGetter(); const queryClient = useQueryClient(); const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false); + const user = useUser().get(); + const email = user?.email; + const isSkyvernUser = email?.toLowerCase().endsWith("@skyvern.com") ?? false; const { data: workflowRun } = useWorkflowRunQuery(); const { data: workflow } = useWorkflowQuery({ @@ -41,6 +66,10 @@ function WorkflowDebugger() { (state) => state.setHasChanges, ); + const handleOnCycle = () => { + setOpenDialogue(true); + }; + useMountEffect(() => { setCollapsed(true); setHasChanges(false); @@ -53,6 +82,37 @@ function WorkflowDebugger() { } }); + const cycleBrowser = useMutation({ + mutationFn: async (id: string) => { + const client = await getClient(credentialGetter, "sans-api-v1"); + return client.post(`/debug-session/${id}/new`); + }, + onSuccess: (response) => { + const newDebugSession = response.data; + setActiveDebugSession(newDebugSession); + + queryClient.invalidateQueries({ + queryKey: ["debugSession", workflowPermanentId], + }); + + toast({ + title: "Browser cycled", + variant: "success", + description: "Your browser has been cycled.", + }); + + setOpenDialogue(false); + }, + onError: (error: AxiosError) => { + toast({ + variant: "destructive", + title: "Failed to cycle browser", + description: error.message, + }); + setOpenDialogue(false); + }, + }); + const intervalRef = useRef(null); useEffect(() => { @@ -71,6 +131,10 @@ function WorkflowDebugger() { clearInterval(intervalRef.current); intervalRef.current = null; } + + if (debugSession) { + setActiveDebugSession(debugSession); + } } return () => { @@ -107,6 +171,52 @@ function WorkflowDebugger() { return (
+ { + if (!open && cycleBrowser.isPending) { + return; + } + setOpenDialogue(open); + }} + > + + + Cycle (Get a new browser) + +
+ {cycleBrowser.isPending ? ( + <> + Cooking you up a fresh browser... + + + ) : ( + "Abandon this browser for a new one. Are you sure?" + )} +
+
+
+ + {!cycleBrowser.isPending && ( + + + + )} + + +
+
- {debugSession && ( + {activeDebugSession && ( - {debugSession && debugSession.browser_session_id ? ( + {activeDebugSession && activeDebugSession.browser_session_id ? ( ) : (