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.
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Comprehensive Presentation Scoring System
4
+ *
5
+ * Scores presentations on 7 dimensions (1-100 each):
6
+ * 1. Layout - Clear slide layout, nothing off-screen, proper spacing
7
+ * 2. Contrast - All text readable against backgrounds
8
+ * 3. Graphics - Visual quality and appropriateness
9
+ * 4. Content - Information quality and density
10
+ * 5. Clarity - Message clarity and simplicity
11
+ * 6. Effectiveness - Impact and persuasiveness
12
+ * 7. Consistency - Visual and thematic consistency
13
+ *
14
+ * Usage: node score-presentation.cjs <html-file>
15
+ */
16
+
17
+ const { chromium } = require('playwright');
18
+ const path = require('path');
19
+ const fs = require('fs');
20
+
21
+ // Scoring criteria and thresholds
22
+ const CRITERIA = {
23
+ // Layout scoring
24
+ LAYOUT: {
25
+ MAX_WORD_COUNT: 40, // Ideal max words per slide
26
+ MIN_FONT_SIZE: 18, // Minimum readable font
27
+ TITLE_MIN_SIZE: 36, // Titles should be big
28
+ ELEMENT_MAX: 7, // Miller's Law
29
+ PADDING_MIN: 40, // Minimum edge padding in px
30
+ },
31
+
32
+ // Contrast scoring (WCAG)
33
+ CONTRAST: {
34
+ TITLE_MIN: 7.0, // AAA for large text
35
+ BODY_MIN: 4.5, // AA standard
36
+ SMALL_TEXT_MIN: 7.0, // AAA for small text
37
+ },
38
+
39
+ // Content scoring
40
+ CONTENT: {
41
+ IDEAL_WORDS: [15, 30], // Sweet spot for word count
42
+ MAX_BULLET_POINTS: 5,
43
+ IDEAL_HIERARCHY_LEVELS: 2,
44
+ }
45
+ };
46
+
47
+ // Color utilities
48
+ function parseColor(colorStr) {
49
+ if (!colorStr) return null;
50
+ const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
51
+ if (match) {
52
+ return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
53
+ }
54
+ // Handle hex colors
55
+ const hexMatch = colorStr.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
56
+ if (hexMatch) {
57
+ return { r: parseInt(hexMatch[1], 16), g: parseInt(hexMatch[2], 16), b: parseInt(hexMatch[3], 16) };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function getLuminance(rgb) {
63
+ const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
64
+ c = c / 255;
65
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
66
+ });
67
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
68
+ }
69
+
70
+ function getContrastRatio(fg, bg) {
71
+ if (!fg || !bg) return 1;
72
+ const l1 = getLuminance(fg);
73
+ const l2 = getLuminance(bg);
74
+ const lighter = Math.max(l1, l2);
75
+ const darker = Math.min(l1, l2);
76
+ return (lighter + 0.05) / (darker + 0.05);
77
+ }
78
+
79
+ async function scorePresentation(htmlPath) {
80
+ const browser = await chromium.launch();
81
+ const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
82
+
83
+ const absolutePath = path.resolve(htmlPath);
84
+ await page.goto(`file://${absolutePath}`, { waitUntil: 'networkidle' });
85
+ await page.waitForTimeout(3000);
86
+
87
+ const totalSlides = await page.evaluate(() => {
88
+ if (typeof Reveal !== 'undefined') return Reveal.getTotalSlides();
89
+ return document.querySelectorAll('.slides > section').length || 10;
90
+ });
91
+
92
+ console.log('\n' + '═'.repeat(70));
93
+ console.log(' COMPREHENSIVE PRESENTATION SCORING SYSTEM');
94
+ console.log('═'.repeat(70));
95
+ console.log(`\nAnalyzing ${totalSlides} slides across 7 dimensions...\n`);
96
+
97
+ const slideScores = [];
98
+ const screenshotDir = path.join(path.dirname(absolutePath), 'score-screenshots');
99
+
100
+ if (!fs.existsSync(screenshotDir)) {
101
+ fs.mkdirSync(screenshotDir, { recursive: true });
102
+ }
103
+
104
+ for (let i = 0; i < totalSlides; i++) {
105
+ const screenshotPath = path.join(screenshotDir, `slide-${i + 1}.png`);
106
+ await page.screenshot({ path: screenshotPath });
107
+
108
+ // Comprehensive slide analysis
109
+ const analysis = await page.evaluate(() => {
110
+ const slide = document.querySelector('.present') || document.querySelector('section');
111
+ if (!slide) return { error: 'No slide found' };
112
+
113
+ // Helper functions (same as before)
114
+ function parseColorInner(colorStr) {
115
+ if (!colorStr) return null;
116
+ const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
117
+ if (match) {
118
+ return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
119
+ }
120
+ return null;
121
+ }
122
+
123
+ function getLuminanceInner(rgb) {
124
+ const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
125
+ c = c / 255;
126
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
127
+ });
128
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
129
+ }
130
+
131
+ function getContrastInner(fg, bg) {
132
+ if (!fg || !bg) return 21; // Assume good if can't calculate
133
+ const l1 = getLuminanceInner(fg);
134
+ const l2 = getLuminanceInner(bg);
135
+ const lighter = Math.max(l1, l2);
136
+ const darker = Math.min(l1, l2);
137
+ return (lighter + 0.05) / (darker + 0.05);
138
+ }
139
+
140
+ // Get effective background color - improved detection
141
+ function getEffectiveBg(element) {
142
+ let el = element;
143
+ while (el && el !== document.body) {
144
+ const style = window.getComputedStyle(el);
145
+ const bg = style.backgroundColor;
146
+ if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
147
+ const parsed = parseColorInner(bg);
148
+ if (parsed) return parsed;
149
+ }
150
+ el = el.parentElement;
151
+ }
152
+ // Default to near-black for dark theme presentations
153
+ return { r: 10, g: 10, b: 11 };
154
+ }
155
+
156
+ // Parse color from CSS - handles more formats
157
+ function parseColorFromCSS(colorStr) {
158
+ if (!colorStr) return null;
159
+ // rgb/rgba format
160
+ const rgbMatch = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
161
+ if (rgbMatch) {
162
+ return { r: parseInt(rgbMatch[1]), g: parseInt(rgbMatch[2]), b: parseInt(rgbMatch[3]) };
163
+ }
164
+ // hex format
165
+ const hexMatch = colorStr.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
166
+ if (hexMatch) {
167
+ return { r: parseInt(hexMatch[1], 16), g: parseInt(hexMatch[2], 16), b: parseInt(hexMatch[3], 16) };
168
+ }
169
+ // Named colors
170
+ if (colorStr === 'white' || colorStr === '#fff' || colorStr === '#ffffff') {
171
+ return { r: 255, g: 255, b: 255 };
172
+ }
173
+ return null;
174
+ }
175
+
176
+ // Text content analysis
177
+ const textContent = slide.innerText || '';
178
+ const words = textContent.split(/\s+/).filter(w => w.length > 0);
179
+ const wordCount = words.length;
180
+
181
+ // Find all text elements and analyze
182
+ const allTextElements = slide.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, span, div, label');
183
+ const textAnalysis = [];
184
+
185
+ allTextElements.forEach(el => {
186
+ const text = el.innerText?.trim();
187
+ if (!text || text.length < 2) return;
188
+
189
+ const style = window.getComputedStyle(el);
190
+ const fontSize = parseFloat(style.fontSize);
191
+
192
+ // Get text color - try inline style first, then computed
193
+ let color = null;
194
+ const inlineColor = el.style.color;
195
+ if (inlineColor) {
196
+ color = parseColorFromCSS(inlineColor);
197
+ }
198
+ if (!color) {
199
+ color = parseColorInner(style.color);
200
+ }
201
+ // Default white for dark themes if color detection fails
202
+ if (!color || (color.r === 0 && color.g === 0 && color.b === 0)) {
203
+ color = { r: 255, g: 255, b: 255 }; // Assume white text on dark theme
204
+ }
205
+
206
+ const bgColor = getEffectiveBg(el);
207
+ const contrast = getContrastInner(color, bgColor);
208
+
209
+ const rect = el.getBoundingClientRect();
210
+ const isVisible = rect.width > 0 && rect.height > 0;
211
+ const isOnScreen = rect.top < window.innerHeight && rect.bottom > 0 &&
212
+ rect.left < window.innerWidth && rect.right > 0;
213
+
214
+ if (isVisible && text.length > 0) {
215
+ textAnalysis.push({
216
+ text: text.substring(0, 50),
217
+ fontSize,
218
+ contrast: Math.round(contrast * 10) / 10,
219
+ color: color ? `rgb(${color.r},${color.g},${color.b})` : 'unknown',
220
+ bgColor: bgColor ? `rgb(${bgColor.r},${bgColor.g},${bgColor.b})` : 'unknown',
221
+ isOnScreen,
222
+ isTitle: el.tagName.match(/^H[1-3]$/) || fontSize >= 30,
223
+ isSmall: fontSize < 16
224
+ });
225
+ }
226
+ });
227
+
228
+ // Element count (visual complexity)
229
+ const majorElements = slide.querySelectorAll('h1, h2, h3, img, .card, .metric, .stat, .chart, .grid > *, ul, ol, blockquote');
230
+
231
+ // Bullet points
232
+ const bulletPoints = slide.querySelectorAll('li').length;
233
+
234
+ // Images
235
+ const images = slide.querySelectorAll('img');
236
+ const imageData = Array.from(images).map(img => ({
237
+ loaded: img.complete && img.naturalHeight > 0,
238
+ width: img.clientWidth,
239
+ height: img.clientHeight
240
+ }));
241
+
242
+ return {
243
+ wordCount,
244
+ textAnalysis,
245
+ elementCount: majorElements.length,
246
+ bulletPoints,
247
+ imageCount: images.length,
248
+ imageData,
249
+ hasTitle: textAnalysis.some(t => t.isTitle),
250
+ slideRect: slide.getBoundingClientRect()
251
+ };
252
+ });
253
+
254
+ // Calculate dimension scores
255
+ const scores = calculateDimensionScores(analysis, i + 1);
256
+ slideScores.push({ slide: i + 1, ...scores, analysis, screenshot: screenshotPath });
257
+
258
+ // Print slide summary
259
+ printSlideScore(i + 1, scores);
260
+
261
+ // Next slide
262
+ await page.keyboard.press('ArrowRight');
263
+ await page.waitForTimeout(1500);
264
+ }
265
+
266
+ await browser.close();
267
+
268
+ // Calculate and print overall scores
269
+ printOverallScores(slideScores, totalSlides);
270
+
271
+ // Save detailed report
272
+ const reportPath = path.join(screenshotDir, 'score-report.json');
273
+ fs.writeFileSync(reportPath, JSON.stringify({
274
+ file: htmlPath,
275
+ timestamp: new Date().toISOString(),
276
+ slideScores,
277
+ overall: calculateOverallScores(slideScores)
278
+ }, null, 2));
279
+
280
+ console.log(`\n Detailed report: ${reportPath}\n`);
281
+
282
+ return slideScores;
283
+ }
284
+
285
+ function calculateDimensionScores(analysis, slideNum) {
286
+ const issues = [];
287
+
288
+ // 1. LAYOUT Score (0-100)
289
+ let layoutScore = 100;
290
+
291
+ if (analysis.wordCount > CRITERIA.LAYOUT.MAX_WORD_COUNT) {
292
+ const excess = analysis.wordCount - CRITERIA.LAYOUT.MAX_WORD_COUNT;
293
+ layoutScore -= Math.min(30, excess * 2);
294
+ issues.push(`Too many words: ${analysis.wordCount}`);
295
+ }
296
+
297
+ if (analysis.elementCount > CRITERIA.LAYOUT.ELEMENT_MAX) {
298
+ layoutScore -= 15;
299
+ issues.push(`Too many elements: ${analysis.elementCount}`);
300
+ }
301
+
302
+ const smallFonts = analysis.textAnalysis.filter(t => t.fontSize < CRITERIA.LAYOUT.MIN_FONT_SIZE && !t.isSmall);
303
+ if (smallFonts.length > 0) {
304
+ layoutScore -= smallFonts.length * 5;
305
+ issues.push(`Small text: ${smallFonts.length} elements`);
306
+ }
307
+
308
+ const smallTitles = analysis.textAnalysis.filter(t => t.isTitle && t.fontSize < CRITERIA.LAYOUT.TITLE_MIN_SIZE);
309
+ if (smallTitles.length > 0) {
310
+ layoutScore -= 15;
311
+ issues.push(`Titles too small`);
312
+ }
313
+
314
+ const offScreen = analysis.textAnalysis.filter(t => !t.isOnScreen);
315
+ if (offScreen.length > 0) {
316
+ layoutScore -= 20;
317
+ issues.push(`Content off-screen: ${offScreen.length} elements`);
318
+ }
319
+
320
+ // 2. CONTRAST Score (0-100)
321
+ let contrastScore = 100;
322
+
323
+ const lowContrastTitles = analysis.textAnalysis.filter(t => t.isTitle && t.contrast < CRITERIA.CONTRAST.TITLE_MIN);
324
+ if (lowContrastTitles.length > 0) {
325
+ contrastScore -= lowContrastTitles.length * 20;
326
+ const worst = Math.min(...lowContrastTitles.map(t => t.contrast));
327
+ issues.push(`LOW TITLE CONTRAST: ${worst.toFixed(1)}:1 (need ${CRITERIA.CONTRAST.TITLE_MIN}:1)`);
328
+ }
329
+
330
+ const lowContrastBody = analysis.textAnalysis.filter(t => !t.isTitle && t.contrast < CRITERIA.CONTRAST.BODY_MIN);
331
+ if (lowContrastBody.length > 0) {
332
+ contrastScore -= lowContrastBody.length * 10;
333
+ const worst = Math.min(...lowContrastBody.map(t => t.contrast));
334
+ issues.push(`Low body contrast: ${worst.toFixed(1)}:1`);
335
+ }
336
+
337
+ // 3. GRAPHICS Score (0-100)
338
+ let graphicsScore = 100;
339
+
340
+ if (analysis.imageCount === 0 && analysis.wordCount > 20) {
341
+ graphicsScore -= 20; // Text-heavy slides benefit from images
342
+ issues.push('No images on text-heavy slide');
343
+ }
344
+
345
+ const brokenImages = analysis.imageData.filter(img => !img.loaded);
346
+ if (brokenImages.length > 0) {
347
+ graphicsScore -= brokenImages.length * 25;
348
+ issues.push(`Broken images: ${brokenImages.length}`);
349
+ }
350
+
351
+ // 4. CONTENT Score (0-100)
352
+ let contentScore = 100;
353
+
354
+ const [minWords, maxWords] = CRITERIA.CONTENT.IDEAL_WORDS;
355
+ if (analysis.wordCount < minWords && analysis.wordCount > 0) {
356
+ contentScore -= 10; // Too sparse
357
+ } else if (analysis.wordCount > maxWords * 2) {
358
+ contentScore -= 25; // Way too dense
359
+ issues.push('Content too dense');
360
+ }
361
+
362
+ if (analysis.bulletPoints > CRITERIA.CONTENT.MAX_BULLET_POINTS) {
363
+ contentScore -= 15;
364
+ issues.push(`Too many bullets: ${analysis.bulletPoints}`);
365
+ }
366
+
367
+ if (!analysis.hasTitle) {
368
+ contentScore -= 20;
369
+ issues.push('No clear title/heading');
370
+ }
371
+
372
+ // 5. CLARITY Score (0-100)
373
+ let clarityScore = 100;
374
+
375
+ // Clarity penalized by too much content, too many elements
376
+ if (analysis.wordCount > 50) {
377
+ clarityScore -= 20;
378
+ }
379
+ if (analysis.elementCount > 5) {
380
+ clarityScore -= (analysis.elementCount - 5) * 5;
381
+ }
382
+
383
+ // 6. EFFECTIVENESS Score (0-100)
384
+ let effectivenessScore = 100;
385
+
386
+ // Effectiveness is about impact - needs clear message
387
+ if (!analysis.hasTitle) effectivenessScore -= 25;
388
+ if (analysis.wordCount > 40) effectivenessScore -= 15;
389
+ if (lowContrastTitles.length > 0) effectivenessScore -= 30; // Can't be effective if unreadable
390
+
391
+ // 7. CONSISTENCY Score (calculated at deck level, placeholder here)
392
+ let consistencyScore = 100;
393
+
394
+ return {
395
+ layout: Math.max(0, Math.round(layoutScore)),
396
+ contrast: Math.max(0, Math.round(contrastScore)),
397
+ graphics: Math.max(0, Math.round(graphicsScore)),
398
+ content: Math.max(0, Math.round(contentScore)),
399
+ clarity: Math.max(0, Math.round(clarityScore)),
400
+ effectiveness: Math.max(0, Math.round(effectivenessScore)),
401
+ consistency: Math.max(0, Math.round(consistencyScore)),
402
+ issues,
403
+ overall: Math.round((layoutScore + contrastScore + graphicsScore + contentScore + clarityScore + effectivenessScore + consistencyScore) / 7)
404
+ };
405
+ }
406
+
407
+ function printSlideScore(slideNum, scores) {
408
+ const overall = scores.overall;
409
+ const status = overall >= 85 ? '✅' : overall >= 70 ? '⚠️' : '❌';
410
+
411
+ console.log(`Slide ${String(slideNum).padStart(2)}: ${status} Overall: ${overall}/100`);
412
+ console.log(` Layout:${String(scores.layout).padStart(3)} | Contrast:${String(scores.contrast).padStart(3)} | Graphics:${String(scores.graphics).padStart(3)} | Content:${String(scores.content).padStart(3)}`);
413
+ console.log(` Clarity:${String(scores.clarity).padStart(3)} | Effect:${String(scores.effectiveness).padStart(3)} | Consist:${String(scores.consistency).padStart(3)}`);
414
+
415
+ if (scores.issues.length > 0) {
416
+ scores.issues.forEach(issue => console.log(` ⚠ ${issue}`));
417
+ }
418
+ console.log('');
419
+ }
420
+
421
+ function calculateOverallScores(slideScores) {
422
+ const dimensions = ['layout', 'contrast', 'graphics', 'content', 'clarity', 'effectiveness', 'consistency'];
423
+ const overall = {};
424
+
425
+ dimensions.forEach(dim => {
426
+ const scores = slideScores.map(s => s[dim]);
427
+ overall[dim] = Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
428
+ });
429
+
430
+ overall.total = Math.round(Object.values(overall).reduce((a, b) => a + b, 0) / dimensions.length);
431
+
432
+ return overall;
433
+ }
434
+
435
+ function printOverallScores(slideScores, totalSlides) {
436
+ const overall = calculateOverallScores(slideScores);
437
+
438
+ console.log('═'.repeat(70));
439
+ console.log(' OVERALL PRESENTATION SCORES');
440
+ console.log('═'.repeat(70));
441
+ console.log('');
442
+ console.log(' Dimension Scores (1-100):');
443
+ console.log(' ─────────────────────────');
444
+ console.log(` 📐 Layout: ${overall.layout}/100 ${getBar(overall.layout)}`);
445
+ console.log(` 🎨 Contrast: ${overall.contrast}/100 ${getBar(overall.contrast)}`);
446
+ console.log(` 🖼️ Graphics: ${overall.graphics}/100 ${getBar(overall.graphics)}`);
447
+ console.log(` 📝 Content: ${overall.content}/100 ${getBar(overall.content)}`);
448
+ console.log(` 💡 Clarity: ${overall.clarity}/100 ${getBar(overall.clarity)}`);
449
+ console.log(` 🎯 Effectiveness: ${overall.effectiveness}/100 ${getBar(overall.effectiveness)}`);
450
+ console.log(` 🔄 Consistency: ${overall.consistency}/100 ${getBar(overall.consistency)}`);
451
+ console.log('');
452
+ console.log(' ═════════════════════════════════════');
453
+ console.log(` 📊 TOTAL SCORE: ${overall.total}/100`);
454
+ console.log(' ═════════════════════════════════════');
455
+
456
+ const passedSlides = slideScores.filter(s => s.overall >= 85).length;
457
+ const needsWork = slideScores.filter(s => s.overall >= 70 && s.overall < 85).length;
458
+ const failedSlides = slideScores.filter(s => s.overall < 70).length;
459
+
460
+ console.log('');
461
+ console.log(` Slide Quality: ${passedSlides} passed | ${needsWork} need work | ${failedSlides} failed`);
462
+
463
+ if (overall.total >= 85) {
464
+ console.log('\n ✅ PRESENTATION READY FOR DELIVERY\n');
465
+ } else if (overall.total >= 70) {
466
+ console.log('\n ⚠️ PRESENTATION NEEDS IMPROVEMENT\n');
467
+ } else {
468
+ console.log('\n ❌ PRESENTATION REQUIRES SIGNIFICANT WORK\n');
469
+ }
470
+ }
471
+
472
+ function getBar(score) {
473
+ const filled = Math.round(score / 5);
474
+ const empty = 20 - filled;
475
+ return '█'.repeat(filled) + '░'.repeat(empty);
476
+ }
477
+
478
+ // Run
479
+ const htmlFile = process.argv[2];
480
+ if (!htmlFile) {
481
+ console.error('Usage: node score-presentation.cjs <html-file>');
482
+ process.exit(1);
483
+ }
484
+
485
+ scorePresentation(htmlFile).catch(err => {
486
+ console.error('Scoring Error:', err.message);
487
+ process.exit(1);
488
+ });