diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index d818e1d7..cfebe57a 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -200,6 +200,7 @@ export const ActionTypes = { complete: "complete", wait: "wait", terminate: "terminate", + SolveCaptcha: "solve_captcha", } as const; export type ActionType = (typeof ActionTypes)[keyof typeof ActionTypes]; @@ -214,6 +215,7 @@ export const ReadableActionTypes: { complete: "Complete", wait: "Wait", terminate: "Terminate", + solve_captcha: "Solve Captcha", }; export type Option = { diff --git a/skyvern-frontend/src/routes/tasks/detail/ActionTypePill.tsx b/skyvern-frontend/src/routes/tasks/detail/ActionTypePill.tsx new file mode 100644 index 00000000..b58550e2 --- /dev/null +++ b/skyvern-frontend/src/routes/tasks/detail/ActionTypePill.tsx @@ -0,0 +1,22 @@ +import { ActionType, ReadableActionTypes } from "@/api/types"; +import { CursorArrowIcon, InputIcon } from "@radix-ui/react-icons"; + +type Props = { + actionType: ActionType; +}; + +const icons: Partial> = { + click: , + input_text: , +}; + +function ActionTypePill({ actionType }: Props) { + return ( +
+ {icons[actionType] ?? null} + {ReadableActionTypes[actionType]} +
+ ); +} + +export { ActionTypePill }; diff --git a/skyvern-frontend/src/routes/tasks/detail/ScrollableActionList.tsx b/skyvern-frontend/src/routes/tasks/detail/ScrollableActionList.tsx index 381cf7c8..75e4f300 100644 --- a/skyvern-frontend/src/routes/tasks/detail/ScrollableActionList.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/ScrollableActionList.tsx @@ -1,7 +1,5 @@ import { getClient } from "@/api/AxiosClient"; -import { Action, ActionTypes, ReadableActionTypes } from "@/api/types"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Action, ActionTypes } from "@/api/types"; import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { @@ -13,8 +11,6 @@ import { import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { cn } from "@/util/utils"; import { - ArrowDownIcon, - ArrowUpIcon, CheckCircledIcon, CrossCircledIcon, DotFilledIcon, @@ -22,6 +18,7 @@ import { import { useQueryClient } from "@tanstack/react-query"; import { ReactNode, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; +import { ActionTypePill } from "./ActionTypePill"; type Props = { data: Array; @@ -30,15 +27,19 @@ type Props = { onActiveIndexChange: (index: number | "stream") => void; activeIndex: number | "stream"; showStreamOption: boolean; + taskDetails: { + steps: number; + actions: number; + cost?: string; + }; }; function ScrollableActionList({ data, - onNext, - onPrevious, activeIndex, onActiveIndexChange, showStreamOption, + taskDetails, }: Props) { const { taskId } = useParams(); const queryClient = useQueryClient(); @@ -78,9 +79,11 @@ function ScrollableActionList({ refs.current[actionIndex] = element; }} className={cn( - "flex cursor-pointer rounded-lg border p-4 shadow-md hover:border-slate-300", + "flex cursor-pointer rounded-lg border-2 bg-slate-elevation3 hover:border-slate-50", { - "border-slate-300": selected, + "border-l-destructive": !action.success, + "border-l-success": action.success, + "border-slate-50": selected, }, )} onClick={() => onActiveIndexChange(actionIndex)} @@ -96,36 +99,49 @@ function ScrollableActionList({ }); }} > -
+
#{i + 1} - {ReadableActionTypes[action.type]}
+ + {action.success ? ( +
+ + Success +
+ ) : ( +
+ + Fail +
+ )} {typeof action.confidence === "number" && ( - {action.confidence} +
+ + {action.confidence} + + CS +
Confidence Score
)} - {action.success ? ( - - ) : ( - - )}
-
{action.reasoning}
+
{action.reasoning}
{action.type === ActionTypes.InputText && ( <> - -
Input: {action.input}
+ +
+ Input: {action.input} +
)}
@@ -135,30 +151,23 @@ function ScrollableActionList({ return elements; } - const actionIndex = - typeof activeIndex === "number" ? data.length - activeIndex - 1 : "stream"; - return ( -
-
- - {typeof actionIndex === "number" && - `#${actionIndex + 1} of ${data.length} total actions`} - {activeIndex === "stream" && "Livestream"} - +
+
+
+ Steps: {taskDetails.steps} +
+
+ Actions: {taskDetails.actions} +
+
+ Cost: {taskDetails.cost} +
- - -
+ + + +
{showStreamOption && (
onActiveIndexChange("stream")} > -
- +
+ Live
diff --git a/skyvern-frontend/src/routes/tasks/detail/TaskActions.tsx b/skyvern-frontend/src/routes/tasks/detail/TaskActions.tsx index 6af2513b..78b9b05d 100644 --- a/skyvern-frontend/src/routes/tasks/detail/TaskActions.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/TaskActions.tsx @@ -17,6 +17,12 @@ import { toast } from "@/components/ui/use-toast"; import { envCredential } from "@/util/env"; import { statusIsNotFinalized, statusIsRunningOrQueued } from "../types"; import { ZoomableImage } from "@/components/ZoomableImage"; +import { useCostCalculator } from "@/hooks/useCostCalculator"; + +const formatter = Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", +}); type StreamMessage = { task_id: string; @@ -45,6 +51,7 @@ function TaskActions() { const credentialGetter = useCredentialGetter(); const [streamImgSrc, setStreamImgSrc] = useState(""); const [selectedAction, setSelectedAction] = useState(0); + const costCalculator = useCostCalculator(); const { data: task, isLoading: taskIsLoading } = useQuery({ queryKey: ["task", taskId], @@ -202,7 +209,7 @@ function TaskActions() { function getStream() { if (task?.status === Status.Created) { return ( -
+
Task has been created. Stream will start when the task is running.
@@ -210,7 +217,7 @@ function TaskActions() { } if (task?.status === Status.Queued) { return ( -
+
Your task is queued. Typical queue time is 1-2 minutes. Stream will start when the task is running.
@@ -219,7 +226,7 @@ function TaskActions() { if (task?.status === Status.Running && streamImgSrc.length === 0) { return ( -
+
Starting the stream...
); @@ -235,9 +242,12 @@ function TaskActions() { return null; } + const showCost = typeof costCalculator === "function"; + const notRunningSteps = steps?.filter((step) => step.status !== "running"); + return (
-
+
{selectedAction === "stream" ? getStream() : null} {typeof selectedAction === "number" && activeAction ? ( @@ -253,6 +263,13 @@ function TaskActions() { data={actions ?? []} onActiveIndexChange={setSelectedAction} showStreamOption={Boolean(taskIsNotFinalized)} + taskDetails={{ + steps: steps?.length ?? 0, + actions: actions?.length ?? 0, + cost: showCost + ? formatter.format(costCalculator(notRunningSteps ?? [])) + : undefined, + }} onNext={() => { if (!actions) { return; diff --git a/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx b/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx index 342cf212..c62b53a2 100644 --- a/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx @@ -17,14 +17,12 @@ import { } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; -import { Textarea } from "@/components/ui/textarea"; import { toast } from "@/components/ui/use-toast"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { cn } from "@/util/utils"; import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, NavLink, Outlet, useParams } from "react-router-dom"; -import { TaskInfo } from "./TaskInfo"; import { useTaskQuery } from "./hooks/useTaskQuery"; import { taskIsFinalized } from "@/api/utils"; import fetchToCurl from "fetch-to-curl"; @@ -32,6 +30,8 @@ import { apiBaseUrl } from "@/util/env"; import { useApiCredential } from "@/hooks/useApiCredential"; import { copyText } from "@/util/copyText"; import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes"; +import { StatusBadge } from "@/components/StatusBadge"; +import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; function createTaskRequestObject(values: TaskApiResponse) { return { @@ -119,12 +119,16 @@ function TaskDetails() { const showExtractedInformation = task?.status === Status.Completed && task.extracted_information !== null; const extractedInformation = showExtractedInformation ? ( -
- -