feat: Running tasks and steps UI (#165)

This commit is contained in:
Salih Altun
2024-04-07 21:52:59 +03:00
committed by GitHub
parent 112b44e41a
commit 533ed32d9c
32 changed files with 1523 additions and 225 deletions

View File

@@ -0,0 +1,40 @@
import { artifactApiClient } from "@/api/AxiosClient";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { useQuery } from "@tanstack/react-query";
type Props = {
uri: string;
};
function JSONArtifact({ uri }: Props) {
const { data, isFetching, isError, error } = useQuery<
Record<string, unknown>
>({
queryKey: ["artifact", uri],
queryFn: async () => {
return artifactApiClient
.get(`/artifact/json`, {
params: {
path: uri.slice(7),
},
})
.then((response) => response.data);
},
});
if (isFetching) {
return <Skeleton className="w-full h-48" />;
}
return (
<Textarea
className="w-full"
rows={15}
value={isError ? JSON.stringify(error) : JSON.stringify(data, null, 2)}
readOnly
/>
);
}
export { JSONArtifact };

View File

@@ -0,0 +1,205 @@
import { client } from "@/api/AxiosClient";
import {
ArtifactApiResponse,
ArtifactType,
StepApiResponse,
} from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { Label } from "@/components/ui/label";
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { artifactApiBaseUrl } from "@/util/env";
import { ZoomableImage } from "@/components/ZoomableImage";
import { Skeleton } from "@/components/ui/skeleton";
import { JSONArtifact } from "./JSONArtifact";
import { TextArtifact } from "./TextArtifact";
type Props = {
id: string;
stepProps: StepApiResponse;
};
function StepArtifacts({ id, stepProps }: Props) {
const { taskId } = useParams();
const {
data: artifacts,
isFetching,
isError,
error,
} = useQuery<Array<ArtifactApiResponse>>({
queryKey: ["task", taskId, "steps", id, "artifacts"],
queryFn: async () => {
return client
.get(`/tasks/${taskId}/steps/${id}/artifacts`)
.then((response) => response.data);
},
});
if (isError) {
return <div>Error: {error?.message}</div>;
}
const llmScreenshotUris = artifacts
?.filter(
(artifact) => artifact.artifact_type === ArtifactType.LLMScreenshot,
)
.map((artifact) => artifact.uri);
const actionScreenshotUris = artifacts
?.filter(
(artifact) => artifact.artifact_type === ArtifactType.ActionScreenshot,
)
.map((artifact) => artifact.uri);
const visibleElementsTreeUri = artifacts?.find(
(artifact) => artifact.artifact_type === ArtifactType.VisibleElementsTree,
)?.uri;
const visibleElementsTreeTrimmedUri = artifacts?.find(
(artifact) =>
artifact.artifact_type === ArtifactType.VisibleElementsTreeTrimmed,
)?.uri;
const llmPromptUri = artifacts?.find(
(artifact) => artifact.artifact_type === ArtifactType.LLMPrompt,
)?.uri;
const llmRequestUri = artifacts?.find(
(artifact) => artifact.artifact_type === ArtifactType.LLMRequest,
)?.uri;
const llmResponseRawUri = artifacts?.find(
(artifact) => artifact.artifact_type === ArtifactType.LLMResponseRaw,
)?.uri;
const llmResponseParsedUri = artifacts?.find(
(artifact) => artifact.artifact_type === ArtifactType.LLMResponseParsed,
)?.uri;
const htmlRawUri = artifacts?.find(
(artifact) => artifact.artifact_type === ArtifactType.HTMLScrape,
)?.uri;
return (
<Tabs defaultValue="info" className="w-full">
<TabsList className="grid w-full h-16 grid-cols-5">
<TabsTrigger value="info">Info</TabsTrigger>
<TabsTrigger value="screenshot_llm">LLM Screenshots</TabsTrigger>
<TabsTrigger value="screenshot_action">Action Screenshots</TabsTrigger>
<TabsTrigger value="element_tree">Element Tree</TabsTrigger>
<TabsTrigger value="element_tree_trimmed">
Element Tree (Trimmed)
</TabsTrigger>
<TabsTrigger value="llm_prompt">LLM Prompt</TabsTrigger>
<TabsTrigger value="llm_request">LLM Request</TabsTrigger>
<TabsTrigger value="llm_response_raw">LLM Response (Raw)</TabsTrigger>
<TabsTrigger value="llm_response_parsed">
LLM Response (Parsed)
</TabsTrigger>
<TabsTrigger value="html_raw">HTML (Raw)</TabsTrigger>
</TabsList>
<TabsContent value="info">
<div className="flex flex-col gap-4 p-4">
<div className="flex items-center">
<Label className="w-24">Step ID:</Label>
{isFetching ? (
<Skeleton className="h-4 w-40" />
) : (
<span>{stepProps?.step_id}</span>
)}
</div>
<div className="flex items-center">
<Label className="w-24">Status:</Label>
{isFetching ? (
<Skeleton className="h-4 w-40" />
) : stepProps ? (
<StatusBadge status={stepProps.status} />
) : null}
</div>
<div className="flex items-center">
<Label className="w-24">Created At:</Label>
{isFetching ? (
<Skeleton className="h-4 w-40" />
) : stepProps ? (
<span>{stepProps.created_at}</span>
) : null}
</div>
</div>
</TabsContent>
<TabsContent value="screenshot_llm">
{llmScreenshotUris && llmScreenshotUris.length > 0 ? (
<div className="grid grid-cols-3 gap-4 p-4">
{llmScreenshotUris.map((uri, index) => (
<ZoomableImage
key={index}
src={`${artifactApiBaseUrl}/artifact/image?path=${uri.slice(7)}`}
className="object-cover w-full h-full"
alt="action-screenshot"
/>
))}
</div>
) : isFetching ? (
<div className="grid grid-cols-3 gap-4 p-4">
<Skeleton className="w-full h-full" />
<Skeleton className="w-full h-full" />
<Skeleton className="w-full h-full" />
</div>
) : (
<div>No screenshots found</div>
)}
</TabsContent>
<TabsContent value="screenshot_action">
{actionScreenshotUris && actionScreenshotUris.length > 0 ? (
<div className="grid grid-cols-3 gap-4 p-4">
{actionScreenshotUris.map((uri, index) => (
<ZoomableImage
key={index}
src={`${artifactApiBaseUrl}/artifact/image?path=${uri.slice(7)}`}
className="object-cover w-full h-full"
alt="action-screenshot"
/>
))}
</div>
) : isFetching ? (
<div className="grid grid-cols-3 gap-4 p-4">
<Skeleton className="w-full h-full" />
<Skeleton className="w-full h-full" />
<Skeleton className="w-full h-full" />
</div>
) : (
<div>No screenshots found</div>
)}
</TabsContent>
<TabsContent value="element_tree">
{visibleElementsTreeUri ? (
<JSONArtifact uri={visibleElementsTreeUri} />
) : null}
</TabsContent>
<TabsContent value="element_tree_trimmed">
{visibleElementsTreeTrimmedUri ? (
<JSONArtifact uri={visibleElementsTreeTrimmedUri} />
) : null}
</TabsContent>
<TabsContent value="llm_prompt">
{llmPromptUri ? <TextArtifact uri={llmPromptUri} /> : null}
</TabsContent>
<TabsContent value="llm_request">
{llmRequestUri ? <JSONArtifact uri={llmRequestUri} /> : null}
</TabsContent>
<TabsContent value="llm_response_raw">
{llmResponseRawUri ? <JSONArtifact uri={llmResponseRawUri} /> : null}
</TabsContent>
<TabsContent value="llm_response_parsed">
{llmResponseParsedUri ? (
<JSONArtifact uri={llmResponseParsedUri} />
) : null}
</TabsContent>
<TabsContent value="html_raw">
{htmlRawUri ? <TextArtifact uri={htmlRawUri} /> : null}
</TabsContent>
</Tabs>
);
}
export { StepArtifacts };

View File

@@ -0,0 +1,58 @@
import { useState } from "react";
import { StepNavigation } from "./StepNavigation";
import { StepArtifacts } from "./StepArtifacts";
import { useQuery } from "@tanstack/react-query";
import { StepApiResponse } from "@/api/types";
import { useParams } from "react-router-dom";
import { client } from "@/api/AxiosClient";
function StepArtifactsLayout() {
const [activeIndex, setActiveIndex] = useState(0);
const { taskId } = useParams();
const {
data: steps,
isFetching,
isError,
error,
} = useQuery<Array<StepApiResponse>>({
queryKey: ["task", taskId, "steps"],
queryFn: async () => {
return client
.get(`/tasks/${taskId}/steps`)
.then((response) => response.data);
},
});
if (isFetching) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error: {error?.message}</div>;
}
if (!steps) {
return <div>No steps found</div>;
}
const activeStep = steps[activeIndex];
return (
<div className="px-4 flex">
<aside className="w-64 shrink-0">
<StepNavigation
activeIndex={activeIndex}
onActiveIndexChange={setActiveIndex}
/>
</aside>
<main className="px-4 w-full">
{activeStep ? (
<StepArtifacts id={activeStep.step_id} stepProps={activeStep} />
) : null}
</main>
</div>
);
}
export { StepArtifactsLayout };

