import { jsPDF } from 'jspdf'; import autoTable from 'jspdf-autotable'; /** LLMagnet brand palette (purple / plum / accent) */ export const REPORT_COLORS = { primary: [124, 58, 237] as [number, number, number], accent: [219, 39, 119] as [number, number, number], border: [242, 215, 242] as [number, number, number], text: [17, 24, 39] as [number, number, number], muted: [107, 114, 128] as [number, number, number], headerBg: [250, 245, 255] as [number, number, number], }; const HEADER_BAND_H = 26; const PAGE_MARGIN = 14; const HEADER_LOGO_TOP = 7; function formatNow(): string { return new Date().toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short', }); } export async function loadLogoDataUrl(pluginUrl: string): Promise { const base = pluginUrl.replace(/\/?$/, '/'); const url = `${base}assets/react-build/assets/llmmagnetlogo.svg`; return new Promise((resolve) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { try { const w = 64; const h = 64; const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); if (!ctx) { resolve(null); return; } ctx.drawImage(img, 0, 0, w, h); resolve(canvas.toDataURL('image/png')); } catch { resolve(null); } }; img.onerror = () => resolve(null); img.src = url; }); } function drawHeaderBand( doc: jsPDF, pageWidth: number, opts: { logoDataUrl: string | null; reportTitle: string; pageNum: number; } ) { doc.setFillColor(REPORT_COLORS.headerBg[0], REPORT_COLORS.headerBg[1], REPORT_COLORS.headerBg[2]); doc.rect(0, 0, pageWidth, HEADER_BAND_H + 2, 'F'); doc.setDrawColor(REPORT_COLORS.border[0], REPORT_COLORS.border[1], REPORT_COLORS.border[2]); doc.setLineWidth(0.3); doc.line(0, HEADER_BAND_H + 2, pageWidth, HEADER_BAND_H + 2); let x = PAGE_MARGIN; if (opts.logoDataUrl) { try { doc.addImage(opts.logoDataUrl, 'PNG', x, HEADER_LOGO_TOP - 2, 10, 10); x += 14; } catch { // ignore } } doc.setTextColor(REPORT_COLORS.primary[0], REPORT_COLORS.primary[1], REPORT_COLORS.primary[2]); doc.setFont('helvetica', 'bold'); doc.setFontSize(11); doc.text('LLMagnet', x, HEADER_LOGO_TOP + 6); doc.setTextColor(REPORT_COLORS.muted[0], REPORT_COLORS.muted[1], REPORT_COLORS.muted[2]); doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.text(opts.reportTitle, x + 42, HEADER_LOGO_TOP + 6); doc.text(`Page ${opts.pageNum}`, pageWidth - PAGE_MARGIN, HEADER_LOGO_TOP + 6, { align: 'right' }); } function tableStartAfterMeta(metaLines: string[], summaryTailMm: number): number { return ( HEADER_BAND_H + 6 + 7 + 5 + 5 + metaLines.length * 4.5 + 8 + summaryTailMm ); } export interface BotAnalyticsPdfRow { bot: string; visits: string; clicks: string; ctr: string; trend: string; } export async function createBotAnalyticsPdf(params: { pluginUrl: string; siteName: string; reportTitle: string; metaLines: string[]; tableRows: BotAnalyticsPdfRow[]; }): Promise { const logo = await loadLogoDataUrl(params.pluginUrl); const doc = new jsPDF({ unit: 'mm', format: 'a4' }); const pageW = doc.internal.pageSize.getWidth(); const head = [['Bot', 'Visits', 'Clicks', 'CTR', 'Trend']]; const body = params.tableRows.map((r) => [ r.bot, r.visits, r.clicks, r.ctr, r.trend, ]); const tableStartY = tableStartAfterMeta(params.metaLines, 0); autoTable(doc, { head, body, startY: tableStartY, margin: { left: PAGE_MARGIN, right: PAGE_MARGIN, bottom: 16, top: HEADER_BAND_H + 2 }, styles: { fontSize: 9, cellPadding: 2.5, textColor: REPORT_COLORS.text, }, headStyles: { fillColor: REPORT_COLORS.primary, textColor: 255, fontStyle: 'bold', }, alternateRowStyles: { fillColor: [252, 250, 255] }, theme: 'striped', willDrawPage: (data) => { drawHeaderBand(data.doc, pageW, { logoDataUrl: logo, reportTitle: params.reportTitle, pageNum: data.pageNumber, }); if (data.pageNumber !== 1) return; let yy = HEADER_BAND_H + 6; data.doc.setTextColor(REPORT_COLORS.text[0], REPORT_COLORS.text[1], REPORT_COLORS.text[2]); data.doc.setFont('helvetica', 'bold'); data.doc.setFontSize(14); data.doc.text('Bot Analytics', PAGE_MARGIN, yy); yy += 7; data.doc.setFont('helvetica', 'normal'); data.doc.setFontSize(9); data.doc.setTextColor(REPORT_COLORS.muted[0], REPORT_COLORS.muted[1], REPORT_COLORS.muted[2]); data.doc.text(params.siteName, PAGE_MARGIN, yy); yy += 5; data.doc.text(`Generated: ${formatNow()}`, PAGE_MARGIN, yy); yy += 5; params.metaLines.forEach((line) => { data.doc.text(line, PAGE_MARGIN, yy); yy += 4.5; }); }, }); return doc; } export interface PageStatPdfRow { page: string; visits: string; clicks: string; ctr: string; models: string; } export async function createPagesReportPdf(params: { pluginUrl: string; siteName: string; reportTitle: string; metaLines: string[]; rows: PageStatPdfRow[]; }): Promise { const logo = await loadLogoDataUrl(params.pluginUrl); const doc = new jsPDF({ unit: 'mm', format: 'a4' }); const pageW = doc.internal.pageSize.getWidth(); const head = [['Page', 'Bot visits', 'Clicks', 'CTR', 'Models']]; const body = params.rows.map((r) => [r.page, r.visits, r.clicks, r.ctr, r.models]); const tableStartY = tableStartAfterMeta(params.metaLines, 0); autoTable(doc, { head, body, startY: tableStartY, margin: { left: PAGE_MARGIN, right: PAGE_MARGIN, bottom: 16, top: HEADER_BAND_H + 2 }, styles: { fontSize: 8, cellPadding: 2, textColor: REPORT_COLORS.text }, headStyles: { fillColor: REPORT_COLORS.primary, textColor: 255, fontStyle: 'bold', }, columnStyles: { 0: { cellWidth: 62 }, 4: { cellWidth: 42 }, }, alternateRowStyles: { fillColor: [252, 250, 255] }, theme: 'striped', willDrawPage: (data) => { drawHeaderBand(data.doc, pageW, { logoDataUrl: logo, reportTitle: params.reportTitle, pageNum: data.pageNumber, }); if (data.pageNumber !== 1) return; let yy = HEADER_BAND_H + 6; data.doc.setTextColor(REPORT_COLORS.text[0], REPORT_COLORS.text[1], REPORT_COLORS.text[2]); data.doc.setFont('helvetica', 'bold'); data.doc.setFontSize(14); data.doc.text('Pages', PAGE_MARGIN, yy); yy += 7; data.doc.setFont('helvetica', 'normal'); data.doc.setFontSize(9); data.doc.setTextColor(REPORT_COLORS.muted[0], REPORT_COLORS.muted[1], REPORT_COLORS.muted[2]); data.doc.text(params.siteName, PAGE_MARGIN, yy); yy += 5; data.doc.text(`Generated: ${formatNow()}`, PAGE_MARGIN, yy); yy += 5; params.metaLines.forEach((line) => { data.doc.text(line, PAGE_MARGIN, yy); yy += 4.5; }); }, }); return doc; } export interface ProductPdfRow { name: string; impressions: string; clicks: string; ctr: string; addToCart: string; revenue: string; } export async function createProductsReportPdf(params: { pluginUrl: string; siteName: string; reportTitle: string; metaLines: string[]; summaryLines: string[]; rows: ProductPdfRow[]; }): Promise { const logo = await loadLogoDataUrl(params.pluginUrl); const doc = new jsPDF({ unit: 'mm', format: 'a4' }); const pageW = doc.internal.pageSize.getWidth(); const head = [['Product', 'Impr.', 'Clicks', 'CTR', 'Add to cart', 'Revenue']]; const body = params.rows.map((r) => [ r.name, r.impressions, r.clicks, r.ctr, r.addToCart, r.revenue, ]); const summaryTail = 3 + 5 + params.summaryLines.length * 4 + 8; const tableStartY = tableStartAfterMeta(params.metaLines, summaryTail); autoTable(doc, { head, body, startY: tableStartY, margin: { left: PAGE_MARGIN, right: PAGE_MARGIN, bottom: 16, top: HEADER_BAND_H + 2 }, styles: { fontSize: 7.5, cellPadding: 1.8, textColor: REPORT_COLORS.text }, headStyles: { fillColor: REPORT_COLORS.primary, textColor: 255, fontStyle: 'bold', }, columnStyles: { 0: { cellWidth: 52 }, }, alternateRowStyles: { fillColor: [252, 250, 255] }, theme: 'striped', willDrawPage: (data) => { drawHeaderBand(data.doc, pageW, { logoDataUrl: logo, reportTitle: params.reportTitle, pageNum: data.pageNumber, }); if (data.pageNumber !== 1) return; let yy = HEADER_BAND_H + 6; data.doc.setTextColor(REPORT_COLORS.text[0], REPORT_COLORS.text[1], REPORT_COLORS.text[2]); data.doc.setFont('helvetica', 'bold'); data.doc.setFontSize(14); data.doc.text('Products', PAGE_MARGIN, yy); yy += 7; data.doc.setFont('helvetica', 'normal'); data.doc.setFontSize(9); data.doc.setTextColor(REPORT_COLORS.muted[0], REPORT_COLORS.muted[1], REPORT_COLORS.muted[2]); data.doc.text(params.siteName, PAGE_MARGIN, yy); yy += 5; data.doc.text(`Generated: ${formatNow()}`, PAGE_MARGIN, yy); yy += 5; params.metaLines.forEach((line) => { data.doc.text(line, PAGE_MARGIN, yy); yy += 4.5; }); yy += 3; data.doc.setFont('helvetica', 'bold'); data.doc.setFontSize(9); data.doc.setTextColor(REPORT_COLORS.primary[0], REPORT_COLORS.primary[1], REPORT_COLORS.primary[2]); data.doc.text('Summary', PAGE_MARGIN, yy); yy += 5; data.doc.setFont('helvetica', 'normal'); data.doc.setFontSize(8.5); data.doc.setTextColor(REPORT_COLORS.text[0], REPORT_COLORS.text[1], REPORT_COLORS.text[2]); params.summaryLines.forEach((line) => { data.doc.text(line, PAGE_MARGIN, yy); yy += 4; }); }, }); return doc; } export function arrayBufferToBase64(buffer: ArrayBuffer): string { let binary = ''; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); }