| | import PptxGenJS from 'pptxgenjs'; |
| | import { PitchDeckData, SlideData } from '../ai/types'; |
| | import { DocumentRenderer } from './types'; |
| |
|
| | export class PptxDeckRenderer implements DocumentRenderer<PitchDeckData> { |
| | async render(data: PitchDeckData): Promise<Buffer> { |
| | const pres = new PptxGenJS(); |
| |
|
| | |
| | |
| | pres.defineSlideMaster({ |
| | title: 'MASTER_SLIDE', |
| | bkgd: 'FFFFFF', |
| | objects: [ |
| | { rect: { x: 0, y: 0, w: '100%', h: 1.0, fill: { color: '1B3A57' } } }, |
| | { rect: { x: 0, y: 1.0, w: '100%', h: 0.05, fill: { color: 'F4A261' } } }, |
| | { text: { text: "XAMLÉ 🇸🇳 - Pitch Deck Stratégique", options: { x: 0.5, y: 5.15, fontSize: 10, color: '1B3A57' } } } |
| | ] |
| | }); |
| |
|
| | |
| | const titleSlide = pres.addSlide(); |
| | titleSlide.bkgd = '1B3A57'; |
| | titleSlide.addText(data.title.toUpperCase(), { |
| | x: 0, y: 2, w: '100%', h: 1, |
| | fontSize: 48, color: 'FFFFFF', bold: true, fontFace: 'Montserrat', align: 'center' |
| | }); |
| | if (data.subtitle) { |
| | titleSlide.addText(data.subtitle, { |
| | x: 0, y: 3.2, w: '100%', h: 1, |
| | fontSize: 22, color: 'F4A261', fontFace: 'Inter', align: 'center', italic: true |
| | }); |
| | } |
| | titleSlide.addText("Propulsé par XAMLÉ AI", { |
| | x: 0, y: 4.8, w: '100%', h: 0.5, |
| | fontSize: 14, color: 'FFFFFF', fontFace: 'Inter', align: 'center' |
| | }); |
| |
|
| | |
| | data.slides.forEach((slideData: SlideData, index: number) => { |
| | const slide = pres.addSlide({ masterName: 'MASTER_SLIDE' }); |
| |
|
| | |
| | slide.addText(slideData.title.toUpperCase(), { |
| | x: 0.5, y: 0.25, w: '90%', h: 0.5, |
| | fontSize: 28, color: 'FFFFFF', bold: true, fontFace: 'Montserrat' |
| | }); |
| |
|
| | const hasVisual = slideData.visualType && slideData.visualType !== 'NONE' && slideData.visualData; |
| | const textWidth = hasVisual ? '50%' : '90%'; |
| |
|
| | |
| | const textBlocks = slideData.content.map(text => ({ text: text + '\n', options: { breakLine: true } })); |
| | slide.addText(textBlocks, { |
| | x: 0.5, y: 1.3, w: textWidth, h: '75%', |
| | fontSize: 15, color: '2D3748', fontFace: 'Inter', |
| | valign: 'top', margin: 5, lineSpacing: 22 |
| | }); |
| |
|
| | |
| | if (hasVisual) { |
| | try { |
| | const vizData = slideData.visualData; |
| | if (slideData.visualType === 'PIE_CHART' && vizData.labels && vizData.values) { |
| | const chartData = [{ name: 'Market Scale', labels: vizData.labels, values: vizData.values }]; |
| | slide.addChart(pres.ChartType.pie, chartData, { |
| | x: 5.2, y: 1.2, w: 4.5, h: 4.0, |
| | showLegend: true, legendPos: 'r', |
| | showValue: true, showPercent: true, |
| | chartColors: ['1C7C54', 'F4A261', '1B3A57'], |
| | dataLabelColor: 'FFFFFF' |
| | }); |
| | } |
| | else if (slideData.visualType === 'BAR_CHART' && vizData.labels && vizData.values) { |
| | const chartData = [{ name: 'Projections (FCFA)', labels: vizData.labels, values: vizData.values }]; |
| | slide.addChart(pres.ChartType.bar, chartData, { |
| | x: 5.2, y: 1.2, w: 4.5, h: 4.0, |
| | barDir: 'col', |
| | showValue: true, |
| | chartColors: ['1C7C54', 'F4A261', '1B3A57'], |
| | dataLabelColor: '2D3748', |
| | catAxisLabelColor: '2D3748', |
| | valAxisLabelColor: '2D3748' |
| | }); |
| | } |
| | else if (slideData.visualType === 'IMAGE' && typeof vizData === 'string' && vizData.startsWith('http')) { |
| | slide.addImage({ path: vizData, x: 5.2, y: 1.0, w: 4.5, h: 4.2 }); |
| | } |
| | else if (slideData.visualType === 'IMAGE') { |
| | slide.addText("📷 [Visual AI Placeholder]", { x: 6.5, y: 2.5, fontSize: 14, color: 'A0AEC0', fontFace: 'Inter' }); |
| | } |
| | } catch (vErr) { |
| | console.warn(`[RENDERER] Failed to add visual to slide ${index + 1}:`, vErr); |
| | } |
| | } |
| |
|
| | |
| | if (slideData.notes) { |
| | slide.addNotes(slideData.notes); |
| | } |
| | }); |
| |
|
| | const buffer = await pres.write({ outputType: 'nodebuffer' }) as Buffer; |
| | return buffer; |
| | } |
| | } |
| |
|