"use client"; import * as React from "react"; import { Star, X } from "lucide-react"; import { ResponsiveDialog } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Input, Textarea, Field, Label } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Segmented } from "@/components/ui/segmented"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import { toast } from "@/components/ui/toast"; import { ModelSelect } from "@/components/presets/model-select"; import { api, useApi } from "@/lib/client/api"; import { errorMessage } from "@/lib/client/humanize"; import { mergeVariables, labelFor, type PromptVariable } from "@/lib/prompts/variables"; import type { PublicPrompt, PromptKind, PublicProject } from "@/lib/client/types"; import { PROMPT_KIND_META, PROMPT_KIND_ORDER } from "./prompt-kind"; import { cn } from "@/lib/utils"; interface Draft { kind: PromptKind; name: string; description: string; content: string; variables: PromptVariable[]; schemaText: string; folder: string; tags: string[]; favorite: boolean; defaultModelKey: string | null; projectId: string | null; } const NONE = "__none__"; const EMPTY: Draft = { kind: "user", name: "", description: "", content: "", variables: [], schemaText: "", folder: "", tags: [], favorite: false, defaultModelKey: null, projectId: null }; function toDraft(p: PublicPrompt): Draft { return { kind: p.kind, name: p.name, description: p.description ?? "", content: p.content, variables: p.variables, schemaText: p.schema ? JSON.stringify(p.schema, null, 2) : "", folder: p.folder ?? "", tags: p.tags, favorite: p.favorite, defaultModelKey: p.defaultModelKey, projectId: p.projectId }; } export interface PromptEditorSheetProps { open: boolean; onOpenChange: (o: boolean) => void; /** Existing prompt to edit; omit for a new one. */ prompt?: PublicPrompt | null; /** Pre-filled values for a new prompt (e.g. `{ projectId }`, or `{ kind: "system", content }` from "Save as prompt"). */ initial?: Partial>; folders?: string[]; onSaved?: (p: PublicPrompt) => void; } /** * Prompt editor: kind, name, description, body with live `{{variable}}` detection (label / default / options / * required per variable), JSON schema for structured prompts, folder, tags, favourite, default model, project. */ export function PromptEditorSheet({ open, onOpenChange, prompt, initial, folders = [], onSaved }: PromptEditorSheetProps) { const [d, setD] = React.useState(EMPTY); const [saving, setSaving] = React.useState(false); const [tagInput, setTagInput] = React.useState(""); const nameRef = React.useRef(null); const { data: projData } = useApi<{ projects: PublicProject[] }>(open ? "/api/projects" : null); const projects = React.useMemo(() => (projData?.projects ?? []).filter((p) => !p.archived), [projData]); React.useEffect(() => { if (!open) return; const base = prompt ? toDraft(prompt) : { ...EMPTY, ...initial, variables: mergeVariables(initial?.content ?? "", []) }; // eslint-disable-next-line react-hooks/set-state-in-effect setD(base); setTagInput(""); const t = setTimeout(() => nameRef.current?.focus(), 80); return () => clearTimeout(t); }, [open, prompt, initial]); const set = (patch: Partial) => setD((s) => ({ ...s, ...patch })); const setContent = (content: string) => setD((s) => ({ ...s, content, variables: mergeVariables(content, s.variables) })); const setVar = (name: string, patch: Partial) => setD((s) => ({ ...s, variables: s.variables.map((v) => (v.name === name ? { ...v, ...patch } : v)) })); const schemaError = React.useMemo(() => { if (d.kind !== "structured" || !d.schemaText.trim()) return null; try { const v = JSON.parse(d.schemaText); return v && typeof v === "object" && !Array.isArray(v) ? null : "Schema must be a JSON object"; } catch (e) { return `Invalid JSON: ${(e as Error).message}`; } }, [d.kind, d.schemaText]); const valid = d.name.trim().length > 0 && d.content.trim().length > 0 && !schemaError; const addTag = (raw: string) => { const tags = raw .split(/[,\n]/) .map((t) => t.trim().toLowerCase()) .filter(Boolean); if (!tags.length) return; set({ tags: Array.from(new Set([...d.tags, ...tags])).slice(0, 12) }); setTagInput(""); }; const save = async () => { if (!valid || saving) return; setSaving(true); const body = { kind: d.kind, name: d.name.trim(), description: d.description.trim() || null, content: d.content, variables: d.variables.map((v) => ({ name: v.name, label: v.label?.trim() || undefined, default: v.default || undefined, required: v.required || undefined, options: v.options?.length ? v.options : undefined })), schema: d.kind === "structured" && d.schemaText.trim() ? (JSON.parse(d.schemaText) as Record) : null, folder: d.folder.trim() || null, tags: d.tags, favorite: d.favorite, defaultModelKey: d.defaultModelKey, projectId: d.projectId, }; try { const res = prompt ? await api<{ prompt: PublicPrompt }>(`/api/prompts/${prompt.id}`, { method: "PATCH", json: body }) : await api<{ prompt: PublicPrompt }>("/api/prompts", { method: "POST", json: body }); toast.success(prompt ? "Prompt updated" : "Prompt saved to your library"); onOpenChange(false); onSaved?.(res.prompt); } catch (e) { toast.error("Could not save prompt", errorMessage(e)); } finally { setSaving(false); } }; const meta = PROMPT_KIND_META[d.kind]; return (
} >
{ e.preventDefault(); void save(); }} > set({ kind: k })} options={PROMPT_KIND_ORDER.map((k) => ({ value: k, label: PROMPT_KIND_META[k].short, icon: PROMPT_KIND_META[k].icon }))} fill className="w-full" /> set({ name: e.target.value })} placeholder={d.kind === "system" ? "Senior code reviewer" : "Summarize an article"} maxLength={120} required className="h-11 sm:h-9" /> set({ description: e.target.value })} placeholder="Reviews diffs for correctness, then style" maxLength={600} className="h-11 sm:h-9" />