import capitalize from "lodash/capitalize"; import router from "next/router"; import { useState, useEffect, useRef } from "react"; import { useForm } from "react-hook-form"; import { Button } from "@/src/components/ui/button"; import { Checkbox } from "@/src/components/ui/checkbox"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/src/components/ui/form"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/src/components/ui/tabs"; import { Textarea } from "@/src/components/ui/textarea"; import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL"; import { api } from "@/src/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { type CreatePromptTRPCType, PRODUCTION_LABEL, type Prompt, PromptType, extractVariables, getIsCharOrUnderscore, } from "@langfuse/shared"; import { PromptChatMessages } from "./PromptChatMessages"; import { ReviewPromptDialog } from "./ReviewPromptDialog"; import { NewPromptFormSchema, type NewPromptFormSchemaType, PromptVariantSchema, type PromptVariant, } from "./validation"; import { Input } from "@/src/components/ui/input"; import Link from "next/link"; import { SquareArrowOutUpRight } from "lucide-react"; import { PromptVariableListPreview } from "@/src/features/prompts/components/PromptVariableListPreview"; import { CodeMirrorEditor } from "@/src/components/editor/CodeMirrorEditor"; import { PromptLinkingEditor } from "@/src/components/editor/PromptLinkingEditor"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import usePlaygroundCache from "@/src/features/playground/page/hooks/usePlaygroundCache"; import { useQueryParam } from "use-query-params"; import { usePromptNameValidation } from "@/src/features/prompts/hooks/usePromptNameValidation"; import { useFormPersistence } from "@/src/hooks/useFormPersistence"; type NewPromptFormProps = { initialPrompt?: Prompt | null; onFormSuccess?: () => void; }; export const NewPromptForm: React.FC = (props) => { const { onFormSuccess, initialPrompt } = props; const projectId = useProjectIdFromURL(); const [shouldLoadPlaygroundCache] = useQueryParam("loadPlaygroundCache"); const [folderPath] = useQueryParam("folder"); const [formError, setFormError] = useState(null); const { playgroundCache } = usePlaygroundCache(); const [initialMessages, setInitialMessages] = useState([]); const utils = api.useUtils(); const capture = usePostHogClientCapture(); let initialPromptVariant: PromptVariant | null; try { initialPromptVariant = PromptVariantSchema.parse({ type: initialPrompt?.type, prompt: initialPrompt?.prompt?.valueOf(), }); } catch (_err) { initialPromptVariant = null; } const defaultValues = { type: initialPromptVariant?.type ?? PromptType.Text, chatPrompt: initialPromptVariant?.type === PromptType.Chat ? initialPromptVariant?.prompt : [], textPrompt: initialPromptVariant?.type === PromptType.Text ? initialPromptVariant?.prompt : "", name: initialPrompt?.name ?? (folderPath ? `${folderPath}/` : ""), config: JSON.stringify(initialPrompt?.config?.valueOf(), null, 2) || "{}", isActive: !Boolean(initialPrompt), commitMessage: undefined, }; const form = useForm({ resolver: zodResolver(NewPromptFormSchema), mode: "onTouched", defaultValues, }); const currentName = form.watch("name"); const currentType = form.watch("type"); const currentExtractedVariables = extractVariables( currentType === PromptType.Text ? form.watch("textPrompt") : JSON.stringify(form.watch("chatPrompt"), null, 2), ).filter(getIsCharOrUnderscore); const createPromptMutation = api.prompts.create.useMutation({ onSuccess: () => utils.prompts.invalidate(), onError: (error) => setFormError(error.message), }); const allPrompts = api.prompts.filterOptions.useQuery( { projectId: projectId as string, // Typecast as query is enabled only when projectId is present }, { enabled: Boolean(projectId), refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: Infinity, }, ).data?.name; function onSubmit(values: NewPromptFormSchemaType) { capture( initialPrompt ? "prompts:update_form_submit" : "prompts:new_form_submit", { type: values.type, active: values.isActive, hasConfig: values.config !== "{}", countVariables: currentExtractedVariables.length, }, ); if (!projectId) throw Error("Project ID is not defined."); const { type, textPrompt, chatPrompt } = values; // TS does not narrow down type of 'prompt' property given the type of 'type' property in ternary operator let newPrompt: CreatePromptTRPCType; if (type === PromptType.Chat) { newPrompt = { ...values, projectId, type, prompt: chatPrompt, config: JSON.parse(values.config), labels: values.isActive ? [PRODUCTION_LABEL] : [], }; } else { newPrompt = { ...values, projectId, type, prompt: textPrompt, config: JSON.parse(values.config), labels: values.isActive ? [PRODUCTION_LABEL] : [], }; } createPromptMutation .mutateAsync(newPrompt) .then((newPrompt) => { clearDraft(); onFormSuccess?.(); form.reset(); if ("name" in newPrompt) { void router.push( `/project/${projectId}/prompts/${encodeURIComponent(newPrompt.name)}`, ); } }) .catch((error) => { console.error(error); }); } const hasInitializedMessages = useRef(false); useEffect(() => { if (hasInitializedMessages.current) return; hasInitializedMessages.current = true; if (shouldLoadPlaygroundCache && playgroundCache) { form.setValue("type", PromptType.Chat); setInitialMessages(playgroundCache.messages); } else if (initialPrompt?.type === PromptType.Chat) { setInitialMessages(initialPrompt.prompt); } }, [playgroundCache, initialPrompt, form, shouldLoadPlaygroundCache]); usePromptNameValidation({ currentName, allPrompts, form, }); const formId = initialPrompt ? `prompt-edit:${initialPrompt.id}` : "prompt-new"; const { hadDraft, clearDraft } = useFormPersistence({ formId, projectId: projectId ?? "", form, enabled: Boolean(projectId) && !shouldLoadPlaygroundCache, onDraftRestored: (draft) => { // Restore chat messages if present if ( draft.chatPrompt && Array.isArray(draft.chatPrompt) && draft.chatPrompt.length > 0 ) { setInitialMessages(draft.chatPrompt); } }, }); return (
{/* Prompt name field - text vs. chat only for new prompts */} {!initialPrompt ? ( { const errorMessage = form.getFieldState("name").error?.message; return (
Name Use slashes '/' in prompt names to organize them into{" "} folders . {/* Custom form message to include a link to the already existing prompt */} {form.getFieldState("name").error ? (

{errorMessage}

{errorMessage?.includes("already exist") ? ( Create a new version for it here. ) : null}
) : null}
); }} /> ) : null} {/* Prompt content field - text vs. chat */} <> Prompt Define your prompt template. You can use{" "} {"{{variable}}"} to insert variables into your prompt. Note: Variables must be alphabetical characters or underscores. You can also link other text prompts using the plus button. { form.setValue("type", e as PromptType); }} > {!initialPrompt ? ( {capitalize(PromptType.Text)} {capitalize(PromptType.Chat)} ) : null} {hadDraft && (

Draft restored.{" "}

)} ( <> )} /> ( <> )} />
{/* Prompt Config field */} ( Config Arbitrary JSON configuration that is available on the prompt. Use this to track LLM parameters, function definitions, or any other metadata. )} /> {/* Activate prompt field */} (
Set the "production" label
)} /> ( Commit message Provide information about the changes made in this version. Helps maintain a clear history of prompt iterations.