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;
    }
}