SPB Git

spb/vquant Public MIT

VibeQuant — AI-powered institutional-grade financial intelligence platform.

TypeScript 84.3% Python 11.7% JavaScript 1.6% CSS 1.5% HTML 0.7%
19.6 KB · 656 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      client/src/utils/pdfGenerator.ts6 *7 *  Author:    Simon-Pierre Boucher8 *  Contact:   contact@spboucher.ai9 *  Website:   https://www.spboucher.ai10 *  Demo:      https://www.vquant.ai11 *  License:   MIT (see LICENSE)12 *13 *  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import jsPDF from 'jspdf';18import html2canvas from 'html2canvas';1920interface PDFOptions {21  title: string;22  content: string;23  includeCharts?: boolean;24}2526interface AdvancedPDFOptions {27  title: string;28  contentElement: HTMLElement;29  includeAllVisuals?: boolean;30}3132// Helper function to draw the VQuant logo33function drawLogo(doc: jsPDF, x: number, y: number, size: number): void {34  // Draw gradient circle background35  doc.setFillColor(139, 92, 246); // Purple36  doc.circle(x, y, size, 'F');3738  // Add lighter circle for depth39  doc.setFillColor(168, 85, 247);40  doc.circle(x - size * 0.15, y - size * 0.15, size * 0.6, 'F');4142  // Draw "VQ" text43  doc.setTextColor(255, 255, 255);44  doc.setFontSize(size * 1.2);45  doc.setFont('helvetica', 'bold');46  const vqText = 'VQ';47  const textWidth = doc.getTextWidth(vqText);48  doc.text(vqText, x - textWidth / 2, y + size * 0.25);4950  // Draw small chart lines for visual effect51  doc.setDrawColor(255, 255, 255);52  doc.setLineWidth(0.5);53  const chartY = y + size * 0.7;54  const chartPoints = [55    { x: x - size * 0.6, y: chartY },56    { x: x - size * 0.3, y: chartY - size * 0.2 },57    { x: x, y: chartY - size * 0.1 },58    { x: x + size * 0.3, y: chartY - size * 0.3 },59    { x: x + size * 0.6, y: chartY - size * 0.15 }60  ];6162  for (let i = 0; i < chartPoints.length - 1; i++) {63    doc.line(64      chartPoints[i].x,65      chartPoints[i].y,66      chartPoints[i + 1].x,67      chartPoints[i + 1].y68    );69    doc.circle(chartPoints[i].x, chartPoints[i].y, 0.5, 'F');70  }71  doc.circle(chartPoints[chartPoints.length - 1].x, chartPoints[chartPoints.length - 1].y, 0.5, 'F');72}7374// Helper function to add a professional title page75function addTitlePage(doc: jsPDF, title: string): void {76  const pageWidth = doc.internal.pageSize.getWidth();77  const pageHeight = doc.internal.pageSize.getHeight();78  const margin = 20;79  const maxWidth = pageWidth - (margin * 2);8081  // Background gradient effect - plus subtil82  doc.setFillColor(250, 251, 255); // Ultra light indigo83  doc.rect(0, 0, pageWidth, pageHeight, 'F');8485  // Top decorative bar avec dégradé simulé86  doc.setFillColor(99, 102, 241);87  doc.rect(0, 0, pageWidth, 10, 'F');88  doc.setFillColor(79, 82, 221, 0.8);89  doc.rect(0, 8, pageWidth, 2, 'F');9091  // Draw large logo in center - position améliorée92  const logoSize = 28;93  const logoX = pageWidth / 2;94  const logoY = 75;95  drawLogo(doc, logoX, logoY, logoSize);9697  // Main title avec meilleur espacement98  doc.setTextColor(55, 48, 163); // Indigo-70099  doc.setFontSize(42);100  doc.setFont('helvetica', 'bold');101  const mainTitle = 'VQuant';102  const titleWidth = doc.getTextWidth(mainTitle);103  doc.text(mainTitle, (pageWidth - titleWidth) / 2, 125);104105  // Subtitle avec meilleure taille106  doc.setFontSize(13);107  doc.setFont('helvetica', 'normal');108  doc.setTextColor(99, 102, 241);109  const subtitle = 'Analyse Financière par Intelligence Artificielle';110  const subtitleWidth = doc.getTextWidth(subtitle);111  doc.text(subtitle, (pageWidth - subtitleWidth) / 2, 137);112113  // Decorative line plus élégante114  doc.setDrawColor(139, 92, 246);115  doc.setLineWidth(0.8);116  const lineWidth = 70;117  doc.line((pageWidth - lineWidth) / 2, 148, (pageWidth + lineWidth) / 2, 148);118119  // Report title120  doc.setFontSize(11);121  doc.setFont('helvetica', 'bold');122  doc.setTextColor(71, 85, 105);123  doc.text('RAPPORT D\'ANALYSE', pageWidth / 2, 165, { align: 'center' });124125  // Title box avec shadow effect126  doc.setFontSize(12);127  doc.setFont('helvetica', 'normal');128  doc.setTextColor(15, 23, 42);129  const titleLines = doc.splitTextToSize(title, maxWidth - 30);130  const boxHeight = titleLines.length * 8 + 16;131  const boxY = 175;132133  // Shadow134  doc.setFillColor(226, 232, 240);135  doc.roundedRect((pageWidth - maxWidth + 28) / 2 + 1, boxY + 1, maxWidth - 26, boxHeight, 4, 4, 'F');136137  // Box principal138  doc.setFillColor(255, 255, 255);139  doc.setDrawColor(203, 213, 225);140  doc.setLineWidth(0.5);141  doc.roundedRect((pageWidth - maxWidth + 28) / 2, boxY, maxWidth - 26, boxHeight, 4, 4, 'FD');142143  let textY = boxY + 10;144  titleLines.forEach((line: string) => {145    doc.text(line, pageWidth / 2, textY, { align: 'center' });146    textY += 8;147  });148149  // Date and metadata avec meilleur style150  doc.setFontSize(10);151  doc.setFont('helvetica', 'italic');152  doc.setTextColor(100, 116, 139);153  const dateStr = new Date().toLocaleDateString('fr-FR', {154    weekday: 'long',155    year: 'numeric',156    month: 'long',157    day: 'numeric'158  });159  doc.text(`Généré le ${dateStr}`, pageWidth / 2, pageHeight - 45, { align: 'center' });160161  // Footer decorative line162  doc.setDrawColor(203, 213, 225);163  doc.setLineWidth(0.3);164  doc.line(40, pageHeight - 30, pageWidth - 40, pageHeight - 30);165166  // Footer decorative elements - plus élégants167  doc.setFillColor(139, 92, 246);168  doc.circle(35, pageHeight - 20, 1.5, 'F');169  doc.circle(pageWidth - 35, pageHeight - 20, 1.5, 'F');170171  doc.setFontSize(9);172  doc.setFont('helvetica', 'normal');173  doc.setTextColor(148, 163, 184);174  doc.text('www.vibequant.com', pageWidth / 2, pageHeight - 18, { align: 'center' });175176  // Watermark subtil177  doc.setFontSize(8);178  doc.setTextColor(203, 213, 225);179  doc.text('Confidentiel', pageWidth / 2, pageHeight - 10, { align: 'center' });180181  // Add new page for content182  doc.addPage();183}184185// Helper to capture element as image186async function captureElementAsImage(element: HTMLElement): Promise<string | null> {187  try {188    console.log('captureElementAsImage: Starting capture');189    console.log('Element:', element);190    console.log('Element dimensions:', {191      width: element.scrollWidth,192      height: element.scrollHeight,193      offsetWidth: element.offsetWidth,194      offsetHeight: element.offsetHeight195    });196    console.log('Element innerHTML length:', element.innerHTML.length);197198    // Clone the element to avoid modifying the original199    const clone = element.cloneNode(true) as HTMLElement;200201    // Remove any animations or transitions that might interfere202    clone.style.animation = 'none';203    clone.style.transition = 'none';204205    // Temporarily add to DOM for measurement206    clone.style.position = 'absolute';207    clone.style.left = '-9999px';208    clone.style.visibility = 'hidden';209    clone.style.pointerEvents = 'none';210    clone.style.width = element.scrollWidth + 'px';211    document.body.appendChild(clone);212213    // Wait for any images or charts to load214    await new Promise(resolve => setTimeout(resolve, 500));215216    const canvas = await html2canvas(clone, {217      scale: 2, // Réduit à 2 pour équilibrer qualité et vitesse218      backgroundColor: '#ffffff',219      logging: true, // Activé pour déboguer220      useCORS: true,221      allowTaint: true,222      foreignObjectRendering: true,223      removeContainer: false,224      imageTimeout: 15000,225      // Amélioration de la qualité de rendu226      width: clone.scrollWidth,227      height: clone.scrollHeight,228      windowWidth: clone.scrollWidth,229      windowHeight: clone.scrollHeight,230      onclone: (clonedDoc) => {231        const style = clonedDoc.createElement('style');232        style.textContent = `233          * {234            animation: none !important;235            transition: none !important;236            -webkit-font-smoothing: antialiased;237            -moz-osx-font-smoothing: grayscale;238          }239          /* Améliorer le rendu des polices */240          body {241            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;242          }243          /* Ensure charts and canvases are visible */244          canvas, svg {245            display: block !important;246            visibility: visible !important;247          }248          /* Fix potential display issues */249          .recharts-wrapper, .recharts-surface {250            display: block !important;251          }252        `;253        clonedDoc.head.appendChild(style);254255        // Force render all canvas elements256        const canvases = clonedDoc.querySelectorAll('canvas');257        canvases.forEach((canvas) => {258          canvas.style.display = 'block';259          canvas.style.visibility = 'visible';260        });261      }262    });263264    // Clean up265    document.body.removeChild(clone);266267    console.log('Canvas created:', {268      width: canvas.width,269      height: canvas.height270    });271272    const dataUrl = canvas.toDataURL('image/png', 1.0);273    console.log('DataURL created, length:', dataUrl.length);274275    return dataUrl;276  } catch (error) {277    console.error('Error capturing element:', error);278    console.error('Error stack:', error);279    return null;280  }281}282283// New function to generate PDF from full HTML content284export async function generateAdvancedPDF(options: AdvancedPDFOptions): Promise<void> {285  const { title, contentElement, includeAllVisuals = true } = options;286287  console.log('generateAdvancedPDF: Starting PDF generation');288  console.log('Title:', title);289  console.log('Content element:', contentElement);290  console.log('Include visuals:', includeAllVisuals);291292  // Create new PDF document293  const pdf = new jsPDF({294    orientation: 'portrait',295    unit: 'mm',296    format: 'a4',297  });298299  const pageWidth = pdf.internal.pageSize.getWidth();300  const pageHeight = pdf.internal.pageSize.getHeight();301  const margin = 15;302  const contentWidth = pageWidth - (margin * 2);303304  console.log('PDF page dimensions:', { pageWidth, pageHeight, margin, contentWidth });305306  // Add title page first307  console.log('Adding title page...');308  addTitlePage(pdf, title);309310  let yPosition = margin;311312  // Add VQuant header with logo - amélioré313  // Dégradé simulé pour l'en-tête314  pdf.setFillColor(139, 92, 246);315  pdf.rect(0, 0, pageWidth, 40, 'F');316  pdf.setFillColor(119, 72, 226, 0.9);317  pdf.rect(0, 38, pageWidth, 2, 'F');318319  // Draw small logo in header320  drawLogo(pdf, 20, 20, 9);321322  pdf.setTextColor(255, 255, 255);323  pdf.setFontSize(22);324  pdf.setFont('helvetica', 'bold');325  pdf.text('VQuant', 34, 18);326327  pdf.setFontSize(10);328  pdf.setFont('helvetica', 'normal');329  pdf.setTextColor(240, 240, 255);330  pdf.text('Financial Intelligence Platform', 34, 27);331332  // Add date in header - style amélioré333  pdf.setFontSize(8);334  pdf.setFont('helvetica', 'italic');335  pdf.setTextColor(240, 240, 255);336  const date = new Date().toLocaleDateString('fr-FR', {337    year: 'numeric',338    month: 'long',339    day: 'numeric',340  });341  pdf.text(date, pageWidth - margin - 40, 22);342343  yPosition = 52;344345  // Add title avec meilleur style346  pdf.setTextColor(15, 23, 42);347  pdf.setFontSize(18);348  pdf.setFont('helvetica', 'bold');349  const titleLines = pdf.splitTextToSize(title, contentWidth);350  pdf.text(titleLines, margin, yPosition);351  yPosition += (titleLines.length * 8) + 10;352353  // Add horizontal line - plus élégante354  pdf.setDrawColor(139, 92, 246);355  pdf.setLineWidth(0.8);356  pdf.line(margin, yPosition, pageWidth - margin, yPosition);357  yPosition += 12;358359  try {360    // Capture the entire content element directly361    console.log('Starting PDF capture...');362    const imgData = await captureElementAsImage(contentElement);363364    if (!imgData) {365      console.error('Failed to capture content - imgData is null');366      throw new Error('Failed to capture content');367    }368369    console.log('Content captured successfully');370    const img = new Image();371    img.src = imgData;372    await new Promise((resolve) => { img.onload = resolve; });373374    const imgWidth = contentWidth;375    const imgHeight = (img.height * imgWidth) / img.width;376377    console.log(`Image dimensions: ${imgWidth}x${imgHeight}`);378379    // Split into multiple pages if needed380    const pageContentHeight = pageHeight - yPosition - 15;381    let currentHeight = 0;382383    while (currentHeight < imgHeight) {384      const heightToAdd = Math.min(imgHeight - currentHeight, pageContentHeight);385386      if (currentHeight > 0) {387        pdf.addPage();388        yPosition = margin;389      }390391      pdf.addImage(392        imgData,393        'PNG',394        margin,395        yPosition - currentHeight,396        imgWidth,397        imgHeight,398        undefined,399        'FAST'400      );401402      currentHeight += pageContentHeight;403404      if (currentHeight < imgHeight) {405        yPosition = margin;406      }407    }408  } catch (error) {409    console.error('Error capturing content:', error);410411    // Fallback: Add error message with details412    pdf.setFontSize(11);413    pdf.setTextColor(255, 0, 0);414    pdf.text('Error: Could not capture content for PDF', margin, yPosition);415    yPosition += 10;416417    pdf.setFontSize(9);418    pdf.setTextColor(100, 100, 100);419    const errorMsg = error instanceof Error ? error.message : 'Unknown error';420    const errorLines = pdf.splitTextToSize(`Details: ${errorMsg}`, contentWidth);421    pdf.text(errorLines, margin, yPosition);422423    throw error; // Re-throw to be caught by caller424  }425426  // Add footer on each page - amélioré427  const pageCount = pdf.internal.pages.length - 1;428  for (let i = 1; i <= pageCount; i++) {429    pdf.setPage(i);430431    // Footer line avec dégradé432    pdf.setDrawColor(203, 213, 225);433    pdf.setLineWidth(0.5);434    pdf.line(margin, pageHeight - 15, pageWidth - margin, pageHeight - 15);435436    // Footer dots décoratifs437    pdf.setFillColor(139, 92, 246);438    pdf.circle(margin + 2, pageHeight - 15, 0.8, 'F');439    pdf.circle(pageWidth - margin - 2, pageHeight - 15, 0.8, 'F');440441    // Footer text - meilleur style442    pdf.setFontSize(7.5);443    pdf.setFont('helvetica', 'normal');444    pdf.setTextColor(100, 116, 139);445    pdf.text('Généré par VQuant • www.vibequant.com', margin, pageHeight - 9);446447    pdf.setFont('helvetica', 'bold');448    pdf.setTextColor(99, 102, 241);449    pdf.text(`Page ${i} / ${pageCount}`, pageWidth - margin - 18, pageHeight - 9);450  }451452  // Save PDF453  const filename = `VQuant_Report_${new Date().toISOString().split('T')[0]}.pdf`;454  pdf.save(filename);455}456457export async function generatePDF(options: PDFOptions): Promise<void> {458  const { title, content, includeCharts = true } = options;459460  // Create new PDF document461  const pdf = new jsPDF({462    orientation: 'portrait',463    unit: 'mm',464    format: 'a4',465  });466467  const pageWidth = pdf.internal.pageSize.getWidth();468  const pageHeight = pdf.internal.pageSize.getHeight();469  const margin = 20;470  const contentWidth = pageWidth - (margin * 2);471472  // Add title page first473  addTitlePage(pdf, title);474475  let yPosition = margin;476477  // Add VQuant header with logo478  pdf.setFillColor(59, 130, 246); // Blue479  pdf.rect(0, 0, pageWidth, 30, 'F');480481  // Draw small logo in header482  drawLogo(pdf, 25, 15, 7);483484  pdf.setTextColor(255, 255, 255);485  pdf.setFontSize(24);486  pdf.setFont('helvetica', 'bold');487  pdf.text('VQuant', 35, 15);488489  pdf.setFontSize(12);490  pdf.setFont('helvetica', 'normal');491  pdf.text('Financial Intelligence Platform', 35, 23);492493  yPosition = 40;494495  // Add title496  pdf.setTextColor(0, 0, 0);497  pdf.setFontSize(18);498  pdf.setFont('helvetica', 'bold');499  const titleLines = pdf.splitTextToSize(title, contentWidth);500  pdf.text(titleLines, margin, yPosition);501  yPosition += (titleLines.length * 8) + 10;502503  // Add date504  pdf.setFontSize(10);505  pdf.setFont('helvetica', 'normal');506  pdf.setTextColor(100, 100, 100);507  const date = new Date().toLocaleDateString('en-US', {508    year: 'numeric',509    month: 'long',510    day: 'numeric',511  });512  pdf.text(`Generated on ${date}`, margin, yPosition);513  yPosition += 15;514515  // Add horizontal line516  pdf.setDrawColor(200, 200, 200);517  pdf.line(margin, yPosition, pageWidth - margin, yPosition);518  yPosition += 10;519520  // Process and add content521  pdf.setFontSize(11);522  pdf.setTextColor(0, 0, 0);523  pdf.setFont('helvetica', 'normal');524525  // Split content into sections and paragraphs526  const sections = content.split(/\n\n+/);527528  for (const section of sections) {529    if (!section.trim()) continue;530531    // Check if section is a heading (starts with #)532    if (section.trim().startsWith('#')) {533      const heading = section.trim().replace(/^#+\s*/, '');534535      // Check if new page needed536      if (yPosition > pageHeight - 30) {537        pdf.addPage();538        yPosition = margin;539      }540541      pdf.setFontSize(14);542      pdf.setFont('helvetica', 'bold');543      pdf.setTextColor(59, 130, 246);544      const headingLines = pdf.splitTextToSize(heading, contentWidth);545      pdf.text(headingLines, margin, yPosition);546      yPosition += (headingLines.length * 7) + 5;547548      pdf.setFontSize(11);549      pdf.setFont('helvetica', 'normal');550      pdf.setTextColor(0, 0, 0);551      continue;552    }553554    // Regular paragraph555    const lines = pdf.splitTextToSize(section.trim(), contentWidth);556557    for (const line of lines) {558      // Check if new page needed559      if (yPosition > pageHeight - 30) {560        pdf.addPage();561        yPosition = margin;562      }563564      pdf.text(line, margin, yPosition);565      yPosition += 6;566    }567568    yPosition += 4; // Add space between paragraphs569  }570571  // Capture charts if requested572  if (includeCharts) {573    try {574      // Find all chart containers575      const chartElements = document.querySelectorAll('[data-chart]');576577      for (let i = 0; i < chartElements.length; i++) {578        const element = chartElements[i] as HTMLElement;579580        // Add new page for chart581        pdf.addPage();582        yPosition = margin;583584        // Add chart title if available585        const chartTitle = element.getAttribute('data-chart-title');586        if (chartTitle) {587          pdf.setFontSize(14);588          pdf.setFont('helvetica', 'bold');589          pdf.text(chartTitle, margin, yPosition);590          yPosition += 10;591        }592593        // Capture chart as image594        const canvas = await html2canvas(element, {595          scale: 2,596          backgroundColor: '#ffffff',597          logging: false,598        });599600        const imgData = canvas.toDataURL('image/png');601        const imgWidth = contentWidth;602        const imgHeight = (canvas.height * imgWidth) / canvas.width;603604        // Check if image fits on current page605        if (yPosition + imgHeight > pageHeight - margin) {606          pdf.addPage();607          yPosition = margin;608        }609610        pdf.addImage(imgData, 'PNG', margin, yPosition, imgWidth, imgHeight);611        yPosition += imgHeight + 10;612      }613    } catch (error) {614      console.error('Error capturing charts:', error);615    }616  }617618  // Add footer on each page619  const pageCount = pdf.internal.pages.length - 1; // Subtract 1 for the internal page array620  for (let i = 1; i <= pageCount; i++) {621    pdf.setPage(i);622623    // Footer line624    pdf.setDrawColor(200, 200, 200);625    pdf.line(margin, pageHeight - 15, pageWidth - margin, pageHeight - 15);626627    // Footer text628    pdf.setFontSize(9);629    pdf.setTextColor(100, 100, 100);630    pdf.setFont('helvetica', 'normal');631    pdf.text('Generated by VQuant', margin, pageHeight - 10);632    pdf.text(`Page ${i} of ${pageCount}`, pageWidth - margin - 20, pageHeight - 10);633  }634635  // Save PDF636  const filename = `VQuant_Analysis_${new Date().toISOString().split('T')[0]}.pdf`;637  pdf.save(filename);638}639640// Helper function to clean markdown formatting for PDF641export function cleanMarkdownForPDF(markdown: string): string {642  return markdown643    // Remove code blocks644    .replace(/```[\s\S]*?```/g, '')645    // Remove inline code646    .replace(/`([^`]+)`/g, '$1')647    // Remove bold/italic648    .replace(/\*\*([^*]+)\*\*/g, '$1')649    .replace(/\*([^*]+)\*/g, '$1')650    // Remove links but keep text651    .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')652    // Clean up extra whitespace653    .replace(/\n{3,}/g, '\n\n')654    .trim();655}656