436 lines
9.1 KiB
TypeScript
436 lines
9.1 KiB
TypeScript
/**
|
|
* 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<Buffer> {
|
|
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`;
|
|
}
|