edtech / apps /api /src /services /renderers /pptx-renderer.ts
CognxSafeTrack
feat(ai): team building visual experience and persistence
7b4936e
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();
// 🎨 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 => ({ 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) {
console.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;
}
}