import type PptxGenJSModule from 'pptxgenjs' let _PptxGenJS: (new () => PptxGenJSModule) | null = null async function getPptxGenClass(): Promise PptxGenJSModule> { if (!_PptxGenJS) { const mod = await import('pptxgenjs') _PptxGenJS = (mod.default ?? mod) as unknown as new () => PptxGenJSModule } return _PptxGenJS } // ── Theme — cohérent avec l'identité visuelle Memento ─────────────────────── const T = { bg: 'F2F0E9', primary: '1C1C1C', accent: 'A47148', secondary: 'D4A373', muted: '9A8C87', wave1: 'fb923c', wave2: '60a5fa', wave3: 'a78bfa', green: '10b981', } const WAVE_LABELS: Record = { 1: '🔄 Variations', 2: '🔗 Analogies', 3: '💥 Disruptions', } const WAVE_COLORS: Record = { 1: T.wave1, 2: T.wave2, 3: T.wave3, } // ── Types (minimal subset) ────────────────────────────────────────────────── interface IdeaLike { id: string title: string description: string waveNumber: number status: string isStarred: boolean convertedToNoteId: string | null } interface SessionLike { seedIdea: string createdAt: Date ideas: IdeaLike[] } // ── Helpers ───────────────────────────────────────────────────────────────── function slugify(text: string): string { return text .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .slice(0, 40) } function truncate(text: string, max: number): string { return text.length > max ? text.slice(0, max - 1) + '…' : text } /** * PLG viral watermark — added to every slide automatically via addSlide monkey-patch. * LAYOUT_WIDE = 13.33" × 7.5" → watermark anchored near bottom-right at y ≈ 7.1 */ function addWatermark(slide: any) { slide.addText('memento-note.com', { x: 10.0, y: 7.1, w: 3.0, h: 0.25, fontSize: 7, fontFace: 'Arial', color: 'B8B0A8', align: 'right', italic: true, }) } /** Wrap pres.addSlide so every new slide gets the Memento watermark automatically */ function withWatermark(pres: PptxGenJSModule): PptxGenJSModule { const original = pres.addSlide.bind(pres) ;(pres as any).addSlide = (...args: any[]) => { const slide = original(...args) addWatermark(slide) return slide } return pres } // Add a consistent slide background function addBg(slide: any) { slide.background = { color: T.bg } } // Accent bar at the top of a slide function addTopBar(slide: any, color: string = T.accent) { slide.addShape('rect', { x: 0, y: 0, w: '100%', h: 0.12, fill: { color } }) } // ── Slide builders ─────────────────────────────────────────────────────────── function buildCoverSlide(pres: PptxGenJSModule, session: SessionLike, stats: { total: number; converted: number; starred: number }) { const slide = pres.addSlide() addBg(slide) // Left accent panel slide.addShape('rect', { x: 0, y: 0, w: 3.6, h: '100%', fill: { color: T.primary } }) // Brand label on accent panel slide.addText('MOMENTO', { x: 0.3, y: 0.4, w: 3.0, h: 0.4, fontSize: 9, fontFace: 'Arial', color: T.accent, bold: true, charSpacing: 4, align: 'left', }) // Seed idea (big) on right slide.addText(truncate(session.seedIdea, 80), { x: 4.0, y: 1.2, w: 5.6, h: 2.4, fontSize: 26, fontFace: 'Georgia', color: T.primary, bold: false, align: 'left', valign: 'middle', wrap: true, }) // Date slide.addText(session.createdAt.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }), { x: 4.0, y: 4.0, w: 5.6, h: 0.4, fontSize: 10, fontFace: 'Arial', color: T.muted, align: 'left', }) // Stats row const statItems = [ { label: 'idées', value: String(stats.total) }, { label: 'converties', value: String(stats.converted) }, { label: 'favorites', value: String(stats.starred) }, ] statItems.forEach((s, i) => { const x = 4.0 + i * 2.0 slide.addText(s.value, { x, y: 4.8, w: 1.8, h: 0.55, fontSize: 22, fontFace: 'Georgia', color: T.accent, bold: true, align: 'left' }) slide.addText(s.label, { x, y: 5.35, w: 1.8, h: 0.3, fontSize: 9, fontFace: 'Arial', color: T.muted, align: 'left', charSpacing: 1 }) }) // Vertical label "BRAINSTORM" on left panel slide.addText('BRAINSTORM', { x: 0.1, y: 2.0, w: 3.2, h: 0.5, fontSize: 11, fontFace: 'Arial', color: 'FFFFFF', bold: true, charSpacing: 6, align: 'center', rotate: 270, }) } function buildWaveSlide(pres: PptxGenJSModule, wave: number, ideas: IdeaLike[]) { const slide = pres.addSlide() addBg(slide) addTopBar(slide, WAVE_COLORS[wave] || T.accent) // Wave label slide.addText(WAVE_LABELS[wave] || `Wave ${wave}`, { x: 0.5, y: 0.25, w: 9.0, h: 0.5, fontSize: 14, fontFace: 'Arial', color: WAVE_COLORS[wave] || T.accent, bold: true, charSpacing: 2, }) // Count badge slide.addShape('roundRect', { x: 9.0, y: 0.28, w: 0.6, h: 0.4, fill: { color: WAVE_COLORS[wave] || T.accent }, rectRadius: 0.1 }) slide.addText(String(ideas.length), { x: 9.0, y: 0.28, w: 0.6, h: 0.4, fontSize: 11, fontFace: 'Arial', color: 'FFFFFF', bold: true, align: 'center', valign: 'middle' }) const maxPerSlide = 6 const shown = ideas.slice(0, maxPerSlide) const colW = 4.5 const rowH = 1.3 const startY = 1.0 shown.forEach((idea, i) => { const col = i % 2 const row = Math.floor(i / 2) const x = 0.4 + col * (colW + 0.3) const y = startY + row * (rowH + 0.15) // Card background slide.addShape('roundRect', { x, y, w: colW, h: rowH, fill: { color: 'FFFFFF' }, line: { color: 'E8E6E0', width: 0.75 }, rectRadius: 0.08 }) // Star / converted badge if (idea.isStarred || idea.convertedToNoteId) { const badge = idea.convertedToNoteId ? '✓' : '⭐' const badgeColor = idea.convertedToNoteId ? T.green : T.wave1 slide.addText(badge, { x: x + colW - 0.45, y: y + 0.08, w: 0.35, h: 0.35, fontSize: 10, fontFace: 'Arial', color: badgeColor, align: 'center' }) } // Title slide.addText(truncate(idea.title, 55), { x: x + 0.18, y: y + 0.12, w: colW - 0.55, h: 0.4, fontSize: 11, fontFace: 'Arial', color: T.primary, bold: true, wrap: true, }) // Description slide.addText(truncate(idea.description, 120), { x: x + 0.18, y: y + 0.52, w: colW - 0.36, h: 0.65, fontSize: 9, fontFace: 'Arial', color: T.muted, wrap: true, valign: 'top', }) }) if (ideas.length > maxPerSlide) { slide.addText(`+ ${ideas.length - maxPerSlide} autres idées`, { x: 0.4, y: 5.5, w: 9.2, h: 0.3, fontSize: 9, fontFace: 'Arial', color: T.muted, align: 'center', italic: true, }) } } function buildTopIdeasSlide(pres: PptxGenJSModule, starred: IdeaLike[], converted: IdeaLike[]) { const slide = pres.addSlide() addBg(slide) addTopBar(slide, T.accent) slide.addText('Top Idées', { x: 0.5, y: 0.25, w: 9.0, h: 0.5, fontSize: 14, fontFace: 'Arial', color: T.accent, bold: true, charSpacing: 2, }) const all = [ ...starred.map(i => ({ ...i, badge: '⭐', badgeColor: T.wave1 })), ...converted.filter(i => !i.isStarred).map(i => ({ ...i, badge: '✓', badgeColor: T.green })), ].slice(0, 6) if (all.length === 0) { slide.addText('Aucune idée favorite ou convertie.', { x: 0.5, y: 3.0, w: 9.0, h: 0.5, fontSize: 12, fontFace: 'Georgia', color: T.muted, align: 'center', italic: true, }) return } const colW = 4.5 const rowH = 1.25 all.forEach((idea, i) => { const col = i % 2 const row = Math.floor(i / 2) const x = 0.4 + col * (colW + 0.3) const y = 1.1 + row * (rowH + 0.15) const waveColor = WAVE_COLORS[idea.waveNumber] || T.muted slide.addShape('roundRect', { x, y, w: colW, h: rowH, fill: { color: 'FFFFFF' }, line: { color: waveColor, width: 1.5 }, rectRadius: 0.08 }) slide.addText(idea.badge, { x: x + 0.1, y: y + 0.1, w: 0.4, h: 0.4, fontSize: 14, fontFace: 'Arial', color: idea.badgeColor, align: 'center' }) slide.addText(truncate(idea.title, 55), { x: x + 0.55, y: y + 0.1, w: colW - 0.7, h: 0.4, fontSize: 11, fontFace: 'Arial', color: T.primary, bold: true, wrap: true, }) slide.addText(truncate(idea.description, 110), { x: x + 0.18, y: y + 0.55, w: colW - 0.36, h: 0.6, fontSize: 9, fontFace: 'Arial', color: T.muted, wrap: true, valign: 'top', }) }) } function buildSummarySlide(pres: PptxGenJSModule, session: SessionLike, stats: { total: number; converted: number; starred: number; dismissed: number }) { const slide = pres.addSlide() addBg(slide) addTopBar(slide, T.accent) slide.addText('BILAN DE SESSION', { x: 0.8, y: 0.25, w: 11.5, h: 0.5, fontSize: 10, fontFace: 'Arial', color: T.accent, bold: true, charSpacing: 5, align: 'left', }) slide.addText(truncate(session.seedIdea, 80), { x: 0.8, y: 0.9, w: 11.5, h: 1.0, fontSize: 20, fontFace: 'Georgia', color: T.primary, align: 'left', wrap: true, }) // Divider slide.addShape('rect', { x: 0.8, y: 2.1, w: 11.5, h: 0.02, fill: { color: T.accent } }) // Stats cards const statCols = [ { label: 'IDÉES GÉNÉRÉES', value: String(stats.total), color: T.wave2, bg: 'EEF6FF' }, { label: 'CONVERTIES EN NOTES', value: String(stats.converted), color: T.green, bg: 'EDFAF4' }, { label: 'FAVORITES', value: String(stats.starred), color: T.wave1, bg: 'FFF4EE' }, { label: 'REJETÉES', value: String(stats.dismissed), color: T.muted, bg: 'F5F4F2' }, ] const cardW = 2.7 const cardH = 2.8 const startX = 0.8 const startY = 2.3 statCols.forEach((s, i) => { const x = startX + i * (cardW + 0.2) // Card bg slide.addShape('roundRect', { x, y: startY, w: cardW, h: cardH, fill: { color: s.bg }, rectRadius: 0.1 }) // Top accent strip slide.addShape('roundRect', { x, y: startY, w: cardW, h: 0.1, fill: { color: s.color }, rectRadius: 0.05 }) // Big number slide.addText(s.value, { x: x + 0.2, y: startY + 0.35, w: cardW - 0.4, h: 1.2, fontSize: 48, fontFace: 'Georgia', color: s.color, bold: true, align: 'left', valign: 'middle' }) // Label slide.addText(s.label, { x: x + 0.2, y: startY + 1.7, w: cardW - 0.4, h: 0.9, fontSize: 8, fontFace: 'Arial', color: T.muted, align: 'left', charSpacing: 1.5, wrap: true }) }) } // ── Main export function ───────────────────────────────────────────────────── export async function generateBrainstormPptx(session: SessionLike): Promise<{ buffer: Buffer; filename: string }> { const PptxGenJS = await getPptxGenClass() const pres = withWatermark(new PptxGenJS()) pres.layout = 'LAYOUT_WIDE' pres.author = 'Memento' pres.subject = `Brainstorm: ${session.seedIdea}` const activeIdeas = session.ideas.filter(i => i.status !== 'dismissed') const dismissedCount = session.ideas.filter(i => i.status === 'dismissed').length const converted = activeIdeas.filter(i => i.convertedToNoteId !== null) const starred = activeIdeas.filter(i => i.isStarred) const stats = { total: activeIdeas.length, converted: converted.length, starred: starred.length, dismissed: dismissedCount, } // Slide 1 — Cover buildCoverSlide(pres, session, stats) // Slides 2-4 — One per active wave for (const wave of [1, 2, 3]) { const waveIdeas = activeIdeas.filter(i => i.waveNumber === wave) if (waveIdeas.length === 0) continue buildWaveSlide(pres, wave, waveIdeas) } // Slide N — Top ideas (starred + converted) if (starred.length > 0 || converted.length > 0) { buildTopIdeasSlide(pres, starred, converted) } // Last slide — Summary buildSummarySlide(pres, session, stats) const buffer = (await pres.write({ outputType: 'nodebuffer' })) as unknown as Buffer const filename = `brainstorm-${slugify(session.seedIdea)}.pptx` return { buffer, filename } }