Files
our-claude-skills/custom-skills/03-ourdigital-presentation/code/scripts/generate_pptx.js
Andrew Yim b69e4b6f3a refactor: Reorganize skill numbering and update documentation
Skill Numbering Changes:
- 01-03: OurDigital core (was 30-32)
- 31-32: Notion tools (was 01-02)
- 99_archive: Renamed from _archive for sorting

New Files:
- AGENTS.md: Claude Code agent routing guide
- requirements.txt for 00-claude-code-setting, 32-notion-writer, 43-jamie-youtube-manager

Documentation Updates:
- CLAUDE.md: Updated skill inventory (23 skills)
- AUDIT_REPORT.md: Current completion status (91%)
- Archived REFACTORING_PLAN.md (most tasks complete)

Removed:
- ga-agent-skills/ (moved to separate repo ~/Project/dintel-ga4-agent)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 18:42:39 +07:00

474 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* Generate PowerPoint presentation from synthesis data
* Uses pptxgenjs library to create professional presentations
*/
const fs = require('fs');
const path = require('path');
// Note: In production, install via npm install pptxgenjs
// For this example, we'll show the structure
let PptxGenJS;
try {
PptxGenJS = require('pptxgenjs');
} catch (e) {
console.log('📦 Installing pptxgenjs... Run: npm install pptxgenjs');
process.exit(1);
}
class PresentationGenerator {
constructor(synthesisData) {
this.synthesis = synthesisData;
this.pptx = new PptxGenJS();
this.setupPresentation();
this.defineLayouts();
}
setupPresentation() {
// Set presentation properties
this.pptx.layout = 'LAYOUT_16x9';
this.pptx.author = this.synthesis.metadata.author || 'Research Team';
this.pptx.company = 'Generated by Research-to-Presentation';
this.pptx.title = this.synthesis.metadata.title || 'Research Presentation';
// Define theme colors
this.colors = {
primary: '#1a73e8',
secondary: '#34a853',
accent: '#ea4335',
dark: '#202124',
light: '#f8f9fa',
text: '#3c4043',
subtext: '#5f6368'
};
// Define font styles
this.fonts = {
title: { face: 'Arial', size: 44, bold: true },
heading: { face: 'Arial', size: 32, bold: true },
subheading: { face: 'Arial', size: 24, bold: false },
body: { face: 'Arial', size: 18 },
small: { face: 'Arial', size: 14 }
};
}
defineLayouts() {
// Define master slides/layouts
this.pptx.defineSlideMaster({
title: 'CUSTOM_LAYOUT',
background: { color: this.colors.light },
objects: [
// Footer with slide number
{
text: {
text: '[[slideNumber]]',
options: {
x: 8.5,
y: 5,
w: 1,
h: 0.4,
fontSize: 12,
color: this.colors.subtext
}
}
}
]
});
}
generate() {
console.log('🎨 Generating slides...');
this.synthesis.slide_plan.forEach(slide => {
switch(slide.type) {
case 'title':
this.createTitleSlide(slide);
break;
case 'executive_summary':
this.createExecutiveSummarySlide(slide);
break;
case 'agenda':
this.createAgendaSlide(slide);
break;
case 'content':
this.createContentSlide(slide);
break;
case 'data_visualization':
this.createDataSlide(slide);
break;
case 'recommendations':
this.createRecommendationsSlide(slide);
break;
case 'closing':
this.createClosingSlide(slide);
break;
default:
this.createContentSlide(slide);
}
});
return this.pptx;
}
createTitleSlide(slideData) {
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
// Add background gradient
slide.background = {
fill: {
type: 'gradient',
colors: [
{ color: this.colors.primary, position: 0 },
{ color: this.colors.secondary, position: 100 }
]
}
};
// Title
slide.addText(slideData.title, {
x: 0.5,
y: 2,
w: 9,
h: 1.5,
fontSize: 44,
bold: true,
color: 'FFFFFF',
align: 'center'
});
// Subtitle
if (slideData.subtitle) {
slide.addText(slideData.subtitle, {
x: 0.5,
y: 3.5,
w: 9,
h: 0.75,
fontSize: 24,
color: 'FFFFFF',
align: 'center'
});
}
// Speaker notes
if (slideData.speaker_notes) {
slide.addNotes(slideData.speaker_notes);
}
}
createExecutiveSummarySlide(slideData) {
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
// Title
slide.addText(slideData.title, {
x: 0.5,
y: 0.5,
w: 9,
h: 0.75,
...this.fonts.heading,
color: this.colors.primary
});
// Summary content
slide.addText(slideData.content, {
x: 0.5,
y: 1.5,
w: 9,
h: 1.5,
...this.fonts.body,
color: this.colors.text
});
// Key points
if (slideData.key_points && slideData.key_points.length > 0) {
const bulletPoints = slideData.key_points.map(point => ({
text: point,
options: { bullet: true }
}));
slide.addText(bulletPoints, {
x: 0.5,
y: 3.25,
w: 9,
h: 2,
...this.fonts.body,
color: this.colors.text,
bullet: { type: 'circle' }
});
}
slide.addNotes(slideData.speaker_notes || '');
}
createAgendaSlide(slideData) {
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
// Title
slide.addText(slideData.title, {
x: 0.5,
y: 0.5,
w: 9,
h: 0.75,
...this.fonts.heading,
color: this.colors.primary
});
// Agenda items
const agendaItems = slideData.items.map((item, index) => ({
text: `${index + 1}. ${item}`,
options: {
fontSize: 20,
bullet: false,
indentLevel: 0
}
}));
slide.addText(agendaItems, {
x: 1,
y: 1.5,
w: 8,
h: 3.5,
color: this.colors.text,
lineSpacing: 32
});
// Duration footer
if (slideData.total_duration) {
slide.addText(`Total Duration: ${slideData.total_duration} minutes`, {
x: 0.5,
y: 5,
w: 4,
h: 0.4,
...this.fonts.small,
color: this.colors.subtext
});
}
slide.addNotes(slideData.speaker_notes || '');
}
createContentSlide(slideData) {
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
// Title
slide.addText(slideData.title, {
x: 0.5,
y: 0.5,
w: 9,
h: 0.75,
...this.fonts.heading,
color: this.colors.primary
});
// Determine layout based on content
const hasData = slideData.data && slideData.data.length > 0;
const contentWidth = hasData ? 5 : 9;
// Bullet points
if (slideData.bullets && slideData.bullets.length > 0) {
const bulletPoints = slideData.bullets.map(point => ({
text: point,
options: { bullet: true }
}));
slide.addText(bulletPoints, {
x: 0.5,
y: 1.5,
w: contentWidth,
h: 3.5,
...this.fonts.body,
color: this.colors.text,
bullet: { type: 'circle' },
lineSpacing: 24
});
}
// Data table if present
if (hasData) {
const tableData = [
['Metric', 'Value'],
...slideData.data.map(d => [d.metric, d.value])
];
slide.addTable(tableData, {
x: 6,
y: 1.5,
w: 3.5,
h: 2,
fontSize: 14,
border: { pt: 1, color: this.colors.secondary },
fill: { color: this.colors.light }
});
}
slide.addNotes(slideData.speaker_notes || '');
}
createDataSlide(slideData) {
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
// Title
slide.addText(slideData.title, {
x: 0.5,
y: 0.5,
w: 9,
h: 0.75,
...this.fonts.heading,
color: this.colors.primary
});
// Create data table
if (slideData.data_points && slideData.data_points.length > 0) {
const tableData = [
['Source', 'Metric', 'Value'],
...slideData.data_points.map(dp => [
dp.source || '',
dp.metric,
dp.value
])
];
slide.addTable(tableData, {
x: 0.5,
y: 1.5,
w: 9,
h: 3.5,
fontSize: 16,
border: { pt: 1, color: this.colors.secondary },
fill: { color: this.colors.light },
rowH: 0.5
});
}
slide.addNotes(slideData.speaker_notes || '');
}
createRecommendationsSlide(slideData) {
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
// Title
slide.addText(slideData.title, {
x: 0.5,
y: 0.5,
w: 9,
h: 0.75,
...this.fonts.heading,
color: this.colors.primary
});
// Recommendations as numbered list
const recommendations = slideData.items.map((item, index) => ({
text: `${index + 1}. ${item}`,
options: {
fontSize: 20,
bullet: false
}
}));
slide.addText(recommendations, {
x: 0.5,
y: 1.5,
w: 9,
h: 3.5,
color: this.colors.text,
lineSpacing: 28
});
slide.addNotes(slideData.speaker_notes || '');
}
createClosingSlide(slideData) {
const slide = this.pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' });
// Gradient background
slide.background = {
fill: {
type: 'gradient',
colors: [
{ color: this.colors.secondary, position: 0 },
{ color: this.colors.primary, position: 100 }
]
}
};
// Thank you text
slide.addText(slideData.title, {
x: 0.5,
y: 2,
w: 9,
h: 1,
fontSize: 48,
bold: true,
color: 'FFFFFF',
align: 'center'
});
// Subtitle
if (slideData.subtitle) {
slide.addText(slideData.subtitle, {
x: 0.5,
y: 3.5,
w: 9,
h: 0.75,
fontSize: 28,
color: 'FFFFFF',
align: 'center'
});
}
slide.addNotes(slideData.speaker_notes || '');
}
async save(outputPath) {
try {
await this.pptx.writeFile({ fileName: outputPath });
console.log(`✅ Presentation saved: ${outputPath}`);
} catch (error) {
console.error(`❌ Error saving presentation: ${error}`);
throw error;
}
}
}
// Main execution
async function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('Usage: node generate_pptx.js <synthesis.json> <output.pptx>');
process.exit(1);
}
const synthesisFile = args[0];
const outputFile = args[1];
console.log(`📊 Loading synthesis from: ${synthesisFile}`);
try {
// Load synthesis data
const synthesisData = JSON.parse(
fs.readFileSync(synthesisFile, 'utf8')
);
// Generate presentation
const generator = new PresentationGenerator(synthesisData);
const presentation = generator.generate();
// Save to file
await generator.save(outputFile);
console.log(`📈 Created ${synthesisData.slide_plan.length} slides`);
} catch (error) {
console.error(`❌ Error: ${error.message}`);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
main();
}
module.exports = { PresentationGenerator };