From 2765382befc0d63483db6134b572e6925c7eecb6 Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Tue, 11 Nov 2025 21:10:02 -0500 Subject: [PATCH] add some UI for prompt improval [sic] (#3974) --- .../src/components/ImprovePrompt.tsx | 181 ++++++++++++++++++ .../components/WorkflowBlockInputTextarea.tsx | 69 +++++-- .../src/routes/tasks/create/PromptBox.tsx | 55 ++++-- .../editor/nodes/Taskv2Node/Taskv2Node.tsx | 1 + .../routes/workflows/types/workflowTypes.ts | 6 + 5 files changed, 277 insertions(+), 35 deletions(-) create mode 100644 skyvern-frontend/src/components/ImprovePrompt.tsx diff --git a/skyvern-frontend/src/components/ImprovePrompt.tsx b/skyvern-frontend/src/components/ImprovePrompt.tsx new file mode 100644 index 00000000..8e005c72 --- /dev/null +++ b/skyvern-frontend/src/components/ImprovePrompt.tsx @@ -0,0 +1,181 @@ +import { AxiosError } from "axios"; +import { MagicWandIcon, ReloadIcon } from "@radix-ui/react-icons"; +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; + +import { getClient } from "@/api/AxiosClient"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { SwitchBar } from "@/components/SwitchBar"; +import { toast } from "@/components/ui/use-toast"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { ImprovePromptForWorkflowResponse } from "@/routes/workflows/types/workflowTypes"; + +interface Props { + context?: string; + isVisible?: boolean; + onBegin?: () => void; + onEnd?: () => void; + onImprove: (improvedPrompt: string) => void; + prompt: string; + size?: "small" | "large"; + useCase: string; +} + +function ImprovePrompt(props: Props) { + const { size = "large" } = props; + const credentialGetter = useCredentialGetter(); + const [showImproveDialog, setShowImproveDialog] = useState(false); + const [improvedPrompt, setImprovedPrompt] = useState(""); + const [originalPrompt, setOriginalPrompt] = useState(""); + const [selectedPromptVersion, setSelectedPromptVersion] = useState< + "improved" | "original" + >("improved"); + + const improvePromptMutation = useMutation({ + mutationFn: async ({ prompt }: { prompt: string }) => { + props.onBegin?.(); + const client = await getClient(credentialGetter, "sans-api-v1"); + + const result = await client.post< + { prompt: string }, + { data: ImprovePromptForWorkflowResponse } + >(`/prompts/improve?use-case=${props.useCase}`, { + context: props.context, + prompt, + }); + + return result; + }, + onSuccess: ({ data: { error, improved, original } }) => { + props.onEnd?.(); + + if (error) { + console.error("Error improving prompt:", error); + + toast({ + variant: "default", + title: + "We're sorry - we could not improve upon the prompt at this time.", + description: `Please try again later.\n\n[${error}]`, + }); + + return; + } + + setImprovedPrompt(improved); + setOriginalPrompt(original); + setSelectedPromptVersion("improved"); + setShowImproveDialog(true); + }, + onError: (error: AxiosError) => { + props.onEnd?.(); + + toast({ + variant: "destructive", + title: "Error improving prompt", + description: error.message, + }); + }, + }); + + return ( +
+ {improvePromptMutation.isPending ? ( + + ) : ( + + + + { + improvePromptMutation.mutate({ + prompt: props.prompt, + }); + }} + /> + + +

Have AI improve your prompt!

+
+
+
+ )} + + + + Choose Your Prompt + + Select which version of the prompt you'd like to use + + +
+ + setSelectedPromptVersion(value as "improved" | "original") + } + /> +
+

+ {selectedPromptVersion === "improved" + ? improvedPrompt + : originalPrompt} +

