claude-presentation-master 7.4.0 → 8.0.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/dist/index.mjs CHANGED
@@ -26,6 +26,11 @@ var TemplateNotFoundError = class extends Error {
26
26
  }
27
27
  };
28
28
 
29
+ // src/core/PresentationEngine.ts
30
+ import * as fs2 from "fs";
31
+ import * as path2 from "path";
32
+ import * as os from "os";
33
+
29
34
  // src/kb/KnowledgeGateway.ts
30
35
  import { readFileSync } from "fs";
31
36
  import { join, dirname } from "path";
@@ -128,12 +133,12 @@ var KnowledgeGateway = class {
128
133
  join(process.cwd(), "assets/presentation-knowledge.yaml"),
129
134
  join(process.cwd(), "node_modules/claude-presentation-master/assets/presentation-knowledge.yaml")
130
135
  ];
131
- for (const path of possiblePaths) {
136
+ for (const path3 of possiblePaths) {
132
137
  try {
133
- const content = readFileSync(path, "utf-8");
138
+ const content = readFileSync(path3, "utf-8");
134
139
  this.kb = yaml.parse(content);
135
140
  this.loaded = true;
136
- logger.step(`Knowledge base loaded from ${path}`);
141
+ logger.step(`Knowledge base loaded from ${path3}`);
137
142
  return;
138
143
  } catch {
139
144
  }
@@ -158,33 +163,54 @@ var KnowledgeGateway = class {
158
163
  return "keynote";
159
164
  }
160
165
  /**
161
- * Get word limits for the specified mode
166
+ * Get word limits for the specified mode.
167
+ * READS FROM KB - NO HARDCODED VALUES.
162
168
  */
163
169
  getWordLimits(mode) {
170
+ this.ensureLoaded();
164
171
  const modeConfig = this.getMode(mode);
165
- const range = modeConfig.characteristics.words_per_slide;
166
- if (mode === "keynote") {
167
- return { min: 6, max: 15, ideal: 10 };
168
- } else {
169
- return { min: 40, max: 80, ideal: 60 };
172
+ const rangeStr = modeConfig.characteristics.words_per_slide;
173
+ const match = rangeStr.match(/(\d+)-(\d+)/);
174
+ if (!match || !match[1] || !match[2]) {
175
+ throw new Error(`[KB ERROR] Invalid words_per_slide format in KB for ${mode}: "${rangeStr}". Expected "min-max" format.`);
170
176
  }
177
+ const min = parseInt(match[1], 10);
178
+ const max = parseInt(match[2], 10);
179
+ const ideal = Math.round((min + max) / 2);
180
+ logger.debug(`[KB] getWordLimits(${mode}): min=${min}, max=${max}, ideal=${ideal} (from KB: "${rangeStr}")`);
181
+ return { min, max, ideal };
171
182
  }
172
183
  /**
173
- * Get bullet point limits
184
+ * Get bullet point limits.
185
+ * READS FROM KB - NO HARDCODED VALUES.
174
186
  */
175
187
  getBulletLimits(mode) {
176
- if (mode === "keynote") {
177
- return { maxBullets: 3, maxWordsPerBullet: 8 };
178
- } else {
179
- return { maxBullets: 5, maxWordsPerBullet: 20 };
188
+ this.ensureLoaded();
189
+ const type = mode === "keynote" ? "ted_keynote" : "consulting_deck";
190
+ const typeConfig = this.kb.presentation_types?.[type];
191
+ if (typeConfig?.validation_rules?.bullets_per_slide) {
192
+ const maxBullets = typeConfig.validation_rules.bullets_per_slide.max;
193
+ const wordLimits = this.getWordLimits(mode);
194
+ const maxWordsPerBullet = maxBullets > 0 ? Math.floor(wordLimits.max / maxBullets) : 0;
195
+ logger.debug(`[KB] getBulletLimits(${mode}): maxBullets=${maxBullets}, maxWordsPerBullet=${maxWordsPerBullet} (from KB presentation_types.${type})`);
196
+ return { maxBullets, maxWordsPerBullet };
180
197
  }
198
+ throw new Error(`[KB ERROR] Missing bullets_per_slide in KB for presentation_types.${type}.validation_rules`);
181
199
  }
182
200
  /**
183
- * Get whitespace percentage requirement
201
+ * Get whitespace percentage requirement.
202
+ * READS FROM KB - NO HARDCODED VALUES.
184
203
  */
185
204
  getWhitespaceRequirement(mode) {
186
- if (mode === "keynote") return 0.4;
187
- return 0.25;
205
+ this.ensureLoaded();
206
+ const type = mode === "keynote" ? "ted_keynote" : "consulting_deck";
207
+ const typeConfig = this.kb.presentation_types?.[type];
208
+ if (typeConfig?.validation_rules?.whitespace?.min) {
209
+ const whitespace = typeConfig.validation_rules.whitespace.min / 100;
210
+ logger.debug(`[KB] getWhitespaceRequirement(${mode}): ${whitespace} (from KB presentation_types.${type})`);
211
+ return whitespace;
212
+ }
213
+ throw new Error(`[KB ERROR] Missing whitespace.min in KB for presentation_types.${type}.validation_rules`);
188
214
  }
189
215
  /**
190
216
  * Get slide templates for the specified mode
@@ -295,29 +321,54 @@ var KnowledgeGateway = class {
295
321
  return null;
296
322
  }
297
323
  /**
298
- * Get Duarte's glance test requirements
324
+ * Get Duarte's glance test requirements.
325
+ * READS FROM KB - NO HARDCODED VALUES.
299
326
  */
300
327
  getDuarteGlanceTest() {
301
328
  this.ensureLoaded();
302
- const duarte = this.kb.experts.nancy_duarte;
303
- return {
304
- wordLimit: duarte.core_principles.glance_test.word_limit || 25,
305
- timeSeconds: 3
306
- };
329
+ const duarte = this.kb.experts?.nancy_duarte;
330
+ if (!duarte?.core_principles?.glance_test?.word_limit) {
331
+ throw new Error("[KB ERROR] Missing experts.nancy_duarte.core_principles.glance_test.word_limit in KB");
332
+ }
333
+ if (!duarte?.core_principles?.glance_test?.time_limit) {
334
+ throw new Error("[KB ERROR] Missing experts.nancy_duarte.core_principles.glance_test.time_limit in KB");
335
+ }
336
+ const wordLimit = duarte.core_principles.glance_test.word_limit;
337
+ const timeSeconds = parseInt(duarte.core_principles.glance_test.time_limit.replace(/\D/g, ""), 10);
338
+ logger.debug(`[KB] getDuarteGlanceTest(): wordLimit=${wordLimit}, timeSeconds=${timeSeconds} (from KB experts.nancy_duarte)`);
339
+ return { wordLimit, timeSeconds };
307
340
  }
308
341
  /**
309
- * Get Miller's Law constraints
342
+ * Get Miller's Law constraints.
343
+ * READS FROM KB - NO HARDCODED VALUES.
310
344
  */
311
345
  getMillersLaw() {
312
346
  this.ensureLoaded();
313
- return { minItems: 5, maxItems: 9 };
347
+ const millersLaw = this.kb.cognitive_science?.millers_law;
348
+ if (!millersLaw?.principle) {
349
+ throw new Error("[KB ERROR] Missing cognitive_science.millers_law.principle in KB");
350
+ }
351
+ const match = millersLaw.principle.match(/(\d+)\s*[±+-]\s*(\d+)/);
352
+ if (!match) {
353
+ throw new Error(`[KB ERROR] Cannot parse Miller's Law from KB: "${millersLaw.principle}". Expected format: "7 \xB1 2"`);
354
+ }
355
+ const center = parseInt(match[1], 10);
356
+ const range = parseInt(match[2], 10);
357
+ const minItems = center - range;
358
+ const maxItems = center + range;
359
+ logger.debug(`[KB] getMillersLaw(): minItems=${minItems}, maxItems=${maxItems} (from KB: "${millersLaw.principle}")`);
360
+ return { minItems, maxItems };
314
361
  }
315
362
  /**
316
- * Get cognitive load constraints
363
+ * Get cognitive load constraints.
364
+ * READS FROM KB - NO HARDCODED VALUES.
317
365
  */
318
366
  getCognitiveLoadLimits() {
319
367
  this.ensureLoaded();
320
- return { maxItemsPerChunk: 7 };
368
+ const millers = this.getMillersLaw();
369
+ const maxItemsPerChunk = millers.maxItems - 2;
370
+ logger.debug(`[KB] getCognitiveLoadLimits(): maxItemsPerChunk=${maxItemsPerChunk} (derived from Miller's Law)`);
371
+ return { maxItemsPerChunk };
321
372
  }
322
373
  /**
323
374
  * Get chart selection guidance
@@ -381,42 +432,40 @@ var KnowledgeGateway = class {
381
432
  this.ensureLoaded();
382
433
  const typeConfig = this.kb.presentation_types?.[type];
383
434
  if (!typeConfig?.validation_rules) {
384
- const mode = this.getModeForType(type);
385
- return mode === "keynote" ? {
386
- wordsPerSlide: { min: 1, max: 25, ideal: 10 },
387
- whitespace: { min: 40, ideal: 50 },
388
- bulletsPerSlide: { max: 3 },
389
- actionTitlesRequired: false,
390
- sourcesRequired: false
391
- } : {
392
- wordsPerSlide: { min: 40, max: 80, ideal: 60 },
393
- whitespace: { min: 25, ideal: 30 },
394
- bulletsPerSlide: { max: 5 },
395
- actionTitlesRequired: true,
396
- sourcesRequired: true
397
- };
435
+ throw new Error(`[KB ERROR] Missing presentation_types.${type}.validation_rules in KB. Cannot proceed without KB rules.`);
398
436
  }
399
437
  const rules = typeConfig.validation_rules;
400
- return {
438
+ if (rules.words_per_slide?.max === void 0) {
439
+ throw new Error(`[KB ERROR] Missing presentation_types.${type}.validation_rules.words_per_slide.max in KB`);
440
+ }
441
+ if (rules.whitespace?.min === void 0) {
442
+ throw new Error(`[KB ERROR] Missing presentation_types.${type}.validation_rules.whitespace.min in KB`);
443
+ }
444
+ if (rules.bullets_per_slide?.max === void 0) {
445
+ throw new Error(`[KB ERROR] Missing presentation_types.${type}.validation_rules.bullets_per_slide.max in KB`);
446
+ }
447
+ const result = {
401
448
  wordsPerSlide: {
402
- min: rules.words_per_slide?.min ?? 1,
403
- max: rules.words_per_slide?.max ?? 80,
404
- ideal: rules.words_per_slide?.ideal ?? 40
449
+ min: rules.words_per_slide.min,
450
+ max: rules.words_per_slide.max,
451
+ ideal: rules.words_per_slide.ideal
405
452
  },
406
453
  whitespace: {
407
- min: rules.whitespace?.min ?? 25,
408
- ideal: rules.whitespace?.ideal ?? 30
454
+ min: rules.whitespace.min,
455
+ ideal: rules.whitespace.ideal
409
456
  },
410
457
  bulletsPerSlide: {
411
- max: rules.bullets_per_slide?.max ?? 5
458
+ max: rules.bullets_per_slide.max
412
459
  },
413
- actionTitlesRequired: rules.action_titles_required ?? false,
414
- sourcesRequired: rules.sources_required ?? false,
460
+ actionTitlesRequired: rules.action_titles_required,
461
+ sourcesRequired: rules.sources_required,
415
462
  calloutsRequired: rules.callouts_required,
416
463
  denseDataAllowed: rules.dense_data_allowed,
417
464
  codeBlocksAllowed: rules.code_blocks_allowed,
418
465
  diagramsRequired: rules.diagrams_required
419
466
  };
467
+ logger.debug(`[KB] getValidationRules(${type}): words=${result.wordsPerSlide.min}-${result.wordsPerSlide.max}, bullets=${result.bulletsPerSlide.max}, whitespace=${result.whitespace.min}%`);
468
+ return result;
420
469
  }
421
470
  /**
422
471
  * Get required elements that MUST be in the deck for this type.
@@ -526,39 +575,146 @@ var KnowledgeGateway = class {
526
575
  * Map a content pattern to the best allowed slide type.
527
576
  * CRITICAL: This is how SlideFactory decides which slide type to use.
528
577
  */
578
+ /**
579
+ * Map semantic slide types to structural types that SlideFactory can handle.
580
+ * This bridges the gap between KB's rich semantic types and code's structural types.
581
+ */
582
+ normalizeToStructuralType(semanticType) {
583
+ const normalizedType = semanticType.toLowerCase().replace(/_/g, "-");
584
+ const structuralTypes = /* @__PURE__ */ new Set([
585
+ "title",
586
+ "agenda",
587
+ "thank-you",
588
+ "cta",
589
+ "single-statement",
590
+ "big-number",
591
+ "quote",
592
+ "bullet-points",
593
+ "two-column",
594
+ "three-column",
595
+ "three-points",
596
+ "comparison",
597
+ "timeline",
598
+ "process",
599
+ "metrics-grid",
600
+ "data-insight",
601
+ "full-image",
602
+ "screenshot",
603
+ "code-snippet",
604
+ "title-impact"
605
+ ]);
606
+ if (structuralTypes.has(normalizedType)) {
607
+ return normalizedType;
608
+ }
609
+ const semanticToStructural = {
610
+ // TED Keynote semantic types
611
+ "star-moment": "single-statement",
612
+ "call-to-action": "cta",
613
+ // Sales pitch semantic types
614
+ "problem-statement": "single-statement",
615
+ "solution-overview": "single-statement",
616
+ "social-proof": "quote",
617
+ "testimonial": "quote",
618
+ "pricing": "metrics-grid",
619
+ "demo-screenshot": "screenshot",
620
+ // Consulting deck semantic types
621
+ "executive-summary-scr": "bullet-points",
622
+ "executive-summary": "bullet-points",
623
+ "mece-breakdown": "three-column",
624
+ "process-timeline": "timeline",
625
+ "recommendation": "single-statement",
626
+ "risks-mitigation": "two-column",
627
+ "next-steps": "bullet-points",
628
+ "detailed-findings": "bullet-points",
629
+ // Investment banking semantic types
630
+ "situation-overview": "bullet-points",
631
+ "credentials": "metrics-grid",
632
+ "valuation-summary": "metrics-grid",
633
+ "football-field": "comparison",
634
+ "comparable-companies": "metrics-grid",
635
+ "precedent-transactions": "metrics-grid",
636
+ "dcf-summary": "metrics-grid",
637
+ "waterfall-bridge": "timeline",
638
+ "sources-uses": "two-column",
639
+ "risk-factors": "bullet-points",
640
+ // Investor pitch semantic types
641
+ "problem": "single-statement",
642
+ "solution": "single-statement",
643
+ "market-size": "big-number",
644
+ "product": "three-points",
645
+ "business-model": "three-column",
646
+ "traction": "metrics-grid",
647
+ "competition": "comparison",
648
+ "team": "three-column",
649
+ "financials": "metrics-grid",
650
+ "ask": "single-statement",
651
+ // Technical presentation semantic types
652
+ "architecture-diagram": "full-image",
653
+ "data-flow": "full-image",
654
+ "metrics": "metrics-grid",
655
+ "tradeoffs": "comparison",
656
+ // All hands semantic types
657
+ "celebration": "single-statement",
658
+ "announcement": "single-statement",
659
+ "milestones": "timeline",
660
+ "team-recognition": "three-column",
661
+ "roadmap": "timeline"
662
+ };
663
+ return semanticToStructural[normalizedType] || "bullet-points";
664
+ }
529
665
  mapContentPatternToSlideType(pattern, allowedTypes) {
530
666
  const patternToTypes = {
531
- big_number: ["big_number", "big-number", "data_insight", "metrics_grid", "metrics-grid"],
532
- comparison: ["comparison", "options_comparison", "two_column", "two-column"],
533
- timeline: ["timeline", "process_timeline", "process", "roadmap"],
534
- process: ["process", "process_timeline", "timeline", "three_column", "three-column"],
535
- metrics: ["metrics_grid", "metrics-grid", "data_insight", "big_number", "big-number"],
536
- quote: ["quote", "testimonial", "social_proof", "social-proof"],
537
- code: ["code_snippet", "technical", "two_column", "two-column"],
538
- bullets: ["bullet_points", "bullet-points", "detailed_findings", "two_column", "two-column"],
539
- prose: ["two_column", "two-column", "bullet_points", "bullet-points", "single_statement"]
667
+ big_number: ["big-number", "metrics-grid", "single-statement"],
668
+ comparison: ["comparison", "two-column", "three-column"],
669
+ timeline: ["timeline", "process", "three-points", "bullet-points"],
670
+ process: ["process", "timeline", "three-column", "three-points", "bullet-points"],
671
+ metrics: ["metrics-grid", "big-number", "three-points"],
672
+ quote: ["quote", "single-statement"],
673
+ code: ["code-snippet", "two-column"],
674
+ bullets: ["bullet-points", "three-points", "three-column", "two-column", "process"],
675
+ prose: ["single-statement", "title-impact", "two-column"]
540
676
  };
677
+ if (pattern.primaryPattern === "process" || pattern.primaryPattern === "bullets") {
678
+ if (pattern.bulletCount === 3) {
679
+ const threePointTypes = ["three-points", "three-column"];
680
+ for (const t of threePointTypes) {
681
+ if (this.isTypeAllowed(t, allowedTypes)) return t;
682
+ }
683
+ }
684
+ if (pattern.bulletCount && pattern.bulletCount >= 2 && pattern.bulletCount <= 6) {
685
+ const processTypes = ["timeline", "process", "bullet-points"];
686
+ for (const t of processTypes) {
687
+ if (this.isTypeAllowed(t, allowedTypes)) return t;
688
+ }
689
+ }
690
+ }
541
691
  const preferredTypes = patternToTypes[pattern.primaryPattern] || patternToTypes.prose || [];
542
692
  for (const preferred of preferredTypes) {
543
- const underscoreVersion = preferred.replace(/-/g, "_");
544
- const dashVersion = preferred.replace(/_/g, "-");
545
- if (allowedTypes.includes(preferred)) return preferred;
546
- if (allowedTypes.includes(underscoreVersion)) return underscoreVersion;
547
- if (allowedTypes.includes(dashVersion)) return dashVersion;
548
- }
549
- const contentTypes = [
550
- "bullet_points",
551
- "bullet-points",
552
- "two_column",
553
- "two-column",
554
- "three_column",
555
- "three-column",
556
- "data_insight"
557
- ];
693
+ if (this.isTypeAllowed(preferred, allowedTypes)) {
694
+ return preferred;
695
+ }
696
+ }
697
+ const contentTypes = ["bullet-points", "three-points", "single-statement"];
558
698
  for (const ct of contentTypes) {
559
- if (allowedTypes.includes(ct)) return ct;
699
+ if (this.isTypeAllowed(ct, allowedTypes)) return ct;
700
+ }
701
+ return this.normalizeToStructuralType(allowedTypes[0] ?? "bullet-points");
702
+ }
703
+ /**
704
+ * Check if a structural type is allowed (directly or via semantic mapping)
705
+ */
706
+ isTypeAllowed(structuralType, allowedTypes) {
707
+ const normalized = structuralType.toLowerCase().replace(/_/g, "-");
708
+ const underscored = normalized.replace(/-/g, "_");
709
+ if (allowedTypes.includes(normalized) || allowedTypes.includes(underscored)) {
710
+ return true;
711
+ }
712
+ for (const allowed of allowedTypes) {
713
+ if (this.normalizeToStructuralType(allowed) === normalized) {
714
+ return true;
715
+ }
560
716
  }
561
- return allowedTypes[0] ?? "bullet-points";
717
+ return false;
562
718
  }
563
719
  /**
564
720
  * Get a specific slide template by name from the KB.
@@ -619,7 +775,8 @@ var KnowledgeGateway = class {
619
775
  optionLabels: ["Option A", "Option B", "Option C", "Option D"]
620
776
  },
621
777
  column: { labelTemplate: "Point {n}" },
622
- subtitle: { maxWords: Math.min(15, Math.floor(maxWords / 3)) },
778
+ // Subtitle needs more room for complete phrases - at least 10 words for keynote
779
+ subtitle: { maxWords: mode === "keynote" ? 12 : Math.min(20, Math.floor(maxWords / 2)) },
623
780
  context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
624
781
  step: { maxWords: Math.min(20, Math.floor(maxWords / 4)) },
625
782
  columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
@@ -974,7 +1131,14 @@ var ContentAnalyzer = class {
974
1131
  if (foundTitle && i > titleLineIndex && line.length > 0) {
975
1132
  if (line.startsWith("#")) break;
976
1133
  if (line.startsWith("-") || line.startsWith("*") || /^\d+\./.test(line)) break;
977
- subtitle = line.replace(/\*\*/g, "").trim().slice(0, 100);
1134
+ const cleanLine = line.replace(/\*\*/g, "").trim();
1135
+ if (cleanLine.length <= 120) {
1136
+ subtitle = cleanLine;
1137
+ } else {
1138
+ const truncated = cleanLine.slice(0, 120);
1139
+ const lastSpace = truncated.lastIndexOf(" ");
1140
+ subtitle = lastSpace > 80 ? truncated.slice(0, lastSpace) : truncated;
1141
+ }
978
1142
  break;
979
1143
  }
980
1144
  }
@@ -1097,7 +1261,60 @@ var ContentAnalyzer = class {
1097
1261
  sections.push(currentSection);
1098
1262
  }
1099
1263
  }
