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>
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
#!/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 };
|
||||
Reference in New Issue
Block a user