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,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
|
+
});
|