1100
- return sections;
1264
+ return this.combineParentChildSections(sections);
1265
+ }
1266
+ /**
1267
+ * Combine parent sections with their child sub-sections.
1268
+ * Detects patterns like "Three Pillars" + 3 child sections and merges them.
1269
+ */
1270
+ combineParentChildSections(sections) {
1271
+ const result = [];
1272
+ let i = 0;
1273
+ while (i < sections.length) {
1274
+ const current = sections[i];
1275
+ if (current && current.level === 2 && (!current.content || current.content.length < 20) && current.bullets.length === 0) {
1276
+ const children = [];
1277
+ let j = i + 1;
1278
+ while (j < sections.length && children.length < 4) {
1279
+ const child = sections[j];
1280
+ if (!child || child.level !== 3) break;
1281
+ children.push(child);
1282
+ j++;
1283
+ }
1284
+ if (children.length >= 2 && children.length <= 4) {
1285
+ const combined = {
1286
+ header: current.header,
1287
+ level: current.level,
1288
+ content: "",
1289
+ // Will use bullets instead
1290
+ bullets: children.map((child) => {
1291
+ const title = child.header.replace(/^\d+\.\s*/, "").trim();
1292
+ if (child.content) {
1293
+ return `**${title}**: ${child.content.split("\n")[0] || ""}`;
1294
+ }
1295
+ return title;
1296
+ }),
1297
+ metrics: [
1298
+ ...current.metrics,
1299
+ ...children.flatMap((c) => c.metrics)
1300
+ ],
1301
+ // Add sub-sections as a property for advanced slide types
1302
+ subSections: children.map((child) => ({
1303
+ title: child.header.replace(/^\d+\.\s*/, "").trim(),
1304
+ content: child.content
1305
+ }))
1306
+ };
1307
+ result.push(combined);
1308
+ i = j;
1309
+ continue;
1310
+ }
1311
+ }
1312
+ if (current) {
1313
+ result.push(current);
1314
+ }
1315
+ i++;
1316
+ }
1317
+ return result;
1101
1318
  }
1102
1319
  /**
1103
1320
  * Extract SCQA structure (Barbara Minto)
@@ -1680,11 +1897,538 @@ var ContentPatternClassifier = class {
1680
1897
  }
1681
1898
  };
1682
1899
 
1900
+ // src/kb/ExpertGuidanceEngine.ts
1901
+ var ExpertGuidanceEngine = class {
1902
+ kb;
1903
+ presentationType;
1904
+ constructor(kb, presentationType) {
1905
+ this.kb = kb;
1906
+ this.presentationType = presentationType;
1907
+ }
1908
+ /**
1909
+ * Get the narrative arc for this presentation type.
1910
+ * Returns the story structure with required elements and their purposes.
1911
+ */
1912
+ getNarrativeArc() {
1913
+ const mode = this.kb.getModeForType(this.presentationType);
1914
+ if (mode === "keynote") {
1915
+ return {
1916
+ framework: "Duarte Sparkline",
1917
+ expert: "Nancy Duarte",
1918
+ description: "Alternate between What Is (current reality) and What Could Be (possibility), building emotional tension to a climax, then resolve with New Bliss",
1919
+ elements: [
1920
+ {
1921
+ id: "opening_hook",
1922
+ name: "Opening Hook",
1923
+ purpose: "Grab attention in first 10 seconds with something unexpected",
1924
+ expertSource: 'Chris Anderson: "You have one chance to hook them"',
1925
+ designPrinciples: [
1926
+ "Start with a story, question, or surprising fact",
1927
+ "Create curiosity gap - make them NEED to know more",
1928
+ "Touch heart before head (Gallo)"
1929
+ ],
1930
+ required: true
1931
+ },
1932
+ {
1933
+ id: "what_is",
1934
+ name: "What Is (Current Reality)",
1935
+ purpose: "Establish the current state that audience recognizes",
1936
+ expertSource: 'Nancy Duarte: "Ground them in familiar reality"',
1937
+ designPrinciples: [
1938
+ "Use concrete, recognizable situations",
1939
+ "Create emotional resonance with current pain",
1940
+ "Keep it brief - setup for contrast"
1941
+ ],
1942
+ required: true
1943
+ },
1944
+ {
1945
+ id: "what_could_be",
1946
+ name: "What Could Be (Vision)",
1947
+ purpose: "Paint the picture of the transformed future",
1948
+ expertSource: 'Nancy Duarte: "Make them FEEL the possibility"',
1949
+ designPrinciples: [
1950
+ "Be specific and vivid about the future state",
1951
+ "Connect emotionally - how will they FEEL?",
1952
+ "Create contrast with What Is"
1953
+ ],
1954
+ required: true
1955
+ },
1956
+ {
1957
+ id: "star_moment",
1958
+ name: "STAR Moment",
1959
+ purpose: "Something They'll Always Remember - the emotional climax",
1960
+ expertSource: 'Nancy Duarte: "One dramatic memorable moment"',
1961
+ designPrinciples: [
1962
+ "Make it unexpected and emotionally impactful",
1963
+ "Use a dramatic statistic, story, or demonstration",
1964
+ "This is what they'll tell others about",
1965
+ "It should give them chills or make them gasp"
1966
+ ],
1967
+ required: true
1968
+ },
1969
+ {
1970
+ id: "call_to_action",
1971
+ name: "Call to Action",
1972
+ purpose: "Clear next step that audience can take",
1973
+ expertSource: 'Chris Anderson: "Give them something to DO"',
1974
+ designPrinciples: [
1975
+ "Be specific and actionable",
1976
+ "Make it achievable - first step, not whole journey",
1977
+ "Connect action to the vision you painted"
1978
+ ],
1979
+ required: true
1980
+ },
1981
+ {
1982
+ id: "new_bliss",
1983
+ name: "New Bliss (Resolution)",
1984
+ purpose: "Leave them with the transformed future firmly in mind",
1985
+ expertSource: 'Nancy Duarte: "End with the new world, not the old"',
1986
+ designPrinciples: [
1987
+ "Reinforce the vision one final time",
1988
+ "Make them want to be part of it",
1989
+ "End on emotional high, not logistics"
1990
+ ],
1991
+ required: false
1992
+ }
1993
+ ]
1994
+ };
1995
+ } else {
1996
+ return {
1997
+ framework: "Minto Pyramid (SCQA)",
1998
+ expert: "Barbara Minto",
1999
+ description: "Lead with the answer/recommendation, then support with evidence in MECE structure",
2000
+ elements: [
2001
+ {
2002
+ id: "situation",
2003
+ name: "Situation",
2004
+ purpose: "Context the audience already knows and agrees with",
2005
+ expertSource: 'Barbara Minto: "Start from common ground"',
2006
+ designPrinciples: [
2007
+ "Facts everyone agrees on",
2008
+ "Recent and relevant context",
2009
+ "Brief - not the star of the show"
2010
+ ],
2011
+ required: true
2012
+ },
2013
+ {
2014
+ id: "complication",
2015
+ name: "Complication",
2016
+ purpose: "The problem, threat, or opportunity that requires action",
2017
+ expertSource: 'Barbara Minto: "Why are we here?"',
2018
+ designPrinciples: [
2019
+ "Clear articulation of the challenge",
2020
+ "Why the status quo is unacceptable",
2021
+ "Creates urgency for the answer"
2022
+ ],
2023
+ required: true
2024
+ },
2025
+ {
2026
+ id: "answer",
2027
+ name: "Answer/Recommendation",
2028
+ purpose: "The core recommendation - upfront, not buried",
2029
+ expertSource: 'Barbara Minto: "Answer first, evidence second"',
2030
+ designPrinciples: [
2031
+ "Clear, actionable recommendation",
2032
+ "Use an action title that states the conclusion",
2033
+ "Executive should understand even if they stop here"
2034
+ ],
2035
+ required: true
2036
+ },
2037
+ {
2038
+ id: "evidence",
2039
+ name: "Supporting Evidence",
2040
+ purpose: "MECE support for the recommendation",
2041
+ expertSource: 'Barbara Minto: "Mutually Exclusive, Collectively Exhaustive"',
2042
+ designPrinciples: [
2043
+ "Group into 3-5 supporting pillars",
2044
+ "Each pillar has sub-evidence",
2045
+ "No gaps, no overlaps (MECE)",
2046
+ "Use action titles throughout"
2047
+ ],
2048
+ required: true
2049
+ },
2050
+ {
2051
+ id: "next_steps",
2052
+ name: "Next Steps",
2053
+ purpose: "Clear actions with owners and timelines",
2054
+ expertSource: 'Analyst Academy: "End with who does what by when"',
2055
+ designPrinciples: [
2056
+ 'Specific actions, not vague "follow up"',
2057
+ "Named owners where possible",
2058
+ "Realistic timelines"
2059
+ ],
2060
+ required: true
2061
+ }
2062
+ ]
2063
+ };
2064
+ }
2065
+ }
2066
+ /**
2067
+ * Get expert guidance for designing a specific type of slide.
2068
+ * This returns PRINCIPLES for excellence, not constraints.
2069
+ */
2070
+ getSlideDesignGuidance(slideType) {
2071
+ const mode = this.kb.getModeForType(this.presentationType);
2072
+ switch (slideType) {
2073
+ case "title":
2074
+ case "title_impact":
2075
+ return {
2076
+ purpose: "Create immediate emotional connection and establish the one idea worth spreading",
2077
+ principles: [
2078
+ {
2079
+ expert: "Chris Anderson",
2080
+ principle: "One Idea Worth Spreading",
2081
+ application: "The title should encapsulate a single powerful idea that the audience will remember",
2082
+ technique: "Ask: If they remember ONE thing, what should it be? That's your title."
2083
+ },
2084
+ {
2085
+ expert: "Carmine Gallo",
2086
+ principle: "Headline Test",
2087
+ application: "Title should work as a Twitter headline - compelling in under 280 characters",
2088
+ technique: "Write 10 versions, pick the one that makes you want to hear more"
2089
+ },
2090
+ {
2091
+ expert: "Nancy Duarte",
2092
+ principle: "Glance Test",
2093
+ application: "Audience should grasp the essence in 3 seconds",
2094
+ technique: "Show it to someone for 3 seconds, ask what they remember"
2095
+ }
2096
+ ],
2097
+ whatMakesItGreat: [
2098
+ "Creates curiosity - makes audience WANT to hear more",
2099
+ "Emotionally resonant - connects to something they care about",
2100
+ "Unexpected angle - not the obvious framing",
2101
+ "Memorable - they could repeat it to someone else"
2102
+ ],
2103
+ antiPatterns: [
2104
+ 'Generic topic labels ("Q4 Update")',
2105
+ "Long sentences that require reading",
2106
+ "Jargon or acronyms",
2107
+ "Multiple ideas competing for attention"
2108
+ ],
2109
+ questions: [
2110
+ "Would someone share this title with a colleague?",
2111
+ "Does it create a curiosity gap?",
2112
+ "Can you understand the value in 3 seconds?",
2113
+ "Is there emotional resonance?"
2114
+ ]
2115
+ };
2116
+ case "single_statement":
2117
+ case "big_idea":
2118
+ return {
2119
+ purpose: "Communicate ONE powerful insight that shifts perspective",
2120
+ principles: [
2121
+ {
2122
+ expert: "Garr Reynolds",
2123
+ principle: "Signal to Noise",
2124
+ application: "Everything on the slide should serve the one message. Eliminate ruthlessly.",
2125
+ technique: "For each element, ask: Does this AMPLIFY the message or distract from it?"
2126
+ },
2127
+ {
2128
+ expert: "Garr Reynolds",
2129
+ principle: "Amplification Through Simplification",
2130
+ application: "Empty space makes the remaining content more powerful",
2131
+ technique: "Try removing 50% of what's there. Is it clearer?"
2132
+ },
2133
+ {
2134
+ expert: "Nancy Duarte",
2135
+ principle: "Audience Paradox",
2136
+ application: "They will read OR listen, never both. Design for one.",
2137
+ technique: "If presenting live, the statement should be short enough to absorb while listening"
2138
+ }
2139
+ ],
2140
+ whatMakesItGreat: [
2141
+ "One clear insight that shifts thinking",
2142
+ "Ample whitespace that emphasizes the message",
2143
+ "Words are carefully chosen - each one earns its place",
2144
+ "Can be absorbed in a glance"
2145
+ ],
2146
+ antiPatterns: [
2147
+ "Cramming multiple points onto one slide",
2148
+ 'Adding "supporting" text that dilutes the message',
2149
+ "Decorative elements that don't serve the idea",
2150
+ "Topic labels instead of insights"
2151
+ ],
2152
+ questions: [
2153
+ "What is the ONE thing this slide should communicate?",
2154
+ "If I remove this word/element, is the message clearer?",
2155
+ "Will this shift how they think about the topic?"
2156
+ ]
2157
+ };
2158
+ case "star_moment":
2159
+ return {
2160
+ purpose: "Create the moment they'll remember forever and tell others about",
2161
+ principles: [
2162
+ {
2163
+ expert: "Nancy Duarte",
2164
+ principle: "STAR Moment",
2165
+ application: "Something They'll Always Remember - this is the emotional climax",
2166
+ technique: "What would make the audience gasp, laugh, or get chills?"
2167
+ },
2168
+ {
2169
+ expert: "Chip & Dan Heath",
2170
+ principle: "Unexpected",
2171
+ application: "Break a pattern to get attention. Violate expectations.",
2172
+ technique: "What assumption can you shatter? What surprise can you deliver?"
2173
+ }
2174
+ ],
2175
+ whatMakesItGreat: [
2176
+ "Creates visceral emotional response",
2177
+ "Breaks expectations - genuinely surprising",
2178
+ "Connects logically to the core message",
2179
+ "Memorable enough to retell"
2180
+ ],
2181
+ antiPatterns: [
2182
+ "Playing it safe with expected information",
2183
+ "Burying the dramatic element in other content",
2184
+ "Gimmicks that don't connect to the message"
2185
+ ],
2186
+ questions: [
2187
+ "Will this give them chills or make them gasp?",
2188
+ "Would they tell someone about this tomorrow?",
2189
+ "Does it support or distract from the core message?"
2190
+ ]
2191
+ };
2192
+ case "big_number":
2193
+ case "data_insight":
2194
+ return {
2195
+ purpose: "Make data memorable by providing context that reveals meaning",
2196
+ principles: [
2197
+ {
2198
+ expert: "Edward Tufte",
2199
+ principle: "Data-Ink Ratio",
2200
+ application: "Maximize ink used for data, minimize everything else",
2201
+ technique: "Remove all decoration. Is the data still clear? Good. Now make it beautiful."
2202
+ },
2203
+ {
2204
+ expert: "Lea Pica",
2205
+ principle: "One Insight Per Chart",
2206
+ application: "Don't let data speak for itself - tell them exactly what to see",
2207
+ technique: "What's the ONE conclusion? Make that the headline."
2208
+ },
2209
+ {
2210
+ expert: "Cole Knaflic",
2211
+ principle: "Context Creates Meaning",
2212
+ application: "Numbers mean nothing without context",
2213
+ technique: "Compare to something they understand - time, money, scale"
2214
+ }
2215
+ ],
2216
+ whatMakesItGreat: [
2217
+ "The number is HUGE and simple",
2218
+ "Context makes the number meaningful",
2219
+ "Audience immediately understands significance",
2220
+ "One clear insight, not a data dump"
2221
+ ],
2222
+ antiPatterns: [
2223
+ "Multiple competing numbers",
2224
+ 'Data without context ("$5M" - is that good?)',
2225
+ "Complex charts when simple would work",
2226
+ "Decorative elements that distract from data"
2227
+ ],
2228
+ questions: [
2229
+ "What is the ONE insight this data reveals?",
2230
+ "What context makes this number meaningful?",
2231
+ "Can someone understand the significance in 3 seconds?"
2232
+ ]
2233
+ };
2234
+ case "three_points":
2235
+ case "three_column":
2236
+ return {
2237
+ purpose: "Present key messages in memorable groups of three",
2238
+ principles: [
2239
+ {
2240
+ expert: "Carmine Gallo",
2241
+ principle: "Rule of Three",
2242
+ application: "Three is the magic number for memory. Not two, not four.",
2243
+ technique: "Can you group your content into exactly three main points?"
2244
+ },
2245
+ {
2246
+ expert: "Barbara Minto",
2247
+ principle: "MECE",
2248
+ application: "Points should be Mutually Exclusive (no overlap) and Collectively Exhaustive (no gaps)",
2249
+ technique: "Do these three points cover everything without repeating?"
2250
+ }
2251
+ ],
2252
+ whatMakesItGreat: [
2253
+ "Exactly three points - no more, no less",
2254
+ "Each point is substantively different",
2255
+ "Together they tell a complete story",
2256
+ "Each point is memorable on its own"
2257
+ ],
2258
+ antiPatterns: [
2259
+ "Forcing content into three when it should be two or four",
2260
+ "Overlapping points that aren't distinct",
2261
+ "Generic labels instead of insights",
2262
+ "Uneven importance across points"
2263
+ ],
2264
+ questions: [
2265
+ "Are these three truly distinct and non-overlapping?",
2266
+ "Do they together cover the complete picture?",
2267
+ "Is each point valuable on its own?"
2268
+ ]
2269
+ };
2270
+ case "call_to_action":
2271
+ case "cta":
2272
+ return {
2273
+ purpose: "Give the audience a clear, compelling next step they can take",
2274
+ principles: [
2275
+ {
2276
+ expert: "Chris Anderson",
2277
+ principle: "Gift Giving",
2278
+ application: "Your job is to give them something valuable they can USE",
2279
+ technique: "What can they DO tomorrow because of this presentation?"
2280
+ },
2281
+ {
2282
+ expert: "Robert Cialdini",
2283
+ principle: "Commitment",
2284
+ application: "Small commitments lead to larger ones",
2285
+ technique: "What's the smallest first step that gets them moving?"
2286
+ }
2287
+ ],
2288
+ whatMakesItGreat: [
2289
+ 'Specific and actionable - not vague "learn more"',
2290
+ "Achievable first step, not overwhelming",
2291
+ "Clearly connected to the value proposition",
2292
+ "Creates urgency without being pushy"
2293
+ ],
2294
+ antiPatterns: [
2295
+ 'Vague calls to action ("contact us")',
2296
+ "Multiple competing CTAs",
2297
+ "Actions that feel like too much work",
2298
+ "Disconnected from the presentation content"
2299
+ ],
2300
+ questions: [
2301
+ "Can they do this tomorrow?",
2302
+ "Is this the logical first step?",
2303
+ "Does it connect to the value you promised?"
2304
+ ]
2305
+ };
2306
+ default:
2307
+ return {
2308
+ purpose: "Communicate one clear message that advances the narrative",
2309
+ principles: [
2310
+ {
2311
+ expert: "Garr Reynolds",
2312
+ principle: "Signal to Noise",
2313
+ application: "Maximize signal (valuable content), minimize noise (distractions)",
2314
+ technique: "For every element, ask: Does this serve the message?"
2315
+ },
2316
+ {
2317
+ expert: "Nancy Duarte",
2318
+ principle: "Glance Test",
2319
+ application: "Key message should be clear in 3 seconds",
2320
+ technique: "What would someone remember after a 3-second glance?"
2321
+ }
2322
+ ],
2323
+ whatMakesItGreat: [
2324
+ "One clear message per slide",
2325
+ "Visual hierarchy guides the eye",
2326
+ "Every element serves a purpose",
2327
+ "Can be understood at a glance"
2328
+ ],
2329
+ antiPatterns: [
2330
+ "Multiple competing messages",
2331
+ "Decorative elements that distract",
2332
+ "Text-heavy slides that require reading",
2333
+ "Unclear what's most important"
2334
+ ],
2335
+ questions: [
2336
+ "What is the ONE message?",
2337
+ "What can be removed without losing meaning?",
2338
+ "Does visual hierarchy match content hierarchy?"
2339
+ ]
2340
+ };
2341
+ }
2342
+ }
2343
+ /**
2344
+ * Get guidance for extracting the core message from content.
2345
+ * This helps identify WHAT should be on the slide, not just how to fit content.
2346
+ */
2347
+ getCoreMessageGuidance() {
2348
+ return [
2349
+ {
2350
+ expert: "Chris Anderson",
2351
+ principle: "One Idea Worth Spreading",
2352
+ application: "Find the single most valuable insight in this content",
2353
+ technique: "If they could only remember ONE thing, what should it be?"
2354
+ },
2355
+ {
2356
+ expert: "Carmine Gallo",
2357
+ principle: "Twitter Test",
2358
+ application: "Can you express the key insight in a tweet?",
2359
+ technique: "Write the insight in under 280 characters. If you can't, you don't understand it well enough."
2360
+ },
2361
+ {
2362
+ expert: "Barbara Minto",
2363
+ principle: "So What?",
2364
+ application: 'Keep asking "so what?" until you reach the insight',
2365
+ technique: 'For every statement, ask "so what?" The answer is closer to the real message.'
2366
+ }
2367
+ ];
2368
+ }
2369
+ /**
2370
+ * Evaluate whether a slide meets expert standards.
2371
+ * Returns guidance on how to improve, not just pass/fail.
2372
+ */
2373
+ evaluateSlideExcellence(slide) {
2374
+ const guidance = this.getSlideDesignGuidance(slide.type);
2375
+ const strengths = [];
2376
+ const improvements = [];
2377
+ const expertFeedback = [];
2378
+ let score = 50;
2379
+ for (const quality of guidance.whatMakesItGreat) {
2380
+ }
2381
+ for (const antiPattern of guidance.antiPatterns) {
2382
+ }
2383
+ for (const question of guidance.questions) {
2384
+ expertFeedback.push(`Ask: ${question}`);
2385
+ }
2386
+ if (slide.title) {
2387
+ if (slide.title.length <= 60) {
2388
+ strengths.push("Title passes Duarte Glance Test (under 60 chars)");
2389
+ score += 10;
2390
+ } else {
2391
+ improvements.push("Shorten title - Duarte says 3 seconds to comprehend");
2392
+ }
2393
+ if (!/^(update|overview|introduction|summary)$/i.test(slide.title)) {
2394
+ strengths.push("Title is specific, not generic (Gallo Headline Test)");
2395
+ score += 5;
2396
+ } else {
2397
+ improvements.push("Replace generic title with insight - Gallo says it should work as a headline");
2398
+ score -= 10;
2399
+ }
2400
+ if (!slide.title.includes(" and ") && !slide.title.includes(" & ")) {
2401
+ strengths.push("Title focuses on one idea (Chris Anderson)");
2402
+ score += 5;
2403
+ } else {
2404
+ improvements.push("Split into two slides - Anderson says one idea per slide");
2405
+ }
2406
+ }
2407
+ if (slide.bullets && slide.bullets.length > 5) {
2408
+ improvements.push("Reduce bullets to 3-5 - Reynolds says maximize signal, eliminate noise");
2409
+ score -= 10;
2410
+ }
2411
+ return {
2412
+ score: Math.max(0, Math.min(100, score)),
2413
+ strengths,
2414
+ improvements,
2415
+ expertFeedback
2416
+ };
2417
+ }
2418
+ };
2419
+ function createExpertGuidanceEngine(kb, presentationType) {
2420
+ return new ExpertGuidanceEngine(kb, presentationType);
2421
+ }
2422
+
1683
2423
  // src/core/SlideFactory.ts