+
+
+ + + + +
+
+
+ ); +} + +export { ImprovePrompt }; diff --git a/skyvern-frontend/src/components/WorkflowBlockInputTextarea.tsx b/skyvern-frontend/src/components/WorkflowBlockInputTextarea.tsx index bd3b2432..492230e5 100644 --- a/skyvern-frontend/src/components/WorkflowBlockInputTextarea.tsx +++ b/skyvern-frontend/src/components/WorkflowBlockInputTextarea.tsx @@ -7,10 +7,18 @@ import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore"; import { useEffect, useRef, useState } from "react"; import { useDebouncedCallback } from "use-debounce"; +import { ImprovePrompt } from "./ImprovePrompt"; + +interface AiImprove { + context?: string; + useCase: string; +} + type Props = Omit< React.ComponentProps, "onChange" > & { + aiImprove?: AiImprove; canWriteTitle?: boolean; onChange: (value: string) => void; nodeId: string; @@ -18,7 +26,13 @@ type Props = Omit< function WorkflowBlockInputTextarea(props: Props) { const { maybeAcceptTitle, maybeWriteTitle } = useWorkflowTitleStore(); - const { nodeId, onChange, canWriteTitle = false, ...textAreaProps } = props; + const { + aiImprove, + nodeId, + onChange, + canWriteTitle = false, + ...textAreaProps + } = props; const [internalValue, setInternalValue] = useState(props.value ?? ""); const textareaRef = useRef(null); const [cursorPosition, setCursorPosition] = useState<{ @@ -71,6 +85,12 @@ function WorkflowBlockInputTextarea(props: Props) { } }; + const handleOnChange = (value: string) => { + setInternalValue(value); + handleTextareaSelect(); + doOnChange(value); + }; + return (
{ - setInternalValue(event.target.value); - handleTextareaSelect(); - doOnChange(event.target.value); + handleOnChange(event.target.value); }} onClick={handleTextareaSelect} onKeyUp={handleTextareaSelect} onSelect={handleTextareaSelect} - className={cn("pr-9", props.className)} + className={cn(`${aiImprove ? "pr-12" : "pr-9"}`, props.className)} /> -
- - -
- -
-
- - +
+ {aiImprove && ( + handleOnChange(prompt)} + useCase={aiImprove.useCase} /> - - + )} +
+ + +
+ +
+
+ + + +
+
+
); diff --git a/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx b/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx index 2a0a2545..ef6981e0 100644 --- a/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx +++ b/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx @@ -47,6 +47,8 @@ import { } from "@/routes/workflows/editor/nodes/Taskv2Node/types"; import { useAutoplayStore } from "@/store/useAutoplayStore"; import { TestWebhookDialog } from "@/components/TestWebhookDialog"; +import { ImprovePrompt } from "@/components/ImprovePrompt"; +import { cn } from "@/util/utils"; const exampleCases = [ { @@ -136,6 +138,7 @@ function PromptBox() { const [dataSchema, setDataSchema] = useState(null); const [extraHttpHeaders, setExtraHttpHeaders] = useState(null); const { setAutoplay } = useAutoplayStore(); + const [promptImprovalIsPending, setPromptImprovalIsPending] = useState(false); const generateWorkflowMutation = useMutation({ mutationFn: async ({ @@ -240,7 +243,14 @@ function PromptBox() { What task would you like to accomplish?
-
+
+ { + setPromptImprovalIsPending(true); + }} + onEnd={() => { + setPromptImprovalIsPending(false); + }} + onImprove={(prompt) => setPrompt(prompt)} + prompt={prompt} + size="large" + useCase="new_workflow" + />
-
+
{generateWorkflowMutation.isPending ? ( ) : ( -
- {generateWorkflowMutation.isPending ? ( - - ) : ( - { - generateWorkflowMutation.mutate({ - prompt, - version: selectValue, - }); - }} - /> - )} -
+ { + generateWorkflowMutation.mutate({ + prompt, + version: selectValue, + }); + }} + /> )}
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx index a7d48227..bf5dd711 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx @@ -107,6 +107,7 @@ function Taskv2Node({ id, data, type }: NodeProps) { ) : null}
{ update({ prompt: value }); diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts index 68126f78..8ed9beed 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts @@ -584,3 +584,9 @@ export function isOutputParameter( ): parameter is OutputParameter { return parameter.parameter_type === "output"; } + +export type ImprovePromptForWorkflowResponse = { + error: string | null; + improved: string; + original: string; +};