TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { Copy, MessageSquarePlus } from "lucide-react";5import { ResponsiveDialog } from "@/components/ui/sheet";6import { Button } from "@/components/ui/button";7import { Input, Textarea, Field } from "@/components/ui/input";8import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";9import { toast } from "@/components/ui/toast";10import { api } from "@/lib/client/api";11import { errorMessage } from "@/lib/client/humanize";12import { useCopy } from "@/lib/client/hooks";13import { labelFor, renderTemplate, variablesToAsk, type PromptVariable } from "@/lib/prompts/variables";14import type { PublicPrompt, UsePromptResult } from "@/lib/client/types";15import { dispatchPromptInsert, promptChatHref, toInsertDetail } from "./insert";1617/** Records the use, renders with `variables`, dispatches the insert event and navigates to the chat. */18export async function insertPromptIntoChat(prompt: Pick<PublicPrompt, "id" | "name">, variables: Record<string, string>, router: { push: (href: string) => void }) {19 const res = await api<UsePromptResult>(`/api/prompts/${prompt.id}/use`, { method: "POST", json: { variables } });20 dispatchPromptInsert(toInsertDetail(res, variables));21 router.push(promptChatHref(prompt.id, variables));22 toast.success(res.kind === "system" ? `System prompt “${prompt.name}” applied` : `“${prompt.name}” inserted`, res.missing.length ? `Left empty: ${res.missing.join(", ")}` : undefined);23 return res;24}2526/**27 * Fill-in sheet for `{{variables}}`: one input (or select when `options` exist) per variable, live preview,28 * then "Insert into chat". Prompts without variables to ask should call `insertPromptIntoChat` directly.29 */30export function PromptUseSheet({ prompt, open, onOpenChange }: { prompt: PublicPrompt | null; open: boolean; onOpenChange: (o: boolean) => void }) {31 const router = useRouter();32 const [values, setValues] = React.useState<Record<string, string>>({});33 const [busy, setBusy] = React.useState(false);34 const [copied, copy] = useCopy();35 const firstRef = React.useRef<HTMLInputElement & HTMLTextAreaElement>(null);3637 const vars: PromptVariable[] = React.useMemo(() => prompt?.variables ?? [], [prompt]);38 const ask = React.useMemo(() => variablesToAsk(vars), [vars]);3940 React.useEffect(() => {41 if (!open || !prompt) return;42 // eslint-disable-next-line react-hooks/set-state-in-effect43 setValues(Object.fromEntries(vars.map((v) => [v.name, v.default ?? ""])));44 const t = setTimeout(() => firstRef.current?.focus(), 80);45 return () => clearTimeout(t);46 }, [open, prompt, vars]);4748 const preview = React.useMemo(() => (prompt ? renderTemplate(prompt.content, values, vars) : null), [prompt, values, vars]);49 const missingRequired = ask.filter((v) => v.required && !values[v.name]?.trim()).map((v) => labelFor(v));5051 const submit = async () => {52 if (!prompt || busy) return;53 setBusy(true);54 try {55 await insertPromptIntoChat(prompt, values, router);56 onOpenChange(false);57 } catch (e) {58 toast.error("Could not use prompt", errorMessage(e));59 } finally {60 setBusy(false);61 }62 };6364 return (65 <ResponsiveDialog66 open={open && prompt !== null}67 onOpenChange={onOpenChange}68 title={prompt?.name ?? "Use prompt"}69 description={ask.length ? `Fill in ${ask.length} variable${ask.length === 1 ? "" : "s"} before inserting.` : "Review and insert into a new chat."}70 size="lg"71 snap="full"72 footer={73 <div className="flex gap-2 sm:justify-end">74 <Button variant="ghost" className="sm:flex-none" onClick={() => preview && copy(preview.text)} disabled={!preview}>75 <Copy /> {copied ? "Copied" : "Copy"}76 </Button>77 <div className="flex-1 sm:hidden" />78 <Button className="flex-1 sm:flex-none" loading={busy} disabled={missingRequired.length > 0} onClick={submit}>79 <MessageSquarePlus /> Insert into chat80 </Button>81 </div>82 }83 >84 <form85 className="space-y-4 pt-1"86 onSubmit={(e) => {87 e.preventDefault();88 void submit();89 }}90 >91 {vars.length ? (92 <div className="grid gap-3 sm:grid-cols-2">93 {vars.map((v, i) => {94 const id = `var-${v.name}`;95 const label = labelFor(v);96 const val = values[v.name] ?? "";97 return (98 <Field key={v.name} label={`${label}${v.required ? " *" : ""}`} htmlFor={id} hint={v.default && val === v.default ? "Using the default" : undefined} className={v.options?.length ? "" : val.length > 60 ? "sm:col-span-2" : ""}>99 {v.options?.length ? (100 <Select value={val || undefined} onValueChange={(x) => setValues((s) => ({ ...s, [v.name]: x }))}>101 <SelectTrigger id={id} className="h-11 sm:h-9">102 <SelectValue placeholder={`Choose ${label.toLowerCase()}`} />103 </SelectTrigger>104 <SelectContent>105 {v.options.map((o) => (106 <SelectItem key={o} value={o}>107 {o}108 </SelectItem>109 ))}110 </SelectContent>111 </Select>112 ) : val.length > 60 || val.includes("\n") ? (113 <Textarea ref={i === 0 ? firstRef : undefined} id={id} value={val} onChange={(e) => setValues((s) => ({ ...s, [v.name]: e.target.value }))} rows={3} placeholder={v.default ? `Default: ${v.default}` : `{{${v.name}}}`} />114 ) : (115 <Input ref={i === 0 ? firstRef : undefined} id={id} value={val} onChange={(e) => setValues((s) => ({ ...s, [v.name]: e.target.value }))} placeholder={v.default ? `Default: ${v.default}` : `{{${v.name}}}`} className="h-11 sm:h-9" />116 )}117 </Field>118 );119 })}120 </div>121 ) : null}122 {missingRequired.length ? <p className="text-[12px] text-warning">Required: {missingRequired.join(", ")}</p> : null}123 <Field label="Preview" hint={preview?.missing.length ? `Empty: ${preview.missing.join(", ")}` : undefined}>124 <pre className="max-h-[40dvh] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-border bg-bg-subtle p-3 font-mono text-[12.5px] leading-5 text-fg-muted">{preview?.text || " "}</pre>125 </Field>126 </form>127 </ResponsiveDialog>128 );129}130