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: server/services/beamerSlidesService.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 axios from 'axios';18import { spawn } from 'child_process';19import { promises as fs } from 'fs';20import path from 'path';21import { fileURLToPath } from 'url';22import { dirname } from 'path';2324const __filename = fileURLToPath(import.meta.url);25const __dirname = dirname(__filename);2627const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;28const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';2930interface BeamerOptions {31 question: string;32 answer: string;33 author: string;34 company?: string;35 figureUrls?: string[]; // e.g. ["/figures/fig-123-0.png", ...]36}3738/**39 * Converts analysis to LaTeX Beamer structure using Claude40 */41async function generateBeamerStructure(options: BeamerOptions): Promise<string> {42 if (!ANTHROPIC_API_KEY) {43 throw new Error('ANTHROPIC_API_KEY not configured');44 }4546 const today = new Date().toLocaleDateString('fr-FR', {47 day: 'numeric',48 month: 'long',49 year: 'numeric'50 });5152 const systemPrompt = `Tu es un expert en présentations LaTeX Beamer avec le thème Metropolis. Ton travail est d'analyser un contenu d'analyse financière et de créer des slides modernes, épurées et professionnelles.5354RÈGLES CRITIQUES:551. Retourne UNIQUEMENT le code LaTeX - PAS d'explications, PAS de \\documentclass, PAS de préambule562. Le thème Metropolis est déjà configuré (aspect ratio 16:8, couleurs EventBlue/EventRed/EventGray)573. Design ÉPURÉ: une idée par slide, max 4-5 points584. Échappe TOUS les caractères spéciaux: $ → \\$, % → \\%, & → \\&, _ → \\_595. Utilise \\alert{} pour nombres et pourcentages importants (couleur rouge EventRed)6061STRUCTURE MODERNE METROPOLIS:62• \\section{Nom} - Crée une slide de section automatique (design épuré)63• \\begin{frame}{Titre} ... \\end{frame} - Slide standard64• \\begin{frame}[fragile] si code/verbatim65• \\begin{block}{Titre} ... \\end{block} - Boîte bleue pour info importante66• \\begin{alertblock}{Titre} ... \\end{alertblock} - Boîte rouge pour alertes67• \\begin{itemize} \\item ... \\end{itemize} - Liste à puces (bullets ronds bleus)68• \\begin{enumerate} \\item ... \\end{enumerate} - Liste numérotée69• \\alert{texte} - Highlight rouge pour chiffres/pourcentages7071DESIGN ÉPURÉ METROPOLIS:72✓ Utilise des blocks pour structurer (pas de texte brut)73✓ Espace blanc généreux74✓ Titres courts et clairs75✓ Max 4-5 bullets par slide76✓ Privilégie les sections pour diviser le contenu77✗ Évite les slides surchargées78✗ Pas de sous-sous-points complexes79✗ Pas de paragraphes longs8081EXEMPLES DE BON STYLE:8283EXEMPLE 1 - Slide avec block:84\\begin{frame}{Résumé Exécutif}85\\begin{block}{Points Clés}86\\begin{itemize}87 \\item Rendement annuel: \\alert{+23\\%}88 \\item Volatilité: \\alert{37\\%}89 \\item Ratio Sharpe: \\alert{0.62}90\\end{itemize}91\\end{block}92\\end{frame}9394EXEMPLE 2 - Slide avec alertblock:95\\begin{frame}{Risques Identifiés}96\\begin{alertblock}{Attention}97Persistance très faible (\\alert{0.11 jours}) - Les chocs se dissipent rapidement98\\end{alertblock}99\\begin{itemize}100 \\item Impact sur stratégies de hedging101 \\item Ajustements fréquents nécessaires102\\end{itemize}103\\end{frame}104105EXEMPLE 3 - Section + contenu:106\\section{Résultats du Modèle}107108\\begin{frame}{Paramètres Estimés}109\\begin{block}{Coefficients GJR-GARCH}110\\begin{itemize}111 \\item $\\alpha$ (ARCH): \\alert{0.0016}112 \\item $\\gamma$ (effet levier): \\alert{0.00015}113 \\item $\\beta$ (GARCH): \\alert{0.000009}114\\end{itemize}115\\end{block}116\\end{frame}`;117118 // For very long content, extract just the key sections119 let contentToUse = options.answer;120 if (options.answer.length > 20000) {121 console.log('[BeamerSlides] Content is long (',options.answer.length,'chars), extracting key sections...');122 // Extract summary sections (usually contain # headers and key info)123 const sections = options.answer.split(/\n#{1,3}\s+/);124 // Take first 5 sections or 15000 chars, whichever is shorter125 contentToUse = sections.slice(0, 5).join('\n## ').substring(0, 15000);126 console.log('[BeamerSlides] Reduced to',contentToUse.length,'chars');127 }128129 // Build figure instructions if figures are available130 const figureCount = options.figureUrls?.length || 0;131 const figureInstructions = figureCount > 0132 ? `\n\n📊 FIGURES DISPONIBLES (${figureCount} images):\nDes figures ont été générées. Pour CHAQUE figure, crée une slide dédiée avec:\n\\begin{frame}{Titre Descriptif de la Figure}\n\\begin{center}\n\\includegraphics[width=0.85\\textwidth]{figure-INDEX.png}\n\\end{center}\n\\end{frame}\n\nLes INDEX disponibles sont: ${Array.from({length: figureCount}, (_, i) => i).join(', ')}\nIntègre ces slides de figures aux endroits logiques dans ta présentation (après l'analyse correspondante).`133 : '';134135 const userPrompt = `Crée des slides Beamer LaTeX MODERNES avec le thème Metropolis pour cette analyse.136137⚠️ IMPORTANT: Retourne UNIQUEMENT le code LaTeX (PAS de \\documentclass, PAS de préambule, juste les frames)138139CONTENU DE L'ANALYSE:140${contentToUse}${figureInstructions}141142━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━143STRUCTURE DES SLIDES (8-10 slides maximum):144━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━1451461️⃣ TABLE DES MATIÈRES147\\begin{frame}{Plan}148\\tableofcontents149\\end{frame}1501512️⃣ RÉSUMÉ EXÉCUTIF (1 slide)152\\section{Résumé}153\\begin{frame}{Résumé Exécutif}154\\begin{block}{Points Clés}155\\begin{itemize}156 \\item Point 1 avec \\alert{chiffre\\%}157 \\item Point 2 avec \\alert{\\$valeur}158 \\item Point 3 avec \\alert{résultat}159\\end{itemize}160\\end{block}161\\end{frame}1621633️⃣ CONTENU PRINCIPAL (5-6 slides)164Divise en 2-3 \\section{} logiques, chacune avec 2-3 frames:165166\\section{Nom de la Section}167168\\begin{frame}{Titre Clair}169\\begin{block}{Sous-titre Descriptif}170\\begin{itemize}171 \\item Point concis (max 1 ligne)172 \\item Chiffre important: \\alert{X\\%}173 \\item Conclusion claire174\\end{itemize}175\\end{block}176\\end{frame}177178\\begin{frame}{Points Critiques}179\\begin{alertblock}{⚠️ Attention}180Message d'alerte important avec \\alert{valeur critique}181\\end{alertblock}182183\\begin{block}{Implications}184\\begin{itemize}185 \\item Conséquence 1186 \\item Conséquence 2187\\end{itemize}188\\end{block}189\\end{frame}1901914️⃣ MÉTRIQUES CLÉS (1-2 slides)192\\section{Métriques}193194\\begin{frame}{Indicateurs Financiers}195\\begin{block}{Performance}196\\begin{itemize}197 \\item Métrique 1: \\alert{valeur}198 \\item Métrique 2: \\alert{valeur}199\\end{itemize}200\\end{block}201202\\begin{block}{Risque}203\\begin{itemize}204 \\item Indicateur 1: \\alert{valeur}205 \\item Indicateur 2: \\alert{valeur}206\\end{itemize}207\\end{block}208\\end{frame}2092105️⃣ CONCLUSION (1 slide)211\\section{Conclusion}212213\\begin{frame}{Conclusions}214\\begin{block}{Résultats Principaux}215\\begin{itemize}216 \\item Résultat 1217 \\item Résultat 2218\\end{itemize}219\\end{block}220221\\begin{alertblock}{Recommandation Principale}222Action clé à prendre avec \\alert{justification}223\\end{alertblock}224\\end{frame}225226━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━227RÈGLES DE STYLE METROPOLIS:228━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━229230✓ TOUJOURS utiliser \\begin{block}{} pour structurer231✓ \\alert{} pour TOUS les chiffres/pourcentages/montants232✓ Titres courts (max 6 mots)233✓ Max 4-5 bullets par slide234✓ Une idée = une slide235✓ Espaces généreux (pas de surcharge)236✓ \\section{} pour diviser le contenu237238✗ PAS de texte brut sans block239✗ PAS de paragraphes longs240✗ PAS de sous-sous-points241✗ PAS de slides surchargées242243⚠️ ÉCHAPPEMENT OBLIGATOIRE:244$ → \\$ | % → \\% | & → \\& | _ → \\_ | # → \\#`;245246 try {247 console.log('[BeamerSlides] Structuring content with Claude...');248249 const response = await axios.post(250 ANTHROPIC_API_URL,251 {252 model: 'claude-opus-4-8',253 max_tokens: 8000,254 temperature: 0.2,255 messages: [{ role: 'user', content: userPrompt }],256 system: systemPrompt257 },258 {259 headers: {260 'x-api-key': ANTHROPIC_API_KEY,261 'anthropic-version': '2023-06-01',262 'content-type': 'application/json'263 },264 timeout: 120000 // 120 seconds for complex analyses265 }266 );267268 let latexContent = response.data.content[0].text;269270 // Clean up markdown fences if Claude added them271 latexContent = latexContent.replace(/^```latex\n?/gm, '').replace(/^```tex\n?/gm, '').replace(/\n?```$/gm, '').trim();272273 console.log('[BeamerSlides] ✓ LaTeX content generated:', latexContent.length, 'chars');274 return latexContent;275276 } catch (error) {277 console.error('[BeamerSlides] Error generating structure:', error);278 if (axios.isAxiosError(error) && error.response) {279 throw new Error(`Claude API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);280 }281 throw new Error(`Failed to generate Beamer structure: ${(error as Error).message}`);282 }283}284285/**286 * Creates complete Beamer .tex file287 */288function createBeamerDocument(content: string, options: BeamerOptions): string {289 const today = new Date().toLocaleDateString('fr-FR', {290 day: 'numeric',291 month: 'long',292 year: 'numeric'293 });294295 // Extract title from first markdown heading in answer, or use generic title296 const headingMatch = options.answer.match(/^#{1,2}\s+(.+)$/m);297 const slideTitle = headingMatch ? headingMatch[1].trim() : 'Analyse Financière';298 const escTitle = slideTitle.replace(/\$/g, '\\$').replace(/%/g, '\\%').replace(/_/g, '\\_').replace(/&/g, '\\&').replace(/#/g, '\\#').replace(/\*/g, '');299 const escA = options.author.replace(/&/g, '\\&').replace(/_/g, '\\_');300 const escC = (options.company || 'VQuant').replace(/&/g, '\\&').replace(/_/g, '\\_');301302 return `\\documentclass[aspectratio=169,12pt]{beamer}303304% =====================================================305% THEME METROPOLIS — MODERN PREMIUM STYLE306% =====================================================307\\usetheme[308 progressbar=frametitle,309 numbering=fraction,310 sectionpage=progressbar,311 subsectionpage=none312]{metropolis}313\\usepackage{appendixnumberbeamer}314315% =====================================================316% PACKAGES317% =====================================================318\\usepackage[utf8]{inputenc}319\\usepackage[T1]{fontenc}320\\usepackage[french]{babel}321\\usepackage{amsmath, amsfonts, amssymb}322\\usepackage{booktabs}323\\usepackage{graphicx}324\\usepackage{hyperref}325\\usepackage{tikz}326\\usepackage{xcolor}327\\usepackage{fontawesome5}328\\usepackage{tcolorbox}329\\tcbuselibrary{skins,breakable}330\\usetikzlibrary{arrows.meta, positioning, calc, backgrounds}331332% =====================================================333% COULEURS — VIBEQUANT BRAND PALETTE334% =====================================================335\\definecolor{VQTeal}{HTML}{119993}336\\definecolor{VQBlue}{HTML}{2072B2}337\\definecolor{VQPurple}{HTML}{652E89}338\\definecolor{VQOrange}{HTML}{F49422}339\\definecolor{VQRed}{HTML}{EF632B}340\\definecolor{VQGreen}{HTML}{6CBC5A}341\\definecolor{VQDark}{HTML}{1A1A2E}342\\definecolor{VQGray}{HTML}{6B7280}343\\definecolor{VQLightBg}{HTML}{F8FAFC}344345% =====================================================346% METROPOLIS OVERRIDES347% =====================================================348\\setbeamercolor{normal text}{fg=VQDark, bg=white}349\\setbeamercolor{alerted text}{fg=VQRed}350\\setbeamercolor{example text}{fg=VQGreen}351\\setbeamercolor{structure}{fg=VQTeal}352353\\setbeamercolor{progress bar}{fg=VQTeal, bg=VQTeal!15}354\\setbeamercolor{title separator}{fg=VQTeal}355\\setbeamercolor{frametitle}{fg=VQDark, bg=VQLightBg}356357% Blocks — Glassmorphism-inspired358\\setbeamercolor{block title}{fg=white, bg=VQTeal}359\\setbeamercolor{block body}{fg=VQDark, bg=VQTeal!5}360\\setbeamercolor{block title alerted}{fg=white, bg=VQRed}361\\setbeamercolor{block body alerted}{fg=VQDark, bg=VQRed!5}362\\setbeamercolor{block title example}{fg=white, bg=VQGreen!80!black}363\\setbeamercolor{block body example}{fg=VQDark, bg=VQGreen!5}364\\setbeamertemplate{blocks}[rounded][shadow=false]365366% Navigation367\\setbeamertemplate{navigation symbols}{}368369% Items — Clean circles with gradient feel370\\setbeamertemplate{itemize items}[circle]371\\setbeamercolor{itemize item}{fg=VQTeal}372\\setbeamercolor{itemize subitem}{fg=VQBlue}373\\setbeamercolor{itemize subsubitem}{fg=VQGray}374\\setbeamercolor{enumerate item}{fg=VQTeal}375\\setbeamercolor{enumerate subitem}{fg=VQBlue}376377% TOC378\\setbeamercolor{section in toc}{fg=VQDark}379\\setbeamercolor{subsection in toc}{fg=VQGray}380\\setbeamercolor{section number projected}{bg=VQTeal, fg=white}381382% Standout383\\setbeamercolor{standout}{fg=white, bg=VQTeal}384385% Footer386\\setbeamertemplate{footline}{387 \\hbox{%388 \\begin{beamercolorbox}[wd=\\paperwidth,ht=3ex,dp=1.5ex]{footline}%389 \\hspace{1em}%390 {\\tiny\\color{VQGray}\\textbf{VQuant} \\,|\\, ${today}}%391 \\hfill%392 {\\tiny\\color{VQGray}\\insertframenumber\\,/\\,\\inserttotalframenumber}%393 \\hspace{1em}%394 \\end{beamercolorbox}%395 }396}397398% =====================================================399% CUSTOM TCOLORBOXES400% =====================================================401\\newtcolorbox{keyinsight}{402 colback=VQTeal!5, colframe=VQTeal, coltitle=white,403 fonttitle=\\bfseries, title={\\faLightbulb\\hspace{0.5em}Point Clé},404 boxrule=0.5pt, arc=3pt, left=6pt, right=6pt, top=4pt, bottom=4pt405}406407\\newtcolorbox{warningbox}{408 colback=VQOrange!5, colframe=VQOrange, coltitle=white,409 fonttitle=\\bfseries, title={\\faExclamationTriangle\\hspace{0.5em}Attention},410 boxrule=0.5pt, arc=3pt, left=6pt, right=6pt, top=4pt, bottom=4pt411}412413% =====================================================414% TITLE INFORMATION415% =====================================================416\\title{${escTitle}}417\\subtitle{\\textcolor{VQGray}{Analyse Financière}}418\\author{${escA}}419\\institute{${escC}}420\\date{${today}}421422% =====================================================423\\begin{document}424425% ---- Custom Title Slide ----426{427\\setbeamercolor{background canvas}{bg=VQDark}428\\begin{frame}[plain,noframenumbering]429 \\begin{tikzpicture}[remember picture,overlay]430 % Gradient accent circles431 \\fill[VQTeal, opacity=0.15] (current page.north west) ++(-1,-2) circle (4cm);432 \\fill[VQBlue, opacity=0.1] (current page.south east) ++(1,2) circle (5cm);433 \\fill[VQPurple, opacity=0.08] (current page.north east) ++(0,-4) circle (3cm);434 \\end{tikzpicture}435436 \\vfill437 \\begin{center}438 {\\Large\\color{VQTeal}\\textbf{VQuant}}\\\\[2pt]439 {\\tiny\\color{VQGray}Financial Intelligence Platform}\\\\[20pt]440 {\\LARGE\\color{white}\\textbf{${escTitle}}}\\\\[12pt]441 {\\color{VQTeal}\\rule{60pt}{2pt}}\\\\[12pt]442 {\\normalsize\\color{VQGray}${escA}}\\\\[4pt]443 {\\small\\color{VQGray!70}${escC} \\,|\\, ${today}}444 \\end{center}445 \\vfill446\\end{frame}447}448449% Content from Claude450${content}451452% ---- Final Slide ----453{454\\setbeamercolor{background canvas}{bg=VQDark}455\\begin{frame}[plain,noframenumbering]456 \\begin{tikzpicture}[remember picture,overlay]457 \\fill[VQTeal, opacity=0.12] (current page.south west) ++(-1,2) circle (4cm);458 \\fill[VQPurple, opacity=0.08] (current page.north east) ++(0,-3) circle (3cm);459 \\end{tikzpicture}460461 \\vfill462 \\begin{center}463 {\\Huge\\color{white}\\textbf{Merci}}\\\\[20pt]464 {\\color{VQTeal}\\rule{40pt}{2pt}}\\\\[16pt]465 {\\normalsize\\color{VQGray}Questions \\& Suggestions}\\\\[12pt]466 {\\small\\color{VQTeal}\\faEnvelope\\hspace{0.5em}\\href{mailto:admin@vquant.ai}{admin@vquant.ai}}\\\\[6pt]467 {\\small\\color{VQTeal}\\faGlobe\\hspace{0.5em}\\href{https://www.vquant.ai}{www.vquant.ai}}\\\\[20pt]468 {\\footnotesize\\color{VQGray!60}Généré avec \\textbf{\\color{VQTeal}VQuant AI}}469 \\end{center}470 \\vfill471\\end{frame}472}473474\\end{document}`;475}476477/**478 * Compiles .tex file to PDF using pdflatex479 */480async function compileToPDF(texContent: string, outputName: string, figureUrls?: string[]): Promise<Buffer> {481 const tempDir = path.join(__dirname, '../../temp');482 const fileName = `slides-${outputName}`;483 const texPath = path.join(tempDir, `${fileName}.tex`);484 const pdfPath = path.join(tempDir, `${fileName}.pdf`);485486 try {487 // Create temp directory if doesn't exist488 await fs.mkdir(tempDir, { recursive: true });489490 // Copy figures into temp dir so LaTeX can find them491 if (figureUrls && figureUrls.length > 0) {492 const figuresSourceDir = path.join(process.cwd(), 'dist', 'public', 'figures');493 for (let i = 0; i < figureUrls.length; i++) {494 const url = figureUrls[i]; // e.g. "/figures/fig-123-0.png"495 const sourceFilename = url.replace('/figures/', '');496 const sourcePath = path.join(figuresSourceDir, sourceFilename);497 const destPath = path.join(tempDir, `figure-${i}.png`);498 try {499 await fs.copyFile(sourcePath, destPath);500 console.log(`[BeamerSlides] Copied figure ${i}: ${sourceFilename}`);501 } catch (err) {502 console.warn(`[BeamerSlides] Could not copy figure ${sourceFilename}:`, (err as Error).message);503 }504 }505 }506507 // Write .tex file508 await fs.writeFile(texPath, texContent, 'utf-8');509 console.log('[BeamerSlides] .tex file written:', texPath);510511 // Compile with pdflatex (2 passes for references)512 const pdflatexPath = '/Library/TeX/texbin/pdflatex';513514 for (let i = 1; i <= 2; i++) {515 console.log(`[BeamerSlides] Compiling pass ${i}/2...`);516517 await new Promise<void>((resolve, reject) => {518 const process = spawn(pdflatexPath, [519 '-interaction=nonstopmode',520 '-output-directory=' + tempDir,521 texPath522 ], {523 cwd: tempDir524 });525526 let stdout = '';527 let stderr = '';528529 process.stdout.on('data', (data) => {530 stdout += data.toString();531 });532533 process.stderr.on('data', (data) => {534 stderr += data.toString();535 });536537 process.on('close', async (code) => {538 // pdflatex peut retourner code 1 même si PDF généré (à cause de warnings)539 // Donc on vérifie si le PDF existe au lieu de juste regarder le code de retour540 try {541 const stats = await fs.stat(pdfPath);542 if (stats.size > 0) {543 console.log(`[BeamerSlides] ✓ Pass ${i} completed (PDF exists, ${stats.size} bytes)`);544 resolve();545 return;546 }547 } catch {548 // PDF doesn't exist549 }550551 // If we get here, PDF wasn't generated552 if (code !== 0) {553 console.error('[BeamerSlides] pdflatex exit code:', code);554 console.error('[BeamerSlides] pdflatex stderr:', stderr);555 console.error('[BeamerSlides] pdflatex stdout (last 500 chars):', stdout.substring(Math.max(0, stdout.length - 500)));556 reject(new Error(`pdflatex failed with code ${code}`));557 } else {558 resolve();559 }560 });561562 process.on('error', (error) => {563 reject(new Error(`Failed to start pdflatex: ${error.message}`));564 });565 });566 }567568 // Check if PDF was generated569 try {570 await fs.access(pdfPath);571 } catch {572 throw new Error('PDF file was not generated - compilation may have failed');573 }574575 // Read the generated PDF576 const pdfBuffer = await fs.readFile(pdfPath);577 console.log('[BeamerSlides] ✓ PDF generated successfully:', pdfBuffer.length, 'bytes');578579 if (pdfBuffer.length === 0) {580 throw new Error('PDF file is empty');581 }582583 // Cleanup temp files (but keep for debugging if cleanup fails)584 setTimeout(async () => {585 try {586 await fs.unlink(texPath);587 await fs.unlink(path.join(tempDir, `${fileName}.aux`));588 await fs.unlink(path.join(tempDir, `${fileName}.log`));589 await fs.unlink(path.join(tempDir, `${fileName}.nav`));590 await fs.unlink(path.join(tempDir, `${fileName}.out`));591 await fs.unlink(path.join(tempDir, `${fileName}.snm`));592 await fs.unlink(path.join(tempDir, `${fileName}.toc`));593 console.log('[BeamerSlides] Temp files cleaned up');594 } catch (cleanupError) {595 console.warn('[BeamerSlides] Cleanup failed (files kept for debugging):', (cleanupError as Error).message);596 }597 }, 5000); // Cleanup after 5 seconds to ensure PDF is sent598599 // Keep PDF for a bit longer in case of issues600 setTimeout(async () => {601 try {602 await fs.unlink(pdfPath);603 console.log('[BeamerSlides] PDF temp file deleted');604 } catch {605 console.warn('[BeamerSlides] PDF cleanup failed');606 }607 }, 30000); // Delete PDF after 30 seconds608609 return pdfBuffer;610611 } catch (error) {612 console.error('[BeamerSlides] Compilation error:', error);613 console.error('[BeamerSlides] Error details:', (error as Error).message);614615 // Try to read log file for more info616 try {617 const logPath = path.join(tempDir, `${fileName}.log`);618 const logContent = await fs.readFile(logPath, 'utf-8');619 const errorLines = logContent.split('\n').filter(line =>620 line.includes('Error') || line.includes('!')621 ).slice(0, 10);622 if (errorLines.length > 0) {623 console.error('[BeamerSlides] LaTeX errors from log:', errorLines.join('\n'));624 }625 } catch (logError) {626 console.warn('[BeamerSlides] Could not read log file');627 }628629 throw error;630 }631}632633/**634 * Main function: Convert analysis to Beamer PDF635 */636export async function convertToBeamerPDF(options: BeamerOptions): Promise<Buffer> {637 console.log('[BeamerSlides] Starting Beamer PDF generation...');638639 // Step 1: Generate LaTeX content with Claude640 const latexContent = await generateBeamerStructure(options);641642 // Step 2: Create complete .tex document643 const fullDocument = createBeamerDocument(latexContent, options);644645 // Step 3: Compile to PDF (with figures if available)646 const pdfBuffer = await compileToPDF(fullDocument, Date.now().toString(), options.figureUrls);647648 console.log('[BeamerSlides] ✓ Complete! PDF ready:', pdfBuffer.length, 'bytes');649 return pdfBuffer;650}651