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", complete: "complete",
wait: "wait", wait: "wait",
terminate: "terminate", terminate: "terminate",
SolveCaptcha: "solve_captcha",
} as const; } as const;
export type ActionType = (typeof ActionTypes)[keyof typeof ActionTypes]; export type ActionType = (typeof ActionTypes)[keyof typeof ActionTypes];
@@ -214,6 +215,7 @@ export const ReadableActionTypes: {
complete: "Complete", complete: "Complete",
wait: "Wait", wait: "Wait",
terminate: "Terminate", terminate: "Terminate",
solve_captcha: "Solve Captcha",
}; };
export type Option = { 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 { getClient } from "@/api/AxiosClient";
import { Action, ActionTypes, ReadableActionTypes } from "@/api/types"; import { Action, ActionTypes } from "@/api/types";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import {
@@ -13,8 +11,6 @@ import {
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { import {
ArrowDownIcon,
ArrowUpIcon,
CheckCircledIcon, CheckCircledIcon,
CrossCircledIcon, CrossCircledIcon,
DotFilledIcon, DotFilledIcon,
@@ -22,6 +18,7 @@ import {
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { ReactNode, useEffect, useRef } from "react"; import { ReactNode, useEffect, useRef } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { ActionTypePill } from "./ActionTypePill";
type Props = { type Props = {
data: Array<Action | null>; data: Array<Action | null>;
@@ -30,15 +27,19 @@ type Props = {
onActiveIndexChange: (index: number | "stream") => void; onActiveIndexChange: (index: number | "stream") => void;
activeIndex: number | "stream"; activeIndex: number | "stream";
showStreamOption: boolean; showStreamOption: boolean;
taskDetails: {
steps: number;
actions: number;
cost?: string;
};
}; };
function ScrollableActionList({ function ScrollableActionList({
data, data,
onNext,
onPrevious,
activeIndex, activeIndex,
onActiveIndexChange, onActiveIndexChange,
showStreamOption, showStreamOption,
taskDetails,
}: Props) { }: Props) {
const { taskId } = useParams(); const { taskId } = useParams();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -78,9 +79,11 @@ function ScrollableActionList({
refs.current[actionIndex] = element; refs.current[actionIndex] = element;
}} }}
className={cn( 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)} 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 justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>#{i + 1}</span> <span>#{i + 1}</span>
<Badge>{ReadableActionTypes[action.type]}</Badge>
</div> </div>
<div className="flex items-center gap-2"> <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" && ( {typeof action.confidence === "number" && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <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> </TooltipTrigger>
<TooltipContent>Confidence Score</TooltipContent> <TooltipContent>Confidence Score</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
{action.success ? (
<CheckCircledIcon className="h-6 w-6 text-success" />
) : (
<CrossCircledIcon className="h-6 w-6 text-destructive" />
)}
</div> </div>
</div> </div>
<div className="text-sm">{action.reasoning}</div> <div className="text-xs text-slate-400">{action.reasoning}</div>
{action.type === ActionTypes.InputText && ( {action.type === ActionTypes.InputText && (
<> <>
<Separator className="block bg-slate-50" /> <Separator />
<div className="text-sm">Input: {action.input}</div> <div className="text-xs text-slate-400">
Input: {action.input}
</div>
</> </>
)} )}
</div> </div>
@@ -135,30 +151,23 @@ function ScrollableActionList({
return elements; return elements;
} }
const actionIndex =
typeof activeIndex === "number" ? data.length - activeIndex - 1 : "stream";
return ( return (
<div className="flex h-[40rem] w-1/3 flex-col items-center rounded border"> <div className="h-[40rem] w-1/4 rounded border bg-slate-elevation1">
<div className="flex items-center gap-2 p-4 text-sm"> <div className="grid grid-cols-3 gap-2 p-4">
<Button <div className="flex h-8 items-center justify-center rounded-sm bg-slate-700 px-3 text-xs text-gray-50">
size="icon" Steps: {taskDetails.steps}
onClick={() => { </div>
onPrevious(); <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>
<ArrowUpIcon /> <div className="flex h-8 items-center justify-center rounded-sm bg-slate-700 px-3 text-xs text-gray-50">
</Button> Cost: {taskDetails.cost}
{typeof actionIndex === "number" && </div>
`#${actionIndex + 1} of ${data.length} total actions`}
{activeIndex === "stream" && "Livestream"}
<Button size="icon" onClick={() => onNext()}>
<ArrowDownIcon />
</Button>
</div> </div>
<ScrollArea className="w-full"> <Separator />
<ScrollAreaViewport className="h-full w-full rounded-[inherit]"> <ScrollArea className="p-4">
<div className="w-full space-y-4 px-4 pb-4"> <ScrollAreaViewport className="max-h-[34rem]">
<div className="space-y-4">
{showStreamOption && ( {showStreamOption && (
<div <div
key="stream" key="stream"
@@ -166,15 +175,15 @@ function ScrollableActionList({
refs.current[data.length] = element; refs.current[data.length] = element;
}} }}
className={cn( 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")} onClick={() => onActiveIndexChange("stream")}
> >
<div className="flex items-center gap-2 text-lg"> <div className="flex items-center gap-2">
<DotFilledIcon className="h-6 w-6 text-red-500" /> <DotFilledIcon className="h-6 w-6 text-destructive" />
Live Live
</div> </div>
</div> </div>

View File

@@ -17,6 +17,12 @@ import { toast } from "@/components/ui/use-toast";
import { envCredential } from "@/util/env"; import { envCredential } from "@/util/env";
import { statusIsNotFinalized, statusIsRunningOrQueued } from "../types"; import { statusIsNotFinalized, statusIsRunningOrQueued } from "../types";
import { ZoomableImage } from "@/components/ZoomableImage"; import { ZoomableImage } from "@/components/ZoomableImage";
import { useCostCalculator } from "@/hooks/useCostCalculator";
const formatter = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
type StreamMessage = { type StreamMessage = {
task_id: string; task_id: string;
@@ -45,6 +51,7 @@ function TaskActions() {
const credentialGetter = useCredentialGetter(); const credentialGetter = useCredentialGetter();
const [streamImgSrc, setStreamImgSrc] = useState<string>(""); const [streamImgSrc, setStreamImgSrc] = useState<string>("");
const [selectedAction, setSelectedAction] = useState<number | "stream">(0); const [selectedAction, setSelectedAction] = useState<number | "stream">(0);
const costCalculator = useCostCalculator();
const { data: task, isLoading: taskIsLoading } = useQuery<TaskApiResponse>({ const { data: task, isLoading: taskIsLoading } = useQuery<TaskApiResponse>({
queryKey: ["task", taskId], queryKey: ["task", taskId],
@@ -202,7 +209,7 @@ function TaskActions() {
function getStream() { function getStream() {
if (task?.status === Status.Created) { if (task?.status === Status.Created) {
return ( 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>Task has been created.</span>
<span>Stream will start when the task is running.</span> <span>Stream will start when the task is running.</span>
</div> </div>
@@ -210,7 +217,7 @@ function TaskActions() {
} }
if (task?.status === Status.Queued) { if (task?.status === Status.Queued) {
return ( 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>Your task is queued. Typical queue time is 1-2 minutes.</span>
<span>Stream will start when the task is running.</span> <span>Stream will start when the task is running.</span>
</div> </div>
@@ -219,7 +226,7 @@ function TaskActions() {
if (task?.status === Status.Running && streamImgSrc.length === 0) { if (task?.status === Status.Running && streamImgSrc.length === 0) {
return ( 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... Starting the stream...
</div> </div>
); );
@@ -235,9 +242,12 @@ function TaskActions() {
return null; return null;
} }
const showCost = typeof costCalculator === "function";
const notRunningSteps = steps?.filter((step) => step.status !== "running");
return ( return (
<div className="flex gap-2"> <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"> <div className="h-full w-full p-4">
{selectedAction === "stream" ? getStream() : null} {selectedAction === "stream" ? getStream() : null}
{typeof selectedAction === "number" && activeAction ? ( {typeof selectedAction === "number" && activeAction ? (
@@ -253,6 +263,13 @@ function TaskActions() {
data={actions ?? []} data={actions ?? []}
onActiveIndexChange={setSelectedAction} onActiveIndexChange={setSelectedAction}
showStreamOption={Boolean(taskIsNotFinalized)} showStreamOption={Boolean(taskIsNotFinalized)}
taskDetails={{
steps: steps?.length ?? 0,
actions: actions?.length ?? 0,
cost: showCost
? formatter.format(costCalculator(notRunningSteps ?? []))
: undefined,
}}
onNext={() => { onNext={() => {
if (!actions) { if (!actions) {
return; return;

View File

@@ -17,14 +17,12 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, NavLink, Outlet, useParams } from "react-router-dom"; import { Link, NavLink, Outlet, useParams } from "react-router-dom";
import { TaskInfo } from "./TaskInfo";
import { useTaskQuery } from "./hooks/useTaskQuery"; import { useTaskQuery } from "./hooks/useTaskQuery";
import { taskIsFinalized } from "@/api/utils"; import { taskIsFinalized } from "@/api/utils";
import fetchToCurl from "fetch-to-curl"; import fetchToCurl from "fetch-to-curl";
@@ -32,6 +30,8 @@ import { apiBaseUrl } from "@/util/env";
import { useApiCredential } from "@/hooks/useApiCredential"; import { useApiCredential } from "@/hooks/useApiCredential";
import { copyText } from "@/util/copyText"; import { copyText } from "@/util/copyText";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes"; import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
import { StatusBadge } from "@/components/StatusBadge";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
function createTaskRequestObject(values: TaskApiResponse) { function createTaskRequestObject(values: TaskApiResponse) {
return { return {
@@ -119,12 +119,16 @@ function TaskDetails() {
const showExtractedInformation = const showExtractedInformation =
task?.status === Status.Completed && task.extracted_information !== null; task?.status === Status.Completed && task.extracted_information !== null;
const extractedInformation = showExtractedInformation ? ( const extractedInformation = showExtractedInformation ? (
<div className="flex items-center"> <div className="space-y-1">
<Label className="w-32 shrink-0 text-lg">Extracted Information</Label> <Label className="text-lg">Extracted Information</Label>
<Textarea <CodeEditor
rows={5} language="json"
value={JSON.stringify(task.extracted_information, null, 2)} value={JSON.stringify(task.extracted_information, null, 2)}
readOnly disabled
fontSize={12}
minHeight={"96px"}
maxHeight={"500px"}
className="w-full"
/> />
</div> </div>
) : null; ) : null;
@@ -139,12 +143,16 @@ function TaskDetails() {
task?.status === Status.Terminated || task?.status === Status.Terminated ||
task?.status === Status.TimedOut; task?.status === Status.TimedOut;
const failureReason = showFailureReason ? ( const failureReason = showFailureReason ? (
<div className="flex items-center"> <div className="space-y-1">
<Label className="w-32 shrink-0 text-lg">Failure Reason</Label> <Label className="text-lg">Failure Reason</Label>
<Textarea <CodeEditor
rows={5} language="json"
value={JSON.stringify(task.failure_reason, null, 2)} value={JSON.stringify(task.failure_reason, null, 2)}
readOnly disabled
fontSize={12}
minHeight={"96px"}
maxHeight={"500px"}
className="w-full"
/> />
</div> </div>
) : null; ) : null;
@@ -153,9 +161,13 @@ function TaskDetails() {
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<header className="space-y-3"> <header className="space-y-3">
<div className="flex items-center justify-between"> <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> <span className="text-3xl">{taskId}</span>
{taskId && <TaskInfo id={taskId} />} {taskIsLoading ? (
<Skeleton className="h-8 w-32" />
) : (
task && <StatusBadge status={task.status} />
)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@@ -245,10 +257,7 @@ function TaskDetails() {
</header> </header>
{taskIsLoading ? ( {taskIsLoading ? (
<div className="flex items-center gap-2"> <Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-32" />
<Skeleton className="h-32 w-full" />
</div>
) : ( ) : (
<> <>
{extractedInformation} {extractedInformation}

View File

@@ -22,6 +22,7 @@ function CodeEditor({
maxHeight, maxHeight,
language, language,
className, className,
disabled,
fontSize = 8, fontSize = 8,
}: Props) { }: Props) {
const extensions = const extensions =
@@ -36,6 +37,7 @@ function CodeEditor({
theme={tokyoNightStorm} theme={tokyoNightStorm}
minHeight={minHeight} minHeight={minHeight}
maxHeight={maxHeight} maxHeight={maxHeight}
readOnly={disabled}
className={cn("cursor-auto", className)} className={cn("cursor-auto", className)}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,