View File

@@ -0,0 +1,42 @@
import { StepApiResponse } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
type Props = {
isFetching: boolean;
stepProps?: StepApiResponse;
};
function StepInfo({ isFetching, stepProps }: Props) {
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex items-center">
<Label className="w-24">Step ID:</Label>
{isFetching ? (
<Skeleton className="h-4 w-40" />
) : (
<span>{stepProps?.step_id}</span>
)}
</div>
<div className="flex items-center">
<Label className="w-24">Status:</Label>
{isFetching ? (
<Skeleton className="h-4 w-40" />
) : stepProps ? (
<StatusBadge status={stepProps.status} />
) : null}
</div>
<div className="flex items-center">
<Label className="w-24">Created At:</Label>
{isFetching ? (
<Skeleton className="h-4 w-40" />
) : stepProps ? (
<span>{stepProps.created_at}</span>
) : null}
</div>
</div>
);
}
export { StepInfo };

View File

@@ -1,123 +0,0 @@
import { client } from "@/api/AxiosClient";
import { StepApiResponse } from "@/api/types";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useQuery } from "@tanstack/react-query";
import { useParams, useSearchParams } from "react-router-dom";
import { StepListSkeleton } from "./StepListSkeleton";
import { TaskStatusBadge } from "@/components/TaskStatusBadge";
import { basicTimeFormat } from "@/util/timeFormat";
function StepList() {
const { taskId } = useParams();
const [searchParams, setSearchParams] = useSearchParams();
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
const {
data: steps,
isFetching,
isError,
error,
} = useQuery<Array<StepApiResponse>>({
queryKey: ["task", taskId, "steps", page],
queryFn: async () => {
return client
.get(`/tasks/${taskId}/steps`, {
params: {
page,
},
})
.then((response) => response.data);
},
});
if (isFetching) {
return <StepListSkeleton />;
}
if (isError) {
return <div>Error: {error?.message}</div>;
}
if (!steps) {
return <div>No steps found</div>;
}
return (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-1/3">Order</TableHead>
<TableHead className="w-1/3">Status</TableHead>
<TableHead className="w-1/3">Created At</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{steps.length === 0 ? (
<TableRow>
<TableCell colSpan={3}>No tasks found</TableCell>
</TableRow>
) : (
steps.map((step) => {
return (
<TableRow key={step.step_id} className="cursor-pointer w-4">
<TableCell className="w-1/3">{step.order}</TableCell>
<TableCell className="w-1/3">
<TaskStatusBadge status={step.status} />
</TableCell>
<TableCell className="w-1/3">
{basicTimeFormat(step.created_at)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={() => {
const params = new URLSearchParams();
params.set("page", String(Math.max(1, page - 1)));
setSearchParams(params);
}}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">{page}</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext
href="#"
onClick={() => {
const params = new URLSearchParams();
params.set("page", String(page + 1));
setSearchParams(params);
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</>
);
}
export { StepList };

View File

@@ -1,46 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const pageSizeArray = new Array(15).fill(null); // doesn't matter the value
function StepListSkeleton() {
return (
<div className="flex flex-col gap-2">
<Table>
<TableHeader>
<TableRow>
<TableHead>Order</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pageSizeArray.map((_, index) => {
return (
<TableRow key={index}>
<TableCell>
<Skeleton className="h-4 w-24" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-24" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-24" />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}
export { StepListSkeleton };

View File

@@ -0,0 +1,78 @@
import { client } from "@/api/AxiosClient";
import { StepApiResponse } from "@/api/types";
import { cn } from "@/util/utils";
import { useQuery } from "@tanstack/react-query";
import { useParams, useSearchParams } from "react-router-dom";
import { PAGE_SIZE } from "../constants";
type Props = {
activeIndex: number;
onActiveIndexChange: (index: number) => void;
};
function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
const { taskId } = useParams();
const [searchParams] = useSearchParams();
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
const {
data: steps,
isFetching,
isError,
error,
} = useQuery<Array<StepApiResponse>>({
queryKey: ["task", taskId, "steps", page],
queryFn: async () => {
return client
.get(`/tasks/${taskId}/steps`, {
params: {
page,
page_size: PAGE_SIZE,
},
})
.then((response) => response.data);
},
});
if (isFetching) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error: {error?.message}</div>;
}
if (!steps) {
return <div>No steps found</div>;
}
return (
<nav className="flex flex-col gap-4">
{steps.map((step, index) => {
const isActive = activeIndex === index;
return (
<div
className={cn(
"flex justify-center items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl cursor-pointer",
{
"bg-primary-foreground": isActive,
},
)}
key={step.step_id}
onClick={() => {
onActiveIndexChange(index);
}}
>
<span>
{step.retry_index > 0
? `Step ${step.order + 1} ( Retry ${step.retry_index} )`
: `Step ${step.order + 1}`}
</span>
</div>
);
})}
</nav>
);
}
export { StepNavigation };

View File

@@ -2,20 +2,22 @@ import { client } from "@/api/AxiosClient";
import { Status, TaskApiResponse } from "@/api/types";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { StepList } from "./StepList";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { TaskStatusBadge } from "@/components/TaskStatusBadge";
import { StatusBadge } from "@/components/StatusBadge";
import { artifactApiBaseUrl } from "@/util/env";
import { Button } from "@/components/ui/button";
import { ReloadIcon } from "@radix-ui/react-icons";
import { basicTimeFormat } from "@/util/timeFormat";
import { StepArtifactsLayout } from "./StepArtifactsLayout";
import Zoom from "react-medium-image-zoom";
import { AspectRatio } from "@/components/ui/aspect-ratio";
function TaskDetails() {
const { taskId } = useParams();
@@ -27,11 +29,10 @@ function TaskDetails() {
error: taskError,
refetch,
} = useQuery<TaskApiResponse>({
queryKey: ["task", taskId],
queryKey: ["task", taskId, "details"],
queryFn: async () => {
return client.get(`/tasks/${taskId}`).then((response) => response.data);
},
placeholderData: keepPreviousData,
});
if (isTaskError) {
@@ -63,14 +64,14 @@ function TaskDetails() {
<div className="flex">
<Label className="w-32">Recording</Label>
<video
src={`${artifactApiBaseUrl}/artifact?path=${task.recording_url.slice(7)}`}
src={`${artifactApiBaseUrl}/artifact/recording?path=${task.recording_url.slice(7)}`}
controls
/>
</div>
) : null}
<div className="flex items-center">
<Label className="w-32">Status</Label>
<TaskStatusBadge status={task.status} />
<StatusBadge status={task.status} />
</div>
{task.status === Status.Completed ? (
<div className="flex items-center">
@@ -93,8 +94,8 @@ function TaskDetails() {
</div>
) : null}
</div>
<Accordion type="single" collapsible>
<AccordionItem value="task-details">
<Accordion type="multiple">
<AccordionItem value="task-parameters">
<AccordionTrigger>
<h1>Task Parameters</h1>
</AccordionTrigger>
@@ -102,7 +103,9 @@ function TaskDetails() {
<div>
<p className="py-2">Task ID: {taskId}</p>
<p className="py-2">URL: {task.request.url}</p>
<p className="py-2">{basicTimeFormat(task.created_at)}</p>
<p className="py-2">
Created: {basicTimeFormat(task.created_at)}
</p>
<div className="py-2">
<Label>Navigation Goal</Label>
<Textarea
@@ -130,11 +133,36 @@ function TaskDetails() {
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="task-artifacts">
<AccordionTrigger>
<h1>Screenshot</h1>
</AccordionTrigger>
<AccordionContent>
<div className="max-w-sm mx-auto">
{task.screenshot_url ? (
<Zoom zoomMargin={16}>
<AspectRatio ratio={16 / 9}>
<img
src={`${artifactApiBaseUrl}/artifact/image?path=${task.screenshot_url.slice(7)}`}
alt="screenshot"
/>
</AspectRatio>
</Zoom>
) : (
<p>No screenshot</p>
)}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="task-steps">
<AccordionTrigger>
<h1>Task Steps</h1>
</AccordionTrigger>
<AccordionContent>
<StepArtifactsLayout />
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="py-2">
<h1>Task Steps</h1>
<StepList />
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { artifactApiClient } from "@/api/AxiosClient";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { useQuery } from "@tanstack/react-query";
type Props = {
uri: string;
};
function TextArtifact({ uri }: Props) {
const { data, isFetching, isError, error } = useQuery<string>({
queryKey: ["artifact", uri],
queryFn: async () => {
return artifactApiClient
.get(`/artifact/text`, {
params: {
path: uri.slice(7),
},
})
.then((response) => response.data);
},
});
if (isFetching) {
return <Skeleton className="w-full h-48" />;
}
return (
<Textarea
className="w-full"
rows={15}
value={isError ? JSON.stringify(error) : data}
readOnly
/>
);
}
export { TextArtifact };

View File

@@ -22,7 +22,7 @@ import { TaskListSkeleton } from "./TaskListSkeleton";
import { RunningTasks } from "../running/RunningTasks";
import { cn } from "@/util/utils";
import { PAGE_SIZE } from "../constants";
import { TaskStatusBadge } from "@/components/TaskStatusBadge";
import { StatusBadge } from "@/components/StatusBadge";
import { basicTimeFormat } from "@/util/timeFormat";
function TaskList() {
@@ -102,7 +102,7 @@ function TaskList() {
>
<TableCell className="w-1/3">{task.request.url}</TableCell>
<TableCell className="w-1/3">
<TaskStatusBadge status={task.status} />
<StatusBadge status={task.status} />
</TableCell>
<TableCell className="w-1/3">
{basicTimeFormat(task.created_at)}

View File

@@ -0,0 +1,84 @@
import { client } from "@/api/AxiosClient";
import {
ArtifactApiResponse,
ArtifactType,
StepApiResponse,
} from "@/api/types";
import { Skeleton } from "@/components/ui/skeleton";
import { artifactApiBaseUrl } from "@/util/env";
import { useQuery } from "@tanstack/react-query";
type Props = {
id: string;
};
function LatestScreenshot({ id }: Props) {
const {
data: screenshotUri,
isFetching,
isError,
} = useQuery<string | undefined>({
queryKey: ["task", id, "latestScreenshot"],
queryFn: async () => {
const steps: StepApiResponse[] = await client
.get(`/tasks/${id}/steps`)
.then((response) => response.data);
if (steps.length === 0) {
return;
}
const latestStep = steps[steps.length - 1];
if (!latestStep) {
return;
}
const artifacts: ArtifactApiResponse[] = await client
.get(`/tasks/${id}/steps/${latestStep.step_id}/artifacts`)
.then((response) => response.data);
const actionScreenshotUris = artifacts
?.filter(
(artifact) =>
artifact.artifact_type === ArtifactType.ActionScreenshot,
)
.map((artifact) => artifact.uri);
if (actionScreenshotUris.length > 0) {
return actionScreenshotUris[0];
}
const llmScreenshotUris = artifacts
?.filter(
(artifact) => artifact.artifact_type === ArtifactType.LLMScreenshot,
)
.map((artifact) => artifact.uri);
if (llmScreenshotUris.length > 0) {
return llmScreenshotUris[0];
}
return Promise.reject("No screenshots found");
},
refetchInterval: 2000,
});
if (isFetching) {
return <Skeleton className="w-full h-full" />;
}
if (isError || !screenshotUri || typeof screenshotUri !== "string") {
return null;
}
return (
<img
src={`${artifactApiBaseUrl}/artifact/image?path=${screenshotUri.slice(7)}`}
className="w-full h-full object-contain"
alt="Latest screenshot"
/>
);
}
export { LatestScreenshot };

View File

@@ -11,20 +11,15 @@ import {
CardTitle,
} from "@/components/ui/card";
import { PAGE_SIZE } from "../constants";
import { RunningTaskSkeleton } from "./RunningTaskSkeleton";
import { basicTimeFormat } from "@/util/timeFormat";
import { LatestScreenshot } from "./LatestScreenshot";
function RunningTasks() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
const {
data: tasks,
isPending,
isError,
error,
} = useQuery<Array<TaskApiResponse>>({
const { data: tasks } = useQuery<Array<TaskApiResponse>>({
queryKey: ["tasks", page],
queryFn: async () => {
return client
@@ -40,25 +35,13 @@ function RunningTasks() {
placeholderData: keepPreviousData,
});
if (isPending) {
return <RunningTaskSkeleton />;
}
const runningTasks = tasks?.filter((task) => task.status === Status.Running);
if (isError) {
return <div>Error: {error?.message}</div>;
}
if (!tasks) {
return null;
}
const runningTasks = tasks.filter((task) => task.status === Status.Running);
if (runningTasks.length === 0) {
if (runningTasks?.length === 0) {
return <div>No running tasks</div>;
}
return runningTasks.map((task) => {
return runningTasks?.map((task) => {
return (
<Card
key={task.task_id}
@@ -68,10 +51,17 @@ function RunningTasks() {
}}
>
<CardHeader>
<CardTitle>{task.request.url}</CardTitle>
<CardDescription></CardDescription>
<CardTitle>{task.task_id}</CardTitle>
<CardDescription className="whitespace-nowrap overflow-hidden text-ellipsis">
{task.request.url}
</CardDescription>
</CardHeader>
<CardContent>Goal: {task.request.navigation_goal}</CardContent>
<CardContent>
Latest screenshot:
<div className="w-40 h-40 border-2">
<LatestScreenshot id={task.task_id} />
</div>
</CardContent>
<CardFooter>Created: {basicTimeFormat(task.created_at)}</CardFooter>
</Card>
);