File size: 6,927 Bytes
de6a95b 2d7440e 2849f33 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2d7440e eac938a 2849f33 eac938a 2d7440e eac938a 7b4936e eac938a de6a95b eac938a 2d7440e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | import { logger } from '../../logger';
import PptxGenJS from 'pptxgenjs';
import { PitchDeckData, SlideData } from '@repo/ai-sdk';
import { DocumentRenderer } from './types';
export class PptxDeckRenderer implements DocumentRenderer<PitchDeckData> {
async render(data: PitchDeckData): Promise<Buffer> {
const pres = new PptxGenJS();
// 🎨 Premium Design System
// XAMLÉ Palette: Emerald (#1C7C54), Navy (#1B3A57), Saffron (#F4A261)
pres.defineSlideMaster({
title: 'MASTER_SLIDE',
bkgd: 'FFFFFF',
objects: [
{ rect: { x: 0, y: 0, w: '100%', h: 1.0, fill: { color: '1B3A57' } } }, // Header
{ rect: { x: 0, y: 1.0, w: '100%', h: 0.05, fill: { color: 'F4A261' } } }, // Accent line
{ text: { text: "XAMLÉ 🇸🇳 - Pitch Deck Stratégique", options: { x: 0.5, y: 5.15, fontSize: 10, color: '1B3A57' } } }
]
});
// 1. Title Slide (Slide 1)
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'
});
// 2. Content Slides
data.slides.forEach((slideData: SlideData, index: number) => {
const slide = pres.addSlide({ masterName: 'MASTER_SLIDE' });
// Slide Title (Montserrat Bold, White on Navy)
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%';
// 📝 Storytelling Text Blocks (Inter)
const textBlocks = slideData.content.map((text: string) => ({ 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
});
// 📊 Graphical Integration (WOW Effect - Flat Design)
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' });
}
else if (slideData.visualType === 'TEAM' && Array.isArray(vizData)) {
const maxMembers = Math.min(vizData.length, 4);
for (let i = 0; i < maxMembers; i++) {
const member = vizData[i];
const row = Math.floor(i / 2);
const col = i % 2;
const xP = 5.2 + (col * 2.3);
const yP = 1.0 + (row * 2.2);
if (member.photoUrl && typeof member.photoUrl === 'string' && member.photoUrl.startsWith('http')) {
slide.addImage({ path: member.photoUrl, x: xP, y: yP, w: 1.8, h: 1.8, sizing: { type: 'cover', w: 1.8, h: 1.8 } });
} else {
// Default icon or placeholder shape
slide.addShape(pres.ShapeType.ellipse, { x: xP + 0.15, y: yP + 0.15, w: 1.5, h: 1.5, fill: { color: 'E2E8F0' } });
}
slide.addText(member.name?.toUpperCase() || "MEMBRE", {
x: xP, y: yP + 1.85, w: 1.8, h: 0.3,
fontSize: 12, color: '1B3A57', bold: true, align: 'center', fontFace: 'Montserrat'
});
slide.addText(member.role || "Rôle", {
x: xP, y: yP + 2.15, w: 1.8, h: 0.2,
fontSize: 10, color: '1C7C54', align: 'center', fontFace: 'Inter'
});
}
}
} catch (vErr) {
logger.warn(`[RENDERER] Failed to add visual to slide ${index + 1}:`, vErr);
}
}
// Speaker Notes
if (slideData.notes) {
slide.addNotes(slideData.notes);
}
});
const buffer = await pres.write({ outputType: 'nodebuffer' }) as Buffer;
return buffer;
}
}
|