1684
2424
  var SlideFactory = class {
1685
2425
  kb;
1686
2426
  presentationType;
1687
2427
  classifier;
2428
+ expertEngine;
2429
+ // NEW: Expert guidance for world-class slides
2430
+ narrativeArc;
2431
+ // NEW: Story structure from experts
1688
2432
  config;
1689
2433
  usedContent;
1690
2434
  usedTitles;
@@ -1695,7 +2439,10 @@ var SlideFactory = class {
1695
2439
  this.usedContent = /* @__PURE__ */ new Set();
1696
2440
  this.usedTitles = /* @__PURE__ */ new Set();
1697
2441
  this.config = this.loadKBConfig(type);
1698
- logger.step(`SlideFactory v7.1.0 initialized for ${type} (${this.config.mode} mode)`);
2442
+ this.expertEngine = createExpertGuidanceEngine(kb, type);
2443
+ this.narrativeArc = this.expertEngine.getNarrativeArc();
2444
+ logger.step(`SlideFactory v8.0.0 initialized for ${type} (${this.config.mode} mode)`);
2445
+ logger.step(`Using ${this.narrativeArc.framework} narrative (${this.narrativeArc.expert})`);
1699
2446
  logger.step(`KB Config loaded: ${this.config.allowedTypes.length} allowed types`);
1700
2447
  }
1701
2448
  /**
@@ -1735,15 +2482,56 @@ var SlideFactory = class {
1735
2482
  } else if (storyStructure.framework === "sparkline") {
1736
2483
  this.addSparklineSlides(slides, analysis);
1737
2484
  }
2485
+ let hasCTASection = false;
1738
2486
  for (const section of analysis.sections) {
2487
+ logger.debug(`[TRACE] Processing section: "${section.header}"`);
2488
+ logger.debug(`[TRACE] Level: ${section.level}, Content: ${section.content?.length ?? 0} chars`);
2489
+ logger.debug(`[TRACE] Bullets: ${section.bullets?.length ?? 0} items`);
2490
+ if (section.bullets?.length > 0) {
2491
+ section.bullets.slice(0, 3).forEach((b, i) => {
2492
+ logger.debug(`[TRACE] Bullet ${i}: "${b.slice(0, 50)}..."`);
2493
+ });
2494
+ }
1739
2495
  const contentKey = this.normalizeKey(section.header);
1740
- if (this.usedContent.has(contentKey)) continue;
2496
+ if (this.usedContent.has(contentKey)) {
2497
+ logger.debug(`[TRACE] SKIPPED: Already used content key "${contentKey}"`);
2498
+ continue;
2499
+ }
1741
2500
  this.usedContent.add(contentKey);
2501
+ const headerLower = section.header.toLowerCase();
2502
+ const isCTASection = headerLower.includes("call to action") || headerLower.includes("next step") || headerLower.includes("take action") || headerLower.includes("get started") || headerLower.includes("start today");
2503
+ if (isCTASection) {
2504
+ hasCTASection = true;
2505
+ const ctaDefaults = this.config.defaults.cta;
2506
+ const ctaContent = section.bullets.length > 0 ? section.bullets.slice(0, 4).join(". ") : section.content || ctaDefaults.fallback;
2507
+ const craftedCTA = this.craftExpertContent(
2508
+ ctaContent,
2509
+ "call_to_action",
2510
+ this.config.rules.wordsPerSlide.max * 3
2511
+ // Allow more words for CTA steps
2512
+ );
2513
+ slides.push({
2514
+ index: slides.length,
2515
+ type: "cta",
2516
+ data: {
2517
+ title: section.header,
2518
+ body: craftedCTA,
2519
+ keyMessage: ctaDefaults.message
2520
+ },
2521
+ classes: ["cta-slide"]
2522
+ });
2523
+ continue;
2524
+ }
1742
2525
  const pattern = this.classifier.classify(section);
2526
+ logger.debug(`[TRACE] Pattern: ${pattern.primaryPattern}, bulletCount: ${pattern.bulletCount}`);
1743
2527
  const slideType = this.kb.mapContentPatternToSlideType(pattern, this.config.allowedTypes);
2528
+ logger.debug(`[TRACE] KB returned slide type: "${slideType}"`);
1744
2529
  const slide = this.createSlideByType(slides.length, slideType, section, pattern);
1745
- if (slide) {
2530
+ if (slide && slide.type && slide.data) {
2531
+ logger.debug(`[TRACE] Created slide type: "${slide.type}" with ${slide.data.bullets?.length ?? 0} bullets`);
1746
2532
  slides.push(slide);
2533
+ } else {
2534
+ logger.debug(`[TRACE] SKIPPED: Slide filtered out (no valid content)`);
1747
2535
  }
1748
2536
  }
1749
2537
  const minDataPointsForMetrics = 2;
@@ -1755,7 +2543,7 @@ var SlideFactory = class {
1755
2543
  slides.push(this.createMetricsGridSlide(slides.length, analysis.dataPoints));
1756
2544
  }
1757
2545
  }
1758
- if (analysis.sparkline?.callToAdventure || analysis.scqa?.answer) {
2546
+ if (!hasCTASection && (analysis.sparkline?.callToAdventure || analysis.scqa?.answer)) {
1759
2547
  slides.push(this.createCTASlide(slides.length, analysis));
1760
2548
  }
1761
2549
  slides.push(this.createThankYouSlide(slides.length));
@@ -1771,34 +2559,55 @@ var SlideFactory = class {
1771
2559
  createSlideByType(index, type, section, pattern) {
1772
2560
  const normalizedType = type.toLowerCase().replace(/_/g, "-");
1773
2561
  switch (normalizedType) {
2562
+ // Big number slides - show prominent metrics
1774
2563
  case "big-number":
1775
- case "data-insight":
1776
2564
  return this.createBigNumberSlide(index, section, pattern);
2565
+ // Data insight - for findings that may or may not have numbers
2566
+ case "data-insight":
2567
+ return this.createDataInsightSlide(index, section, pattern);
2568
+ // Comparison slides
1777
2569
  case "comparison":
1778
2570
  case "options-comparison":
1779
2571
  return this.createComparisonSlide(index, section);
2572
+ // Timeline and roadmap slides
1780
2573
  case "timeline":
1781
2574
  case "process-timeline":
1782
2575
  case "roadmap":
1783
2576
  return this.createTimelineSlide(index, section);
2577
+ // Process flow slides (with steps)
1784
2578
  case "process":
2579
+ case "mece-breakdown":
1785
2580
  return this.createProcessSlide(index, section);
2581
+ // Three points slide (for exactly 3 items)
2582
+ case "three-points":
2583
+ return this.createThreePointsSlide(index, section);
2584
+ // Column layouts
1786
2585
  case "three-column":
1787
2586
  return this.createThreeColumnSlide(index, section);
1788
2587
  case "two-column":
1789
2588
  return this.createTwoColumnSlide(index, section);
2589
+ // Quote and testimonial slides
1790
2590
  case "quote":
1791
2591
  case "testimonial":
1792
2592
  case "social-proof":
1793
2593
  return this.createQuoteSlide(index, section);
2594
+ // Metrics grid for multiple KPIs
1794
2595
  case "metrics-grid":
1795
2596
  return this.createMetricsGridSlide(index, section.metrics);
2597
+ // Standard bullet point slides
1796
2598
  case "bullet-points":
1797
- case "detailed-findings":
1798
2599
  return this.createBulletSlide(index, section);
2600
+ // Title impact - emphasized single statement
2601
+ case "title-impact":
2602
+ return this.createTitleImpactSlide(index, section);
2603
+ // Single statement slides
1799
2604
  case "single-statement":
1800
2605
  case "big-idea":
1801
2606
  return this.createSingleStatementSlide(index, section);
2607
+ // Detailed findings - bullets with context
2608
+ case "detailed-findings":
2609
+ return this.createDetailedFindingsSlide(index, section);
2610
+ // Code and technical slides
1802
2611
  case "code-snippet":
1803
2612
  case "technical":
1804
2613
  return this.createCodeSlide(index, section);
@@ -1811,15 +2620,21 @@ var SlideFactory = class {
1811
2620
  // STRUCTURAL SLIDES (title, agenda, thank you) - ALL from KB
1812
2621
  // ===========================================================================
1813
2622
  createTitleSlide(index, analysis) {
2623
+ const craftedTitle = this.craftExpertContent(
2624
+ analysis.title,
2625
+ "title",
2626
+ this.config.rules.wordsPerSlide.max
2627
+ );
1814
2628
  const data = {
1815
- title: this.truncateText(analysis.title, this.config.rules.wordsPerSlide.max)
2629
+ title: craftedTitle
1816
2630
  };
1817
2631
  const subtitleSource = analysis.subtitle || analysis.keyMessages[0] || "";
1818
2632
  if (subtitleSource) {
1819
- data.subtitle = this.truncateText(
2633
+ data.subtitle = this.craftExpertContent(
1820
2634
  subtitleSource,
2635
+ "title",
2636
+ // Use title guidance for subtitle too
1821
2637
  this.config.defaults.subtitle.maxWords
1822
- // FROM KB
1823
2638
  );
1824
2639
  }
1825
2640
  return {
@@ -1908,31 +2723,43 @@ var SlideFactory = class {
1908
2723
  addSparklineSlides(slides, analysis) {
1909
2724
  const spark = analysis.sparkline;
1910
2725
  const titles = this.config.sparklineTitles;
2726
+ const maxWords = this.config.rules.wordsPerSlide.max;
1911
2727
  const whatIsFirst = spark?.whatIs?.[0];
1912
- const whatIsBody = whatIsFirst ? this.truncateText(whatIsFirst, this.config.rules.wordsPerSlide.max) : "";
1913
- if (whatIsBody.length >= 20 && !this.usedContent.has("spark-what-is")) {
2728
+ const whatIsBody = whatIsFirst ? this.craftExpertContent(whatIsFirst, "single_statement", maxWords) : "";
2729
+ const whatCouldBeFirst = spark?.whatCouldBe?.[0];
2730
+ const whatCouldBeBody = whatCouldBeFirst ? this.craftExpertContent(whatCouldBeFirst, "single_statement", maxWords) : "";
2731
+ const normalizedTitle = analysis.title.toLowerCase().slice(0, 50);
2732
+ const normalizedSubtitle = (analysis.subtitle || "").toLowerCase().slice(0, 50);
2733
+ const normalizedWhatIs = whatIsBody.toLowerCase().slice(0, 50);
2734
+ const normalizedWhatCouldBe = whatCouldBeBody.toLowerCase().slice(0, 50);
2735
+ const whatIsDuplicatesTitle = normalizedWhatIs === normalizedTitle || normalizedWhatIs.includes(normalizedTitle.slice(0, 30)) || normalizedTitle.includes(normalizedWhatIs.slice(0, 30));
2736
+ const whatIsDuplicatesSubtitle = normalizedWhatIs === normalizedSubtitle || normalizedWhatIs.includes(normalizedSubtitle.slice(0, 30)) || normalizedSubtitle.includes(normalizedWhatIs.slice(0, 30));
2737
+ const isDuplicatePair = normalizedWhatIs === normalizedWhatCouldBe;
2738
+ if (whatIsBody.length >= 30 && !isDuplicatePair && !whatIsDuplicatesTitle && !whatIsDuplicatesSubtitle && !this.usedContent.has("spark-what-is")) {
1914
2739
  this.usedContent.add("spark-what-is");
2740
+ this.usedContent.add(normalizedWhatIs);
1915
2741
  slides.push({
1916
2742
  index: slides.length,
1917
2743
  type: "single-statement",
1918
2744
  data: {
1919
2745
  title: titles.whatIs,
1920
- // FROM KB - not hardcoded 'Where We Are Today'
2746
+ // FROM KB
1921
2747
  body: whatIsBody
1922
2748
  },
1923
2749
  classes: ["what-is-slide"]
1924
2750
  });
1925
2751
  }
1926
- const whatCouldBeFirst = spark?.whatCouldBe?.[0];
1927
- const whatCouldBeBody = whatCouldBeFirst ? this.truncateText(whatCouldBeFirst, this.config.rules.wordsPerSlide.max) : "";
1928
- if (whatCouldBeBody.length >= 20 && !this.usedContent.has("spark-could-be")) {
2752
+ const whatCouldBeDuplicatesTitle = normalizedWhatCouldBe === normalizedTitle || normalizedWhatCouldBe.includes(normalizedTitle.slice(0, 30)) || normalizedTitle.includes(normalizedWhatCouldBe.slice(0, 30));
2753
+ const whatCouldBeDuplicatesSubtitle = normalizedWhatCouldBe === normalizedSubtitle || normalizedWhatCouldBe.includes(normalizedSubtitle.slice(0, 30)) || normalizedSubtitle.includes(normalizedWhatCouldBe.slice(0, 30));
2754
+ if (whatCouldBeBody.length >= 30 && !isDuplicatePair && !whatCouldBeDuplicatesTitle && !whatCouldBeDuplicatesSubtitle && !this.usedContent.has("spark-could-be")) {
1929
2755
  this.usedContent.add("spark-could-be");
2756
+ this.usedContent.add(normalizedWhatCouldBe);
1930
2757
  slides.push({
1931
2758
  index: slides.length,
1932
2759
  type: "single-statement",
1933
2760
  data: {
1934
2761
  title: titles.whatCouldBe,
1935
- // FROM KB - not hardcoded 'What Could Be'
2762
+ // FROM KB
1936
2763
  body: whatCouldBeBody
1937
2764
  },
1938
2765
  classes: ["what-could-be-slide"]
@@ -1950,17 +2777,19 @@ var SlideFactory = class {
1950
2777
  logger.warn(`No number found for big-number slide, falling back to single-statement`);
1951
2778
  return this.createSingleStatementSlide(index, section);
1952
2779
  }
2780
+ const contextContent = bigNumber?.context || section.content;
2781
+ const craftedContext = this.craftExpertContent(
2782
+ contextContent,
2783
+ "big_number",
2784
+ this.config.defaults.context.maxWords
2785
+ );
1953
2786
  return {
1954
2787
  index,
1955
2788
  type: "big-number",
1956
2789
  data: {
1957
2790
  title: this.createTitle(section.header, section),
1958
2791
  keyMessage: actualValue,
1959
- body: bigNumber?.context || this.truncateText(
1960
- section.content,
1961
- this.config.defaults.context.maxWords
1962
- // FROM KB - not hardcoded 30
1963
- )
2792
+ body: craftedContext
1964
2793
  },
1965
2794
  classes: ["big-number-slide"]
1966
2795
  };
@@ -2007,7 +2836,8 @@ var SlideFactory = class {
2007
2836
  data: {
2008
2837
  title: this.createTitle(section.header, section),
2009
2838
  steps: steps.slice(0, maxSteps).map((step) => ({
2010
- label: step.label,
2839
+ label: this.cleanText(step.label),
2840
+ // Clean markdown from labels
2011
2841
  description: this.truncateText(
2012
2842
  step.description,
2013
2843
  this.config.defaults.step.maxWords
@@ -2028,7 +2858,8 @@ var SlideFactory = class {
2028
2858
  title: this.createTitle(section.header, section),
2029
2859
  steps: steps.slice(0, maxSteps).map((step, i) => ({
2030
2860
  number: i + 1,
2031
- title: step.label,
2861
+ title: this.cleanText(step.label),
2862
+ // Clean markdown from labels
2032
2863
  description: this.truncateText(
2033
2864
  step.description,
2034
2865
  this.config.defaults.step.maxWords
@@ -2063,6 +2894,69 @@ var SlideFactory = class {
2063
2894
  classes: ["three-column-slide"]
2064
2895
  };
2065
2896
  }
2897
+ /**
2898
+ * Create a three-points slide for exactly 3 key points.
2899
+ * Used for content that fits the "rule of three" (Carmine Gallo).
2900
+ */
2901
+ createThreePointsSlide(index, section) {
2902
+ const points = [];
2903
+ if (section.bullets.length >= 3) {
2904
+ for (let i = 0; i < 3; i++) {
2905
+ const bullet = section.bullets[i] || "";
2906
+ const extracted = this.classifier.extractSteps({ ...section, bullets: [bullet] });
2907
+ if (extracted.length > 0 && extracted[0] && extracted[0].description) {
2908
+ points.push({
2909
+ number: i + 1,
2910
+ title: this.cleanText(extracted[0].label) || `Step ${i + 1}`,
2911
+ description: this.truncateText(extracted[0].description, 20)
2912
+ });
2913
+ } else {
2914
+ const firstWords = this.cleanText(bullet).split(/\s+/).slice(0, 3).join(" ");
2915
+ points.push({
2916
+ number: i + 1,
2917
+ title: firstWords || `Step ${i + 1}`,
2918
+ description: this.truncateText(bullet, 20)
2919
+ });
2920
+ }
2921
+ }
2922
+ } else if (section.bullets.length > 0) {
2923
+ for (let i = 0; i < 3; i++) {
2924
+ const bullet = section.bullets[i % section.bullets.length] || "";
2925
+ const firstWords = this.cleanText(bullet).split(/\s+/).slice(0, 3).join(" ");
2926
+ points.push({
2927
+ number: i + 1,
2928
+ title: firstWords || `Step ${i + 1}`,
2929
+ description: this.truncateText(bullet, 20)
2930
+ });
2931
+ }
2932
+ } else {
2933
+ const sentences = section.content.split(/[.!?]+/).filter((s) => s.trim().length > 0).slice(0, 3);
2934
+ sentences.forEach((s, i) => {
2935
+ const firstWords = this.cleanText(s.trim()).split(/\s+/).slice(0, 3).join(" ");
2936
+ points.push({
2937
+ number: i + 1,
2938
+ title: firstWords || `Step ${i + 1}`,
2939
+ description: this.truncateText(s.trim(), 20)
2940
+ });
2941
+ });
2942
+ }
2943
+ return {
2944
+ index,
2945
+ type: "three-column",
2946
+ // Uses three-column template
2947
+ data: {
2948
+ title: this.createTitle(section.header, section),
2949
+ columns: points.map((p) => ({
2950
+ title: String(p.number),
2951
+ // Use the sequential number
2952
+ content: p.description,
2953
+ subtitle: p.title
2954
+ // Use extracted title as subtitle
2955
+ }))
2956
+ },
2957
+ classes: ["three-points-slide", "three-column-slide"]
2958
+ };
2959
+ }
2066
2960
  createTwoColumnSlide(index, section) {
2067
2961
  const midpoint = Math.ceil(section.bullets.length / 2);
2068
2962
  const leftBullets = section.bullets.slice(0, midpoint);
@@ -2125,10 +3019,22 @@ var SlideFactory = class {
2125
3019
  };
2126
3020
  }
2127
3021
  createBulletSlide(index, section) {
3022
+ logger.debug(`[TRACE] createBulletSlide for "${section.header}"`);
3023
+ logger.debug(`[TRACE] Input bullets: ${section.bullets?.length ?? 0}`);
2128
3024
  const maxBullets = this.config.rules.bulletsPerSlide.max;
2129
3025
  const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
2130
- const cleanedBullets = section.bullets.slice(0, maxBullets).map((b) => this.truncateText(this.cleanText(b), wordsPerBullet)).filter((b) => b.length > 0);
3026
+ logger.debug(`[TRACE] maxBullets: ${maxBullets}, wordsPerBullet: ${wordsPerBullet}`);
3027
+ const slicedBullets = section.bullets.slice(0, maxBullets);
3028
+ logger.debug(`[TRACE] After slice: ${slicedBullets.length} bullets`);
3029
+ const cleanedBullets = slicedBullets.map((b, i) => {
3030
+ const cleaned = this.cleanText(b);
3031
+ const truncated = this.truncateText(cleaned, wordsPerBullet);
3032
+ logger.debug(`[TRACE] Bullet ${i}: "${b.slice(0, 30)}..." \u2192 "${truncated.slice(0, 30)}..."`);
3033
+ return truncated;
3034
+ }).filter((b) => b.length > 0);
3035
+ logger.debug(`[TRACE] Final cleaned bullets: ${cleanedBullets.length}`);
2131
3036
  if (cleanedBullets.length === 0 && section.content) {
3037
+ logger.debug(`[TRACE] No bullets, falling back to single-statement`);
2132
3038
  return this.createSingleStatementSlide(index, section);
2133
3039
  }
2134
3040
  return {
@@ -2142,22 +3048,107 @@ var SlideFactory = class {
2142
3048
  };
2143
3049
  }
2144
3050
  createSingleStatementSlide(index, section) {
3051
+ const title = this.createTitle(section.header, section);
3052
+ const titleWords = title.split(/\s+/).length;
3053
+ const maxTotalWords = this.config.rules.wordsPerSlide.max;
3054
+ const maxBodyWords = Math.max(3, maxTotalWords - titleWords);
2145
3055
  const bodyContent = section.content || section.bullets.join(" ") || "";
2146
- const truncatedBody = this.truncateText(bodyContent, this.config.rules.wordsPerSlide.max);
2147
- if (truncatedBody.length < 15 && section.bullets.length > 0) {
2148
- return this.createBulletSlide(index, section);
3056
+ const craftedBody = this.craftExpertContent(
3057
+ bodyContent,
3058
+ "single_statement",
3059
+ // Tell expert engine what type of slide this is
3060
+ maxBodyWords
3061
+ );
3062
+ const normalizedBody = craftedBody.toLowerCase().replace(/[^a-z0-9]/g, "");
3063
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
3064
+ const bodyIsDuplicate = normalizedBody === normalizedTitle || craftedBody.length < 10;
3065
+ if (bodyIsDuplicate) {
3066
+ if (section.bullets.length >= 2 && this.config.mode === "business") {
3067
+ return this.createBulletSlide(index, section);
3068
+ }
3069
+ if (section.metrics.length >= 2) {
3070
+ return this.createMetricsGridSlide(index, section.metrics);
3071
+ }
3072
+ logger.warn(`Skipping slide "${title}" - no distinct body content`);
3073
+ return null;
2149
3074
  }
2150
3075
  return {
2151
3076
  index,
2152
3077
  type: "single-statement",
2153
3078
  data: {
2154
- title: this.createTitle(section.header, section),
2155
- body: truncatedBody || section.header
2156
- // Fallback to header if no body
3079
+ title,
3080
+ body: craftedBody
2157
3081
  },
2158
3082
  classes: ["single-statement-slide"]
2159
3083
  };
2160
3084
  }
3085
+ /**
3086
+ * Create a title impact slide - emphasized statement with key takeaway.
3087
+ * Used for section headers with strong conclusions.
3088
+ */
3089
+ createTitleImpactSlide(index, section) {
3090
+ const supportingText = section.content || section.bullets.slice(0, 2).join(". ");
3091
+ const truncatedSupport = this.truncateText(supportingText, this.config.defaults.context.maxWords);
3092
+ const data = {
3093
+ title: this.cleanText(section.header)
3094
+ };
3095
+ if (truncatedSupport) {
3096
+ data.body = truncatedSupport;
3097
+ }
3098
+ return {
3099
+ index,
3100
+ type: "single-statement",
3101
+ // Uses single-statement template
3102
+ data,
3103
+ classes: ["title-impact-slide", "single-statement-slide"]
3104
+ };
3105
+ }
3106
+ /**
3107
+ * Create a data insight slide - for presenting key findings with context.
3108
+ * Similar to big-number but works even without a prominent metric.
3109
+ */
3110
+ createDataInsightSlide(index, section, pattern) {
3111
+ const fullContent = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
3112
+ const bigNumber = this.classifier.extractBigNumber(fullContent);
3113
+ if (bigNumber?.value || pattern.bigNumberValue) {
3114
+ return this.createBigNumberSlide(index, section, pattern);
3115
+ }
3116
+ const keyInsight = section.content || section.bullets[0] || section.header;
3117
+ return {
3118
+ index,
3119
+ type: "single-statement",
3120
+ data: {
3121
+ title: this.createTitle(section.header, section),
3122
+ body: this.truncateText(keyInsight, this.config.rules.wordsPerSlide.max)
3123
+ },
3124
+ classes: ["data-insight-slide", "single-statement-slide"]
3125
+ };
3126
+ }
3127
+ /**
3128
+ * Create a detailed findings slide - bullet points with introductory context.
3129
+ */
3130
+ createDetailedFindingsSlide(index, section) {
3131
+ const introText = section.content ? this.truncateText(section.content, this.config.defaults.context.maxWords) : "";
3132
+ const maxBullets = this.config.rules.bulletsPerSlide.max;
3133
+ const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
3134
+ const cleanedBullets = section.bullets.slice(0, maxBullets).map((b) => this.truncateText(this.cleanText(b), wordsPerBullet)).filter((b) => b.length > 0);
3135
+ if (cleanedBullets.length === 0) {
3136
+ return this.createSingleStatementSlide(index, section);
3137
+ }
3138
+ const data = {
3139
+ title: this.createTitle(section.header, section),
3140
+ bullets: cleanedBullets
3141
+ };
3142
+ if (introText) {
3143
+ data.body = introText;
3144
+ }
3145
+ return {
3146
+ index,
3147
+ type: "bullet-points",
3148
+ data,
3149
+ classes: ["detailed-findings-slide", "bullet-points-slide"]
3150
+ };
3151
+ }
2161
3152
  createCodeSlide(index, section) {
2162
3153
  const codeMatch = section.content.match(/```[\s\S]*?```|`[^`]+`/);
2163
3154
  const code = codeMatch ? codeMatch[0].replace(/```/g, "").trim() : section.content;
@@ -2182,13 +3173,18 @@ var SlideFactory = class {
2182
3173
  createCTASlide(index, analysis) {
2183
3174
  const ctaDefaults = this.config.defaults.cta;
2184
3175
  const ctaText = analysis.sparkline?.callToAdventure || analysis.scqa?.answer || ctaDefaults.fallback;
3176
+ const craftedCTA = this.craftExpertContent(
3177
+ ctaText,
3178
+ "call_to_action",
3179
+ this.config.rules.wordsPerSlide.max
3180
+ );
2185
3181
  return {
2186
3182
  index,
2187
3183
  type: "cta",
2188
3184
  data: {
2189
3185
  title: ctaDefaults.title,
2190
3186
  // FROM KB - not hardcoded 'Next Steps'
2191
- body: this.truncateText(ctaText, this.config.rules.wordsPerSlide.max),
3187
+ body: craftedCTA,
2192
3188
  keyMessage: ctaDefaults.message
2193
3189
  // FROM KB - not hardcoded 'Ready to Begin?'
2194
3190
  },
@@ -2301,11 +3297,220 @@ var SlideFactory = class {
2301
3297
  }
2302
3298
  }
2303
3299
  // ===========================================================================
2304
- // HELPER METHODS - All use KB configuration
3300
+ // EXPERT-DRIVEN CONTENT CRAFTING (v8.0.0)
3301
+ // Instead of truncating content, we CRAFT it using expert principles
2305
3302
  // ===========================================================================
2306
3303
  /**
2307
- * Create a title for a slide - uses action titles for business mode per KB
2308
- */
3304
+ * Craft content using expert principles - NOT truncation.
3305
+ *
3306
+ * The old approach: "Take 100 words, chop to 15, add ellipsis"
3307
+ * The new approach: "What would Nancy Duarte say is the ONE thing to remember?"
3308
+ *
3309
+ * @param content - The raw content to craft from
3310
+ * @param slideType - The type of slide being created
3311
+ * @param maxWords - Word budget from KB
3312
+ * @returns Expertly crafted content that serves the slide's purpose
3313
+ */
3314
+ craftExpertContent(content, slideType, maxWords) {
3315
+ if (!content || content.trim().length === 0) return "";
3316
+ const guidance = this.expertEngine.getSlideDesignGuidance(slideType);
3317
+ switch (slideType) {
3318
+ case "title":
3319
+ case "title_impact":
3320
+ return this.extractCoreIdea(content, maxWords);
3321
+ case "star_moment":
3322
+ return this.extractDramaticElement(content, maxWords);
3323
+ case "big_number":
3324
+ case "data_insight":
3325
+ return this.extractDataInsight(content, maxWords);
3326
+ case "single_statement":
3327
+ case "big_idea":
3328
+ return this.extractSingleInsight(content, maxWords);
3329
+ case "call_to_action":
3330
+ case "cta":
3331
+ return this.extractActionableStep(content, maxWords);
3332
+ default:
3333
+ return this.extractCoreIdea(content, maxWords);
3334
+ }
3335
+ }
3336
+ /**
3337
+ * Chris Anderson's "One Idea Worth Spreading"
3338
+ * Extract the single most valuable insight from content.
3339
+ */
3340
+ extractCoreIdea(content, maxWords) {
3341
+ const cleaned = this.cleanText(content);
3342
+ const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
3343
+ let bestSentence = sentences[0] || cleaned;
3344
+ let bestScore = 0;
3345
+ for (const sentence of sentences) {
3346
+ let score = 0;
3347
+ const lowerSentence = sentence.toLowerCase();
3348
+ for (const marker of this.config.insightMarkers) {
3349
+ if (lowerSentence.includes(marker.toLowerCase())) {
3350
+ score += 10;
3351
+ }
3352
+ }
3353
+ if (/\d+%|\$[\d,]+|\d+x|\d+\+/.test(sentence)) {
3354
+ score += 5;
3355
+ }
3356
+ if (/transform|revolutionize|breakthrough|critical|essential|must|never|always/i.test(sentence)) {
3357
+ score += 3;
3358
+ }
3359
+ if (/overview|introduction|agenda|summary|the following/i.test(sentence)) {
3360
+ score -= 10;
3361
+ }
3362
+ if (score > bestScore) {
3363
+ bestScore = score;
3364
+ bestSentence = sentence.trim();
3365
+ }
3366
+ }
3367
+ const words = bestSentence.split(/\s+/);
3368
+ if (words.length <= maxWords) {
3369
+ return bestSentence;
3370
+ }
3371
+ return this.extractKeyPhrase(bestSentence, maxWords);
3372
+ }
3373
+ /**
3374
+ * Extract the key phrase from a sentence - the part that matters most.
3375
+ */
3376
+ extractKeyPhrase(sentence, maxWords) {
3377
+ const patterns = [
3378
+ /(?:the key (?:is|to)|critical|essential|must|should)\s+(.+)/i,
3379
+ /(.+?)\s+(?:is critical|is essential|matters most)/i,
3380
+ /^(.+?)\s*[-–—:]\s*.+$/
3381
+ // Before a colon/dash is often the key
3382
+ ];
3383
+ for (const pattern of patterns) {
3384
+ const match = sentence.match(pattern);
3385
+ if (match && match[1]) {
3386
+ const extracted = match[1].trim();
3387
+ if (extracted.split(/\s+/).length <= maxWords) {
3388
+ return extracted;
3389
+ }
3390
+ }
3391
+ }
3392
+ const words = sentence.split(/\s+/);
3393
+ let result = words.slice(0, maxWords).join(" ");
3394
+ const lastPunctuation = result.lastIndexOf(",");
3395
+ if (lastPunctuation > result.length / 2) {
3396
+ result = result.slice(0, lastPunctuation);
3397
+ }
3398
+ return result;
3399
+ }
3400
+ /**
3401
+ * Nancy Duarte's "STAR Moment" - Something They'll Always Remember
3402
+ * Find the most dramatic, unexpected element.
3403
+ */
3404
+ extractDramaticElement(content, maxWords) {
3405
+ const cleaned = this.cleanText(content);
3406
+ const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
3407
+ let mostDramatic = sentences[0] || cleaned;
3408
+ let highestDrama = 0;
3409
+ for (const sentence of sentences) {
3410
+ let drama = 0;
3411
+ const numbers = sentence.match(/\d+/g);
3412
+ if (numbers) {
3413
+ for (const num of numbers) {
3414
+ const value = parseInt(num);
3415
+ if (value >= 1e3) drama += 15;
3416
+ else if (value >= 100) drama += 10;
3417
+ else if (value >= 50) drama += 5;
3418
+ }
3419
+ }
3420
+ if (/\d+%/.test(sentence)) drama += 10;
3421
+ if (/\$[\d,]+[MBK]?/i.test(sentence)) drama += 12;
3422
+ if (/never|always|revolutionary|transforming|critical|dramatic|shocking|surprising/i.test(sentence)) {
3423
+ drama += 8;
3424
+ }
3425
+ if (/but|however|yet|instead|versus|vs/i.test(sentence)) {
3426
+ drama += 5;
3427
+ }
3428
+ if (drama > highestDrama) {
3429
+ highestDrama = drama;
3430
+ mostDramatic = sentence.trim();
3431
+ }
3432
+ }
3433
+ const words = mostDramatic.split(/\s+/);
3434
+ if (words.length <= maxWords) {
3435
+ return mostDramatic;
3436
+ }
3437
+ return this.extractKeyPhrase(mostDramatic, maxWords);
3438
+ }
3439
+ /**
3440
+ * Edward Tufte / Cole Knaflic - Extract data insight with context
3441
+ */
3442
+ extractDataInsight(content, maxWords) {
3443
+ const cleaned = this.cleanText(content);
3444
+ const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
3445
+ const datasentences = sentences.filter(
3446
+ (s) => /\d+%|\$[\d,]+|\d+x|\d+ (million|billion|thousand)/i.test(s)
3447
+ );
3448
+ if (datasentences.length > 0 && datasentences[0]) {
3449
+ const best = datasentences[0].trim();
3450
+ const words = best.split(/\s+/);
3451
+ if (words.length <= maxWords) {
3452
+ return best;
3453
+ }
3454
+ return this.extractKeyPhrase(best, maxWords);
3455
+ }
3456
+ return this.extractCoreIdea(content, maxWords);
3457
+ }
3458
+ /**
3459
+ * Garr Reynolds - Signal to Noise
3460
+ * Extract the ONE insight, eliminate everything else.
3461
+ */
3462
+ extractSingleInsight(content, maxWords) {
3463
+ const cleaned = this.cleanText(content);
3464
+ const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
3465
+ const conclusionPatterns = [
3466
+ /(?:therefore|thus|hence|consequently|as a result|this means|the bottom line|in conclusion)/i,
3467
+ /(?:we (?:must|should|need to)|it's (?:critical|essential|important) to)/i,
3468
+ /(?:the key (?:is|takeaway)|what this means)/i
3469
+ ];
3470
+ for (const sentence of sentences) {
3471
+ for (const pattern of conclusionPatterns) {
3472
+ if (pattern.test(sentence)) {
3473
+ const words = sentence.trim().split(/\s+/);
3474
+ if (words.length <= maxWords) {
3475
+ return sentence.trim();
3476
+ }
3477
+ return this.extractKeyPhrase(sentence, maxWords);
3478
+ }
3479
+ }
3480
+ }
3481
+ return this.extractCoreIdea(content, maxWords);
3482
+ }
3483
+ /**
3484
+ * Chris Anderson - Gift Giving
3485
+ * What can they DO tomorrow?
3486
+ */
3487
+ extractActionableStep(content, maxWords) {
3488
+ const cleaned = this.cleanText(content);
3489
+ const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
3490
+ const actionPatterns = [
3491
+ /(?:start|begin|try|implement|apply|use|adopt|build|create|develop)/i,
3492
+ /(?:step \d|first,|next,|then,|finally)/i,
3493
+ /(?:you (?:can|should|must|need to))/i
3494
+ ];
3495
+ for (const sentence of sentences) {
3496
+ for (const pattern of actionPatterns) {
3497
+ if (pattern.test(sentence)) {
3498
+ const words = sentence.trim().split(/\s+/);
3499
+ if (words.length <= maxWords) {
3500
+ return sentence.trim();
3501
+ }
3502
+ return this.extractKeyPhrase(sentence, maxWords);
3503
+ }
3504
+ }
3505
+ }
3506
+ return this.extractCoreIdea(content, maxWords);
3507
+ }
3508
+ // ===========================================================================
3509
+ // HELPER METHODS - All use KB configuration
3510
+ // ===========================================================================
3511
+ /**
3512
+ * Create a title for a slide - uses action titles for business mode per KB
3513
+ */
2309
3514
  createTitle(header, section) {
2310
3515
  const cleanHeader = this.cleanText(header);
2311
3516
  if (this.usedTitles.has(cleanHeader.toLowerCase())) {
@@ -3949,949 +5154,554 @@ var QAEngine = class {
3949
5154
  }
3950
5155
  };
3951
5156
 
3952
- // src/qa/SevenDimensionScorer.ts
3953
- var DIMENSION_WEIGHTS = {
3954
- layout: 0.15,
3955
- contrast: 0.15,
3956
- graphics: 0.1,
3957
- content: 0.2,
3958
- clarity: 0.15,
3959
- effectiveness: 0.15,
3960
- consistency: 0.1
3961
- };
3962
- var SevenDimensionScorer = class {
3963
- kb;
3964
- mode;
3965
- presentationType;
3966
- constructor(mode, presentationType) {
3967
- this.mode = mode;
3968
- this.presentationType = presentationType;
3969
- }
3970
- /**
3971
- * Score a presentation across all 7 dimensions.
3972
- */
3973
- async score(slides, html, threshold = 95) {
3974
- this.kb = await getKnowledgeGateway();
3975
- const layout = await this.scoreLayout(slides, html);
3976
- const contrast = await this.scoreContrast(html);
3977
- const graphics = await this.scoreGraphics(slides);
3978
- const content = await this.scoreContent(slides);
3979
- const clarity = await this.scoreClarity(slides);
3980
- const effectiveness = await this.scoreEffectiveness(slides);
3981
- const consistency = await this.scoreConsistency(slides, html);
3982
- const overallScore = Math.round(
3983
- 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
3984
- );
3985
- const issues = [
3986
- ...layout.issues,
3987
- ...contrast.issues,
3988
- ...graphics.issues,
3989
- ...content.issues,
3990
- ...clarity.issues,
3991
- ...effectiveness.issues,
3992
- ...consistency.issues
3993
- ];
3994
- return {
3995
- overallScore,
3996
- dimensions: {
3997
- layout,
3998
- contrast,
3999
- graphics,
4000
- content,
4001
- clarity,
4002
- effectiveness,
4003
- consistency
4004
- },
4005
- issues,
4006
- passed: overallScore >= threshold,
4007
- threshold
4008
- };
4009
- }
4010
- /**
4011
- * Score layout dimension (whitespace, visual balance, structure)
4012
- */
4013
- async scoreLayout(slides, html) {
4014
- const issues = [];
4015
- let totalScore = 0;
4016
- let checks = 0;
4017
- const minWhitespace = this.mode === "keynote" ? 0.45 : 0.35;
4018
- const maxWhitespace = 0.6;
4019
- const slideSections = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
4020
- for (let i = 0; i < slideSections.length; i++) {
4021
- const section = slideSections[i];
4022
- if (!section) continue;
4023
- const textContent = section.replace(/<[^>]+>/g, "").trim();
4024
- const totalArea = 1920 * 1080;
4025
- const estimatedTextArea = textContent.length * 100;
4026
- const whitespaceRatio = 1 - Math.min(estimatedTextArea / totalArea, 1);
4027
- if (whitespaceRatio < minWhitespace) {
4028
- issues.push({
4029
- slideIndex: i,
4030
- dimension: "layout",
4031
- severity: "error",
4032
- message: `Slide ${i + 1}: Insufficient whitespace (${Math.round(whitespaceRatio * 100)}% < ${Math.round(minWhitespace * 100)}%)`,
4033
- currentValue: whitespaceRatio,
4034
- expectedValue: minWhitespace,
4035
- autoFixable: true,
4036
- fixSuggestion: "Reduce content or increase margins"
4037
- });
4038
- totalScore += 50;
4039
- } else if (whitespaceRatio > maxWhitespace) {
4040
- issues.push({
4041
- slideIndex: i,
4042
- dimension: "layout",
4043
- severity: "warning",
4044
- message: `Slide ${i + 1}: Too much whitespace (${Math.round(whitespaceRatio * 100)}% > ${Math.round(maxWhitespace * 100)}%)`,
4045
- currentValue: whitespaceRatio,
4046
- expectedValue: maxWhitespace,
4047
- autoFixable: false,
4048
- fixSuggestion: "Add more content or reduce margins"
4049
- });
4050
- totalScore += 80;
4051
- } else {
4052
- totalScore += 100;
4053
- }
4054
- checks++;
4055
- }
4056
- for (let i = 0; i < slides.length; i++) {
4057
- const slide = slides[i];
4058
- if (!slide) continue;
4059
- if (!["thank-you", "section-divider"].includes(slide.type)) {
4060
- if (!slide.data.title || slide.data.title.trim().length === 0) {
4061
- issues.push({
4062
- slideIndex: i,
4063
- dimension: "layout",
4064
- severity: "warning",
4065
- message: `Slide ${i + 1}: Missing title`,
4066
- autoFixable: false,
4067
- fixSuggestion: "Add a clear slide title"
4068
- });
4069
- totalScore += 70;
4070
- } else {
4071
- totalScore += 100;
4072
- }
4073
- checks++;
4074
- }
4075
- }
4076
- const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4077
- return {
4078
- name: "Layout",
4079
- score,
4080
- weight: DIMENSION_WEIGHTS.layout,
4081
- issues,
4082
- details: {
4083
- slidesAnalyzed: slides.length,
4084
- whitespaceTarget: `${Math.round(minWhitespace * 100)}-${Math.round(maxWhitespace * 100)}%`
4085
- }
4086
- };
5157
+ // src/qa/VisualQualityEvaluator.ts
5158
+ import * as path from "path";
5159
+ import * as fs from "fs";
5160
+ var VisualQualityEvaluator = class {
5161
+ browser = null;
5162
+ page = null;
5163
+ screenshotDir;
5164
+ constructor(screenshotDir = "/tmp/presentation-qa") {
5165
+ this.screenshotDir = screenshotDir;
4087
5166
  }
4088
5167
  /**
4089
- * Score contrast dimension (WCAG compliance, readability)
5168
+ * Evaluate a presentation's visual quality.
5169
+ * This opens the HTML in a real browser and evaluates each slide.
4090
5170
  */
4091
- async scoreContrast(html) {
4092
- const issues = [];
4093
- let score = 100;
4094
- const minContrastRatio = 4.5;
4095
- const colorMatches = html.match(/color:\s*([^;]+);/gi) || [];
4096
- const bgColorMatches = html.match(/background(-color)?:\s*([^;]+);/gi) || [];
4097
- const hasGoodContrast = html.includes("color: #fff") || html.includes("color: white") || html.includes("color: #18181B") || html.includes("color: #F5F5F4");
4098
- const hasDarkBackground = html.includes("background-color: #18181B") || html.includes("background: #18181B") || html.includes("background-color: rgb(24, 24, 27)");
4099
- if (html.includes("color: gray") || html.includes("color: #999") || html.includes("color: #888")) {
4100
- issues.push({
4101
- slideIndex: -1,
4102
- dimension: "contrast",
4103
- severity: "error",
4104
- message: "Low contrast text color detected (gray text)",
4105
- currentValue: "gray",
4106
- expectedValue: "High contrast color",
4107
- autoFixable: true,
4108
- fixSuggestion: "Use white (#fff) or dark (#18181B) text depending on background"
4109
- });
4110
- score -= 20;
5171
+ async evaluate(htmlPath) {
5172
+ if (!fs.existsSync(this.screenshotDir)) {
5173
+ fs.mkdirSync(this.screenshotDir, { recursive: true });
4111
5174
  }
4112
- if (!hasGoodContrast && !hasDarkBackground) {
4113
- issues.push({
4114
- slideIndex: -1,
4115
- dimension: "contrast",
4116
- severity: "warning",
4117
- message: "Could not verify WCAG-compliant contrast ratios",
4118
- autoFixable: false,
4119
- fixSuggestion: "Ensure text has 4.5:1 contrast ratio against background"
4120
- });
4121
- score -= 10;
4122
- }
4123
- const hasSmallFont = html.match(/font-size:\s*(1[0-3]|[0-9])px/i) !== null;
4124
- if (hasSmallFont) {
4125
- issues.push({
4126
- slideIndex: -1,
4127
- dimension: "contrast",
4128
- severity: "error",
4129
- message: "Font size too small for presentation (< 14px)",
4130
- currentValue: "Small font",
4131
- expectedValue: "18px minimum for body text",
4132
- autoFixable: true,
4133
- fixSuggestion: "Increase font size to minimum 18px"
4134
- });
4135
- score -= 15;
4136
- }
4137
- return {
4138
- name: "Contrast",
4139
- score: Math.max(0, score),
4140
- weight: DIMENSION_WEIGHTS.contrast,
4141
- issues,
4142
- details: {
4143
- wcagLevel: "AA",
4144
- minContrastRatio,
4145
- colorDefinitions: colorMatches.length,
4146
- backgroundDefinitions: bgColorMatches.length
4147
- }
4148
- };
4149
- }
4150
- /**
4151
- * Score graphics dimension (images, placement, relevance)
4152
- */
4153
- async scoreGraphics(slides) {
4154
- const issues = [];
4155
- let score = 100;
4156
- let slidesWithImages = 0;
4157
- let totalImageSlides = 0;
4158
- for (let i = 0; i < slides.length; i++) {
4159
- const slide = slides[i];
4160
- if (!slide) continue;
4161
- const shouldHaveImage = ["hero", "image", "feature"].includes(slide.type);
4162
- if (shouldHaveImage) {
4163
- totalImageSlides++;
4164
- if (slide.data.image || slide.data.backgroundImage) {
4165
- slidesWithImages++;
4166
- } else {
4167
- issues.push({
4168
- slideIndex: i,
4169
- dimension: "graphics",
4170
- severity: "warning",
4171
- message: `Slide ${i + 1} (${slide.type}): Missing expected image`,
4172
- autoFixable: false,
4173
- fixSuggestion: "Add a relevant image or change slide type"
4174
- });
4175
- score -= 5;
4176
- }
5175
+ try {
5176
+ await this.launchBrowser();
5177
+ const absolutePath = path.resolve(htmlPath);
5178
+ await this.page.goto(`file://${absolutePath}`, { waitUntil: "networkidle" });
5179
+ await this.page.waitForSelector(".reveal", { timeout: 5e3 });
5180
+ await this.page.waitForTimeout(1e3);
5181
+ const slideCount = await this.getSlideCount();
5182
+ console.log(`Evaluating ${slideCount} slides...`);
5183
+ const slideScores = [];
5184
+ for (let i = 0; i < slideCount; i++) {
5185
+ const score = await this.evaluateSlide(i);
5186
+ slideScores.push(score);
4177
5187
  }
5188
+ const narrativeFlow = this.evaluateNarrativeFlow(slideScores);
5189
+ const visualConsistency = this.evaluateVisualConsistency(slideScores);
5190
+ const contentQuality = this.evaluateContentQuality(slideScores);
5191
+ const executiveReadiness = this.evaluateExecutiveReadiness(slideScores);
5192
+ const overallScore = Math.round(
5193
+ narrativeFlow.score + visualConsistency.score + contentQuality.score + executiveReadiness.score
5194
+ );
5195
+ const verdict = this.determineVerdict(overallScore);
5196
+ const { topIssues, topStrengths } = this.extractTopIssuesAndStrengths(slideScores);
5197
+ return {
5198
+ overallScore,
5199
+ narrativeFlow,
5200
+ visualConsistency,
5201
+ contentQuality,
5202
+ executiveReadiness,
5203
+ slideScores,
5204
+ verdict,
5205
+ verdictExplanation: this.explainVerdict(verdict, overallScore),
5206
+ topIssues,
5207
+ topStrengths
5208
+ };
5209
+ } finally {
5210
+ await this.closeBrowser();
4178
5211
  }
4179
- return {
4180
- name: "Graphics",
4181
- score: Math.max(0, score),
4182
- weight: DIMENSION_WEIGHTS.graphics,
4183
- issues,
4184
- details: {
4185
- slidesWithImages,
4186
- expectedImageSlides: totalImageSlides,
4187
- imageRatio: totalImageSlides > 0 ? slidesWithImages / totalImageSlides : 1
4188
- }
4189
- };
4190
5212
  }
4191
- /**
4192
- * Score content dimension (word limits, bullet counts, structure)
4193
- * This is critical - must use KB rules exactly.
4194
- */
4195
- async scoreContent(slides) {
4196
- const issues = [];
4197
- let totalScore = 0;
4198
- let checks = 0;
4199
- const wordLimits = this.kb.getWordLimits(this.mode);
4200
- const bulletLimits = this.kb.getBulletLimits(this.mode);
4201
- const defaultMaxWords = this.mode === "keynote" ? 25 : 80;
4202
- const defaultMinWords = this.mode === "keynote" ? 3 : 15;
4203
- const defaultMaxBullets = 5;
4204
- for (let i = 0; i < slides.length; i++) {
4205
- const slide = slides[i];
4206
- if (!slide) continue;
4207
- const wordCount = this.countWords(slide);
4208
- const slideType = slide.type;
4209
- const maxWords = wordLimits[slideType] ?? defaultMaxWords;
4210
- const minWords = defaultMinWords;
4211
- if (wordCount > maxWords) {
4212
- const severity = wordCount > maxWords * 1.5 ? "error" : "warning";
4213
- issues.push({
4214
- slideIndex: i,
4215
- dimension: "content",
4216
- severity,
4217
- message: `Slide ${i + 1}: Word count ${wordCount} exceeds limit of ${maxWords} for ${this.mode} mode`,
4218
- currentValue: wordCount,
4219
- expectedValue: maxWords,
4220
- autoFixable: true,
4221
- fixSuggestion: "Condense text to key points only"
4222
- });
4223
- totalScore += severity === "error" ? 40 : 70;
4224
- } else if (wordCount < minWords && !["title", "section-divider", "thank-you"].includes(slide.type)) {
4225
- issues.push({
4226
- slideIndex: i,
4227
- dimension: "content",
4228
- severity: "warning",
4229
- message: `Slide ${i + 1}: Word count ${wordCount} may be too sparse (min: ${minWords})`,
4230
- currentValue: wordCount,
4231
- expectedValue: minWords,
4232
- autoFixable: false,
4233
- fixSuggestion: "Add supporting content"
4234
- });
4235
- totalScore += 80;
4236
- } else {
4237
- totalScore += 100;
4238
- }
4239
- checks++;
4240
- if (slide.data.bullets && Array.isArray(slide.data.bullets)) {
4241
- const bulletCount = slide.data.bullets.length;
4242
- const maxBullets = bulletLimits[slideType] ?? defaultMaxBullets;
4243
- if (bulletCount > maxBullets) {
4244
- issues.push({
4245
- slideIndex: i,
4246
- dimension: "content",
4247
- severity: "error",
4248
- message: `Slide ${i + 1}: ${bulletCount} bullets exceeds limit of ${maxBullets}`,
4249
- currentValue: bulletCount,
4250
- expectedValue: maxBullets,
4251
- autoFixable: true,
4252
- fixSuggestion: "Reduce to top 3-5 key points"
4253
- });
4254
- totalScore += 50;
4255
- } else {
4256
- totalScore += 100;
4257
- }
4258
- checks++;
4259
- }
4260
- }
4261
- const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4262
- return {
4263
- name: "Content",
4264
- score,
4265
- weight: DIMENSION_WEIGHTS.content,
4266
- issues,
4267
- details: {
4268
- mode: this.mode,
4269
- wordLimits,
4270
- bulletLimits,
4271
- totalSlides: slides.length
4272
- }
4273
- };
5213
+ // ===========================================================================
5214
+ // BROWSER MANAGEMENT
5215
+ // ===========================================================================
5216
+ async launchBrowser() {
5217
+ const { chromium: chromium2 } = await import("playwright");
5218
+ this.browser = await chromium2.launch({ headless: true });
5219
+ const context = await this.browser.newContext({
5220
+ viewport: { width: 1920, height: 1080 }
5221
+ });
5222
+ this.page = await context.newPage();
4274
5223
  }
4275
- /**
4276
- * Score clarity dimension (message focus, information density)
4277
- */
4278
- async scoreClarity(slides) {
4279
- const issues = [];
4280
- let totalScore = 0;
4281
- let checks = 0;
4282
- for (let i = 0; i < slides.length; i++) {
4283
- const slide = slides[i];
4284
- if (!slide) continue;
4285
- if (slide.data.keyMessage) {
4286
- const keyMessageWords = slide.data.keyMessage.split(/\s+/).length;
4287
- const maxKeyMessageWords = this.mode === "keynote" ? 15 : 25;
4288
- if (keyMessageWords > maxKeyMessageWords) {
4289
- issues.push({
4290
- slideIndex: i,
4291
- dimension: "clarity",
4292
- severity: "warning",
4293
- message: `Slide ${i + 1}: Key message too long (${keyMessageWords} words > ${maxKeyMessageWords})`,
4294
- currentValue: keyMessageWords,
4295
- expectedValue: maxKeyMessageWords,
4296
- autoFixable: true,
4297
- fixSuggestion: "Shorten key message to one impactful sentence"
4298
- });
4299
- totalScore += 70;
4300
- } else {
4301
- totalScore += 100;
4302
- }
4303
- checks++;
4304
- }
4305
- if (slide.data.title) {
4306
- const title = slide.data.title;
4307
- const titleWords = title.split(/\s+/).length;
4308
- if (titleWords > 10) {
4309
- issues.push({
4310
- slideIndex: i,
4311
- dimension: "clarity",
4312
- severity: "warning",
4313
- message: `Slide ${i + 1}: Title too long (${titleWords} words)`,
4314
- currentValue: titleWords,
4315
- expectedValue: "2-8 words",
4316
- autoFixable: true,
4317
- fixSuggestion: "Use action-oriented, concise title"
4318
- });
4319
- totalScore += 75;
4320
- } else {
4321
- totalScore += 100;
4322
- }
4323
- checks++;
4324
- }
4325
- const elementCount = (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);
4326
- const maxElements = this.mode === "keynote" ? 4 : 6;
4327
- if (elementCount > maxElements) {
4328
- issues.push({
4329
- slideIndex: i,
4330
- dimension: "clarity",
4331
- severity: "warning",
4332
- message: `Slide ${i + 1}: Too many elements (${elementCount} > ${maxElements})`,
4333
- currentValue: elementCount,
4334
- expectedValue: maxElements,
4335
- autoFixable: true,
4336
- fixSuggestion: "Split into multiple slides for clarity"
4337
- });
4338
- totalScore += 70;
4339
- } else {
4340
- totalScore += 100;
4341
- }
4342
- checks++;
5224
+ async closeBrowser() {
5225
+ if (this.browser) {
5226
+ await this.browser.close();
5227
+ this.browser = null;
5228
+ this.page = null;
4343
5229
  }
4344
- const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4345
- return {
4346
- name: "Clarity",
4347
- score,
4348
- weight: DIMENSION_WEIGHTS.clarity,
4349
- issues,
4350
- details: {
4351
- slidesAnalyzed: slides.length,
4352
- mode: this.mode
4353
- }
4354
- };
4355
5230
  }
4356
- /**
4357
- * Score effectiveness dimension (expert methodology compliance)
4358
- */
4359
- async scoreEffectiveness(slides) {
4360
- const issues = [];
4361
- let score = 100;
4362
- if (slides.length >= 3) {
4363
- const firstSlide = slides[0];
4364
- const lastSlide = slides[slides.length - 1];
4365
- if (firstSlide && !["title", "hero"].includes(firstSlide.type)) {
4366
- issues.push({
4367
- slideIndex: 0,
4368
- dimension: "effectiveness",
4369
- severity: "warning",
4370
- message: "Presentation should start with a title or hero slide",
4371
- currentValue: firstSlide.type,
4372
- expectedValue: "title or hero",
4373
- autoFixable: false,
4374
- fixSuggestion: "Add a compelling opening slide"
4375
- });
4376
- score -= 10;
4377
- }
4378
- if (lastSlide && !["thank-you", "cta", "closing"].includes(lastSlide.type)) {
4379
- issues.push({
4380
- slideIndex: slides.length - 1,
4381
- dimension: "effectiveness",
4382
- severity: "warning",
4383
- message: "Presentation should end with a closing or CTA slide",
4384
- currentValue: lastSlide.type,
4385
- expectedValue: "thank-you, cta, or closing",
4386
- autoFixable: false,
4387
- fixSuggestion: "Add a clear call-to-action or closing"
4388
- });
4389
- score -= 10;
4390
- }
4391
- }
4392
- const keyMessages = slides.filter((s) => s.data.keyMessage);
4393
- if (keyMessages.length > 0 && keyMessages.length !== 3 && keyMessages.length > 4) {
4394
- issues.push({
4395
- slideIndex: -1,
4396
- dimension: "effectiveness",
4397
- severity: "info",
4398
- message: `Consider using Rule of Three: ${keyMessages.length} key messages found`,
4399
- currentValue: keyMessages.length,
4400
- expectedValue: 3,
4401
- autoFixable: false,
4402
- fixSuggestion: "Group messages into 3 main themes"
4403
- });
4404
- score -= 5;
4405
- }
4406
- const hasScqaElements = slides.some(
4407
- (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")
4408
- );
4409
- if (!hasScqaElements && this.presentationType === "consulting_deck") {
4410
- issues.push({
4411
- slideIndex: -1,
4412
- dimension: "effectiveness",
4413
- severity: "warning",
4414
- message: "Consulting deck should follow SCQA structure (Situation, Complication, Question, Answer)",
4415
- autoFixable: false,
4416
- fixSuggestion: "Organize content using Barbara Minto Pyramid Principle"
4417
- });
4418
- score -= 10;
4419
- }
4420
- const firstSlideType = slides[0]?.type;
4421
- const lastSlideType = slides[slides.length - 1]?.type;
4422
- return {
4423
- name: "Effectiveness",
4424
- score: Math.max(0, score),
4425
- weight: DIMENSION_WEIGHTS.effectiveness,
4426
- issues,
4427
- details: {
4428
- presentationType: this.presentationType,
4429
- slideCount: slides.length,
4430
- hasOpeningSlide: firstSlideType ? ["title", "hero"].includes(firstSlideType) : false,
4431
- hasClosingSlide: lastSlideType ? ["thank-you", "cta", "closing"].includes(lastSlideType) : false
4432
- }
4433
- };
5231
+ async getSlideCount() {
5232
+ return await this.page.evaluate(() => {
5233
+ const slides = document.querySelectorAll(".slides > section");
5234
+ return slides.length;
5235
+ });
4434
5236
  }
4435
- /**
4436
- * Score consistency dimension (style uniformity, design coherence)
4437
- */
4438
- async scoreConsistency(slides, html) {
4439
- const issues = [];
4440
- let score = 100;
4441
- const hasCssVariables = html.includes("var(--") || html.includes(":root");
4442
- if (!hasCssVariables) {
4443
- issues.push({
4444
- slideIndex: -1,
4445
- dimension: "consistency",
4446
- severity: "warning",
4447
- message: "Presentation lacks CSS variables for consistent styling",
4448
- autoFixable: true,
4449
- fixSuggestion: "Use CSS variables for colors, fonts, and spacing"
4450
- });
4451
- score -= 10;
4452
- }
4453
- const titlePatterns = /* @__PURE__ */ new Set();
4454
- for (const slide of slides) {
4455
- if (slide.data.title) {
4456
- const isUpperCase = slide.data.title === slide.data.title.toUpperCase();
4457
- const words = slide.data.title.split(" ").filter((w) => w.length > 0);
4458
- const isTitleCase = words.length > 0 && words.every(
4459
- (w) => w.length > 0 && w[0] === w[0]?.toUpperCase()
4460
- );
4461
- titlePatterns.add(isUpperCase ? "UPPER" : isTitleCase ? "Title" : "sentence");
4462
- }
4463
- }
4464
- if (titlePatterns.size > 1) {
4465
- issues.push({
4466
- slideIndex: -1,
4467
- dimension: "consistency",
4468
- severity: "warning",
4469
- message: `Inconsistent title casing: ${Array.from(titlePatterns).join(", ")}`,
4470
- autoFixable: true,
4471
- fixSuggestion: "Use consistent title case throughout"
4472
- });
4473
- score -= 10;
4474
- }
4475
- const fontMatches = html.match(/font-family:\s*([^;]+);/gi) || [];
4476
- const uniqueFonts = new Set(fontMatches.map((f) => f.toLowerCase()));
4477
- if (uniqueFonts.size > 3) {
4478
- issues.push({
4479
- slideIndex: -1,
4480
- dimension: "consistency",
4481
- severity: "warning",
4482
- message: `Too many font families (${uniqueFonts.size} > 3)`,
4483
- autoFixable: true,
4484
- fixSuggestion: "Use 2-3 complementary fonts max"
4485
- });
4486
- score -= 10;
4487
- }
4488
- return {
4489
- name: "Consistency",
4490
- score: Math.max(0, score),
4491
- weight: DIMENSION_WEIGHTS.consistency,
4492
- issues,
4493
- details: {
4494
- hasCssVariables,
4495
- titlePatterns: Array.from(titlePatterns),
4496
- fontFamilyCount: uniqueFonts.size
5237
+ // ===========================================================================
5238
+ // SLIDE-LEVEL EVALUATION
5239
+ // ===========================================================================
5240
+ async evaluateSlide(slideIndex) {
5241
+ await this.page.evaluate((index) => {
5242
+ window.Reveal?.slide(index, 0);
5243
+ }, slideIndex);
5244
+ await this.page.waitForTimeout(600);
5245
+ await this.page.evaluate(() => {
5246
+ const currentSlide = document.querySelector(".present");
5247
+ if (currentSlide) {
5248
+ currentSlide.getAnimations().forEach((anim) => anim.finish());
4497
5249
  }
4498
- };
4499
- }
4500
- /**
4501
- * Count words in a slide.
4502
- */
4503
- countWords(slide) {
4504
- let text = "";
4505
- if (slide.data.title) text += slide.data.title + " ";
4506
- if (slide.data.subtitle) text += slide.data.subtitle + " ";
4507
- if (slide.data.body) text += slide.data.body + " ";
4508
- if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
4509
- if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
4510
- return text.split(/\s+/).filter((w) => w.length > 0).length;
5250
+ });
5251
+ await this.page.waitForTimeout(100);
5252
+ const screenshotPath = path.join(this.screenshotDir, `slide-${slideIndex}.png`);
5253
+ await this.page.screenshot({ path: screenshotPath, fullPage: false });
5254
+ const slideData = await this.page.evaluate(() => {
5255
+ const currentSlide = document.querySelector(".present");
5256
+ if (!currentSlide) return null;
5257
+ const title = currentSlide.querySelector("h1, h2, .title")?.textContent?.trim() || "";
5258
+ const body = currentSlide.querySelector(".body, p:not(.subtitle)")?.textContent?.trim() || "";
5259
+ const bullets = Array.from(currentSlide.querySelectorAll("li")).map((li) => li.textContent?.trim() || "");
5260
+ const hasImage = !!currentSlide.querySelector("img");
5261
+ const hasChart = !!currentSlide.querySelector(".chart, svg, canvas");
5262
+ const classList = Array.from(currentSlide.classList);
5263
+ const backgroundColor = window.getComputedStyle(currentSlide).backgroundColor;
5264
+ const titleEl = currentSlide.querySelector("h1, h2, .title");
5265
+ const titleStyles = titleEl ? window.getComputedStyle(titleEl) : null;
5266
+ return {
5267
+ title,
5268
+ body,
5269
+ bullets,
5270
+ hasImage,
5271
+ hasChart,
5272
+ classList,
5273
+ backgroundColor,
5274
+ titleFontSize: titleStyles?.fontSize || "",
5275
+ titleColor: titleStyles?.color || "",
5276
+ contentLength: (title + body + bullets.join(" ")).length
5277
+ };
5278
+ });
5279
+ return this.scoreSlide(slideIndex, slideData, screenshotPath);
4511
5280
  }
4512
- /**
4513
- * Get a formatted report of the scoring results.
4514
- */
4515
- formatReport(result) {
4516
- const lines = [];
4517
- 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");
4518
- lines.push("\u2551 7-DIMENSION QUALITY ASSESSMENT \u2551");
4519
- 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");
4520
- lines.push("");
4521
- lines.push(`Overall Score: ${result.overallScore}/100 ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
4522
- lines.push(`Threshold: ${result.threshold}/100`);
4523
- lines.push("");
4524
- lines.push("Dimension Breakdown:");
4525
- 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");
4526
- const dims = result.dimensions;
4527
- const formatDim = (name, d) => {
4528
- const bar = "\u2588".repeat(Math.floor(d.score / 10)) + "\u2591".repeat(10 - Math.floor(d.score / 10));
4529
- const status = d.score >= 95 ? "\u2705" : d.score >= 80 ? "\u26A0\uFE0F" : "\u274C";
4530
- return `${status} ${name.padEnd(14)} ${bar} ${d.score.toString().padStart(3)}/100 (${(d.weight * 100).toFixed(0)}%)`;
4531
- };
4532
- lines.push(formatDim("Layout", dims.layout));
4533
- lines.push(formatDim("Contrast", dims.contrast));
4534
- lines.push(formatDim("Graphics", dims.graphics));
4535
- lines.push(formatDim("Content", dims.content));
4536
- lines.push(formatDim("Clarity", dims.clarity));
4537
- lines.push(formatDim("Effectiveness", dims.effectiveness));
4538
- lines.push(formatDim("Consistency", dims.consistency));
4539
- lines.push("");
4540
- const errors = result.issues.filter((i) => i.severity === "error");
4541
- const warnings = result.issues.filter((i) => i.severity === "warning");
4542
- if (errors.length > 0) {
4543
- lines.push("\u274C Errors:");
4544
- errors.forEach((e) => lines.push(` \u2022 ${e.message}`));
4545
- lines.push("");
5281
+ scoreSlide(slideIndex, slideData, screenshotPath) {
5282
+ if (!slideData) {
5283
+ return {
5284
+ slideIndex,
5285
+ slideType: "unknown",
5286
+ visualImpact: 0,
5287
+ visualImpactNotes: "Could not analyze slide",
5288
+ contentClarity: 0,
5289
+ contentClarityNotes: "Could not analyze slide",
5290
+ professionalPolish: 0,
5291
+ professionalPolishNotes: "Could not analyze slide",
5292
+ themeCoherence: 0,
5293
+ themeCoherenceNotes: "Could not analyze slide",
5294
+ totalScore: 0,
5295
+ screenshotPath
5296
+ };
4546
5297
  }
4547
- if (warnings.length > 0) {
4548
- lines.push("\u26A0\uFE0F Warnings:");
4549
- warnings.slice(0, 10).forEach((w) => lines.push(` \u2022 ${w.message}`));
4550
- if (warnings.length > 10) {
4551
- lines.push(` ... and ${warnings.length - 10} more warnings`);
4552
- }
4553
- lines.push("");
5298
+ const slideType = this.inferSlideType(slideData);
5299
+ let visualImpact = 5;
5300
+ const visualNotes = [];
5301
+ if (slideData.hasImage) {
5302
+ visualImpact += 2;
5303
+ visualNotes.push("Has imagery");
4554
5304
  }
4555
- const autoFixable = result.issues.filter((i) => i.autoFixable);
4556
- if (autoFixable.length > 0) {
4557
- lines.push(`\u{1F527} ${autoFixable.length} issues can be auto-fixed`);
5305
+ if (slideData.hasChart) {
5306
+ visualImpact += 2;
5307
+ visualNotes.push("Has data visualization");
4558
5308
  }
4559
- return lines.join("\n");
5309
+ const highImpactTypes = [
5310
+ "big-number",
5311
+ "big_number",
5312
+ "metrics-grid",
5313
+ "metrics_grid",
5314
+ "three-column",
5315
+ "three_column",
5316
+ "three-points",
5317
+ "three_points",
5318
+ "title-impact",
5319
+ "title_impact",
5320
+ "cta",
5321
+ "call-to-action",
5322
+ "comparison",
5323
+ "timeline",
5324
+ "process",
5325
+ "quote",
5326
+ "testimonial"
5327
+ ];
5328
+ if (highImpactTypes.some((t) => slideType.includes(t.replace(/_/g, "-")) || slideType.includes(t.replace(/-/g, "_")))) {
5329
+ visualImpact += 2;
5330
+ visualNotes.push("High-impact slide type");
5331
+ }
5332
+ if (slideType === "single-statement" && slideData.body.length < 50) {
5333
+ visualImpact += 2;
5334
+ visualNotes.push("Clean single statement");
5335
+ }
5336
+ if (slideType === "title" && slideData.title.length > 0 && slideData.title.length < 80) {
5337
+ visualImpact += 1;
5338
+ visualNotes.push("Strong title");
5339
+ }
5340
+ if (slideData.contentLength > 300) {
5341
+ visualImpact -= 3;
5342
+ visualNotes.push("Too much text - overwhelming");
5343
+ }
5344
+ if (slideData.bullets.length > 5) {
5345
+ visualImpact -= 2;
5346
+ visualNotes.push("Too many bullets");
5347
+ }
5348
+ visualImpact = Math.max(0, Math.min(10, visualImpact));
5349
+ let contentClarity = 7;
5350
+ const clarityNotes = [];
5351
+ if (slideData.title.length > 80) {
5352
+ contentClarity -= 2;
5353
+ clarityNotes.push("Title too long");
5354
+ }
5355
+ if (slideData.title.length === 0) {
5356
+ contentClarity -= 3;
5357
+ clarityNotes.push("No title");
5358
+ }
5359
+ if (slideData.body && slideData.body.length > 0 && slideData.body.length < 200) {
5360
+ contentClarity += 1;
5361
+ clarityNotes.push("Good content length");
5362
+ }
5363
+ const avgBulletLength = slideData.bullets.length > 0 ? slideData.bullets.reduce((sum, b) => sum + b.length, 0) / slideData.bullets.length : 0;
5364
+ if (avgBulletLength > 100) {
5365
+ contentClarity -= 2;
5366
+ clarityNotes.push("Bullets too long - not scannable");
5367
+ }
5368
+ contentClarity = Math.max(0, Math.min(10, contentClarity));
5369
+ let professionalPolish = 6;
5370
+ const polishNotes = [];
5371
+ const titleFontSize = parseFloat(slideData.titleFontSize || "0");
5372
+ if (titleFontSize >= 40) {
5373
+ professionalPolish += 2;
5374
+ polishNotes.push("Strong title typography");
5375
+ } else if (titleFontSize < 24) {
5376
+ professionalPolish -= 1;
5377
+ polishNotes.push("Title could be more prominent");
5378
+ }
5379
+ if (slideData.contentLength > 10 && slideData.contentLength < 200) {
5380
+ professionalPolish += 1;
5381
+ polishNotes.push("Well-balanced content");
5382
+ }
5383
+ const polishedTypes = [
5384
+ "big-number",
5385
+ "metrics-grid",
5386
+ "three-column",
5387
+ "three-points",
5388
+ "comparison",
5389
+ "timeline",
5390
+ "process",
5391
+ "cta",
5392
+ "title",
5393
+ "thank-you"
5394
+ ];
5395
+ if (polishedTypes.some((t) => slideType.includes(t))) {
5396
+ professionalPolish += 1;
5397
+ polishNotes.push("Well-structured layout");
5398
+ }
5399
+ professionalPolish = Math.max(0, Math.min(10, professionalPolish));
5400
+ let themeCoherence = 7;
5401
+ const coherenceNotes = [];
5402
+ if (slideData.classList.some((c) => c.includes("slide-"))) {
5403
+ themeCoherence += 1;
5404
+ coherenceNotes.push("Has slide type class");
5405
+ }
5406
+ themeCoherence = Math.max(0, Math.min(10, themeCoherence));
5407
+ const totalScore = visualImpact + contentClarity + professionalPolish + themeCoherence;
5408
+ return {
5409
+ slideIndex,
5410
+ slideType,
5411
+ visualImpact,
5412
+ visualImpactNotes: visualNotes.join("; ") || "Standard",
5413
+ contentClarity,
5414
+ contentClarityNotes: clarityNotes.join("; ") || "Good",
5415
+ professionalPolish,
5416
+ professionalPolishNotes: polishNotes.join("; ") || "Acceptable",
5417
+ themeCoherence,
5418
+ themeCoherenceNotes: coherenceNotes.join("; ") || "Consistent",
5419
+ totalScore,
5420
+ screenshotPath
5421
+ };
4560
5422
  }
4561
- };
4562
-
4563
- // src/qa/AutoFixEngine.ts
4564
- var AutoFixEngine = class {
4565
- kb;
4566
- mode;
4567
- presentationType;
4568
- constructor(mode, presentationType) {
4569
- this.mode = mode;
4570
- this.presentationType = presentationType;
5423
+ inferSlideType(slideData) {
5424
+ const classList = slideData.classList || [];
5425
+ for (const cls of classList) {
5426
+ if (cls.includes("metrics-grid")) return "metrics-grid";
5427
+ if (cls.includes("three-column")) return "three-column";
5428
+ if (cls.includes("three-points")) return "three-points";
5429
+ if (cls.includes("two-column")) return "two-column";
5430
+ if (cls.includes("big-number")) return "big-number";
5431
+ if (cls.includes("comparison")) return "comparison";
5432
+ if (cls.includes("timeline")) return "timeline";
5433
+ if (cls.includes("process")) return "process";
5434
+ if (cls.includes("quote")) return "quote";
5435
+ if (cls.includes("testimonial")) return "testimonial";
5436
+ if (cls.includes("cta")) return "cta";
5437
+ if (cls.includes("thank-you")) return "thank-you";
5438
+ if (cls.includes("title")) return "title";
5439
+ if (cls.includes("bullet")) return "bullet-points";
5440
+ if (cls.includes("single-statement")) return "single-statement";
5441
+ if (cls.includes("agenda")) return "agenda";
5442
+ }
5443
+ if (!slideData.body && slideData.bullets.length > 0) return "bullet-points";
5444
+ if (slideData.body && !slideData.bullets.length) return "content";
5445
+ if (slideData.hasChart) return "data-visualization";
5446
+ return "standard";
4571
5447
  }
4572
- /**
4573
- * Apply automatic fixes to slides based on scoring results.
4574
- */
4575
- async fix(slides, scoringResult) {
4576
- this.kb = await getKnowledgeGateway();
4577
- const slidesFixed = JSON.parse(JSON.stringify(slides));
4578
- const fixesApplied = [];
4579
- const fixesSkipped = [];
4580
- const autoFixableIssues = scoringResult.issues.filter((i) => i.autoFixable);
4581
- for (const issue of autoFixableIssues) {
4582
- const result = await this.applyFix(slidesFixed, issue);
4583
- if (result.applied) {
4584
- fixesApplied.push(result);
4585
- } else {
4586
- fixesSkipped.push(result);
4587
- }
5448
+ // ===========================================================================
5449
+ // PRESENTATION-LEVEL EVALUATION
5450
+ // ===========================================================================
5451
+ evaluateNarrativeFlow(slideScores) {
5452
+ let score = 25;
5453
+ const notes = [];
5454
+ const firstSlide = slideScores[0];
5455
+ const hasStrongOpening = Boolean(firstSlide && (firstSlide.slideType === "title" || firstSlide.visualImpact >= 7));
5456
+ if (!hasStrongOpening) {
5457
+ score -= 5;
5458
+ notes.push("Opening could be stronger");
5459
+ }
5460
+ const middleSlides = slideScores.slice(1, -1);
5461
+ const highImpactMiddle = middleSlides.filter((s) => s.visualImpact >= 7).length;
5462
+ const hasCompellingMiddle = highImpactMiddle >= middleSlides.length * 0.3;
5463
+ if (!hasCompellingMiddle) {
5464
+ score -= 7;
5465
+ notes.push("Middle section needs more visual variety");
5466
+ }
5467
+ const lastSlide = slideScores[slideScores.length - 1];
5468
+ const secondLastSlide = slideScores[slideScores.length - 2];
5469
+ const closingTypes = ["cta", "thank-you", "call-to-action"];
5470
+ const hasMemorableClose = Boolean(
5471
+ lastSlide && (closingTypes.includes(lastSlide.slideType) || lastSlide.visualImpact >= 7) || secondLastSlide && closingTypes.includes(secondLastSlide.slideType)
5472
+ );
5473
+ if (!hasMemorableClose) {
5474
+ score -= 5;
5475
+ notes.push("Ending should be more memorable");
5476
+ }
5477
+ const slideTypes = new Set(slideScores.map((s) => s.slideType));
5478
+ const storyArcComplete = slideTypes.size >= 3;
5479
+ if (!storyArcComplete) {
5480
+ score -= 5;
5481
+ notes.push("Needs more variety in slide types");
4588
5482
  }
4589
- const summary = this.generateSummary(fixesApplied, fixesSkipped);
4590
5483
  return {
4591
- slidesFixed,
4592
- fixesApplied,
4593
- fixesSkipped,
4594
- summary
5484
+ score: Math.max(0, score),
5485
+ hasStrongOpening,
5486
+ hasCompellingMiddle,
5487
+ hasMemorableClose,
5488
+ storyArcComplete,
5489
+ notes: notes.join(". ") || "Good narrative structure"
4595
5490
  };
4596
5491
  }
4597
- /**
4598
- * Apply a single fix based on the issue.
4599
- */
4600
- async applyFix(slides, issue) {
4601
- const result = {
4602
- slideIndex: issue.slideIndex,
4603
- dimension: issue.dimension,
4604
- originalValue: issue.currentValue,
4605
- newValue: null,
4606
- description: issue.message,
4607
- applied: false
4608
- };
4609
- switch (issue.dimension) {
4610
- case "content":
4611
- return this.fixContentIssue(slides, issue, result);
4612
- case "clarity":
4613
- return this.fixClarityIssue(slides, issue, result);
4614
- case "layout":
4615
- return this.fixLayoutIssue(slides, issue, result);
4616
- case "consistency":
4617
- return this.fixConsistencyIssue(slides, issue, result);
4618
- case "contrast":
4619
- return this.fixContrastIssue(slides, issue, result);
4620
- default:
4621
- result.description = `No auto-fix available for ${issue.dimension}`;
4622
- return result;
5492
+ evaluateVisualConsistency(slideScores) {
5493
+ let score = 25;
5494
+ const notes = [];
5495
+ const visualScores = slideScores.map((s) => s.visualImpact);
5496
+ const avgVisual = visualScores.reduce((a, b) => a + b, 0) / visualScores.length;
5497
+ const variance = visualScores.reduce((sum, s) => sum + Math.pow(s - avgVisual, 2), 0) / visualScores.length;
5498
+ const colorPaletteConsistent = variance < 4;
5499
+ if (!colorPaletteConsistent) {
5500
+ score -= 5;
5501
+ notes.push("Visual quality varies too much between slides");
4623
5502
  }
4624
- }
4625
- /**
4626
- * Fix content-related issues (word count, bullet count).
4627
- */
4628
- fixContentIssue(slides, issue, result) {
4629
- if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4630
- return result;
5503
+ const polishScores = slideScores.map((s) => s.professionalPolish);
5504
+ const avgPolish = polishScores.reduce((a, b) => a + b, 0) / polishScores.length;
5505
+ const typographyConsistent = avgPolish >= 6;
5506
+ if (!typographyConsistent) {
5507
+ score -= 5;
5508
+ notes.push("Typography could be more polished");
4631
5509
  }
4632
- const slide = slides[issue.slideIndex];
4633
- if (!slide) return result;
4634
- if (issue.message.includes("Word count") && issue.message.includes("exceeds")) {
4635
- const maxWords = issue.expectedValue;
4636
- result.originalValue = this.countWords(slide);
4637
- if (slide.data.body) {
4638
- slide.data.body = this.condenseText(slide.data.body, maxWords / 2);
4639
- }
4640
- if (slide.data.bullets && slide.data.bullets.length > 0) {
4641
- const bulletCount = slide.data.bullets.length;
4642
- slide.data.bullets = slide.data.bullets.map(
4643
- (bullet) => this.condenseText(bullet, Math.floor(maxWords / bulletCount))
4644
- );
4645
- }
4646
- if (slide.data.subtitle) {
4647
- slide.data.subtitle = this.condenseText(slide.data.subtitle, 10);
4648
- }
4649
- result.newValue = this.countWords(slide);
4650
- result.applied = result.newValue <= maxWords;
4651
- result.description = `Condensed content from ${result.originalValue} to ${result.newValue} words`;
5510
+ const coherenceScores = slideScores.map((s) => s.themeCoherence);
5511
+ const avgCoherence = coherenceScores.reduce((a, b) => a + b, 0) / coherenceScores.length;
5512
+ const layoutPatternsConsistent = avgCoherence >= 6;
5513
+ if (!layoutPatternsConsistent) {
5514
+ score -= 5;
5515
+ notes.push("Layout patterns should be more consistent");
4652
5516
  }
4653
- if (issue.message.includes("bullets exceeds")) {
4654
- const maxBullets = issue.expectedValue;
4655
- if (slide.data.bullets) {
4656
- result.originalValue = slide.data.bullets.length;
4657
- slide.data.bullets = slide.data.bullets.slice(0, maxBullets);
4658
- result.newValue = slide.data.bullets.length;
4659
- result.applied = true;
4660
- result.description = `Reduced bullets from ${result.originalValue} to ${result.newValue}`;
4661
- }
5517
+ const professionalLook = avgPolish >= 7 && avgCoherence >= 7;
5518
+ if (!professionalLook) {
5519
+ score -= 5;
5520
+ notes.push("Overall polish could be improved");
4662
5521
  }
4663
- return result;
5522
+ return {
5523
+ score: Math.max(0, score),
5524
+ colorPaletteConsistent,
5525
+ typographyConsistent,
5526
+ layoutPatternsConsistent,
5527
+ professionalLook,
5528
+ notes: notes.join(". ") || "Consistent visual design"
5529
+ };
4664
5530
  }
4665
- /**
4666
- * Fix clarity-related issues (key message length, title length).
4667
- */
4668
- fixClarityIssue(slides, issue, result) {
4669
- if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4670
- return result;
4671
- }
4672
- const slide = slides[issue.slideIndex];
4673
- if (!slide) return result;
4674
- if (issue.message.includes("Key message too long")) {
4675
- if (slide.data.keyMessage) {
4676
- result.originalValue = slide.data.keyMessage;
4677
- const maxWords = issue.expectedValue;
4678
- slide.data.keyMessage = this.condenseText(slide.data.keyMessage, maxWords);
4679
- result.newValue = slide.data.keyMessage;
4680
- result.applied = true;
4681
- result.description = `Shortened key message to ${maxWords} words`;
4682
- }
5531
+ evaluateContentQuality(slideScores) {
5532
+ let score = 25;
5533
+ const notes = [];
5534
+ const clarityScores = slideScores.map((s) => s.contentClarity);
5535
+ const avgClarity = clarityScores.reduce((a, b) => a + b, 0) / clarityScores.length;
5536
+ const messagesAreClear = avgClarity >= 7;
5537
+ if (!messagesAreClear) {
5538
+ score -= 7;
5539
+ notes.push("Messages could be clearer");
5540
+ }
5541
+ const lowClarity = slideScores.filter((s) => s.contentClarity < 5).length;
5542
+ const appropriateDepth = lowClarity < slideScores.length * 0.2;
5543
+ if (!appropriateDepth) {
5544
+ score -= 5;
5545
+ notes.push("Some slides have content issues");
4683
5546
  }
4684
- if (issue.message.includes("Title too long")) {
4685
- if (slide.data.title) {
4686
- result.originalValue = slide.data.title;
4687
- const words = slide.data.title.split(/\s+/);
4688
- const originalLength = words.length;
4689
- if (words.length > 6) {
4690
- slide.data.title = words.slice(0, 6).join(" ");
4691
- }
4692
- result.newValue = slide.data.title;
4693
- result.applied = true;
4694
- result.description = `Shortened title from ${originalLength} to ${slide.data.title.split(/\s+/).length} words`;
4695
- }
5547
+ const overloadedSlides = slideScores.filter(
5548
+ (s) => s.contentClarityNotes.includes("too long") || s.visualImpactNotes.includes("Too much")
5549
+ ).length;
5550
+ const noOverload = overloadedSlides === 0;
5551
+ if (!noOverload) {
5552
+ score -= 5;
5553
+ notes.push(`${overloadedSlides} slides have too much content`);
4696
5554
  }
4697
- if (issue.message.includes("Too many elements")) {
4698
- result.originalValue = issue.currentValue;
4699
- if (slide.data.subtitle && slide.data.body) {
4700
- delete slide.data.subtitle;
4701
- result.applied = true;
4702
- result.description = "Removed subtitle to reduce element count";
4703
- } else if (slide.data.body && slide.data.bullets && slide.data.bullets.length > 0) {
4704
- delete slide.data.body;
4705
- result.applied = true;
4706
- result.description = "Removed body text, keeping bullets";
4707
- }
4708
- result.newValue = this.countElements(slide);
5555
+ const insightSlides = slideScores.filter(
5556
+ (s) => s.visualImpact >= 7 && s.contentClarity >= 7
5557
+ ).length;
5558
+ const actionableInsights = insightSlides >= slideScores.length * 0.3;
5559
+ if (!actionableInsights) {
5560
+ score -= 5;
5561
+ notes.push("Need more high-impact insight slides");
4709
5562
  }
4710
- return result;
5563
+ return {
5564
+ score: Math.max(0, score),
5565
+ messagesAreClear,
5566
+ appropriateDepth,
5567
+ noOverload,
5568
+ actionableInsights,
5569
+ notes: notes.join(". ") || "Strong content quality"
5570
+ };
4711
5571
  }
4712
- /**
4713
- * Fix layout-related issues.
4714
- */
4715
- fixLayoutIssue(slides, issue, result) {
4716
- if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4717
- return result;
4718
- }
4719
- const slide = slides[issue.slideIndex];
4720
- if (!slide) return result;
4721
- if (issue.message.includes("Insufficient whitespace")) {
4722
- const currentWordCount = this.countWords(slide);
4723
- const targetReduction = 0.3;
4724
- const targetWords = Math.floor(currentWordCount * (1 - targetReduction));
4725
- result.originalValue = currentWordCount;
4726
- if (slide.data.body) {
4727
- slide.data.body = this.condenseText(slide.data.body, Math.floor(targetWords * 0.5));
4728
- }
4729
- if (slide.data.bullets && slide.data.bullets.length > 0) {
4730
- const wordsPerBullet = Math.floor(targetWords / (slide.data.bullets.length * 2));
4731
- slide.data.bullets = slide.data.bullets.map((b) => this.condenseText(b, wordsPerBullet));
4732
- }
4733
- result.newValue = this.countWords(slide);
4734
- result.applied = true;
4735
- result.description = `Reduced content from ${result.originalValue} to ${result.newValue} words for better whitespace`;
5572
+ evaluateExecutiveReadiness(slideScores) {
5573
+ let score = 25;
5574
+ const notes = [];
5575
+ const avgVisual = slideScores.reduce((sum, s) => sum + s.visualImpact, 0) / slideScores.length;
5576
+ const avgPolish = slideScores.reduce((sum, s) => sum + s.professionalPolish, 0) / slideScores.length;
5577
+ const avgClarity = slideScores.reduce((sum, s) => sum + s.contentClarity, 0) / slideScores.length;
5578
+ const avgTotal = slideScores.reduce((sum, s) => sum + s.totalScore, 0) / slideScores.length;
5579
+ const wouldImpress = avgTotal >= 26;
5580
+ if (!wouldImpress) {
5581
+ score -= 7;
5582
+ notes.push("Needs more visual impact to impress");
5583
+ }
5584
+ const readyForBoardroom = avgPolish >= 6 && avgClarity >= 6;
5585
+ if (!readyForBoardroom) {
5586
+ score -= 7;
5587
+ notes.push("Needs more polish for executive audience");
5588
+ }
5589
+ const compelling = avgVisual >= 6;
5590
+ if (!compelling) {
5591
+ score -= 5;
5592
+ notes.push("Could be more visually compelling");
4736
5593
  }
4737
- return result;
4738
- }
4739
- /**
4740
- * Fix consistency-related issues.
4741
- */
4742
- fixConsistencyIssue(slides, issue, result) {
4743
- if (issue.message.includes("Inconsistent title casing")) {
4744
- let fixedCount = 0;
4745
- for (const slide of slides) {
4746
- if (slide.data.title) {
4747
- const original = slide.data.title;
4748
- slide.data.title = this.toTitleCase(slide.data.title);
4749
- if (slide.data.title !== original) fixedCount++;
4750
- }
4751
- }
4752
- result.originalValue = "Mixed casing";
4753
- result.newValue = "Title Case";
4754
- result.applied = fixedCount > 0;
4755
- result.description = `Applied Title Case to ${fixedCount} slide titles`;
5594
+ const excellentSlides = slideScores.filter((s) => s.totalScore >= 30).length;
5595
+ const shareworthy = excellentSlides >= slideScores.length * 0.4;
5596
+ if (!shareworthy) {
5597
+ score -= 5;
5598
+ notes.push("Less than 40% of slides are excellent");
4756
5599
  }
4757
- return result;
5600
+ return {
5601
+ score: Math.max(0, score),
5602
+ wouldImpress,
5603
+ readyForBoardroom,
5604
+ compelling,
5605
+ shareworthy,
5606
+ notes: notes.join(". ") || "Executive-ready presentation"
5607
+ };
4758
5608
  }
4759
- /**
4760
- * Fix contrast-related issues.
4761
- * Note: These are CSS fixes, typically handled at generation time.
4762
- */
4763
- fixContrastIssue(slides, issue, result) {
4764
- result.description = "Contrast issues flagged for CSS regeneration";
4765
- result.applied = false;
4766
- return result;
5609
+ // ===========================================================================
5610
+ // VERDICT & SUMMARY
5611
+ // ===========================================================================
5612
+ determineVerdict(score) {
5613
+ if (score >= 90) return "world-class";
5614
+ if (score >= 80) return "professional";
5615
+ if (score >= 65) return "acceptable";
5616
+ if (score >= 50) return "needs-work";
5617
+ return "poor";
5618
+ }
5619
+ explainVerdict(verdict, score) {
5620
+ const explanations = {
5621
+ "world-class": `Score: ${score}/100. This presentation would impress any audience. Ready for TED, boardrooms, and high-stakes pitches.`,
5622
+ "professional": `Score: ${score}/100. Solid professional presentation. Ready for most business contexts with minor polish.`,
5623
+ "acceptable": `Score: ${score}/100. Meets basic standards but lacks the polish and impact of world-class work.`,
5624
+ "needs-work": `Score: ${score}/100. Significant improvements needed in visual design, content clarity, or structure.`,
5625
+ "poor": `Score: ${score}/100. Fundamental issues with content, design, or structure. Major rework required.`
5626
+ };
5627
+ return explanations[verdict];
4767
5628
  }
4768
- /**
4769
- * Condense text to approximately maxWords.
4770
- * Uses smart truncation that preserves meaning.
4771
- */
4772
- condenseText(text, maxWords) {
4773
- const words = text.split(/\s+/);
4774
- if (words.length <= maxWords) {
4775
- return text;
4776
- }
4777
- const fillerWords = /* @__PURE__ */ new Set([
4778
- "very",
4779
- "really",
4780
- "actually",
4781
- "basically",
4782
- "literally",
4783
- "obviously",
4784
- "clearly",
4785
- "simply",
4786
- "just",
4787
- "that",
4788
- "which",
4789
- "would",
4790
- "could",
4791
- "should",
4792
- "might"
4793
- ]);
4794
- let filtered = words.filter((w) => !fillerWords.has(w.toLowerCase()));
4795
- if (filtered.length <= maxWords) {
4796
- return filtered.join(" ");
4797
- }
4798
- const punctuation = [".", ",", ";", ":", "-"];
4799
- let breakPoint = maxWords;
4800
- for (let i = maxWords - 1; i >= maxWords - 5 && i >= 0; i--) {
4801
- const word = filtered[i];
4802
- if (word && punctuation.some((p) => word.endsWith(p))) {
4803
- breakPoint = i + 1;
4804
- break;
5629
+ extractTopIssuesAndStrengths(slideScores) {
5630
+ const issues = [];
5631
+ const strengths = [];
5632
+ for (const slide of slideScores) {
5633
+ if (slide.visualImpact < 5) {
5634
+ issues.push(`Slide ${slide.slideIndex}: Low visual impact - ${slide.visualImpactNotes}`);
4805
5635
  }
4806
- }
4807
- filtered = filtered.slice(0, breakPoint);
4808
- let result = filtered.join(" ");
4809
- if (!result.endsWith(".") && !result.endsWith("!") && !result.endsWith("?")) {
4810
- result = result.replace(/[,;:]$/, "") + "...";
4811
- }
4812
- return result;
4813
- }
4814
- /**
4815
- * Convert text to Title Case.
4816
- */
4817
- toTitleCase(text) {
4818
- const minorWords = /* @__PURE__ */ new Set([
4819
- "a",
4820
- "an",
4821
- "the",
4822
- "and",
4823
- "but",
4824
- "or",
4825
- "for",
4826
- "nor",
4827
- "on",
4828
- "at",
4829
- "to",
4830
- "by",
4831
- "of",
4832
- "in",
4833
- "with"
4834
- ]);
4835
- return text.split(" ").map((word, index) => {
4836
- if (index === 0) {
4837
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
5636
+ if (slide.contentClarity < 5) {
5637
+ issues.push(`Slide ${slide.slideIndex}: Clarity issue - ${slide.contentClarityNotes}`);
4838
5638
  }
4839
- if (minorWords.has(word.toLowerCase())) {
4840
- return word.toLowerCase();
5639
+ if (slide.totalScore >= 35) {
5640
+ strengths.push(`Slide ${slide.slideIndex}: Excellent overall (${slide.slideType})`);
4841
5641
  }
4842
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
4843
- }).join(" ");
4844
- }
4845
- /**
4846
- * Count words in a slide.
4847
- */
4848
- countWords(slide) {
4849
- let text = "";
4850
- if (slide.data.title) text += slide.data.title + " ";
4851
- if (slide.data.subtitle) text += slide.data.subtitle + " ";
4852
- if (slide.data.body) text += slide.data.body + " ";
4853
- if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
4854
- if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
4855
- return text.split(/\s+/).filter((w) => w.length > 0).length;
4856
- }
4857
- /**
4858
- * Count elements in a slide.
4859
- */
4860
- countElements(slide) {
4861
- 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);
5642
+ }
5643
+ return {
5644
+ topIssues: issues.slice(0, 5),
5645
+ topStrengths: strengths.slice(0, 3)
5646
+ };
4862
5647
  }
4863
- /**
4864
- * Generate a summary of fixes applied.
4865
- */
4866
- generateSummary(applied, skipped) {
4867
- const lines = [];
4868
- lines.push(`
4869
- \u{1F527} Auto-Fix Summary`);
4870
- 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`);
4871
- lines.push(`Fixes Applied: ${applied.length}`);
4872
- lines.push(`Fixes Skipped: ${skipped.length}`);
4873
- lines.push("");
4874
- if (applied.length > 0) {
4875
- lines.push("\u2705 Applied Fixes:");
4876
- for (const fix of applied) {
4877
- lines.push(` \u2022 ${fix.description}`);
4878
- }
5648
+ // ===========================================================================
5649
+ // FORMATTED REPORT
5650
+ // ===========================================================================
5651
+ generateReport(result) {
5652
+ const lines = [
5653
+ "\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",
5654
+ "\u2551 VISUAL QUALITY EVALUATION REPORT \u2551",
5655
+ "\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",
5656
+ "",
5657
+ ` VERDICT: ${result.verdict.toUpperCase()}`,
5658
+ ` ${result.verdictExplanation}`,
5659
+ "",
5660
+ "\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",
5661
+ "\u2502 DIMENSION SCORES \u2502",
5662
+ "\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",
5663
+ `\u2502 Narrative Flow: ${this.scoreBar(result.narrativeFlow.score, 25)} ${result.narrativeFlow.score}/25`,
5664
+ `\u2502 Visual Consistency: ${this.scoreBar(result.visualConsistency.score, 25)} ${result.visualConsistency.score}/25`,
5665
+ `\u2502 Content Quality: ${this.scoreBar(result.contentQuality.score, 25)} ${result.contentQuality.score}/25`,
5666
+ `\u2502 Executive Readiness: ${this.scoreBar(result.executiveReadiness.score, 25)} ${result.executiveReadiness.score}/25`,
5667
+ "\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",
5668
+ `\u2502 OVERALL SCORE: ${this.scoreBar(result.overallScore, 100)} ${result.overallScore}/100`,
5669
+ "\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",
5670
+ ""
5671
+ ];
5672
+ if (result.topStrengths.length > 0) {
5673
+ lines.push("\u2713 TOP STRENGTHS:");
5674
+ result.topStrengths.forEach((s) => lines.push(` \u2022 ${s}`));
4879
5675
  lines.push("");
4880
5676
  }
4881
- if (skipped.length > 0) {
4882
- lines.push("\u26A0\uFE0F Skipped Fixes (require manual attention):");
4883
- for (const fix of skipped.slice(0, 5)) {
4884
- lines.push(` \u2022 ${fix.description}`);
4885
- }
4886
- if (skipped.length > 5) {
4887
- lines.push(` ... and ${skipped.length - 5} more`);
4888
- }
5677
+ if (result.topIssues.length > 0) {
5678
+ lines.push("\u2717 TOP ISSUES:");
5679
+ result.topIssues.forEach((i) => lines.push(` \u2022 ${i}`));
5680
+ lines.push("");
4889
5681
  }
5682
+ 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");
5683
+ lines.push("\u2502 SLIDE-BY-SLIDE BREAKDOWN \u2502");
5684
+ 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");
5685
+ for (const slide of result.slideScores) {
5686
+ const scoreColor = slide.totalScore >= 30 ? "\u2713" : slide.totalScore >= 20 ? "\u25D0" : "\u2717";
5687
+ lines.push(`\u2502 ${scoreColor} Slide ${slide.slideIndex.toString().padStart(2)} (${slide.slideType.padEnd(18)}): ${slide.totalScore}/40`);
5688
+ lines.push(`\u2502 Visual: ${slide.visualImpact}/10 Clarity: ${slide.contentClarity}/10 Polish: ${slide.professionalPolish}/10 Theme: ${slide.themeCoherence}/10`);
5689
+ }
5690
+ 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");
5691
+ lines.push("");
5692
+ 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");
4890
5693
  return lines.join("\n");
4891
5694
  }
5695
+ scoreBar(score, max) {
5696
+ const percentage = score / max;
5697
+ const filled = Math.round(percentage * 20);
5698
+ const empty = 20 - filled;
5699
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
5700
+ }
4892
5701
  };
4893
- function createAutoFixEngine(mode, presentationType) {
4894
- return new AutoFixEngine(mode, presentationType);
5702
+ async function evaluatePresentation(htmlPath, screenshotDir) {
5703
+ const evaluator = new VisualQualityEvaluator(screenshotDir);
5704
+ return evaluator.evaluate(htmlPath);
4895
5705
  }
4896
5706
 
4897
5707
  // src/generators/html/RevealJsGenerator.ts
@@ -5185,7 +5995,7 @@ ${slides}
5185
5995
  .reveal .number {
5186
5996
  font-size: 4em;
5187
5997
  font-weight: 800;
5188
- color: var(--color-highlight);
5998
+ color: var(--color-primary);
5189
5999
  text-align: center;
5190
6000
  }
5191
6001
 
@@ -5236,12 +6046,12 @@ ${slides}
5236
6046
  .reveal .metric-value {
5237
6047
  font-size: 2em;
5238
6048
  font-weight: 700;
5239
- color: var(--color-highlight);
6049
+ color: var(--color-primary);
5240
6050
  }
5241
6051
 
5242
6052
  .reveal .metric-label {
5243
6053
  font-size: 0.8em;
5244
- color: var(--color-text-light);
6054
+ color: var(--color-text);
5245
6055
  }
5246
6056
 
5247
6057
  .reveal .metric-change {
@@ -5318,27 +6128,31 @@ ${slides}
5318
6128
  ================================================================ */
5319
6129
 
5320
6130
  /* Title slide: Bold background - makes strong first impression */
5321
- .reveal .slide-title {
5322
- background: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
5323
- }
5324
- .reveal .slide-title h1,
5325
- .reveal .slide-title h2,
5326
- .reveal .slide-title p,
5327
- .reveal .slide-title .subtitle {
5328
- color: ${isDark ? primary : "#ffffff"};
6131
+ /* Using .slides section.slide-title for higher specificity than .reveal .slides section */
6132
+ .reveal .slides section.slide-title {
6133
+ background-color: ${titleBg} !important;
6134
+ background-image: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
6135
+ }
6136
+ .reveal .slides section.slide-title h1,
6137
+ .reveal .slides section.slide-title h2,
6138
+ .reveal .slides section.slide-title p,
6139
+ .reveal .slides section.slide-title .subtitle {
6140
+ color: ${isDark ? "#ffffff" : "#ffffff"} !important;
6141
+ -webkit-text-fill-color: ${isDark ? "#ffffff" : "#ffffff"} !important;
6142
+ background: none !important;
5329
6143
  }
5330
6144
 
5331
6145
  /* Section dividers: Subtle visual breaks */
5332
- .reveal .slide-section-divider {
6146
+ .reveal .slides section.slide-section-divider {
5333
6147
  background: ${isDark ? this.lightenColor(background, 8) : `linear-gradient(180deg, ${this.lightenColor(primary, 85)} 0%, ${this.lightenColor(primary, 92)} 100%)`};
5334
6148
  }
5335
6149
 
5336
6150
  /* Big number slides: Clean with accent highlight - emphasizes the data */
5337
- .reveal .slide-big-number {
6151
+ .reveal .slides section.slide-big-number {
5338
6152
  background: var(--color-background);
5339
6153
  border-left: 8px solid var(--color-highlight);
5340
6154
  }
5341
- .reveal .slide-big-number .number {
6155
+ .reveal .slides section.slide-big-number .number {
5342
6156
  font-size: 5em;
5343
6157
  background: linear-gradient(135deg, var(--color-highlight) 0%, var(--color-accent) 100%);
5344
6158
  -webkit-background-clip: text;
@@ -5347,50 +6161,56 @@ ${slides}
5347
6161
  }
5348
6162
 
5349
6163
  /* Metrics grid: Light accent background - data-focused */
5350
- .reveal .slide-metrics-grid {
6164
+ .reveal .slides section.slide-metrics-grid {
5351
6165
  background: ${isDark ? this.lightenColor(background, 8) : this.lightenColor(accent, 90)};
5352
6166
  }
5353
- ${isDark ? `.reveal .slide-metrics-grid { color: ${text}; }` : ""}
6167
+ ${isDark ? `.reveal .slides section.slide-metrics-grid { color: ${text}; }` : ""}
5354
6168
 
5355
- /* CTA slide: Highlight color - drives action */
5356
- .reveal .slide-cta {
5357
- background: linear-gradient(135deg, var(--color-highlight) 0%, var(--color-accent) 100%);
5358
- }
5359
- .reveal .slide-cta h2,
5360
- .reveal .slide-cta p {
5361
- color: #ffffff;
5362
- }
5363
- .reveal .slide-cta .cta-button {
6169
+ /* CTA slide: Highlight color - drives action (darkened for 7:1 contrast with white) */
6170
+ .reveal .slides section.slide-cta {
6171
+ background-color: ${this.darkenColor(highlight, 30)} !important;
6172
+ background-image: linear-gradient(135deg, ${this.darkenColor(highlight, 25)} 0%, ${this.darkenColor(accent, 30)} 100%);
6173
+ }
6174
+ .reveal .slides section.slide-cta h2,
6175
+ .reveal .slides section.slide-cta p {
6176
+ color: #ffffff !important;
6177
+ -webkit-text-fill-color: #ffffff !important;
6178
+ background: none !important;
6179
+ }
6180
+ .reveal .slides section.slide-cta .cta-button {
5364
6181
  background: #ffffff;
5365
6182
  color: var(--color-highlight);
5366
6183
  }
5367
6184
 
5368
6185
  /* Thank you slide: Matches title for bookend effect */
5369
- .reveal .slide-thank-you {
5370
- background: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
6186
+ .reveal .slides section.slide-thank-you {
6187
+ background-color: ${titleBg} !important;
6188
+ background-image: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
5371
6189
  }
5372
- .reveal .slide-thank-you h2,
5373
- .reveal .slide-thank-you p,
5374
- .reveal .slide-thank-you .subtitle {
5375
- color: ${isDark ? primary : "#ffffff"};
6190
+ .reveal .slides section.slide-thank-you h2,
6191
+ .reveal .slides section.slide-thank-you p,
6192
+ .reveal .slides section.slide-thank-you .subtitle {
6193
+ color: #ffffff !important;
6194
+ -webkit-text-fill-color: #ffffff !important;
6195
+ background: none !important;
5376
6196
  }
5377
6197
 
5378
6198
  /* Quote slides: Elegant subtle background */
5379
- .reveal .slide-quote {
6199
+ .reveal .slides section.slide-quote {
5380
6200
  background: ${isDark ? this.lightenColor(background, 5) : this.lightenColor(secondary, 92)};
5381
6201
  }
5382
- .reveal .slide-quote blockquote {
6202
+ .reveal .slides section.slide-quote blockquote {
5383
6203
  border-left-color: var(--color-accent);
5384
6204
  font-size: 1.3em;
5385
6205
  }
5386
- ${isDark ? `.reveal .slide-quote { color: ${text}; }` : ""}
6206
+ ${isDark ? `.reveal .slides section.slide-quote { color: ${text}; }` : ""}
5387
6207
 
5388
6208
  /* Single statement: Clean, centered, impactful */
5389
- .reveal .slide-single-statement {
6209
+ .reveal .slides section.slide-single-statement {
5390
6210
  background: var(--color-background);
5391
6211
  }
5392
- .reveal .slide-single-statement .statement,
5393
- .reveal .slide-single-statement .big-idea-text {
6212
+ .reveal .slides section.slide-single-statement .statement,
6213
+ .reveal .slides section.slide-single-statement .big-idea-text {
5394
6214
  font-size: 2.2em;
5395
6215
  max-width: 80%;
5396
6216
  margin: 0 auto;
@@ -5407,52 +6227,77 @@ ${slides}
5407
6227
  padding-right: 30px;
5408
6228
  }
5409
6229
  .reveal .column-title {
5410
- font-size: 1.2em;
6230
+ font-size: 1.3em;
5411
6231
  font-weight: 600;
5412
6232
  margin-bottom: 0.5em;
5413
- color: var(--color-accent);
6233
+ color: var(--color-primary);
5414
6234
  }
5415
6235
  .reveal .column-content,
5416
6236
  .reveal .column-body {
5417
6237
  line-height: 1.6;
5418
6238
  }
5419
6239
 
5420
- /* Timeline/Process: Visual flow */
6240
+ /* Timeline/Process: Visual flow - ENHANCED */
6241
+ .reveal .slide-timeline .slide-content,
6242
+ .reveal .slide-process .slide-content {
6243
+ justify-content: center;
6244
+ }
5421
6245
  .reveal .slide-timeline .steps,
5422
6246
  .reveal .slide-timeline .timeline,
5423
6247
  .reveal .slide-process .steps,
5424
6248
  .reveal .slide-process .process-steps {
5425
6249
  display: flex;
5426
- gap: 20px;
6250
+ gap: 24px;
5427
6251
  flex-wrap: wrap;
5428
6252
  justify-content: center;
6253
+ align-items: stretch;
6254
+ margin-top: 30px;
6255
+ padding: 0 20px;
5429
6256
  }
5430
6257
  .reveal .slide-timeline .step,
5431
6258
  .reveal .slide-timeline .timeline-item,
5432
6259
  .reveal .slide-process .step,
5433
6260
  .reveal .slide-process .process-step {
5434
- flex: 1;
5435
- min-width: 150px;
5436
- max-width: 250px;
5437
- padding: 20px;
5438
- background: ${isDark ? this.lightenColor(background, 10) : this.lightenColor(secondary, 92)};
5439
- border-radius: 8px;
5440
- border-top: 4px solid var(--color-accent);
6261
+ flex: 1 1 200px;
6262
+ min-width: 180px;
6263
+ max-width: 280px;
6264
+ padding: 28px 24px;
6265
+ background: ${isDark ? this.lightenColor(background, 10) : "#ffffff"};
6266
+ border-radius: 12px;
6267
+ border-top: 5px solid var(--color-accent);
6268
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
5441
6269
  ${isDark ? `color: ${text};` : ""}
6270
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
6271
+ }
6272
+ .reveal .slide-timeline .step:hover,
6273
+ .reveal .slide-timeline .timeline-item:hover,
6274
+ .reveal .slide-process .step:hover,
6275
+ .reveal .slide-process .process-step:hover {
6276
+ transform: translateY(-4px);
6277
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
5442
6278
  }
5443
6279
  .reveal .step-number {
5444
- font-size: 1.5em;
6280
+ display: inline-flex;
6281
+ align-items: center;
6282
+ justify-content: center;
6283
+ width: 40px;
6284
+ height: 40px;
6285
+ background: var(--color-accent);
6286
+ color: #ffffff;
6287
+ border-radius: 50%;
6288
+ font-size: 1.2em;
5445
6289
  font-weight: 700;
5446
- color: var(--color-accent);
5447
- margin-bottom: 0.5em;
6290
+ margin-bottom: 16px;
5448
6291
  }
5449
6292
  .reveal .step-title {
5450
- font-size: 1.1em;
6293
+ font-size: 1.3em;
5451
6294
  font-weight: 600;
5452
- margin-bottom: 0.5em;
6295
+ margin-bottom: 8px;
6296
+ color: var(--color-primary);
5453
6297
  }
5454
6298
  .reveal .step-desc {
5455
- font-size: 0.9em;
6299
+ font-size: 0.95em;
6300
+ line-height: 1.5;
5456
6301
  color: var(--color-text-light);
5457
6302
  }
5458
6303
  .reveal .step-arrow {
@@ -5462,6 +6307,54 @@ ${slides}
5462
6307
  color: var(--color-accent);
5463
6308
  }
5464
6309
 
6310
+ /* Improved slide layout - PROFESSIONAL */
6311
+ .reveal .slides section {
6312
+ overflow: hidden;
6313
+ }
6314
+ .reveal .slides section .slide-content {
6315
+ overflow-y: auto;
6316
+ overflow-x: hidden;
6317
+ max-height: calc(100vh - 120px);
6318
+ }
6319
+
6320
+ /* Three column professional styling */
6321
+ .reveal .three-columns .column {
6322
+ padding: 24px;
6323
+ background: ${isDark ? this.lightenColor(background, 8) : "#f8f9fa"};
6324
+ border-radius: 12px;
6325
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
6326
+ }
6327
+ .reveal .three-columns .column-title {
6328
+ font-size: 1.3em;
6329
+ color: var(--color-primary);
6330
+ border-bottom: 2px solid var(--color-accent);
6331
+ padding-bottom: 8px;
6332
+ margin-bottom: 12px;
6333
+ }
6334
+
6335
+ /* Bullets professional styling */
6336
+ .reveal .bullets {
6337
+ list-style: none;
6338
+ margin: 0;
6339
+ padding: 0;
6340
+ }
6341
+ .reveal .bullets li {
6342
+ position: relative;
6343
+ padding-left: 28px;
6344
+ margin-bottom: 16px;
6345
+ line-height: 1.5;
6346
+ }
6347
+ .reveal .bullets li::before {
6348
+ content: "";
6349
+ position: absolute;
6350
+ left: 0;
6351
+ top: 8px;
6352
+ width: 8px;
6353
+ height: 8px;
6354
+ background: var(--color-accent);
6355
+ border-radius: 50%;
6356
+ }
6357
+
5465
6358
  /* Progress bar enhancement */
5466
6359
  .reveal .progress {
5467
6360
  background: ${this.lightenColor(primary, 80)};
@@ -5697,182 +6590,6 @@ ${slides}
5697
6590
  }
5698
6591
  };
5699
6592
 
5700
- // src/qa/IterativeQAEngine.ts
5701
- var DEFAULT_OPTIONS = {
5702
- minScore: 95,
5703
- maxIterations: 5,
5704
- verbose: true
5705
- };
5706
- var IterativeQAEngine = class {
5707
- kb;
5708
- scorer;
5709
- fixer;
5710
- generator;
5711
- mode;
5712
- presentationType;
5713
- config;
5714
- constructor(mode, presentationType, config) {
5715
- this.mode = mode;
5716
- this.presentationType = presentationType;
5717
- this.config = config;
5718
- this.scorer = new SevenDimensionScorer(mode, presentationType);
5719
- this.fixer = new AutoFixEngine(mode, presentationType);
5720
- this.generator = new RevealJsGenerator();
5721
- }
5722
- /**
5723
- * Run the iterative QA process.
5724
- */
5725
- async run(initialSlides, initialHtml, options = {}) {
5726
- const opts = { ...DEFAULT_OPTIONS, ...options };
5727
- this.kb = await getKnowledgeGateway();
5728
- const iterations = [];
5729
- let currentSlides = initialSlides;
5730
- let currentHtml = initialHtml;
5731
- let scoringResult;
5732
- let autoFixSummary = "";
5733
- let totalFixesApplied = 0;
5734
- if (opts.verbose) {
5735
- logger.progress("\n\u{1F504} Starting Iterative QA Process");
5736
- logger.info(` Target Score: ${opts.minScore}/100`);
5737
- logger.info(` Max Iterations: ${opts.maxIterations}`);
5738
- logger.progress("");
5739
- }
5740
- for (let i = 0; i < opts.maxIterations; i++) {
5741
- const iterationNum = i + 1;
5742
- if (opts.verbose) {
5743
- logger.progress(`\u{1F4CA} Iteration ${iterationNum}/${opts.maxIterations}...`);
5744
- }
5745
- scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
5746
- iterations.push({
5747
- iteration: iterationNum,
5748
- score: scoringResult.overallScore,
5749
- dimensionScores: {
5750
- layout: scoringResult.dimensions.layout.score,
5751
- contrast: scoringResult.dimensions.contrast.score,
5752
- graphics: scoringResult.dimensions.graphics.score,
5753
- content: scoringResult.dimensions.content.score,
5754
- clarity: scoringResult.dimensions.clarity.score,
5755
- effectiveness: scoringResult.dimensions.effectiveness.score,
5756
- consistency: scoringResult.dimensions.consistency.score
5757
- },
5758
- fixesApplied: 0,
5759
- timestamp: /* @__PURE__ */ new Date()
5760
- });
5761
- if (opts.verbose) {
5762
- logger.info(` Score: ${scoringResult.overallScore}/100`);
5763
- }
5764
- if (scoringResult.passed) {
5765
- if (opts.verbose) {
5766
- logger.success(`PASSED - Score meets threshold (${opts.minScore})`);
5767
- }
5768
- break;
5769
- }
5770
- if (i < opts.maxIterations - 1) {
5771
- if (opts.verbose) {
5772
- logger.warn("Below threshold, applying auto-fixes...");
5773
- }
5774
- const fixResult = await this.fixer.fix(currentSlides, scoringResult);
5775
- if (fixResult.fixesApplied.length > 0) {
5776
- currentSlides = fixResult.slidesFixed;
5777
- currentHtml = await this.generator.generate(currentSlides, this.config);
5778
- const lastIteration = iterations[iterations.length - 1];
5779
- if (lastIteration) {
5780
- lastIteration.fixesApplied = fixResult.fixesApplied.length;
5781
- }
5782
- totalFixesApplied += fixResult.fixesApplied.length;
5783
- if (opts.verbose) {
5784
- logger.info(` \u{1F527} Applied ${fixResult.fixesApplied.length} fixes`);
5785
- }
5786
- autoFixSummary += fixResult.summary + "\n";
5787
- } else {
5788
- if (opts.verbose) {
5789
- logger.warn("No auto-fixes available, manual review needed");
5790
- }
5791
- break;
5792
- }
5793
- }
5794
- }
5795
- if (!scoringResult.passed) {
5796
- scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
5797
- }
5798
- const report = this.generateReport(
5799
- scoringResult,
5800
- iterations,
5801
- opts,
5802
- totalFixesApplied
5803
- );
5804
- if (opts.verbose) {
5805
- logger.info(report);
5806
- }
5807
- return {
5808
- finalScore: scoringResult.overallScore,
5809
- passed: scoringResult.passed,
5810
- threshold: opts.minScore,
5811
- iterations,
5812
- totalIterations: iterations.length,
5813
- maxIterations: opts.maxIterations,
5814
- slides: currentSlides,
5815
- html: currentHtml,
5816
- finalScoring: scoringResult,
5817
- autoFixSummary,
5818
- report
5819
- };
5820
- }
5821
- /**
5822
- * Generate a comprehensive report.
5823
- */
5824
- generateReport(finalScoring, iterations, options, totalFixesApplied) {
5825
- const lines = [];
5826
- lines.push("");
5827
- 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");
5828
- lines.push("\u2551 ITERATIVE QA FINAL REPORT \u2551");
5829
- 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");
5830
- lines.push("");
5831
- const passStatus = finalScoring.passed ? "\u2705 PASSED" : "\u274C FAILED";
5832
- lines.push(`Final Score: ${finalScoring.overallScore}/100 ${passStatus}`);
5833
- lines.push(`Threshold: ${options.minScore}/100`);
5834
- lines.push(`Iterations: ${iterations.length}/${options.maxIterations}`);
5835
- lines.push(`Total Fixes Applied: ${totalFixesApplied}`);
5836
- lines.push("");
5837
- if (iterations.length > 1) {
5838
- lines.push("Score Progression:");
5839
- 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");
5840
- for (const iter of iterations) {
5841
- const bar = "\u2588".repeat(Math.floor(iter.score / 10)) + "\u2591".repeat(10 - Math.floor(iter.score / 10));
5842
- lines.push(` Iter ${iter.iteration}: ${bar} ${iter.score}/100 (+${iter.fixesApplied} fixes)`);
5843
- }
5844
- lines.push("");
5845
- }
5846
- lines.push(this.scorer.formatReport(finalScoring));
5847
- lines.push("");
5848
- lines.push("\u{1F4DA} Knowledge Base Compliance:");
5849
- 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");
5850
- lines.push(` Mode: ${this.mode}`);
5851
- lines.push(` Presentation Type: ${this.presentationType}`);
5852
- lines.push(` Word Limits: ${this.mode === "keynote" ? "6-25" : "40-80"} per slide`);
5853
- lines.push(` Expert Frameworks: Duarte, Reynolds, Gallo, Anderson`);
5854
- lines.push("");
5855
- if (!finalScoring.passed) {
5856
- lines.push("\u{1F4CB} Recommendations for Manual Review:");
5857
- 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");
5858
- const manualIssues = finalScoring.issues.filter((i) => !i.autoFixable);
5859
- for (const issue of manualIssues.slice(0, 5)) {
5860
- lines.push(` \u2022 ${issue.message}`);
5861
- if (issue.fixSuggestion) {
5862
- lines.push(` \u2192 ${issue.fixSuggestion}`);
5863
- }
5864
- }
5865
- if (manualIssues.length > 5) {
5866
- lines.push(` ... and ${manualIssues.length - 5} more issues`);
5867
- }
5868
- }
5869
- return lines.join("\n");
5870
- }
5871
- };
5872
- function createIterativeQAEngine(mode, presentationType, config) {
5873
- return new IterativeQAEngine(mode, presentationType, config);
5874
- }
5875
-
5876
6593
  // src/generators/pptx/PowerPointGenerator.ts
5877
6594
  import PptxGenJS from "pptxgenjs";
5878
6595
 
@@ -6642,11 +7359,11 @@ var PDFGenerator = class {
6642
7359
  waitUntil: ["networkidle0", "domcontentloaded"]
6643
7360
  });
6644
7361
  await page.evaluate(() => {
6645
- return new Promise((resolve) => {
7362
+ return new Promise((resolve2) => {
6646
7363
  if (document.fonts && document.fonts.ready) {
6647
- document.fonts.ready.then(() => resolve());
7364
+ document.fonts.ready.then(() => resolve2());
6648
7365
  } else {
6649
- setTimeout(resolve, 1e3);
7366
+ setTimeout(resolve2, 1e3);
6650
7367
  }
6651
7368
  });
6652
7369
  });
@@ -6826,46 +7543,38 @@ var PresentationEngine = class {
6826
7543
  }
6827
7544
  let qaResults;
6828
7545
  let score = 100;
6829
- let iterativeResult = null;
6830
- let finalSlides = slides;
6831
- let finalHtml = outputs.html;
7546
+ let visualQAResult = null;
7547
+ const finalSlides = slides;
7548
+ const finalHtml = outputs.html;
6832
7549
  if (!config.skipQA && outputs.html) {
6833
- const threshold = config.qaThreshold ?? 95;
6834
- const maxIterations = config.maxIterations ?? 5;
6835
- const useIterativeQA = config.useIterativeQA !== false;
6836
- if (useIterativeQA) {
6837
- logger.progress("\u{1F50D} Running Iterative QA (7-Dimension Scoring)...");
6838
- const iterativeEngine = createIterativeQAEngine(
6839
- config.mode,
6840
- presentationType,
6841
- config
6842
- );
6843
- iterativeResult = await iterativeEngine.run(slides, outputs.html, {
6844
- minScore: threshold,
6845
- maxIterations,
6846
- verbose: true
6847
- });
6848
- score = iterativeResult.finalScore;
6849
- finalSlides = iterativeResult.slides;
6850
- finalHtml = iterativeResult.html;
6851
- if (outputs.html) {
6852
- outputs.html = finalHtml;
6853
- }
6854
- qaResults = this.buildQAResultsFrom7Dimension(iterativeResult);
6855
- if (!iterativeResult.passed) {
6856
- throw new QAFailureError(score, threshold, qaResults);
6857
- }
6858
- } else {
6859
- logger.progress("\u{1F50D} Running QA validation (legacy mode)...");
6860
- qaResults = await this.qaEngine.validate(outputs.html, {
6861
- mode: config.mode,
6862
- strictMode: true
6863
- });
6864
- score = this.scoreCalculator.calculate(qaResults);
6865
- logger.info(`\u{1F4CA} QA Score: ${score}/100`);
7550
+ const threshold = config.qaThreshold ?? 80;
7551
+ logger.progress("\u{1F50D} Running Visual Quality Evaluation (Playwright)...");
7552
+ const tempDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "presentation-qa-"));
7553
+ const tempHtmlPath = path2.join(tempDir, "presentation.html");
7554
+ fs2.writeFileSync(tempHtmlPath, outputs.html);
7555
+ try {
7556
+ const visualEvaluator = new VisualQualityEvaluator(path2.join(tempDir, "screenshots"));
7557
+ visualQAResult = await visualEvaluator.evaluate(tempHtmlPath);
7558
+ score = visualQAResult.overallScore;
7559
+ logger.info(`\u{1F4CA} Visual QA Score: ${score}/100 (${visualQAResult.verdict.toUpperCase()})`);
7560
+ logger.info(` Narrative Flow: ${visualQAResult.narrativeFlow.score}/25`);
7561
+ logger.info(` Visual Consistency: ${visualQAResult.visualConsistency.score}/25`);
7562
+ logger.info(` Content Quality: ${visualQAResult.contentQuality.score}/25`);
7563
+ logger.info(` Executive Readiness: ${visualQAResult.executiveReadiness.score}/25`);
7564
+ qaResults = this.buildQAResultsFromVisual(visualQAResult);
6866
7565
  if (score < threshold) {
7566
+ logger.warn(`\u26A0\uFE0F Score ${score} below threshold ${threshold}`);
7567
+ if (visualQAResult.topIssues.length > 0) {
7568
+ logger.warn("Top issues:");
7569
+ visualQAResult.topIssues.forEach((issue) => logger.warn(` \u2022 ${issue}`));
7570
+ }
6867
7571
  throw new QAFailureError(score, threshold, qaResults);
6868
7572
  }
7573
+ } finally {
7574
+ try {
7575
+ fs2.rmSync(tempDir, { recursive: true, force: true });
7576
+ } catch {
7577
+ }
6869
7578
  }
6870
7579
  } else {
6871
7580
  qaResults = this.qaEngine.createEmptyResults();
@@ -6881,7 +7590,7 @@ var PresentationEngine = class {
6881
7590
  logger.warn(`PDF generation failed (non-critical): ${errorMsg}`);
6882
7591
  }
6883
7592
  }
6884
- const metadata = this.buildMetadata(config, analysis, finalSlides, iterativeResult);
7593
+ const metadata = this.buildMetadata(config, analysis, finalSlides, visualQAResult);
6885
7594
  return {
6886
7595
  outputs,
6887
7596
  qaResults,
@@ -6890,43 +7599,57 @@ var PresentationEngine = class {
6890
7599
  };
6891
7600
  }
6892
7601
  /**
6893
- * Build QA results structure from 7-dimension scoring.
7602
+ * Build QA results structure from visual quality evaluation.
6894
7603
  */
6895
- buildQAResultsFrom7Dimension(iterativeResult) {
6896
- const scoring = iterativeResult.finalScoring;
7604
+ buildQAResultsFromVisual(visualResult) {
7605
+ const passed = visualResult.overallScore >= 80;
6897
7606
  return {
6898
- passed: scoring.passed,
6899
- score: scoring.overallScore,
7607
+ passed,
7608
+ score: visualResult.overallScore,
6900
7609
  visual: {
6901
- whitespacePercentage: scoring.dimensions.layout.score,
6902
- layoutBalance: scoring.dimensions.layout.score / 100,
6903
- colorContrast: scoring.dimensions.contrast.score / 100
7610
+ whitespacePercentage: visualResult.visualConsistency.score * 4,
7611
+ // Scale 25 to 100
7612
+ layoutBalance: visualResult.visualConsistency.professionalLook ? 1 : 0.7,
7613
+ colorContrast: visualResult.visualConsistency.colorPaletteConsistent ? 0.95 : 0.8
6904
7614
  },
6905
7615
  content: {
6906
- perSlide: [],
6907
- issues: scoring.issues.filter((i) => i.dimension === "content")
7616
+ perSlide: visualResult.slideScores.map((slide) => ({
7617
+ slideIndex: slide.slideIndex,
7618
+ wordCount: 0,
7619
+ // Not tracked by visual QA
7620
+ bulletCount: 0,
7621
+ withinLimit: true,
7622
+ hasActionTitle: slide.contentClarity >= 7,
7623
+ issues: slide.contentClarity < 7 ? [slide.contentClarityNotes] : []
7624
+ })),
7625
+ issues: visualResult.topIssues.map((issue) => ({
7626
+ severity: "warning",
7627
+ message: issue,
7628
+ dimension: "content"
7629
+ }))
6908
7630
  },
6909
7631
  accessibility: {
6910
- wcagLevel: scoring.dimensions.contrast.score >= 95 ? "AAA" : "AA",
6911
- issues: scoring.issues.filter((i) => i.dimension === "contrast")
7632
+ wcagLevel: visualResult.visualConsistency.professionalLook ? "AA" : "A",
7633
+ issues: []
6912
7634
  },
6913
- issues: scoring.issues.map((issue) => ({
6914
- severity: issue.severity,
6915
- message: issue.message,
6916
- slideIndex: issue.slideIndex,
6917
- dimension: issue.dimension
7635
+ issues: visualResult.topIssues.map((issue, i) => ({
7636
+ severity: "warning",
7637
+ message: issue,
7638
+ slideIndex: i,
7639
+ dimension: "visual"
6918
7640
  })),
6919
7641
  dimensions: {
6920
- layout: scoring.dimensions.layout.score,
6921
- contrast: scoring.dimensions.contrast.score,
6922
- graphics: scoring.dimensions.graphics.score,
6923
- content: scoring.dimensions.content.score,
6924
- clarity: scoring.dimensions.clarity.score,
6925
- effectiveness: scoring.dimensions.effectiveness.score,
6926
- consistency: scoring.dimensions.consistency.score
7642
+ layout: visualResult.narrativeFlow.score * 4,
7643
+ contrast: visualResult.visualConsistency.colorPaletteConsistent ? 100 : 80,
7644
+ graphics: visualResult.executiveReadiness.score * 4,
7645
+ content: visualResult.contentQuality.score * 4,
7646
+ clarity: visualResult.contentQuality.messagesAreClear ? 100 : 75,
7647
+ effectiveness: visualResult.executiveReadiness.wouldImpress ? 100 : 60,
7648
+ consistency: visualResult.visualConsistency.score * 4
6927
7649
  },
6928
- iterations: iterativeResult.iterations,
6929
- report: iterativeResult.report
7650
+ iterations: [],
7651
+ // Visual QA is single-pass, no iteration history
7652
+ report: visualResult.verdictExplanation
6930
7653
  };
6931
7654
  }
6932
7655
  /**
@@ -7012,7 +7735,7 @@ var PresentationEngine = class {
7012
7735
  /**
7013
7736
  * Build presentation metadata.
7014
7737
  */
7015
- buildMetadata(config, analysis, slides, iterativeResult) {
7738
+ buildMetadata(config, analysis, slides, visualResult) {
7016
7739
  const wordCounts = slides.map((s) => this.countWords(s));
7017
7740
  const totalWords = wordCounts.reduce((sum, count) => sum + count, 0);
7018
7741
  const avgWordsPerSlide = Math.round(totalWords / slides.length);
@@ -7030,10 +7753,9 @@ var PresentationEngine = class {
7030
7753
  frameworks: this.detectFrameworks(analysis),
7031
7754
  presentationType: config.presentationType || analysis.detectedType
7032
7755
  };
7033
- if (iterativeResult) {
7034
- metadata.qaIterations = iterativeResult.totalIterations;
7035
- metadata.qaMaxIterations = iterativeResult.maxIterations;
7036
- metadata.dimensionScores = iterativeResult.finalScoring.dimensions;
7756
+ if (visualResult) {
7757
+ metadata.qaIterations = 1;
7758
+ metadata.qaMaxIterations = 1;
7037
7759
  }
7038
7760
  return metadata;
7039
7761
  }
@@ -7275,7 +7997,7 @@ var UnsplashImageProvider = class {
7275
7997
  };
7276
7998
  }
7277
7999
  delay(ms) {
7278
- return new Promise((resolve) => setTimeout(resolve, ms));
8000
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
7279
8001
  }
7280
8002
  };
7281
8003
  var CompositeImageProvider = class {
@@ -7326,16 +8048,11 @@ async function generate(config) {
7326
8048
  const engine = new PresentationEngine();
7327
8049
  return engine.generate(config);
7328
8050
  }
7329
- async function validate(presentation, options) {
7330
- const qaEngine = new QAEngine();
7331
- const results = await qaEngine.validate(presentation, options);
7332
- const score = qaEngine.calculateScore(results);
7333
- return {
7334
- ...results,
7335
- score
7336
- };
8051
+ async function validate(htmlPath, options) {
8052
+ const evaluator = new VisualQualityEvaluator(options?.screenshotDir);
8053
+ return evaluator.evaluate(htmlPath);
7337
8054
  }
7338
- var VERSION = "7.1.0";
8055
+ var VERSION = "9.0.0";
7339
8056
  var index_default = {
7340
8057
  generate,
7341
8058
  validate,
@@ -7344,13 +8061,11 @@ var index_default = {
7344
8061
  VERSION
7345
8062
  };
7346
8063
  export {
7347
- AutoFixEngine,
7348
8064
  ChartJsProvider,
7349
8065
  CompositeChartProvider,
7350
8066
  CompositeImageProvider,
7351
8067
  ContentAnalyzer,
7352
8068
  ContentPatternClassifier,
7353
- IterativeQAEngine,
7354
8069
  KnowledgeGateway,
7355
8070
  LocalImageProvider,
7356
8071
  MermaidProvider,
@@ -7362,7 +8077,6 @@ export {
7362
8077
  QuickChartProvider,
7363
8078
  RevealJsGenerator,
7364
8079
  ScoreCalculator,
7365
- SevenDimensionScorer,
7366
8080
  SlideFactory,
7367
8081
  SlideGenerator,
7368
8082
  TemplateEngine,
@@ -7370,12 +8084,12 @@ export {
7370
8084
  UnsplashImageProvider,
7371
8085
  VERSION,
7372
8086
  ValidationError,
7373
- createAutoFixEngine,
8087
+ VisualQualityEvaluator,
7374
8088
  createDefaultChartProvider,
7375
8089
  createDefaultImageProvider,
7376
- createIterativeQAEngine,
7377
8090
  createSlideFactory,
7378
8091
  index_default as default,
8092
+ evaluatePresentation,
7379
8093
  generate,
7380
8094
  getKnowledgeGateway,
7381
8095
  initSlideGenerator,