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%
22.5 KB · 651 lines typescript
Raw Blame History
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