"use client"; import type { ExportFormat } from "@/lib/client/types"; import { toast } from "@/components/ui/toast"; /** Client-side export formats: the server formats plus `pdf` (= HTML opened in a new tab + `window.print()`). */ export type ClientExportFormat = ExportFormat | "pdf"; export interface ExportOption { format: ClientExportFormat; label: string; hint: string; description: string; } export const EXPORT_OPTIONS: ExportOption[] = [ { format: "markdown", label: "Markdown", hint: ".md", description: "Readable transcript with headings and code blocks." }, { format: "txt", label: "Plain text", hint: ".txt", description: "No markup, easy to paste anywhere." }, { format: "json", label: "JSON", hint: ".json", description: "Full data: messages, parts, usage, settings." }, { format: "html", label: "HTML", hint: ".html", description: "Self-contained page with print styles." }, { format: "pdf", label: "PDF (print)", hint: "Print dialog", description: "Opens a print-optimized page — choose “Save as PDF”." }, ]; export function exportUrl(conversationId: string, format: ClientExportFormat, opts: { download?: boolean; print?: boolean } = {}): string { const sp = new URLSearchParams(); sp.set("format", format === "pdf" ? "html" : format); if (opts.download) sp.set("download", "1"); if (opts.print) sp.set("print", "1"); return `/api/conversations/${encodeURIComponent(conversationId)}/export?${sp.toString()}`; } function filenameFrom(res: Response, fallback: string): string { const cd = res.headers.get("content-disposition") ?? ""; return /filename="?([^";]+)"?/.exec(cd)?.[1] ?? fallback; } async function download(url: string, fallbackName: string) { const res = await fetch(url, { credentials: "same-origin" }); if (!res.ok) { let msg = `Export failed (${res.status})`; try { const body = (await res.json()) as { error?: { message?: string } }; if (body.error?.message) msg = body.error.message; } catch { /* not json */ } throw new Error(msg); } const blob = await res.blob(); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filenameFrom(res, fallbackName); document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); } /** * Export a conversation from the browser. * - json / markdown / txt / html → file download * - pdf → opens the HTML export in a new tab which triggers `window.print()` ("Save as PDF"). Falls back to an * HTML download when the pop-up is blocked. * Shows toasts; resolves when done. Throws only on network/API failure (already toasted). */ export async function exportConversation(conversationId: string, format: ClientExportFormat): Promise { try { if (format === "pdf") { const w = window.open(exportUrl(conversationId, "pdf", { print: true }), "_blank", "noopener"); if (!w) { await download(exportUrl(conversationId, "html", { download: true }), "conversation.html"); toast.warning("Pop-up blocked", "The HTML version was downloaded instead — open it and use Print → Save as PDF."); } return; } const ext = format === "markdown" ? "md" : format; await download(exportUrl(conversationId, format, { download: true }), `conversation.${ext}`); toast.success("Export ready", "Your download should start now."); } catch (e) { toast.error("Export failed", e instanceof Error ? e.message : undefined); throw e; } }