From e2bff34a074857dcb20bd2f5edb0479807a56a67 Mon Sep 17 00:00:00 2001 From: Kerem Yilmaz Date: Tue, 8 Oct 2024 11:36:41 -0700 Subject: [PATCH] Adjust task placeholders (#933) --- .../src/components/ui/multi-select.tsx | 4 +- .../routes/tasks/create/CreateNewTaskForm.tsx | 41 +++++++--- .../src/routes/tasks/create/SavedTaskForm.tsx | 37 ++++++--- .../src/routes/tasks/create/taskFormTypes.ts | 2 +- .../nodes/DownloadNode/DownloadNode.tsx | 2 +- .../nodes/FileParserNode/FileParserNode.tsx | 2 +- .../nodes/SendEmailNode/SendEmailNode.tsx | 8 +- .../editor/nodes/TaskNode/TaskNode.tsx | 78 ++++++++++++------- .../workflows/editor/nodes/TaskNode/types.ts | 24 ++++++ .../nodes/TextPromptNode/TextPromptNode.tsx | 2 +- .../editor/nodes/UploadNode/UploadNode.tsx | 2 +- 11 files changed, 140 insertions(+), 62 deletions(-) diff --git a/skyvern-frontend/src/components/ui/multi-select.tsx b/skyvern-frontend/src/components/ui/multi-select.tsx index 5661d31d4..d5aa43c49 100644 --- a/skyvern-frontend/src/components/ui/multi-select.tsx +++ b/skyvern-frontend/src/components/ui/multi-select.tsx @@ -174,7 +174,7 @@ export const MultiSelect = React.forwardRef< {...props} onClick={handleTogglePopover} className={cn( - "flex h-auto min-h-10 w-full items-center justify-between rounded-md border bg-inherit p-1 hover:bg-inherit", + "flex h-auto min-h-8 w-full items-center justify-between rounded-md border bg-inherit p-1 hover:bg-inherit", className, )} > @@ -238,7 +238,7 @@ export const MultiSelect = React.forwardRef< ) : (
- + {placeholder} diff --git a/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx b/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx index e4fae2f53..d2bd301d5 100644 --- a/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx +++ b/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx @@ -1,5 +1,5 @@ import { getClient } from "@/api/AxiosClient"; -import { CreateTaskRequest } from "@/api/types"; +import { CreateTaskRequest, OrganizationApiResponse } from "@/api/types"; import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { Button } from "@/components/ui/button"; import { @@ -21,7 +21,7 @@ import { apiBaseUrl } from "@/util/env"; import { zodResolver } from "@hookform/resolvers/zod"; import { ReloadIcon } from "@radix-ui/react-icons"; import { ToastAction } from "@radix-ui/react-toast"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; import fetchToCurl from "fetch-to-curl"; import { useState } from "react"; @@ -91,12 +91,24 @@ function CreateNewTaskForm({ initialValues }: Props) { ]); 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, values: { ...initialValues, - maxStepsOverride: MAX_STEPS_DEFAULT, + maxStepsOverride: null, }, }); const { errors } = useFormState({ control: form.control }); @@ -106,6 +118,7 @@ function CreateNewTaskForm({ initialValues }: Props) { const taskRequest = createTaskRequestObject(formValues); const client = await getClient(credentialGetter); const includeOverrideHeader = + formValues.maxStepsOverride !== null && formValues.maxStepsOverride !== MAX_STEPS_DEFAULT; return client.post< ReturnType, @@ -113,8 +126,7 @@ function CreateNewTaskForm({ initialValues }: Props) { >("/tasks", taskRequest, { ...(includeOverrideHeader && { headers: { - "x-max-steps-override": - formValues.maxStepsOverride ?? MAX_STEPS_DEFAULT, + "x-max-steps-override": formValues.maxStepsOverride, }, }), }); @@ -237,8 +249,8 @@ function CreateNewTaskForm({ initialValues }: Props) {
@@ -345,8 +357,8 @@ function CreateNewTaskForm({ initialValues }: Props) {
@@ -428,9 +440,14 @@ function CreateNewTaskForm({ initialValues }: Props) { {...field} type="number" min={1} - value={field.value} + value={field.value ?? ""} + placeholder={`Default: ${organization?.max_steps_per_run ?? MAX_STEPS_DEFAULT}`} onChange={(event) => { - field.onChange(parseInt(event.target.value)); + const value = + event.target.value === "" + ? null + : Number(event.target.value); + field.onChange(value); }} /> @@ -458,8 +475,8 @@ function CreateNewTaskForm({ initialValues }: Props) {
@@ -517,8 +534,8 @@ function CreateNewTaskForm({ initialValues }: Props) {
@@ -543,8 +560,8 @@ function CreateNewTaskForm({ initialValues }: Props) {
diff --git a/skyvern-frontend/src/routes/tasks/create/SavedTaskForm.tsx b/skyvern-frontend/src/routes/tasks/create/SavedTaskForm.tsx index 62cd9e76c..2becdbf0f 100644 --- a/skyvern-frontend/src/routes/tasks/create/SavedTaskForm.tsx +++ b/skyvern-frontend/src/routes/tasks/create/SavedTaskForm.tsx @@ -21,7 +21,7 @@ import { apiBaseUrl } from "@/util/env"; import { zodResolver } from "@hookform/resolvers/zod"; import { ReloadIcon } from "@radix-ui/react-icons"; import { ToastAction } from "@radix-ui/react-toast"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; import fetchToCurl from "fetch-to-curl"; import { useState } from "react"; @@ -31,6 +31,7 @@ import { stringify as convertToYAML } from "yaml"; import { MAX_STEPS_DEFAULT } from "../constants"; import { TaskFormSection } from "./TaskFormSection"; import { savedTaskFormSchema, SavedTaskFormValues } from "./taskFormTypes"; +import { OrganizationApiResponse } from "@/api/types"; type Props = { initialValues: SavedTaskFormValues; @@ -142,12 +143,24 @@ function SavedTaskForm({ initialValues }: Props) { ]); 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(savedTaskFormSchema), defaultValues: initialValues, values: { ...initialValues, - maxStepsOverride: initialValues.maxStepsOverride ?? MAX_STEPS_DEFAULT, + maxStepsOverride: initialValues.maxStepsOverride ?? null, }, }); @@ -168,6 +181,7 @@ function SavedTaskForm({ initialValues }: Props) { .then(() => { const taskRequest = createTaskRequestObject(formValues); const includeOverrideHeader = + formValues.maxStepsOverride !== null && formValues.maxStepsOverride !== MAX_STEPS_DEFAULT; return client.post< ReturnType, @@ -404,8 +418,8 @@ function SavedTaskForm({ initialValues }: Props) {
@@ -512,8 +526,8 @@ function SavedTaskForm({ initialValues }: Props) {
@@ -600,9 +614,14 @@ function SavedTaskForm({ initialValues }: Props) { {...field} type="number" min={1} - value={field.value} + value={field.value ?? ""} + placeholder={`Default: ${organization?.max_steps_per_run ?? MAX_STEPS_DEFAULT}`} onChange={(event) => { - field.onChange(parseInt(event.target.value)); + const value = + event.target.value === "" + ? null + : Number(event.target.value); + field.onChange(value); }} /> @@ -630,8 +649,8 @@ function SavedTaskForm({ initialValues }: Props) {
@@ -689,8 +708,8 @@ function SavedTaskForm({ initialValues }: Props) {
@@ -715,8 +734,8 @@ function SavedTaskForm({ initialValues }: Props) {
diff --git a/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts b/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts index d0805460d..e4555d80c 100644 --- a/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts +++ b/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts @@ -9,7 +9,7 @@ const createNewTaskFormSchemaBase = z.object({ dataExtractionGoal: z.string().or(z.null()), navigationPayload: z.string().or(z.null()), extractedInformationSchema: z.string().or(z.null()), - maxStepsOverride: z.number().optional(), + maxStepsOverride: z.number().or(z.null()).optional(), totpVerificationUrl: z.string().or(z.null()), totpIdentifier: z.string().or(z.null()), errorCodeMapping: z.string().or(z.null()), diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/DownloadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/DownloadNode.tsx index 5a18721e2..964127164 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/DownloadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/DownloadNode.tsx @@ -55,7 +55,7 @@ function DownloadNode({ id, data }: NodeProps) {
- +
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx index e292b5129..c2e32fb84 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx @@ -68,7 +68,7 @@ function FileParserNode({ id, data }: NodeProps) { setInputs({ ...inputs, fileUrl: event.target.value }); updateNodeData(id, { fileUrl: event.target.value }); }} - className="nopan" + className="nopan text-xs" />
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx index 21f036907..71446ef15 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx @@ -80,7 +80,7 @@ function SendEmailNode({ id, data }: NodeProps) { }} value={inputs.recipients} placeholder="example@gmail.com, example2@gmail.com..." - className="nopan" + className="nopan text-xs" />
@@ -95,7 +95,7 @@ function SendEmailNode({ id, data }: NodeProps) { }} value={inputs.subject} placeholder="What is the gist?" - className="nopan" + className="nopan text-xs" />
@@ -109,7 +109,7 @@ function SendEmailNode({ id, data }: NodeProps) { }} value={inputs.body} placeholder="What would you like to say?" - className="nopan" + className="nopan text-xs" />
@@ -124,7 +124,7 @@ function SendEmailNode({ id, data }: NodeProps) { handleChange("fileAttachments", event.target.value); }} disabled - className="nopan" + className="nopan text-xs" />
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx index c30ea97bf..1e98beab3 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx @@ -30,6 +30,9 @@ import { NodeActionMenu } from "../NodeActionMenu"; import { TaskNodeDisplayModeSwitch } from "./TaskNodeDisplayModeSwitch"; import { TaskNodeParametersPanel } from "./TaskNodeParametersPanel"; import { + dataSchemaExampleValue, + errorMappingExampleValue, + fieldPlaceholders, helpTooltipContent, type TaskNode, type TaskNodeDisplayMode, @@ -90,7 +93,7 @@ function TaskNode({ id, data }: NodeProps) { { if (!editable) { @@ -98,7 +101,7 @@ function TaskNode({ id, data }: NodeProps) { } handleChange("url", event.target.value); }} - placeholder="https://" + placeholder={fieldPlaceholders["url"]} />
@@ -111,8 +114,8 @@ function TaskNode({ id, data }: NodeProps) { handleChange("navigationGoal", event.target.value); }} value={inputs.navigationGoal} - placeholder="What are you looking to do?" - className="nopan" + placeholder={fieldPlaceholders["navigationGoal"]} + className="nopan text-xs" />
@@ -151,8 +154,8 @@ function TaskNode({ id, data }: NodeProps) { handleChange("url", event.target.value); }} value={inputs.url} - placeholder="https://" - className="nopan" + placeholder={fieldPlaceholders["url"]} + className="nopan text-xs" />
@@ -165,8 +168,8 @@ function TaskNode({ id, data }: NodeProps) { handleChange("navigationGoal", event.target.value); }} value={inputs.navigationGoal} - placeholder="What are you looking to do?" - className="nopan" + placeholder={fieldPlaceholders["navigationGoal"]} + className="nopan text-xs" />
@@ -204,8 +207,8 @@ function TaskNode({ id, data }: NodeProps) { handleChange("dataExtractionGoal", event.target.value); }} value={inputs.dataExtractionGoal} - placeholder="What outputs are you looking to get?" - className="nopan" + placeholder={fieldPlaceholders["dataExtractionGoal"]} + className="nopan text-xs" />
@@ -217,7 +220,12 @@ function TaskNode({ id, data }: NodeProps) { if (!editable) { return; } - handleChange("dataSchema", checked ? "{}" : "null"); + handleChange( + "dataSchema", + checked + ? JSON.stringify(dataSchemaExampleValue, null, 2) + : "null", + ); }} />
@@ -257,15 +265,19 @@ function TaskNode({ id, data }: NodeProps) { { if (!editable) { return; } - handleChange("maxRetries", Number(event.target.value)); + const value = + event.target.value === "" + ? null + : Number(event.target.value); + handleChange("maxRetries", value); }} />
@@ -275,18 +287,19 @@ function TaskNode({ id, data }: NodeProps) { { if (!editable) { return; } - handleChange( - "maxStepsOverride", - Number(event.target.value), - ); + const value = + event.target.value === "" + ? null + : Number(event.target.value); + handleChange("maxStepsOverride", value); }} />
@@ -308,12 +321,12 @@ function TaskNode({ id, data }: NodeProps) {
{ if (!editable) { @@ -335,7 +348,12 @@ function TaskNode({ id, data }: NodeProps) { if (!editable) { return; } - handleChange("errorCodeMapping", checked ? "{}" : "null"); + handleChange( + "errorCodeMapping", + checked + ? JSON.stringify(errorMappingExampleValue, null, 2) + : "null", + ); }} />
@@ -381,8 +399,8 @@ function TaskNode({ id, data }: NodeProps) { handleChange("totpVerificationUrl", event.target.value); }} value={inputs.totpVerificationUrl ?? ""} - placeholder="Link your 2FA storage endpoint" - className="nopan" + placeholder={fieldPlaceholders["totpVerificationUrl"]} + className="nopan text-xs" />
@@ -397,8 +415,8 @@ function TaskNode({ id, data }: NodeProps) { handleChange("totpIdentifier", event.target.value); }} value={inputs.totpIdentifier ?? ""} - placeholder="Add an ID that links your TOTP to the task" - className="nopan" + placeholder={fieldPlaceholders["totpIdentifier"]} + className="nopan text-xs" />
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/types.ts index d1a69d085..ae96ebc3e 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/types.ts @@ -41,6 +41,7 @@ export const taskNodeDefaultData: TaskNodeData = { export function isTaskNode(node: Node): node is TaskNode { return node.type === "task"; } + export const helpTooltipContent = { base: "Tell Skyvern what to do. This is the core of your task block, so make sure your prompt tells Skyvern when it has completed its task, any guardrails, and if there are cases where it should terminate the task early. Define placeholder values using the “parameters” drop down that you predefine or redefine run-to-run. This allows you to make a workflow generalizable to a variety of use cases that change with every run.", extraction: @@ -48,4 +49,27 @@ export const helpTooltipContent = { limits: "Give Skyvern limitations, such as number of retries on failure, the number of maximum steps, the option to download and append suffix identifiers, and return message for errors Skyvern encounters.", totp: "Link your internal TOTP storage system to relay 2FA codes we encounter straight to Skyvern. If you have multiple tasks running simultaneously, make sure to link an identifier so Skyvern knows which TOTP URL goes with which task.", +} as const; + +export const fieldPlaceholders = { + url: "https://", + navigationGoal: "Tell Skyvern what to do.", + dataExtractionGoal: "What data do you need to extract?", + maxRetries: "Default: 3", + maxStepsOverride: "Default: 10", + downloadSuffix: "Suffix for file downloads", + label: "Task", + totpVerificationUrl: "Provide your 2FA endpoint", + totpIdentifier: "Add an ID that links your TOTP to the task", +}; + +export const errorMappingExampleValue = { + sample_invalid_credentials: "if the credentials are incorrect, terminate", +}; + +export const dataSchemaExampleValue = { + type: "object", + properties: { + sample: { type: "string" }, + }, }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx index 945631130..72148d58c 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx @@ -75,7 +75,7 @@ function TextPromptNode({ id, data }: NodeProps) { }} value={inputs.prompt} placeholder="What do you want to generate?" - className="nopan" + className="nopan text-xs" /> diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx index 74d4fe689..fe3ca3173 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx @@ -55,7 +55,7 @@ function UploadNode({ id, data }: NodeProps) {
- +