SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
5.6 KB · 118 lines tsx
Raw Blame History
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