Task details page improvements (#1004)

This commit is contained in:
Shuchang Zheng
2024-10-18 11:49:49 -07:00
committed by GitHub
parent dd677132fe
commit a6e257369b
6 changed files with 129 additions and 68 deletions

View File

@@ -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 = {

View 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 };

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}

View File

@@ -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,