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%
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