Tasks page implementation (#120)
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import { Button } from "./components/ui/Button";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||
import { router } from "./router";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { queryClient } from "./api/QueryClient";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="skyvern-theme">
|
||||
<div className="h-screen w-screen flex items-center justify-center">
|
||||
<Button>Hello Skyvern!</Button>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="skyvern-theme">
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
12
skyvern-frontend/src/api/AxiosClient.ts
Normal file
12
skyvern-frontend/src/api/AxiosClient.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { apiBaseUrl, credential } from "@/util/env";
|
||||
import axios from "axios";
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: apiBaseUrl,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": credential,
|
||||
},
|
||||
});
|
||||
|
||||
export { client };
|
||||
11
skyvern-frontend/src/api/QueryClient.ts
Normal file
11
skyvern-frontend/src/api/QueryClient.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { queryClient };
|
||||
69
skyvern-frontend/src/api/types.ts
Normal file
69
skyvern-frontend/src/api/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export const ArtifactType = {
|
||||
Recording: "recording",
|
||||
ActionScreenshot: "screenshot_action",
|
||||
} as const;
|
||||
|
||||
export const Status = {
|
||||
Created: "created",
|
||||
Running: "running",
|
||||
Failed: "failed",
|
||||
Terminated: "terminated",
|
||||
Completed: "completed",
|
||||
} as const;
|
||||
|
||||
export type Status = (typeof Status)[keyof typeof Status];
|
||||
|
||||
export type ArtifactType = (typeof ArtifactType)[keyof typeof ArtifactType];
|
||||
|
||||
export type ArtifactApiResponse = {
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
artifact_id: string;
|
||||
task_id: string;
|
||||
step_id: string;
|
||||
artifact_type: ArtifactType;
|
||||
uri: string;
|
||||
organization_id: string;
|
||||
};
|
||||
|
||||
export type StepApiResponse = {
|
||||
step_id: string;
|
||||
task_id: string;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
input_token_count: number;
|
||||
is_last: boolean;
|
||||
order: number;
|
||||
organization_id: string;
|
||||
output: {
|
||||
action_results: unknown[];
|
||||
actions_and_results: unknown[];
|
||||
errors: unknown[];
|
||||
};
|
||||
retry_index: number;
|
||||
status: Status;
|
||||
step_cost: number;
|
||||
};
|
||||
|
||||
export type TaskApiResponse = {
|
||||
request: {
|
||||
title: string | null;
|
||||
url: string;
|
||||
webhook_callback_url: string;
|
||||
navigation_goal: string;
|
||||
data_extraction_goal: string;
|
||||
navigation_payload: string; // stringified JSON
|
||||
error_code_mapping: null;
|
||||
proxy_location: string;
|
||||
extracted_information_schema: string;
|
||||
};
|
||||
task_id: string;
|
||||
status: Status;
|
||||
created_at: string; // ISO 8601
|
||||
modified_at: string; // ISO 8601
|
||||
extracted_information: unknown;
|
||||
screenshot_url: string | null;
|
||||
recording_url: string | null;
|
||||
failure_reason: string | null;
|
||||
errors: unknown[];
|
||||
};
|
||||
21
skyvern-frontend/src/components/TaskStatusBadge.tsx
Normal file
21
skyvern-frontend/src/components/TaskStatusBadge.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Status } from "@/api/types";
|
||||
import { Badge } from "./ui/badge";
|
||||
|
||||
type Props = {
|
||||
status: Status;
|
||||
};
|
||||
|
||||
function TaskStatusBadge({ status }: Props) {
|
||||
let variant: "default" | "success" | "destructive" | "warning" = "default";
|
||||
if (status === "completed") {
|
||||
variant = "success";
|
||||
} else if (status === "failed" || status === "terminated") {
|
||||
variant = "destructive";
|
||||
} else if (status === "running") {
|
||||
variant = "warning";
|
||||
}
|
||||
|
||||
return <Badge variant={variant}>{status}</Badge>;
|
||||
}
|
||||
|
||||
export { TaskStatusBadge };
|
||||
55
skyvern-frontend/src/components/ui/accordion.tsx
Normal file
55
skyvern-frontend/src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
27
skyvern-frontend/src/components/ui/badge-variants.ts
Normal file
27
skyvern-frontend/src/components/ui/badge-variants.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
success:
|
||||
"border-transparent bg-green-700 text-secondary-foreground shadow hover:bg-green-700/80",
|
||||
warning:
|
||||
"border-transparent bg-yellow-500 text-primary-foreground shadow hover:bg-yellow-500/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export { badgeVariants };
|
||||
17
skyvern-frontend/src/components/ui/badge.tsx
Normal file
17
skyvern-frontend/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from "react";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
import { badgeVariants } from "./badge-variants";
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge };
|
||||
@@ -1,8 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
@@ -34,24 +30,4 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button };
|
||||
export { buttonVariants };
|
||||
28
skyvern-frontend/src/components/ui/button.tsx
Normal file
28
skyvern-frontend/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
import { buttonVariants } from "./button-variants";
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button };
|
||||
83
skyvern-frontend/src/components/ui/card.tsx
Normal file
83
skyvern-frontend/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
120
skyvern-frontend/src/components/ui/dialog.tsx
Normal file
120
skyvern-frontend/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
138
skyvern-frontend/src/components/ui/form.tsx
Normal file
138
skyvern-frontend/src/components/ui/form.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
FormFieldContext,
|
||||
FormItemContext,
|
||||
useFormField,
|
||||
} from "./use-form-field";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
};
|
||||
25
skyvern-frontend/src/components/ui/input.tsx
Normal file
25
skyvern-frontend/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
24
skyvern-frontend/src/components/ui/label.tsx
Normal file
24
skyvern-frontend/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
122
skyvern-frontend/src/components/ui/pagination.tsx
Normal file
122
skyvern-frontend/src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DotsHorizontalIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
import { ButtonProps } from "@/components/ui/button";
|
||||
import { buttonVariants } from "./button-variants";
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Pagination.displayName = "Pagination";
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
PaginationContent.displayName = "PaginationContent";
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
));
|
||||
PaginationItem.displayName = "PaginationItem";
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<ButtonProps, "size"> &
|
||||
React.ComponentProps<"a">;
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
PaginationLink.displayName = "PaginationLink";
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationPrevious.displayName = "PaginationPrevious";
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationNext.displayName = "PaginationNext";
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<DotsHorizontalIcon className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis";
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
};
|
||||
42
skyvern-frontend/src/components/ui/radio-group.tsx
Normal file
42
skyvern-frontend/src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from "react";
|
||||
import { CheckIcon } from "@radix-ui/react-icons";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
162
skyvern-frontend/src/components/ui/select.tsx
Normal file
162
skyvern-frontend/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
CaretSortIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<CaretSortIcon className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
15
skyvern-frontend/src/components/ui/skeleton.tsx
Normal file
15
skyvern-frontend/src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
120
skyvern-frontend/src/components/ui/table.tsx
Normal file
120
skyvern-frontend/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
24
skyvern-frontend/src/components/ui/textarea.tsx
Normal file
24
skyvern-frontend/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea };
|
||||
127
skyvern-frontend/src/components/ui/toast.tsx
Normal file
127
skyvern-frontend/src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
||||
33
skyvern-frontend/src/components/ui/toaster.tsx
Normal file
33
skyvern-frontend/src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
28
skyvern-frontend/src/components/ui/tooltip.tsx
Normal file
28
skyvern-frontend/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
46
skyvern-frontend/src/components/ui/use-form-field.ts
Normal file
46
skyvern-frontend/src/components/ui/use-form-field.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { FieldPath, FieldValues, useFormContext } from "react-hook-form";
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
export const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
export { useFormField };
|
||||
189
skyvern-frontend/src/components/ui/use-toast.ts
Normal file
189
skyvern-frontend/src/components/ui/use-toast.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react";
|
||||
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const;
|
||||
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
||||
@@ -74,3 +74,8 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
body,
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
51
skyvern-frontend/src/router.tsx
Normal file
51
skyvern-frontend/src/router.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Navigate, createBrowserRouter } from "react-router-dom";
|
||||
import { RootLayout } from "./routes/root/RootLayout";
|
||||
import { TasksPageLayout } from "./routes/tasks/TasksPageLayout";
|
||||
import { CreateNewTask } from "./routes/tasks/create/CreateNewTask";
|
||||
import { TaskList } from "./routes/tasks/list/TaskList";
|
||||
import { Settings } from "./routes/settings/Settings";
|
||||
import { SettingsPageLayout } from "./routes/settings/SettingsPageLayout";
|
||||
import { TaskDetails } from "./routes/tasks/detail/TaskDetails";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <RootLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to="/tasks" />,
|
||||
},
|
||||
{
|
||||
path: "tasks",
|
||||
element: <TasksPageLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <TaskList />,
|
||||
},
|
||||
{
|
||||
path: ":taskId",
|
||||
element: <TaskDetails />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
element: <CreateNewTask />,
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
element: <SettingsPageLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Settings />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export { router };
|
||||
49
skyvern-frontend/src/routes/root/RootLayout.tsx
Normal file
49
skyvern-frontend/src/routes/root/RootLayout.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { SideNav } from "./SideNav";
|
||||
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
|
||||
function RootLayout() {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full h-full px-4 max-w-screen-2xl mx-auto">
|
||||
<aside className="fixed w-72 px-6 shrink-0 min-h-screen">
|
||||
<Link
|
||||
to="https://skyvern.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="h-24 flex items-center justify-center">
|
||||
<img src="/skyvern-logo.png" width={48} height={48} />
|
||||
<img src="/skyvern-logo-text.png" height={48} width={192} />
|
||||
</div>
|
||||
</Link>
|
||||
<SideNav />
|
||||
</aside>
|
||||
<div className="pl-72 h-24 flex justify-end items-center px-6 gap-4">
|
||||
<Link
|
||||
to="https://discord.com/invite/fG2XXEuQX3"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<DiscordLogoIcon className="w-6 h-6 text-gray-400 hover:text-white" />
|
||||
</Link>
|
||||
<Link
|
||||
to="https://github.com/Skyvern-AI/skyvern"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GitHubLogoIcon className="w-6 h-6 text-gray-400 hover:text-white" />
|
||||
</Link>
|
||||
</div>
|
||||
<main className="pl-72">
|
||||
<Outlet />
|
||||
</main>
|
||||
<aside className="w-72 shrink-0"></aside>
|
||||
</div>
|
||||
<Toaster />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { RootLayout };
|
||||
58
skyvern-frontend/src/routes/root/SideNav.tsx
Normal file
58
skyvern-frontend/src/routes/root/SideNav.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { cn } from "@/util/utils";
|
||||
import {
|
||||
GearIcon,
|
||||
ListBulletIcon,
|
||||
PlusCircledIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
function SideNav() {
|
||||
return (
|
||||
<nav className="flex flex-col gap-4">
|
||||
<NavLink
|
||||
to="create"
|
||||
className={({ isActive }) => {
|
||||
return cn(
|
||||
"flex items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl",
|
||||
{
|
||||
"bg-primary-foreground": isActive,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PlusCircledIcon className="mr-4" />
|
||||
<span>New Task</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="tasks"
|
||||
className={({ isActive }) => {
|
||||
return cn(
|
||||
"flex items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl",
|
||||
{
|
||||
"bg-primary-foreground": isActive,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ListBulletIcon className="mr-4" />
|
||||
<span>Task History</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="settings"
|
||||
className={({ isActive }) => {
|
||||
return cn(
|
||||
"flex items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl",
|
||||
{
|
||||
"bg-primary-foreground": isActive,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<GearIcon className="mr-4" />
|
||||
<span>Settings</span>
|
||||
</NavLink>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export { SideNav };
|
||||
49
skyvern-frontend/src/routes/settings/Settings.tsx
Normal file
49
skyvern-frontend/src/routes/settings/Settings.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useId } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useSettingsStore } from "@/store/SettingsStore";
|
||||
|
||||
function Settings() {
|
||||
const { environment, organization, setEnvironment, setOrganization } =
|
||||
useSettingsStore();
|
||||
const environmentInputId = useId();
|
||||
const organizationInputId = useId();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<h1>Settings</h1>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Label htmlFor={environmentInputId}>Environment</Label>
|
||||
<div>
|
||||
<Select value={environment} onValueChange={setEnvironment}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Environment" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="local">local</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Label htmlFor={organizationInputId}>Organization</Label>
|
||||
<div>
|
||||
<Select value={organization} onValueChange={setOrganization}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Organization" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="skyvern">Skyvern</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Settings };
|
||||
13
skyvern-frontend/src/routes/settings/SettingsPageLayout.tsx
Normal file
13
skyvern-frontend/src/routes/settings/SettingsPageLayout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
function SettingsPageLayout() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 px-6">
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SettingsPageLayout };
|
||||
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";
|
||||
19
skyvern-frontend/src/store/SettingsStore.ts
Normal file
19
skyvern-frontend/src/store/SettingsStore.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
type SettingsStore = {
|
||||
environment: string;
|
||||
organization: string;
|
||||
setEnvironment: (environment: string) => void;
|
||||
setOrganization: (organization: string) => void;
|
||||
};
|
||||
|
||||
const useSettingsStore = create<SettingsStore>((set) => {
|
||||
return {
|
||||
environment: "local",
|
||||
organization: "skyvern",
|
||||
setEnvironment: (environment: string) => set({ environment }),
|
||||
setOrganization: (organization: string) => set({ organization }),
|
||||
};
|
||||
});
|
||||
|
||||
export { useSettingsStore };
|
||||
25
skyvern-frontend/src/util/env.ts
Normal file
25
skyvern-frontend/src/util/env.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL as string;
|
||||
|
||||
if (!apiBaseUrl) {
|
||||
console.error("apiBaseUrl environment variable was not set");
|
||||
}
|
||||
|
||||
const environment = import.meta.env.VITE_ENVIRONMENT as string;
|
||||
|
||||
if (!environment) {
|
||||
console.error("environment environment variable was not set");
|
||||
}
|
||||
|
||||
const credential = import.meta.env.VITE_API_CREDENTIAL as string;
|
||||
|
||||
if (!credential) {
|
||||
console.error("credential environment variable was not set");
|
||||
}
|
||||
|
||||
const artifactApiBaseUrl = import.meta.env.VITE_ARTIFACT_API_BASE_URL as string;
|
||||
|
||||
if (!artifactApiBaseUrl) {
|
||||
console.error("artifactApiBaseUrl environment variable was not set");
|
||||
}
|
||||
|
||||
export { apiBaseUrl, environment, credential, artifactApiBaseUrl };
|
||||
13
skyvern-frontend/src/util/timeFormat.ts
Normal file
13
skyvern-frontend/src/util/timeFormat.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
function basicTimeFormat(time: string): string {
|
||||
const date = new Date(time);
|
||||
const dateString = date.toLocaleDateString("en-us", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
const timeString = date.toLocaleTimeString("en-us");
|
||||
return `${dateString} at ${timeString}`;
|
||||
}
|
||||
|
||||
export { basicTimeFormat };
|
||||
Reference in New Issue
Block a user