diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index b938d4b4..8d4fbee3 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -317,6 +317,7 @@ export type WorkflowRunStatusApiResponse = { webhook_callback_url: string | null; extra_http_headers: Record | null; created_at: string; + finished_at: string; modified_at: string; parameters: Record; screenshot_urls: Array | null; diff --git a/skyvern-frontend/src/components/Timer.tsx b/skyvern-frontend/src/components/Timer.tsx index 415eb0fc..d5e6a2e4 100644 --- a/skyvern-frontend/src/components/Timer.tsx +++ b/skyvern-frontend/src/components/Timer.tsx @@ -7,10 +7,26 @@ interface HMS { } interface Props { + override?: number; startAt?: HMS; } -function Timer({ startAt }: Props) { +const formatMs = (elapsed: number) => { + let seconds = Math.floor(elapsed / 1000); + let minutes = Math.floor(seconds / 60); + let hours = Math.floor(minutes / 60); + seconds = seconds % 60; + minutes = minutes % 60; + hours = hours % 24; + + return { + hour: hours, + minute: minutes, + second: seconds, + }; +}; + +function Timer({ override, startAt }: Props) { const [time, setTime] = useState({ hour: 0, minute: 0, @@ -18,20 +34,22 @@ function Timer({ startAt }: Props) { }); useEffect(() => { + if (override) { + const formatted = formatMs(override); + setTime(() => formatted); + + return; + } + const start = performance.now(); const loop = () => { const elapsed = performance.now() - start; - let seconds = Math.floor(elapsed / 1000); - let minutes = Math.floor(seconds / 60); - let hours = Math.floor(minutes / 60); - seconds = seconds % 60; - minutes = minutes % 60; - hours = hours % 24; + const formatted = formatMs(elapsed); setTime(() => ({ - hour: hours + (startAt?.hour ?? 0), - minute: minutes + (startAt?.minute ?? 0), - second: seconds + (startAt?.second ?? 0), + hour: formatted.hour + (startAt?.hour ?? 0), + minute: formatted.minute + (startAt?.minute ?? 0), + second: formatted.second + (startAt?.second ?? 0), })); rAF = requestAnimationFrame(loop); @@ -40,7 +58,7 @@ function Timer({ startAt }: Props) { let rAF = requestAnimationFrame(loop); return () => cancelAnimationFrame(rAF); - }, [startAt]); + }, [override, startAt]); return (
diff --git a/skyvern-frontend/src/router.tsx b/skyvern-frontend/src/router.tsx index 36e46301..a20d2115 100644 --- a/skyvern-frontend/src/router.tsx +++ b/skyvern-frontend/src/router.tsx @@ -18,7 +18,7 @@ import { WorkflowRun } from "./routes/workflows/WorkflowRun"; import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters"; import { Workflows } from "./routes/workflows/Workflows"; import { WorkflowsPageLayout } from "./routes/workflows/WorkflowsPageLayout"; -import { WorkflowDebugger } from "./routes/workflows/editor/WorkflowDebugger"; +import { Debugger } from "./routes/workflows/debugger/Debugger"; import { WorkflowEditor } from "./routes/workflows/editor/WorkflowEditor"; import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/WorkflowPostRunParameters"; import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput"; @@ -111,11 +111,11 @@ const router = createBrowserRouter([ }, { path: "debug", - element: , + element: , }, { path: ":workflowRunId/:blockLabel/debug", - element: , + element: , }, { path: "edit", diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx b/skyvern-frontend/src/routes/workflows/debugger/Debugger.tsx similarity index 86% rename from skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx rename to skyvern-frontend/src/routes/workflows/debugger/Debugger.tsx index a82c0cd2..71cb6c62 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx +++ b/skyvern-frontend/src/routes/workflows/debugger/Debugger.tsx @@ -4,12 +4,12 @@ import { ReactFlowProvider } from "@xyflow/react"; import { useWorkflowQuery } from "../hooks/useWorkflowQuery"; import { WorkflowSettings } from "../types/workflowTypes"; -import { getElements } from "./workflowEditorUtils"; -import { getInitialParameters } from "./utils"; -import { Workspace } from "./Workspace"; +import { getElements } from "@/routes/workflows/editor/workflowEditorUtils"; +import { getInitialParameters } from "@/routes/workflows/editor/utils"; +import { Workspace } from "@/routes/workflows/editor/Workspace"; import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore"; -function WorkflowDebugger() { +function Debugger() { const { workflowPermanentId } = useParams(); const { data: workflow } = useWorkflowQuery({ workflowPermanentId, @@ -64,4 +64,4 @@ function WorkflowDebugger() { ); } -export { WorkflowDebugger }; +export { Debugger }; diff --git a/skyvern-frontend/src/routes/workflows/debugger/DebuggerPostRunParameters.tsx b/skyvern-frontend/src/routes/workflows/debugger/DebuggerPostRunParameters.tsx new file mode 100644 index 00000000..3196f55b --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/debugger/DebuggerPostRunParameters.tsx @@ -0,0 +1,201 @@ +import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery"; +import { CodeEditor } from "../components/CodeEditor"; +import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; +import { useActiveWorkflowRunItem } from "@/routes/workflows/workflowRun/useActiveWorkflowRunItem"; +import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery"; +import { isAction, isWorkflowRunBlock } from "../types/workflowRunTypes"; +import { findBlockSurroundingAction } from "@/routes/workflows/workflowRun/workflowTimelineUtils"; +import { DebuggerTaskBlockParameters } from "./DebuggerTaskBlockParameters"; +import { isTaskVariantBlock, WorkflowBlockTypes } from "../types/workflowTypes"; +import { Input } from "@/components/ui/input"; +import { ProxySelector } from "@/components/ProxySelector"; +import { DebuggerSendEmailBlockParameters } from "./DebuggerSendEmailBlockInfo"; +import { ProxyLocation } from "@/api/types"; +import { KeyValueInput } from "@/components/KeyValueInput"; +import { HelpTooltip } from "@/components/HelpTooltip"; + +function DebuggerPostRunParameters() { + const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } = + useWorkflowRunTimelineQuery(); + const [activeItem] = useActiveWorkflowRunItem(); + const { data: workflowRun, isLoading: workflowRunIsLoading } = + useWorkflowRunQuery(); + const parameters = workflowRun?.parameters ?? {}; + + if (workflowRunIsLoading || workflowRunTimelineIsLoading) { + return
Loading workflow parameters...
; + } + + if (!workflowRun || !workflowRunTimeline) { + return null; + } + + function getActiveBlock() { + if (!workflowRunTimeline) { + return; + } + if (isWorkflowRunBlock(activeItem)) { + return activeItem; + } + if (isAction(activeItem)) { + return findBlockSurroundingAction( + workflowRunTimeline, + activeItem.action_id, + ); + } + } + + const activeBlock = getActiveBlock(); + const isTaskV2 = workflowRun.task_v2 !== null; + + const webhookCallbackUrl = isTaskV2 + ? workflowRun.task_v2?.webhook_callback_url + : workflowRun.webhook_callback_url; + + const proxyLocation = isTaskV2 + ? workflowRun.task_v2?.proxy_location + : workflowRun.proxy_location; + + const extraHttpHeaders = isTaskV2 + ? workflowRun.task_v2?.extra_http_headers + : workflowRun.extra_http_headers; + + return ( +
+ {activeBlock && isTaskVariantBlock(activeBlock) ? ( +
+
+

Task Block Parameters

+ +
+
+ ) : null} + {activeBlock && + activeBlock.block_type === WorkflowBlockTypes.SendEmail ? ( +
+
+

Email Block Parameters

+ +
+
+ ) : null} + {activeBlock && activeBlock.block_type === WorkflowBlockTypes.ForLoop ? ( +
+
+

For Loop Block Parameters

+
+
+

Loop Values

+ +
+ +
+
+
+ ) : null} +
+
+

Workflow Parameters

+ {Object.entries(parameters).map(([key, value]) => { + return ( +
+
+

{key}

+ +
+ {typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" ? ( + + ) : ( + + )} +
+ ); + })} + {Object.entries(parameters).length === 0 ? ( +
+ No input parameters found for this workflow +
+ ) : null} +

Other Workflow Parameters

+
+
+

Webhook Callback URL

+ +
+ +
+
+
+

Proxy Location

+ +
+ { + // TODO + }} + /> +
+
+
+

Extra HTTP Headers

+ +
+
+ {}} + /> +
+
+
+
+ {workflowRun.task_v2 ? ( +
+
+

Task 2.0 Parameters

+
+
+

Task 2.0 Prompt

+ +
+ +
+ +
+
+ ) : null} +
+ ); +} + +export { DebuggerPostRunParameters }; diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebuggerRun.tsx b/skyvern-frontend/src/routes/workflows/debugger/DebuggerRun.tsx similarity index 50% rename from skyvern-frontend/src/routes/workflows/editor/WorkflowDebuggerRun.tsx rename to skyvern-frontend/src/routes/workflows/debugger/DebuggerRun.tsx index 3d3af1ca..97821c45 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebuggerRun.tsx +++ b/skyvern-frontend/src/routes/workflows/debugger/DebuggerRun.tsx @@ -1,12 +1,12 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; -import { WorkflowDebuggerRunTimeline } from "./WorkflowDebuggerRunTimeline"; +import { DebuggerRunTimeline } from "./DebuggerRunTimeline"; -function WorkflowDebuggerRun() { +function DebuggerRun() { const { data: workflowRun } = useWorkflowRunQuery(); const workflowFailureReason = workflowRun?.failure_reason ? (
-
- {workflowFailureReason} -
- {}} - onBlockItemSelected={() => {}} - onObserverThoughtCardSelected={() => {}} - /> -
+ {workflowFailureReason} +
+ {}} + onBlockItemSelected={() => {}} + onObserverThoughtCardSelected={() => {}} + />
); } -export { WorkflowDebuggerRun }; +export { DebuggerRun }; diff --git a/skyvern-frontend/src/routes/workflows/debugger/DebuggerRunOutput.tsx b/skyvern-frontend/src/routes/workflows/debugger/DebuggerRunOutput.tsx new file mode 100644 index 00000000..ddce0547 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/debugger/DebuggerRunOutput.tsx @@ -0,0 +1,175 @@ +import { FileIcon } from "@radix-ui/react-icons"; +import { CodeEditor } from "../components/CodeEditor"; +import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery"; +import { useActiveWorkflowRunItem } from "@/routes/workflows/workflowRun/useActiveWorkflowRunItem"; +import { + hasExtractedInformation, + isAction, + isWorkflowRunBlock, +} from "../types/workflowRunTypes"; +import { findBlockSurroundingAction } from "@/routes/workflows/workflowRun/workflowTimelineUtils"; +import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery"; +import { Status } from "@/api/types"; +import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; +import { isTaskVariantBlock } from "../types/workflowTypes"; + +function DebuggerRunOutput() { + const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } = + useWorkflowRunTimelineQuery(); + const [activeItem] = useActiveWorkflowRunItem(); + const { data: workflowRun } = useWorkflowRunQuery(); + + if (workflowRunTimelineIsLoading) { + return
Loading...
; + } + + if (!workflowRunTimeline) { + return null; + } + + function getActiveBlock() { + if (!workflowRunTimeline) { + return; + } + if (isWorkflowRunBlock(activeItem)) { + return activeItem; + } + if (isAction(activeItem)) { + return findBlockSurroundingAction( + workflowRunTimeline, + activeItem.action_id, + ); + } + } + + const activeBlock = getActiveBlock(); + + const showExtractedInformation = + activeBlock && + isTaskVariantBlock(activeBlock) && + activeBlock.status === Status.Completed; + + const outputs = workflowRun?.outputs; + const fileUrls = workflowRun?.downloaded_file_urls ?? []; + const observerOutput = workflowRun?.task_v2?.output; + const webhookFailureReasonData = + workflowRun?.task_v2?.webhook_failure_reason ?? + workflowRun?.webhook_failure_reason; + + return ( +
+ {webhookFailureReasonData ? ( +
+
+

Webhook Failure Reason

+
+ {webhookFailureReasonData} +
+
+
+ ) : null} + {activeBlock ? ( +
+
+

Block Outputs

+ {activeBlock.output === null ? ( +
This block has no outputs
+ ) : isTaskVariantBlock(activeBlock) ? ( +
+

+ {showExtractedInformation + ? "Extracted Information" + : "Failure Reason"} +

+ {showExtractedInformation ? ( + + ) : ( + + )} +
+ ) : ( +
+

Output

+ +
+ )} +
+
+ ) : null} + {observerOutput ? ( +
+
+

Task 2.0 Output

+ +
+
+ ) : null} +
+
+

Workflow Run Outputs

+ +
+
+
+
+

Workflow Run Downloaded Files

+
+ {fileUrls.length > 0 ? ( + fileUrls.map((url, index) => { + return ( + + ); + }) + ) : ( +
No files downloaded
+ )} +
+
+
+
+ ); +} + +export { DebuggerRunOutput }; diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebuggerRunTimeline.tsx b/skyvern-frontend/src/routes/workflows/debugger/DebuggerRunTimeline.tsx similarity index 98% rename from skyvern-frontend/src/routes/workflows/editor/WorkflowDebuggerRunTimeline.tsx rename to skyvern-frontend/src/routes/workflows/debugger/DebuggerRunTimeline.tsx index f385877e..652a180b 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebuggerRunTimeline.tsx +++ b/skyvern-frontend/src/routes/workflows/debugger/DebuggerRunTimeline.tsx @@ -25,7 +25,7 @@ type Props = { onBlockItemSelected: (item: WorkflowRunBlock) => void; }; -function WorkflowDebuggerRunTimeline({ +function DebuggerRunTimeline({ activeItem, onObserverThoughtCardSelected, onActionItemSelected, @@ -126,4 +126,4 @@ function WorkflowDebuggerRunTimeline({ ); } -export { WorkflowDebuggerRunTimeline }; +export { DebuggerRunTimeline }; diff --git a/skyvern-frontend/src/routes/workflows/debugger/DebuggerSendEmailBlockInfo.tsx b/skyvern-frontend/src/routes/workflows/debugger/DebuggerSendEmailBlockInfo.tsx new file mode 100644 index 00000000..3ecf9e19 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/debugger/DebuggerSendEmailBlockInfo.tsx @@ -0,0 +1,43 @@ +import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; +import { Input } from "@/components/ui/input"; +import { HelpTooltip } from "@/components/HelpTooltip"; + +type Props = { + recipients: Array; + body: string; + subject: string; +}; + +function DebuggerSendEmailBlockParameters({ + recipients, + body, + subject, +}: Props) { + return ( +
+
+
+

To

+ +
+ +
+
+
+

Subject

+ +
+ +
+
+
+

Body

+ +
+ +
+
+ ); +} + +export { DebuggerSendEmailBlockParameters }; diff --git a/skyvern-frontend/src/routes/workflows/debugger/DebuggerTaskBlockParameters.tsx b/skyvern-frontend/src/routes/workflows/debugger/DebuggerTaskBlockParameters.tsx new file mode 100644 index 00000000..1252aa6c --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/debugger/DebuggerTaskBlockParameters.tsx @@ -0,0 +1,147 @@ +import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; +import { Input } from "@/components/ui/input"; +import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; +import { WorkflowRunBlock } from "../types/workflowRunTypes"; +import { isTaskVariantBlock, WorkflowBlockTypes } from "../types/workflowTypes"; +import { Switch } from "@/components/ui/switch"; +import { HelpTooltip } from "@/components/HelpTooltip"; + +type Props = { + block: WorkflowRunBlock; +}; + +function DebuggerTaskBlockParameters({ block }: Props) { + const isTaskVariant = isTaskVariantBlock(block); + if (!isTaskVariant) { + return null; + } + + const showNavigationParameters = + block.block_type === WorkflowBlockTypes.Task || + block.block_type === WorkflowBlockTypes.Action || + block.block_type === WorkflowBlockTypes.Login || + block.block_type === WorkflowBlockTypes.Navigation; + + const showDataExtractionParameters = + block.block_type === WorkflowBlockTypes.Task || + block.block_type === WorkflowBlockTypes.Extraction; + + const showValidationParameters = + block.block_type === WorkflowBlockTypes.Validation; + + const showIncludeActionHistoryInVerification = + block.block_type === WorkflowBlockTypes.Task || + block.block_type === WorkflowBlockTypes.Navigation; + + return ( + <> +
+
+

URL

+ +
+ +
+ + {showNavigationParameters ? ( +
+
+

Navigation Goal

+ +
+ +
+ ) : null} + + {showNavigationParameters ? ( +
+
+

Navigation Payload

+ +
+ +
+ ) : null} + + {showDataExtractionParameters ? ( +
+
+

Data Extraction Goal

+ +
+ +
+ ) : null} + + {showDataExtractionParameters ? ( +
+
+

Data Schema

+ +
+ +
+ ) : null} + + {showValidationParameters ? ( +
+
+

Completion Criteria

+ +
+ +
+ ) : null} + + {showValidationParameters ? ( +
+
+

Termination Criteria

+ +
+ +
+ ) : null} + + {showIncludeActionHistoryInVerification ? ( +
+
+

Include Action History

+ +
+
+ +
+
+ ) : null} + + ); +} + +export { DebuggerTaskBlockParameters }; diff --git a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx index 16f1892f..5ea7d43d 100644 --- a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx @@ -226,7 +226,7 @@ type Props = { initialTitle: string; // initialParameters: ParametersState; workflow: WorkflowApiResponse; - onDebuggableBlockCountChange: (count: number) => void; + onDebuggableBlockCountChange?: (count: number) => void; onMouseDownCapture?: () => void; zIndex?: number; }; @@ -305,7 +305,7 @@ function FlowRenderer({ } } - onDebuggableBlockCountChange(debuggable.length); + onDebuggableBlockCountChange?.(debuggable.length); }, [nodes, edges, onDebuggableBlockCountChange]); const constructSaveData = useCallback((): WorkflowSaveData => { diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx index 8ab77701..19c3e499 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx @@ -15,17 +15,18 @@ import { ReloadIcon, } from "@radix-ui/react-icons"; import { useNavigate, useParams } from "react-router-dom"; +import { statusIsRunningOrQueued } from "@/routes/tasks/types"; import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery"; import { EditableNodeTitle } from "./nodes/components/EditableNodeTitle"; import { useCreateWorkflowMutation } from "../hooks/useCreateWorkflowMutation"; import { convert } from "./workflowEditorUtils"; +import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useDebugStore } from "@/store/useDebugStore"; import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore"; import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; import { cn } from "@/util/utils"; type Props = { - debuggableBlockCount: number; parametersPanelOpen: boolean; onParametersClick: () => void; onSave: () => void; @@ -34,7 +35,6 @@ type Props = { }; function WorkflowHeader({ - debuggableBlockCount, parametersPanelOpen, onParametersClick, onSave, @@ -43,13 +43,14 @@ function WorkflowHeader({ }: Props) { const { title, setTitle } = useWorkflowTitleStore(); const workflowChangesStore = useWorkflowHasChangesStore(); - const { blockLabel: urlBlockLabel, workflowPermanentId } = useParams(); + const { workflowPermanentId } = useParams(); const { data: globalWorkflows } = useGlobalWorkflowsQuery(); const navigate = useNavigate(); const createWorkflowMutation = useCreateWorkflowMutation(); + const { data: workflowRun } = useWorkflowRunQuery(); const debugStore = useDebugStore(); - const anyBlockIsPlaying = - urlBlockLabel !== undefined && urlBlockLabel.length > 0; + const workflowRunIsRunningOrQueued = + workflowRun && statusIsRunningOrQueued(workflowRun); if (!globalWorkflows) { return null; // this should be loaded already by some other components @@ -105,7 +106,7 @@ function WorkflowHeader({