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 { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { ImprovePrompt } from "./ImprovePrompt";
|
||||
|
||||
interface AiImprove {
|
||||
context?: string;
|
||||
useCase: string;
|
||||
}
|
||||
|
||||
type Props = Omit<
|
||||
React.ComponentProps<typeof AutoResizingTextarea>,
|
||||
"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<HTMLTextAreaElement>(null);
|
||||
const [cursorPosition, setCursorPosition] = useState<{
|
||||
@@ -71,6 +85,12 @@ function WorkflowBlockInputTextarea(props: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (value: string) => {
|
||||
setInternalValue(value);
|
||||
handleTextareaSelect();
|
||||
doOnChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<AutoResizingTextarea
|
||||
@@ -81,29 +101,42 @@ function WorkflowBlockInputTextarea(props: Props) {
|
||||
doOnChange.flush();
|
||||
}}
|
||||
onChange={(event) => {
|
||||
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)}
|
||||
/>
|
||||
<div className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center">
|
||||
<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}
|
||||
|
||||
<div className="absolute right-1 top-0 flex size-9 items-center justify-end">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{aiImprove && (
|
||||
<ImprovePrompt
|
||||
context={aiImprove.context}
|
||||
isVisible={Boolean(internalValue.trim())}
|
||||
size="small"
|
||||
prompt={internalValue}
|
||||
onImprove={(prompt) => handleOnChange(prompt)}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [extraHttpHeaders, setExtraHttpHeaders] = useState<string | null>(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?
|
||||
</span>
|
||||
<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
|
||||
className="min-h-0 resize-none rounded-xl border-transparent px-4 hover:border-transparent focus-visible:ring-0"
|
||||
value={prompt}
|
||||
@@ -312,6 +322,19 @@ function PromptBox() {
|
||||
</CustomSelectItem>
|
||||
</SelectContent>
|
||||
</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">
|
||||
<GearIcon
|
||||
className="size-6 cursor-pointer"
|
||||
@@ -320,25 +343,23 @@ function PromptBox() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={cn("flex items-center", {
|
||||
"pointer-events-none opacity-20": !prompt.trim(),
|
||||
})}
|
||||
>
|
||||
{generateWorkflowMutation.isPending ? (
|
||||
<ReloadIcon className="size-6 animate-spin" />
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
{generateWorkflowMutation.isPending ? (
|
||||
<ReloadIcon className="size-6 animate-spin" />
|
||||
) : (
|
||||
<PaperPlaneIcon
|
||||
className="size-6 cursor-pointer"
|
||||
onClick={async () => {
|
||||
generateWorkflowMutation.mutate({
|
||||
prompt,
|
||||
version: selectValue,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<PaperPlaneIcon
|
||||
className="size-6 cursor-pointer"
|
||||
onClick={async () => {
|
||||
generateWorkflowMutation.mutate({
|
||||
prompt,
|
||||
version: selectValue,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,6 +107,7 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
) : null}
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
aiImprove={{ useCase: "task_v2_prompt" }}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
update({ prompt: value });
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user