Implement RPC contract validation baseline
This commit is contained in:
435
packages/shared/src/export/pptx.ts
Normal file
435
packages/shared/src/export/pptx.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* 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`;
|
||||
}
|
||||
Reference in New Issue
Block a user