add some UI for prompt improval [sic] (#3974)

This commit is contained in:
Jonathan Dobson
2025-11-11 21:10:02 -05:00
committed by GitHub
parent 6f5d721c37
commit 2765382bef
5 changed files with 277 additions and 35 deletions

View 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 };

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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 });

View File

@@ -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;
};