/** * PowerPoint export functionality * Generates presentation decks from memos and models */ import PptxGenJS from "pptxgenjs"; import type { Memo, Company } from "@mosaiciq/contracts/rpc"; export interface PPTXOptions { includeCharts?: boolean; includeModel?: boolean; maxBulletPoints?: number; } /** * Export memo as PowerPoint presentation */ export async function exportPresentation( memo: Memo, company: Company, options: PPTXOptions = {} ): Promise { const { includeCharts = true, includeModel = true, maxBulletPoints = 5, } = options; const pptx = new (PptxGenJS as any)(); pptx.author = "MosaicIQ"; pptx.company = "MosaicIQ Research"; pptx.title = `${company.name} Investment Committee Presentation`; pptx.subject = "Investment Research"; const COLORS = { primary: "4472C4", secondary: "6C757D", accent: "28A745", warning: "FFC107", danger: "DC3545", dark: "363636", light: "F8F9FA", }; // Slide 1: Title slide addTitleSlide(pptx, company, COLORS); // Slide 2: Investment Thesis const thesisSection = memo.sections.find((s) => s.id === "thesis"); if (thesisSection) { addThesisSlide(pptx, thesisSection, COLORS); } // Slide 3: Key Drivers const driversSection = memo.sections.find((s) => s.id === "drivers"); if (driversSection) { addDriversSlide(pptx, driversSection, COLORS, maxBulletPoints); } // Slide 4: Business Quality const qualitySection = memo.sections.find((s) => s.id === "quality"); if (qualitySection) { addContentSlide(pptx, "Business Quality", qualitySection.content, COLORS); } // Slide 5: Financial Summary const financialsSection = memo.sections.find((s) => s.id === "financials"); if (financialsSection) { addContentSlide(pptx, "Financial Summary", financialsSection.content, COLORS); } // Slide 6: Valuation const valuationSection = memo.sections.find((s) => s.id === "valuation"); if (valuationSection) { addValuationSlide(pptx, valuationSection, COLORS); } // Slide 7: Key Risks const risksSection = memo.sections.find((s) => s.id === "risks"); if (risksSection) { addRisksSlide(pptx, risksSection.content, COLORS, maxBulletPoints); } // Slide 8: Catalysts const catalystsSection = memo.sections.find((s) => s.id === "catalysts"); if (catalystsSection) { addCatalystsSlide(pptx, catalystsSection.content, COLORS, maxBulletPoints); } // Slide 9: Conclusion/Recommendation addConclusionSlide(pptx, company, COLORS); // Generate buffer const buffer = await pptx.write({ outputType: "nodebuffer", compression: false }) as Buffer; return buffer; } function addTitleSlide(pptx: any, company: Company, COLORS: any) { const slide = pptx.addSlide(); // Background color slide.background = { color: COLORS.light }; // Company name and ticker slide.addText(company.name, { x: 0.5, y: 1.5, w: 9, h: 0.8, fontSize: 44, bold: true, color: COLORS.dark, fontFace: "Arial", }); slide.addText(`(${company.ticker})`, { x: 0.5, y: 2.2, w: 9, h: 0.4, fontSize: 24, color: COLORS.secondary, fontFace: "Arial", }); // Subtitle slide.addText("Investment Committee Presentation", { x: 0.5, y: 3.0, w: 9, h: 0.5, fontSize: 20, color: COLORS.primary, fontFace: "Arial", }); // Date slide.addText(new Date().toLocaleDateString(), { x: 0.5, y: 3.6, w: 9, h: 0.4, fontSize: 16, color: COLORS.secondary, fontFace: "Arial", }); // Divider line slide.addShape("line", { x: 0.5, y: 4.2, w: 9, h: 0, line: { color: COLORS.primary, width: 2 }, }); // Sector info slide.addText(`Sector: ${company.sector}`, { x: 0.5, y: 4.5, w: 9, h: 0.4, fontSize: 14, color: COLORS.secondary, fontFace: "Arial", }); } function addThesisSlide(pptx: any, section: any, COLORS: any) { const slide = pptx.addSlide(); // Title slide.addText("Investment Thesis", { x: 0.5, y: 0.5, w: 9, h: 0.5, fontSize: 32, bold: true, color: COLORS.primary, fontFace: "Arial", }); // Content (truncated if too long) const content = section.content.slice(0, 600) + (section.content.length > 600 ? "..." : ""); slide.addText(content, { x: 0.5, y: 1.2, w: 9, h: 4, fontSize: 18, color: COLORS.dark, fontFace: "Arial", align: "justify", }); } function addDriversSlide(pptx: any, section: any, COLORS: any, maxBullets: number) { const slide = pptx.addSlide(); // Title slide.addText("Key Value Drivers", { x: 0.5, y: 0.5, w: 9, h: 0.5, fontSize: 32, bold: true, color: COLORS.primary, fontFace: "Arial", }); // Parse bullets (simplified - assumes line breaks separate bullets) const lines = section.content.split("\n").filter((l: string) => l.trim().length > 0); const bullets = lines.slice(0, maxBullets); bullets.forEach((bullet: string, index: number) => { slide.addText(bullet.trim(), { x: 0.5, y: 1.2 + index * 0.5, w: 9, h: 0.4, fontSize: 18, color: COLORS.dark, fontFace: "Arial", bullet: true, }); }); } function addValuationSlide(pptx: any, section: any, COLORS: any) { const slide = pptx.addSlide(); // Title slide.addText("Valuation", { x: 0.5, y: 0.5, w: 9, h: 0.5, fontSize: 32, bold: true, color: COLORS.primary, fontFace: "Arial", }); // Content const content = section.content.slice(0, 500); slide.addText(content, { x: 0.5, y: 1.2, w: 9, h: 3, fontSize: 16, color: COLORS.dark, fontFace: "Arial", align: "justify", }); // Valuation summary box slide.addShape("rect", { x: 0.5, y: 4.5, w: 9, h: 1.5, fill: { color: COLORS.light }, line: { color: COLORS.primary, width: 1 }, }); slide.addText("Valuation Summary", { x: 0.7, y: 4.7, w: 8.6, h: 0.3, fontSize: 14, bold: true, color: COLORS.primary, fontFace: "Arial", }); } function addRisksSlide(pptx: any, content: string, COLORS: any, maxBullets: number) { const slide = pptx.addSlide(); // Title slide.addText("Key Risks", { x: 0.5, y: 0.5, w: 9, h: 0.5, fontSize: 32, bold: true, color: COLORS.danger, fontFace: "Arial", }); // Parse bullets const lines = content.split("\n").filter((l) => l.trim().length > 0); const bullets = lines.slice(0, maxBullets); bullets.forEach((bullet, index) => { slide.addText(bullet.trim(), { x: 0.5, y: 1.2 + index * 0.5, w: 9, h: 0.4, fontSize: 16, color: COLORS.dark, fontFace: "Arial", bullet: true, }); }); } function addCatalystsSlide(pptx: any, content: string, COLORS: any, maxBullets: number) { const slide = pptx.addSlide(); // Title slide.addText("Upcoming Catalysts", { x: 0.5, y: 0.5, w: 9, h: 0.5, fontSize: 32, bold: true, color: COLORS.accent, fontFace: "Arial", }); // Parse bullets const lines = content.split("\n").filter((l) => l.trim().length > 0); const bullets = lines.slice(0, maxBullets); bullets.forEach((bullet, index) => { slide.addText(bullet.trim(), { x: 0.5, y: 1.2 + index * 0.5, w: 9, h: 0.4, fontSize: 16, color: COLORS.dark, fontFace: "Arial", bullet: true, }); }); } function addContentSlide(pptx: any, title: string, content: string, COLORS: any) { const slide = pptx.addSlide(); // Title slide.addText(title, { x: 0.5, y: 0.5, w: 9, h: 0.5, fontSize: 32, bold: true, color: COLORS.primary, fontFace: "Arial", }); // Content const truncated = content.slice(0, 700); slide.addText(truncated, { x: 0.5, y: 1.2, w: 9, h: 5, fontSize: 16, color: COLORS.dark, fontFace: "Arial", align: "justify", }); } function addConclusionSlide(pptx: any, company: Company, COLORS: any) { const slide = pptx.addSlide(); // Title slide.addText("Conclusion & Recommendation", { x: 0.5, y: 0.5, w: 9, h: 0.5, fontSize: 32, bold: true, color: COLORS.primary, fontFace: "Arial", }); // Recommendation box slide.addShape("rect", { x: 0.5, y: 1.2, w: 9, h: 2, fill: { color: "E7F3FF" }, line: { color: COLORS.primary, width: 2 }, }); slide.addText("Investment Recommendation", { x: 0.7, y: 1.4, w: 8.6, h: 0.4, fontSize: 18, bold: true, color: COLORS.primary, fontFace: "Arial", }); // Disclaimer slide.addText( "This presentation is for informational purposes only and does not constitute investment advice. " + "Please refer to the full investment memo for detailed analysis and disclosures.", { x: 0.5, y: 5.5, w: 9, h: 1, fontSize: 10, color: COLORS.secondary, fontFace: "Arial", align: "center", } ); } /** * Generate filename for presentation export */ export function getPresentationFilename(company: Company): string { const date = new Date().toISOString().split("T")[0]; return `${company.ticker}_Presentation_${date}.pptx`; }