"use client"; import { useCallback, useState } from "react"; import { FileText } from "lucide-react"; import { useToast } from "@/components/Toast"; import { Button } from "@/components/ui/Button"; import { EmptyState } from "@/components/ui/EmptyState"; import { Field } from "@/components/ui/Field"; import { SectionHeader } from "@/components/ui/SectionHeader"; import { Select } from "@/components/ui/Select"; import { TextArea } from "@/components/ui/TextArea"; import { TextInput } from "@/components/ui/TextInput"; import { api } from "@/lib/api/client"; import type { TemplateResponse, ToolDescriptorView } from "@/lib/api/types"; import { buildTemplateCreateRequest, EMPTY_TEMPLATE_DRAFT, isTemplateDraftValid, type TemplateDraft, } from "@/lib/templates/templates"; import { toolKeyOptions } from "@/lib/toolbox/toolbox"; import { cardClass } from "@/lib/ui/variants"; interface TemplatesManagerProps { // 初始模板列表(Server Component 预取;失败给空数组 + loadError 文案)。 initial: TemplateResponse[]; // 工具描述符(供「关联工具」下拉:展示中文标题、值用 key)。空 → 退化为「不关联」单选。 tools: ToolDescriptorView[]; } // 提示词/模板库管理(F3,单用户本地版):列出 / 新建 / 删除。 // 列表本地维护(乐观更新失败回滚);CRUD 走 OpenAPI 客户端。分享/市场不做(需多租户)。 export function TemplatesManager({ initial, tools }: TemplatesManagerProps) { const toast = useToast(); const [templates, setTemplates] = useState(initial); const [draft, setDraft] = useState(EMPTY_TEMPLATE_DRAFT); const [creating, setCreating] = useState(false); const toolOptions = toolKeyOptions(tools); const setField = useCallback( (field: keyof TemplateDraft, value: string): void => { setDraft((prev) => ({ ...prev, [field]: value })); }, [], ); const onCreate = useCallback(async (): Promise => { if (!isTemplateDraftValid(draft)) { toast("请填写标题与正文。", "error"); return; } setCreating(true); try { const { data, error } = await api.POST("/templates", { body: buildTemplateCreateRequest(draft), }); if (error || !data) { toast("新建模板失败,请重试。", "error"); return; } setTemplates((prev) => [data, ...prev]); setDraft(EMPTY_TEMPLATE_DRAFT); toast("已新建模板。", "success"); } catch { toast("新建模板请求异常,请检查网络。", "error"); } finally { setCreating(false); } }, [draft, toast]); // 撤销删除:以原模板内容重新入库(后端无恢复接口,新建即可;新 id 不影响用途)。 const restore = useCallback( async (removed: TemplateResponse): Promise => { try { const { data, error } = await api.POST("/templates", { body: { title: removed.title, body: removed.body, category: removed.category ?? null, tool_key: removed.tool_key ?? null, }, }); if (error || !data) { toast("撤销失败,请重新新建。", "error"); return; } setTemplates((cur) => [data, ...cur]); } catch { toast("撤销请求异常,请检查网络。", "error"); } }, [toast], ); const onDelete = useCallback( async (removed: TemplateResponse): Promise => { const prev = templates; // 乐观删除:先移除,失败回滚。 setTemplates((cur) => cur.filter((t) => t.id !== removed.id)); try { const { error } = await api.DELETE("/templates/{template_id}", { params: { path: { template_id: removed.id } }, }); if (error) { setTemplates(prev); toast("删除模板失败,请重试。", "error"); return; } // 成功后给「撤销」action(第三参):误删可一键以原内容重新入库。 toast(`已删除模板「${removed.title}」。`, "success", { action: { label: "撤销", onClick: () => void restore(removed) }, }); } catch { setTemplates(prev); toast("删除模板请求异常,请检查网络。", "error"); } }, [templates, toast, restore], ); return (
{ e.preventDefault(); void onCreate(); }} > setField("title", e.target.value)} aria-label="标题" />