import { type PromptComposerSubmitOptions } from "@agent-native/core/client/composer"; import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { ShareButton } from "@agent-native/core/client/sharing"; import { useSetHeaderActions, useSetPageTitle, } from "@agent-native/toolkit/app-shell"; import { derivePromptTitle } from "@shared/prompt-title"; import { IconDots, IconLock, IconSearch, IconTemplate, IconTrash, } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router"; import { toast } from "sonner"; import PromptPopover from "@/components/editor/PromptDialog"; import type { UploadedFile } from "@/components/editor/PromptDialog"; import { QueryErrorState } from "@/components/QueryErrorState"; import { TemplatePreview } from "@/components/templates/TemplatePreview"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { useDesignSystems } from "@/hooks/use-design-systems"; import { writePendingGeneration } from "@/lib/pending-generation"; type TemplateCategory = | "ad" | "one-pager" | "landing-page" | "social" | "presentation" | "other"; interface DesignTemplateSummary { id: string; title: string; description?: string | null; category: TemplateCategory; width?: number | null; height?: number | null; lockedLayerCount: number; designSystemId?: string | null; visibility: "private" | "org" | "public"; isOwner: boolean; isBuiltIn: boolean; source: "starter" | "saved"; previewHtml?: string | null; } export default function Templates() { const t = useT(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); const [search, setSearch] = useState(""); const [selected, setSelected] = useState(null); const [promptOpen, setPromptOpen] = useState(false); const [creating, setCreating] = useState(false); const [selectedDesignSystemId, setSelectedDesignSystemId] = useState< string | null | undefined >(undefined); const [deleteTemplate, setDeleteTemplate] = useState(null); const anchorElRef = useRef(null); const handledTemplateIdRef = useRef(null); const anchorRef = useRef(null); anchorRef.current = anchorElRef.current; const { data, isLoading, isError, isFetching, refetch } = useActionQuery( "list-design-templates", { includePreview: "true", }, ); const createMutation = useActionMutation("create-design-from-template"); const deleteMutation = useActionMutation("delete-design-template"); const { designSystems, defaultSystem, isLoading: designSystemsLoading, } = useDesignSystems(); const templates = data?.templates ?? []; const linkedTemplateId = searchParams.get("templateId"); const filtered = useMemo(() => { const query = search.trim().toLowerCase(); return query ? templates.filter( (template) => template.title.toLowerCase().includes(query) || template.description?.toLowerCase().includes(query) || template.category.includes(query), ) : templates; }, [search, templates]); const builtIns = filtered.filter((template) => template.isBuiltIn); const userTemplates = filtered.filter((template) => !template.isBuiltIn); const resolveTemplateDesignSystemId = ( template: DesignTemplateSummary, ): string | null => { if ( template.designSystemId && designSystems.some((system) => system.id === template.designSystemId) ) { return template.designSystemId; } return defaultSystem?.id ?? designSystems[0]?.id ?? null; }; const setSelectedTemplateParam = (templateId: string | null) => { const next = new URLSearchParams(searchParams); if (templateId) next.set("templateId", templateId); else next.delete("templateId"); setSearchParams(next, { replace: true }); }; useEffect(() => { if ( !linkedTemplateId || handledTemplateIdRef.current === linkedTemplateId ) { return; } const template = templates.find( (candidate) => candidate.id === linkedTemplateId, ); if (!template || designSystemsLoading) return; const card = document.getElementById(`design-template-${linkedTemplateId}`); const useButton = card?.querySelector( "[data-template-use-button]", ); anchorElRef.current = useButton ?? card; handledTemplateIdRef.current = linkedTemplateId; setSearch(""); setSelected(template); setSelectedDesignSystemId(resolveTemplateDesignSystemId(template)); setPromptOpen(true); card?.scrollIntoView({ block: "center", behavior: "smooth" }); useButton?.focus(); }, [designSystemsLoading, linkedTemplateId, templates]); const openTemplatePrompt = ( template: DesignTemplateSummary, element: HTMLElement, ) => { anchorElRef.current = element; handledTemplateIdRef.current = template.id; setSelectedTemplateParam(template.id); setSelected(template); setSelectedDesignSystemId(resolveTemplateDesignSystemId(template)); setPromptOpen(true); }; const createFromTemplate = async ( template: DesignTemplateSummary, prompt?: string, options: PromptComposerSubmitOptions = {}, ) => { setCreating(true); try { const trimmedPrompt = prompt?.trim() ?? ""; const title = trimmedPrompt ? derivePromptTitle(trimmedPrompt) : template.title; const designSystemId = selectedDesignSystemId === undefined ? resolveTemplateDesignSystemId(template) : selectedDesignSystemId; const result = (await createMutation.mutateAsync({ templateId: template.id, title, designSystemId, ...(trimmedPrompt ? { prompt: trimmedPrompt } : {}), })) as { id: string; title?: string; designSystemId?: string | null; adaptationPending?: boolean; templateBaselineFiles?: Array<{ id: string; contentHash: string }>; }; if (!result.id) throw new Error("Template copy did not return a design ID"); if (result.adaptationPending) { const effectiveDesignSystemId = result.designSystemId ?? null; const effectiveSystemTitle = designSystems.find((system) => system.id === effectiveDesignSystemId) ?.title ?? t("promptDialog.designSystem"); writePendingGeneration(result.id, { prompt: trimmedPrompt || t("promptDialog.reskinTemplatePrompt", { title: template.title, system: effectiveSystemTitle, }), title: result.title ?? title, source: template.title, templateId: template.id, templateBaselineFiles: result.templateBaselineFiles, designSystemId: effectiveDesignSystemId, skipQuestions: true, ...options, }); } void queryClient .invalidateQueries({ queryKey: ["action", "list-designs"], }) .catch(() => {}); navigate(`/design/${result.id}`); } catch (error) { setCreating(false); throw error; } }; const handleSubmit = ( prompt: string, _files: UploadedFile[], options: PromptComposerSubmitOptions, ) => { if (!selected) return; return createFromTemplate(selected, prompt, options); }; const handleDelete = async () => { if (!deleteTemplate) return; const id = deleteTemplate.id; setDeleteTemplate(null); try { await deleteMutation.mutateAsync({ id }); await queryClient.invalidateQueries({ queryKey: ["action", "list-design-templates"], }); toast.success(t("templatesPage.deleted")); } catch (error) { toast.error( error instanceof Error ? error.message : t("templatesPage.deleteFailed"), ); } }; useSetPageTitle(t("templatesPage.title")); useSetHeaderActions(
setSearch(event.target.value)} placeholder={t("templatesPage.searchPlaceholder")} className="h-8 w-48 bg-accent/50 ps-8 text-sm" />
, ); return ( <> {creating ? (
{t("templatesPage.opening")}
) : null}

{t("templatesPage.title")}

{t("templatesPage.description")}

setSearch(event.target.value)} placeholder={t("templatesPage.searchPlaceholder")} className="h-9 w-full bg-accent/50 ps-8 text-sm" />
{isLoading ? ( ) : isError ? ( void refetch()} retrying={isFetching} /> ) : (
)}
{ setPromptOpen(open); if (!open) { setSelected(null); setSelectedDesignSystemId(undefined); setSelectedTemplateParam(null); } }} title={selected?.title ?? t("templatesPage.useTemplate")} placeholder={t("templatesPage.promptPlaceholder")} onSkip={async () => { if (!selected) return; try { await createFromTemplate(selected); return false; } catch (error) { toast.error( error instanceof Error ? error.message : t("templatesPage.createFailed"), ); throw error; } }} skipLabel={t("templatesPage.useAsIs")} onSubmit={handleSubmit} anchorRef={anchorRef} loading={creating} designSystems={designSystems} designSystemsLoading={designSystemsLoading} selectedDesignSystemId={selectedDesignSystemId ?? null} onDesignSystemChange={setSelectedDesignSystemId} onCreateDesignSystem={() => { setPromptOpen(false); navigate("/design-systems/setup"); }} /> !open && setDeleteTemplate(null)} > {t("templatesPage.deleteTitle")} {t("templatesPage.deleteDescription", { title: deleteTemplate?.title ?? "", })} {t("home.cancel")} void handleDelete()} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {t("home.delete")} ); } function TemplateSection({ title, description, templates, linkedTemplateId, empty, onUse, onDelete, }: { title: string; description?: string; templates: DesignTemplateSummary[]; linkedTemplateId?: string | null; empty?: string; onUse: (template: DesignTemplateSummary, element: HTMLElement) => void; onDelete?: (template: DesignTemplateSummary) => void; }) { if (templates.length === 0 && !empty) return null; return (

{title}

{description ? (

{description}

) : null}
{templates.length === 0 ? (
{empty}
) : (
{templates.map((template) => ( ))}
)}
); } function TemplateCard({ template, linked, onUse, onDelete, }: { template: DesignTemplateSummary; linked?: boolean; onUse: (template: DesignTemplateSummary, element: HTMLElement) => void; onDelete?: (template: DesignTemplateSummary) => void; }) { const t = useT(); return (

{template.title}

{template.isBuiltIn ? ( {t("templatesPage.builtIn")} ) : null}

{template.description}

{!template.isBuiltIn && template.isOwner ? (
{onDelete ? ( onDelete(template)} className="text-destructive focus:text-destructive" > {t("home.delete")} ) : null}
) : null}
{t(`templatesPage.categories.${template.category}`)} {template.width && template.height ? ( {template.width} × {template.height} ) : null} {template.lockedLayerCount > 0 ? ( {t("templatesPage.lockedCount", { count: template.lockedLayerCount, })} ) : null}
); } function TemplateGridSkeleton() { return (
{Array.from({ length: 8 }).map((_, index) => (
))}
); }