claude-presentation-master 6.1.1 → 7.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -562
- package/bin/auto-fix-presentation.cjs +446 -0
- package/bin/cli.js +101 -32
- package/bin/qa-presentation.cjs +279 -0
- package/bin/score-presentation.cjs +488 -0
- package/dist/index.d.mts +838 -74
- package/dist/index.d.ts +838 -74
- package/dist/index.js +3203 -522
- package/dist/index.mjs +3195 -523
- package/package.json +3 -2
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Automatic Presentation QA Review System
|
|
4
|
+
*
|
|
5
|
+
* Captures screenshots of every slide and performs automated visual analysis.
|
|
6
|
+
* Reports issues and scores each slide against quality criteria.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node qa-presentation.cjs <html-file>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { chromium } = require('playwright');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
const QA_CRITERIA = {
|
|
16
|
+
// Text readability
|
|
17
|
+
MIN_TITLE_SIZE: 40, // px - titles should be large
|
|
18
|
+
MIN_BODY_SIZE: 16, // px - body text readable
|
|
19
|
+
MAX_WORDS_PER_SLIDE: 50, // Glance test
|
|
20
|
+
|
|
21
|
+
// Visual balance
|
|
22
|
+
MIN_WHITESPACE_RATIO: 0.3, // 30% whitespace minimum
|
|
23
|
+
MAX_ELEMENTS: 7, // Miller's Law
|
|
24
|
+
|
|
25
|
+
// Contrast
|
|
26
|
+
MIN_CONTRAST_RATIO: 4.5, // WCAG AA
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async function captureAndAnalyzeSlides(htmlPath) {
|
|
30
|
+
const browser = await chromium.launch();
|
|
31
|
+
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
|
|
32
|
+
|
|
33
|
+
const absolutePath = path.resolve(htmlPath);
|
|
34
|
+
await page.goto(`file://${absolutePath}`, { waitUntil: 'networkidle' });
|
|
35
|
+
await page.waitForTimeout(3000); // Wait for images and animations
|
|
36
|
+
|
|
37
|
+
// Get total slides
|
|
38
|
+
const totalSlides = await page.evaluate(() => {
|
|
39
|
+
if (typeof Reveal !== 'undefined') {
|
|
40
|
+
return Reveal.getTotalSlides();
|
|
41
|
+
}
|
|
42
|
+
return document.querySelectorAll('.slides > section').length || 10;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log('\n' + '='.repeat(60));
|
|
46
|
+
console.log(' AUTOMATIC PRESENTATION QA REVIEW');
|
|
47
|
+
console.log('='.repeat(60));
|
|
48
|
+
console.log(`\nAnalyzing ${totalSlides} slides...\n`);
|
|
49
|
+
|
|
50
|
+
const results = [];
|
|
51
|
+
const screenshotDir = path.join(path.dirname(absolutePath), 'qa-screenshots');
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(screenshotDir)) {
|
|
54
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < totalSlides; i++) {
|
|
58
|
+
const screenshotPath = path.join(screenshotDir, `slide-${i + 1}.png`);
|
|
59
|
+
await page.screenshot({ path: screenshotPath });
|
|
60
|
+
|
|
61
|
+
// Analyze the current slide
|
|
62
|
+
const analysis = await page.evaluate(() => {
|
|
63
|
+
const slide = document.querySelector('.present') || document.querySelector('section');
|
|
64
|
+
if (!slide) return { error: 'No slide found' };
|
|
65
|
+
|
|
66
|
+
// Get all text content
|
|
67
|
+
const textContent = slide.innerText || '';
|
|
68
|
+
const wordCount = textContent.split(/\s+/).filter(w => w.length > 0).length;
|
|
69
|
+
|
|
70
|
+
// Helper to parse RGB color
|
|
71
|
+
function parseColor(colorStr) {
|
|
72
|
+
const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
73
|
+
if (match) {
|
|
74
|
+
return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Calculate relative luminance
|
|
80
|
+
function getLuminance(rgb) {
|
|
81
|
+
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
|
|
82
|
+
c = c / 255;
|
|
83
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
84
|
+
});
|
|
85
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Calculate contrast ratio
|
|
89
|
+
function getContrastRatio(fg, bg) {
|
|
90
|
+
const l1 = getLuminance(fg);
|
|
91
|
+
const l2 = getLuminance(bg);
|
|
92
|
+
const lighter = Math.max(l1, l2);
|
|
93
|
+
const darker = Math.min(l1, l2);
|
|
94
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Get background color (walk up to find non-transparent)
|
|
98
|
+
let bgColor = null;
|
|
99
|
+
let el = slide;
|
|
100
|
+
while (el && !bgColor) {
|
|
101
|
+
const bg = window.getComputedStyle(el).backgroundColor;
|
|
102
|
+
const parsed = parseColor(bg);
|
|
103
|
+
if (parsed && (parsed.r > 0 || parsed.g > 0 || parsed.b > 0)) {
|
|
104
|
+
bgColor = parsed;
|
|
105
|
+
}
|
|
106
|
+
el = el.parentElement;
|
|
107
|
+
}
|
|
108
|
+
if (!bgColor) bgColor = { r: 10, g: 10, b: 11 }; // Default dark bg
|
|
109
|
+
|
|
110
|
+
// Get computed styles of text elements and check contrast
|
|
111
|
+
const titles = slide.querySelectorAll('h1, h2, h3, .display-xl, .display-lg, .display-md, .title');
|
|
112
|
+
const titleData = Array.from(titles).map(t => {
|
|
113
|
+
const style = window.getComputedStyle(t);
|
|
114
|
+
const color = parseColor(style.color);
|
|
115
|
+
const contrast = color ? getContrastRatio(color, bgColor) : 0;
|
|
116
|
+
return {
|
|
117
|
+
size: parseFloat(style.fontSize),
|
|
118
|
+
contrast: contrast,
|
|
119
|
+
text: t.textContent.substring(0, 30)
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const bodyText = slide.querySelectorAll('p, li, .body-lg, .stat-label, .subtitle');
|
|
124
|
+
const bodyData = Array.from(bodyText).map(t => {
|
|
125
|
+
const style = window.getComputedStyle(t);
|
|
126
|
+
const color = parseColor(style.color);
|
|
127
|
+
const contrast = color ? getContrastRatio(color, bgColor) : 0;
|
|
128
|
+
return {
|
|
129
|
+
size: parseFloat(style.fontSize),
|
|
130
|
+
contrast: contrast
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Count major elements
|
|
135
|
+
const majorElements = slide.querySelectorAll('h1, h2, h3, .metric-card, .challenge-item, .timeline-phase, .invest-bar, .result-item, img, .chart-container');
|
|
136
|
+
|
|
137
|
+
// Check for empty content
|
|
138
|
+
const hasContent = textContent.trim().length > 10;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
wordCount,
|
|
142
|
+
titleData,
|
|
143
|
+
bodyData,
|
|
144
|
+
titleSizes: titleData.map(t => t.size),
|
|
145
|
+
bodySizes: bodyData.map(t => t.size),
|
|
146
|
+
titleContrasts: titleData.map(t => t.contrast),
|
|
147
|
+
bodyContrasts: bodyData.map(t => t.contrast),
|
|
148
|
+
elementCount: majorElements.length,
|
|
149
|
+
hasContent,
|
|
150
|
+
bgColorRGB: bgColor,
|
|
151
|
+
textPreview: textContent.substring(0, 100).replace(/\s+/g, ' ')
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Score this slide
|
|
156
|
+
const issues = [];
|
|
157
|
+
let score = 100;
|
|
158
|
+
|
|
159
|
+
// Check word count (glance test)
|
|
160
|
+
if (analysis.wordCount > QA_CRITERIA.MAX_WORDS_PER_SLIDE) {
|
|
161
|
+
issues.push(`Too many words: ${analysis.wordCount} (max ${QA_CRITERIA.MAX_WORDS_PER_SLIDE})`);
|
|
162
|
+
score -= 15;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (analysis.wordCount < 3) {
|
|
166
|
+
issues.push(`Too sparse: only ${analysis.wordCount} words`);
|
|
167
|
+
score -= 10;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check title sizes
|
|
171
|
+
const smallTitles = (analysis.titleSizes || []).filter(s => s < QA_CRITERIA.MIN_TITLE_SIZE);
|
|
172
|
+
if (smallTitles.length > 0) {
|
|
173
|
+
issues.push(`Small title text: ${smallTitles.map(s => Math.round(s) + 'px').join(', ')}`);
|
|
174
|
+
score -= 10;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check body text sizes
|
|
178
|
+
const smallBody = (analysis.bodySizes || []).filter(s => s < QA_CRITERIA.MIN_BODY_SIZE);
|
|
179
|
+
if (smallBody.length > 0) {
|
|
180
|
+
issues.push(`Small body text: ${smallBody.length} elements under ${QA_CRITERIA.MIN_BODY_SIZE}px`);
|
|
181
|
+
score -= 5;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check element count (Miller's Law)
|
|
185
|
+
if (analysis.elementCount > QA_CRITERIA.MAX_ELEMENTS) {
|
|
186
|
+
issues.push(`Too many elements: ${analysis.elementCount} (max ${QA_CRITERIA.MAX_ELEMENTS})`);
|
|
187
|
+
score -= 10;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for content
|
|
191
|
+
if (!analysis.hasContent) {
|
|
192
|
+
issues.push('Slide appears empty or has minimal content');
|
|
193
|
+
score -= 20;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// CHECK CONTRAST - CRITICAL
|
|
197
|
+
const lowContrastTitles = (analysis.titleContrasts || []).filter(c => c < 4.5);
|
|
198
|
+
if (lowContrastTitles.length > 0) {
|
|
199
|
+
const worstContrast = Math.min(...lowContrastTitles).toFixed(1);
|
|
200
|
+
issues.push(`LOW CONTRAST: ${lowContrastTitles.length} title(s) below 4.5:1 (worst: ${worstContrast}:1)`);
|
|
201
|
+
score -= 25; // Major penalty - this is a critical failure
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const lowContrastBody = (analysis.bodyContrasts || []).filter(c => c < 3.0);
|
|
205
|
+
if (lowContrastBody.length > 0) {
|
|
206
|
+
issues.push(`Low contrast body text: ${lowContrastBody.length} elements below 3:1`);
|
|
207
|
+
score -= 15;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const slideResult = {
|
|
211
|
+
slide: i + 1,
|
|
212
|
+
score: Math.max(0, score),
|
|
213
|
+
issues,
|
|
214
|
+
analysis,
|
|
215
|
+
screenshot: screenshotPath
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
results.push(slideResult);
|
|
219
|
+
|
|
220
|
+
// Print result
|
|
221
|
+
const status = score >= 90 ? '✅' : score >= 70 ? '⚠️' : '❌';
|
|
222
|
+
console.log(`Slide ${i + 1}: ${status} Score: ${Math.max(0, score)}/100`);
|
|
223
|
+
if (issues.length > 0) {
|
|
224
|
+
issues.forEach(issue => console.log(` └─ ${issue}`));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Next slide
|
|
228
|
+
await page.keyboard.press('ArrowRight');
|
|
229
|
+
await page.waitForTimeout(1500); // Longer wait for animations and images
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await browser.close();
|
|
233
|
+
|
|
234
|
+
// Summary
|
|
235
|
+
const avgScore = Math.round(results.reduce((sum, r) => sum + r.score, 0) / results.length);
|
|
236
|
+
const passedSlides = results.filter(r => r.score >= 80).length;
|
|
237
|
+
const failedSlides = results.filter(r => r.score < 70).length;
|
|
238
|
+
|
|
239
|
+
console.log('\n' + '='.repeat(60));
|
|
240
|
+
console.log(' SUMMARY');
|
|
241
|
+
console.log('='.repeat(60));
|
|
242
|
+
console.log(`\n Average Score: ${avgScore}/100`);
|
|
243
|
+
console.log(` Passed (80+): ${passedSlides}/${totalSlides}`);
|
|
244
|
+
console.log(` Failed (<70): ${failedSlides}/${totalSlides}`);
|
|
245
|
+
console.log(` Screenshots: ${screenshotDir}/`);
|
|
246
|
+
|
|
247
|
+
if (avgScore >= 90) {
|
|
248
|
+
console.log('\n ✅ PRESENTATION PASSED QA\n');
|
|
249
|
+
} else if (avgScore >= 70) {
|
|
250
|
+
console.log('\n ⚠️ PRESENTATION NEEDS IMPROVEMENT\n');
|
|
251
|
+
} else {
|
|
252
|
+
console.log('\n ❌ PRESENTATION FAILED QA\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Write detailed report
|
|
256
|
+
const reportPath = path.join(screenshotDir, 'qa-report.json');
|
|
257
|
+
fs.writeFileSync(reportPath, JSON.stringify({
|
|
258
|
+
file: htmlPath,
|
|
259
|
+
timestamp: new Date().toISOString(),
|
|
260
|
+
summary: { avgScore, passedSlides, failedSlides, totalSlides },
|
|
261
|
+
slides: results
|
|
262
|
+
}, null, 2));
|
|
263
|
+
|
|
264
|
+
console.log(` Report saved: ${reportPath}\n`);
|
|
265
|
+
|
|
266
|
+
return { avgScore, results };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Run if called directly
|
|
270
|
+
const htmlFile = process.argv[2];
|
|
271
|
+
if (!htmlFile) {
|
|
272
|
+
console.error('Usage: node qa-presentation.cjs <html-file>');
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
captureAndAnalyzeSlides(htmlFile).catch(err => {
|
|
277
|
+
console.error('QA Error:', err.message);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|