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 { ArrowDownToLine, Check, History, MessageSquare, Trash2, WandSparkles } from "lucide-react";5import { Button } from "@/components/ui/button";6import { toast } from "@/components/ui/toast";7import { ConfirmDialog } from "@/components/common/confirm-dialog";8import { RowMenu } from "@/components/projects/row-menu";9import { api, useApi } from "@/lib/client/api";10import { errorMessage } from "@/lib/client/humanize";11import { truncate } from "@/lib/utils";12import type { PromptPreset, PublicPrompt } from "@/lib/client/types";13import type { ActionSheetItem } from "@/components/ui/sheet";1415interface PresetsResponse {16 promptPresets: PromptPreset[];17}1819/**20 * Legacy `prompt_presets` (pre-library system prompts). They still work in chat via `?prompt=<id>`;21 * "Import to library" copies one into the new prompt library as a `system` prompt.22 */23export function LegacyPresets({ onImported }: { onImported?: (p: PublicPrompt) => void }) {24 const router = useRouter();25 const q = useApi<PresetsResponse>("/api/presets");26 const [imported, setImported] = React.useState<Set<string>>(new Set());27 const [busy, setBusy] = React.useState<string | null>(null);28 const [deleting, setDeleting] = React.useState<PromptPreset | null>(null);29 const presets = React.useMemo(() => q.data?.promptPresets ?? [], [q.data]);3031 if (q.isLoading || !presets.length) return null;3233 const importOne = async (p: PromptPreset) => {34 setBusy(p.id);35 try {36 const res = await api<{ prompt: PublicPrompt }>("/api/prompts", {37 method: "POST",38 json: { kind: "system", name: p.name, description: p.description ?? null, content: p.systemPrompt, defaultModelKey: p.defaultModelKey ?? null, tags: ["imported"], folder: "Imported presets" },39 });40 setImported((s) => new Set(s).add(p.id));41 toast.success(`Imported “${p.name}”`, "Find it in the library above. The legacy preset is unchanged.");42 onImported?.(res.prompt);43 } catch (e) {44 toast.error("Could not import", errorMessage(e));45 } finally {46 setBusy(null);47 }48 };49 const remove = async (p: PromptPreset) => {50 try {51 await api(`/api/presets?kind=prompt&id=${p.id}`, { method: "DELETE" });52 await q.mutate();53 toast.success("Legacy preset deleted");54 } catch (e) {55 toast.error("Could not delete", errorMessage(e));56 throw e;57 }58 };59 const items = (p: PromptPreset): (ActionSheetItem | "separator")[] => [60 { key: "import", label: imported.has(p.id) ? "Import again" : "Import to library", icon: <ArrowDownToLine />, onSelect: () => void importOne(p) },61 { key: "use", label: "Use in chat (legacy)", icon: <MessageSquare />, onSelect: () => router.push(`/app/chat?prompt=${p.id}`) },62 "separator",63 { key: "delete", label: "Delete legacy preset", icon: <Trash2 />, destructive: true, onSelect: () => setDeleting(p) },64 ];6566 return (67 <section className="mt-10" aria-labelledby="legacy-presets">68 <div className="flex items-end justify-between gap-3">69 <div className="min-w-0">70 <h2 id="legacy-presets" className="flex items-center gap-1.5 text-[15px] font-semibold tracking-tight">71 <History className="size-4 text-fg-subtle" /> Legacy presets72 </h2>73 <p className="mt-0.5 text-[13px] text-fg-muted">System prompts saved before the library existed. They keep working in chat; import them to get variables, folders and tags.</p>74 </div>75 {presets.length > 1 ? (76 <Button77 variant="outline"78 size="sm"79 className="shrink-0"80 loading={busy === "*"}81 onClick={async () => {82 setBusy("*");83 for (const p of presets) if (!imported.has(p.id)) await importOne(p);84 setBusy(null);85 }}86 >87 <ArrowDownToLine /> Import all88 </Button>89 ) : null}90 </div>91 <ul className="mt-3 divide-y divide-hairline rounded-xl border border-border bg-bg-elevated">92 {presets.map((p) => (93 <li key={p.id} className="group flex min-h-[56px] items-center gap-3 px-3 py-2">94 <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-bg-muted text-[16px] leading-none text-fg-muted" aria-hidden>95 {p.icon || <WandSparkles className="size-4" />}96 </span>97 <div className="min-w-0 flex-1">98 <p className="truncate text-[14px] font-medium leading-5">{p.name}</p>99 <p className="truncate text-[12px] leading-4 text-fg-subtle">{p.description || truncate(p.systemPrompt, 90)}</p>100 </div>101 {imported.has(p.id) ? (102 <span className="hidden items-center gap-1 text-[12px] text-success sm:inline-flex">103 <Check className="size-3.5" /> Imported104 </span>105 ) : (106 <Button size="sm" variant="secondary" className="hidden sm:inline-flex" loading={busy === p.id} onClick={() => importOne(p)}>107 <ArrowDownToLine /> Import108 </Button>109 )}110 <RowMenu items={items(p)} title={p.name} alwaysVisible />111 </li>112 ))}113 </ul>114 <ConfirmDialog open={deleting !== null} onOpenChange={(o) => !o && setDeleting(null)} destructive title={`Delete legacy preset “${deleting?.name ?? ""}”?`} description="Conversations that used it are not affected. Import it first if you want to keep it in the library." confirmLabel="Delete" onConfirm={() => (deleting ? remove(deleting) : Promise.resolve())} />115 </section>116 );117}118