Tasks page implementation (#120)
This commit is contained in:
13
skyvern-frontend/src/routes/tasks/TasksPageLayout.tsx
Normal file
13
skyvern-frontend/src/routes/tasks/TasksPageLayout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
function TasksPageLayout() {
|
||||
return (
|
||||
<div className="px-6 flex grow flex-col gap-4">
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TasksPageLayout };
|
||||
1
skyvern-frontend/src/routes/tasks/constants.ts
Normal file
1
skyvern-frontend/src/routes/tasks/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PAGE_SIZE = 15;
|
||||
49
skyvern-frontend/src/routes/tasks/create/CreateNewTask.tsx
Normal file
49
skyvern-frontend/src/routes/tasks/create/CreateNewTask.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useId, useState } from "react";
|
||||
import { CreateNewTaskForm } from "./CreateNewTaskForm";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { SampleCase } from "../types";
|
||||
import { getSampleForInitialFormValues } from "../data/sampleTaskData";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
function CreateNewTask() {
|
||||
const [selectedCase, setSelectedCase] = useState<SampleCase>("geico");
|
||||
const caseInputId = useId();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 px-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
<Label htmlFor={caseInputId} className="whitespace-nowrap">
|
||||
Select a sample:
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedCase}
|
||||
onValueChange={(value) => {
|
||||
setSelectedCase(value as SampleCase);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a case" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="geico">Geico</SelectItem>
|
||||
<SelectItem value="finditparts">Finditparts</SelectItem>
|
||||
<SelectItem value="california_edd">California_EDD</SelectItem>
|
||||
<SelectItem value="bci_seguros">bci_seguros</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<CreateNewTaskForm
|
||||
key={selectedCase}
|
||||
initialValues={getSampleForInitialFormValues(selectedCase)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreateNewTask };
|
||||
297
skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx
Normal file
297
skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
dataExtractionGoalDescription,
|
||||
extractedInformationSchemaDescription,
|
||||
navigationGoalDescription,
|
||||
navigationPayloadDescription,
|
||||
urlDescription,
|
||||
webhookCallbackUrlDescription,
|
||||
} from "../data/descriptionHelperContent";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { client } from "@/api/AxiosClient";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ToastAction } from "@radix-ui/react-toast";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const createNewTaskFormSchema = z.object({
|
||||
url: z.string().url({
|
||||
message: "Invalid URL",
|
||||
}),
|
||||
webhookCallbackUrl: z.string().optional(), // url maybe, but shouldn't be validated as one
|
||||
navigationGoal: z.string().optional(),
|
||||
dataExtractionGoal: z.string().optional(),
|
||||
navigationPayload: z.string().optional(),
|
||||
extractedInformationSchema: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CreateNewTaskFormValues = z.infer<typeof createNewTaskFormSchema>;
|
||||
|
||||
type Props = {
|
||||
initialValues: CreateNewTaskFormValues;
|
||||
};
|
||||
|
||||
function createTaskRequestObject(formValues: CreateNewTaskFormValues) {
|
||||
return {
|
||||
url: formValues.url,
|
||||
webhook_callback_url: formValues.webhookCallbackUrl ?? "",
|
||||
navigation_goal: formValues.navigationGoal ?? "",
|
||||
data_extraction_goal: formValues.dataExtractionGoal ?? "",
|
||||
proxy_location: "NONE",
|
||||
navigation_payload: formValues.navigationPayload ?? "",
|
||||
extracted_information_schema: formValues.extractedInformationSchema ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function CreateNewTaskForm({ initialValues }: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<CreateNewTaskFormValues>({
|
||||
resolver: zodResolver(createNewTaskFormSchema),
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (formValues: CreateNewTaskFormValues) => {
|
||||
const taskRequest = createTaskRequestObject(formValues);
|
||||
return client.post<
|
||||
ReturnType<typeof createTaskRequestObject>,
|
||||
{ data: { task_id: string } }
|
||||
>("/tasks", taskRequest);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast({
|
||||
title: "Task Created",
|
||||
description: `${response.data.task_id} created successfully.`,
|
||||
action: (
|
||||
<ToastAction altText="View">
|
||||
<Button asChild>
|
||||
<Link to={`/tasks/${response.data.task_id}`}>View</Link>
|
||||
</Button>
|
||||
</ToastAction>
|
||||
),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["tasks"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: CreateNewTaskFormValues) {
|
||||
mutation.mutate(values);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<div className="flex gap-2">
|
||||
URL*
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircledIcon />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>{urlDescription}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookCallbackUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<div className="flex gap-2">
|
||||
Webhook Callback URL
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircledIcon />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>{webhookCallbackUrlDescription}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="navigationGoal"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<div className="flex gap-2">
|
||||
Navigation Goal
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircledIcon />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>{navigationGoalDescription}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows={5} placeholder="Navigation Goal" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dataExtractionGoal"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<div className="flex gap-2">
|
||||
Data Extraction Goal
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircledIcon />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>{dataExtractionGoalDescription}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder="Data Extraction Goal"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="navigationPayload"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<div className="flex gap-2">
|
||||
Navigation Payload
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircledIcon />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>{navigationPayloadDescription}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder="Navigation Payload"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="extractedInformationSchema"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<div className="flex gap-2">
|
||||
Extracted Information Schema
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircledIcon />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>{extractedInformationSchemaDescription}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Extracted Information Schema"
|
||||
rows={5}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="outline">Copy cURL</Button>
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreateNewTaskForm };
|
||||
@@ -0,0 +1,16 @@
|
||||
export const urlDescription = "The starting URL for the task.";
|
||||
|
||||
export const webhookCallbackUrlDescription =
|
||||
"The URL to call with the results when the task is completed.";
|
||||
|
||||
export const navigationGoalDescription =
|
||||
"The user's goal for the task. Nullable if the task is only for data extraction.";
|
||||
|
||||
export const dataExtractionGoalDescription =
|
||||
"The user's goal for data extraction. Nullable if the task is only for navigation.";
|
||||
|
||||
export const navigationPayloadDescription =
|
||||
"The user's details needed to achieve the task. This is an unstructured field, and information can be passed in in any format you desire. Skyvern will map this information to the questions on the screen in real-time.";
|
||||
|
||||
export const extractedInformationSchemaDescription =
|
||||
"(Optional) The requested schema of the extracted information for data extraction goal. This is a JSON object with keys as the field names and values as the data types. The data types can be any of the following: string, number, boolean, date, datetime, time, float, integer, object, array, null. If the schema is not provided, Skyvern will infer the schema from the extracted data.";
|
||||
282
skyvern-frontend/src/routes/tasks/data/sampleTaskData.ts
Normal file
282
skyvern-frontend/src/routes/tasks/data/sampleTaskData.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { SampleCase } from "../types";
|
||||
|
||||
export const bci_seguros = {
|
||||
url: "https://www.bciseguros.cl/nuestros_seguros/personas/seguro-automotriz/",
|
||||
navigationGoal:
|
||||
"Generate an auto insurance quote. A quote has been generated when there's a table of coverages shown on the website.",
|
||||
dataExtractionGoal:
|
||||
"Extract ALL quote information in JSON format, with one entry per plan visible on the page. The output should include: the selected UF coverage value (3), auto plan name, the online price",
|
||||
navigationPayload: {
|
||||
Rut: "7.250.199-3",
|
||||
Sexo: "Masculino",
|
||||
"Fecha de Nacimiento": "03-02-2000",
|
||||
Telefono: "96908116",
|
||||
Comuna: "Lo Barnachea",
|
||||
"e-mail": "notarealemail@gmail.com",
|
||||
estado: "Usado",
|
||||
patente: "HZZV68",
|
||||
marca: "Subaru",
|
||||
modelo: "XV",
|
||||
ano: "2016",
|
||||
"tipo de combustible": "Bencina",
|
||||
"km approx a recorrer": "28,000",
|
||||
},
|
||||
};
|
||||
|
||||
export const california_edd = {
|
||||
url: "https://eddservices.edd.ca.gov/acctservices/AccountManagement/AccountServlet?Command=NEW_SIGN_UP",
|
||||
navigationGoal:
|
||||
"Navigate through the employer services online enrollment form. Terminate when the form is completed",
|
||||
navigationPayload: {
|
||||
username: "isthisreal1",
|
||||
password: "Password123!",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
pin: "1234",
|
||||
email: "isthisreal1@gmail.com",
|
||||
phone_number: "412-444-1234",
|
||||
},
|
||||
};
|
||||
|
||||
export const finditparts = {
|
||||
url: "https://www.finditparts.com",
|
||||
navigationGoal:
|
||||
"Search for the specified product id, add it to cart and then navigate to the cart page",
|
||||
dataExtractionGoal:
|
||||
"Extract all product quantity information from the cart page",
|
||||
navigationPayload: {
|
||||
product_id: "W01-377-8537",
|
||||
},
|
||||
};
|
||||
|
||||
export const geico = {
|
||||
url: "https://www.geico.com",
|
||||
navigationGoal:
|
||||
"Navigate through the website until you generate an auto insurance quote. Do not generate a home insurance quote. If this page contains an auto insurance quote, consider the goal achieved",
|
||||
dataExtractionGoal:
|
||||
"Extract all quote information in JSON format including the premium amount, the timeframe for the quote.",
|
||||
navigationPayload: {
|
||||
licensed_at_age: 19,
|
||||
education_level: "HIGH_SCHOOL",
|
||||
phone_number: "8042221111",
|
||||
full_name: "Chris P. Bacon",
|
||||
past_claim: [],
|
||||
has_claims: false,
|
||||
spouse_occupation: "Florist",
|
||||
auto_current_carrier: "None",
|
||||
home_commercial_uses: null,
|
||||
spouse_full_name: "Amy Stake",
|
||||
auto_commercial_uses: null,
|
||||
requires_sr22: false,
|
||||
previous_address_move_date: null,
|
||||
line_of_work: null,
|
||||
spouse_age: "1987-12-12",
|
||||
auto_insurance_deadline: null,
|
||||
email: "chris.p.bacon@abc.com",
|
||||
net_worth_numeric: 1000000,
|
||||
spouse_gender: "F",
|
||||
marital_status: "married",
|
||||
spouse_licensed_at_age: 20,
|
||||
license_number: "AAAAAAA090AA",
|
||||
spouse_license_number: "AAAAAAA080AA",
|
||||
how_much_can_you_lose: 25000,
|
||||
vehicles: [
|
||||
{
|
||||
annual_mileage: 10000,
|
||||
commute_mileage: 4000,
|
||||
existing_coverages: null,
|
||||
ideal_coverages: {
|
||||
bodily_injury_per_incident_limit: 50000,
|
||||
bodily_injury_per_person_limit: 25000,
|
||||
collision_deductible: 1000,
|
||||
comprehensive_deductible: 1000,
|
||||
personal_injury_protection: null,
|
||||
property_damage_per_incident_limit: null,
|
||||
property_damage_per_person_limit: 25000,
|
||||
rental_reimbursement_per_incident_limit: null,
|
||||
rental_reimbursement_per_person_limit: null,
|
||||
roadside_assistance_limit: null,
|
||||
underinsured_motorist_bodily_injury_per_incident_limit: 50000,
|
||||
underinsured_motorist_bodily_injury_per_person_limit: 25000,
|
||||
underinsured_motorist_property_limit: null,
|
||||
},
|
||||
ownership: "Owned",
|
||||
parked: "Garage",
|
||||
purpose: "commute",
|
||||
vehicle: {
|
||||
style: "AWD 3.0 quattro TDI 4dr Sedan",
|
||||
model: "A8 L",
|
||||
price_estimate: 29084,
|
||||
year: 2015,
|
||||
make: "Audi",
|
||||
},
|
||||
vehicle_id: null,
|
||||
vin: null,
|
||||
},
|
||||
],
|
||||
additional_drivers: [],
|
||||
home: [
|
||||
{
|
||||
home_ownership: "owned",
|
||||
},
|
||||
],
|
||||
spouse_line_of_work: "Agriculture, Forestry and Fishing",
|
||||
occupation: "Customer Service Representative",
|
||||
id: null,
|
||||
gender: "M",
|
||||
credit_check_authorized: false,
|
||||
age: "1987-11-11",
|
||||
license_state: "Washington",
|
||||
cash_on_hand: "$10000–14999",
|
||||
address: {
|
||||
city: "HOUSTON",
|
||||
country: "US",
|
||||
state: "TX",
|
||||
street: "9625 GARFIELD AVE.",
|
||||
zip: "77082",
|
||||
},
|
||||
spouse_education_level: "MASTERS",
|
||||
spouse_email: "amy.stake@abc.com",
|
||||
spouse_added_to_auto_policy: true,
|
||||
},
|
||||
extractedInformationSchema: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
quotes: {
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
coverages: {
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
amount: {
|
||||
description:
|
||||
"The coverage amount in USD, which can be a single value or a range (e.g., '$300,000' or '$300,000/$300,000').",
|
||||
type: "string",
|
||||
},
|
||||
included: {
|
||||
description:
|
||||
"Indicates whether the coverage is included in the policy (true or false).",
|
||||
type: "boolean",
|
||||
},
|
||||
type: {
|
||||
description:
|
||||
"The limit of the coverage (e.g., 'bodily_injury_limit', 'property_damage_limit', 'underinsured_motorist_bodily_injury_limit').\nTranslate the english name of the coverage to snake case values in the following list:\n * bodily_injury_limit\n * property_damage_limit\n * underinsured_motorist_bodily_injury_limit\n * personal_injury_protection\n * accidental_death\n * work_loss_exclusion\n",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
premium_amount: {
|
||||
description:
|
||||
"The total premium amount for the whole quote timeframe in USD, formatted as a string (e.g., '$321.57').",
|
||||
type: "string",
|
||||
},
|
||||
quote_number: {
|
||||
description:
|
||||
"The quote number generated by the carrier that identifies this quote",
|
||||
type: "string",
|
||||
},
|
||||
timeframe: {
|
||||
description:
|
||||
"The duration of the coverage, typically expressed in months or years.",
|
||||
type: "string",
|
||||
},
|
||||
vehicle_coverages: {
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
collision_deductible: {
|
||||
description:
|
||||
"The collision deductible amount in USD, which is a single value (e.g., '$500') or null if it is not included",
|
||||
type: "string",
|
||||
},
|
||||
comprehensive_deductible: {
|
||||
description:
|
||||
"The collision deductible amount in USD, which is a single value (e.g., '$500') or null if it is not included",
|
||||
type: "string",
|
||||
},
|
||||
for_vehicle: {
|
||||
additionalProperties: false,
|
||||
description:
|
||||
"The vehicle that the collision and comprehensive coverage is for",
|
||||
properties: {
|
||||
make: {
|
||||
description: "The make of the vehicle",
|
||||
type: "string",
|
||||
},
|
||||
model: {
|
||||
description: "The model of the vehicle",
|
||||
type: "string",
|
||||
},
|
||||
year: {
|
||||
description: "The year of the vehicle",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
underinsured_property_damage: {
|
||||
description:
|
||||
"The underinsured property damage limit for this vehicle, which is a limit and a deductible (e.g., '$25,000/$250 deductible') or null if it is not included",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
};
|
||||
|
||||
export function getSampleForInitialFormValues(sample: SampleCase) {
|
||||
switch (sample) {
|
||||
case "geico":
|
||||
return {
|
||||
...geico,
|
||||
navigationPayload: JSON.stringify(geico.navigationPayload, null, 2),
|
||||
extractedInformationSchema: JSON.stringify(
|
||||
geico.extractedInformationSchema,
|
||||
null,
|
||||
2,
|
||||
),
|
||||
};
|
||||
case "finditparts":
|
||||
return {
|
||||
...finditparts,
|
||||
navigationPayload: JSON.stringify(
|
||||
finditparts.navigationPayload,
|
||||
null,
|
||||
2,
|
||||
),
|
||||
};
|
||||
case "california_edd":
|
||||
return {
|
||||
...california_edd,
|
||||
navigationPayload: JSON.stringify(
|
||||
california_edd.navigationPayload,
|
||||
null,
|
||||
2,
|
||||
),
|
||||
};
|
||||
case "bci_seguros":
|
||||
return {
|
||||
...bci_seguros,
|
||||
navigationPayload: JSON.stringify(
|
||||
bci_seguros.navigationPayload,
|
||||
null,
|
||||
2,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
123
skyvern-frontend/src/routes/tasks/detail/StepList.tsx
Normal file
123
skyvern-frontend/src/routes/tasks/detail/StepList.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
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 };
|
||||
@@ -0,0 +1,46 @@
|
||||
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 };
|
||||
142
skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx
Normal file
142
skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
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 { useParams } from "react-router-dom";
|
||||
import { StepList } from "./StepList";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { TaskStatusBadge } from "@/components/TaskStatusBadge";
|
||||
import { artifactApiBaseUrl } from "@/util/env";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { basicTimeFormat } from "@/util/timeFormat";
|
||||
|
||||
function TaskDetails() {
|
||||
const { taskId } = useParams();
|
||||
|
||||
const {
|
||||
data: task,
|
||||
isFetching: isTaskFetching,
|
||||
isError: isTaskError,
|
||||
error: taskError,
|
||||
refetch,
|
||||
} = useQuery<TaskApiResponse>({
|
||||
queryKey: ["task", taskId],
|
||||
queryFn: async () => {
|
||||
return client.get(`/tasks/${taskId}`).then((response) => response.data);
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
if (isTaskError) {
|
||||
return <div>Error: {taskError?.message}</div>;
|
||||
}
|
||||
|
||||
if (isTaskFetching) {
|
||||
return <div>Loading...</div>; // TODO: skeleton
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return <div>Task not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4 relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="cursor-pointer absolute top-0 right-0"
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<ReloadIcon />
|
||||
</Button>
|
||||
{task.recording_url ? (
|
||||
<div className="flex">
|
||||
<Label className="w-32">Recording</Label>
|
||||
<video
|
||||
src={`${artifactApiBaseUrl}/artifact?path=${task.recording_url.slice(7)}`}
|
||||
controls
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center">
|
||||
<Label className="w-32">Status</Label>
|
||||
<TaskStatusBadge status={task.status} />
|
||||
</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}
|
||||
</div>
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="task-details">
|
||||
<AccordionTrigger>
|
||||
<h1>Task Parameters</h1>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<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>
|
||||
<div className="py-2">
|
||||
<Label>Navigation Goal</Label>
|
||||
<Textarea
|
||||
rows={5}
|
||||
value={task.request.navigation_goal}
|
||||
readOnly
|
||||
/>
|
||||
</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>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="py-2">
|
||||
<h1>Task Steps</h1>
|
||||
<StepList />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TaskDetails };
|
||||
151
skyvern-frontend/src/routes/tasks/list/TaskList.tsx
Normal file
151
skyvern-frontend/src/routes/tasks/list/TaskList.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { client } from "@/api/AxiosClient";
|
||||
import { TaskApiResponse } from "@/api/types";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
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 { basicTimeFormat } from "@/util/timeFormat";
|
||||
|
||||
function TaskList() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
|
||||
|
||||
const {
|
||||
data: tasks,
|
||||
isPending,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<Array<TaskApiResponse>>({
|
||||
queryKey: ["tasks", page],
|
||||
queryFn: async () => {
|
||||
return client
|
||||
.get("/tasks", {
|
||||
params: {
|
||||
page,
|
||||
page_size: PAGE_SIZE,
|
||||
},
|
||||
})
|
||||
.then((response) => response.data);
|
||||
},
|
||||
refetchInterval: 3000,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return <TaskListSkeleton />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>Error: {error?.message}</div>;
|
||||
}
|
||||
|
||||
if (!tasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedTasks = tasks.filter(
|
||||
(task) =>
|
||||
task.status === "completed" ||
|
||||
task.status === "failed" ||
|
||||
task.status === "terminated",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="text-2xl py-2 border-b-2">Running Tasks</h1>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<RunningTasks />
|
||||
</div>
|
||||
<h1 className="text-2xl py-2 border-b-2">Task History</h1>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">URL</TableHead>
|
||||
<TableHead className="w-1/3">Status</TableHead>
|
||||
<TableHead className="w-1/3">Created At</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3}>No tasks found</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
resolvedTasks.map((task) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={task.task_id}
|
||||
className="cursor-pointer w-4"
|
||||
onClick={() => {
|
||||
navigate(task.task_id);
|
||||
}}
|
||||
>
|
||||
<TableCell className="w-1/3">{task.request.url}</TableCell>
|
||||
<TableCell className="w-1/3">
|
||||
<TaskStatusBadge status={task.status} />
|
||||
</TableCell>
|
||||
<TableCell className="w-1/3">
|
||||
{basicTimeFormat(task.created_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
className={cn({ "cursor-not-allowed": page === 1 })}
|
||||
onClick={() => {
|
||||
if (page === 1) {
|
||||
return;
|
||||
}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TaskList };
|
||||
46
skyvern-frontend/src/routes/tasks/list/TaskListSkeleton.tsx
Normal file
46
skyvern-frontend/src/routes/tasks/list/TaskListSkeleton.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
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 TaskListSkeleton() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">URL</TableHead>
|
||||
<TableHead className="w-1/3">Status</TableHead>
|
||||
<TableHead className="w-1/3">Created At</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pageSizeArray.map((_, index) => {
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="w-1/3">
|
||||
<Skeleton className="w-full h-full" />
|
||||
</TableCell>
|
||||
<TableCell className="w-1/3">
|
||||
<Skeleton className="w-full h-full" />
|
||||
</TableCell>
|
||||
<TableCell className="w-1/3">
|
||||
<Skeleton className="w-full h-full" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TaskListSkeleton };
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
function RunningTaskSkeleton() {
|
||||
// 4 cards with skeletons for each part
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { RunningTaskSkeleton };
|
||||
81
skyvern-frontend/src/routes/tasks/running/RunningTasks.tsx
Normal file
81
skyvern-frontend/src/routes/tasks/running/RunningTasks.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { client } from "@/api/AxiosClient";
|
||||
import { Status, TaskApiResponse } from "@/api/types";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { PAGE_SIZE } from "../constants";
|
||||
import { RunningTaskSkeleton } from "./RunningTaskSkeleton";
|
||||
import { basicTimeFormat } from "@/util/timeFormat";
|
||||
|
||||
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>>({
|
||||
queryKey: ["tasks", page],
|
||||
queryFn: async () => {
|
||||
return client
|
||||
.get("/tasks", {
|
||||
params: {
|
||||
page,
|
||||
page_size: PAGE_SIZE,
|
||||
},
|
||||
})
|
||||
.then((response) => response.data);
|
||||
},
|
||||
refetchInterval: 3000,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return <RunningTaskSkeleton />;
|
||||
}
|
||||
|
||||
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) {
|
||||
return <div>No running tasks</div>;
|
||||
}
|
||||
|
||||
return runningTasks.map((task) => {
|
||||
return (
|
||||
<Card
|
||||
key={task.task_id}
|
||||
className="hover:bg-primary-foreground cursor-pointer"
|
||||
onClick={() => {
|
||||
navigate(`/tasks/${task.task_id}`);
|
||||
}}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{task.request.url}</CardTitle>
|
||||
<CardDescription></CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Goal: {task.request.navigation_goal}</CardContent>
|
||||
<CardFooter>Created: {basicTimeFormat(task.created_at)}</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export { RunningTasks };
|
||||
5
skyvern-frontend/src/routes/tasks/types.ts
Normal file
5
skyvern-frontend/src/routes/tasks/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type SampleCase =
|
||||
| "geico"
|
||||
| "finditparts"
|
||||
| "california_edd"
|
||||
| "bci_seguros";
|
||||
Reference in New Issue
Block a user