import { getClient } from "@/api/AxiosClient"; import { CreateTaskRequest, OrganizationApiResponse, ProxyLocation, } from "@/api/types"; import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { toast } from "@/components/ui/use-toast"; import { useApiCredential } from "@/hooks/useApiCredential"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { apiBaseUrl } from "@/util/env"; import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown"; import { type ApiCommandOptions } from "@/util/apiCommands"; import { zodResolver } from "@hookform/resolvers/zod"; import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; import { ToastAction } from "@radix-ui/react-toast"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { useState } from "react"; import { useForm, useFormState } from "react-hook-form"; import { Link } from "react-router-dom"; import { MAX_STEPS_DEFAULT } from "../constants"; import { TaskFormSection } from "./TaskFormSection"; import { createNewTaskFormSchema, CreateNewTaskFormValues, } from "./taskFormTypes"; import { ProxySelector } from "@/components/ProxySelector"; import { Switch } from "@/components/ui/switch"; import { MAX_SCREENSHOT_SCROLLING_TIMES_DEFAULT } from "@/routes/workflows/editor/nodes/Taskv2Node/types"; type Props = { initialValues: CreateNewTaskFormValues; }; function transform(value: T): T | null { return value === "" ? null : value; } function createTaskRequestObject( formValues: CreateNewTaskFormValues, ): CreateTaskRequest { let extractedInformationSchema = null; if (formValues.extractedInformationSchema) { try { extractedInformationSchema = JSON.parse( formValues.extractedInformationSchema, ); } catch (e) { extractedInformationSchema = formValues.extractedInformationSchema; } } let errorCodeMapping = null; if (formValues.errorCodeMapping) { try { errorCodeMapping = JSON.parse(formValues.errorCodeMapping); } catch (e) { errorCodeMapping = formValues.errorCodeMapping; } } return { title: null, url: formValues.url, webhook_callback_url: transform(formValues.webhookCallbackUrl), navigation_goal: transform(formValues.navigationGoal), data_extraction_goal: transform(formValues.dataExtractionGoal), proxy_location: formValues.proxyLocation ?? ProxyLocation.Residential, navigation_payload: transform(formValues.navigationPayload), extracted_information_schema: extractedInformationSchema, totp_identifier: transform(formValues.totpIdentifier), error_code_mapping: errorCodeMapping, max_screenshot_scrolling_times: formValues.maxScreenshotScrollingTimes, include_action_history_in_verification: formValues.includeActionHistoryInVerification, }; } type Section = "base" | "extraction" | "advanced"; function CreateNewTaskForm({ initialValues }: Props) { const queryClient = useQueryClient(); const credentialGetter = useCredentialGetter(); const apiCredential = useApiCredential(); const [activeSections, setActiveSections] = useState>([ "base", ]); const [showAdvancedBaseContent, setShowAdvancedBaseContent] = useState(false); const { data: organizations } = useQuery>({ queryKey: ["organizations"], queryFn: async () => { const client = await getClient(credentialGetter); return await client .get("/organizations") .then((response) => response.data.organizations); }, }); const organization = organizations?.[0]; const form = useForm({ resolver: zodResolver(createNewTaskFormSchema), defaultValues: { ...initialValues, maxStepsOverride: initialValues.maxStepsOverride ?? null, proxyLocation: initialValues.proxyLocation ?? ProxyLocation.Residential, maxScreenshotScrollingTimes: initialValues.maxScreenshotScrollingTimes ?? null, }, }); const { errors } = useFormState({ control: form.control }); const mutation = useMutation({ mutationFn: async (formValues: CreateNewTaskFormValues) => { const taskRequest = createTaskRequestObject(formValues); const client = await getClient(credentialGetter); const includeOverrideHeader = formValues.maxStepsOverride !== null && formValues.maxStepsOverride !== MAX_STEPS_DEFAULT; return client.post< ReturnType, { data: { task_id: string } } >("/tasks", taskRequest, { ...(includeOverrideHeader && { headers: { "x-max-steps-override": formValues.maxStepsOverride, }, }), }); }, onError: (error: AxiosError) => { if (error.response?.status === 402) { toast({ variant: "destructive", title: "Failed to create task", description: "You don't have enough credits to run this task. Go to billing to see your credit balance.", action: ( ), }); return; } toast({ variant: "destructive", title: "There was an error creating the task.", description: error.message, }); }, onSuccess: (response) => { toast({ variant: "success", title: "Task Created", description: `${response.data.task_id} created successfully.`, action: ( ), }); queryClient.invalidateQueries({ queryKey: ["tasks"], }); queryClient.invalidateQueries({ queryKey: ["runs"], }); }, }); function onSubmit(values: CreateNewTaskFormValues) { mutation.mutate(values); } function isActive(section: Section) { return activeSections.includes(section); } function toggleSection(section: Section) { if (isActive(section)) { setActiveSections(activeSections.filter((s) => s !== section)); } else { setActiveSections([...activeSections, section]); } } return (
{ toggleSection("base"); }} hasError={ typeof errors.url !== "undefined" || typeof errors.navigationGoal !== "undefined" } > {isActive("base") && (
(

URL

The starting URL for the task

)} /> (

Navigation Goal

Where should Skyvern go and what should Skyvern do?

)} /> {showAdvancedBaseContent ? (
(

Navigation Payload

Specify important parameters, routes, or states

)} />
) : (
)}
)}
{ toggleSection("extraction"); }} hasError={ typeof errors.dataExtractionGoal !== "undefined" || typeof errors.extractedInformationSchema !== "undefined" } > {isActive("extraction") && (
(

Data Extraction Goal

What outputs are you looking to get?

)} /> (

Data Schema

Specify the output format in JSON

)} />
)}
{ toggleSection("advanced"); }} hasError={ typeof errors.navigationPayload !== "undefined" || typeof errors.maxStepsOverride !== "undefined" || typeof errors.webhookCallbackUrl !== "undefined" || typeof errors.errorCodeMapping !== "undefined" } > {isActive("advanced") && (
(

Include Action History

Whether to include action history when verifying the task completion.

{ field.onChange(checked); }} />
)} /> (

Max Steps Override

Want to allow this task to execute more or less steps than the default?

{ const value = event.target.value === "" ? null : Number(event.target.value); field.onChange(value); }} />
)} /> (

Webhook Callback URL

The URL of a webhook endpoint to send the extracted information

)} /> { return (
Proxy Location

Route Skyvern through one of our available proxies.

); }} /> (

Max Scrolling Screenshots

{`The maximum number of times to scroll down the page to take merged screenshots after action. Default is ${MAX_SCREENSHOT_SCROLLING_TIMES_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`}

{ const value = event.target.value === "" ? null : Number(event.target.value); field.onChange(value); }} />
)} /> (

Error Messages

Specify any error outputs you would like to be notified about

)} /> (

2FA Identifier

)} />
)}
({ method: "POST", url: `${apiBaseUrl}/tasks`, body: createTaskRequestObject(form.getValues()), headers: { "Content-Type": "application/json", "x-api-key": apiCredential ?? "", }, }) satisfies ApiCommandOptions } />
); } export { CreateNewTaskForm };