"use client"; import * as React from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { Folder, Plus, Search, Star, WandSparkles, X } from "lucide-react"; import { api, useApi } from "@/lib/client/api"; import { errorMessage } from "@/lib/client/humanize"; import { useDebounced } from "@/lib/client/hooks"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Segmented, ChipRow } from "@/components/ui/segmented"; import { EmptyState, Skeleton } from "@/components/ui/misc"; import { toast } from "@/components/ui/toast"; import { ConfirmDialog } from "@/components/common/confirm-dialog"; import type { PublicPrompt, PromptKind } from "@/lib/client/types"; import { variablesToAsk } from "@/lib/prompts/variables"; import { WorkspacePageHeader, WorkspacePageBody } from "@/components/projects/page-header"; import { PromptCard } from "./prompt-card"; import { PromptEditorSheet } from "./prompt-editor-sheet"; import { PromptUseSheet, insertPromptIntoChat } from "./prompt-use-sheet"; import { LegacyPresets } from "./legacy-presets"; import { PROMPT_KIND_META, PROMPT_KIND_ORDER } from "./prompt-kind"; import { cn } from "@/lib/utils"; interface ListResponse { prompts: PublicPrompt[]; folders: string[]; tags: string[]; total: number; } type KindFilter = PromptKind | "all"; export function PromptLibrary() { const router = useRouter(); const params = useSearchParams(); const [q, setQ] = React.useState(""); const dq = useDebounced(q, 150); const [kind, setKind] = React.useState("all"); const [folder, setFolder] = React.useState(null); const [tag, setTag] = React.useState(null); const [favOnly, setFavOnly] = React.useState(false); const [editor, setEditor] = React.useState<{ open: boolean; prompt: PublicPrompt | null; initial?: { kind?: PromptKind; content?: string; name?: string } }>({ open: false, prompt: null }); const [using, setUsing] = React.useState(null); const [deleting, setDeleting] = React.useState(null); const key = React.useMemo(() => { const p = new URLSearchParams(); if (dq.trim()) p.set("q", dq.trim()); if (kind !== "all") p.set("kind", kind); if (folder) p.set("folder", folder); if (tag) p.set("tag", tag); if (favOnly) p.set("favorite", "1"); const s = p.toString(); return `/api/prompts${s ? `?${s}` : ""}`; }, [dq, kind, folder, tag, favOnly]); const { data, isLoading, error, mutate } = useApi(key, { keepPreviousData: true }); const prompts = React.useMemo(() => data?.prompts ?? [], [data]); const total = data?.total ?? 0; const filtered = kind !== "all" || Boolean(folder) || Boolean(tag) || favOnly || Boolean(dq.trim()); // `?new=1` / `?edit=new` open a blank editor; `?edit=` edits; `?content=` prefills (e.g. "Save as prompt" from chat). const editParam = params.get("edit") ?? (params.get("new") === "1" ? "new" : null); const contentParam = params.get("content"); const kindParam = params.get("kind"); const handled = React.useRef(false); React.useEffect(() => { if (handled.current || (!editParam && !contentParam)) return; if (editParam && editParam !== "new" && !data) return; // wait for the list handled.current = true; const target = editParam && editParam !== "new" ? prompts.find((p) => p.id === editParam) ?? null : null; // eslint-disable-next-line react-hooks/set-state-in-effect setEditor({ open: true, prompt: target, initial: target ? undefined : { content: contentParam ?? undefined, kind: (PROMPT_KIND_ORDER as string[]).includes(kindParam ?? "") ? (kindParam as PromptKind) : undefined } }); router.replace("/app/prompts"); }, [editParam, contentParam, kindParam, data, prompts, router]); const refresh = () => Promise.all([mutate()]); const use = (p: PublicPrompt) => { if (variablesToAsk(p.variables).length) setUsing(p); else void insertPromptIntoChat(p, {}, router).catch((e) => toast.error("Could not use prompt", errorMessage(e))); }; const toggleFavorite = async (p: PublicPrompt) => { try { await api(`/api/prompts/${p.id}`, { method: "PATCH", json: { favorite: !p.favorite } }); await refresh(); } catch (e) { toast.error("Could not update", errorMessage(e)); } }; const duplicate = async (p: PublicPrompt) => { try { await api("/api/prompts", { method: "POST", json: { kind: p.kind, name: `${p.name} (copy)`, description: p.description, content: p.content, variables: p.variables, schema: p.schema, folder: p.folder, tags: p.tags, defaultModelKey: p.defaultModelKey, projectId: p.projectId } }); await refresh(); toast.success("Prompt duplicated"); } catch (e) { toast.error("Could not duplicate", errorMessage(e)); } }; const remove = async (p: PublicPrompt) => { try { await api(`/api/prompts/${p.id}`, { method: "DELETE" }); await refresh(); toast.success("Prompt deleted"); } catch (e) { toast.error("Could not delete", errorMessage(e)); throw e; } }; const actions = { onUse: use, onEdit: (p: PublicPrompt) => setEditor({ open: true, prompt: p }), onDuplicate: duplicate, onToggleFavorite: toggleFavorite, onDelete: setDeleting }; const clearFilters = () => { setKind("all"); setFolder(null); setTag(null); setFavOnly(false); setQ(""); }; return (
setEditor({ open: true, prompt: null })}> New prompt New } /> {total > 0 || filtered ? (
setQ(e.target.value)} placeholder="Search name, content, tags…" className="h-10 pl-8 sm:h-9" aria-label="Search prompts" />
({ value: k as KindFilter, label: PROMPT_KIND_META[k].short, icon: PROMPT_KIND_META[k].icon }))]} />
{(data?.folders.length ?? 0) + (data?.tags.length ?? 0) > 0 || favOnly ? (
{data?.folders.length ? setFolder(folder === f ? null : f)} options={data.folders.map((f) => ({ value: f, label: f, icon: }))} className="shrink-0 overflow-visible" /> : null} {data?.tags.length ? setTag(tag === t ? null : t)} options={data.tags.slice(0, 12).map((t) => ({ value: t, label: `#${t}` }))} className="shrink-0 overflow-visible" /> : null} {filtered ? ( ) : null}
) : null}
) : null}
0 || filtered) && "mt-4")}> {isLoading && !data ? (
    {Array.from({ length: 6 }).map((_, i) => (
  • ))}
) : error ? ( mutate()}>Retry} /> ) : total === 0 && !filtered ? ( } title="No prompts yet" description="Save the prompts you keep retyping — a code reviewer, a translator, a strict JSON extractor. Add {{variables}} to fill in before sending." action={ } /> ) : prompts.length === 0 ? ( Clear filters} /> ) : (
    {prompts.map((p) => ( ))}
)}
refresh()} />
setEditor((s) => ({ ...s, open: o }))} prompt={editor.prompt} initial={editor.initial} folders={data?.folders} onSaved={() => refresh()} /> !o && setUsing(null)} /> !o && setDeleting(null)} destructive title={`Delete “${deleting?.name ?? ""}”?`} description="Conversations that used this prompt are not affected." confirmLabel="Delete" onConfirm={() => (deleting ? remove(deleting) : Promise.resolve())} />
); }