Task details page improvements (#1004)
This commit is contained in:
@@ -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 = {
|
||||
|
||||
22
skyvern-frontend/src/routes/tasks/detail/ActionTypePill.tsx
Normal file
22
skyvern-frontend/src/routes/tasks/detail/ActionTypePill.tsx
Normal file
@@ -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<Record<ActionType, React.ReactNode>> = {
|
||||
click: <CursorArrowIcon className="h-4 w-4" />,
|
||||
input_text: <InputIcon className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
function ActionTypePill({ actionType }: Props) {
|
||||
return (
|
||||
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||
{icons[actionType] ?? null}
|
||||
<span className="text-xs">{ReadableActionTypes[actionType]}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ActionTypePill };
|
||||
@@ -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<Action | null>;
|
||||
@@ -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({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 space-y-2 p-2 pt-0">
|
||||
<div className="flex-1 space-y-2 p-4 pl-5">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>#{i + 1}</span>
|
||||
<Badge>{ReadableActionTypes[action.type]}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ActionTypePill actionType={action.type} />
|
||||
{action.success ? (
|
||||
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||
<CheckCircledIcon className="h-4 w-4 text-success" />
|
||||
<span className="text-xs">Success</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||
<CrossCircledIcon className="h-4 w-4 text-destructive" />
|
||||
<span className="text-xs">Fail</span>
|
||||
</div>
|
||||
)}
|
||||
{typeof action.confidence === "number" && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Badge variant="secondary">{action.confidence}</Badge>
|
||||
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||
<span className="text-xs text-success">
|
||||
{action.confidence}
|
||||
</span>
|
||||
<span className="text-xs">CS</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Confidence Score</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{action.success ? (
|
||||
<CheckCircledIcon className="h-6 w-6 text-success" />
|
||||
) : (
|
||||
<CrossCircledIcon className="h-6 w-6 text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">{action.reasoning}</div>
|
||||
<div className="text-xs text-slate-400">{action.reasoning}</div>
|
||||
{action.type === ActionTypes.InputText && (
|
||||
<>
|
||||
<Separator className="block bg-slate-50" />
|
||||
<div className="text-sm">Input: {action.input}</div>
|
||||
<Separator />
|
||||
<div className="text-xs text-slate-400">
|
||||
Input: {action.input}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -135,30 +151,23 @@ function ScrollableActionList({
|
||||
return elements;
|
||||
}
|
||||
|
||||
const actionIndex =
|
||||
typeof activeIndex === "number" ? data.length - activeIndex - 1 : "stream";
|
||||
|
||||
return (
|
||||
<div className="flex h-[40rem] w-1/3 flex-col items-center rounded border">
|
||||
<div className="flex items-center gap-2 p-4 text-sm">
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
onPrevious();
|
||||
}}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</Button>
|
||||
{typeof actionIndex === "number" &&
|
||||
`#${actionIndex + 1} of ${data.length} total actions`}
|
||||
{activeIndex === "stream" && "Livestream"}
|
||||
<Button size="icon" onClick={() => onNext()}>
|
||||
<ArrowDownIcon />
|
||||
</Button>
|
||||
<div className="h-[40rem] w-1/4 rounded border bg-slate-elevation1">
|
||||
<div className="grid grid-cols-3 gap-2 p-4">
|
||||
<div className="flex h-8 items-center justify-center rounded-sm bg-slate-700 px-3 text-xs text-gray-50">
|
||||
Steps: {taskDetails.steps}
|
||||
</div>
|
||||
<div className="flex h-8 items-center justify-center rounded-sm bg-slate-700 px-3 text-xs text-gray-50">
|
||||
Actions: {taskDetails.actions}
|
||||
</div>
|
||||
<div className="flex h-8 items-center justify-center rounded-sm bg-slate-700 px-3 text-xs text-gray-50">
|
||||
Cost: {taskDetails.cost}
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="w-full">
|
||||
<ScrollAreaViewport className="h-full w-full rounded-[inherit]">
|
||||
<div className="w-full space-y-4 px-4 pb-4">
|
||||
<Separator />
|
||||
<ScrollArea className="p-4">
|
||||
<ScrollAreaViewport className="max-h-[34rem]">
|
||||
<div className="space-y-4">
|
||||
{showStreamOption && (
|
||||
<div
|
||||
key="stream"
|
||||
@@ -166,15 +175,15 @@ function ScrollableActionList({
|
||||
refs.current[data.length] = 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 p-4 hover:border-slate-50",
|
||||
{
|
||||
"border-slate-300": activeIndex === "stream",
|
||||
"border-slate-50": activeIndex === "stream",
|
||||
},
|
||||
)}
|
||||
onClick={() => onActiveIndexChange("stream")}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-lg">
|
||||
<DotFilledIcon className="h-6 w-6 text-red-500" />
|
||||
<div className="flex items-center gap-2">
|
||||
<DotFilledIcon className="h-6 w-6 text-destructive" />
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<string>("");
|
||||
const [selectedAction, setSelectedAction] = useState<number | "stream">(0);
|
||||
const costCalculator = useCostCalculator();
|
||||
|
||||
const { data: task, isLoading: taskIsLoading } = useQuery<TaskApiResponse>({
|
||||
queryKey: ["task", taskId],
|
||||
@@ -202,7 +209,7 @@ function TaskActions() {
|
||||
function getStream() {
|
||||
if (task?.status === Status.Created) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-slate-900 text-lg">
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-slate-elevation1 text-lg">
|
||||
<span>Task has been created.</span>
|
||||
<span>Stream will start when the task is running.</span>
|
||||
</div>
|
||||
@@ -210,7 +217,7 @@ function TaskActions() {
|
||||
}
|
||||
if (task?.status === Status.Queued) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-slate-900 text-lg">
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-slate-elevation1 text-lg">
|
||||
<span>Your task is queued. Typical queue time is 1-2 minutes.</span>
|
||||
<span>Stream will start when the task is running.</span>
|
||||
</div>
|
||||
@@ -219,7 +226,7 @@ function TaskActions() {
|
||||
|
||||
if (task?.status === Status.Running && streamImgSrc.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-slate-900 text-lg">
|
||||
<div className="flex h-full w-full items-center justify-center bg-slate-elevation1 text-lg">
|
||||
Starting the stream...
|
||||
</div>
|
||||
);
|
||||
@@ -235,9 +242,12 @@ function TaskActions() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showCost = typeof costCalculator === "function";
|
||||
const notRunningSteps = steps?.filter((step) => step.status !== "running");
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="w-2/3 rounded border">
|
||||
<div className="w-3/4 rounded border">
|
||||
<div className="h-full w-full p-4">
|
||||
{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;
|
||||
|
||||
@@ -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 ? (
|
||||
<div className="flex items-center">
|
||||
<Label className="w-32 shrink-0 text-lg">Extracted Information</Label>
|
||||
<Textarea
|
||||
rows={5}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-lg">Extracted Information</Label>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={JSON.stringify(task.extracted_information, null, 2)}
|
||||
readOnly
|
||||
disabled
|
||||
fontSize={12}
|
||||
minHeight={"96px"}
|
||||
maxHeight={"500px"}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
@@ -139,12 +143,16 @@ function TaskDetails() {
|
||||
task?.status === Status.Terminated ||
|
||||
task?.status === Status.TimedOut;
|
||||
const failureReason = showFailureReason ? (
|
||||
<div className="flex items-center">
|
||||
<Label className="w-32 shrink-0 text-lg">Failure Reason</Label>
|
||||
<Textarea
|
||||
rows={5}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-lg">Failure Reason</Label>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={JSON.stringify(task.failure_reason, null, 2)}
|
||||
readOnly
|
||||
disabled
|
||||
fontSize={12}
|
||||
minHeight={"96px"}
|
||||
maxHeight={"500px"}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
@@ -153,9 +161,13 @@ function TaskDetails() {
|
||||
<div className="flex flex-col gap-8">
|
||||
<header className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-5">
|
||||
<span className="text-3xl">{taskId}</span>
|
||||
{taskId && <TaskInfo id={taskId} />}
|
||||
{taskIsLoading ? (
|
||||
<Skeleton className="h-8 w-32" />
|
||||
) : (
|
||||
task && <StatusBadge status={task.status} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -245,10 +257,7 @@ function TaskDetails() {
|
||||
</header>
|
||||
|
||||
{taskIsLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-32 w-32" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
<Skeleton className="h-32 w-full" />
|
||||
) : (
|
||||
<>
|
||||
{extractedInformation}
|
||||
|
||||
@@ -22,6 +22,7 @@ function CodeEditor({
|
||||
maxHeight,
|
||||
language,
|
||||
className,
|
||||
disabled,
|
||||
fontSize = 8,
|
||||
}: Props) {
|
||||
const extensions =
|
||||
@@ -36,6 +37,7 @@ function CodeEditor({
|
||||
theme={tokyoNightStorm}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
readOnly={disabled}
|
||||
className={cn("cursor-auto", className)}
|
||||
style={{
|
||||
fontSize: fontSize,
|
||||
|
||||
Reference in New Issue
Block a user