Remove accordion, add cards, change layout of task page (#225)

This commit is contained in:
Salih Altun
2024-04-24 17:11:12 +03:00
committed by GitHub
parent a23c9f11a8
commit 90fbe50579
7 changed files with 224 additions and 142 deletions

View File

@@ -17,6 +17,7 @@
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
@@ -1451,6 +1452,29 @@
} }
} }
}, },
"node_modules/@radix-ui/react-separator": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz",
"integrity": "sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",

View File

@@ -23,6 +23,7 @@
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",

View File

@@ -0,0 +1,29 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/util/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

View File

@@ -14,6 +14,8 @@ import { Skeleton } from "@/components/ui/skeleton";
import { JSONArtifact } from "./JSONArtifact"; import { JSONArtifact } from "./JSONArtifact";
import { TextArtifact } from "./TextArtifact"; import { TextArtifact } from "./TextArtifact";
import { getImageURL } from "./artifactUtils"; import { getImageURL } from "./artifactUtils";
import { Input } from "@/components/ui/input";
import { basicTimeFormat } from "@/util/timeFormat";
type Props = { type Props = {
id: string; id: string;
@@ -81,7 +83,7 @@ function StepArtifacts({ id, stepProps }: Props) {
<Tabs defaultValue="info" className="w-full"> <Tabs defaultValue="info" className="w-full">
<TabsList className="grid w-full h-16 grid-cols-5"> <TabsList className="grid w-full h-16 grid-cols-5">
<TabsTrigger value="info">Info</TabsTrigger> <TabsTrigger value="info">Info</TabsTrigger>
<TabsTrigger value="screenshot_llm">LLM Screenshots</TabsTrigger> <TabsTrigger value="screenshot_llm">Page Screenshots</TabsTrigger>
<TabsTrigger value="screenshot_action">Action Screenshots</TabsTrigger> <TabsTrigger value="screenshot_action">Action Screenshots</TabsTrigger>
<TabsTrigger value="element_tree">Element Tree</TabsTrigger> <TabsTrigger value="element_tree">Element Tree</TabsTrigger>
<TabsTrigger value="element_tree_trimmed"> <TabsTrigger value="element_tree_trimmed">
@@ -96,17 +98,17 @@ function StepArtifacts({ id, stepProps }: Props) {
<TabsTrigger value="html_raw">HTML (Raw)</TabsTrigger> <TabsTrigger value="html_raw">HTML (Raw)</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="info"> <TabsContent value="info">
<div className="flex flex-col gap-4 p-4"> <div className="flex flex-col gap-6 p-4">
<div className="flex items-center"> <div className="flex items-center">
<Label className="w-24">Step ID:</Label> <Label className="w-32 shrink-0 text-xl">Step ID</Label>
{isFetching ? ( {isFetching ? (
<Skeleton className="h-4 w-40" /> <Skeleton className="h-4 w-40" />
) : ( ) : (
<span>{stepProps?.step_id}</span> <Input value={stepProps?.step_id} readOnly />
)} )}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<Label className="w-24">Status:</Label> <Label className="w-32 shrink-0 text-xl">Status</Label>
{isFetching ? ( {isFetching ? (
<Skeleton className="h-4 w-40" /> <Skeleton className="h-4 w-40" />
) : stepProps ? ( ) : stepProps ? (
@@ -114,11 +116,11 @@ function StepArtifacts({ id, stepProps }: Props) {
) : null} ) : null}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<Label className="w-24">Created At:</Label> <Label className="w-32 shrink-0 text-xl">Created At</Label>
{isFetching ? ( {isFetching ? (
<Skeleton className="h-4 w-40" /> <Skeleton className="h-4 w-40" />
) : stepProps ? ( ) : stepProps ? (
<span>{stepProps.created_at}</span> <Input value={basicTimeFormat(stepProps.created_at)} readOnly />
) : null} ) : null}
</div> </div>
</div> </div>

View File

@@ -12,7 +12,6 @@ function StepArtifactsLayout() {
const { const {
data: steps, data: steps,
isFetching,
isError, isError,
error, error,
} = useQuery<Array<StepApiResponse>>({ } = useQuery<Array<StepApiResponse>>({
@@ -24,10 +23,6 @@ function StepArtifactsLayout() {
}, },
}); });
if (isFetching) {
return <div>Loading...</div>;
}
if (isError) { if (isError) {
return <div>Error: {error?.message}</div>; return <div>Error: {error?.message}</div>;
} }

View File

@@ -4,6 +4,7 @@ import { cn } from "@/util/utils";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { PAGE_SIZE } from "../constants"; import { PAGE_SIZE } from "../constants";
import { CheckboxIcon, CrossCircledIcon } from "@radix-ui/react-icons";
type Props = { type Props = {
activeIndex: number; activeIndex: number;
@@ -17,7 +18,6 @@ function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
const { const {
data: steps, data: steps,
isFetching,
isError, isError,
error, error,
} = useQuery<Array<StepApiResponse>>({ } = useQuery<Array<StepApiResponse>>({
@@ -34,10 +34,6 @@ function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
}, },
}); });
if (isFetching) {
return <div>Loading...</div>;
}
if (isError) { if (isError) {
return <div>Error: {error?.message}</div>; return <div>Error: {error?.message}</div>;
} }
@@ -53,7 +49,7 @@ function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
return ( return (
<div <div
className={cn( className={cn(
"flex justify-center items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl cursor-pointer", "flex items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl cursor-pointer",
{ {
"bg-primary-foreground": isActive, "bg-primary-foreground": isActive,
}, },
@@ -63,6 +59,12 @@ function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
onActiveIndexChange(index); onActiveIndexChange(index);
}} }}
> >
{step.status === "completed" && (
<CheckboxIcon className="w-6 h-6 mr-2 text-green-500" />
)}
{step.status === "failed" && (
<CrossCircledIcon className="w-6 h-6 mr-2 text-red-500" />
)}
<span> <span>
{step.retry_index > 0 {step.retry_index > 0
? `Step ${step.order + 1} ( Retry ${step.retry_index} )` ? `Step ${step.order + 1} ( Retry ${step.retry_index} )`

View File

@@ -2,23 +2,24 @@ import { client } from "@/api/AxiosClient";
import { Status, TaskApiResponse } from "@/api/types"; import { Status, TaskApiResponse } from "@/api/types";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { StatusBadge } from "@/components/StatusBadge"; import { StatusBadge } from "@/components/StatusBadge";
import { Button } from "@/components/ui/button";
import { ReloadIcon } from "@radix-ui/react-icons";
import { basicTimeFormat } from "@/util/timeFormat"; import { basicTimeFormat } from "@/util/timeFormat";
import { StepArtifactsLayout } from "./StepArtifactsLayout"; import { StepArtifactsLayout } from "./StepArtifactsLayout";
import Zoom from "react-medium-image-zoom";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { getRecordingURL, getScreenshotURL } from "./artifactUtils"; import { getRecordingURL, getScreenshotURL } from "./artifactUtils";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Input } from "@/components/ui/input";
import { ZoomableImage } from "@/components/ZoomableImage";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
function TaskDetails() { function TaskDetails() {
const { taskId } = useParams(); const { taskId } = useParams();
@@ -28,12 +29,21 @@ function TaskDetails() {
isFetching: isTaskFetching, isFetching: isTaskFetching,
isError: isTaskError, isError: isTaskError,
error: taskError, error: taskError,
refetch,
} = useQuery<TaskApiResponse>({ } = useQuery<TaskApiResponse>({
queryKey: ["task", taskId, "details"], queryKey: ["task", taskId, "details"],
queryFn: async () => { queryFn: async () => {
return client.get(`/tasks/${taskId}`).then((response) => response.data); return client.get(`/tasks/${taskId}`).then((response) => response.data);
}, },
refetchInterval: (query) => {
if (
query.state.data?.status === Status.Running ||
query.state.data?.status === Status.Queued
) {
return 3000;
}
return false;
},
placeholderData: keepPreviousData,
}); });
if (isTaskError) { if (isTaskError) {
@@ -41,124 +51,143 @@ function TaskDetails() {
} }
return ( return (
<div> <div className="flex flex-col gap-8">
<div className="flex flex-col gap-4 relative"> <div className="flex items-center">
<Button <Label className="w-32 shrink-0 text-lg">Task ID</Label>
variant="ghost" <Input value={taskId} readOnly />
size="icon" </div>
className="cursor-pointer absolute top-0 right-0" <div className="flex items-center">
onClick={() => { <Label className="w-32 text-lg">Status</Label>
refetch(); {isTaskFetching ? (
}} <Skeleton className="w-32 h-8" />
> ) : task ? (
<ReloadIcon className="w-4 h-4" /> <StatusBadge status={task?.status} />
</Button>
{task?.recording_url ? (
<div className="flex">
<Label className="w-32">Recording</Label>
<video src={getRecordingURL(task)} controls />
</div>
) : null}
<div className="flex items-center">
<Label className="w-32">Status</Label>
{isTaskFetching ? (
<Skeleton className="w-32 h-8" />
) : task ? (
<StatusBadge status={task?.status} />
) : null}
</div>
{task?.status === Status.Completed ? (
<div className="flex items-center">
<Label className="w-32 shrink-0">Extracted Information</Label>
<Textarea
rows={5}
value={JSON.stringify(task.extracted_information, null, 2)}
readOnly
/>
</div>
) : null}
{task?.status === Status.Failed ||
task?.status === Status.Terminated ? (
<div className="flex items-center">
<Label className="w-32 shrink-0">Failure Reason</Label>
<Textarea
rows={5}
value={JSON.stringify(task.failure_reason)}
readOnly
/>
</div>
) : null} ) : null}
</div> </div>
<Accordion type="multiple"> {task?.status === Status.Completed ? (
<AccordionItem value="task-parameters"> <div className="flex items-center">
<AccordionTrigger> <Label className="w-32 shrink-0 text-lg">Extracted Information</Label>
<h1>Task Parameters</h1> <Textarea
</AccordionTrigger> rows={5}
<AccordionContent> value={JSON.stringify(task.extracted_information, null, 2)}
{task ? ( readOnly
<div> />
<p className="py-2">Task ID: {taskId}</p> </div>
<p className="py-2">URL: {task.request.url}</p> ) : null}
<p className="py-2"> {task?.status === Status.Failed || task?.status === Status.Terminated ? (
Created: {basicTimeFormat(task.created_at)} <div className="flex items-center">
</p> <Label className="w-32 shrink-0 text-lg">Failure Reason</Label>
<div className="py-2"> <Textarea
<Label>Navigation Goal</Label> rows={5}
<Textarea value={JSON.stringify(task.failure_reason)}
rows={5} readOnly
value={task.request.navigation_goal} />
readOnly </div>
) : null}
{task ? (
<Card>
<CardHeader className="border-b-2">
<CardTitle className="text-xl">Task Artifacts</CardTitle>
<CardDescription>
Recording and final screenshot of the task
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="recording">
<TabsList>
<TabsTrigger value="recording">Recording</TabsTrigger>
<TabsTrigger value="final-screenshot">
Final Screenshot
</TabsTrigger>
</TabsList>
<TabsContent value="recording">
{task.recording_url ? (
<video
width={800}
height={450}
src={getRecordingURL(task)}
controls
/> />
</div>
<div className="py-2">
<Label>Navigation Payload</Label>
<Textarea
rows={5}
value={task.request.navigation_payload}
readOnly
/>
</div>
<div className="py-2">
<Label>Data Extraction Goal</Label>
<Textarea
rows={5}
value={task.request.data_extraction_goal}
readOnly
/>
</div>
</div>
) : null}
</AccordionContent>
</AccordionItem>
<AccordionItem value="task-artifacts">
<AccordionTrigger>
<h1>Final Screenshot</h1>
</AccordionTrigger>
<AccordionContent>
{task ? (
<div className="max-w-sm mx-auto">
{task.screenshot_url ? (
<Zoom zoomMargin={16}>
<AspectRatio ratio={16 / 9}>
<img src={getScreenshotURL(task)} alt="screenshot" />
</AspectRatio>
</Zoom>
) : ( ) : (
<p>No screenshot</p> <div>No recording available</div>
)} )}
</TabsContent>
<TabsContent value="final-screenshot">
{task ? (
<div className="h-[450px] w-[800px]">
{task.screenshot_url ? (
<ZoomableImage
src={getScreenshotURL(task)}
alt="screenshot"
className="object-cover w-full h-full"
/>
) : (
<p>No screenshot available</p>
)}
</div>
) : null}
</TabsContent>
</Tabs>
</CardContent>
</Card>
) : null}
<Card>
<CardHeader className="border-b-2">
<CardTitle className="text-xl">Parameters</CardTitle>
<CardDescription>Task URL and Input Parameters</CardDescription>
</CardHeader>
<CardContent className="py-8">
{task ? (
<div className="flex flex-col gap-8">
<div className="flex items-center">
<Label className="w-40 shrink-0">URL</Label>
<Input value={task.request.url} readOnly />
</div> </div>
) : null} <Separator />
</AccordionContent> <div className="flex items-center">
</AccordionItem> <Label className="w-40 shrink-0">Created at</Label>
<AccordionItem value="task-steps"> <Input value={basicTimeFormat(task.created_at)} readOnly />
<AccordionTrigger> </div>
<h1>Task Steps</h1> <Separator />
</AccordionTrigger> <div className="flex items-center">
<AccordionContent> <Label className="w-40 shrink-0">Navigation Goal</Label>
<StepArtifactsLayout /> <Textarea
</AccordionContent> rows={5}
</AccordionItem> value={task.request.navigation_goal}
</Accordion> readOnly
/>
</div>
<Separator />
<div className="flex items-center">
<Label className="w-40 shrink-0">Navigation Payload</Label>
<Textarea
rows={5}
value={task.request.navigation_payload}
readOnly
/>
</div>
<Separator />
<div className="flex items-center">
<Label className="w-40 shrink-0">Data Extraction Goal</Label>
<Textarea
rows={5}
value={task.request.data_extraction_goal}
readOnly
/>
</div>
</div>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader className="border-b-2">
<CardTitle className="text-lg">Steps</CardTitle>
<CardDescription>Task Steps and Step Artifacts</CardDescription>
</CardHeader>
<CardContent className="min-h-96">
<StepArtifactsLayout />
</CardContent>
</Card>
</div> </div>
); );
} }