add some UI for prompt improval [sic] (#3974)
This commit is contained in:
181
skyvern-frontend/src/components/ImprovePrompt.tsx
Normal file
181
skyvern-frontend/src/components/ImprovePrompt.tsx
Normal file
@@ -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<string>("");
|
||||||
|
const [originalPrompt, setOriginalPrompt] = useState<string>("");
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={`flex items-center overflow-hidden transition-all duration-300 ${
|
||||||
|
props.isVisible
|
||||||
|
? `${size === "large" ? "w-14" : "w-4"} opacity-100`
|
||||||
|
: "pointer-events-none w-0 opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{improvePromptMutation.isPending ? (
|
||||||
|
<ReloadIcon
|
||||||
|
className={`size-${size === "large" ? "6" : "4"} shrink-0 animate-spin`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<MagicWandIcon
|
||||||
|
className={`${size === "large" ? "size-6" : "size-4"} shrink-0 cursor-pointer`}
|
||||||
|
onClick={async () => {
|
||||||
|
improvePromptMutation.mutate({
|
||||||
|
prompt: props.prompt,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Have AI improve your prompt!</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
<Dialog open={showImproveDialog} onOpenChange={setShowImproveDialog}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Choose Your Prompt</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select which version of the prompt you'd like to use
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SwitchBar
|
||||||
|
options={[
|
||||||
|
{ label: "Improved", value: "improved" },
|
||||||
|
{ label: "Original", value: "original" },
|
||||||
|
]}
|
||||||
|
value={selectedPromptVersion}
|
||||||
|
onChange={(value) =>
|
||||||
|
setSelectedPromptVersion(value as "improved" | "original")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="max-h-96 overflow-y-auto rounded-md border border-slate-700 bg-slate-800 p-4">
|
||||||
|
<p className="whitespace-pre-wrap text-sm">
|
||||||
|
{selectedPromptVersion === "improved"
|
||||||
|
? improvedPrompt
|
||||||
|
: originalPrompt}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowImproveDialog(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
props.onImprove(
|
||||||
|
selectedPromptVersion === "improved"
|
||||||
|
? improvedPrompt
|
||||||
|
: originalPrompt,
|
||||||
|
);
|
||||||
|
setShowImproveDialog(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use This Prompt
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ImprovePrompt };
|
||||||
@@ -7,10 +7,18 @@ import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
|
import { ImprovePrompt } from "./ImprovePrompt";
|
||||||
|
|
||||||
|
interface AiImprove {
|
||||||
|
context?: string;
|
||||||
|
useCase: string;
|
||||||
|
}
|
||||||
|
|
||||||
type Props = Omit<
|
type Props = Omit<
|
||||||
React.ComponentProps<typeof AutoResizingTextarea>,
|
React.ComponentProps<typeof AutoResizingTextarea>,
|
||||||
"onChange"
|
"onChange"
|
||||||
> & {
|
> & {
|
||||||
|
aiImprove?: AiImprove;
|
||||||
canWriteTitle?: boolean;
|
canWriteTitle?: boolean;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -18,7 +26,13 @@ type Props = Omit<
|
|||||||
|
|
||||||
function WorkflowBlockInputTextarea(props: Props) {
|
function WorkflowBlockInputTextarea(props: Props) {
|
||||||
const { maybeAcceptTitle, maybeWriteTitle } = useWorkflowTitleStore();
|
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 [internalValue, setInternalValue] = useState(props.value ?? "");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [cursorPosition, setCursorPosition] = useState<{
|
const [cursorPosition, setCursorPosition] = useState<{
|
||||||
@@ -71,6 +85,12 @@ function WorkflowBlockInputTextarea(props: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnChange = (value: string) => {
|
||||||
|
setInternalValue(value);
|
||||||
|
handleTextareaSelect();
|
||||||
|
doOnChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
@@ -81,29 +101,42 @@ function WorkflowBlockInputTextarea(props: Props) {
|
|||||||
doOnChange.flush();
|
doOnChange.flush();
|
||||||
}}
|
}}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setInternalValue(event.target.value);
|
handleOnChange(event.target.value);
|
||||||
handleTextareaSelect();
|
|
||||||
doOnChange(event.target.value);
|
|
||||||
}}
|
}}
|
||||||
onClick={handleTextareaSelect}
|
onClick={handleTextareaSelect}
|
||||||
onKeyUp={handleTextareaSelect}
|
onKeyUp={handleTextareaSelect}
|
||||||
onSelect={handleTextareaSelect}
|
onSelect={handleTextareaSelect}
|
||||||
className={cn("pr-9", props.className)}
|
className={cn(`${aiImprove ? "pr-12" : "pr-9"}`, props.className)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center">
|
|
||||||
<Popover>
|
<div className="absolute right-1 top-0 flex size-9 items-center justify-end">
|
||||||
<PopoverTrigger asChild>
|
<div className="flex items-center justify-center gap-1">
|
||||||
<div className="rounded p-1 hover:bg-muted">
|
{aiImprove && (
|
||||||
<PlusIcon className="size-4" />
|
<ImprovePrompt
|
||||||
</div>
|
context={aiImprove.context}
|
||||||
</PopoverTrigger>
|
isVisible={Boolean(internalValue.trim())}
|
||||||
<PopoverContent className="w-[22rem]">
|
size="small"
|
||||||
<WorkflowBlockParameterSelect
|
prompt={internalValue}
|
||||||
nodeId={nodeId}
|
onImprove={(prompt) => handleOnChange(prompt)}
|
||||||
onAdd={insertParameterAtCursor}
|
useCase={aiImprove.useCase}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
)}
|
||||||
</Popover>
|
<div className="cursor-pointer">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<div className="rounded p-1 hover:bg-muted">
|
||||||
|
<PlusIcon className="size-4" />
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[22rem]">
|
||||||
|
<WorkflowBlockParameterSelect
|
||||||
|
nodeId={nodeId}
|
||||||
|
onAdd={insertParameterAtCursor}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import {
|
|||||||
} from "@/routes/workflows/editor/nodes/Taskv2Node/types";
|
} from "@/routes/workflows/editor/nodes/Taskv2Node/types";
|
||||||
import { useAutoplayStore } from "@/store/useAutoplayStore";
|
import { useAutoplayStore } from "@/store/useAutoplayStore";
|
||||||
import { TestWebhookDialog } from "@/components/TestWebhookDialog";
|
import { TestWebhookDialog } from "@/components/TestWebhookDialog";
|
||||||
|
import { ImprovePrompt } from "@/components/ImprovePrompt";
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
const exampleCases = [
|
const exampleCases = [
|
||||||
{
|
{
|
||||||
@@ -136,6 +138,7 @@ function PromptBox() {
|
|||||||
const [dataSchema, setDataSchema] = useState<string | null>(null);
|
const [dataSchema, setDataSchema] = useState<string | null>(null);
|
||||||
const [extraHttpHeaders, setExtraHttpHeaders] = useState<string | null>(null);
|
const [extraHttpHeaders, setExtraHttpHeaders] = useState<string | null>(null);
|
||||||
const { setAutoplay } = useAutoplayStore();
|
const { setAutoplay } = useAutoplayStore();
|
||||||
|
const [promptImprovalIsPending, setPromptImprovalIsPending] = useState(false);
|
||||||
|
|
||||||
const generateWorkflowMutation = useMutation({
|
const generateWorkflowMutation = useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
@@ -240,7 +243,14 @@ function PromptBox() {
|
|||||||
What task would you like to accomplish?
|
What task would you like to accomplish?
|
||||||
</span>
|
</span>
|
||||||
<div className="flex w-full max-w-xl flex-col">
|
<div className="flex w-full max-w-xl flex-col">
|
||||||
<div className="flex w-full items-center gap-2 rounded-xl bg-slate-700 py-2 pr-4">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center gap-2 rounded-xl bg-slate-700 py-2 pr-4",
|
||||||
|
{
|
||||||
|
"pointer-events-none opacity-50": promptImprovalIsPending,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
className="min-h-0 resize-none rounded-xl border-transparent px-4 hover:border-transparent focus-visible:ring-0"
|
className="min-h-0 resize-none rounded-xl border-transparent px-4 hover:border-transparent focus-visible:ring-0"
|
||||||
value={prompt}
|
value={prompt}
|
||||||
@@ -312,6 +322,19 @@ function PromptBox() {
|
|||||||
</CustomSelectItem>
|
</CustomSelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<ImprovePrompt
|
||||||
|
isVisible={Boolean(prompt.trim())}
|
||||||
|
onBegin={() => {
|
||||||
|
setPromptImprovalIsPending(true);
|
||||||
|
}}
|
||||||
|
onEnd={() => {
|
||||||
|
setPromptImprovalIsPending(false);
|
||||||
|
}}
|
||||||
|
onImprove={(prompt) => setPrompt(prompt)}
|
||||||
|
prompt={prompt}
|
||||||
|
size="large"
|
||||||
|
useCase="new_workflow"
|
||||||
|
/>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<GearIcon
|
<GearIcon
|
||||||
className="size-6 cursor-pointer"
|
className="size-6 cursor-pointer"
|
||||||
@@ -320,25 +343,23 @@ function PromptBox() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div
|
||||||
|
className={cn("flex items-center", {
|
||||||
|
"pointer-events-none opacity-20": !prompt.trim(),
|
||||||
|
})}
|
||||||
|
>
|
||||||
{generateWorkflowMutation.isPending ? (
|
{generateWorkflowMutation.isPending ? (
|
||||||
<ReloadIcon className="size-6 animate-spin" />
|
<ReloadIcon className="size-6 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center">
|
<PaperPlaneIcon
|
||||||
{generateWorkflowMutation.isPending ? (
|
className="size-6 cursor-pointer"
|
||||||
<ReloadIcon className="size-6 animate-spin" />
|
onClick={async () => {
|
||||||
) : (
|
generateWorkflowMutation.mutate({
|
||||||
<PaperPlaneIcon
|
prompt,
|
||||||
className="size-6 cursor-pointer"
|
version: selectValue,
|
||||||
onClick={async () => {
|
});
|
||||||
generateWorkflowMutation.mutate({
|
}}
|
||||||
prompt,
|
/>
|
||||||
version: selectValue,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<WorkflowBlockInputTextarea
|
<WorkflowBlockInputTextarea
|
||||||
|
aiImprove={{ useCase: "task_v2_prompt" }}
|
||||||
nodeId={id}
|
nodeId={id}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
update({ prompt: value });
|
update({ prompt: value });
|
||||||
|
|||||||
@@ -584,3 +584,9 @@ export function isOutputParameter(
|
|||||||
): parameter is OutputParameter {
|
): parameter is OutputParameter {
|
||||||
return parameter.parameter_type === "output";
|
return parameter.parameter_type === "output";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ImprovePromptForWorkflowResponse = {
|
||||||
|
error: string | null;
|
||||||
|
improved: string;
|
||||||
|
original: string;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user