claude-presentation-master 7.4.0 → 8.1.0
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 +57 -11
- package/assets/presentation-knowledge.yaml +90 -0
- package/bin/cli.js +121 -8
- package/dist/index.d.mts +257 -284
- package/dist/index.d.ts +257 -284
- package/dist/index.js +2317 -1323
- package/dist/index.mjs +2315 -1318
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,13 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
AutoFixEngine: () => AutoFixEngine,
|
|
34
33
|
ChartJsProvider: () => ChartJsProvider,
|
|
35
34
|
CompositeChartProvider: () => CompositeChartProvider,
|
|
36
35
|
CompositeImageProvider: () => CompositeImageProvider,
|
|
37
36
|
ContentAnalyzer: () => ContentAnalyzer,
|
|
38
37
|
ContentPatternClassifier: () => ContentPatternClassifier,
|
|
39
|
-
IterativeQAEngine: () => IterativeQAEngine,
|
|
40
38
|
KnowledgeGateway: () => KnowledgeGateway,
|
|
41
39
|
LocalImageProvider: () => LocalImageProvider,
|
|
42
40
|
MermaidProvider: () => MermaidProvider,
|
|
@@ -48,7 +46,6 @@ __export(index_exports, {
|
|
|
48
46
|
QuickChartProvider: () => QuickChartProvider,
|
|
49
47
|
RevealJsGenerator: () => RevealJsGenerator,
|
|
50
48
|
ScoreCalculator: () => ScoreCalculator,
|
|
51
|
-
SevenDimensionScorer: () => SevenDimensionScorer,
|
|
52
49
|
SlideFactory: () => SlideFactory,
|
|
53
50
|
SlideGenerator: () => SlideGenerator,
|
|
54
51
|
TemplateEngine: () => TemplateEngine,
|
|
@@ -56,12 +53,12 @@ __export(index_exports, {
|
|
|
56
53
|
UnsplashImageProvider: () => UnsplashImageProvider,
|
|
57
54
|
VERSION: () => VERSION,
|
|
58
55
|
ValidationError: () => ValidationError,
|
|
59
|
-
|
|
56
|
+
VisualQualityEvaluator: () => VisualQualityEvaluator,
|
|
60
57
|
createDefaultChartProvider: () => createDefaultChartProvider,
|
|
61
58
|
createDefaultImageProvider: () => createDefaultImageProvider,
|
|
62
|
-
createIterativeQAEngine: () => createIterativeQAEngine,
|
|
63
59
|
createSlideFactory: () => createSlideFactory,
|
|
64
60
|
default: () => index_default,
|
|
61
|
+
evaluatePresentation: () => evaluatePresentation,
|
|
65
62
|
generate: () => generate,
|
|
66
63
|
getKnowledgeGateway: () => getKnowledgeGateway,
|
|
67
64
|
initSlideGenerator: () => initSlideGenerator,
|
|
@@ -97,6 +94,11 @@ var TemplateNotFoundError = class extends Error {
|
|
|
97
94
|
}
|
|
98
95
|
};
|
|
99
96
|
|
|
97
|
+
// src/core/PresentationEngine.ts
|
|
98
|
+
var fs2 = __toESM(require("fs"));
|
|
99
|
+
var path2 = __toESM(require("path"));
|
|
100
|
+
var os = __toESM(require("os"));
|
|
101
|
+
|
|
100
102
|
// src/kb/KnowledgeGateway.ts
|
|
101
103
|
var import_fs = require("fs");
|
|
102
104
|
var import_path = require("path");
|
|
@@ -200,12 +202,12 @@ var KnowledgeGateway = class {
|
|
|
200
202
|
(0, import_path.join)(process.cwd(), "assets/presentation-knowledge.yaml"),
|
|
201
203
|
(0, import_path.join)(process.cwd(), "node_modules/claude-presentation-master/assets/presentation-knowledge.yaml")
|
|
202
204
|
];
|
|
203
|
-
for (const
|
|
205
|
+
for (const path3 of possiblePaths) {
|
|
204
206
|
try {
|
|
205
|
-
const content = (0, import_fs.readFileSync)(
|
|
207
|
+
const content = (0, import_fs.readFileSync)(path3, "utf-8");
|
|
206
208
|
this.kb = yaml.parse(content);
|
|
207
209
|
this.loaded = true;
|
|
208
|
-
logger.step(`Knowledge base loaded from ${
|
|
210
|
+
logger.step(`Knowledge base loaded from ${path3}`);
|
|
209
211
|
return;
|
|
210
212
|
} catch {
|
|
211
213
|
}
|
|
@@ -230,33 +232,54 @@ var KnowledgeGateway = class {
|
|
|
230
232
|
return "keynote";
|
|
231
233
|
}
|
|
232
234
|
/**
|
|
233
|
-
* Get word limits for the specified mode
|
|
235
|
+
* Get word limits for the specified mode.
|
|
236
|
+
* READS FROM KB - NO HARDCODED VALUES.
|
|
234
237
|
*/
|
|
235
238
|
getWordLimits(mode) {
|
|
239
|
+
this.ensureLoaded();
|
|
236
240
|
const modeConfig = this.getMode(mode);
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return { min: 40, max: 80, ideal: 60 };
|
|
241
|
+
const rangeStr = modeConfig.characteristics.words_per_slide;
|
|
242
|
+
const match = rangeStr.match(/(\d+)-(\d+)/);
|
|
243
|
+
if (!match || !match[1] || !match[2]) {
|
|
244
|
+
throw new Error(`[KB ERROR] Invalid words_per_slide format in KB for ${mode}: "${rangeStr}". Expected "min-max" format.`);
|
|
242
245
|
}
|
|
246
|
+
const min = parseInt(match[1], 10);
|
|
247
|
+
const max = parseInt(match[2], 10);
|
|
248
|
+
const ideal = Math.round((min + max) / 2);
|
|
249
|
+
logger.debug(`[KB] getWordLimits(${mode}): min=${min}, max=${max}, ideal=${ideal} (from KB: "${rangeStr}")`);
|
|
250
|
+
return { min, max, ideal };
|
|
243
251
|
}
|
|
244
252
|
/**
|
|
245
|
-
* Get bullet point limits
|
|
253
|
+
* Get bullet point limits.
|
|
254
|
+
* READS FROM KB - NO HARDCODED VALUES.
|
|
246
255
|
*/
|
|
247
256
|
getBulletLimits(mode) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
this.ensureLoaded();
|
|
258
|
+
const type = mode === "keynote" ? "ted_keynote" : "consulting_deck";
|
|
259
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
260
|
+
if (typeConfig?.validation_rules?.bullets_per_slide) {
|
|
261
|
+
const maxBullets = typeConfig.validation_rules.bullets_per_slide.max;
|
|
262
|
+
const wordLimits = this.getWordLimits(mode);
|
|
263
|
+
const maxWordsPerBullet = maxBullets > 0 ? Math.floor(wordLimits.max / maxBullets) : 0;
|
|
264
|
+
logger.debug(`[KB] getBulletLimits(${mode}): maxBullets=${maxBullets}, maxWordsPerBullet=${maxWordsPerBullet} (from KB presentation_types.${type})`);
|
|
265
|
+
return { maxBullets, maxWordsPerBullet };
|
|
252
266
|
}
|
|
267
|
+
throw new Error(`[KB ERROR] Missing bullets_per_slide in KB for presentation_types.${type}.validation_rules`);
|
|
253
268
|
}
|
|
254
269
|
/**
|
|
255
|
-
* Get whitespace percentage requirement
|
|
270
|
+
* Get whitespace percentage requirement.
|
|
271
|
+
* READS FROM KB - NO HARDCODED VALUES.
|
|
256
272
|
*/
|
|
257
273
|
getWhitespaceRequirement(mode) {
|
|
258
|
-
|
|
259
|
-
|
|
274
|
+
this.ensureLoaded();
|
|
275
|
+
const type = mode === "keynote" ? "ted_keynote" : "consulting_deck";
|
|
276
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
277
|
+
if (typeConfig?.validation_rules?.whitespace?.min) {
|
|
278
|
+
const whitespace = typeConfig.validation_rules.whitespace.min / 100;
|
|
279
|
+
logger.debug(`[KB] getWhitespaceRequirement(${mode}): ${whitespace} (from KB presentation_types.${type})`);
|
|
280
|
+
return whitespace;
|
|
281
|
+
}
|
|
282
|
+
throw new Error(`[KB ERROR] Missing whitespace.min in KB for presentation_types.${type}.validation_rules`);
|
|
260
283
|
}
|
|
261
284
|
/**
|
|
262
285
|
* Get slide templates for the specified mode
|
|
@@ -367,29 +390,54 @@ var KnowledgeGateway = class {
|
|
|
367
390
|
return null;
|
|
368
391
|
}
|
|
369
392
|
/**
|
|
370
|
-
* Get Duarte's glance test requirements
|
|
393
|
+
* Get Duarte's glance test requirements.
|
|
394
|
+
* READS FROM KB - NO HARDCODED VALUES.
|
|
371
395
|
*/
|
|
372
396
|
getDuarteGlanceTest() {
|
|
373
397
|
this.ensureLoaded();
|
|
374
|
-
const duarte = this.kb.experts
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
398
|
+
const duarte = this.kb.experts?.nancy_duarte;
|
|
399
|
+
if (!duarte?.core_principles?.glance_test?.word_limit) {
|
|
400
|
+
throw new Error("[KB ERROR] Missing experts.nancy_duarte.core_principles.glance_test.word_limit in KB");
|
|
401
|
+
}
|
|
402
|
+
if (!duarte?.core_principles?.glance_test?.time_limit) {
|
|
403
|
+
throw new Error("[KB ERROR] Missing experts.nancy_duarte.core_principles.glance_test.time_limit in KB");
|
|
404
|
+
}
|
|
405
|
+
const wordLimit = duarte.core_principles.glance_test.word_limit;
|
|
406
|
+
const timeSeconds = parseInt(duarte.core_principles.glance_test.time_limit.replace(/\D/g, ""), 10);
|
|
407
|
+
logger.debug(`[KB] getDuarteGlanceTest(): wordLimit=${wordLimit}, timeSeconds=${timeSeconds} (from KB experts.nancy_duarte)`);
|
|
408
|
+
return { wordLimit, timeSeconds };
|
|
379
409
|
}
|
|
380
410
|
/**
|
|
381
|
-
* Get Miller's Law constraints
|
|
411
|
+
* Get Miller's Law constraints.
|
|
412
|
+
* READS FROM KB - NO HARDCODED VALUES.
|
|
382
413
|
*/
|
|
383
414
|
getMillersLaw() {
|
|
384
415
|
this.ensureLoaded();
|
|
385
|
-
|
|
416
|
+
const millersLaw = this.kb.cognitive_science?.millers_law;
|
|
417
|
+
if (!millersLaw?.principle) {
|
|
418
|
+
throw new Error("[KB ERROR] Missing cognitive_science.millers_law.principle in KB");
|
|
419
|
+
}
|
|
420
|
+
const match = millersLaw.principle.match(/(\d+)\s*[±+-]\s*(\d+)/);
|
|
421
|
+
if (!match) {
|
|
422
|
+
throw new Error(`[KB ERROR] Cannot parse Miller's Law from KB: "${millersLaw.principle}". Expected format: "7 \xB1 2"`);
|
|
423
|
+
}
|
|
424
|
+
const center = parseInt(match[1], 10);
|
|
425
|
+
const range = parseInt(match[2], 10);
|
|
426
|
+
const minItems = center - range;
|
|
427
|
+
const maxItems = center + range;
|
|
428
|
+
logger.debug(`[KB] getMillersLaw(): minItems=${minItems}, maxItems=${maxItems} (from KB: "${millersLaw.principle}")`);
|
|
429
|
+
return { minItems, maxItems };
|
|
386
430
|
}
|
|
387
431
|
/**
|
|
388
|
-
* Get cognitive load constraints
|
|
432
|
+
* Get cognitive load constraints.
|
|
433
|
+
* READS FROM KB - NO HARDCODED VALUES.
|
|
389
434
|
*/
|
|
390
435
|
getCognitiveLoadLimits() {
|
|
391
436
|
this.ensureLoaded();
|
|
392
|
-
|
|
437
|
+
const millers = this.getMillersLaw();
|
|
438
|
+
const maxItemsPerChunk = millers.maxItems - 2;
|
|
439
|
+
logger.debug(`[KB] getCognitiveLoadLimits(): maxItemsPerChunk=${maxItemsPerChunk} (derived from Miller's Law)`);
|
|
440
|
+
return { maxItemsPerChunk };
|
|
393
441
|
}
|
|
394
442
|
/**
|
|
395
443
|
* Get chart selection guidance
|
|
@@ -453,42 +501,40 @@ var KnowledgeGateway = class {
|
|
|
453
501
|
this.ensureLoaded();
|
|
454
502
|
const typeConfig = this.kb.presentation_types?.[type];
|
|
455
503
|
if (!typeConfig?.validation_rules) {
|
|
456
|
-
|
|
457
|
-
return mode === "keynote" ? {
|
|
458
|
-
wordsPerSlide: { min: 1, max: 25, ideal: 10 },
|
|
459
|
-
whitespace: { min: 40, ideal: 50 },
|
|
460
|
-
bulletsPerSlide: { max: 3 },
|
|
461
|
-
actionTitlesRequired: false,
|
|
462
|
-
sourcesRequired: false
|
|
463
|
-
} : {
|
|
464
|
-
wordsPerSlide: { min: 40, max: 80, ideal: 60 },
|
|
465
|
-
whitespace: { min: 25, ideal: 30 },
|
|
466
|
-
bulletsPerSlide: { max: 5 },
|
|
467
|
-
actionTitlesRequired: true,
|
|
468
|
-
sourcesRequired: true
|
|
469
|
-
};
|
|
504
|
+
throw new Error(`[KB ERROR] Missing presentation_types.${type}.validation_rules in KB. Cannot proceed without KB rules.`);
|
|
470
505
|
}
|
|
471
506
|
const rules = typeConfig.validation_rules;
|
|
472
|
-
|
|
507
|
+
if (rules.words_per_slide?.max === void 0) {
|
|
508
|
+
throw new Error(`[KB ERROR] Missing presentation_types.${type}.validation_rules.words_per_slide.max in KB`);
|
|
509
|
+
}
|
|
510
|
+
if (rules.whitespace?.min === void 0) {
|
|
511
|
+
throw new Error(`[KB ERROR] Missing presentation_types.${type}.validation_rules.whitespace.min in KB`);
|
|
512
|
+
}
|
|
513
|
+
if (rules.bullets_per_slide?.max === void 0) {
|
|
514
|
+
throw new Error(`[KB ERROR] Missing presentation_types.${type}.validation_rules.bullets_per_slide.max in KB`);
|
|
515
|
+
}
|
|
516
|
+
const result = {
|
|
473
517
|
wordsPerSlide: {
|
|
474
|
-
min: rules.words_per_slide
|
|
475
|
-
max: rules.words_per_slide
|
|
476
|
-
ideal: rules.words_per_slide
|
|
518
|
+
min: rules.words_per_slide.min,
|
|
519
|
+
max: rules.words_per_slide.max,
|
|
520
|
+
ideal: rules.words_per_slide.ideal
|
|
477
521
|
},
|
|
478
522
|
whitespace: {
|
|
479
|
-
min: rules.whitespace
|
|
480
|
-
ideal: rules.whitespace
|
|
523
|
+
min: rules.whitespace.min,
|
|
524
|
+
ideal: rules.whitespace.ideal
|
|
481
525
|
},
|
|
482
526
|
bulletsPerSlide: {
|
|
483
|
-
max: rules.bullets_per_slide
|
|
527
|
+
max: rules.bullets_per_slide.max
|
|
484
528
|
},
|
|
485
|
-
actionTitlesRequired: rules.action_titles_required
|
|
486
|
-
sourcesRequired: rules.sources_required
|
|
529
|
+
actionTitlesRequired: rules.action_titles_required,
|
|
530
|
+
sourcesRequired: rules.sources_required,
|
|
487
531
|
calloutsRequired: rules.callouts_required,
|
|
488
532
|
denseDataAllowed: rules.dense_data_allowed,
|
|
489
533
|
codeBlocksAllowed: rules.code_blocks_allowed,
|
|
490
534
|
diagramsRequired: rules.diagrams_required
|
|
491
535
|
};
|
|
536
|
+
logger.debug(`[KB] getValidationRules(${type}): words=${result.wordsPerSlide.min}-${result.wordsPerSlide.max}, bullets=${result.bulletsPerSlide.max}, whitespace=${result.whitespace.min}%`);
|
|
537
|
+
return result;
|
|
492
538
|
}
|
|
493
539
|
/**
|
|
494
540
|
* Get required elements that MUST be in the deck for this type.
|
|
@@ -598,39 +644,146 @@ var KnowledgeGateway = class {
|
|
|
598
644
|
* Map a content pattern to the best allowed slide type.
|
|
599
645
|
* CRITICAL: This is how SlideFactory decides which slide type to use.
|
|
600
646
|
*/
|
|
647
|
+
/**
|
|
648
|
+
* Map semantic slide types to structural types that SlideFactory can handle.
|
|
649
|
+
* This bridges the gap between KB's rich semantic types and code's structural types.
|
|
650
|
+
*/
|
|
651
|
+
normalizeToStructuralType(semanticType) {
|
|
652
|
+
const normalizedType = semanticType.toLowerCase().replace(/_/g, "-");
|
|
653
|
+
const structuralTypes = /* @__PURE__ */ new Set([
|
|
654
|
+
"title",
|
|
655
|
+
"agenda",
|
|
656
|
+
"thank-you",
|
|
657
|
+
"cta",
|
|
658
|
+
"single-statement",
|
|
659
|
+
"big-number",
|
|
660
|
+
"quote",
|
|
661
|
+
"bullet-points",
|
|
662
|
+
"two-column",
|
|
663
|
+
"three-column",
|
|
664
|
+
"three-points",
|
|
665
|
+
"comparison",
|
|
666
|
+
"timeline",
|
|
667
|
+
"process",
|
|
668
|
+
"metrics-grid",
|
|
669
|
+
"data-insight",
|
|
670
|
+
"full-image",
|
|
671
|
+
"screenshot",
|
|
672
|
+
"code-snippet",
|
|
673
|
+
"title-impact"
|
|
674
|
+
]);
|
|
675
|
+
if (structuralTypes.has(normalizedType)) {
|
|
676
|
+
return normalizedType;
|
|
677
|
+
}
|
|
678
|
+
const semanticToStructural = {
|
|
679
|
+
// TED Keynote semantic types
|
|
680
|
+
"star-moment": "single-statement",
|
|
681
|
+
"call-to-action": "cta",
|
|
682
|
+
// Sales pitch semantic types
|
|
683
|
+
"problem-statement": "single-statement",
|
|
684
|
+
"solution-overview": "single-statement",
|
|
685
|
+
"social-proof": "quote",
|
|
686
|
+
"testimonial": "quote",
|
|
687
|
+
"pricing": "metrics-grid",
|
|
688
|
+
"demo-screenshot": "screenshot",
|
|
689
|
+
// Consulting deck semantic types
|
|
690
|
+
"executive-summary-scr": "bullet-points",
|
|
691
|
+
"executive-summary": "bullet-points",
|
|
692
|
+
"mece-breakdown": "three-column",
|
|
693
|
+
"process-timeline": "timeline",
|
|
694
|
+
"recommendation": "single-statement",
|
|
695
|
+
"risks-mitigation": "two-column",
|
|
696
|
+
"next-steps": "bullet-points",
|
|
697
|
+
"detailed-findings": "bullet-points",
|
|
698
|
+
// Investment banking semantic types
|
|
699
|
+
"situation-overview": "bullet-points",
|
|
700
|
+
"credentials": "metrics-grid",
|
|
701
|
+
"valuation-summary": "metrics-grid",
|
|
702
|
+
"football-field": "comparison",
|
|
703
|
+
"comparable-companies": "metrics-grid",
|
|
704
|
+
"precedent-transactions": "metrics-grid",
|
|
705
|
+
"dcf-summary": "metrics-grid",
|
|
706
|
+
"waterfall-bridge": "timeline",
|
|
707
|
+
"sources-uses": "two-column",
|
|
708
|
+
"risk-factors": "bullet-points",
|
|
709
|
+
// Investor pitch semantic types
|
|
710
|
+
"problem": "single-statement",
|
|
711
|
+
"solution": "single-statement",
|
|
712
|
+
"market-size": "big-number",
|
|
713
|
+
"product": "three-points",
|
|
714
|
+
"business-model": "three-column",
|
|
715
|
+
"traction": "metrics-grid",
|
|
716
|
+
"competition": "comparison",
|
|
717
|
+
"team": "three-column",
|
|
718
|
+
"financials": "metrics-grid",
|
|
719
|
+
"ask": "single-statement",
|
|
720
|
+
// Technical presentation semantic types
|
|
721
|
+
"architecture-diagram": "full-image",
|
|
722
|
+
"data-flow": "full-image",
|
|
723
|
+
"metrics": "metrics-grid",
|
|
724
|
+
"tradeoffs": "comparison",
|
|
725
|
+
// All hands semantic types
|
|
726
|
+
"celebration": "single-statement",
|
|
727
|
+
"announcement": "single-statement",
|
|
728
|
+
"milestones": "timeline",
|
|
729
|
+
"team-recognition": "three-column",
|
|
730
|
+
"roadmap": "timeline"
|
|
731
|
+
};
|
|
732
|
+
return semanticToStructural[normalizedType] || "bullet-points";
|
|
733
|
+
}
|
|
601
734
|
mapContentPatternToSlideType(pattern, allowedTypes) {
|
|
602
735
|
const patternToTypes = {
|
|
603
|
-
big_number: ["
|
|
604
|
-
comparison: ["comparison", "
|
|
605
|
-
timeline: ["timeline", "
|
|
606
|
-
process: ["process", "
|
|
607
|
-
metrics: ["
|
|
608
|
-
quote: ["quote", "
|
|
609
|
-
code: ["
|
|
610
|
-
bullets: ["
|
|
611
|
-
prose: ["
|
|
736
|
+
big_number: ["big-number", "metrics-grid", "single-statement"],
|
|
737
|
+
comparison: ["comparison", "two-column", "three-column"],
|
|
738
|
+
timeline: ["timeline", "process", "three-points", "bullet-points"],
|
|
739
|
+
process: ["process", "timeline", "three-column", "three-points", "bullet-points"],
|
|
740
|
+
metrics: ["metrics-grid", "big-number", "three-points"],
|
|
741
|
+
quote: ["quote", "single-statement"],
|
|
742
|
+
code: ["code-snippet", "two-column"],
|
|
743
|
+
bullets: ["bullet-points", "three-points", "three-column", "two-column", "process"],
|
|
744
|
+
prose: ["single-statement", "title-impact", "two-column"]
|
|
612
745
|
};
|
|
746
|
+
if (pattern.primaryPattern === "process" || pattern.primaryPattern === "bullets") {
|
|
747
|
+
if (pattern.bulletCount === 3) {
|
|
748
|
+
const threePointTypes = ["three-points", "three-column"];
|
|
749
|
+
for (const t of threePointTypes) {
|
|
750
|
+
if (this.isTypeAllowed(t, allowedTypes)) return t;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (pattern.bulletCount && pattern.bulletCount >= 2 && pattern.bulletCount <= 6) {
|
|
754
|
+
const processTypes = ["timeline", "process", "bullet-points"];
|
|
755
|
+
for (const t of processTypes) {
|
|
756
|
+
if (this.isTypeAllowed(t, allowedTypes)) return t;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
613
760
|
const preferredTypes = patternToTypes[pattern.primaryPattern] || patternToTypes.prose || [];
|
|
614
761
|
for (const preferred of preferredTypes) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
const contentTypes = [
|
|
622
|
-
"bullet_points",
|
|
623
|
-
"bullet-points",
|
|
624
|
-
"two_column",
|
|
625
|
-
"two-column",
|
|
626
|
-
"three_column",
|
|
627
|
-
"three-column",
|
|
628
|
-
"data_insight"
|
|
629
|
-
];
|
|
762
|
+
if (this.isTypeAllowed(preferred, allowedTypes)) {
|
|
763
|
+
return preferred;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const contentTypes = ["bullet-points", "three-points", "single-statement"];
|
|
630
767
|
for (const ct of contentTypes) {
|
|
631
|
-
if (
|
|
768
|
+
if (this.isTypeAllowed(ct, allowedTypes)) return ct;
|
|
769
|
+
}
|
|
770
|
+
return this.normalizeToStructuralType(allowedTypes[0] ?? "bullet-points");
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Check if a structural type is allowed (directly or via semantic mapping)
|
|
774
|
+
*/
|
|
775
|
+
isTypeAllowed(structuralType, allowedTypes) {
|
|
776
|
+
const normalized = structuralType.toLowerCase().replace(/_/g, "-");
|
|
777
|
+
const underscored = normalized.replace(/-/g, "_");
|
|
778
|
+
if (allowedTypes.includes(normalized) || allowedTypes.includes(underscored)) {
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
for (const allowed of allowedTypes) {
|
|
782
|
+
if (this.normalizeToStructuralType(allowed) === normalized) {
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
632
785
|
}
|
|
633
|
-
return
|
|
786
|
+
return false;
|
|
634
787
|
}
|
|
635
788
|
/**
|
|
636
789
|
* Get a specific slide template by name from the KB.
|
|
@@ -691,9 +844,11 @@ var KnowledgeGateway = class {
|
|
|
691
844
|
optionLabels: ["Option A", "Option B", "Option C", "Option D"]
|
|
692
845
|
},
|
|
693
846
|
column: { labelTemplate: "Point {n}" },
|
|
694
|
-
|
|
847
|
+
// Subtitle needs more room for complete phrases - at least 10 words for keynote
|
|
848
|
+
subtitle: { maxWords: mode === "keynote" ? 12 : Math.min(20, Math.floor(maxWords / 2)) },
|
|
695
849
|
context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
|
|
696
|
-
|
|
850
|
+
// Step descriptions need minimum 8 words for meaningful content
|
|
851
|
+
step: { maxWords: Math.max(8, Math.min(20, Math.floor(maxWords / 3))) },
|
|
697
852
|
columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
|
|
698
853
|
};
|
|
699
854
|
}
|
|
@@ -1046,7 +1201,14 @@ var ContentAnalyzer = class {
|
|
|
1046
1201
|
if (foundTitle && i > titleLineIndex && line.length > 0) {
|
|
1047
1202
|
if (line.startsWith("#")) break;
|
|
1048
1203
|
if (line.startsWith("-") || line.startsWith("*") || /^\d+\./.test(line)) break;
|
|
1049
|
-
|
|
1204
|
+
const cleanLine = line.replace(/\*\*/g, "").trim();
|
|
1205
|
+
if (cleanLine.length <= 120) {
|
|
1206
|
+
subtitle = cleanLine;
|
|
1207
|
+
} else {
|
|
1208
|
+
const truncated = cleanLine.slice(0, 120);
|
|
1209
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
1210
|
+
subtitle = lastSpace > 80 ? truncated.slice(0, lastSpace) : truncated;
|
|
1211
|
+
}
|
|
1050
1212
|
break;
|
|
1051
1213
|
}
|
|
1052
1214
|
}
|
|
@@ -1169,7 +1331,60 @@ var ContentAnalyzer = class {
|
|
|
1169
1331
|
sections.push(currentSection);
|
|
1170
1332
|
}
|
|
1171
1333
|
}
|
|
1172
|
-
return sections;
|
|
1334
|
+
return this.combineParentChildSections(sections);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Combine parent sections with their child sub-sections.
|
|
1338
|
+
* Detects patterns like "Three Pillars" + 3 child sections and merges them.
|
|
1339
|
+
*/
|
|
1340
|
+
combineParentChildSections(sections) {
|
|
1341
|
+
const result = [];
|
|
1342
|
+
let i = 0;
|
|
1343
|
+
while (i < sections.length) {
|
|
1344
|
+
const current = sections[i];
|
|
1345
|
+
if (current && current.level === 2 && (!current.content || current.content.length < 20) && current.bullets.length === 0) {
|
|
1346
|
+
const children = [];
|
|
1347
|
+
let j = i + 1;
|
|
1348
|
+
while (j < sections.length && children.length < 4) {
|
|
1349
|
+
const child = sections[j];
|
|
1350
|
+
if (!child || child.level !== 3) break;
|
|
1351
|
+
children.push(child);
|
|
1352
|
+
j++;
|
|
1353
|
+
}
|
|
1354
|
+
if (children.length >= 2 && children.length <= 4) {
|
|
1355
|
+
const combined = {
|
|
1356
|
+
header: current.header,
|
|
1357
|
+
level: current.level,
|
|
1358
|
+
content: "",
|
|
1359
|
+
// Will use bullets instead
|
|
1360
|
+
bullets: children.map((child) => {
|
|
1361
|
+
const title = child.header.replace(/^\d+\.\s*/, "").trim();
|
|
1362
|
+
if (child.content) {
|
|
1363
|
+
return `**${title}**: ${child.content.split("\n")[0] || ""}`;
|
|
1364
|
+
}
|
|
1365
|
+
return title;
|
|
1366
|
+
}),
|
|
1367
|
+
metrics: [
|
|
1368
|
+
...current.metrics,
|
|
1369
|
+
...children.flatMap((c) => c.metrics)
|
|
1370
|
+
],
|
|
1371
|
+
// Add sub-sections as a property for advanced slide types
|
|
1372
|
+
subSections: children.map((child) => ({
|
|
1373
|
+
title: child.header.replace(/^\d+\.\s*/, "").trim(),
|
|
1374
|
+
content: child.content
|
|
1375
|
+
}))
|
|
1376
|
+
};
|
|
1377
|
+
result.push(combined);
|
|
1378
|
+
i = j;
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (current) {
|
|
1383
|
+
result.push(current);
|
|
1384
|
+
}
|
|
1385
|
+
i++;
|
|
1386
|
+
}
|
|
1387
|
+
return result;
|
|
1173
1388
|
}
|
|
1174
1389
|
/**
|
|
1175
1390
|
* Extract SCQA structure (Barbara Minto)
|
|
@@ -1752,11 +1967,538 @@ var ContentPatternClassifier = class {
|
|
|
1752
1967
|
}
|
|
1753
1968
|
};
|
|
1754
1969
|
|
|
1970
|
+
// src/kb/ExpertGuidanceEngine.ts
|
|
1971
|
+
var ExpertGuidanceEngine = class {
|
|
1972
|
+
kb;
|
|
1973
|
+
presentationType;
|
|
1974
|
+
constructor(kb, presentationType) {
|
|
1975
|
+
this.kb = kb;
|
|
1976
|
+
this.presentationType = presentationType;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Get the narrative arc for this presentation type.
|
|
1980
|
+
* Returns the story structure with required elements and their purposes.
|
|
1981
|
+
*/
|
|
1982
|
+
getNarrativeArc() {
|
|
1983
|
+
const mode = this.kb.getModeForType(this.presentationType);
|
|
1984
|
+
if (mode === "keynote") {
|
|
1985
|
+
return {
|
|
1986
|
+
framework: "Duarte Sparkline",
|
|
1987
|
+
expert: "Nancy Duarte",
|
|
1988
|
+
description: "Alternate between What Is (current reality) and What Could Be (possibility), building emotional tension to a climax, then resolve with New Bliss",
|
|
1989
|
+
elements: [
|
|
1990
|
+
{
|
|
1991
|
+
id: "opening_hook",
|
|
1992
|
+
name: "Opening Hook",
|
|
1993
|
+
purpose: "Grab attention in first 10 seconds with something unexpected",
|
|
1994
|
+
expertSource: 'Chris Anderson: "You have one chance to hook them"',
|
|
1995
|
+
designPrinciples: [
|
|
1996
|
+
"Start with a story, question, or surprising fact",
|
|
1997
|
+
"Create curiosity gap - make them NEED to know more",
|
|
1998
|
+
"Touch heart before head (Gallo)"
|
|
1999
|
+
],
|
|
2000
|
+
required: true
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
id: "what_is",
|
|
2004
|
+
name: "What Is (Current Reality)",
|
|
2005
|
+
purpose: "Establish the current state that audience recognizes",
|
|
2006
|
+
expertSource: 'Nancy Duarte: "Ground them in familiar reality"',
|
|
2007
|
+
designPrinciples: [
|
|
2008
|
+
"Use concrete, recognizable situations",
|
|
2009
|
+
"Create emotional resonance with current pain",
|
|
2010
|
+
"Keep it brief - setup for contrast"
|
|
2011
|
+
],
|
|
2012
|
+
required: true
|
|
2013
|
+
},
|
|
2014
|
+
{
|
|
2015
|
+
id: "what_could_be",
|
|
2016
|
+
name: "What Could Be (Vision)",
|
|
2017
|
+
purpose: "Paint the picture of the transformed future",
|
|
2018
|
+
expertSource: 'Nancy Duarte: "Make them FEEL the possibility"',
|
|
2019
|
+
designPrinciples: [
|
|
2020
|
+
"Be specific and vivid about the future state",
|
|
2021
|
+
"Connect emotionally - how will they FEEL?",
|
|
2022
|
+
"Create contrast with What Is"
|
|
2023
|
+
],
|
|
2024
|
+
required: true
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
id: "star_moment",
|
|
2028
|
+
name: "STAR Moment",
|
|
2029
|
+
purpose: "Something They'll Always Remember - the emotional climax",
|
|
2030
|
+
expertSource: 'Nancy Duarte: "One dramatic memorable moment"',
|
|
2031
|
+
designPrinciples: [
|
|
2032
|
+
"Make it unexpected and emotionally impactful",
|
|
2033
|
+
"Use a dramatic statistic, story, or demonstration",
|
|
2034
|
+
"This is what they'll tell others about",
|
|
2035
|
+
"It should give them chills or make them gasp"
|
|
2036
|
+
],
|
|
2037
|
+
required: true
|
|
2038
|
+
},
|
|
2039
|
+
{
|
|
2040
|
+
id: "call_to_action",
|
|
2041
|
+
name: "Call to Action",
|
|
2042
|
+
purpose: "Clear next step that audience can take",
|
|
2043
|
+
expertSource: 'Chris Anderson: "Give them something to DO"',
|
|
2044
|
+
designPrinciples: [
|
|
2045
|
+
"Be specific and actionable",
|
|
2046
|
+
"Make it achievable - first step, not whole journey",
|
|
2047
|
+
"Connect action to the vision you painted"
|
|
2048
|
+
],
|
|
2049
|
+
required: true
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
id: "new_bliss",
|
|
2053
|
+
name: "New Bliss (Resolution)",
|
|
2054
|
+
purpose: "Leave them with the transformed future firmly in mind",
|
|
2055
|
+
expertSource: 'Nancy Duarte: "End with the new world, not the old"',
|
|
2056
|
+
designPrinciples: [
|
|
2057
|
+
"Reinforce the vision one final time",
|
|
2058
|
+
"Make them want to be part of it",
|
|
2059
|
+
"End on emotional high, not logistics"
|
|
2060
|
+
],
|
|
2061
|
+
required: false
|
|
2062
|
+
}
|
|
2063
|
+
]
|
|
2064
|
+
};
|
|
2065
|
+
} else {
|
|
2066
|
+
return {
|
|
2067
|
+
framework: "Minto Pyramid (SCQA)",
|
|
2068
|
+
expert: "Barbara Minto",
|
|
2069
|
+
description: "Lead with the answer/recommendation, then support with evidence in MECE structure",
|
|
2070
|
+
elements: [
|
|
2071
|
+
{
|
|
2072
|
+
id: "situation",
|
|
2073
|
+
name: "Situation",
|
|
2074
|
+
purpose: "Context the audience already knows and agrees with",
|
|
2075
|
+
expertSource: 'Barbara Minto: "Start from common ground"',
|
|
2076
|
+
designPrinciples: [
|
|
2077
|
+
"Facts everyone agrees on",
|
|
2078
|
+
"Recent and relevant context",
|
|
2079
|
+
"Brief - not the star of the show"
|
|
2080
|
+
],
|
|
2081
|
+
required: true
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
id: "complication",
|
|
2085
|
+
name: "Complication",
|
|
2086
|
+
purpose: "The problem, threat, or opportunity that requires action",
|
|
2087
|
+
expertSource: 'Barbara Minto: "Why are we here?"',
|
|
2088
|
+
designPrinciples: [
|
|
2089
|
+
"Clear articulation of the challenge",
|
|
2090
|
+
"Why the status quo is unacceptable",
|
|
2091
|
+
"Creates urgency for the answer"
|
|
2092
|
+
],
|
|
2093
|
+
required: true
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
id: "answer",
|
|
2097
|
+
name: "Answer/Recommendation",
|
|
2098
|
+
purpose: "The core recommendation - upfront, not buried",
|
|
2099
|
+
expertSource: 'Barbara Minto: "Answer first, evidence second"',
|
|
2100
|
+
designPrinciples: [
|
|
2101
|
+
"Clear, actionable recommendation",
|
|
2102
|
+
"Use an action title that states the conclusion",
|
|
2103
|
+
"Executive should understand even if they stop here"
|
|
2104
|
+
],
|
|
2105
|
+
required: true
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
id: "evidence",
|
|
2109
|
+
name: "Supporting Evidence",
|
|
2110
|
+
purpose: "MECE support for the recommendation",
|
|
2111
|
+
expertSource: 'Barbara Minto: "Mutually Exclusive, Collectively Exhaustive"',
|
|
2112
|
+
designPrinciples: [
|
|
2113
|
+
"Group into 3-5 supporting pillars",
|
|
2114
|
+
"Each pillar has sub-evidence",
|
|
2115
|
+
"No gaps, no overlaps (MECE)",
|
|
2116
|
+
"Use action titles throughout"
|
|
2117
|
+
],
|
|
2118
|
+
required: true
|
|
2119
|
+
},
|
|
2120
|
+
{
|
|
2121
|
+
id: "next_steps",
|
|
2122
|
+
name: "Next Steps",
|
|
2123
|
+
purpose: "Clear actions with owners and timelines",
|
|
2124
|
+
expertSource: 'Analyst Academy: "End with who does what by when"',
|
|
2125
|
+
designPrinciples: [
|
|
2126
|
+
'Specific actions, not vague "follow up"',
|
|
2127
|
+
"Named owners where possible",
|
|
2128
|
+
"Realistic timelines"
|
|
2129
|
+
],
|
|
2130
|
+
required: true
|
|
2131
|
+
}
|
|
2132
|
+
]
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Get expert guidance for designing a specific type of slide.
|
|
2138
|
+
* This returns PRINCIPLES for excellence, not constraints.
|
|
2139
|
+
*/
|
|
2140
|
+
getSlideDesignGuidance(slideType) {
|
|
2141
|
+
const mode = this.kb.getModeForType(this.presentationType);
|
|
2142
|
+
switch (slideType) {
|
|
2143
|
+
case "title":
|
|
2144
|
+
case "title_impact":
|
|
2145
|
+
return {
|
|
2146
|
+
purpose: "Create immediate emotional connection and establish the one idea worth spreading",
|
|
2147
|
+
principles: [
|
|
2148
|
+
{
|
|
2149
|
+
expert: "Chris Anderson",
|
|
2150
|
+
principle: "One Idea Worth Spreading",
|
|
2151
|
+
application: "The title should encapsulate a single powerful idea that the audience will remember",
|
|
2152
|
+
technique: "Ask: If they remember ONE thing, what should it be? That's your title."
|
|
2153
|
+
},
|
|
2154
|
+
{
|
|
2155
|
+
expert: "Carmine Gallo",
|
|
2156
|
+
principle: "Headline Test",
|
|
2157
|
+
application: "Title should work as a Twitter headline - compelling in under 280 characters",
|
|
2158
|
+
technique: "Write 10 versions, pick the one that makes you want to hear more"
|
|
2159
|
+
},
|
|
2160
|
+
{
|
|
2161
|
+
expert: "Nancy Duarte",
|
|
2162
|
+
principle: "Glance Test",
|
|
2163
|
+
application: "Audience should grasp the essence in 3 seconds",
|
|
2164
|
+
technique: "Show it to someone for 3 seconds, ask what they remember"
|
|
2165
|
+
}
|
|
2166
|
+
],
|
|
2167
|
+
whatMakesItGreat: [
|
|
2168
|
+
"Creates curiosity - makes audience WANT to hear more",
|
|
2169
|
+
"Emotionally resonant - connects to something they care about",
|
|
2170
|
+
"Unexpected angle - not the obvious framing",
|
|
2171
|
+
"Memorable - they could repeat it to someone else"
|
|
2172
|
+
],
|
|
2173
|
+
antiPatterns: [
|
|
2174
|
+
'Generic topic labels ("Q4 Update")',
|
|
2175
|
+
"Long sentences that require reading",
|
|
2176
|
+
"Jargon or acronyms",
|
|
2177
|
+
"Multiple ideas competing for attention"
|
|
2178
|
+
],
|
|
2179
|
+
questions: [
|
|
2180
|
+
"Would someone share this title with a colleague?",
|
|
2181
|
+
"Does it create a curiosity gap?",
|
|
2182
|
+
"Can you understand the value in 3 seconds?",
|
|
2183
|
+
"Is there emotional resonance?"
|
|
2184
|
+
]
|
|
2185
|
+
};
|
|
2186
|
+
case "single_statement":
|
|
2187
|
+
case "big_idea":
|
|
2188
|
+
return {
|
|
2189
|
+
purpose: "Communicate ONE powerful insight that shifts perspective",
|
|
2190
|
+
principles: [
|
|
2191
|
+
{
|
|
2192
|
+
expert: "Garr Reynolds",
|
|
2193
|
+
principle: "Signal to Noise",
|
|
2194
|
+
application: "Everything on the slide should serve the one message. Eliminate ruthlessly.",
|
|
2195
|
+
technique: "For each element, ask: Does this AMPLIFY the message or distract from it?"
|
|
2196
|
+
},
|
|
2197
|
+
{
|
|
2198
|
+
expert: "Garr Reynolds",
|
|
2199
|
+
principle: "Amplification Through Simplification",
|
|
2200
|
+
application: "Empty space makes the remaining content more powerful",
|
|
2201
|
+
technique: "Try removing 50% of what's there. Is it clearer?"
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
expert: "Nancy Duarte",
|
|
2205
|
+
principle: "Audience Paradox",
|
|
2206
|
+
application: "They will read OR listen, never both. Design for one.",
|
|
2207
|
+
technique: "If presenting live, the statement should be short enough to absorb while listening"
|
|
2208
|
+
}
|
|
2209
|
+
],
|
|
2210
|
+
whatMakesItGreat: [
|
|
2211
|
+
"One clear insight that shifts thinking",
|
|
2212
|
+
"Ample whitespace that emphasizes the message",
|
|
2213
|
+
"Words are carefully chosen - each one earns its place",
|
|
2214
|
+
"Can be absorbed in a glance"
|
|
2215
|
+
],
|
|
2216
|
+
antiPatterns: [
|
|
2217
|
+
"Cramming multiple points onto one slide",
|
|
2218
|
+
'Adding "supporting" text that dilutes the message',
|
|
2219
|
+
"Decorative elements that don't serve the idea",
|
|
2220
|
+
"Topic labels instead of insights"
|
|
2221
|
+
],
|
|
2222
|
+
questions: [
|
|
2223
|
+
"What is the ONE thing this slide should communicate?",
|
|
2224
|
+
"If I remove this word/element, is the message clearer?",
|
|
2225
|
+
"Will this shift how they think about the topic?"
|
|
2226
|
+
]
|
|
2227
|
+
};
|
|
2228
|
+
case "star_moment":
|
|
2229
|
+
return {
|
|
2230
|
+
purpose: "Create the moment they'll remember forever and tell others about",
|
|
2231
|
+
principles: [
|
|
2232
|
+
{
|
|
2233
|
+
expert: "Nancy Duarte",
|
|
2234
|
+
principle: "STAR Moment",
|
|
2235
|
+
application: "Something They'll Always Remember - this is the emotional climax",
|
|
2236
|
+
technique: "What would make the audience gasp, laugh, or get chills?"
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
expert: "Chip & Dan Heath",
|
|
2240
|
+
principle: "Unexpected",
|
|
2241
|
+
application: "Break a pattern to get attention. Violate expectations.",
|
|
2242
|
+
technique: "What assumption can you shatter? What surprise can you deliver?"
|
|
2243
|
+
}
|
|
2244
|
+
],
|
|
2245
|
+
whatMakesItGreat: [
|
|
2246
|
+
"Creates visceral emotional response",
|
|
2247
|
+
"Breaks expectations - genuinely surprising",
|
|
2248
|
+
"Connects logically to the core message",
|
|
2249
|
+
"Memorable enough to retell"
|
|
2250
|
+
],
|
|
2251
|
+
antiPatterns: [
|
|
2252
|
+
"Playing it safe with expected information",
|
|
2253
|
+
"Burying the dramatic element in other content",
|
|
2254
|
+
"Gimmicks that don't connect to the message"
|
|
2255
|
+
],
|
|
2256
|
+
questions: [
|
|
2257
|
+
"Will this give them chills or make them gasp?",
|
|
2258
|
+
"Would they tell someone about this tomorrow?",
|
|
2259
|
+
"Does it support or distract from the core message?"
|
|
2260
|
+
]
|
|
2261
|
+
};
|
|
2262
|
+
case "big_number":
|
|
2263
|
+
case "data_insight":
|
|
2264
|
+
return {
|
|
2265
|
+
purpose: "Make data memorable by providing context that reveals meaning",
|
|
2266
|
+
principles: [
|
|
2267
|
+
{
|
|
2268
|
+
expert: "Edward Tufte",
|
|
2269
|
+
principle: "Data-Ink Ratio",
|
|
2270
|
+
application: "Maximize ink used for data, minimize everything else",
|
|
2271
|
+
technique: "Remove all decoration. Is the data still clear? Good. Now make it beautiful."
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
expert: "Lea Pica",
|
|
2275
|
+
principle: "One Insight Per Chart",
|
|
2276
|
+
application: "Don't let data speak for itself - tell them exactly what to see",
|
|
2277
|
+
technique: "What's the ONE conclusion? Make that the headline."
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
expert: "Cole Knaflic",
|
|
2281
|
+
principle: "Context Creates Meaning",
|
|
2282
|
+
application: "Numbers mean nothing without context",
|
|
2283
|
+
technique: "Compare to something they understand - time, money, scale"
|
|
2284
|
+
}
|
|
2285
|
+
],
|
|
2286
|
+
whatMakesItGreat: [
|
|
2287
|
+
"The number is HUGE and simple",
|
|
2288
|
+
"Context makes the number meaningful",
|
|
2289
|
+
"Audience immediately understands significance",
|
|
2290
|
+
"One clear insight, not a data dump"
|
|
2291
|
+
],
|
|
2292
|
+
antiPatterns: [
|
|
2293
|
+
"Multiple competing numbers",
|
|
2294
|
+
'Data without context ("$5M" - is that good?)',
|
|
2295
|
+
"Complex charts when simple would work",
|
|
2296
|
+
"Decorative elements that distract from data"
|
|
2297
|
+
],
|
|
2298
|
+
questions: [
|
|
2299
|
+
"What is the ONE insight this data reveals?",
|
|
2300
|
+
"What context makes this number meaningful?",
|
|
2301
|
+
"Can someone understand the significance in 3 seconds?"
|
|
2302
|
+
]
|
|
2303
|
+
};
|
|
2304
|
+
case "three_points":
|
|
2305
|
+
case "three_column":
|
|
2306
|
+
return {
|
|
2307
|
+
purpose: "Present key messages in memorable groups of three",
|
|
2308
|
+
principles: [
|
|
2309
|
+
{
|
|
2310
|
+
expert: "Carmine Gallo",
|
|
2311
|
+
principle: "Rule of Three",
|
|
2312
|
+
application: "Three is the magic number for memory. Not two, not four.",
|
|
2313
|
+
technique: "Can you group your content into exactly three main points?"
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
expert: "Barbara Minto",
|
|
2317
|
+
principle: "MECE",
|
|
2318
|
+
application: "Points should be Mutually Exclusive (no overlap) and Collectively Exhaustive (no gaps)",
|
|
2319
|
+
technique: "Do these three points cover everything without repeating?"
|
|
2320
|
+
}
|
|
2321
|
+
],
|
|
2322
|
+
whatMakesItGreat: [
|
|
2323
|
+
"Exactly three points - no more, no less",
|
|
2324
|
+
"Each point is substantively different",
|
|
2325
|
+
"Together they tell a complete story",
|
|
2326
|
+
"Each point is memorable on its own"
|
|
2327
|
+
],
|
|
2328
|
+
antiPatterns: [
|
|
2329
|
+
"Forcing content into three when it should be two or four",
|
|
2330
|
+
"Overlapping points that aren't distinct",
|
|
2331
|
+
"Generic labels instead of insights",
|
|
2332
|
+
"Uneven importance across points"
|
|
2333
|
+
],
|
|
2334
|
+
questions: [
|
|
2335
|
+
"Are these three truly distinct and non-overlapping?",
|
|
2336
|
+
"Do they together cover the complete picture?",
|
|
2337
|
+
"Is each point valuable on its own?"
|
|
2338
|
+
]
|
|
2339
|
+
};
|
|
2340
|
+
case "call_to_action":
|
|
2341
|
+
case "cta":
|
|
2342
|
+
return {
|
|
2343
|
+
purpose: "Give the audience a clear, compelling next step they can take",
|
|
2344
|
+
principles: [
|
|
2345
|
+
{
|
|
2346
|
+
expert: "Chris Anderson",
|
|
2347
|
+
principle: "Gift Giving",
|
|
2348
|
+
application: "Your job is to give them something valuable they can USE",
|
|
2349
|
+
technique: "What can they DO tomorrow because of this presentation?"
|
|
2350
|
+
},
|
|
2351
|
+
{
|
|
2352
|
+
expert: "Robert Cialdini",
|
|
2353
|
+
principle: "Commitment",
|
|
2354
|
+
application: "Small commitments lead to larger ones",
|
|
2355
|
+
technique: "What's the smallest first step that gets them moving?"
|
|
2356
|
+
}
|
|
2357
|
+
],
|
|
2358
|
+
whatMakesItGreat: [
|
|
2359
|
+
'Specific and actionable - not vague "learn more"',
|
|
2360
|
+
"Achievable first step, not overwhelming",
|
|
2361
|
+
"Clearly connected to the value proposition",
|
|
2362
|
+
"Creates urgency without being pushy"
|
|
2363
|
+
],
|
|
2364
|
+
antiPatterns: [
|
|
2365
|
+
'Vague calls to action ("contact us")',
|
|
2366
|
+
"Multiple competing CTAs",
|
|
2367
|
+
"Actions that feel like too much work",
|
|
2368
|
+
"Disconnected from the presentation content"
|
|
2369
|
+
],
|
|
2370
|
+
questions: [
|
|
2371
|
+
"Can they do this tomorrow?",
|
|
2372
|
+
"Is this the logical first step?",
|
|
2373
|
+
"Does it connect to the value you promised?"
|
|
2374
|
+
]
|
|
2375
|
+
};
|
|
2376
|
+
default:
|
|
2377
|
+
return {
|
|
2378
|
+
purpose: "Communicate one clear message that advances the narrative",
|
|
2379
|
+
principles: [
|
|
2380
|
+
{
|
|
2381
|
+
expert: "Garr Reynolds",
|
|
2382
|
+
principle: "Signal to Noise",
|
|
2383
|
+
application: "Maximize signal (valuable content), minimize noise (distractions)",
|
|
2384
|
+
technique: "For every element, ask: Does this serve the message?"
|
|
2385
|
+
},
|
|
2386
|
+
{
|
|
2387
|
+
expert: "Nancy Duarte",
|
|
2388
|
+
principle: "Glance Test",
|
|
2389
|
+
application: "Key message should be clear in 3 seconds",
|
|
2390
|
+
technique: "What would someone remember after a 3-second glance?"
|
|
2391
|
+
}
|
|
2392
|
+
],
|
|
2393
|
+
whatMakesItGreat: [
|
|
2394
|
+
"One clear message per slide",
|
|
2395
|
+
"Visual hierarchy guides the eye",
|
|
2396
|
+
"Every element serves a purpose",
|
|
2397
|
+
"Can be understood at a glance"
|
|
2398
|
+
],
|
|
2399
|
+
antiPatterns: [
|
|
2400
|
+
"Multiple competing messages",
|
|
2401
|
+
"Decorative elements that distract",
|
|
2402
|
+
"Text-heavy slides that require reading",
|
|
2403
|
+
"Unclear what's most important"
|
|
2404
|
+
],
|
|
2405
|
+
questions: [
|
|
2406
|
+
"What is the ONE message?",
|
|
2407
|
+
"What can be removed without losing meaning?",
|
|
2408
|
+
"Does visual hierarchy match content hierarchy?"
|
|
2409
|
+
]
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Get guidance for extracting the core message from content.
|
|
2415
|
+
* This helps identify WHAT should be on the slide, not just how to fit content.
|
|
2416
|
+
*/
|
|
2417
|
+
getCoreMessageGuidance() {
|
|
2418
|
+
return [
|
|
2419
|
+
{
|
|
2420
|
+
expert: "Chris Anderson",
|
|
2421
|
+
principle: "One Idea Worth Spreading",
|
|
2422
|
+
application: "Find the single most valuable insight in this content",
|
|
2423
|
+
technique: "If they could only remember ONE thing, what should it be?"
|
|
2424
|
+
},
|
|
2425
|
+
{
|
|
2426
|
+
expert: "Carmine Gallo",
|
|
2427
|
+
principle: "Twitter Test",
|
|
2428
|
+
application: "Can you express the key insight in a tweet?",
|
|
2429
|
+
technique: "Write the insight in under 280 characters. If you can't, you don't understand it well enough."
|
|
2430
|
+
},
|
|
2431
|
+
{
|
|
2432
|
+
expert: "Barbara Minto",
|
|
2433
|
+
principle: "So What?",
|
|
2434
|
+
application: 'Keep asking "so what?" until you reach the insight',
|
|
2435
|
+
technique: 'For every statement, ask "so what?" The answer is closer to the real message.'
|
|
2436
|
+
}
|
|
2437
|
+
];
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Evaluate whether a slide meets expert standards.
|
|
2441
|
+
* Returns guidance on how to improve, not just pass/fail.
|
|
2442
|
+
*/
|
|
2443
|
+
evaluateSlideExcellence(slide) {
|
|
2444
|
+
const guidance = this.getSlideDesignGuidance(slide.type);
|
|
2445
|
+
const strengths = [];
|
|
2446
|
+
const improvements = [];
|
|
2447
|
+
const expertFeedback = [];
|
|
2448
|
+
let score = 50;
|
|
2449
|
+
for (const quality of guidance.whatMakesItGreat) {
|
|
2450
|
+
}
|
|
2451
|
+
for (const antiPattern of guidance.antiPatterns) {
|
|
2452
|
+
}
|
|
2453
|
+
for (const question of guidance.questions) {
|
|
2454
|
+
expertFeedback.push(`Ask: ${question}`);
|
|
2455
|
+
}
|
|
2456
|
+
if (slide.title) {
|
|
2457
|
+
if (slide.title.length <= 60) {
|
|
2458
|
+
strengths.push("Title passes Duarte Glance Test (under 60 chars)");
|
|
2459
|
+
score += 10;
|
|
2460
|
+
} else {
|
|
2461
|
+
improvements.push("Shorten title - Duarte says 3 seconds to comprehend");
|
|
2462
|
+
}
|
|
2463
|
+
if (!/^(update|overview|introduction|summary)$/i.test(slide.title)) {
|
|
2464
|
+
strengths.push("Title is specific, not generic (Gallo Headline Test)");
|
|
2465
|
+
score += 5;
|
|
2466
|
+
} else {
|
|
2467
|
+
improvements.push("Replace generic title with insight - Gallo says it should work as a headline");
|
|
2468
|
+
score -= 10;
|
|
2469
|
+
}
|
|
2470
|
+
if (!slide.title.includes(" and ") && !slide.title.includes(" & ")) {
|
|
2471
|
+
strengths.push("Title focuses on one idea (Chris Anderson)");
|
|
2472
|
+
score += 5;
|
|
2473
|
+
} else {
|
|
2474
|
+
improvements.push("Split into two slides - Anderson says one idea per slide");
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (slide.bullets && slide.bullets.length > 5) {
|
|
2478
|
+
improvements.push("Reduce bullets to 3-5 - Reynolds says maximize signal, eliminate noise");
|
|
2479
|
+
score -= 10;
|
|
2480
|
+
}
|
|
2481
|
+
return {
|
|
2482
|
+
score: Math.max(0, Math.min(100, score)),
|
|
2483
|
+
strengths,
|
|
2484
|
+
improvements,
|
|
2485
|
+
expertFeedback
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
function createExpertGuidanceEngine(kb, presentationType) {
|
|
2490
|
+
return new ExpertGuidanceEngine(kb, presentationType);
|
|
2491
|
+
}
|
|
2492
|
+
|
|
1755
2493
|
// src/core/SlideFactory.ts
|
|
1756
2494
|
var SlideFactory = class {
|
|
1757
2495
|
kb;
|
|
1758
2496
|
presentationType;
|
|
1759
2497
|
classifier;
|
|
2498
|
+
expertEngine;
|
|
2499
|
+
// NEW: Expert guidance for world-class slides
|
|
2500
|
+
narrativeArc;
|
|
2501
|
+
// NEW: Story structure from experts
|
|
1760
2502
|
config;
|
|
1761
2503
|
usedContent;
|
|
1762
2504
|
usedTitles;
|
|
@@ -1767,7 +2509,10 @@ var SlideFactory = class {
|
|
|
1767
2509
|
this.usedContent = /* @__PURE__ */ new Set();
|
|
1768
2510
|
this.usedTitles = /* @__PURE__ */ new Set();
|
|
1769
2511
|
this.config = this.loadKBConfig(type);
|
|
1770
|
-
|
|
2512
|
+
this.expertEngine = createExpertGuidanceEngine(kb, type);
|
|
2513
|
+
this.narrativeArc = this.expertEngine.getNarrativeArc();
|
|
2514
|
+
logger.step(`SlideFactory v8.0.0 initialized for ${type} (${this.config.mode} mode)`);
|
|
2515
|
+
logger.step(`Using ${this.narrativeArc.framework} narrative (${this.narrativeArc.expert})`);
|
|
1771
2516
|
logger.step(`KB Config loaded: ${this.config.allowedTypes.length} allowed types`);
|
|
1772
2517
|
}
|
|
1773
2518
|
/**
|
|
@@ -1807,15 +2552,56 @@ var SlideFactory = class {
|
|
|
1807
2552
|
} else if (storyStructure.framework === "sparkline") {
|
|
1808
2553
|
this.addSparklineSlides(slides, analysis);
|
|
1809
2554
|
}
|
|
2555
|
+
let hasCTASection = false;
|
|
1810
2556
|
for (const section of analysis.sections) {
|
|
2557
|
+
logger.debug(`[TRACE] Processing section: "${section.header}"`);
|
|
2558
|
+
logger.debug(`[TRACE] Level: ${section.level}, Content: ${section.content?.length ?? 0} chars`);
|
|
2559
|
+
logger.debug(`[TRACE] Bullets: ${section.bullets?.length ?? 0} items`);
|
|
2560
|
+
if (section.bullets?.length > 0) {
|
|
2561
|
+
section.bullets.slice(0, 3).forEach((b, i) => {
|
|
2562
|
+
logger.debug(`[TRACE] Bullet ${i}: "${b.slice(0, 50)}..."`);
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
1811
2565
|
const contentKey = this.normalizeKey(section.header);
|
|
1812
|
-
if (this.usedContent.has(contentKey))
|
|
2566
|
+
if (this.usedContent.has(contentKey)) {
|
|
2567
|
+
logger.debug(`[TRACE] SKIPPED: Already used content key "${contentKey}"`);
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
1813
2570
|
this.usedContent.add(contentKey);
|
|
2571
|
+
const headerLower = section.header.toLowerCase();
|
|
2572
|
+
const isCTASection = headerLower.includes("call to action") || headerLower.includes("next step") || headerLower.includes("take action") || headerLower.includes("get started") || headerLower.includes("start today");
|
|
2573
|
+
if (isCTASection) {
|
|
2574
|
+
hasCTASection = true;
|
|
2575
|
+
const ctaDefaults = this.config.defaults.cta;
|
|
2576
|
+
const ctaContent = section.bullets.length > 0 ? section.bullets.slice(0, 4).join(". ") : section.content || ctaDefaults.fallback;
|
|
2577
|
+
const craftedCTA = this.craftExpertContent(
|
|
2578
|
+
ctaContent,
|
|
2579
|
+
"call_to_action",
|
|
2580
|
+
this.config.rules.wordsPerSlide.max * 3
|
|
2581
|
+
// Allow more words for CTA steps
|
|
2582
|
+
);
|
|
2583
|
+
slides.push({
|
|
2584
|
+
index: slides.length,
|
|
2585
|
+
type: "cta",
|
|
2586
|
+
data: {
|
|
2587
|
+
title: section.header,
|
|
2588
|
+
body: craftedCTA,
|
|
2589
|
+
keyMessage: ctaDefaults.message
|
|
2590
|
+
},
|
|
2591
|
+
classes: ["cta-slide"]
|
|
2592
|
+
});
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
1814
2595
|
const pattern = this.classifier.classify(section);
|
|
2596
|
+
logger.debug(`[TRACE] Pattern: ${pattern.primaryPattern}, bulletCount: ${pattern.bulletCount}`);
|
|
1815
2597
|
const slideType = this.kb.mapContentPatternToSlideType(pattern, this.config.allowedTypes);
|
|
2598
|
+
logger.debug(`[TRACE] KB returned slide type: "${slideType}"`);
|
|
1816
2599
|
const slide = this.createSlideByType(slides.length, slideType, section, pattern);
|
|
1817
|
-
if (slide) {
|
|
2600
|
+
if (slide && slide.type && slide.data) {
|
|
2601
|
+
logger.debug(`[TRACE] Created slide type: "${slide.type}" with ${slide.data.bullets?.length ?? 0} bullets`);
|
|
1818
2602
|
slides.push(slide);
|
|
2603
|
+
} else {
|
|
2604
|
+
logger.debug(`[TRACE] SKIPPED: Slide filtered out (no valid content)`);
|
|
1819
2605
|
}
|
|
1820
2606
|
}
|
|
1821
2607
|
const minDataPointsForMetrics = 2;
|
|
@@ -1827,7 +2613,7 @@ var SlideFactory = class {
|
|
|
1827
2613
|
slides.push(this.createMetricsGridSlide(slides.length, analysis.dataPoints));
|
|
1828
2614
|
}
|
|
1829
2615
|
}
|
|
1830
|
-
if (analysis.sparkline?.callToAdventure || analysis.scqa?.answer) {
|
|
2616
|
+
if (!hasCTASection && (analysis.sparkline?.callToAdventure || analysis.scqa?.answer)) {
|
|
1831
2617
|
slides.push(this.createCTASlide(slides.length, analysis));
|
|
1832
2618
|
}
|
|
1833
2619
|
slides.push(this.createThankYouSlide(slides.length));
|
|
@@ -1843,34 +2629,55 @@ var SlideFactory = class {
|
|
|
1843
2629
|
createSlideByType(index, type, section, pattern) {
|
|
1844
2630
|
const normalizedType = type.toLowerCase().replace(/_/g, "-");
|
|
1845
2631
|
switch (normalizedType) {
|
|
2632
|
+
// Big number slides - show prominent metrics
|
|
1846
2633
|
case "big-number":
|
|
1847
|
-
case "data-insight":
|
|
1848
2634
|
return this.createBigNumberSlide(index, section, pattern);
|
|
2635
|
+
// Data insight - for findings that may or may not have numbers
|
|
2636
|
+
case "data-insight":
|
|
2637
|
+
return this.createDataInsightSlide(index, section, pattern);
|
|
2638
|
+
// Comparison slides
|
|
1849
2639
|
case "comparison":
|
|
1850
2640
|
case "options-comparison":
|
|
1851
2641
|
return this.createComparisonSlide(index, section);
|
|
2642
|
+
// Timeline and roadmap slides
|
|
1852
2643
|
case "timeline":
|
|
1853
2644
|
case "process-timeline":
|
|
1854
2645
|
case "roadmap":
|
|
1855
2646
|
return this.createTimelineSlide(index, section);
|
|
2647
|
+
// Process flow slides (with steps)
|
|
1856
2648
|
case "process":
|
|
2649
|
+
case "mece-breakdown":
|
|
1857
2650
|
return this.createProcessSlide(index, section);
|
|
2651
|
+
// Three points slide (for exactly 3 items)
|
|
2652
|
+
case "three-points":
|
|
2653
|
+
return this.createThreePointsSlide(index, section);
|
|
2654
|
+
// Column layouts
|
|
1858
2655
|
case "three-column":
|
|
1859
2656
|
return this.createThreeColumnSlide(index, section);
|
|
1860
2657
|
case "two-column":
|
|
1861
2658
|
return this.createTwoColumnSlide(index, section);
|
|
2659
|
+
// Quote and testimonial slides
|
|
1862
2660
|
case "quote":
|
|
1863
2661
|
case "testimonial":
|
|
1864
2662
|
case "social-proof":
|
|
1865
2663
|
return this.createQuoteSlide(index, section);
|
|
2664
|
+
// Metrics grid for multiple KPIs
|
|
1866
2665
|
case "metrics-grid":
|
|
1867
2666
|
return this.createMetricsGridSlide(index, section.metrics);
|
|
2667
|
+
// Standard bullet point slides
|
|
1868
2668
|
case "bullet-points":
|
|
1869
|
-
case "detailed-findings":
|
|
1870
2669
|
return this.createBulletSlide(index, section);
|
|
2670
|
+
// Title impact - emphasized single statement
|
|
2671
|
+
case "title-impact":
|
|
2672
|
+
return this.createTitleImpactSlide(index, section);
|
|
2673
|
+
// Single statement slides
|
|
1871
2674
|
case "single-statement":
|
|
1872
2675
|
case "big-idea":
|
|
1873
2676
|
return this.createSingleStatementSlide(index, section);
|
|
2677
|
+
// Detailed findings - bullets with context
|
|
2678
|
+
case "detailed-findings":
|
|
2679
|
+
return this.createDetailedFindingsSlide(index, section);
|
|
2680
|
+
// Code and technical slides
|
|
1874
2681
|
case "code-snippet":
|
|
1875
2682
|
case "technical":
|
|
1876
2683
|
return this.createCodeSlide(index, section);
|
|
@@ -1883,15 +2690,21 @@ var SlideFactory = class {
|
|
|
1883
2690
|
// STRUCTURAL SLIDES (title, agenda, thank you) - ALL from KB
|
|
1884
2691
|
// ===========================================================================
|
|
1885
2692
|
createTitleSlide(index, analysis) {
|
|
2693
|
+
const craftedTitle = this.craftExpertContent(
|
|
2694
|
+
analysis.title,
|
|
2695
|
+
"title",
|
|
2696
|
+
this.config.rules.wordsPerSlide.max
|
|
2697
|
+
);
|
|
1886
2698
|
const data = {
|
|
1887
|
-
title:
|
|
2699
|
+
title: craftedTitle
|
|
1888
2700
|
};
|
|
1889
2701
|
const subtitleSource = analysis.subtitle || analysis.keyMessages[0] || "";
|
|
1890
2702
|
if (subtitleSource) {
|
|
1891
|
-
data.subtitle = this.
|
|
2703
|
+
data.subtitle = this.craftExpertContent(
|
|
1892
2704
|
subtitleSource,
|
|
2705
|
+
"title",
|
|
2706
|
+
// Use title guidance for subtitle too
|
|
1893
2707
|
this.config.defaults.subtitle.maxWords
|
|
1894
|
-
// FROM KB
|
|
1895
2708
|
);
|
|
1896
2709
|
}
|
|
1897
2710
|
return {
|
|
@@ -1980,31 +2793,43 @@ var SlideFactory = class {
|
|
|
1980
2793
|
addSparklineSlides(slides, analysis) {
|
|
1981
2794
|
const spark = analysis.sparkline;
|
|
1982
2795
|
const titles = this.config.sparklineTitles;
|
|
2796
|
+
const maxWords = this.config.rules.wordsPerSlide.max;
|
|
1983
2797
|
const whatIsFirst = spark?.whatIs?.[0];
|
|
1984
|
-
const whatIsBody = whatIsFirst ? this.
|
|
1985
|
-
|
|
2798
|
+
const whatIsBody = whatIsFirst ? this.craftExpertContent(whatIsFirst, "single_statement", maxWords) : "";
|
|
2799
|
+
const whatCouldBeFirst = spark?.whatCouldBe?.[0];
|
|
2800
|
+
const whatCouldBeBody = whatCouldBeFirst ? this.craftExpertContent(whatCouldBeFirst, "single_statement", maxWords) : "";
|
|
2801
|
+
const normalizedTitle = analysis.title.toLowerCase().slice(0, 50);
|
|
2802
|
+
const normalizedSubtitle = (analysis.subtitle || "").toLowerCase().slice(0, 50);
|
|
2803
|
+
const normalizedWhatIs = whatIsBody.toLowerCase().slice(0, 50);
|
|
2804
|
+
const normalizedWhatCouldBe = whatCouldBeBody.toLowerCase().slice(0, 50);
|
|
2805
|
+
const whatIsDuplicatesTitle = normalizedWhatIs === normalizedTitle || normalizedWhatIs.includes(normalizedTitle.slice(0, 30)) || normalizedTitle.includes(normalizedWhatIs.slice(0, 30));
|
|
2806
|
+
const whatIsDuplicatesSubtitle = normalizedWhatIs === normalizedSubtitle || normalizedWhatIs.includes(normalizedSubtitle.slice(0, 30)) || normalizedSubtitle.includes(normalizedWhatIs.slice(0, 30));
|
|
2807
|
+
const isDuplicatePair = normalizedWhatIs === normalizedWhatCouldBe;
|
|
2808
|
+
if (whatIsBody.length >= 30 && !isDuplicatePair && !whatIsDuplicatesTitle && !whatIsDuplicatesSubtitle && !this.usedContent.has("spark-what-is")) {
|
|
1986
2809
|
this.usedContent.add("spark-what-is");
|
|
2810
|
+
this.usedContent.add(normalizedWhatIs);
|
|
1987
2811
|
slides.push({
|
|
1988
2812
|
index: slides.length,
|
|
1989
2813
|
type: "single-statement",
|
|
1990
2814
|
data: {
|
|
1991
2815
|
title: titles.whatIs,
|
|
1992
|
-
// FROM KB
|
|
2816
|
+
// FROM KB
|
|
1993
2817
|
body: whatIsBody
|
|
1994
2818
|
},
|
|
1995
2819
|
classes: ["what-is-slide"]
|
|
1996
2820
|
});
|
|
1997
2821
|
}
|
|
1998
|
-
const
|
|
1999
|
-
const
|
|
2000
|
-
if (whatCouldBeBody.length >=
|
|
2822
|
+
const whatCouldBeDuplicatesTitle = normalizedWhatCouldBe === normalizedTitle || normalizedWhatCouldBe.includes(normalizedTitle.slice(0, 30)) || normalizedTitle.includes(normalizedWhatCouldBe.slice(0, 30));
|
|
2823
|
+
const whatCouldBeDuplicatesSubtitle = normalizedWhatCouldBe === normalizedSubtitle || normalizedWhatCouldBe.includes(normalizedSubtitle.slice(0, 30)) || normalizedSubtitle.includes(normalizedWhatCouldBe.slice(0, 30));
|
|
2824
|
+
if (whatCouldBeBody.length >= 30 && !isDuplicatePair && !whatCouldBeDuplicatesTitle && !whatCouldBeDuplicatesSubtitle && !this.usedContent.has("spark-could-be")) {
|
|
2001
2825
|
this.usedContent.add("spark-could-be");
|
|
2826
|
+
this.usedContent.add(normalizedWhatCouldBe);
|
|
2002
2827
|
slides.push({
|
|
2003
2828
|
index: slides.length,
|
|
2004
2829
|
type: "single-statement",
|
|
2005
2830
|
data: {
|
|
2006
2831
|
title: titles.whatCouldBe,
|
|
2007
|
-
// FROM KB
|
|
2832
|
+
// FROM KB
|
|
2008
2833
|
body: whatCouldBeBody
|
|
2009
2834
|
},
|
|
2010
2835
|
classes: ["what-could-be-slide"]
|
|
@@ -2022,17 +2847,19 @@ var SlideFactory = class {
|
|
|
2022
2847
|
logger.warn(`No number found for big-number slide, falling back to single-statement`);
|
|
2023
2848
|
return this.createSingleStatementSlide(index, section);
|
|
2024
2849
|
}
|
|
2850
|
+
const contextContent = bigNumber?.context || section.content;
|
|
2851
|
+
const craftedContext = this.craftExpertContent(
|
|
2852
|
+
contextContent,
|
|
2853
|
+
"big_number",
|
|
2854
|
+
this.config.defaults.context.maxWords
|
|
2855
|
+
);
|
|
2025
2856
|
return {
|
|
2026
2857
|
index,
|
|
2027
2858
|
type: "big-number",
|
|
2028
2859
|
data: {
|
|
2029
2860
|
title: this.createTitle(section.header, section),
|
|
2030
2861
|
keyMessage: actualValue,
|
|
2031
|
-
body:
|
|
2032
|
-
section.content,
|
|
2033
|
-
this.config.defaults.context.maxWords
|
|
2034
|
-
// FROM KB - not hardcoded 30
|
|
2035
|
-
)
|
|
2862
|
+
body: craftedContext
|
|
2036
2863
|
},
|
|
2037
2864
|
classes: ["big-number-slide"]
|
|
2038
2865
|
};
|
|
@@ -2069,9 +2896,8 @@ var SlideFactory = class {
|
|
|
2069
2896
|
const steps = this.classifier.extractSteps(section);
|
|
2070
2897
|
const maxSteps = Math.min(
|
|
2071
2898
|
steps.length,
|
|
2072
|
-
this.config.rules.bulletsPerSlide.max,
|
|
2073
2899
|
this.config.millersLaw.maxItems
|
|
2074
|
-
// FROM KB - 7±2 rule
|
|
2900
|
+
// FROM KB - 7±2 rule (max 9)
|
|
2075
2901
|
);
|
|
2076
2902
|
return {
|
|
2077
2903
|
index,
|
|
@@ -2079,7 +2905,8 @@ var SlideFactory = class {
|
|
|
2079
2905
|
data: {
|
|
2080
2906
|
title: this.createTitle(section.header, section),
|
|
2081
2907
|
steps: steps.slice(0, maxSteps).map((step) => ({
|
|
2082
|
-
label: step.label,
|
|
2908
|
+
label: this.cleanText(step.label),
|
|
2909
|
+
// Clean markdown from labels
|
|
2083
2910
|
description: this.truncateText(
|
|
2084
2911
|
step.description,
|
|
2085
2912
|
this.config.defaults.step.maxWords
|
|
@@ -2092,7 +2919,7 @@ var SlideFactory = class {
|
|
|
2092
2919
|
}
|
|
2093
2920
|
createProcessSlide(index, section) {
|
|
2094
2921
|
const steps = this.classifier.extractSteps(section);
|
|
2095
|
-
const maxSteps = Math.min(steps.length, this.config.
|
|
2922
|
+
const maxSteps = Math.min(steps.length, this.config.millersLaw.maxItems);
|
|
2096
2923
|
return {
|
|
2097
2924
|
index,
|
|
2098
2925
|
type: "process",
|
|
@@ -2100,7 +2927,8 @@ var SlideFactory = class {
|
|
|
2100
2927
|
title: this.createTitle(section.header, section),
|
|
2101
2928
|
steps: steps.slice(0, maxSteps).map((step, i) => ({
|
|
2102
2929
|
number: i + 1,
|
|
2103
|
-
title: step.label,
|
|
2930
|
+
title: this.cleanText(step.label),
|
|
2931
|
+
// Clean markdown from labels
|
|
2104
2932
|
description: this.truncateText(
|
|
2105
2933
|
step.description,
|
|
2106
2934
|
this.config.defaults.step.maxWords
|
|
@@ -2135,6 +2963,69 @@ var SlideFactory = class {
|
|
|
2135
2963
|
classes: ["three-column-slide"]
|
|
2136
2964
|
};
|
|
2137
2965
|
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Create a three-points slide for exactly 3 key points.
|
|
2968
|
+
* Used for content that fits the "rule of three" (Carmine Gallo).
|
|
2969
|
+
*/
|
|
2970
|
+
createThreePointsSlide(index, section) {
|
|
2971
|
+
const points = [];
|
|
2972
|
+
if (section.bullets.length >= 3) {
|
|
2973
|
+
for (let i = 0; i < 3; i++) {
|
|
2974
|
+
const bullet = section.bullets[i] || "";
|
|
2975
|
+
const extracted = this.classifier.extractSteps({ ...section, bullets: [bullet] });
|
|
2976
|
+
if (extracted.length > 0 && extracted[0] && extracted[0].description) {
|
|
2977
|
+
points.push({
|
|
2978
|
+
number: i + 1,
|
|
2979
|
+
title: this.cleanText(extracted[0].label) || `Step ${i + 1}`,
|
|
2980
|
+
description: this.truncateText(extracted[0].description, 20)
|
|
2981
|
+
});
|
|
2982
|
+
} else {
|
|
2983
|
+
const firstWords = this.cleanText(bullet).split(/\s+/).slice(0, 3).join(" ");
|
|
2984
|
+
points.push({
|
|
2985
|
+
number: i + 1,
|
|
2986
|
+
title: firstWords || `Step ${i + 1}`,
|
|
2987
|
+
description: this.truncateText(bullet, 20)
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
} else if (section.bullets.length > 0) {
|
|
2992
|
+
for (let i = 0; i < 3; i++) {
|
|
2993
|
+
const bullet = section.bullets[i % section.bullets.length] || "";
|
|
2994
|
+
const firstWords = this.cleanText(bullet).split(/\s+/).slice(0, 3).join(" ");
|
|
2995
|
+
points.push({
|
|
2996
|
+
number: i + 1,
|
|
2997
|
+
title: firstWords || `Step ${i + 1}`,
|
|
2998
|
+
description: this.truncateText(bullet, 20)
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
} else {
|
|
3002
|
+
const sentences = section.content.split(/[.!?]+/).filter((s) => s.trim().length > 0).slice(0, 3);
|
|
3003
|
+
sentences.forEach((s, i) => {
|
|
3004
|
+
const firstWords = this.cleanText(s.trim()).split(/\s+/).slice(0, 3).join(" ");
|
|
3005
|
+
points.push({
|
|
3006
|
+
number: i + 1,
|
|
3007
|
+
title: firstWords || `Step ${i + 1}`,
|
|
3008
|
+
description: this.truncateText(s.trim(), 20)
|
|
3009
|
+
});
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
return {
|
|
3013
|
+
index,
|
|
3014
|
+
type: "three-column",
|
|
3015
|
+
// Uses three-column template
|
|
3016
|
+
data: {
|
|
3017
|
+
title: this.createTitle(section.header, section),
|
|
3018
|
+
columns: points.map((p) => ({
|
|
3019
|
+
title: String(p.number),
|
|
3020
|
+
// Use the sequential number
|
|
3021
|
+
content: p.description,
|
|
3022
|
+
subtitle: p.title
|
|
3023
|
+
// Use extracted title as subtitle
|
|
3024
|
+
}))
|
|
3025
|
+
},
|
|
3026
|
+
classes: ["three-points-slide", "three-column-slide"]
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
2138
3029
|
createTwoColumnSlide(index, section) {
|
|
2139
3030
|
const midpoint = Math.ceil(section.bullets.length / 2);
|
|
2140
3031
|
const leftBullets = section.bullets.slice(0, midpoint);
|
|
@@ -2197,10 +3088,22 @@ var SlideFactory = class {
|
|
|
2197
3088
|
};
|
|
2198
3089
|
}
|
|
2199
3090
|
createBulletSlide(index, section) {
|
|
3091
|
+
logger.debug(`[TRACE] createBulletSlide for "${section.header}"`);
|
|
3092
|
+
logger.debug(`[TRACE] Input bullets: ${section.bullets?.length ?? 0}`);
|
|
2200
3093
|
const maxBullets = this.config.rules.bulletsPerSlide.max;
|
|
2201
3094
|
const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
|
|
2202
|
-
|
|
3095
|
+
logger.debug(`[TRACE] maxBullets: ${maxBullets}, wordsPerBullet: ${wordsPerBullet}`);
|
|
3096
|
+
const slicedBullets = section.bullets.slice(0, maxBullets);
|
|
3097
|
+
logger.debug(`[TRACE] After slice: ${slicedBullets.length} bullets`);
|
|
3098
|
+
const cleanedBullets = slicedBullets.map((b, i) => {
|
|
3099
|
+
const cleaned = this.cleanText(b);
|
|
3100
|
+
const truncated = this.truncateText(cleaned, wordsPerBullet);
|
|
3101
|
+
logger.debug(`[TRACE] Bullet ${i}: "${b.slice(0, 30)}..." \u2192 "${truncated.slice(0, 30)}..."`);
|
|
3102
|
+
return truncated;
|
|
3103
|
+
}).filter((b) => b.length > 0);
|
|
3104
|
+
logger.debug(`[TRACE] Final cleaned bullets: ${cleanedBullets.length}`);
|
|
2203
3105
|
if (cleanedBullets.length === 0 && section.content) {
|
|
3106
|
+
logger.debug(`[TRACE] No bullets, falling back to single-statement`);
|
|
2204
3107
|
return this.createSingleStatementSlide(index, section);
|
|
2205
3108
|
}
|
|
2206
3109
|
return {
|
|
@@ -2214,22 +3117,107 @@ var SlideFactory = class {
|
|
|
2214
3117
|
};
|
|
2215
3118
|
}
|
|
2216
3119
|
createSingleStatementSlide(index, section) {
|
|
3120
|
+
const title = this.createTitle(section.header, section);
|
|
3121
|
+
const titleWords = title.split(/\s+/).length;
|
|
3122
|
+
const maxTotalWords = this.config.rules.wordsPerSlide.max;
|
|
3123
|
+
const maxBodyWords = Math.max(3, maxTotalWords - titleWords);
|
|
2217
3124
|
const bodyContent = section.content || section.bullets.join(" ") || "";
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2220
|
-
|
|
3125
|
+
const craftedBody = this.craftExpertContent(
|
|
3126
|
+
bodyContent,
|
|
3127
|
+
"single_statement",
|
|
3128
|
+
// Tell expert engine what type of slide this is
|
|
3129
|
+
maxBodyWords
|
|
3130
|
+
);
|
|
3131
|
+
const normalizedBody = craftedBody.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3132
|
+
const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3133
|
+
const bodyIsDuplicate = normalizedBody === normalizedTitle || craftedBody.length < 10;
|
|
3134
|
+
if (bodyIsDuplicate) {
|
|
3135
|
+
if (section.bullets.length >= 2 && this.config.mode === "business") {
|
|
3136
|
+
return this.createBulletSlide(index, section);
|
|
3137
|
+
}
|
|
3138
|
+
if (section.metrics.length >= 2) {
|
|
3139
|
+
return this.createMetricsGridSlide(index, section.metrics);
|
|
3140
|
+
}
|
|
3141
|
+
logger.warn(`Skipping slide "${title}" - no distinct body content`);
|
|
3142
|
+
return null;
|
|
2221
3143
|
}
|
|
2222
3144
|
return {
|
|
2223
3145
|
index,
|
|
2224
3146
|
type: "single-statement",
|
|
2225
3147
|
data: {
|
|
2226
|
-
title
|
|
2227
|
-
body:
|
|
2228
|
-
// Fallback to header if no body
|
|
3148
|
+
title,
|
|
3149
|
+
body: craftedBody
|
|
2229
3150
|
},
|
|
2230
3151
|
classes: ["single-statement-slide"]
|
|
2231
3152
|
};
|
|
2232
3153
|
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Create a title impact slide - emphasized statement with key takeaway.
|
|
3156
|
+
* Used for section headers with strong conclusions.
|
|
3157
|
+
*/
|
|
3158
|
+
createTitleImpactSlide(index, section) {
|
|
3159
|
+
const supportingText = section.content || section.bullets.slice(0, 2).join(". ");
|
|
3160
|
+
const truncatedSupport = this.truncateText(supportingText, this.config.defaults.context.maxWords);
|
|
3161
|
+
const data = {
|
|
3162
|
+
title: this.cleanText(section.header)
|
|
3163
|
+
};
|
|
3164
|
+
if (truncatedSupport) {
|
|
3165
|
+
data.body = truncatedSupport;
|
|
3166
|
+
}
|
|
3167
|
+
return {
|
|
3168
|
+
index,
|
|
3169
|
+
type: "single-statement",
|
|
3170
|
+
// Uses single-statement template
|
|
3171
|
+
data,
|
|
3172
|
+
classes: ["title-impact-slide", "single-statement-slide"]
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Create a data insight slide - for presenting key findings with context.
|
|
3177
|
+
* Similar to big-number but works even without a prominent metric.
|
|
3178
|
+
*/
|
|
3179
|
+
createDataInsightSlide(index, section, pattern) {
|
|
3180
|
+
const fullContent = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
|
|
3181
|
+
const bigNumber = this.classifier.extractBigNumber(fullContent);
|
|
3182
|
+
if (bigNumber?.value || pattern.bigNumberValue) {
|
|
3183
|
+
return this.createBigNumberSlide(index, section, pattern);
|
|
3184
|
+
}
|
|
3185
|
+
const keyInsight = section.content || section.bullets[0] || section.header;
|
|
3186
|
+
return {
|
|
3187
|
+
index,
|
|
3188
|
+
type: "single-statement",
|
|
3189
|
+
data: {
|
|
3190
|
+
title: this.createTitle(section.header, section),
|
|
3191
|
+
body: this.truncateText(keyInsight, this.config.rules.wordsPerSlide.max)
|
|
3192
|
+
},
|
|
3193
|
+
classes: ["data-insight-slide", "single-statement-slide"]
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Create a detailed findings slide - bullet points with introductory context.
|
|
3198
|
+
*/
|
|
3199
|
+
createDetailedFindingsSlide(index, section) {
|
|
3200
|
+
const introText = section.content ? this.truncateText(section.content, this.config.defaults.context.maxWords) : "";
|
|
3201
|
+
const maxBullets = this.config.rules.bulletsPerSlide.max;
|
|
3202
|
+
const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
|
|
3203
|
+
const cleanedBullets = section.bullets.slice(0, maxBullets).map((b) => this.truncateText(this.cleanText(b), wordsPerBullet)).filter((b) => b.length > 0);
|
|
3204
|
+
if (cleanedBullets.length === 0) {
|
|
3205
|
+
return this.createSingleStatementSlide(index, section);
|
|
3206
|
+
}
|
|
3207
|
+
const data = {
|
|
3208
|
+
title: this.createTitle(section.header, section),
|
|
3209
|
+
bullets: cleanedBullets
|
|
3210
|
+
};
|
|
3211
|
+
if (introText) {
|
|
3212
|
+
data.body = introText;
|
|
3213
|
+
}
|
|
3214
|
+
return {
|
|
3215
|
+
index,
|
|
3216
|
+
type: "bullet-points",
|
|
3217
|
+
data,
|
|
3218
|
+
classes: ["detailed-findings-slide", "bullet-points-slide"]
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
2233
3221
|
createCodeSlide(index, section) {
|
|
2234
3222
|
const codeMatch = section.content.match(/```[\s\S]*?```|`[^`]+`/);
|
|
2235
3223
|
const code = codeMatch ? codeMatch[0].replace(/```/g, "").trim() : section.content;
|
|
@@ -2254,13 +3242,18 @@ var SlideFactory = class {
|
|
|
2254
3242
|
createCTASlide(index, analysis) {
|
|
2255
3243
|
const ctaDefaults = this.config.defaults.cta;
|
|
2256
3244
|
const ctaText = analysis.sparkline?.callToAdventure || analysis.scqa?.answer || ctaDefaults.fallback;
|
|
3245
|
+
const craftedCTA = this.craftExpertContent(
|
|
3246
|
+
ctaText,
|
|
3247
|
+
"call_to_action",
|
|
3248
|
+
this.config.rules.wordsPerSlide.max
|
|
3249
|
+
);
|
|
2257
3250
|
return {
|
|
2258
3251
|
index,
|
|
2259
3252
|
type: "cta",
|
|
2260
3253
|
data: {
|
|
2261
3254
|
title: ctaDefaults.title,
|
|
2262
3255
|
// FROM KB - not hardcoded 'Next Steps'
|
|
2263
|
-
body:
|
|
3256
|
+
body: craftedCTA,
|
|
2264
3257
|
keyMessage: ctaDefaults.message
|
|
2265
3258
|
// FROM KB - not hardcoded 'Ready to Begin?'
|
|
2266
3259
|
},
|
|
@@ -2373,12 +3366,221 @@ var SlideFactory = class {
|
|
|
2373
3366
|
}
|
|
2374
3367
|
}
|
|
2375
3368
|
// ===========================================================================
|
|
2376
|
-
//
|
|
3369
|
+
// EXPERT-DRIVEN CONTENT CRAFTING (v8.0.0)
|
|
3370
|
+
// Instead of truncating content, we CRAFT it using expert principles
|
|
2377
3371
|
// ===========================================================================
|
|
2378
3372
|
/**
|
|
2379
|
-
*
|
|
2380
|
-
|
|
2381
|
-
|
|
3373
|
+
* Craft content using expert principles - NOT truncation.
|
|
3374
|
+
*
|
|
3375
|
+
* The old approach: "Take 100 words, chop to 15, add ellipsis"
|
|
3376
|
+
* The new approach: "What would Nancy Duarte say is the ONE thing to remember?"
|
|
3377
|
+
*
|
|
3378
|
+
* @param content - The raw content to craft from
|
|
3379
|
+
* @param slideType - The type of slide being created
|
|
3380
|
+
* @param maxWords - Word budget from KB
|
|
3381
|
+
* @returns Expertly crafted content that serves the slide's purpose
|
|
3382
|
+
*/
|
|
3383
|
+
craftExpertContent(content, slideType, maxWords) {
|
|
3384
|
+
if (!content || content.trim().length === 0) return "";
|
|
3385
|
+
const guidance = this.expertEngine.getSlideDesignGuidance(slideType);
|
|
3386
|
+
switch (slideType) {
|
|
3387
|
+
case "title":
|
|
3388
|
+
case "title_impact":
|
|
3389
|
+
return this.extractCoreIdea(content, maxWords);
|
|
3390
|
+
case "star_moment":
|
|
3391
|
+
return this.extractDramaticElement(content, maxWords);
|
|
3392
|
+
case "big_number":
|
|
3393
|
+
case "data_insight":
|
|
3394
|
+
return this.extractDataInsight(content, maxWords);
|
|
3395
|
+
case "single_statement":
|
|
3396
|
+
case "big_idea":
|
|
3397
|
+
return this.extractSingleInsight(content, maxWords);
|
|
3398
|
+
case "call_to_action":
|
|
3399
|
+
case "cta":
|
|
3400
|
+
return this.extractActionableStep(content, maxWords);
|
|
3401
|
+
default:
|
|
3402
|
+
return this.extractCoreIdea(content, maxWords);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Chris Anderson's "One Idea Worth Spreading"
|
|
3407
|
+
* Extract the single most valuable insight from content.
|
|
3408
|
+
*/
|
|
3409
|
+
extractCoreIdea(content, maxWords) {
|
|
3410
|
+
const cleaned = this.cleanText(content);
|
|
3411
|
+
const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
|
|
3412
|
+
let bestSentence = sentences[0] || cleaned;
|
|
3413
|
+
let bestScore = 0;
|
|
3414
|
+
for (const sentence of sentences) {
|
|
3415
|
+
let score = 0;
|
|
3416
|
+
const lowerSentence = sentence.toLowerCase();
|
|
3417
|
+
for (const marker of this.config.insightMarkers) {
|
|
3418
|
+
if (lowerSentence.includes(marker.toLowerCase())) {
|
|
3419
|
+
score += 10;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
if (/\d+%|\$[\d,]+|\d+x|\d+\+/.test(sentence)) {
|
|
3423
|
+
score += 5;
|
|
3424
|
+
}
|
|
3425
|
+
if (/transform|revolutionize|breakthrough|critical|essential|must|never|always/i.test(sentence)) {
|
|
3426
|
+
score += 3;
|
|
3427
|
+
}
|
|
3428
|
+
if (/overview|introduction|agenda|summary|the following/i.test(sentence)) {
|
|
3429
|
+
score -= 10;
|
|
3430
|
+
}
|
|
3431
|
+
if (score > bestScore) {
|
|
3432
|
+
bestScore = score;
|
|
3433
|
+
bestSentence = sentence.trim();
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
const words = bestSentence.split(/\s+/);
|
|
3437
|
+
if (words.length <= maxWords) {
|
|
3438
|
+
return bestSentence;
|
|
3439
|
+
}
|
|
3440
|
+
return this.extractKeyPhrase(bestSentence, maxWords);
|
|
3441
|
+
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Extract the key phrase from a sentence - the part that matters most.
|
|
3444
|
+
*/
|
|
3445
|
+
extractKeyPhrase(sentence, maxWords) {
|
|
3446
|
+
const patterns = [
|
|
3447
|
+
/(?:the key (?:is|to)|critical|essential|must|should)\s+(.+)/i,
|
|
3448
|
+
/(.+?)\s+(?:is critical|is essential|matters most)/i,
|
|
3449
|
+
/^(.+?)\s*[-–—:]\s*.+$/
|
|
3450
|
+
// Before a colon/dash is often the key
|
|
3451
|
+
];
|
|
3452
|
+
for (const pattern of patterns) {
|
|
3453
|
+
const match = sentence.match(pattern);
|
|
3454
|
+
if (match && match[1]) {
|
|
3455
|
+
const extracted = match[1].trim();
|
|
3456
|
+
if (extracted.split(/\s+/).length <= maxWords) {
|
|
3457
|
+
return extracted;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
const words = sentence.split(/\s+/);
|
|
3462
|
+
let result = words.slice(0, maxWords).join(" ");
|
|
3463
|
+
const lastPunctuation = result.lastIndexOf(",");
|
|
3464
|
+
if (lastPunctuation > result.length / 2) {
|
|
3465
|
+
result = result.slice(0, lastPunctuation);
|
|
3466
|
+
}
|
|
3467
|
+
return result;
|
|
3468
|
+
}
|
|
3469
|
+
/**
|
|
3470
|
+
* Nancy Duarte's "STAR Moment" - Something They'll Always Remember
|
|
3471
|
+
* Find the most dramatic, unexpected element.
|
|
3472
|
+
*/
|
|
3473
|
+
extractDramaticElement(content, maxWords) {
|
|
3474
|
+
const cleaned = this.cleanText(content);
|
|
3475
|
+
const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
|
|
3476
|
+
let mostDramatic = sentences[0] || cleaned;
|
|
3477
|
+
let highestDrama = 0;
|
|
3478
|
+
for (const sentence of sentences) {
|
|
3479
|
+
let drama = 0;
|
|
3480
|
+
const numbers = sentence.match(/\d+/g);
|
|
3481
|
+
if (numbers) {
|
|
3482
|
+
for (const num of numbers) {
|
|
3483
|
+
const value = parseInt(num);
|
|
3484
|
+
if (value >= 1e3) drama += 15;
|
|
3485
|
+
else if (value >= 100) drama += 10;
|
|
3486
|
+
else if (value >= 50) drama += 5;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
if (/\d+%/.test(sentence)) drama += 10;
|
|
3490
|
+
if (/\$[\d,]+[MBK]?/i.test(sentence)) drama += 12;
|
|
3491
|
+
if (/never|always|revolutionary|transforming|critical|dramatic|shocking|surprising/i.test(sentence)) {
|
|
3492
|
+
drama += 8;
|
|
3493
|
+
}
|
|
3494
|
+
if (/but|however|yet|instead|versus|vs/i.test(sentence)) {
|
|
3495
|
+
drama += 5;
|
|
3496
|
+
}
|
|
3497
|
+
if (drama > highestDrama) {
|
|
3498
|
+
highestDrama = drama;
|
|
3499
|
+
mostDramatic = sentence.trim();
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
const words = mostDramatic.split(/\s+/);
|
|
3503
|
+
if (words.length <= maxWords) {
|
|
3504
|
+
return mostDramatic;
|
|
3505
|
+
}
|
|
3506
|
+
return this.extractKeyPhrase(mostDramatic, maxWords);
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Edward Tufte / Cole Knaflic - Extract data insight with context
|
|
3510
|
+
*/
|
|
3511
|
+
extractDataInsight(content, maxWords) {
|
|
3512
|
+
const cleaned = this.cleanText(content);
|
|
3513
|
+
const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
|
|
3514
|
+
const datasentences = sentences.filter(
|
|
3515
|
+
(s) => /\d+%|\$[\d,]+|\d+x|\d+ (million|billion|thousand)/i.test(s)
|
|
3516
|
+
);
|
|
3517
|
+
if (datasentences.length > 0 && datasentences[0]) {
|
|
3518
|
+
const best = datasentences[0].trim();
|
|
3519
|
+
const words = best.split(/\s+/);
|
|
3520
|
+
if (words.length <= maxWords) {
|
|
3521
|
+
return best;
|
|
3522
|
+
}
|
|
3523
|
+
return this.extractKeyPhrase(best, maxWords);
|
|
3524
|
+
}
|
|
3525
|
+
return this.extractCoreIdea(content, maxWords);
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* Garr Reynolds - Signal to Noise
|
|
3529
|
+
* Extract the ONE insight, eliminate everything else.
|
|
3530
|
+
*/
|
|
3531
|
+
extractSingleInsight(content, maxWords) {
|
|
3532
|
+
const cleaned = this.cleanText(content);
|
|
3533
|
+
const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
|
|
3534
|
+
const conclusionPatterns = [
|
|
3535
|
+
/(?:therefore|thus|hence|consequently|as a result|this means|the bottom line|in conclusion)/i,
|
|
3536
|
+
/(?:we (?:must|should|need to)|it's (?:critical|essential|important) to)/i,
|
|
3537
|
+
/(?:the key (?:is|takeaway)|what this means)/i
|
|
3538
|
+
];
|
|
3539
|
+
for (const sentence of sentences) {
|
|
3540
|
+
for (const pattern of conclusionPatterns) {
|
|
3541
|
+
if (pattern.test(sentence)) {
|
|
3542
|
+
const words = sentence.trim().split(/\s+/);
|
|
3543
|
+
if (words.length <= maxWords) {
|
|
3544
|
+
return sentence.trim();
|
|
3545
|
+
}
|
|
3546
|
+
return this.extractKeyPhrase(sentence, maxWords);
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
return this.extractCoreIdea(content, maxWords);
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* Chris Anderson - Gift Giving
|
|
3554
|
+
* What can they DO tomorrow?
|
|
3555
|
+
*/
|
|
3556
|
+
extractActionableStep(content, maxWords) {
|
|
3557
|
+
const cleaned = this.cleanText(content);
|
|
3558
|
+
const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
|
|
3559
|
+
const actionPatterns = [
|
|
3560
|
+
/(?:start|begin|try|implement|apply|use|adopt|build|create|develop)/i,
|
|
3561
|
+
/(?:step \d|first,|next,|then,|finally)/i,
|
|
3562
|
+
/(?:you (?:can|should|must|need to))/i
|
|
3563
|
+
];
|
|
3564
|
+
for (const sentence of sentences) {
|
|
3565
|
+
for (const pattern of actionPatterns) {
|
|
3566
|
+
if (pattern.test(sentence)) {
|
|
3567
|
+
const words = sentence.trim().split(/\s+/);
|
|
3568
|
+
if (words.length <= maxWords) {
|
|
3569
|
+
return sentence.trim();
|
|
3570
|
+
}
|
|
3571
|
+
return this.extractKeyPhrase(sentence, maxWords);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
return this.extractCoreIdea(content, maxWords);
|
|
3576
|
+
}
|
|
3577
|
+
// ===========================================================================
|
|
3578
|
+
// HELPER METHODS - All use KB configuration
|
|
3579
|
+
// ===========================================================================
|
|
3580
|
+
/**
|
|
3581
|
+
* Create a title for a slide - uses action titles for business mode per KB
|
|
3582
|
+
*/
|
|
3583
|
+
createTitle(header, section) {
|
|
2382
3584
|
const cleanHeader = this.cleanText(header);
|
|
2383
3585
|
if (this.usedTitles.has(cleanHeader.toLowerCase())) {
|
|
2384
3586
|
return `${cleanHeader} (continued)`;
|
|
@@ -4021,949 +5223,837 @@ var QAEngine = class {
|
|
|
4021
5223
|
}
|
|
4022
5224
|
};
|
|
4023
5225
|
|
|
4024
|
-
// src/qa/
|
|
4025
|
-
var
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
};
|
|
4034
|
-
var SevenDimensionScorer = class {
|
|
4035
|
-
kb;
|
|
4036
|
-
mode;
|
|
4037
|
-
presentationType;
|
|
4038
|
-
constructor(mode, presentationType) {
|
|
4039
|
-
this.mode = mode;
|
|
4040
|
-
this.presentationType = presentationType;
|
|
4041
|
-
}
|
|
4042
|
-
/**
|
|
4043
|
-
* Score a presentation across all 7 dimensions.
|
|
4044
|
-
*/
|
|
4045
|
-
async score(slides, html, threshold = 95) {
|
|
4046
|
-
this.kb = await getKnowledgeGateway();
|
|
4047
|
-
const layout = await this.scoreLayout(slides, html);
|
|
4048
|
-
const contrast = await this.scoreContrast(html);
|
|
4049
|
-
const graphics = await this.scoreGraphics(slides);
|
|
4050
|
-
const content = await this.scoreContent(slides);
|
|
4051
|
-
const clarity = await this.scoreClarity(slides);
|
|
4052
|
-
const effectiveness = await this.scoreEffectiveness(slides);
|
|
4053
|
-
const consistency = await this.scoreConsistency(slides, html);
|
|
4054
|
-
const overallScore = Math.round(
|
|
4055
|
-
layout.score * DIMENSION_WEIGHTS.layout + contrast.score * DIMENSION_WEIGHTS.contrast + graphics.score * DIMENSION_WEIGHTS.graphics + content.score * DIMENSION_WEIGHTS.content + clarity.score * DIMENSION_WEIGHTS.clarity + effectiveness.score * DIMENSION_WEIGHTS.effectiveness + consistency.score * DIMENSION_WEIGHTS.consistency
|
|
4056
|
-
);
|
|
4057
|
-
const issues = [
|
|
4058
|
-
...layout.issues,
|
|
4059
|
-
...contrast.issues,
|
|
4060
|
-
...graphics.issues,
|
|
4061
|
-
...content.issues,
|
|
4062
|
-
...clarity.issues,
|
|
4063
|
-
...effectiveness.issues,
|
|
4064
|
-
...consistency.issues
|
|
4065
|
-
];
|
|
4066
|
-
return {
|
|
4067
|
-
overallScore,
|
|
4068
|
-
dimensions: {
|
|
4069
|
-
layout,
|
|
4070
|
-
contrast,
|
|
4071
|
-
graphics,
|
|
4072
|
-
content,
|
|
4073
|
-
clarity,
|
|
4074
|
-
effectiveness,
|
|
4075
|
-
consistency
|
|
4076
|
-
},
|
|
4077
|
-
issues,
|
|
4078
|
-
passed: overallScore >= threshold,
|
|
4079
|
-
threshold
|
|
4080
|
-
};
|
|
5226
|
+
// src/qa/VisualQualityEvaluator.ts
|
|
5227
|
+
var path = __toESM(require("path"));
|
|
5228
|
+
var fs = __toESM(require("fs"));
|
|
5229
|
+
var VisualQualityEvaluator = class {
|
|
5230
|
+
browser = null;
|
|
5231
|
+
page = null;
|
|
5232
|
+
screenshotDir;
|
|
5233
|
+
constructor(screenshotDir = "/tmp/presentation-qa") {
|
|
5234
|
+
this.screenshotDir = screenshotDir;
|
|
4081
5235
|
}
|
|
4082
5236
|
/**
|
|
4083
|
-
*
|
|
5237
|
+
* Evaluate a presentation's visual quality.
|
|
5238
|
+
* This opens the HTML in a real browser and evaluates each slide.
|
|
4084
5239
|
*/
|
|
4085
|
-
async
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
let checks = 0;
|
|
4089
|
-
const minWhitespace = this.mode === "keynote" ? 0.45 : 0.35;
|
|
4090
|
-
const maxWhitespace = 0.6;
|
|
4091
|
-
const slideSections = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
|
|
4092
|
-
for (let i = 0; i < slideSections.length; i++) {
|
|
4093
|
-
const section = slideSections[i];
|
|
4094
|
-
if (!section) continue;
|
|
4095
|
-
const textContent = section.replace(/<[^>]+>/g, "").trim();
|
|
4096
|
-
const totalArea = 1920 * 1080;
|
|
4097
|
-
const estimatedTextArea = textContent.length * 100;
|
|
4098
|
-
const whitespaceRatio = 1 - Math.min(estimatedTextArea / totalArea, 1);
|
|
4099
|
-
if (whitespaceRatio < minWhitespace) {
|
|
4100
|
-
issues.push({
|
|
4101
|
-
slideIndex: i,
|
|
4102
|
-
dimension: "layout",
|
|
4103
|
-
severity: "error",
|
|
4104
|
-
message: `Slide ${i + 1}: Insufficient whitespace (${Math.round(whitespaceRatio * 100)}% < ${Math.round(minWhitespace * 100)}%)`,
|
|
4105
|
-
currentValue: whitespaceRatio,
|
|
4106
|
-
expectedValue: minWhitespace,
|
|
4107
|
-
autoFixable: true,
|
|
4108
|
-
fixSuggestion: "Reduce content or increase margins"
|
|
4109
|
-
});
|
|
4110
|
-
totalScore += 50;
|
|
4111
|
-
} else if (whitespaceRatio > maxWhitespace) {
|
|
4112
|
-
issues.push({
|
|
4113
|
-
slideIndex: i,
|
|
4114
|
-
dimension: "layout",
|
|
4115
|
-
severity: "warning",
|
|
4116
|
-
message: `Slide ${i + 1}: Too much whitespace (${Math.round(whitespaceRatio * 100)}% > ${Math.round(maxWhitespace * 100)}%)`,
|
|
4117
|
-
currentValue: whitespaceRatio,
|
|
4118
|
-
expectedValue: maxWhitespace,
|
|
4119
|
-
autoFixable: false,
|
|
4120
|
-
fixSuggestion: "Add more content or reduce margins"
|
|
4121
|
-
});
|
|
4122
|
-
totalScore += 80;
|
|
4123
|
-
} else {
|
|
4124
|
-
totalScore += 100;
|
|
4125
|
-
}
|
|
4126
|
-
checks++;
|
|
4127
|
-
}
|
|
4128
|
-
for (let i = 0; i < slides.length; i++) {
|
|
4129
|
-
const slide = slides[i];
|
|
4130
|
-
if (!slide) continue;
|
|
4131
|
-
if (!["thank-you", "section-divider"].includes(slide.type)) {
|
|
4132
|
-
if (!slide.data.title || slide.data.title.trim().length === 0) {
|
|
4133
|
-
issues.push({
|
|
4134
|
-
slideIndex: i,
|
|
4135
|
-
dimension: "layout",
|
|
4136
|
-
severity: "warning",
|
|
4137
|
-
message: `Slide ${i + 1}: Missing title`,
|
|
4138
|
-
autoFixable: false,
|
|
4139
|
-
fixSuggestion: "Add a clear slide title"
|
|
4140
|
-
});
|
|
4141
|
-
totalScore += 70;
|
|
4142
|
-
} else {
|
|
4143
|
-
totalScore += 100;
|
|
4144
|
-
}
|
|
4145
|
-
checks++;
|
|
4146
|
-
}
|
|
5240
|
+
async evaluate(htmlPath) {
|
|
5241
|
+
if (!fs.existsSync(this.screenshotDir)) {
|
|
5242
|
+
fs.mkdirSync(this.screenshotDir, { recursive: true });
|
|
4147
5243
|
}
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
5244
|
+
try {
|
|
5245
|
+
await this.launchBrowser();
|
|
5246
|
+
const absolutePath = path.resolve(htmlPath);
|
|
5247
|
+
await this.page.goto(`file://${absolutePath}`, { waitUntil: "networkidle" });
|
|
5248
|
+
await this.page.waitForSelector(".reveal", { timeout: 5e3 });
|
|
5249
|
+
await this.page.waitForTimeout(1e3);
|
|
5250
|
+
const slideCount = await this.getSlideCount();
|
|
5251
|
+
console.log(`Evaluating ${slideCount} slides...`);
|
|
5252
|
+
const slideScores = [];
|
|
5253
|
+
for (let i = 0; i < slideCount; i++) {
|
|
5254
|
+
const score = await this.evaluateSlide(i);
|
|
5255
|
+
slideScores.push(score);
|
|
4157
5256
|
}
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
});
|
|
4182
|
-
score -= 20;
|
|
4183
|
-
}
|
|
4184
|
-
if (!hasGoodContrast && !hasDarkBackground) {
|
|
4185
|
-
issues.push({
|
|
4186
|
-
slideIndex: -1,
|
|
4187
|
-
dimension: "contrast",
|
|
4188
|
-
severity: "warning",
|
|
4189
|
-
message: "Could not verify WCAG-compliant contrast ratios",
|
|
4190
|
-
autoFixable: false,
|
|
4191
|
-
fixSuggestion: "Ensure text has 4.5:1 contrast ratio against background"
|
|
4192
|
-
});
|
|
4193
|
-
score -= 10;
|
|
4194
|
-
}
|
|
4195
|
-
const hasSmallFont = html.match(/font-size:\s*(1[0-3]|[0-9])px/i) !== null;
|
|
4196
|
-
if (hasSmallFont) {
|
|
4197
|
-
issues.push({
|
|
4198
|
-
slideIndex: -1,
|
|
4199
|
-
dimension: "contrast",
|
|
4200
|
-
severity: "error",
|
|
4201
|
-
message: "Font size too small for presentation (< 14px)",
|
|
4202
|
-
currentValue: "Small font",
|
|
4203
|
-
expectedValue: "18px minimum for body text",
|
|
4204
|
-
autoFixable: true,
|
|
4205
|
-
fixSuggestion: "Increase font size to minimum 18px"
|
|
4206
|
-
});
|
|
4207
|
-
score -= 15;
|
|
5257
|
+
const narrativeFlow = this.evaluateNarrativeFlow(slideScores);
|
|
5258
|
+
const visualConsistency = this.evaluateVisualConsistency(slideScores);
|
|
5259
|
+
const contentQuality = this.evaluateContentQuality(slideScores);
|
|
5260
|
+
const executiveReadiness = this.evaluateExecutiveReadiness(slideScores);
|
|
5261
|
+
const overallScore = Math.round(
|
|
5262
|
+
narrativeFlow.score + visualConsistency.score + contentQuality.score + executiveReadiness.score
|
|
5263
|
+
);
|
|
5264
|
+
const verdict = this.determineVerdict(overallScore);
|
|
5265
|
+
const { topIssues, topStrengths } = this.extractTopIssuesAndStrengths(slideScores);
|
|
5266
|
+
return {
|
|
5267
|
+
overallScore,
|
|
5268
|
+
narrativeFlow,
|
|
5269
|
+
visualConsistency,
|
|
5270
|
+
contentQuality,
|
|
5271
|
+
executiveReadiness,
|
|
5272
|
+
slideScores,
|
|
5273
|
+
verdict,
|
|
5274
|
+
verdictExplanation: this.explainVerdict(verdict, overallScore),
|
|
5275
|
+
topIssues,
|
|
5276
|
+
topStrengths
|
|
5277
|
+
};
|
|
5278
|
+
} finally {
|
|
5279
|
+
await this.closeBrowser();
|
|
4208
5280
|
}
|
|
4209
|
-
return {
|
|
4210
|
-
name: "Contrast",
|
|
4211
|
-
score: Math.max(0, score),
|
|
4212
|
-
weight: DIMENSION_WEIGHTS.contrast,
|
|
4213
|
-
issues,
|
|
4214
|
-
details: {
|
|
4215
|
-
wcagLevel: "AA",
|
|
4216
|
-
minContrastRatio,
|
|
4217
|
-
colorDefinitions: colorMatches.length,
|
|
4218
|
-
backgroundDefinitions: bgColorMatches.length
|
|
4219
|
-
}
|
|
4220
|
-
};
|
|
4221
5281
|
}
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
async
|
|
4226
|
-
const
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
} else {
|
|
4239
|
-
issues.push({
|
|
4240
|
-
slideIndex: i,
|
|
4241
|
-
dimension: "graphics",
|
|
4242
|
-
severity: "warning",
|
|
4243
|
-
message: `Slide ${i + 1} (${slide.type}): Missing expected image`,
|
|
4244
|
-
autoFixable: false,
|
|
4245
|
-
fixSuggestion: "Add a relevant image or change slide type"
|
|
4246
|
-
});
|
|
4247
|
-
score -= 5;
|
|
4248
|
-
}
|
|
4249
|
-
}
|
|
5282
|
+
// ===========================================================================
|
|
5283
|
+
// BROWSER MANAGEMENT
|
|
5284
|
+
// ===========================================================================
|
|
5285
|
+
async launchBrowser() {
|
|
5286
|
+
const { chromium: chromium2 } = await import("playwright");
|
|
5287
|
+
this.browser = await chromium2.launch({ headless: true });
|
|
5288
|
+
const context = await this.browser.newContext({
|
|
5289
|
+
viewport: { width: 1920, height: 1080 }
|
|
5290
|
+
});
|
|
5291
|
+
this.page = await context.newPage();
|
|
5292
|
+
}
|
|
5293
|
+
async closeBrowser() {
|
|
5294
|
+
if (this.browser) {
|
|
5295
|
+
await this.browser.close();
|
|
5296
|
+
this.browser = null;
|
|
5297
|
+
this.page = null;
|
|
4250
5298
|
}
|
|
4251
|
-
return {
|
|
4252
|
-
name: "Graphics",
|
|
4253
|
-
score: Math.max(0, score),
|
|
4254
|
-
weight: DIMENSION_WEIGHTS.graphics,
|
|
4255
|
-
issues,
|
|
4256
|
-
details: {
|
|
4257
|
-
slidesWithImages,
|
|
4258
|
-
expectedImageSlides: totalImageSlides,
|
|
4259
|
-
imageRatio: totalImageSlides > 0 ? slidesWithImages / totalImageSlides : 1
|
|
4260
|
-
}
|
|
4261
|
-
};
|
|
4262
5299
|
}
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
const maxWords = wordLimits[slideType] ?? defaultMaxWords;
|
|
4282
|
-
const minWords = defaultMinWords;
|
|
4283
|
-
if (wordCount > maxWords) {
|
|
4284
|
-
const severity = wordCount > maxWords * 1.5 ? "error" : "warning";
|
|
4285
|
-
issues.push({
|
|
4286
|
-
slideIndex: i,
|
|
4287
|
-
dimension: "content",
|
|
4288
|
-
severity,
|
|
4289
|
-
message: `Slide ${i + 1}: Word count ${wordCount} exceeds limit of ${maxWords} for ${this.mode} mode`,
|
|
4290
|
-
currentValue: wordCount,
|
|
4291
|
-
expectedValue: maxWords,
|
|
4292
|
-
autoFixable: true,
|
|
4293
|
-
fixSuggestion: "Condense text to key points only"
|
|
4294
|
-
});
|
|
4295
|
-
totalScore += severity === "error" ? 40 : 70;
|
|
4296
|
-
} else if (wordCount < minWords && !["title", "section-divider", "thank-you"].includes(slide.type)) {
|
|
4297
|
-
issues.push({
|
|
4298
|
-
slideIndex: i,
|
|
4299
|
-
dimension: "content",
|
|
4300
|
-
severity: "warning",
|
|
4301
|
-
message: `Slide ${i + 1}: Word count ${wordCount} may be too sparse (min: ${minWords})`,
|
|
4302
|
-
currentValue: wordCount,
|
|
4303
|
-
expectedValue: minWords,
|
|
4304
|
-
autoFixable: false,
|
|
4305
|
-
fixSuggestion: "Add supporting content"
|
|
4306
|
-
});
|
|
4307
|
-
totalScore += 80;
|
|
4308
|
-
} else {
|
|
4309
|
-
totalScore += 100;
|
|
5300
|
+
async getSlideCount() {
|
|
5301
|
+
return await this.page.evaluate(() => {
|
|
5302
|
+
const slides = document.querySelectorAll(".slides > section");
|
|
5303
|
+
return slides.length;
|
|
5304
|
+
});
|
|
5305
|
+
}
|
|
5306
|
+
// ===========================================================================
|
|
5307
|
+
// SLIDE-LEVEL EVALUATION
|
|
5308
|
+
// ===========================================================================
|
|
5309
|
+
async evaluateSlide(slideIndex) {
|
|
5310
|
+
await this.page.evaluate((index) => {
|
|
5311
|
+
window.Reveal?.slide(index, 0);
|
|
5312
|
+
}, slideIndex);
|
|
5313
|
+
await this.page.waitForTimeout(600);
|
|
5314
|
+
await this.page.evaluate(() => {
|
|
5315
|
+
const currentSlide = document.querySelector(".present");
|
|
5316
|
+
if (currentSlide) {
|
|
5317
|
+
currentSlide.getAnimations().forEach((anim) => anim.finish());
|
|
4310
5318
|
}
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
5319
|
+
});
|
|
5320
|
+
await this.page.waitForTimeout(100);
|
|
5321
|
+
const screenshotPath = path.join(this.screenshotDir, `slide-${slideIndex}.png`);
|
|
5322
|
+
await this.page.screenshot({ path: screenshotPath, fullPage: false });
|
|
5323
|
+
const slideData = await this.page.evaluate(() => {
|
|
5324
|
+
const currentSlide = document.querySelector(".present");
|
|
5325
|
+
if (!currentSlide) return null;
|
|
5326
|
+
const title = currentSlide.querySelector("h1, h2, .title")?.textContent?.trim() || "";
|
|
5327
|
+
const body = currentSlide.querySelector(".body, p:not(.subtitle)")?.textContent?.trim() || "";
|
|
5328
|
+
const bullets = Array.from(currentSlide.querySelectorAll("li")).map((li) => li.textContent?.trim() || "");
|
|
5329
|
+
const hasSteps = !!currentSlide.querySelector(".steps, .process-steps, .timeline");
|
|
5330
|
+
const steps = Array.from(currentSlide.querySelectorAll(".step, .process-step, .timeline-item")).map(
|
|
5331
|
+
(s) => s.textContent?.trim() || ""
|
|
5332
|
+
);
|
|
5333
|
+
const hasMetrics = !!currentSlide.querySelector(".metrics, .metric");
|
|
5334
|
+
const hasImage = !!currentSlide.querySelector("img");
|
|
5335
|
+
const hasChart = !!currentSlide.querySelector(".chart, svg, canvas");
|
|
5336
|
+
const classList = Array.from(currentSlide.classList);
|
|
5337
|
+
const backgroundColor = window.getComputedStyle(currentSlide).backgroundColor;
|
|
5338
|
+
const titleEl = currentSlide.querySelector("h1, h2, .title");
|
|
5339
|
+
const titleStyles = titleEl ? window.getComputedStyle(titleEl) : null;
|
|
5340
|
+
const truncatedElements = [];
|
|
5341
|
+
const contentElements = currentSlide.querySelectorAll("h1, h2, h3, p, span, li, .body, .step-desc, .step-title, .timeline-content");
|
|
5342
|
+
contentElements.forEach((el, idx) => {
|
|
5343
|
+
const styles = window.getComputedStyle(el);
|
|
5344
|
+
const text = el.textContent?.trim() || "";
|
|
5345
|
+
if (text.length < 15) return;
|
|
5346
|
+
const isLayoutContainer = el.classList?.contains("slide-content") || el.classList?.contains("steps") || el.classList?.contains("timeline") || el.classList?.contains("process-steps");
|
|
5347
|
+
if (isLayoutContainer) return;
|
|
5348
|
+
if (styles.textOverflow === "ellipsis") {
|
|
5349
|
+
if (el.scrollWidth > el.clientWidth + 5) {
|
|
5350
|
+
truncatedElements.push(`Element ${idx}: "${text.substring(0, 30)}..." is truncated horizontally`);
|
|
5351
|
+
}
|
|
4329
5352
|
}
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
5353
|
+
const scrollHeight = el.scrollHeight;
|
|
5354
|
+
const clientHeight = el.clientHeight;
|
|
5355
|
+
if (scrollHeight > clientHeight + 20) {
|
|
5356
|
+
const overflow = styles.overflow || styles.overflowY;
|
|
5357
|
+
if (overflow === "hidden" || overflow === "clip") {
|
|
5358
|
+
truncatedElements.push(`Element ${idx}: "${text.substring(0, 30)}..." is truncated vertically`);
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
});
|
|
5362
|
+
const allVisibleText = Array.from(currentSlide.querySelectorAll("*")).map((el) => el.textContent?.trim() || "").join(" ").trim();
|
|
5363
|
+
const isEmptySlide = allVisibleText.length < 10 && !hasImage && !hasChart;
|
|
5364
|
+
const hasOnlyTitle = title.length > 0 && body.length === 0 && bullets.length === 0 && steps.length === 0 && !hasSteps && !hasMetrics && !hasImage && !hasChart;
|
|
5365
|
+
const titleLower = title.toLowerCase();
|
|
5366
|
+
const bodyLower = body.toLowerCase();
|
|
5367
|
+
const isRedundant = titleLower.length > 10 && bodyLower.length > 10 && (titleLower.includes(bodyLower) || bodyLower.includes(titleLower));
|
|
5368
|
+
const contrastIssues = [];
|
|
5369
|
+
const parseRGB = (color) => {
|
|
5370
|
+
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
5371
|
+
if (match && match[1] !== void 0 && match[2] !== void 0 && match[3] !== void 0) {
|
|
5372
|
+
return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
|
|
5373
|
+
}
|
|
5374
|
+
return null;
|
|
5375
|
+
};
|
|
5376
|
+
const getLuminance = (rgb) => {
|
|
5377
|
+
const values = [rgb.r, rgb.g, rgb.b].map((c) => {
|
|
5378
|
+
c = c / 255;
|
|
5379
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
5380
|
+
});
|
|
5381
|
+
return 0.2126 * (values[0] ?? 0) + 0.7152 * (values[1] ?? 0) + 0.0722 * (values[2] ?? 0);
|
|
5382
|
+
};
|
|
5383
|
+
const getContrastRatio = (fg, bg) => {
|
|
5384
|
+
const l1 = getLuminance(fg);
|
|
5385
|
+
const l2 = getLuminance(bg);
|
|
5386
|
+
const lighter = Math.max(l1, l2);
|
|
5387
|
+
const darker = Math.min(l1, l2);
|
|
5388
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
5389
|
+
};
|
|
5390
|
+
const bgRGB = parseRGB(backgroundColor);
|
|
5391
|
+
if (titleEl && bgRGB) {
|
|
5392
|
+
const titleRGB = parseRGB(titleStyles?.color || "");
|
|
5393
|
+
if (titleRGB) {
|
|
5394
|
+
const contrast = getContrastRatio(titleRGB, bgRGB);
|
|
5395
|
+
if (contrast < 4.5) {
|
|
5396
|
+
contrastIssues.push(`Title contrast ${contrast.toFixed(1)}:1 (need 4.5:1)`);
|
|
5397
|
+
}
|
|
4374
5398
|
}
|
|
4375
|
-
checks++;
|
|
4376
5399
|
}
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
const
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
currentValue: titleWords,
|
|
4387
|
-
expectedValue: "2-8 words",
|
|
4388
|
-
autoFixable: true,
|
|
4389
|
-
fixSuggestion: "Use action-oriented, concise title"
|
|
4390
|
-
});
|
|
4391
|
-
totalScore += 75;
|
|
4392
|
-
} else {
|
|
4393
|
-
totalScore += 100;
|
|
5400
|
+
const bodyEl = currentSlide.querySelector(".body, p:not(.subtitle)");
|
|
5401
|
+
if (bodyEl && bgRGB) {
|
|
5402
|
+
const bodyStyles2 = window.getComputedStyle(bodyEl);
|
|
5403
|
+
const bodyRGB = parseRGB(bodyStyles2.color);
|
|
5404
|
+
if (bodyRGB) {
|
|
5405
|
+
const contrast = getContrastRatio(bodyRGB, bgRGB);
|
|
5406
|
+
if (contrast < 4.5) {
|
|
5407
|
+
contrastIssues.push(`Body contrast ${contrast.toFixed(1)}:1 (need 4.5:1)`);
|
|
5408
|
+
}
|
|
4394
5409
|
}
|
|
4395
|
-
checks++;
|
|
4396
5410
|
}
|
|
4397
|
-
const
|
|
4398
|
-
const
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
5411
|
+
const hasContrastIssues = contrastIssues.length > 0;
|
|
5412
|
+
const layoutIssues = [];
|
|
5413
|
+
const slideRect = currentSlide.getBoundingClientRect();
|
|
5414
|
+
const layoutElements = currentSlide.querySelectorAll("h1, h2, h3, p, ul, ol, .number, .metric, img, canvas, svg");
|
|
5415
|
+
let topHeavy = 0;
|
|
5416
|
+
let bottomHeavy = 0;
|
|
5417
|
+
let leftHeavy = 0;
|
|
5418
|
+
let rightHeavy = 0;
|
|
5419
|
+
let centerY = slideRect.height / 2;
|
|
5420
|
+
let centerX = slideRect.width / 2;
|
|
5421
|
+
layoutElements.forEach((el) => {
|
|
5422
|
+
const rect = el.getBoundingClientRect();
|
|
5423
|
+
const elCenterY = rect.top + rect.height / 2 - slideRect.top;
|
|
5424
|
+
const elCenterX = rect.left + rect.width / 2 - slideRect.left;
|
|
5425
|
+
if (elCenterY < centerY) topHeavy++;
|
|
5426
|
+
else bottomHeavy++;
|
|
5427
|
+
if (elCenterX < centerX) leftHeavy++;
|
|
5428
|
+
else rightHeavy++;
|
|
5429
|
+
});
|
|
5430
|
+
const verticalBalance = Math.abs(topHeavy - bottomHeavy) / Math.max(1, topHeavy + bottomHeavy);
|
|
5431
|
+
const horizontalBalance = Math.abs(leftHeavy - rightHeavy) / Math.max(1, leftHeavy + rightHeavy);
|
|
5432
|
+
if (verticalBalance > 0.6 && contentElements.length > 2) {
|
|
5433
|
+
layoutIssues.push(`Vertical imbalance: ${topHeavy} top vs ${bottomHeavy} bottom`);
|
|
4413
5434
|
}
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
const score = checks > 0 ? Math.round(totalScore / checks) : 100;
|
|
4417
|
-
return {
|
|
4418
|
-
name: "Clarity",
|
|
4419
|
-
score,
|
|
4420
|
-
weight: DIMENSION_WEIGHTS.clarity,
|
|
4421
|
-
issues,
|
|
4422
|
-
details: {
|
|
4423
|
-
slidesAnalyzed: slides.length,
|
|
4424
|
-
mode: this.mode
|
|
5435
|
+
if (horizontalBalance > 0.6 && contentElements.length > 2) {
|
|
5436
|
+
layoutIssues.push(`Horizontal imbalance: ${leftHeavy} left vs ${rightHeavy} right`);
|
|
4425
5437
|
}
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
let score = 100;
|
|
4434
|
-
if (slides.length >= 3) {
|
|
4435
|
-
const firstSlide = slides[0];
|
|
4436
|
-
const lastSlide = slides[slides.length - 1];
|
|
4437
|
-
if (firstSlide && !["title", "hero"].includes(firstSlide.type)) {
|
|
4438
|
-
issues.push({
|
|
4439
|
-
slideIndex: 0,
|
|
4440
|
-
dimension: "effectiveness",
|
|
4441
|
-
severity: "warning",
|
|
4442
|
-
message: "Presentation should start with a title or hero slide",
|
|
4443
|
-
currentValue: firstSlide.type,
|
|
4444
|
-
expectedValue: "title or hero",
|
|
4445
|
-
autoFixable: false,
|
|
4446
|
-
fixSuggestion: "Add a compelling opening slide"
|
|
4447
|
-
});
|
|
4448
|
-
score -= 10;
|
|
5438
|
+
const hasLayoutIssues = layoutIssues.length > 0;
|
|
5439
|
+
const typographyIssues = [];
|
|
5440
|
+
const titleSize = parseFloat(titleStyles?.fontSize || "0");
|
|
5441
|
+
const bodyStyles = bodyEl ? window.getComputedStyle(bodyEl) : null;
|
|
5442
|
+
const bodySize = parseFloat(bodyStyles?.fontSize || "0");
|
|
5443
|
+
if (titleSize > 0 && bodySize > 0 && titleSize <= bodySize) {
|
|
5444
|
+
typographyIssues.push("Title not larger than body - hierarchy broken");
|
|
4449
5445
|
}
|
|
4450
|
-
if (
|
|
4451
|
-
|
|
4452
|
-
slideIndex: slides.length - 1,
|
|
4453
|
-
dimension: "effectiveness",
|
|
4454
|
-
severity: "warning",
|
|
4455
|
-
message: "Presentation should end with a closing or CTA slide",
|
|
4456
|
-
currentValue: lastSlide.type,
|
|
4457
|
-
expectedValue: "thank-you, cta, or closing",
|
|
4458
|
-
autoFixable: false,
|
|
4459
|
-
fixSuggestion: "Add a clear call-to-action or closing"
|
|
4460
|
-
});
|
|
4461
|
-
score -= 10;
|
|
5446
|
+
if (titleSize > 0 && bodySize > 0 && titleSize < bodySize * 1.3) {
|
|
5447
|
+
typographyIssues.push("Title not prominent enough vs body");
|
|
4462
5448
|
}
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
dimension: "effectiveness",
|
|
4469
|
-
severity: "info",
|
|
4470
|
-
message: `Consider using Rule of Three: ${keyMessages.length} key messages found`,
|
|
4471
|
-
currentValue: keyMessages.length,
|
|
4472
|
-
expectedValue: 3,
|
|
4473
|
-
autoFixable: false,
|
|
4474
|
-
fixSuggestion: "Group messages into 3 main themes"
|
|
4475
|
-
});
|
|
4476
|
-
score -= 5;
|
|
4477
|
-
}
|
|
4478
|
-
const hasScqaElements = slides.some(
|
|
4479
|
-
(s) => s.data.scqaType || s.data.title && s.data.title.toLowerCase().includes("challenge") || s.data.title && s.data.title.toLowerCase().includes("solution") || s.data.title && s.data.title.toLowerCase().includes("recommendation")
|
|
4480
|
-
);
|
|
4481
|
-
if (!hasScqaElements && this.presentationType === "consulting_deck") {
|
|
4482
|
-
issues.push({
|
|
4483
|
-
slideIndex: -1,
|
|
4484
|
-
dimension: "effectiveness",
|
|
4485
|
-
severity: "warning",
|
|
4486
|
-
message: "Consulting deck should follow SCQA structure (Situation, Complication, Question, Answer)",
|
|
4487
|
-
autoFixable: false,
|
|
4488
|
-
fixSuggestion: "Organize content using Barbara Minto Pyramid Principle"
|
|
4489
|
-
});
|
|
4490
|
-
score -= 10;
|
|
4491
|
-
}
|
|
4492
|
-
const firstSlideType = slides[0]?.type;
|
|
4493
|
-
const lastSlideType = slides[slides.length - 1]?.type;
|
|
4494
|
-
return {
|
|
4495
|
-
name: "Effectiveness",
|
|
4496
|
-
score: Math.max(0, score),
|
|
4497
|
-
weight: DIMENSION_WEIGHTS.effectiveness,
|
|
4498
|
-
issues,
|
|
4499
|
-
details: {
|
|
4500
|
-
presentationType: this.presentationType,
|
|
4501
|
-
slideCount: slides.length,
|
|
4502
|
-
hasOpeningSlide: firstSlideType ? ["title", "hero"].includes(firstSlideType) : false,
|
|
4503
|
-
hasClosingSlide: lastSlideType ? ["thank-you", "cta", "closing"].includes(lastSlideType) : false
|
|
5449
|
+
const hasTypographyIssues = typographyIssues.length > 0;
|
|
5450
|
+
const completenessIssues = [];
|
|
5451
|
+
const emptySteps = currentSlide.querySelectorAll(".steps:empty, .timeline:empty, .process-steps:empty, .steps > :empty");
|
|
5452
|
+
if (emptySteps.length > 0) {
|
|
5453
|
+
completenessIssues.push(`${emptySteps.length} empty step/timeline container(s) - content missing`);
|
|
4504
5454
|
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
/**
|
|
4508
|
-
* Score consistency dimension (style uniformity, design coherence)
|
|
4509
|
-
*/
|
|
4510
|
-
async scoreConsistency(slides, html) {
|
|
4511
|
-
const issues = [];
|
|
4512
|
-
let score = 100;
|
|
4513
|
-
const hasCssVariables = html.includes("var(--") || html.includes(":root");
|
|
4514
|
-
if (!hasCssVariables) {
|
|
4515
|
-
issues.push({
|
|
4516
|
-
slideIndex: -1,
|
|
4517
|
-
dimension: "consistency",
|
|
4518
|
-
severity: "warning",
|
|
4519
|
-
message: "Presentation lacks CSS variables for consistent styling",
|
|
4520
|
-
autoFixable: true,
|
|
4521
|
-
fixSuggestion: "Use CSS variables for colors, fonts, and spacing"
|
|
4522
|
-
});
|
|
4523
|
-
score -= 10;
|
|
4524
|
-
}
|
|
4525
|
-
const titlePatterns = /* @__PURE__ */ new Set();
|
|
4526
|
-
for (const slide of slides) {
|
|
4527
|
-
if (slide.data.title) {
|
|
4528
|
-
const isUpperCase = slide.data.title === slide.data.title.toUpperCase();
|
|
4529
|
-
const words = slide.data.title.split(" ").filter((w) => w.length > 0);
|
|
4530
|
-
const isTitleCase = words.length > 0 && words.every(
|
|
4531
|
-
(w) => w.length > 0 && w[0] === w[0]?.toUpperCase()
|
|
4532
|
-
);
|
|
4533
|
-
titlePatterns.add(isUpperCase ? "UPPER" : isTitleCase ? "Title" : "sentence");
|
|
5455
|
+
if (body.includes("Lorem") || body.includes("placeholder") || body.includes("TODO")) {
|
|
5456
|
+
completenessIssues.push("Contains placeholder text");
|
|
4534
5457
|
}
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
slideIndex: -1,
|
|
4539
|
-
dimension: "consistency",
|
|
4540
|
-
severity: "warning",
|
|
4541
|
-
message: `Inconsistent title casing: ${Array.from(titlePatterns).join(", ")}`,
|
|
4542
|
-
autoFixable: true,
|
|
4543
|
-
fixSuggestion: "Use consistent title case throughout"
|
|
4544
|
-
});
|
|
4545
|
-
score -= 10;
|
|
4546
|
-
}
|
|
4547
|
-
const fontMatches = html.match(/font-family:\s*([^;]+);/gi) || [];
|
|
4548
|
-
const uniqueFonts = new Set(fontMatches.map((f) => f.toLowerCase()));
|
|
4549
|
-
if (uniqueFonts.size > 3) {
|
|
4550
|
-
issues.push({
|
|
4551
|
-
slideIndex: -1,
|
|
4552
|
-
dimension: "consistency",
|
|
4553
|
-
severity: "warning",
|
|
4554
|
-
message: `Too many font families (${uniqueFonts.size} > 3)`,
|
|
4555
|
-
autoFixable: true,
|
|
4556
|
-
fixSuggestion: "Use 2-3 complementary fonts max"
|
|
4557
|
-
});
|
|
4558
|
-
score -= 10;
|
|
4559
|
-
}
|
|
4560
|
-
return {
|
|
4561
|
-
name: "Consistency",
|
|
4562
|
-
score: Math.max(0, score),
|
|
4563
|
-
weight: DIMENSION_WEIGHTS.consistency,
|
|
4564
|
-
issues,
|
|
4565
|
-
details: {
|
|
4566
|
-
hasCssVariables,
|
|
4567
|
-
titlePatterns: Array.from(titlePatterns),
|
|
4568
|
-
fontFamilyCount: uniqueFonts.size
|
|
5458
|
+
const emptyBullets = bullets.filter((b) => b.length < 3).length;
|
|
5459
|
+
if (emptyBullets > 0 && bullets.length > 0) {
|
|
5460
|
+
completenessIssues.push(`${emptyBullets} empty/minimal bullets`);
|
|
4569
5461
|
}
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
formatReport(result) {
|
|
4588
|
-
const lines = [];
|
|
4589
|
-
lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
4590
|
-
lines.push("\u2551 7-DIMENSION QUALITY ASSESSMENT \u2551");
|
|
4591
|
-
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
4592
|
-
lines.push("");
|
|
4593
|
-
lines.push(`Overall Score: ${result.overallScore}/100 ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
4594
|
-
lines.push(`Threshold: ${result.threshold}/100`);
|
|
4595
|
-
lines.push("");
|
|
4596
|
-
lines.push("Dimension Breakdown:");
|
|
4597
|
-
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
4598
|
-
const dims = result.dimensions;
|
|
4599
|
-
const formatDim = (name, d) => {
|
|
4600
|
-
const bar = "\u2588".repeat(Math.floor(d.score / 10)) + "\u2591".repeat(10 - Math.floor(d.score / 10));
|
|
4601
|
-
const status = d.score >= 95 ? "\u2705" : d.score >= 80 ? "\u26A0\uFE0F" : "\u274C";
|
|
4602
|
-
return `${status} ${name.padEnd(14)} ${bar} ${d.score.toString().padStart(3)}/100 (${(d.weight * 100).toFixed(0)}%)`;
|
|
4603
|
-
};
|
|
4604
|
-
lines.push(formatDim("Layout", dims.layout));
|
|
4605
|
-
lines.push(formatDim("Contrast", dims.contrast));
|
|
4606
|
-
lines.push(formatDim("Graphics", dims.graphics));
|
|
4607
|
-
lines.push(formatDim("Content", dims.content));
|
|
4608
|
-
lines.push(formatDim("Clarity", dims.clarity));
|
|
4609
|
-
lines.push(formatDim("Effectiveness", dims.effectiveness));
|
|
4610
|
-
lines.push(formatDim("Consistency", dims.consistency));
|
|
4611
|
-
lines.push("");
|
|
4612
|
-
const errors = result.issues.filter((i) => i.severity === "error");
|
|
4613
|
-
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
4614
|
-
if (errors.length > 0) {
|
|
4615
|
-
lines.push("\u274C Errors:");
|
|
4616
|
-
errors.forEach((e) => lines.push(` \u2022 ${e.message}`));
|
|
4617
|
-
lines.push("");
|
|
4618
|
-
}
|
|
4619
|
-
if (warnings.length > 0) {
|
|
4620
|
-
lines.push("\u26A0\uFE0F Warnings:");
|
|
4621
|
-
warnings.slice(0, 10).forEach((w) => lines.push(` \u2022 ${w.message}`));
|
|
4622
|
-
if (warnings.length > 10) {
|
|
4623
|
-
lines.push(` ... and ${warnings.length - 10} more warnings`);
|
|
5462
|
+
const hasCompletenessIssues = completenessIssues.length > 0;
|
|
5463
|
+
const visualNeedsIssues = [];
|
|
5464
|
+
const visualRequiredTypes = ["data", "chart", "metrics", "comparison", "process", "timeline"];
|
|
5465
|
+
const visualRecommendedTypes = ["big-number", "quote", "testimonial"];
|
|
5466
|
+
const slideClasses = Array.from(currentSlide.classList).join(" ").toLowerCase();
|
|
5467
|
+
const needsVisual = visualRequiredTypes.some((t) => slideClasses.includes(t));
|
|
5468
|
+
const visualRecommended = visualRecommendedTypes.some((t) => slideClasses.includes(t));
|
|
5469
|
+
const contentText = (title + " " + body).toLowerCase();
|
|
5470
|
+
const dataIndicators = /\d+%|\$[\d,]+|\d+x|million|billion|percent|growth|increase|decrease|comparison|versus|vs\b|before|after/i;
|
|
5471
|
+
const hasDataContent = dataIndicators.test(contentText);
|
|
5472
|
+
const hasAnyVisual = hasImage || hasChart;
|
|
5473
|
+
if (needsVisual && !hasAnyVisual) {
|
|
5474
|
+
visualNeedsIssues.push("Data/process slide missing visual - needs chart, diagram, or image");
|
|
5475
|
+
} else if (hasDataContent && !hasAnyVisual && bullets.length === 0) {
|
|
5476
|
+
visualNeedsIssues.push("Content has numbers/data but no visualization - chart would help");
|
|
5477
|
+
} else if (visualRecommended && !hasAnyVisual) {
|
|
5478
|
+
visualNeedsIssues.push("Visual recommended for this slide type");
|
|
4624
5479
|
}
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
5480
|
+
if (hasImage) {
|
|
5481
|
+
const img = currentSlide.querySelector("img");
|
|
5482
|
+
const imgSrc = img?.getAttribute("src") || "";
|
|
5483
|
+
if (imgSrc.includes("placeholder") || imgSrc.includes("picsum") || imgSrc.includes("via.placeholder")) {
|
|
5484
|
+
visualNeedsIssues.push("Placeholder image detected - needs real visual");
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
const needsVisualButMissing = visualNeedsIssues.length > 0;
|
|
5488
|
+
const visualScore = hasAnyVisual ? 10 : needsVisual || hasDataContent ? 0 : 5;
|
|
5489
|
+
return {
|
|
5490
|
+
title,
|
|
5491
|
+
body,
|
|
5492
|
+
bullets,
|
|
5493
|
+
steps,
|
|
5494
|
+
// NEW: Timeline/process step content
|
|
5495
|
+
hasSteps,
|
|
5496
|
+
// NEW: Has .steps/.process-steps container
|
|
5497
|
+
hasMetrics,
|
|
5498
|
+
// NEW: Has metrics content
|
|
5499
|
+
hasImage,
|
|
5500
|
+
hasChart,
|
|
5501
|
+
classList,
|
|
5502
|
+
backgroundColor,
|
|
5503
|
+
titleFontSize: titleStyles?.fontSize || "",
|
|
5504
|
+
titleColor: titleStyles?.color || "",
|
|
5505
|
+
// Include steps in content length calculation
|
|
5506
|
+
contentLength: (title + body + bullets.join(" ") + steps.join(" ")).length,
|
|
5507
|
+
// Visual quality flags
|
|
5508
|
+
hasTruncatedText: truncatedElements.length > 0,
|
|
5509
|
+
truncatedElements,
|
|
5510
|
+
isEmptySlide,
|
|
5511
|
+
hasOnlyTitle,
|
|
5512
|
+
isRedundant,
|
|
5513
|
+
allVisibleTextLength: allVisibleText.length,
|
|
5514
|
+
// NEW: Expert quality checks
|
|
5515
|
+
hasContrastIssues,
|
|
5516
|
+
contrastIssues,
|
|
5517
|
+
hasLayoutIssues,
|
|
5518
|
+
layoutIssues,
|
|
5519
|
+
hasTypographyIssues,
|
|
5520
|
+
typographyIssues,
|
|
5521
|
+
hasCompletenessIssues,
|
|
5522
|
+
completenessIssues,
|
|
5523
|
+
// Visual needs analysis
|
|
5524
|
+
needsVisualButMissing,
|
|
5525
|
+
visualNeedsIssues,
|
|
5526
|
+
visualScore
|
|
5527
|
+
};
|
|
5528
|
+
});
|
|
5529
|
+
return this.scoreSlide(slideIndex, slideData, screenshotPath);
|
|
4643
5530
|
}
|
|
4644
5531
|
/**
|
|
4645
|
-
*
|
|
5532
|
+
* EXPERT-LEVEL SLIDE SCORING
|
|
5533
|
+
*
|
|
5534
|
+
* This evaluates each slide like Nancy Duarte, Carmine Gallo, or a McKinsey partner would.
|
|
5535
|
+
* It's not about rules - it's about whether the slide WORKS.
|
|
4646
5536
|
*/
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
const
|
|
4652
|
-
const
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
5537
|
+
scoreSlide(slideIndex, slideData, screenshotPath) {
|
|
5538
|
+
if (!slideData) {
|
|
5539
|
+
return this.createFailedSlideScore(slideIndex, "unknown", "Could not analyze slide", screenshotPath);
|
|
5540
|
+
}
|
|
5541
|
+
const slideType = this.inferSlideType(slideData);
|
|
5542
|
+
const criticalFailures = [];
|
|
5543
|
+
if (slideData.isEmptySlide) {
|
|
5544
|
+
criticalFailures.push("EMPTY SLIDE: No meaningful content - slide serves no purpose");
|
|
5545
|
+
}
|
|
5546
|
+
if (slideData.hasOnlyTitle && !["title", "agenda", "thank-you", "cta"].includes(slideType)) {
|
|
5547
|
+
criticalFailures.push("INCOMPLETE: Slide has title but no body content - looks unfinished");
|
|
5548
|
+
}
|
|
5549
|
+
if (slideData.hasTruncatedText) {
|
|
5550
|
+
criticalFailures.push(`TRUNCATED TEXT: ${slideData.truncatedElements.length} element(s) cut off - message incomplete`);
|
|
5551
|
+
}
|
|
5552
|
+
if (slideData.isRedundant) {
|
|
5553
|
+
criticalFailures.push("REDUNDANT: Title and body say the same thing - violates one-idea rule");
|
|
5554
|
+
}
|
|
5555
|
+
if (slideData.hasCompletenessIssues) {
|
|
5556
|
+
slideData.completenessIssues.forEach((issue) => {
|
|
5557
|
+
if (issue.includes("empty")) {
|
|
5558
|
+
criticalFailures.push(`INCOMPLETE: ${issue}`);
|
|
5559
|
+
}
|
|
5560
|
+
});
|
|
4660
5561
|
}
|
|
4661
|
-
|
|
5562
|
+
if (criticalFailures.length > 0) {
|
|
5563
|
+
return this.createFailedSlideScore(slideIndex, slideType, criticalFailures.join("; "), screenshotPath, criticalFailures);
|
|
5564
|
+
}
|
|
5565
|
+
let glanceTest = 8;
|
|
5566
|
+
const glanceNotes = [];
|
|
5567
|
+
const wordCount = slideData.contentLength / 6;
|
|
5568
|
+
if (wordCount > 40) {
|
|
5569
|
+
glanceTest = 3;
|
|
5570
|
+
glanceNotes.push("Too much text - fails glance test");
|
|
5571
|
+
} else if (wordCount > 25) {
|
|
5572
|
+
glanceTest -= 3;
|
|
5573
|
+
glanceNotes.push("Text-heavy - borderline glance test");
|
|
5574
|
+
} else if (wordCount < 15) {
|
|
5575
|
+
glanceTest += 1;
|
|
5576
|
+
glanceNotes.push("Clean, minimal text - passes glance test easily");
|
|
5577
|
+
}
|
|
5578
|
+
if (slideData.bullets.length > 5) {
|
|
5579
|
+
glanceTest -= 4;
|
|
5580
|
+
glanceNotes.push("Too many bullets - cannot scan in 3 seconds");
|
|
5581
|
+
} else if (slideData.bullets.length > 3) {
|
|
5582
|
+
glanceTest -= 2;
|
|
5583
|
+
glanceNotes.push("Multiple bullets - needs careful structuring");
|
|
5584
|
+
}
|
|
5585
|
+
if (slideData.hasImage || slideData.hasChart) {
|
|
5586
|
+
glanceTest += 1;
|
|
5587
|
+
glanceNotes.push("Visual element aids quick comprehension");
|
|
5588
|
+
}
|
|
5589
|
+
glanceTest = Math.max(0, Math.min(10, glanceTest));
|
|
5590
|
+
let oneIdea = 7;
|
|
5591
|
+
const oneIdeaNotes = [];
|
|
5592
|
+
if (slideData.title.length > 0 && slideData.title.length < 60) {
|
|
5593
|
+
oneIdea += 1;
|
|
5594
|
+
oneIdeaNotes.push("Clear, focused title");
|
|
5595
|
+
} else if (slideData.title.length > 80) {
|
|
5596
|
+
oneIdea -= 2;
|
|
5597
|
+
oneIdeaNotes.push("Title too long - multiple ideas?");
|
|
5598
|
+
} else if (slideData.title.length === 0) {
|
|
5599
|
+
oneIdea -= 4;
|
|
5600
|
+
oneIdeaNotes.push("No title - what is the one idea?");
|
|
5601
|
+
}
|
|
5602
|
+
const focusedTypes = ["big-number", "single-statement", "quote", "cta", "title"];
|
|
5603
|
+
if (focusedTypes.some((t) => slideType.includes(t))) {
|
|
5604
|
+
oneIdea += 2;
|
|
5605
|
+
oneIdeaNotes.push("Slide type naturally focuses on one idea");
|
|
5606
|
+
}
|
|
5607
|
+
if (slideData.bullets.length > 4) {
|
|
5608
|
+
oneIdea -= 3;
|
|
5609
|
+
oneIdeaNotes.push("Multiple bullets dilute the message");
|
|
5610
|
+
}
|
|
5611
|
+
oneIdea = Math.max(0, Math.min(10, oneIdea));
|
|
5612
|
+
let dataInkRatio = 7;
|
|
5613
|
+
const dataInkNotes = [];
|
|
5614
|
+
const structuredTypes = ["big-number", "metrics-grid", "three-column", "timeline", "process", "comparison"];
|
|
5615
|
+
if (structuredTypes.some((t) => slideType.includes(t))) {
|
|
5616
|
+
dataInkRatio += 1;
|
|
5617
|
+
dataInkNotes.push("Structured layout");
|
|
5618
|
+
}
|
|
5619
|
+
if (slideData.hasChart) {
|
|
5620
|
+
dataInkRatio += 2;
|
|
5621
|
+
dataInkNotes.push("Data visualization present - excellent");
|
|
5622
|
+
}
|
|
5623
|
+
if (slideData.hasImage) {
|
|
5624
|
+
dataInkRatio += 1;
|
|
5625
|
+
dataInkNotes.push("Supporting visual present");
|
|
5626
|
+
}
|
|
5627
|
+
if (slideData.needsVisualButMissing) {
|
|
5628
|
+
dataInkRatio -= 3;
|
|
5629
|
+
slideData.visualNeedsIssues.forEach((issue) => {
|
|
5630
|
+
dataInkNotes.push(`Visual: ${issue}`);
|
|
5631
|
+
});
|
|
5632
|
+
} else if (slideData.visualScore === 10) {
|
|
5633
|
+
dataInkNotes.push("Appropriate visuals for content type");
|
|
5634
|
+
}
|
|
5635
|
+
if (slideData.contentLength > 400) {
|
|
5636
|
+
dataInkRatio -= 4;
|
|
5637
|
+
dataInkNotes.push("Excessive text - low information density");
|
|
5638
|
+
}
|
|
5639
|
+
dataInkRatio = Math.max(0, Math.min(10, dataInkRatio));
|
|
5640
|
+
let professionalExecution = 7;
|
|
5641
|
+
const executionNotes = [];
|
|
5642
|
+
const titleFontSize = parseFloat(slideData.titleFontSize || "0");
|
|
5643
|
+
if (titleFontSize >= 40) {
|
|
5644
|
+
professionalExecution += 1;
|
|
5645
|
+
executionNotes.push("Strong title typography");
|
|
5646
|
+
} else if (titleFontSize > 0 && titleFontSize < 24) {
|
|
5647
|
+
professionalExecution -= 1;
|
|
5648
|
+
executionNotes.push("Title could be more prominent");
|
|
5649
|
+
}
|
|
5650
|
+
if (slideData.contentLength > 10 && slideData.contentLength < 200) {
|
|
5651
|
+
professionalExecution += 1;
|
|
5652
|
+
executionNotes.push("Well-balanced content density");
|
|
5653
|
+
}
|
|
5654
|
+
if (slideData.classList.some((c) => c.includes("slide-"))) {
|
|
5655
|
+
professionalExecution += 1;
|
|
5656
|
+
executionNotes.push("Consistent with design system");
|
|
5657
|
+
}
|
|
5658
|
+
if (slideData.hasContrastIssues) {
|
|
5659
|
+
professionalExecution -= 3;
|
|
5660
|
+
slideData.contrastIssues.forEach((issue) => {
|
|
5661
|
+
executionNotes.push(`Contrast: ${issue}`);
|
|
5662
|
+
});
|
|
5663
|
+
} else {
|
|
5664
|
+
executionNotes.push("Good text contrast");
|
|
5665
|
+
}
|
|
5666
|
+
if (slideData.hasLayoutIssues) {
|
|
5667
|
+
professionalExecution -= 2;
|
|
5668
|
+
slideData.layoutIssues.forEach((issue) => {
|
|
5669
|
+
executionNotes.push(`Layout: ${issue}`);
|
|
5670
|
+
});
|
|
5671
|
+
} else if (slideData.contentLength > 50) {
|
|
5672
|
+
executionNotes.push("Balanced layout");
|
|
5673
|
+
}
|
|
5674
|
+
if (slideData.hasTypographyIssues) {
|
|
5675
|
+
professionalExecution -= 2;
|
|
5676
|
+
slideData.typographyIssues.forEach((issue) => {
|
|
5677
|
+
executionNotes.push(`Typography: ${issue}`);
|
|
5678
|
+
});
|
|
5679
|
+
} else if (titleFontSize > 0) {
|
|
5680
|
+
executionNotes.push("Good type hierarchy");
|
|
5681
|
+
}
|
|
5682
|
+
professionalExecution = Math.max(0, Math.min(10, professionalExecution));
|
|
5683
|
+
const totalScore = glanceTest + oneIdea + dataInkRatio + professionalExecution;
|
|
5684
|
+
const visualImpact = Math.round((glanceTest + oneIdea) / 2);
|
|
5685
|
+
const contentClarity = oneIdea;
|
|
5686
|
+
const professionalPolish = Math.round((dataInkRatio + professionalExecution) / 2);
|
|
5687
|
+
const themeCoherence = professionalExecution;
|
|
4662
5688
|
return {
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
5689
|
+
slideIndex,
|
|
5690
|
+
slideType,
|
|
5691
|
+
// New expert dimensions
|
|
5692
|
+
glanceTest,
|
|
5693
|
+
glanceTestNotes: glanceNotes.join("; ") || "Passes glance test",
|
|
5694
|
+
oneIdea,
|
|
5695
|
+
oneIdeaNotes: oneIdeaNotes.join("; ") || "Clear single message",
|
|
5696
|
+
dataInkRatio,
|
|
5697
|
+
dataInkNotes: dataInkNotes.join("; ") || "Good information density",
|
|
5698
|
+
professionalExecution,
|
|
5699
|
+
professionalExecutionNotes: executionNotes.join("; ") || "Professional quality",
|
|
5700
|
+
// Critical failures
|
|
5701
|
+
hasCriticalFailure: false,
|
|
5702
|
+
criticalFailures: [],
|
|
5703
|
+
// Legacy dimensions (for compatibility)
|
|
5704
|
+
visualImpact,
|
|
5705
|
+
visualImpactNotes: glanceNotes.join("; ") || "Standard",
|
|
5706
|
+
contentClarity,
|
|
5707
|
+
contentClarityNotes: oneIdeaNotes.join("; ") || "Good",
|
|
5708
|
+
professionalPolish,
|
|
5709
|
+
professionalPolishNotes: executionNotes.join("; ") || "Acceptable",
|
|
5710
|
+
themeCoherence,
|
|
5711
|
+
themeCoherenceNotes: "Consistent",
|
|
5712
|
+
totalScore,
|
|
5713
|
+
screenshotPath
|
|
4667
5714
|
};
|
|
4668
5715
|
}
|
|
4669
5716
|
/**
|
|
4670
|
-
*
|
|
5717
|
+
* Create a failed slide score (for critical failures)
|
|
4671
5718
|
*/
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
slideIndex
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
5719
|
+
createFailedSlideScore(slideIndex, slideType, reason, screenshotPath, criticalFailures = []) {
|
|
5720
|
+
return {
|
|
5721
|
+
slideIndex,
|
|
5722
|
+
slideType,
|
|
5723
|
+
glanceTest: 0,
|
|
5724
|
+
glanceTestNotes: "CRITICAL FAILURE: " + reason,
|
|
5725
|
+
oneIdea: 0,
|
|
5726
|
+
oneIdeaNotes: "CRITICAL FAILURE: " + reason,
|
|
5727
|
+
dataInkRatio: 0,
|
|
5728
|
+
dataInkNotes: "CRITICAL FAILURE: " + reason,
|
|
5729
|
+
professionalExecution: 0,
|
|
5730
|
+
professionalExecutionNotes: "CRITICAL FAILURE: " + reason,
|
|
5731
|
+
hasCriticalFailure: true,
|
|
5732
|
+
criticalFailures: criticalFailures.length > 0 ? criticalFailures : [reason],
|
|
5733
|
+
visualImpact: 0,
|
|
5734
|
+
visualImpactNotes: "CRITICAL: " + reason,
|
|
5735
|
+
contentClarity: 0,
|
|
5736
|
+
contentClarityNotes: "CRITICAL: " + reason,
|
|
5737
|
+
professionalPolish: 0,
|
|
5738
|
+
professionalPolishNotes: "CRITICAL: " + reason,
|
|
5739
|
+
themeCoherence: 0,
|
|
5740
|
+
themeCoherenceNotes: "CRITICAL: " + reason,
|
|
5741
|
+
totalScore: 0,
|
|
5742
|
+
screenshotPath
|
|
4680
5743
|
};
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
5744
|
+
}
|
|
5745
|
+
inferSlideType(slideData) {
|
|
5746
|
+
const classList = slideData.classList || [];
|
|
5747
|
+
for (const cls of classList) {
|
|
5748
|
+
if (cls.includes("metrics-grid")) return "metrics-grid";
|
|
5749
|
+
if (cls.includes("three-column")) return "three-column";
|
|
5750
|
+
if (cls.includes("three-points")) return "three-points";
|
|
5751
|
+
if (cls.includes("two-column")) return "two-column";
|
|
5752
|
+
if (cls.includes("big-number")) return "big-number";
|
|
5753
|
+
if (cls.includes("comparison")) return "comparison";
|
|
5754
|
+
if (cls.includes("timeline")) return "timeline";
|
|
5755
|
+
if (cls.includes("process")) return "process";
|
|
5756
|
+
if (cls.includes("quote")) return "quote";
|
|
5757
|
+
if (cls.includes("testimonial")) return "testimonial";
|
|
5758
|
+
if (cls.includes("cta")) return "cta";
|
|
5759
|
+
if (cls.includes("thank-you")) return "thank-you";
|
|
5760
|
+
if (cls.includes("title")) return "title";
|
|
5761
|
+
if (cls.includes("bullet")) return "bullet-points";
|
|
5762
|
+
if (cls.includes("single-statement")) return "single-statement";
|
|
5763
|
+
if (cls.includes("agenda")) return "agenda";
|
|
5764
|
+
}
|
|
5765
|
+
if (!slideData.body && slideData.bullets.length > 0) return "bullet-points";
|
|
5766
|
+
if (slideData.body && !slideData.bullets.length) return "content";
|
|
5767
|
+
if (slideData.hasChart) return "data-visualization";
|
|
5768
|
+
return "standard";
|
|
5769
|
+
}
|
|
5770
|
+
// ===========================================================================
|
|
5771
|
+
// PRESENTATION-LEVEL EVALUATION
|
|
5772
|
+
// ===========================================================================
|
|
5773
|
+
evaluateNarrativeFlow(slideScores) {
|
|
5774
|
+
let score = 25;
|
|
5775
|
+
const notes = [];
|
|
5776
|
+
const firstSlide = slideScores[0];
|
|
5777
|
+
const hasStrongOpening = Boolean(firstSlide && (firstSlide.slideType === "title" || firstSlide.visualImpact >= 7));
|
|
5778
|
+
if (!hasStrongOpening) {
|
|
5779
|
+
score -= 5;
|
|
5780
|
+
notes.push("Opening could be stronger");
|
|
5781
|
+
}
|
|
5782
|
+
const middleSlides = slideScores.slice(1, -1);
|
|
5783
|
+
const highImpactMiddle = middleSlides.filter((s) => s.visualImpact >= 7).length;
|
|
5784
|
+
const hasCompellingMiddle = highImpactMiddle >= middleSlides.length * 0.3;
|
|
5785
|
+
if (!hasCompellingMiddle) {
|
|
5786
|
+
score -= 7;
|
|
5787
|
+
notes.push("Middle section needs more visual variety");
|
|
5788
|
+
}
|
|
5789
|
+
const lastSlide = slideScores[slideScores.length - 1];
|
|
5790
|
+
const secondLastSlide = slideScores[slideScores.length - 2];
|
|
5791
|
+
const closingTypes = ["cta", "thank-you", "call-to-action"];
|
|
5792
|
+
const hasMemorableClose = Boolean(
|
|
5793
|
+
lastSlide && (closingTypes.includes(lastSlide.slideType) || lastSlide.visualImpact >= 7) || secondLastSlide && closingTypes.includes(secondLastSlide.slideType)
|
|
5794
|
+
);
|
|
5795
|
+
if (!hasMemorableClose) {
|
|
5796
|
+
score -= 5;
|
|
5797
|
+
notes.push("Ending should be more memorable");
|
|
5798
|
+
}
|
|
5799
|
+
const slideTypes = new Set(slideScores.map((s) => s.slideType));
|
|
5800
|
+
const storyArcComplete = slideTypes.size >= 3;
|
|
5801
|
+
if (!storyArcComplete) {
|
|
5802
|
+
score -= 5;
|
|
5803
|
+
notes.push("Needs more variety in slide types");
|
|
4695
5804
|
}
|
|
5805
|
+
return {
|
|
5806
|
+
score: Math.max(0, score),
|
|
5807
|
+
hasStrongOpening,
|
|
5808
|
+
hasCompellingMiddle,
|
|
5809
|
+
hasMemorableClose,
|
|
5810
|
+
storyArcComplete,
|
|
5811
|
+
notes: notes.join(". ") || "Good narrative structure"
|
|
5812
|
+
};
|
|
4696
5813
|
}
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
5814
|
+
evaluateVisualConsistency(slideScores) {
|
|
5815
|
+
let score = 25;
|
|
5816
|
+
const notes = [];
|
|
5817
|
+
const visualScores = slideScores.map((s) => s.visualImpact);
|
|
5818
|
+
const avgVisual = visualScores.reduce((a, b) => a + b, 0) / visualScores.length;
|
|
5819
|
+
const variance = visualScores.reduce((sum, s) => sum + Math.pow(s - avgVisual, 2), 0) / visualScores.length;
|
|
5820
|
+
const colorPaletteConsistent = variance < 4;
|
|
5821
|
+
if (!colorPaletteConsistent) {
|
|
5822
|
+
score -= 5;
|
|
5823
|
+
notes.push("Visual quality varies too much between slides");
|
|
4703
5824
|
}
|
|
4704
|
-
const
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
slide.data.body = this.condenseText(slide.data.body, maxWords / 2);
|
|
4711
|
-
}
|
|
4712
|
-
if (slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4713
|
-
const bulletCount = slide.data.bullets.length;
|
|
4714
|
-
slide.data.bullets = slide.data.bullets.map(
|
|
4715
|
-
(bullet) => this.condenseText(bullet, Math.floor(maxWords / bulletCount))
|
|
4716
|
-
);
|
|
4717
|
-
}
|
|
4718
|
-
if (slide.data.subtitle) {
|
|
4719
|
-
slide.data.subtitle = this.condenseText(slide.data.subtitle, 10);
|
|
4720
|
-
}
|
|
4721
|
-
result.newValue = this.countWords(slide);
|
|
4722
|
-
result.applied = result.newValue <= maxWords;
|
|
4723
|
-
result.description = `Condensed content from ${result.originalValue} to ${result.newValue} words`;
|
|
5825
|
+
const polishScores = slideScores.map((s) => s.professionalPolish);
|
|
5826
|
+
const avgPolish = polishScores.reduce((a, b) => a + b, 0) / polishScores.length;
|
|
5827
|
+
const typographyConsistent = avgPolish >= 6;
|
|
5828
|
+
if (!typographyConsistent) {
|
|
5829
|
+
score -= 5;
|
|
5830
|
+
notes.push("Typography could be more polished");
|
|
4724
5831
|
}
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
result.applied = true;
|
|
4732
|
-
result.description = `Reduced bullets from ${result.originalValue} to ${result.newValue}`;
|
|
4733
|
-
}
|
|
5832
|
+
const coherenceScores = slideScores.map((s) => s.themeCoherence);
|
|
5833
|
+
const avgCoherence = coherenceScores.reduce((a, b) => a + b, 0) / coherenceScores.length;
|
|
5834
|
+
const layoutPatternsConsistent = avgCoherence >= 6;
|
|
5835
|
+
if (!layoutPatternsConsistent) {
|
|
5836
|
+
score -= 5;
|
|
5837
|
+
notes.push("Layout patterns should be more consistent");
|
|
4734
5838
|
}
|
|
4735
|
-
|
|
5839
|
+
const professionalLook = avgPolish >= 7 && avgCoherence >= 7;
|
|
5840
|
+
if (!professionalLook) {
|
|
5841
|
+
score -= 5;
|
|
5842
|
+
notes.push("Overall polish could be improved");
|
|
5843
|
+
}
|
|
5844
|
+
return {
|
|
5845
|
+
score: Math.max(0, score),
|
|
5846
|
+
colorPaletteConsistent,
|
|
5847
|
+
typographyConsistent,
|
|
5848
|
+
layoutPatternsConsistent,
|
|
5849
|
+
professionalLook,
|
|
5850
|
+
notes: notes.join(". ") || "Consistent visual design"
|
|
5851
|
+
};
|
|
4736
5852
|
}
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
if (
|
|
4742
|
-
|
|
5853
|
+
evaluateContentQuality(slideScores) {
|
|
5854
|
+
let score = 25;
|
|
5855
|
+
const notes = [];
|
|
5856
|
+
const criticalFailureSlides = slideScores.filter((s) => s.hasCriticalFailure);
|
|
5857
|
+
if (criticalFailureSlides.length > 0) {
|
|
5858
|
+
score = Math.max(0, 25 - criticalFailureSlides.length * 8);
|
|
5859
|
+
notes.push(`CRITICAL: ${criticalFailureSlides.length} slide(s) have critical failures`);
|
|
5860
|
+
criticalFailureSlides.forEach((s) => {
|
|
5861
|
+
s.criticalFailures.forEach((f) => notes.push(` - Slide ${s.slideIndex}: ${f}`));
|
|
5862
|
+
});
|
|
4743
5863
|
}
|
|
4744
|
-
const
|
|
4745
|
-
|
|
4746
|
-
if (
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
const maxWords = issue.expectedValue;
|
|
4750
|
-
slide.data.keyMessage = this.condenseText(slide.data.keyMessage, maxWords);
|
|
4751
|
-
result.newValue = slide.data.keyMessage;
|
|
4752
|
-
result.applied = true;
|
|
4753
|
-
result.description = `Shortened key message to ${maxWords} words`;
|
|
4754
|
-
}
|
|
5864
|
+
const avgGlance = slideScores.reduce((sum, s) => sum + s.glanceTest, 0) / slideScores.length;
|
|
5865
|
+
const passesGlanceTest = avgGlance >= 6;
|
|
5866
|
+
if (!passesGlanceTest) {
|
|
5867
|
+
score -= 5;
|
|
5868
|
+
notes.push(`Glance Test: Average ${avgGlance.toFixed(1)}/10 - too text-heavy`);
|
|
4755
5869
|
}
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
if (words.length > 6) {
|
|
4762
|
-
slide.data.title = words.slice(0, 6).join(" ");
|
|
4763
|
-
}
|
|
4764
|
-
result.newValue = slide.data.title;
|
|
4765
|
-
result.applied = true;
|
|
4766
|
-
result.description = `Shortened title from ${originalLength} to ${slide.data.title.split(/\s+/).length} words`;
|
|
4767
|
-
}
|
|
5870
|
+
const avgOneIdea = slideScores.reduce((sum, s) => sum + s.oneIdea, 0) / slideScores.length;
|
|
5871
|
+
const hasOneIdeaPerSlide = avgOneIdea >= 6;
|
|
5872
|
+
if (!hasOneIdeaPerSlide) {
|
|
5873
|
+
score -= 5;
|
|
5874
|
+
notes.push(`One Idea Rule: Average ${avgOneIdea.toFixed(1)}/10 - messages diluted`);
|
|
4768
5875
|
}
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
} else if (slide.data.body && slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4776
|
-
delete slide.data.body;
|
|
4777
|
-
result.applied = true;
|
|
4778
|
-
result.description = "Removed body text, keeping bullets";
|
|
4779
|
-
}
|
|
4780
|
-
result.newValue = this.countElements(slide);
|
|
5876
|
+
const messagesAreClear = avgOneIdea >= 7;
|
|
5877
|
+
const lowScoreSlides = slideScores.filter((s) => s.totalScore < 20).length;
|
|
5878
|
+
const appropriateDepth = lowScoreSlides < slideScores.length * 0.2;
|
|
5879
|
+
if (!appropriateDepth) {
|
|
5880
|
+
score -= 5;
|
|
5881
|
+
notes.push("Some slides have quality issues");
|
|
4781
5882
|
}
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
fixLayoutIssue(slides, issue, result) {
|
|
4788
|
-
if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
|
|
4789
|
-
return result;
|
|
5883
|
+
const overloadedSlides = slideScores.filter((s) => s.glanceTest < 5).length;
|
|
5884
|
+
const noOverload = overloadedSlides === 0;
|
|
5885
|
+
if (!noOverload) {
|
|
5886
|
+
score -= 3;
|
|
5887
|
+
notes.push(`${overloadedSlides} slides fail glance test - too dense`);
|
|
4790
5888
|
}
|
|
4791
|
-
const
|
|
4792
|
-
|
|
4793
|
-
if (
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
const targetWords = Math.floor(currentWordCount * (1 - targetReduction));
|
|
4797
|
-
result.originalValue = currentWordCount;
|
|
4798
|
-
if (slide.data.body) {
|
|
4799
|
-
slide.data.body = this.condenseText(slide.data.body, Math.floor(targetWords * 0.5));
|
|
4800
|
-
}
|
|
4801
|
-
if (slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4802
|
-
const wordsPerBullet = Math.floor(targetWords / (slide.data.bullets.length * 2));
|
|
4803
|
-
slide.data.bullets = slide.data.bullets.map((b) => this.condenseText(b, wordsPerBullet));
|
|
4804
|
-
}
|
|
4805
|
-
result.newValue = this.countWords(slide);
|
|
4806
|
-
result.applied = true;
|
|
4807
|
-
result.description = `Reduced content from ${result.originalValue} to ${result.newValue} words for better whitespace`;
|
|
5889
|
+
const excellentSlides = slideScores.filter((s) => s.totalScore >= 30).length;
|
|
5890
|
+
const actionableInsights = excellentSlides >= slideScores.length * 0.3;
|
|
5891
|
+
if (!actionableInsights) {
|
|
5892
|
+
score -= 3;
|
|
5893
|
+
notes.push("Need more high-impact slides");
|
|
4808
5894
|
}
|
|
4809
|
-
return
|
|
5895
|
+
return {
|
|
5896
|
+
score: Math.max(0, score),
|
|
5897
|
+
messagesAreClear,
|
|
5898
|
+
appropriateDepth,
|
|
5899
|
+
noOverload,
|
|
5900
|
+
actionableInsights,
|
|
5901
|
+
notes: notes.join(". ") || "Strong content quality"
|
|
5902
|
+
};
|
|
4810
5903
|
}
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
if (
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
5904
|
+
evaluateExecutiveReadiness(slideScores) {
|
|
5905
|
+
let score = 25;
|
|
5906
|
+
const notes = [];
|
|
5907
|
+
const criticalFailureSlides = slideScores.filter((s) => s.hasCriticalFailure);
|
|
5908
|
+
if (criticalFailureSlides.length > 0) {
|
|
5909
|
+
score = 0;
|
|
5910
|
+
notes.push(`CRITICAL: ${criticalFailureSlides.length} slide(s) have critical failures - CANNOT show to executives`);
|
|
5911
|
+
criticalFailureSlides.forEach((s) => {
|
|
5912
|
+
notes.push(` - Slide ${s.slideIndex} (${s.slideType}): ${s.criticalFailures[0]}`);
|
|
5913
|
+
});
|
|
5914
|
+
return {
|
|
5915
|
+
score: 0,
|
|
5916
|
+
wouldImpress: false,
|
|
5917
|
+
readyForBoardroom: false,
|
|
5918
|
+
compelling: false,
|
|
5919
|
+
shareworthy: false,
|
|
5920
|
+
notes: notes.join(". ")
|
|
5921
|
+
};
|
|
4828
5922
|
}
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
}
|
|
4849
|
-
const
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
"literally",
|
|
4855
|
-
"obviously",
|
|
4856
|
-
"clearly",
|
|
4857
|
-
"simply",
|
|
4858
|
-
"just",
|
|
4859
|
-
"that",
|
|
4860
|
-
"which",
|
|
4861
|
-
"would",
|
|
4862
|
-
"could",
|
|
4863
|
-
"should",
|
|
4864
|
-
"might"
|
|
4865
|
-
]);
|
|
4866
|
-
let filtered = words.filter((w) => !fillerWords.has(w.toLowerCase()));
|
|
4867
|
-
if (filtered.length <= maxWords) {
|
|
4868
|
-
return filtered.join(" ");
|
|
4869
|
-
}
|
|
4870
|
-
const punctuation = [".", ",", ";", ":", "-"];
|
|
4871
|
-
let breakPoint = maxWords;
|
|
4872
|
-
for (let i = maxWords - 1; i >= maxWords - 5 && i >= 0; i--) {
|
|
4873
|
-
const word = filtered[i];
|
|
4874
|
-
if (word && punctuation.some((p) => word.endsWith(p))) {
|
|
4875
|
-
breakPoint = i + 1;
|
|
4876
|
-
break;
|
|
4877
|
-
}
|
|
5923
|
+
const avgGlance = slideScores.reduce((sum, s) => sum + s.glanceTest, 0) / slideScores.length;
|
|
5924
|
+
const avgOneIdea = slideScores.reduce((sum, s) => sum + s.oneIdea, 0) / slideScores.length;
|
|
5925
|
+
const avgDataInk = slideScores.reduce((sum, s) => sum + s.dataInkRatio, 0) / slideScores.length;
|
|
5926
|
+
const avgExecution = slideScores.reduce((sum, s) => sum + s.professionalExecution, 0) / slideScores.length;
|
|
5927
|
+
const avgTotal = slideScores.reduce((sum, s) => sum + s.totalScore, 0) / slideScores.length;
|
|
5928
|
+
const wouldImpress = avgGlance >= 7 && avgOneIdea >= 7 && avgExecution >= 7;
|
|
5929
|
+
if (!wouldImpress) {
|
|
5930
|
+
score -= 7;
|
|
5931
|
+
notes.push("Needs more visual impact to impress");
|
|
5932
|
+
}
|
|
5933
|
+
const readyForBoardroom = avgExecution >= 6 && avgDataInk >= 6;
|
|
5934
|
+
if (!readyForBoardroom) {
|
|
5935
|
+
score -= 7;
|
|
5936
|
+
notes.push(`Boardroom readiness: Execution ${avgExecution.toFixed(1)}/10, Data-Ink ${avgDataInk.toFixed(1)}/10`);
|
|
5937
|
+
}
|
|
5938
|
+
const compelling = avgGlance >= 6 && avgOneIdea >= 6;
|
|
5939
|
+
if (!compelling) {
|
|
5940
|
+
score -= 5;
|
|
5941
|
+
notes.push(`Compelling: Glance ${avgGlance.toFixed(1)}/10, OneIdea ${avgOneIdea.toFixed(1)}/10`);
|
|
5942
|
+
}
|
|
5943
|
+
const excellentSlides = slideScores.filter((s) => s.totalScore >= 30).length;
|
|
5944
|
+
const shareworthy = excellentSlides >= slideScores.length * 0.4;
|
|
5945
|
+
if (!shareworthy) {
|
|
5946
|
+
score -= 5;
|
|
5947
|
+
notes.push(`Shareworthy: ${excellentSlides}/${slideScores.length} excellent slides (need 40%)`);
|
|
4878
5948
|
}
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
if (!result.endsWith(".") && !result.endsWith("!") && !result.endsWith("?")) {
|
|
4882
|
-
result = result.replace(/[,;:]$/, "") + "...";
|
|
5949
|
+
if (wouldImpress && readyForBoardroom && compelling) {
|
|
5950
|
+
notes.push("Meets expert standards - McKinsey/TED quality");
|
|
4883
5951
|
}
|
|
4884
|
-
return
|
|
5952
|
+
return {
|
|
5953
|
+
score: Math.max(0, score),
|
|
5954
|
+
wouldImpress,
|
|
5955
|
+
readyForBoardroom,
|
|
5956
|
+
compelling,
|
|
5957
|
+
shareworthy,
|
|
5958
|
+
notes: notes.join(". ") || "Executive-ready presentation"
|
|
5959
|
+
};
|
|
4885
5960
|
}
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
"
|
|
4899
|
-
"
|
|
4900
|
-
"
|
|
4901
|
-
"
|
|
4902
|
-
"
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
5961
|
+
// ===========================================================================
|
|
5962
|
+
// VERDICT & SUMMARY
|
|
5963
|
+
// ===========================================================================
|
|
5964
|
+
determineVerdict(score) {
|
|
5965
|
+
if (score >= 90) return "world-class";
|
|
5966
|
+
if (score >= 80) return "professional";
|
|
5967
|
+
if (score >= 65) return "acceptable";
|
|
5968
|
+
if (score >= 50) return "needs-work";
|
|
5969
|
+
return "poor";
|
|
5970
|
+
}
|
|
5971
|
+
explainVerdict(verdict, score) {
|
|
5972
|
+
const explanations = {
|
|
5973
|
+
"world-class": `Score: ${score}/100. This presentation would impress any audience. Ready for TED, boardrooms, and high-stakes pitches.`,
|
|
5974
|
+
"professional": `Score: ${score}/100. Solid professional presentation. Ready for most business contexts with minor polish.`,
|
|
5975
|
+
"acceptable": `Score: ${score}/100. Meets basic standards but lacks the polish and impact of world-class work.`,
|
|
5976
|
+
"needs-work": `Score: ${score}/100. Significant improvements needed in visual design, content clarity, or structure.`,
|
|
5977
|
+
"poor": `Score: ${score}/100. Fundamental issues with content, design, or structure. Major rework required.`
|
|
5978
|
+
};
|
|
5979
|
+
return explanations[verdict];
|
|
5980
|
+
}
|
|
5981
|
+
extractTopIssuesAndStrengths(slideScores) {
|
|
5982
|
+
const issues = [];
|
|
5983
|
+
const strengths = [];
|
|
5984
|
+
for (const slide of slideScores) {
|
|
5985
|
+
if (slide.visualImpact < 5) {
|
|
5986
|
+
issues.push(`Slide ${slide.slideIndex}: Low visual impact - ${slide.visualImpactNotes}`);
|
|
4910
5987
|
}
|
|
4911
|
-
if (
|
|
4912
|
-
|
|
5988
|
+
if (slide.contentClarity < 5) {
|
|
5989
|
+
issues.push(`Slide ${slide.slideIndex}: Clarity issue - ${slide.contentClarityNotes}`);
|
|
4913
5990
|
}
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
}
|
|
4917
|
-
/**
|
|
4918
|
-
* Count words in a slide.
|
|
4919
|
-
*/
|
|
4920
|
-
countWords(slide) {
|
|
4921
|
-
let text = "";
|
|
4922
|
-
if (slide.data.title) text += slide.data.title + " ";
|
|
4923
|
-
if (slide.data.subtitle) text += slide.data.subtitle + " ";
|
|
4924
|
-
if (slide.data.body) text += slide.data.body + " ";
|
|
4925
|
-
if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
|
|
4926
|
-
if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
|
|
4927
|
-
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
4928
|
-
}
|
|
4929
|
-
/**
|
|
4930
|
-
* Count elements in a slide.
|
|
4931
|
-
*/
|
|
4932
|
-
countElements(slide) {
|
|
4933
|
-
return (slide.data.title ? 1 : 0) + (slide.data.subtitle ? 1 : 0) + (slide.data.body ? 1 : 0) + (slide.data.bullets?.length || 0) + (slide.data.keyMessage ? 1 : 0) + (slide.data.image ? 1 : 0);
|
|
4934
|
-
}
|
|
4935
|
-
/**
|
|
4936
|
-
* Generate a summary of fixes applied.
|
|
4937
|
-
*/
|
|
4938
|
-
generateSummary(applied, skipped) {
|
|
4939
|
-
const lines = [];
|
|
4940
|
-
lines.push(`
|
|
4941
|
-
\u{1F527} Auto-Fix Summary`);
|
|
4942
|
-
lines.push(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
4943
|
-
lines.push(`Fixes Applied: ${applied.length}`);
|
|
4944
|
-
lines.push(`Fixes Skipped: ${skipped.length}`);
|
|
4945
|
-
lines.push("");
|
|
4946
|
-
if (applied.length > 0) {
|
|
4947
|
-
lines.push("\u2705 Applied Fixes:");
|
|
4948
|
-
for (const fix of applied) {
|
|
4949
|
-
lines.push(` \u2022 ${fix.description}`);
|
|
5991
|
+
if (slide.totalScore >= 35) {
|
|
5992
|
+
strengths.push(`Slide ${slide.slideIndex}: Excellent overall (${slide.slideType})`);
|
|
4950
5993
|
}
|
|
5994
|
+
}
|
|
5995
|
+
return {
|
|
5996
|
+
topIssues: issues.slice(0, 5),
|
|
5997
|
+
topStrengths: strengths.slice(0, 3)
|
|
5998
|
+
};
|
|
5999
|
+
}
|
|
6000
|
+
// ===========================================================================
|
|
6001
|
+
// FORMATTED REPORT
|
|
6002
|
+
// ===========================================================================
|
|
6003
|
+
generateReport(result) {
|
|
6004
|
+
const lines = [
|
|
6005
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
6006
|
+
"\u2551 VISUAL QUALITY EVALUATION REPORT \u2551",
|
|
6007
|
+
"\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563",
|
|
6008
|
+
"",
|
|
6009
|
+
` VERDICT: ${result.verdict.toUpperCase()}`,
|
|
6010
|
+
` ${result.verdictExplanation}`,
|
|
6011
|
+
"",
|
|
6012
|
+
"\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
|
|
6013
|
+
"\u2502 DIMENSION SCORES \u2502",
|
|
6014
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524",
|
|
6015
|
+
`\u2502 Narrative Flow: ${this.scoreBar(result.narrativeFlow.score, 25)} ${result.narrativeFlow.score}/25`,
|
|
6016
|
+
`\u2502 Visual Consistency: ${this.scoreBar(result.visualConsistency.score, 25)} ${result.visualConsistency.score}/25`,
|
|
6017
|
+
`\u2502 Content Quality: ${this.scoreBar(result.contentQuality.score, 25)} ${result.contentQuality.score}/25`,
|
|
6018
|
+
`\u2502 Executive Readiness: ${this.scoreBar(result.executiveReadiness.score, 25)} ${result.executiveReadiness.score}/25`,
|
|
6019
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524",
|
|
6020
|
+
`\u2502 OVERALL SCORE: ${this.scoreBar(result.overallScore, 100)} ${result.overallScore}/100`,
|
|
6021
|
+
"\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
|
|
6022
|
+
""
|
|
6023
|
+
];
|
|
6024
|
+
if (result.topStrengths.length > 0) {
|
|
6025
|
+
lines.push("\u2713 TOP STRENGTHS:");
|
|
6026
|
+
result.topStrengths.forEach((s) => lines.push(` \u2022 ${s}`));
|
|
4951
6027
|
lines.push("");
|
|
4952
6028
|
}
|
|
4953
|
-
if (
|
|
4954
|
-
lines.push("\
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
}
|
|
4958
|
-
if (skipped.length > 5) {
|
|
4959
|
-
lines.push(` ... and ${skipped.length - 5} more`);
|
|
4960
|
-
}
|
|
6029
|
+
if (result.topIssues.length > 0) {
|
|
6030
|
+
lines.push("\u2717 TOP ISSUES:");
|
|
6031
|
+
result.topIssues.forEach((i) => lines.push(` \u2022 ${i}`));
|
|
6032
|
+
lines.push("");
|
|
4961
6033
|
}
|
|
6034
|
+
lines.push("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
6035
|
+
lines.push("\u2502 SLIDE-BY-SLIDE BREAKDOWN \u2502");
|
|
6036
|
+
lines.push("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
6037
|
+
for (const slide of result.slideScores) {
|
|
6038
|
+
const scoreColor = slide.totalScore >= 30 ? "\u2713" : slide.totalScore >= 20 ? "\u25D0" : "\u2717";
|
|
6039
|
+
lines.push(`\u2502 ${scoreColor} Slide ${slide.slideIndex.toString().padStart(2)} (${slide.slideType.padEnd(18)}): ${slide.totalScore}/40`);
|
|
6040
|
+
lines.push(`\u2502 Visual: ${slide.visualImpact}/10 Clarity: ${slide.contentClarity}/10 Polish: ${slide.professionalPolish}/10 Theme: ${slide.themeCoherence}/10`);
|
|
6041
|
+
}
|
|
6042
|
+
lines.push("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
6043
|
+
lines.push("");
|
|
6044
|
+
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
4962
6045
|
return lines.join("\n");
|
|
4963
6046
|
}
|
|
6047
|
+
scoreBar(score, max) {
|
|
6048
|
+
const percentage = score / max;
|
|
6049
|
+
const filled = Math.round(percentage * 20);
|
|
6050
|
+
const empty = 20 - filled;
|
|
6051
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
6052
|
+
}
|
|
4964
6053
|
};
|
|
4965
|
-
function
|
|
4966
|
-
|
|
6054
|
+
async function evaluatePresentation(htmlPath, screenshotDir) {
|
|
6055
|
+
const evaluator = new VisualQualityEvaluator(screenshotDir);
|
|
6056
|
+
return evaluator.evaluate(htmlPath);
|
|
4967
6057
|
}
|
|
4968
6058
|
|
|
4969
6059
|
// src/generators/html/RevealJsGenerator.ts
|
|
@@ -5257,7 +6347,7 @@ ${slides}
|
|
|
5257
6347
|
.reveal .number {
|
|
5258
6348
|
font-size: 4em;
|
|
5259
6349
|
font-weight: 800;
|
|
5260
|
-
color: var(--color-
|
|
6350
|
+
color: var(--color-primary);
|
|
5261
6351
|
text-align: center;
|
|
5262
6352
|
}
|
|
5263
6353
|
|
|
@@ -5308,12 +6398,12 @@ ${slides}
|
|
|
5308
6398
|
.reveal .metric-value {
|
|
5309
6399
|
font-size: 2em;
|
|
5310
6400
|
font-weight: 700;
|
|
5311
|
-
color: var(--color-
|
|
6401
|
+
color: var(--color-primary);
|
|
5312
6402
|
}
|
|
5313
6403
|
|
|
5314
6404
|
.reveal .metric-label {
|
|
5315
6405
|
font-size: 0.8em;
|
|
5316
|
-
color: var(--color-text
|
|
6406
|
+
color: var(--color-text);
|
|
5317
6407
|
}
|
|
5318
6408
|
|
|
5319
6409
|
.reveal .metric-change {
|
|
@@ -5390,27 +6480,31 @@ ${slides}
|
|
|
5390
6480
|
================================================================ */
|
|
5391
6481
|
|
|
5392
6482
|
/* Title slide: Bold background - makes strong first impression */
|
|
5393
|
-
.
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
.reveal .slide-title
|
|
5399
|
-
.reveal .slide-title
|
|
5400
|
-
|
|
6483
|
+
/* Using .slides section.slide-title for higher specificity than .reveal .slides section */
|
|
6484
|
+
.reveal .slides section.slide-title {
|
|
6485
|
+
background-color: ${titleBg} !important;
|
|
6486
|
+
background-image: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
|
|
6487
|
+
}
|
|
6488
|
+
.reveal .slides section.slide-title h1,
|
|
6489
|
+
.reveal .slides section.slide-title h2,
|
|
6490
|
+
.reveal .slides section.slide-title p,
|
|
6491
|
+
.reveal .slides section.slide-title .subtitle {
|
|
6492
|
+
color: ${isDark ? "#ffffff" : "#ffffff"} !important;
|
|
6493
|
+
-webkit-text-fill-color: ${isDark ? "#ffffff" : "#ffffff"} !important;
|
|
6494
|
+
background: none !important;
|
|
5401
6495
|
}
|
|
5402
6496
|
|
|
5403
6497
|
/* Section dividers: Subtle visual breaks */
|
|
5404
|
-
.reveal .slide-section-divider {
|
|
6498
|
+
.reveal .slides section.slide-section-divider {
|
|
5405
6499
|
background: ${isDark ? this.lightenColor(background, 8) : `linear-gradient(180deg, ${this.lightenColor(primary, 85)} 0%, ${this.lightenColor(primary, 92)} 100%)`};
|
|
5406
6500
|
}
|
|
5407
6501
|
|
|
5408
6502
|
/* Big number slides: Clean with accent highlight - emphasizes the data */
|
|
5409
|
-
.reveal .slide-big-number {
|
|
6503
|
+
.reveal .slides section.slide-big-number {
|
|
5410
6504
|
background: var(--color-background);
|
|
5411
6505
|
border-left: 8px solid var(--color-highlight);
|
|
5412
6506
|
}
|
|
5413
|
-
.reveal .slide-big-number .number {
|
|
6507
|
+
.reveal .slides section.slide-big-number .number {
|
|
5414
6508
|
font-size: 5em;
|
|
5415
6509
|
background: linear-gradient(135deg, var(--color-highlight) 0%, var(--color-accent) 100%);
|
|
5416
6510
|
-webkit-background-clip: text;
|
|
@@ -5419,50 +6513,56 @@ ${slides}
|
|
|
5419
6513
|
}
|
|
5420
6514
|
|
|
5421
6515
|
/* Metrics grid: Light accent background - data-focused */
|
|
5422
|
-
.reveal .slide-metrics-grid {
|
|
6516
|
+
.reveal .slides section.slide-metrics-grid {
|
|
5423
6517
|
background: ${isDark ? this.lightenColor(background, 8) : this.lightenColor(accent, 90)};
|
|
5424
6518
|
}
|
|
5425
|
-
${isDark ? `.reveal .slide-metrics-grid { color: ${text}; }` : ""}
|
|
6519
|
+
${isDark ? `.reveal .slides section.slide-metrics-grid { color: ${text}; }` : ""}
|
|
5426
6520
|
|
|
5427
|
-
/* CTA slide: Highlight color - drives action */
|
|
5428
|
-
.reveal .slide-cta {
|
|
5429
|
-
background:
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
.reveal .slide-cta
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
6521
|
+
/* CTA slide: Highlight color - drives action (darkened for 7:1 contrast with white) */
|
|
6522
|
+
.reveal .slides section.slide-cta {
|
|
6523
|
+
background-color: ${this.darkenColor(highlight, 30)} !important;
|
|
6524
|
+
background-image: linear-gradient(135deg, ${this.darkenColor(highlight, 25)} 0%, ${this.darkenColor(accent, 30)} 100%);
|
|
6525
|
+
}
|
|
6526
|
+
.reveal .slides section.slide-cta h2,
|
|
6527
|
+
.reveal .slides section.slide-cta p {
|
|
6528
|
+
color: #ffffff !important;
|
|
6529
|
+
-webkit-text-fill-color: #ffffff !important;
|
|
6530
|
+
background: none !important;
|
|
6531
|
+
}
|
|
6532
|
+
.reveal .slides section.slide-cta .cta-button {
|
|
5436
6533
|
background: #ffffff;
|
|
5437
6534
|
color: var(--color-highlight);
|
|
5438
6535
|
}
|
|
5439
6536
|
|
|
5440
6537
|
/* Thank you slide: Matches title for bookend effect */
|
|
5441
|
-
.reveal .slide-thank-you {
|
|
5442
|
-
background:
|
|
6538
|
+
.reveal .slides section.slide-thank-you {
|
|
6539
|
+
background-color: ${titleBg} !important;
|
|
6540
|
+
background-image: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
|
|
5443
6541
|
}
|
|
5444
|
-
.reveal .slide-thank-you h2,
|
|
5445
|
-
.reveal .slide-thank-you p,
|
|
5446
|
-
.reveal .slide-thank-you .subtitle {
|
|
5447
|
-
color:
|
|
6542
|
+
.reveal .slides section.slide-thank-you h2,
|
|
6543
|
+
.reveal .slides section.slide-thank-you p,
|
|
6544
|
+
.reveal .slides section.slide-thank-you .subtitle {
|
|
6545
|
+
color: #ffffff !important;
|
|
6546
|
+
-webkit-text-fill-color: #ffffff !important;
|
|
6547
|
+
background: none !important;
|
|
5448
6548
|
}
|
|
5449
6549
|
|
|
5450
6550
|
/* Quote slides: Elegant subtle background */
|
|
5451
|
-
.reveal .slide-quote {
|
|
6551
|
+
.reveal .slides section.slide-quote {
|
|
5452
6552
|
background: ${isDark ? this.lightenColor(background, 5) : this.lightenColor(secondary, 92)};
|
|
5453
6553
|
}
|
|
5454
|
-
.reveal .slide-quote blockquote {
|
|
6554
|
+
.reveal .slides section.slide-quote blockquote {
|
|
5455
6555
|
border-left-color: var(--color-accent);
|
|
5456
6556
|
font-size: 1.3em;
|
|
5457
6557
|
}
|
|
5458
|
-
${isDark ? `.reveal .slide-quote { color: ${text}; }` : ""}
|
|
6558
|
+
${isDark ? `.reveal .slides section.slide-quote { color: ${text}; }` : ""}
|
|
5459
6559
|
|
|
5460
6560
|
/* Single statement: Clean, centered, impactful */
|
|
5461
|
-
.reveal .slide-single-statement {
|
|
6561
|
+
.reveal .slides section.slide-single-statement {
|
|
5462
6562
|
background: var(--color-background);
|
|
5463
6563
|
}
|
|
5464
|
-
.reveal .slide-single-statement .statement,
|
|
5465
|
-
.reveal .slide-single-statement .big-idea-text {
|
|
6564
|
+
.reveal .slides section.slide-single-statement .statement,
|
|
6565
|
+
.reveal .slides section.slide-single-statement .big-idea-text {
|
|
5466
6566
|
font-size: 2.2em;
|
|
5467
6567
|
max-width: 80%;
|
|
5468
6568
|
margin: 0 auto;
|
|
@@ -5479,52 +6579,77 @@ ${slides}
|
|
|
5479
6579
|
padding-right: 30px;
|
|
5480
6580
|
}
|
|
5481
6581
|
.reveal .column-title {
|
|
5482
|
-
font-size: 1.
|
|
6582
|
+
font-size: 1.3em;
|
|
5483
6583
|
font-weight: 600;
|
|
5484
6584
|
margin-bottom: 0.5em;
|
|
5485
|
-
color: var(--color-
|
|
6585
|
+
color: var(--color-primary);
|
|
5486
6586
|
}
|
|
5487
6587
|
.reveal .column-content,
|
|
5488
6588
|
.reveal .column-body {
|
|
5489
6589
|
line-height: 1.6;
|
|
5490
6590
|
}
|
|
5491
6591
|
|
|
5492
|
-
/* Timeline/Process: Visual flow */
|
|
6592
|
+
/* Timeline/Process: Visual flow - ENHANCED */
|
|
6593
|
+
.reveal .slide-timeline .slide-content,
|
|
6594
|
+
.reveal .slide-process .slide-content {
|
|
6595
|
+
justify-content: center;
|
|
6596
|
+
}
|
|
5493
6597
|
.reveal .slide-timeline .steps,
|
|
5494
6598
|
.reveal .slide-timeline .timeline,
|
|
5495
6599
|
.reveal .slide-process .steps,
|
|
5496
6600
|
.reveal .slide-process .process-steps {
|
|
5497
6601
|
display: flex;
|
|
5498
|
-
gap:
|
|
6602
|
+
gap: 24px;
|
|
5499
6603
|
flex-wrap: wrap;
|
|
5500
6604
|
justify-content: center;
|
|
6605
|
+
align-items: stretch;
|
|
6606
|
+
margin-top: 30px;
|
|
6607
|
+
padding: 0 20px;
|
|
5501
6608
|
}
|
|
5502
6609
|
.reveal .slide-timeline .step,
|
|
5503
6610
|
.reveal .slide-timeline .timeline-item,
|
|
5504
6611
|
.reveal .slide-process .step,
|
|
5505
6612
|
.reveal .slide-process .process-step {
|
|
5506
|
-
flex: 1;
|
|
5507
|
-
min-width:
|
|
5508
|
-
max-width:
|
|
5509
|
-
padding:
|
|
5510
|
-
background: ${isDark ? this.lightenColor(background, 10) :
|
|
5511
|
-
border-radius:
|
|
5512
|
-
border-top:
|
|
6613
|
+
flex: 1 1 200px;
|
|
6614
|
+
min-width: 180px;
|
|
6615
|
+
max-width: 280px;
|
|
6616
|
+
padding: 28px 24px;
|
|
6617
|
+
background: ${isDark ? this.lightenColor(background, 10) : "#ffffff"};
|
|
6618
|
+
border-radius: 12px;
|
|
6619
|
+
border-top: 5px solid var(--color-accent);
|
|
6620
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
5513
6621
|
${isDark ? `color: ${text};` : ""}
|
|
6622
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
6623
|
+
}
|
|
6624
|
+
.reveal .slide-timeline .step:hover,
|
|
6625
|
+
.reveal .slide-timeline .timeline-item:hover,
|
|
6626
|
+
.reveal .slide-process .step:hover,
|
|
6627
|
+
.reveal .slide-process .process-step:hover {
|
|
6628
|
+
transform: translateY(-4px);
|
|
6629
|
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
|
5514
6630
|
}
|
|
5515
6631
|
.reveal .step-number {
|
|
5516
|
-
|
|
6632
|
+
display: inline-flex;
|
|
6633
|
+
align-items: center;
|
|
6634
|
+
justify-content: center;
|
|
6635
|
+
width: 40px;
|
|
6636
|
+
height: 40px;
|
|
6637
|
+
background: var(--color-accent);
|
|
6638
|
+
color: #ffffff;
|
|
6639
|
+
border-radius: 50%;
|
|
6640
|
+
font-size: 1.2em;
|
|
5517
6641
|
font-weight: 700;
|
|
5518
|
-
|
|
5519
|
-
margin-bottom: 0.5em;
|
|
6642
|
+
margin-bottom: 16px;
|
|
5520
6643
|
}
|
|
5521
6644
|
.reveal .step-title {
|
|
5522
|
-
font-size: 1.
|
|
6645
|
+
font-size: 1.3em;
|
|
5523
6646
|
font-weight: 600;
|
|
5524
|
-
margin-bottom:
|
|
6647
|
+
margin-bottom: 8px;
|
|
6648
|
+
color: var(--color-primary);
|
|
5525
6649
|
}
|
|
5526
6650
|
.reveal .step-desc {
|
|
5527
|
-
font-size: 0.
|
|
6651
|
+
font-size: 0.95em;
|
|
6652
|
+
line-height: 1.5;
|
|
5528
6653
|
color: var(--color-text-light);
|
|
5529
6654
|
}
|
|
5530
6655
|
.reveal .step-arrow {
|
|
@@ -5534,6 +6659,54 @@ ${slides}
|
|
|
5534
6659
|
color: var(--color-accent);
|
|
5535
6660
|
}
|
|
5536
6661
|
|
|
6662
|
+
/* Improved slide layout - PROFESSIONAL */
|
|
6663
|
+
.reveal .slides section {
|
|
6664
|
+
overflow: hidden;
|
|
6665
|
+
}
|
|
6666
|
+
.reveal .slides section .slide-content {
|
|
6667
|
+
overflow-y: auto;
|
|
6668
|
+
overflow-x: hidden;
|
|
6669
|
+
max-height: calc(100vh - 120px);
|
|
6670
|
+
}
|
|
6671
|
+
|
|
6672
|
+
/* Three column professional styling */
|
|
6673
|
+
.reveal .three-columns .column {
|
|
6674
|
+
padding: 24px;
|
|
6675
|
+
background: ${isDark ? this.lightenColor(background, 8) : "#f8f9fa"};
|
|
6676
|
+
border-radius: 12px;
|
|
6677
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
6678
|
+
}
|
|
6679
|
+
.reveal .three-columns .column-title {
|
|
6680
|
+
font-size: 1.3em;
|
|
6681
|
+
color: var(--color-primary);
|
|
6682
|
+
border-bottom: 2px solid var(--color-accent);
|
|
6683
|
+
padding-bottom: 8px;
|
|
6684
|
+
margin-bottom: 12px;
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6687
|
+
/* Bullets professional styling */
|
|
6688
|
+
.reveal .bullets {
|
|
6689
|
+
list-style: none;
|
|
6690
|
+
margin: 0;
|
|
6691
|
+
padding: 0;
|
|
6692
|
+
}
|
|
6693
|
+
.reveal .bullets li {
|
|
6694
|
+
position: relative;
|
|
6695
|
+
padding-left: 28px;
|
|
6696
|
+
margin-bottom: 16px;
|
|
6697
|
+
line-height: 1.5;
|
|
6698
|
+
}
|
|
6699
|
+
.reveal .bullets li::before {
|
|
6700
|
+
content: "";
|
|
6701
|
+
position: absolute;
|
|
6702
|
+
left: 0;
|
|
6703
|
+
top: 8px;
|
|
6704
|
+
width: 8px;
|
|
6705
|
+
height: 8px;
|
|
6706
|
+
background: var(--color-accent);
|
|
6707
|
+
border-radius: 50%;
|
|
6708
|
+
}
|
|
6709
|
+
|
|
5537
6710
|
/* Progress bar enhancement */
|
|
5538
6711
|
.reveal .progress {
|
|
5539
6712
|
background: ${this.lightenColor(primary, 80)};
|
|
@@ -5769,182 +6942,6 @@ ${slides}
|
|
|
5769
6942
|
}
|
|
5770
6943
|
};
|
|
5771
6944
|
|
|
5772
|
-
// src/qa/IterativeQAEngine.ts
|
|
5773
|
-
var DEFAULT_OPTIONS = {
|
|
5774
|
-
minScore: 95,
|
|
5775
|
-
maxIterations: 5,
|
|
5776
|
-
verbose: true
|
|
5777
|
-
};
|
|
5778
|
-
var IterativeQAEngine = class {
|
|
5779
|
-
kb;
|
|
5780
|
-
scorer;
|
|
5781
|
-
fixer;
|
|
5782
|
-
generator;
|
|
5783
|
-
mode;
|
|
5784
|
-
presentationType;
|
|
5785
|
-
config;
|
|
5786
|
-
constructor(mode, presentationType, config) {
|
|
5787
|
-
this.mode = mode;
|
|
5788
|
-
this.presentationType = presentationType;
|
|
5789
|
-
this.config = config;
|
|
5790
|
-
this.scorer = new SevenDimensionScorer(mode, presentationType);
|
|
5791
|
-
this.fixer = new AutoFixEngine(mode, presentationType);
|
|
5792
|
-
this.generator = new RevealJsGenerator();
|
|
5793
|
-
}
|
|
5794
|
-
/**
|
|
5795
|
-
* Run the iterative QA process.
|
|
5796
|
-
*/
|
|
5797
|
-
async run(initialSlides, initialHtml, options = {}) {
|
|
5798
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
5799
|
-
this.kb = await getKnowledgeGateway();
|
|
5800
|
-
const iterations = [];
|
|
5801
|
-
let currentSlides = initialSlides;
|
|
5802
|
-
let currentHtml = initialHtml;
|
|
5803
|
-
let scoringResult;
|
|
5804
|
-
let autoFixSummary = "";
|
|
5805
|
-
let totalFixesApplied = 0;
|
|
5806
|
-
if (opts.verbose) {
|
|
5807
|
-
logger.progress("\n\u{1F504} Starting Iterative QA Process");
|
|
5808
|
-
logger.info(` Target Score: ${opts.minScore}/100`);
|
|
5809
|
-
logger.info(` Max Iterations: ${opts.maxIterations}`);
|
|
5810
|
-
logger.progress("");
|
|
5811
|
-
}
|
|
5812
|
-
for (let i = 0; i < opts.maxIterations; i++) {
|
|
5813
|
-
const iterationNum = i + 1;
|
|
5814
|
-
if (opts.verbose) {
|
|
5815
|
-
logger.progress(`\u{1F4CA} Iteration ${iterationNum}/${opts.maxIterations}...`);
|
|
5816
|
-
}
|
|
5817
|
-
scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
|
|
5818
|
-
iterations.push({
|
|
5819
|
-
iteration: iterationNum,
|
|
5820
|
-
score: scoringResult.overallScore,
|
|
5821
|
-
dimensionScores: {
|
|
5822
|
-
layout: scoringResult.dimensions.layout.score,
|
|
5823
|
-
contrast: scoringResult.dimensions.contrast.score,
|
|
5824
|
-
graphics: scoringResult.dimensions.graphics.score,
|
|
5825
|
-
content: scoringResult.dimensions.content.score,
|
|
5826
|
-
clarity: scoringResult.dimensions.clarity.score,
|
|
5827
|
-
effectiveness: scoringResult.dimensions.effectiveness.score,
|
|
5828
|
-
consistency: scoringResult.dimensions.consistency.score
|
|
5829
|
-
},
|
|
5830
|
-
fixesApplied: 0,
|
|
5831
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
5832
|
-
});
|
|
5833
|
-
if (opts.verbose) {
|
|
5834
|
-
logger.info(` Score: ${scoringResult.overallScore}/100`);
|
|
5835
|
-
}
|
|
5836
|
-
if (scoringResult.passed) {
|
|
5837
|
-
if (opts.verbose) {
|
|
5838
|
-
logger.success(`PASSED - Score meets threshold (${opts.minScore})`);
|
|
5839
|
-
}
|
|
5840
|
-
break;
|
|
5841
|
-
}
|
|
5842
|
-
if (i < opts.maxIterations - 1) {
|
|
5843
|
-
if (opts.verbose) {
|
|
5844
|
-
logger.warn("Below threshold, applying auto-fixes...");
|
|
5845
|
-
}
|
|
5846
|
-
const fixResult = await this.fixer.fix(currentSlides, scoringResult);
|
|
5847
|
-
if (fixResult.fixesApplied.length > 0) {
|
|
5848
|
-
currentSlides = fixResult.slidesFixed;
|
|
5849
|
-
currentHtml = await this.generator.generate(currentSlides, this.config);
|
|
5850
|
-
const lastIteration = iterations[iterations.length - 1];
|
|
5851
|
-
if (lastIteration) {
|
|
5852
|
-
lastIteration.fixesApplied = fixResult.fixesApplied.length;
|
|
5853
|
-
}
|
|
5854
|
-
totalFixesApplied += fixResult.fixesApplied.length;
|
|
5855
|
-
if (opts.verbose) {
|
|
5856
|
-
logger.info(` \u{1F527} Applied ${fixResult.fixesApplied.length} fixes`);
|
|
5857
|
-
}
|
|
5858
|
-
autoFixSummary += fixResult.summary + "\n";
|
|
5859
|
-
} else {
|
|
5860
|
-
if (opts.verbose) {
|
|
5861
|
-
logger.warn("No auto-fixes available, manual review needed");
|
|
5862
|
-
}
|
|
5863
|
-
break;
|
|
5864
|
-
}
|
|
5865
|
-
}
|
|
5866
|
-
}
|
|
5867
|
-
if (!scoringResult.passed) {
|
|
5868
|
-
scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
|
|
5869
|
-
}
|
|
5870
|
-
const report = this.generateReport(
|
|
5871
|
-
scoringResult,
|
|
5872
|
-
iterations,
|
|
5873
|
-
opts,
|
|
5874
|
-
totalFixesApplied
|
|
5875
|
-
);
|
|
5876
|
-
if (opts.verbose) {
|
|
5877
|
-
logger.info(report);
|
|
5878
|
-
}
|
|
5879
|
-
return {
|
|
5880
|
-
finalScore: scoringResult.overallScore,
|
|
5881
|
-
passed: scoringResult.passed,
|
|
5882
|
-
threshold: opts.minScore,
|
|
5883
|
-
iterations,
|
|
5884
|
-
totalIterations: iterations.length,
|
|
5885
|
-
maxIterations: opts.maxIterations,
|
|
5886
|
-
slides: currentSlides,
|
|
5887
|
-
html: currentHtml,
|
|
5888
|
-
finalScoring: scoringResult,
|
|
5889
|
-
autoFixSummary,
|
|
5890
|
-
report
|
|
5891
|
-
};
|
|
5892
|
-
}
|
|
5893
|
-
/**
|
|
5894
|
-
* Generate a comprehensive report.
|
|
5895
|
-
*/
|
|
5896
|
-
generateReport(finalScoring, iterations, options, totalFixesApplied) {
|
|
5897
|
-
const lines = [];
|
|
5898
|
-
lines.push("");
|
|
5899
|
-
lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
5900
|
-
lines.push("\u2551 ITERATIVE QA FINAL REPORT \u2551");
|
|
5901
|
-
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
5902
|
-
lines.push("");
|
|
5903
|
-
const passStatus = finalScoring.passed ? "\u2705 PASSED" : "\u274C FAILED";
|
|
5904
|
-
lines.push(`Final Score: ${finalScoring.overallScore}/100 ${passStatus}`);
|
|
5905
|
-
lines.push(`Threshold: ${options.minScore}/100`);
|
|
5906
|
-
lines.push(`Iterations: ${iterations.length}/${options.maxIterations}`);
|
|
5907
|
-
lines.push(`Total Fixes Applied: ${totalFixesApplied}`);
|
|
5908
|
-
lines.push("");
|
|
5909
|
-
if (iterations.length > 1) {
|
|
5910
|
-
lines.push("Score Progression:");
|
|
5911
|
-
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5912
|
-
for (const iter of iterations) {
|
|
5913
|
-
const bar = "\u2588".repeat(Math.floor(iter.score / 10)) + "\u2591".repeat(10 - Math.floor(iter.score / 10));
|
|
5914
|
-
lines.push(` Iter ${iter.iteration}: ${bar} ${iter.score}/100 (+${iter.fixesApplied} fixes)`);
|
|
5915
|
-
}
|
|
5916
|
-
lines.push("");
|
|
5917
|
-
}
|
|
5918
|
-
lines.push(this.scorer.formatReport(finalScoring));
|
|
5919
|
-
lines.push("");
|
|
5920
|
-
lines.push("\u{1F4DA} Knowledge Base Compliance:");
|
|
5921
|
-
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5922
|
-
lines.push(` Mode: ${this.mode}`);
|
|
5923
|
-
lines.push(` Presentation Type: ${this.presentationType}`);
|
|
5924
|
-
lines.push(` Word Limits: ${this.mode === "keynote" ? "6-25" : "40-80"} per slide`);
|
|
5925
|
-
lines.push(` Expert Frameworks: Duarte, Reynolds, Gallo, Anderson`);
|
|
5926
|
-
lines.push("");
|
|
5927
|
-
if (!finalScoring.passed) {
|
|
5928
|
-
lines.push("\u{1F4CB} Recommendations for Manual Review:");
|
|
5929
|
-
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5930
|
-
const manualIssues = finalScoring.issues.filter((i) => !i.autoFixable);
|
|
5931
|
-
for (const issue of manualIssues.slice(0, 5)) {
|
|
5932
|
-
lines.push(` \u2022 ${issue.message}`);
|
|
5933
|
-
if (issue.fixSuggestion) {
|
|
5934
|
-
lines.push(` \u2192 ${issue.fixSuggestion}`);
|
|
5935
|
-
}
|
|
5936
|
-
}
|
|
5937
|
-
if (manualIssues.length > 5) {
|
|
5938
|
-
lines.push(` ... and ${manualIssues.length - 5} more issues`);
|
|
5939
|
-
}
|
|
5940
|
-
}
|
|
5941
|
-
return lines.join("\n");
|
|
5942
|
-
}
|
|
5943
|
-
};
|
|
5944
|
-
function createIterativeQAEngine(mode, presentationType, config) {
|
|
5945
|
-
return new IterativeQAEngine(mode, presentationType, config);
|
|
5946
|
-
}
|
|
5947
|
-
|
|
5948
6945
|
// src/generators/pptx/PowerPointGenerator.ts
|
|
5949
6946
|
var import_pptxgenjs = __toESM(require("pptxgenjs"));
|
|
5950
6947
|
|
|
@@ -6714,11 +7711,11 @@ var PDFGenerator = class {
|
|
|
6714
7711
|
waitUntil: ["networkidle0", "domcontentloaded"]
|
|
6715
7712
|
});
|
|
6716
7713
|
await page.evaluate(() => {
|
|
6717
|
-
return new Promise((
|
|
7714
|
+
return new Promise((resolve2) => {
|
|
6718
7715
|
if (document.fonts && document.fonts.ready) {
|
|
6719
|
-
document.fonts.ready.then(() =>
|
|
7716
|
+
document.fonts.ready.then(() => resolve2());
|
|
6720
7717
|
} else {
|
|
6721
|
-
setTimeout(
|
|
7718
|
+
setTimeout(resolve2, 1e3);
|
|
6722
7719
|
}
|
|
6723
7720
|
});
|
|
6724
7721
|
});
|
|
@@ -6898,46 +7895,38 @@ var PresentationEngine = class {
|
|
|
6898
7895
|
}
|
|
6899
7896
|
let qaResults;
|
|
6900
7897
|
let score = 100;
|
|
6901
|
-
let
|
|
6902
|
-
|
|
6903
|
-
|
|
7898
|
+
let visualQAResult = null;
|
|
7899
|
+
const finalSlides = slides;
|
|
7900
|
+
const finalHtml = outputs.html;
|
|
6904
7901
|
if (!config.skipQA && outputs.html) {
|
|
6905
|
-
const threshold = config.qaThreshold ??
|
|
6906
|
-
|
|
6907
|
-
const
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
);
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
score = iterativeResult.finalScore;
|
|
6921
|
-
finalSlides = iterativeResult.slides;
|
|
6922
|
-
finalHtml = iterativeResult.html;
|
|
6923
|
-
if (outputs.html) {
|
|
6924
|
-
outputs.html = finalHtml;
|
|
6925
|
-
}
|
|
6926
|
-
qaResults = this.buildQAResultsFrom7Dimension(iterativeResult);
|
|
6927
|
-
if (!iterativeResult.passed) {
|
|
6928
|
-
throw new QAFailureError(score, threshold, qaResults);
|
|
6929
|
-
}
|
|
6930
|
-
} else {
|
|
6931
|
-
logger.progress("\u{1F50D} Running QA validation (legacy mode)...");
|
|
6932
|
-
qaResults = await this.qaEngine.validate(outputs.html, {
|
|
6933
|
-
mode: config.mode,
|
|
6934
|
-
strictMode: true
|
|
6935
|
-
});
|
|
6936
|
-
score = this.scoreCalculator.calculate(qaResults);
|
|
6937
|
-
logger.info(`\u{1F4CA} QA Score: ${score}/100`);
|
|
7902
|
+
const threshold = config.qaThreshold ?? 80;
|
|
7903
|
+
logger.progress("\u{1F50D} Running Visual Quality Evaluation (Playwright)...");
|
|
7904
|
+
const tempDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "presentation-qa-"));
|
|
7905
|
+
const tempHtmlPath = path2.join(tempDir, "presentation.html");
|
|
7906
|
+
fs2.writeFileSync(tempHtmlPath, outputs.html);
|
|
7907
|
+
try {
|
|
7908
|
+
const visualEvaluator = new VisualQualityEvaluator(path2.join(tempDir, "screenshots"));
|
|
7909
|
+
visualQAResult = await visualEvaluator.evaluate(tempHtmlPath);
|
|
7910
|
+
score = visualQAResult.overallScore;
|
|
7911
|
+
logger.info(`\u{1F4CA} Visual QA Score: ${score}/100 (${visualQAResult.verdict.toUpperCase()})`);
|
|
7912
|
+
logger.info(` Narrative Flow: ${visualQAResult.narrativeFlow.score}/25`);
|
|
7913
|
+
logger.info(` Visual Consistency: ${visualQAResult.visualConsistency.score}/25`);
|
|
7914
|
+
logger.info(` Content Quality: ${visualQAResult.contentQuality.score}/25`);
|
|
7915
|
+
logger.info(` Executive Readiness: ${visualQAResult.executiveReadiness.score}/25`);
|
|
7916
|
+
qaResults = this.buildQAResultsFromVisual(visualQAResult);
|
|
6938
7917
|
if (score < threshold) {
|
|
7918
|
+
logger.warn(`\u26A0\uFE0F Score ${score} below threshold ${threshold}`);
|
|
7919
|
+
if (visualQAResult.topIssues.length > 0) {
|
|
7920
|
+
logger.warn("Top issues:");
|
|
7921
|
+
visualQAResult.topIssues.forEach((issue) => logger.warn(` \u2022 ${issue}`));
|
|
7922
|
+
}
|
|
6939
7923
|
throw new QAFailureError(score, threshold, qaResults);
|
|
6940
7924
|
}
|
|
7925
|
+
} finally {
|
|
7926
|
+
try {
|
|
7927
|
+
fs2.rmSync(tempDir, { recursive: true, force: true });
|
|
7928
|
+
} catch {
|
|
7929
|
+
}
|
|
6941
7930
|
}
|
|
6942
7931
|
} else {
|
|
6943
7932
|
qaResults = this.qaEngine.createEmptyResults();
|
|
@@ -6953,7 +7942,7 @@ var PresentationEngine = class {
|
|
|
6953
7942
|
logger.warn(`PDF generation failed (non-critical): ${errorMsg}`);
|
|
6954
7943
|
}
|
|
6955
7944
|
}
|
|
6956
|
-
const metadata = this.buildMetadata(config, analysis, finalSlides,
|
|
7945
|
+
const metadata = this.buildMetadata(config, analysis, finalSlides, visualQAResult);
|
|
6957
7946
|
return {
|
|
6958
7947
|
outputs,
|
|
6959
7948
|
qaResults,
|
|
@@ -6962,43 +7951,57 @@ var PresentationEngine = class {
|
|
|
6962
7951
|
};
|
|
6963
7952
|
}
|
|
6964
7953
|
/**
|
|
6965
|
-
* Build QA results structure from
|
|
7954
|
+
* Build QA results structure from visual quality evaluation.
|
|
6966
7955
|
*/
|
|
6967
|
-
|
|
6968
|
-
const
|
|
7956
|
+
buildQAResultsFromVisual(visualResult) {
|
|
7957
|
+
const passed = visualResult.overallScore >= 80;
|
|
6969
7958
|
return {
|
|
6970
|
-
passed
|
|
6971
|
-
score:
|
|
7959
|
+
passed,
|
|
7960
|
+
score: visualResult.overallScore,
|
|
6972
7961
|
visual: {
|
|
6973
|
-
whitespacePercentage:
|
|
6974
|
-
|
|
6975
|
-
|
|
7962
|
+
whitespacePercentage: visualResult.visualConsistency.score * 4,
|
|
7963
|
+
// Scale 25 to 100
|
|
7964
|
+
layoutBalance: visualResult.visualConsistency.professionalLook ? 1 : 0.7,
|
|
7965
|
+
colorContrast: visualResult.visualConsistency.colorPaletteConsistent ? 0.95 : 0.8
|
|
6976
7966
|
},
|
|
6977
7967
|
content: {
|
|
6978
|
-
perSlide:
|
|
6979
|
-
|
|
7968
|
+
perSlide: visualResult.slideScores.map((slide) => ({
|
|
7969
|
+
slideIndex: slide.slideIndex,
|
|
7970
|
+
wordCount: 0,
|
|
7971
|
+
// Not tracked by visual QA
|
|
7972
|
+
bulletCount: 0,
|
|
7973
|
+
withinLimit: true,
|
|
7974
|
+
hasActionTitle: slide.contentClarity >= 7,
|
|
7975
|
+
issues: slide.contentClarity < 7 ? [slide.contentClarityNotes] : []
|
|
7976
|
+
})),
|
|
7977
|
+
issues: visualResult.topIssues.map((issue) => ({
|
|
7978
|
+
severity: "warning",
|
|
7979
|
+
message: issue,
|
|
7980
|
+
dimension: "content"
|
|
7981
|
+
}))
|
|
6980
7982
|
},
|
|
6981
7983
|
accessibility: {
|
|
6982
|
-
wcagLevel:
|
|
6983
|
-
issues:
|
|
7984
|
+
wcagLevel: visualResult.visualConsistency.professionalLook ? "AA" : "A",
|
|
7985
|
+
issues: []
|
|
6984
7986
|
},
|
|
6985
|
-
issues:
|
|
6986
|
-
severity:
|
|
6987
|
-
message: issue
|
|
6988
|
-
slideIndex:
|
|
6989
|
-
dimension:
|
|
7987
|
+
issues: visualResult.topIssues.map((issue, i) => ({
|
|
7988
|
+
severity: "warning",
|
|
7989
|
+
message: issue,
|
|
7990
|
+
slideIndex: i,
|
|
7991
|
+
dimension: "visual"
|
|
6990
7992
|
})),
|
|
6991
7993
|
dimensions: {
|
|
6992
|
-
layout:
|
|
6993
|
-
contrast:
|
|
6994
|
-
graphics:
|
|
6995
|
-
content:
|
|
6996
|
-
clarity:
|
|
6997
|
-
effectiveness:
|
|
6998
|
-
consistency:
|
|
7994
|
+
layout: visualResult.narrativeFlow.score * 4,
|
|
7995
|
+
contrast: visualResult.visualConsistency.colorPaletteConsistent ? 100 : 80,
|
|
7996
|
+
graphics: visualResult.executiveReadiness.score * 4,
|
|
7997
|
+
content: visualResult.contentQuality.score * 4,
|
|
7998
|
+
clarity: visualResult.contentQuality.messagesAreClear ? 100 : 75,
|
|
7999
|
+
effectiveness: visualResult.executiveReadiness.wouldImpress ? 100 : 60,
|
|
8000
|
+
consistency: visualResult.visualConsistency.score * 4
|
|
6999
8001
|
},
|
|
7000
|
-
iterations:
|
|
7001
|
-
|
|
8002
|
+
iterations: [],
|
|
8003
|
+
// Visual QA is single-pass, no iteration history
|
|
8004
|
+
report: visualResult.verdictExplanation
|
|
7002
8005
|
};
|
|
7003
8006
|
}
|
|
7004
8007
|
/**
|
|
@@ -7084,7 +8087,7 @@ var PresentationEngine = class {
|
|
|
7084
8087
|
/**
|
|
7085
8088
|
* Build presentation metadata.
|
|
7086
8089
|
*/
|
|
7087
|
-
buildMetadata(config, analysis, slides,
|
|
8090
|
+
buildMetadata(config, analysis, slides, visualResult) {
|
|
7088
8091
|
const wordCounts = slides.map((s) => this.countWords(s));
|
|
7089
8092
|
const totalWords = wordCounts.reduce((sum, count) => sum + count, 0);
|
|
7090
8093
|
const avgWordsPerSlide = Math.round(totalWords / slides.length);
|
|
@@ -7102,10 +8105,9 @@ var PresentationEngine = class {
|
|
|
7102
8105
|
frameworks: this.detectFrameworks(analysis),
|
|
7103
8106
|
presentationType: config.presentationType || analysis.detectedType
|
|
7104
8107
|
};
|
|
7105
|
-
if (
|
|
7106
|
-
metadata.qaIterations =
|
|
7107
|
-
metadata.qaMaxIterations =
|
|
7108
|
-
metadata.dimensionScores = iterativeResult.finalScoring.dimensions;
|
|
8108
|
+
if (visualResult) {
|
|
8109
|
+
metadata.qaIterations = 1;
|
|
8110
|
+
metadata.qaMaxIterations = 1;
|
|
7109
8111
|
}
|
|
7110
8112
|
return metadata;
|
|
7111
8113
|
}
|
|
@@ -7347,7 +8349,7 @@ var UnsplashImageProvider = class {
|
|
|
7347
8349
|
};
|
|
7348
8350
|
}
|
|
7349
8351
|
delay(ms) {
|
|
7350
|
-
return new Promise((
|
|
8352
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7351
8353
|
}
|
|
7352
8354
|
};
|
|
7353
8355
|
var CompositeImageProvider = class {
|
|
@@ -7398,16 +8400,11 @@ async function generate(config) {
|
|
|
7398
8400
|
const engine = new PresentationEngine();
|
|
7399
8401
|
return engine.generate(config);
|
|
7400
8402
|
}
|
|
7401
|
-
async function validate(
|
|
7402
|
-
const
|
|
7403
|
-
|
|
7404
|
-
const score = qaEngine.calculateScore(results);
|
|
7405
|
-
return {
|
|
7406
|
-
...results,
|
|
7407
|
-
score
|
|
7408
|
-
};
|
|
8403
|
+
async function validate(htmlPath, options) {
|
|
8404
|
+
const evaluator = new VisualQualityEvaluator(options?.screenshotDir);
|
|
8405
|
+
return evaluator.evaluate(htmlPath);
|
|
7409
8406
|
}
|
|
7410
|
-
var VERSION = "
|
|
8407
|
+
var VERSION = "9.0.0";
|
|
7411
8408
|
var index_default = {
|
|
7412
8409
|
generate,
|
|
7413
8410
|
validate,
|
|
@@ -7417,13 +8414,11 @@ var index_default = {
|
|
|
7417
8414
|
};
|
|
7418
8415
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7419
8416
|
0 && (module.exports = {
|
|
7420
|
-
AutoFixEngine,
|
|
7421
8417
|
ChartJsProvider,
|
|
7422
8418
|
CompositeChartProvider,
|
|
7423
8419
|
CompositeImageProvider,
|
|
7424
8420
|
ContentAnalyzer,
|
|
7425
8421
|
ContentPatternClassifier,
|
|
7426
|
-
IterativeQAEngine,
|
|
7427
8422
|
KnowledgeGateway,
|
|
7428
8423
|
LocalImageProvider,
|
|
7429
8424
|
MermaidProvider,
|
|
@@ -7435,7 +8430,6 @@ var index_default = {
|
|
|
7435
8430
|
QuickChartProvider,
|
|
7436
8431
|
RevealJsGenerator,
|
|
7437
8432
|
ScoreCalculator,
|
|
7438
|
-
SevenDimensionScorer,
|
|
7439
8433
|
SlideFactory,
|
|
7440
8434
|
SlideGenerator,
|
|
7441
8435
|
TemplateEngine,
|
|
@@ -7443,11 +8437,11 @@ var index_default = {
|
|
|
7443
8437
|
UnsplashImageProvider,
|
|
7444
8438
|
VERSION,
|
|
7445
8439
|
ValidationError,
|
|
7446
|
-
|
|
8440
|
+
VisualQualityEvaluator,
|
|
7447
8441
|
createDefaultChartProvider,
|
|
7448
8442
|
createDefaultImageProvider,
|
|
7449
|
-
createIterativeQAEngine,
|
|
7450
8443
|
createSlideFactory,
|
|
8444
|
+
evaluatePresentation,
|
|
7451
8445
|
generate,
|
|
7452
8446
|
getKnowledgeGateway,
|
|
7453
8447
|
initSlideGenerator,
|