claude-presentation-master 6.1.1 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AutoFixEngine: () => AutoFixEngine,
33
34
  ChartJsProvider: () => ChartJsProvider,
34
35
  CompositeChartProvider: () => CompositeChartProvider,
35
36
  CompositeImageProvider: () => CompositeImageProvider,
36
37
  ContentAnalyzer: () => ContentAnalyzer,
38
+ ContentPatternClassifier: () => ContentPatternClassifier,
39
+ IterativeQAEngine: () => IterativeQAEngine,
37
40
  KnowledgeGateway: () => KnowledgeGateway,
38
41
  LocalImageProvider: () => LocalImageProvider,
39
42
  MermaidProvider: () => MermaidProvider,
@@ -45,17 +48,23 @@ __export(index_exports, {
45
48
  QuickChartProvider: () => QuickChartProvider,
46
49
  RevealJsGenerator: () => RevealJsGenerator,
47
50
  ScoreCalculator: () => ScoreCalculator,
51
+ SevenDimensionScorer: () => SevenDimensionScorer,
48
52
  SlideFactory: () => SlideFactory,
53
+ SlideGenerator: () => SlideGenerator,
49
54
  TemplateEngine: () => TemplateEngine,
50
55
  TemplateNotFoundError: () => TemplateNotFoundError,
51
56
  UnsplashImageProvider: () => UnsplashImageProvider,
52
57
  VERSION: () => VERSION,
53
58
  ValidationError: () => ValidationError,
59
+ createAutoFixEngine: () => createAutoFixEngine,
54
60
  createDefaultChartProvider: () => createDefaultChartProvider,
55
61
  createDefaultImageProvider: () => createDefaultImageProvider,
62
+ createIterativeQAEngine: () => createIterativeQAEngine,
63
+ createSlideFactory: () => createSlideFactory,
56
64
  default: () => index_default,
57
65
  generate: () => generate,
58
66
  getKnowledgeGateway: () => getKnowledgeGateway,
67
+ initSlideGenerator: () => initSlideGenerator,
59
68
  validate: () => validate
60
69
  });
61
70
  module.exports = __toCommonJS(index_exports);
@@ -93,6 +102,75 @@ var import_fs = require("fs");
93
102
  var import_path = require("path");
94
103
  var import_url = require("url");
95
104
  var yaml = __toESM(require("yaml"));
105
+
106
+ // src/utils/Logger.ts
107
+ var Logger = class {
108
+ level;
109
+ prefix;
110
+ timestamps;
111
+ constructor(options = {}) {
112
+ this.level = options.level ?? 1 /* INFO */;
113
+ this.prefix = options.prefix ?? "";
114
+ this.timestamps = options.timestamps ?? false;
115
+ }
116
+ setLevel(level) {
117
+ this.level = level;
118
+ }
119
+ formatMessage(message) {
120
+ const parts = [];
121
+ if (this.timestamps) {
122
+ parts.push(`[${(/* @__PURE__ */ new Date()).toISOString()}]`);
123
+ }
124
+ if (this.prefix) {
125
+ parts.push(`[${this.prefix}]`);
126
+ }
127
+ parts.push(message);
128
+ return parts.join(" ");
129
+ }
130
+ debug(message, ...args) {
131
+ if (this.level <= 0 /* DEBUG */) {
132
+ console.debug(this.formatMessage(message), ...args);
133
+ }
134
+ }
135
+ info(message, ...args) {
136
+ if (this.level <= 1 /* INFO */) {
137
+ console.info(this.formatMessage(message), ...args);
138
+ }
139
+ }
140
+ warn(message, ...args) {
141
+ if (this.level <= 2 /* WARN */) {
142
+ console.warn(this.formatMessage(message), ...args);
143
+ }
144
+ }
145
+ error(message, ...args) {
146
+ if (this.level <= 3 /* ERROR */) {
147
+ console.error(this.formatMessage(message), ...args);
148
+ }
149
+ }
150
+ // Progress messages (always shown unless silent)
151
+ progress(message) {
152
+ if (this.level < 4 /* SILENT */) {
153
+ console.info(message);
154
+ }
155
+ }
156
+ // Success messages
157
+ success(message) {
158
+ if (this.level < 4 /* SILENT */) {
159
+ console.info(`\u2705 ${message}`);
160
+ }
161
+ }
162
+ // Step messages for workflow progress
163
+ step(message) {
164
+ if (this.level <= 1 /* INFO */) {
165
+ console.info(` \u2713 ${message}`);
166
+ }
167
+ }
168
+ };
169
+ var logger = new Logger({
170
+ level: process.env.LOG_LEVEL ? parseInt(process.env.LOG_LEVEL) : 1 /* INFO */
171
+ });
172
+
173
+ // src/kb/KnowledgeGateway.ts
96
174
  var import_meta = {};
97
175
  function getModuleDir() {
98
176
  if (typeof import_meta !== "undefined" && import_meta.url) {
@@ -104,7 +182,7 @@ function getModuleDir() {
104
182
  return process.cwd();
105
183
  }
106
184
  var KnowledgeGateway = class {
107
- kb;
185
+ kb = {};
108
186
  loaded = false;
109
187
  constructor() {
110
188
  }
@@ -127,7 +205,7 @@ var KnowledgeGateway = class {
127
205
  const content = (0, import_fs.readFileSync)(path, "utf-8");
128
206
  this.kb = yaml.parse(content);
129
207
  this.loaded = true;
130
- console.log(` \u2713 Knowledge base loaded from ${path}`);
208
+ logger.step(`Knowledge base loaded from ${path}`);
131
209
  return;
132
210
  } catch {
133
211
  }
@@ -318,7 +396,7 @@ var KnowledgeGateway = class {
318
396
  */
319
397
  getChartGuidance(purpose) {
320
398
  this.ensureLoaded();
321
- return this.kb.chart_selection_guide.by_purpose[purpose];
399
+ return this.kb.chart_selection_guide.by_purpose?.[purpose];
322
400
  }
323
401
  /**
324
402
  * Get charts to avoid
@@ -333,7 +411,7 @@ var KnowledgeGateway = class {
333
411
  */
334
412
  getIBPitchBookStructure(type) {
335
413
  this.ensureLoaded();
336
- return this.kb.investment_banking?.pitch_book_types[type];
414
+ return this.kb.investment_banking?.pitch_book_types?.[type];
337
415
  }
338
416
  /**
339
417
  * Check if knowledge base is loaded
@@ -350,6 +428,439 @@ var KnowledgeGateway = class {
350
428
  this.ensureLoaded();
351
429
  return this.kb;
352
430
  }
431
+ // ==========================================================================
432
+ // KB-DRIVEN SLIDEFACTORY METHODS (v7.0.0)
433
+ // ==========================================================================
434
+ /**
435
+ * Get allowed slide types for a specific presentation type.
436
+ * CRITICAL: SlideFactory must ONLY use types from this list.
437
+ */
438
+ getAllowedSlideTypes(type) {
439
+ this.ensureLoaded();
440
+ const typeConfig = this.kb.presentation_types?.[type];
441
+ if (!typeConfig?.slide_types_allowed) {
442
+ const mode = this.getModeForType(type);
443
+ const templates = this.getSlideTemplates(mode);
444
+ return templates.map((t) => t.name.toLowerCase().replace(/\s+/g, "_"));
445
+ }
446
+ return typeConfig.slide_types_allowed;
447
+ }
448
+ /**
449
+ * Get validation rules for a specific presentation type.
450
+ * Returns word limits, bullet limits, whitespace requirements, etc.
451
+ */
452
+ getValidationRules(type) {
453
+ this.ensureLoaded();
454
+ const typeConfig = this.kb.presentation_types?.[type];
455
+ if (!typeConfig?.validation_rules) {
456
+ const mode = this.getModeForType(type);
457
+ return mode === "keynote" ? {
458
+ wordsPerSlide: { min: 1, max: 25, ideal: 10 },
459
+ whitespace: { min: 40, ideal: 50 },
460
+ bulletsPerSlide: { max: 3 },
461
+ actionTitlesRequired: false,
462
+ sourcesRequired: false
463
+ } : {
464
+ wordsPerSlide: { min: 40, max: 80, ideal: 60 },
465
+ whitespace: { min: 25, ideal: 30 },
466
+ bulletsPerSlide: { max: 5 },
467
+ actionTitlesRequired: true,
468
+ sourcesRequired: true
469
+ };
470
+ }
471
+ const rules = typeConfig.validation_rules;
472
+ return {
473
+ wordsPerSlide: {
474
+ min: rules.words_per_slide?.min ?? 1,
475
+ max: rules.words_per_slide?.max ?? 80,
476
+ ideal: rules.words_per_slide?.ideal ?? 40
477
+ },
478
+ whitespace: {
479
+ min: rules.whitespace?.min ?? 25,
480
+ ideal: rules.whitespace?.ideal ?? 30
481
+ },
482
+ bulletsPerSlide: {
483
+ max: rules.bullets_per_slide?.max ?? 5
484
+ },
485
+ actionTitlesRequired: rules.action_titles_required ?? false,
486
+ sourcesRequired: rules.sources_required ?? false,
487
+ calloutsRequired: rules.callouts_required,
488
+ denseDataAllowed: rules.dense_data_allowed,
489
+ codeBlocksAllowed: rules.code_blocks_allowed,
490
+ diagramsRequired: rules.diagrams_required
491
+ };
492
+ }
493
+ /**
494
+ * Get required elements that MUST be in the deck for this type.
495
+ */
496
+ getRequiredElements(type) {
497
+ this.ensureLoaded();
498
+ const typeConfig = this.kb.presentation_types?.[type];
499
+ return typeConfig?.required_elements ?? [];
500
+ }
501
+ /**
502
+ * Get anti-patterns to avoid for this presentation type.
503
+ */
504
+ getAntiPatternsForType(type) {
505
+ this.ensureLoaded();
506
+ const typeConfig = this.kb.presentation_types?.[type];
507
+ return typeConfig?.anti_patterns ?? [];
508
+ }
509
+ /**
510
+ * Get typography specifications for this presentation type.
511
+ */
512
+ getTypographyForType(type) {
513
+ this.ensureLoaded();
514
+ const typeConfig = this.kb.presentation_types?.[type];
515
+ const typo = typeConfig?.typography;
516
+ if (!typo) {
517
+ const mode = this.getModeForType(type);
518
+ const modeTypo = this.getTypography(mode);
519
+ const maxFontsValue = modeTypo.max_fonts;
520
+ return {
521
+ titles: modeTypo.titles ?? "48px, Bold",
522
+ body: modeTypo.body_text ?? "24px",
523
+ maxFonts: typeof maxFontsValue === "number" ? maxFontsValue : 2
524
+ };
525
+ }
526
+ return {
527
+ titles: typo.titles ?? "48px, Bold",
528
+ body: typo.body ?? "24px",
529
+ maxFonts: typeof typo.max_fonts === "number" ? typo.max_fonts : 2,
530
+ actionTitle: typo.action_title,
531
+ sectionHeaders: typo.section_headers,
532
+ dataLabels: typo.data_labels,
533
+ code: typo.code
534
+ };
535
+ }
536
+ /**
537
+ * Get CSS variables recipe for this presentation type.
538
+ */
539
+ getCSSVariablesForType(type) {
540
+ this.ensureLoaded();
541
+ const recipes = this.kb.type_visual_recipes;
542
+ if (recipes?.[type]?.css_variables) {
543
+ return recipes[type].css_variables;
544
+ }
545
+ return `:root {
546
+ --color-background: #FAFAF9;
547
+ --color-primary: #0F172A;
548
+ --color-secondary: #475569;
549
+ --color-accent: #0369A1;
550
+ --color-text: #18181B;
551
+ --font-display: 'Inter', sans-serif;
552
+ --font-body: 'Inter', sans-serif;
553
+ }`;
554
+ }
555
+ /**
556
+ * Get scoring weights for QA evaluation for this type.
557
+ */
558
+ getScoringWeights(type) {
559
+ this.ensureLoaded();
560
+ const typeConfig = this.kb.presentation_types?.[type];
561
+ const weights = typeConfig?.scoring_weights;
562
+ if (!weights) {
563
+ return {
564
+ visualQuality: 30,
565
+ contentQuality: 30,
566
+ expertCompliance: 30,
567
+ accessibility: 10
568
+ };
569
+ }
570
+ return {
571
+ visualQuality: weights.visual_quality ?? 30,
572
+ contentQuality: weights.content_quality ?? 30,
573
+ expertCompliance: weights.expert_compliance ?? 30,
574
+ accessibility: weights.accessibility ?? 10
575
+ };
576
+ }
577
+ /**
578
+ * Get story structure framework for this presentation type.
579
+ */
580
+ getStoryStructure(type) {
581
+ this.ensureLoaded();
582
+ const typeConfig = this.kb.presentation_types?.[type];
583
+ const mode = this.getModeForType(type);
584
+ if (mode === "business") {
585
+ return {
586
+ framework: "scqa",
587
+ requiredElements: ["situation", "complication", "answer"],
588
+ optionalElements: ["question", "evidence", "recommendation"]
589
+ };
590
+ }
591
+ return {
592
+ framework: "sparkline",
593
+ requiredElements: ["what_is", "what_could_be", "call_to_action"],
594
+ optionalElements: ["star_moment", "hook"]
595
+ };
596
+ }
597
+ /**
598
+ * Map a content pattern to the best allowed slide type.
599
+ * CRITICAL: This is how SlideFactory decides which slide type to use.
600
+ */
601
+ mapContentPatternToSlideType(pattern, allowedTypes) {
602
+ const patternToTypes = {
603
+ big_number: ["big_number", "big-number", "data_insight", "metrics_grid", "metrics-grid"],
604
+ comparison: ["comparison", "options_comparison", "two_column", "two-column"],
605
+ timeline: ["timeline", "process_timeline", "process", "roadmap"],
606
+ process: ["process", "process_timeline", "timeline", "three_column", "three-column"],
607
+ metrics: ["metrics_grid", "metrics-grid", "data_insight", "big_number", "big-number"],
608
+ quote: ["quote", "testimonial", "social_proof", "social-proof"],
609
+ code: ["code_snippet", "technical", "two_column", "two-column"],
610
+ bullets: ["bullet_points", "bullet-points", "detailed_findings", "two_column", "two-column"],
611
+ prose: ["two_column", "two-column", "bullet_points", "bullet-points", "single_statement"]
612
+ };
613
+ const preferredTypes = patternToTypes[pattern.primaryPattern] || patternToTypes.prose || [];
614
+ for (const preferred of preferredTypes) {
615
+ const underscoreVersion = preferred.replace(/-/g, "_");
616
+ const dashVersion = preferred.replace(/_/g, "-");
617
+ if (allowedTypes.includes(preferred)) return preferred;
618
+ if (allowedTypes.includes(underscoreVersion)) return underscoreVersion;
619
+ if (allowedTypes.includes(dashVersion)) return dashVersion;
620
+ }
621
+ const contentTypes = [
622
+ "bullet_points",
623
+ "bullet-points",
624
+ "two_column",
625
+ "two-column",
626
+ "three_column",
627
+ "three-column",
628
+ "data_insight"
629
+ ];
630
+ for (const ct of contentTypes) {
631
+ if (allowedTypes.includes(ct)) return ct;
632
+ }
633
+ return allowedTypes[0] ?? "bullet-points";
634
+ }
635
+ /**
636
+ * Get a specific slide template by name from the KB.
637
+ */
638
+ getSlideTemplateByName(name) {
639
+ this.ensureLoaded();
640
+ const keynoteTemplates = this.kb.slide_templates?.keynote_mode ?? [];
641
+ const businessTemplates = this.kb.slide_templates?.business_mode ?? [];
642
+ const normalizedName = name.toLowerCase().replace(/[-_]/g, " ");
643
+ for (const template of [...keynoteTemplates, ...businessTemplates]) {
644
+ const templateName = template.name.toLowerCase().replace(/[-_]/g, " ");
645
+ if (templateName.includes(normalizedName) || normalizedName.includes(templateName)) {
646
+ return {
647
+ name: template.name,
648
+ purpose: template.purpose,
649
+ elements: template.elements,
650
+ components: template.components,
651
+ wordLimit: template.word_limit ?? 60
652
+ };
653
+ }
654
+ }
655
+ return void 0;
656
+ }
657
+ // ==========================================================================
658
+ // SLIDE DEFAULTS & LABELS (v7.1.0 - Replace ALL Hardcoded Values)
659
+ // ==========================================================================
660
+ /**
661
+ * Get slide defaults for structural slides (titles, messages, labels).
662
+ * CRITICAL: SlideFactory must use these instead of hardcoded strings.
663
+ */
664
+ getSlideDefaults(type) {
665
+ this.ensureLoaded();
666
+ const mode = this.getModeForType(type);
667
+ const rules = this.getValidationRules(type);
668
+ const maxWords = rules.wordsPerSlide.max;
669
+ return {
670
+ agenda: { title: mode === "business" ? "Agenda" : "What We'll Cover" },
671
+ thankYou: {
672
+ title: mode === "business" ? "Thank You" : "Thank You",
673
+ subtitle: mode === "business" ? "Questions?" : "Let's Connect"
674
+ },
675
+ cta: {
676
+ title: mode === "business" ? "Next Steps" : "Take Action",
677
+ message: mode === "business" ? "Recommended Actions" : "Ready to Begin?",
678
+ fallback: mode === "business" ? "Contact us to learn more" : "Take the next step"
679
+ },
680
+ metricsGrid: {
681
+ title: mode === "business" ? "Key Metrics" : "The Numbers",
682
+ maxMetrics: 4
683
+ },
684
+ code: {
685
+ label: "Code Example",
686
+ maxChars: 500
687
+ },
688
+ comparison: {
689
+ leftLabel: mode === "business" ? "Current State" : "Before",
690
+ rightLabel: mode === "business" ? "Future State" : "After",
691
+ optionLabels: ["Option A", "Option B", "Option C", "Option D"]
692
+ },
693
+ column: { labelTemplate: "Point {n}" },
694
+ subtitle: { maxWords: Math.min(15, Math.floor(maxWords / 3)) },
695
+ context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
696
+ step: { maxWords: Math.min(20, Math.floor(maxWords / 4)) },
697
+ columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
698
+ };
699
+ }
700
+ /**
701
+ * Get SCQA framework titles from KB (for business mode).
702
+ */
703
+ getSCQATitles(type) {
704
+ this.ensureLoaded();
705
+ const mode = this.getModeForType(type);
706
+ if (mode === "business") {
707
+ return {
708
+ situation: "Current Situation",
709
+ complication: "The Challenge",
710
+ question: "The Critical Question",
711
+ answer: "Our Recommendation"
712
+ };
713
+ }
714
+ return {
715
+ situation: "Where We Are",
716
+ complication: "What's At Stake",
717
+ question: "The Question We Face",
718
+ answer: "The Path Forward"
719
+ };
720
+ }
721
+ /**
722
+ * Get Sparkline framework titles from KB (for keynote mode).
723
+ */
724
+ getSparklineTitles(type) {
725
+ this.ensureLoaded();
726
+ return {
727
+ whatIs: "Where We Are Today",
728
+ whatCouldBe: "What Could Be",
729
+ callToAdventure: "The Call to Adventure",
730
+ newBliss: "The New World"
731
+ };
732
+ }
733
+ /**
734
+ * Get insight markers for detecting action titles.
735
+ * These are words/phrases that indicate an insight vs a topic.
736
+ */
737
+ getInsightMarkers() {
738
+ this.ensureLoaded();
739
+ return [
740
+ // Trend indicators
741
+ "increase",
742
+ "decrease",
743
+ "grew",
744
+ "declined",
745
+ "achieve",
746
+ "exceed",
747
+ "improve",
748
+ "reduce",
749
+ "save",
750
+ "gain",
751
+ "lost",
752
+ "doubled",
753
+ "tripled",
754
+ "outperform",
755
+ "underperform",
756
+ "accelerate",
757
+ "decelerate",
758
+ // Quantitative markers
759
+ "%",
760
+ "percent",
761
+ "million",
762
+ "billion",
763
+ "thousand",
764
+ "$",
765
+ "\u20AC",
766
+ "\xA3",
767
+ "ROI",
768
+ "revenue",
769
+ "cost",
770
+ "margin",
771
+ "growth",
772
+ "decline",
773
+ // Causality markers
774
+ "due to",
775
+ "because",
776
+ "resulting in",
777
+ "leading to",
778
+ "enabling",
779
+ "driving",
780
+ "caused by",
781
+ "attributed to",
782
+ "as a result of",
783
+ // Action verbs (Minto Pyramid style)
784
+ "should",
785
+ "must",
786
+ "need to",
787
+ "recommend",
788
+ "propose",
789
+ "suggest",
790
+ "requires",
791
+ "demands",
792
+ "enables",
793
+ "prevents",
794
+ "ensures"
795
+ ];
796
+ }
797
+ /**
798
+ * Get word limit for a specific slide element type.
799
+ */
800
+ getWordLimitForElement(type, element) {
801
+ const rules = this.getValidationRules(type);
802
+ const maxWords = rules.wordsPerSlide.max;
803
+ const bulletsMax = rules.bulletsPerSlide.max;
804
+ switch (element) {
805
+ case "title":
806
+ return Math.min(15, Math.floor(maxWords / 4));
807
+ case "subtitle":
808
+ return Math.min(15, Math.floor(maxWords / 5));
809
+ case "bullet":
810
+ return Math.floor(maxWords / (bulletsMax || 5));
811
+ case "body":
812
+ return maxWords;
813
+ case "quote":
814
+ return Math.min(40, maxWords);
815
+ case "step":
816
+ return Math.min(20, Math.floor(maxWords / 4));
817
+ default:
818
+ return maxWords;
819
+ }
820
+ }
821
+ /**
822
+ * Validate a slide against KB rules for the given presentation type.
823
+ */
824
+ validateSlideAgainstKB(slide, type) {
825
+ const rules = this.getValidationRules(type);
826
+ const allowedTypes = this.getAllowedSlideTypes(type);
827
+ const violations = [];
828
+ const warnings = [];
829
+ const normalizedType = slide.type.replace(/-/g, "_");
830
+ const isAllowed = allowedTypes.some(
831
+ (t) => t === slide.type || t === normalizedType || t.replace(/_/g, "-") === slide.type
832
+ );
833
+ if (!isAllowed) {
834
+ violations.push(`Slide type '${slide.type}' not allowed for ${type}. Allowed: ${allowedTypes.join(", ")}`);
835
+ }
836
+ if (slide.wordCount > rules.wordsPerSlide.max) {
837
+ violations.push(`Word count ${slide.wordCount} exceeds max ${rules.wordsPerSlide.max}`);
838
+ } else if (slide.wordCount < rules.wordsPerSlide.min) {
839
+ warnings.push(`Word count ${slide.wordCount} below min ${rules.wordsPerSlide.min}`);
840
+ }
841
+ if (slide.bulletCount > rules.bulletsPerSlide.max) {
842
+ violations.push(`Bullet count ${slide.bulletCount} exceeds max ${rules.bulletsPerSlide.max}`);
843
+ }
844
+ if (rules.actionTitlesRequired && !slide.hasActionTitle) {
845
+ violations.push(`Action title required for ${type} but not present`);
846
+ }
847
+ if (rules.sourcesRequired && !slide.hasSource) {
848
+ warnings.push(`Source citation recommended for ${type}`);
849
+ }
850
+ const fixes = {};
851
+ if (slide.wordCount > rules.wordsPerSlide.max) {
852
+ fixes.wordCount = rules.wordsPerSlide.max;
853
+ }
854
+ if (slide.bulletCount > rules.bulletsPerSlide.max) {
855
+ fixes.bulletCount = rules.bulletsPerSlide.max;
856
+ }
857
+ return {
858
+ valid: violations.length === 0,
859
+ violations,
860
+ warnings,
861
+ fixes
862
+ };
863
+ }
353
864
  };
354
865
  var gatewayInstance = null;
355
866
  async function getKnowledgeGateway() {
@@ -452,14 +963,14 @@ var ContentAnalyzer = class {
452
963
  const text = this.parseContent(content, contentType);
453
964
  const title = this.extractTitle(text);
454
965
  const detectedType = this.detectPresentationType(text);
455
- console.log(` \u2713 Detected type: ${detectedType}`);
966
+ logger.step(`Detected type: ${detectedType}`);
456
967
  const sections = this.extractSections(text);
457
- console.log(` \u2713 Found ${sections.length} sections`);
968
+ logger.step(`Found ${sections.length} sections`);
458
969
  const scqa = this.extractSCQA(text);
459
970
  const sparkline = this.extractSparkline(text);
460
971
  const keyMessages = this.extractKeyMessages(text);
461
972
  const dataPoints = this.extractDataPoints(text);
462
- console.log(` \u2713 Found ${dataPoints.length} data points`);
973
+ logger.step(`Found ${dataPoints.length} data points`);
463
974
  const titles = sections.map((s) => s.header).filter((h) => h.length > 0);
464
975
  const starMoments = this.extractStarMoments(text);
465
976
  const estimatedSlideCount = Math.max(5, sections.length + 3);
@@ -487,12 +998,22 @@ var ContentAnalyzer = class {
487
998
  }
488
999
  /**
489
1000
  * Parse content based on type
1001
+ * CRITICAL: Strip code blocks FIRST to prevent code from becoming slides
490
1002
  */
491
1003
  parseContent(content, contentType) {
492
- if (contentType === "markdown" || content.includes("#") || content.includes("- ")) {
493
- return content;
494
- }
495
- return content;
1004
+ let text = content;
1005
+ text = text.replace(/```[\s\S]*?```/g, "");
1006
+ text = text.replace(/`[^`]{50,}`/g, "");
1007
+ text = text.split("\n").filter((line) => {
1008
+ const trimmed = line.trim();
1009
+ if (/^(import|export|const|let|var|function|class|interface|type|async|await|return|if|for|while)\s/.test(trimmed)) return false;
1010
+ if (/^(\/\/|\/\*|\*|#!)/.test(trimmed)) return false;
1011
+ if (/^\s*[{}\[\]();]/.test(trimmed)) return false;
1012
+ if (/^[\w.]+\s*\(.*\)\s*;?\s*$/.test(trimmed)) return false;
1013
+ if (/^[\w.]+\s*=\s*/.test(trimmed) && /[{(\[]/.test(trimmed)) return false;
1014
+ return true;
1015
+ }).join("\n");
1016
+ return text;
496
1017
  }
497
1018
  /**
498
1019
  * Extract the main title from content
@@ -569,12 +1090,16 @@ var ContentAnalyzer = class {
569
1090
  }
570
1091
  const bulletMatch = trimmedLine.match(/^[-*+]\s+(.+)$/);
571
1092
  if (bulletMatch && bulletMatch[1] && currentSection) {
572
- currentSection.bullets.push(bulletMatch[1]);
1093
+ const bulletText = bulletMatch[1];
1094
+ currentSection.bullets.push(bulletText);
1095
+ this.extractMetricsFromText(bulletText, currentSection.metrics);
573
1096
  continue;
574
1097
  }
575
1098
  const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/);
576
1099
  if (numberedMatch && numberedMatch[1] && currentSection) {
577
- currentSection.bullets.push(numberedMatch[1]);
1100
+ const itemText = numberedMatch[1];
1101
+ currentSection.bullets.push(itemText);
1102
+ this.extractMetricsFromText(itemText, currentSection.metrics);
578
1103
  continue;
579
1104
  }
580
1105
  const metricMatch = trimmedLine.match(/\$?([\d,]+\.?\d*)[%]?\s*[-–—:]\s*(.+)/);
@@ -585,6 +1110,9 @@ var ContentAnalyzer = class {
585
1110
  });
586
1111
  continue;
587
1112
  }
1113
+ if (currentSection && trimmedLine) {
1114
+ this.extractMetricsFromText(trimmedLine, currentSection.metrics);
1115
+ }
588
1116
  if (trimmedLine.includes("|") && currentSection) {
589
1117
  const cells = trimmedLine.split("|").map((c) => c.trim()).filter((c) => c && !c.match(/^-+$/));
590
1118
  if (cells.length >= 2) {
@@ -610,6 +1138,7 @@ var ContentAnalyzer = class {
610
1138
  }
611
1139
  /**
612
1140
  * Extract SCQA structure (Barbara Minto)
1141
+ * CRITICAL: Answer must be clean prose, NOT bullet points
613
1142
  */
614
1143
  extractSCQA(text) {
615
1144
  const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
@@ -617,34 +1146,45 @@ var ContentAnalyzer = class {
617
1146
  let complication = "";
618
1147
  let question = "";
619
1148
  let answer = "";
620
- for (const para of paragraphs.slice(0, 3)) {
1149
+ for (const para of paragraphs.slice(0, 5)) {
1150
+ if (para.startsWith("-") || para.startsWith("#") || para.startsWith("|")) continue;
621
1151
  if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
622
1152
  situation = this.extractFirstSentence(para);
623
1153
  break;
624
1154
  }
625
1155
  }
626
1156
  for (const para of paragraphs) {
1157
+ if (para.startsWith("-") || para.startsWith("|")) continue;
627
1158
  if (this.containsSignals(para.toLowerCase(), this.complicationSignals)) {
628
1159
  complication = this.extractFirstSentence(para);
629
1160
  break;
630
1161
  }
631
1162
  }
632
- const middleStart = Math.floor(paragraphs.length * 0.2);
633
- const middleEnd = Math.floor(paragraphs.length * 0.8);
634
- for (const para of paragraphs.slice(middleStart, middleEnd)) {
1163
+ for (const para of paragraphs) {
1164
+ if (para.startsWith("-") || para.includes("\n-") || para.startsWith("|")) continue;
1165
+ if (para.startsWith("*") && !para.startsWith("**")) continue;
635
1166
  const lowerPara = para.toLowerCase();
636
1167
  if (this.containsSignals(lowerPara, this.answerSignals) && !this.containsSignals(lowerPara, this.ctaSignals)) {
637
- answer = this.extractFirstSentence(para);
638
- break;
1168
+ const sentence = this.extractFirstSentence(para);
1169
+ if (sentence.split(/\s+/).length >= 8 && /\b(is|are|will|can|provides?|delivers?|enables?)\b/i.test(sentence)) {
1170
+ answer = sentence;
1171
+ break;
1172
+ }
639
1173
  }
640
1174
  }
641
- if (!situation && paragraphs.length > 0 && paragraphs[0]) {
642
- situation = this.extractFirstSentence(paragraphs[0]);
1175
+ if (!situation && paragraphs.length > 0) {
1176
+ for (const para of paragraphs.slice(0, 5)) {
1177
+ if (!para.startsWith("-") && !para.startsWith("#") && para.length > 50) {
1178
+ situation = this.extractFirstSentence(para);
1179
+ break;
1180
+ }
1181
+ }
643
1182
  }
644
- if (!answer && paragraphs.length > 2) {
645
- for (const para of paragraphs.slice(1, Math.floor(paragraphs.length * 0.5))) {
1183
+ if (!answer) {
1184
+ for (const para of paragraphs) {
1185
+ if (para.startsWith("-") || para.includes("\n-")) continue;
646
1186
  const lowerPara = para.toLowerCase();
647
- if (lowerPara.includes("recommend") || lowerPara.includes("strategy") || lowerPara.includes("solution") || lowerPara.includes("approach")) {
1187
+ if (lowerPara.includes("bottom line") || lowerPara.includes("delivers both") || lowerPara.includes("the result") || lowerPara.includes("in summary")) {
648
1188
  answer = this.extractFirstSentence(para);
649
1189
  break;
650
1190
  }
@@ -803,56 +1343,123 @@ var ContentAnalyzer = class {
803
1343
  }
804
1344
  /**
805
1345
  * Extract data points (metrics with values)
806
- * IMPROVED: Smarter label extraction that understands markdown tables
1346
+ * REWRITTEN: Proper markdown table parsing and meaningful label extraction
807
1347
  */
808
1348
  extractDataPoints(text) {
809
1349
  const dataPoints = [];
810
1350
  const usedValues = /* @__PURE__ */ new Set();
811
- const tableLines = text.split("\n").filter((line) => line.includes("|"));
812
- if (tableLines.length >= 3) {
813
- const dataRows = tableLines.filter((line) => !line.match(/^[\s|:-]+$/));
814
- if (dataRows.length >= 2) {
815
- for (const row of dataRows.slice(1)) {
816
- const cells = row.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
817
- if (cells.length >= 2) {
818
- const valueCell = cells.find((c) => /^\$?[\d,]+\.?\d*[MBK%]?$/.test(c.replace(/[,$]/g, "")));
819
- const labelCell = cells[0];
820
- if (valueCell && labelCell && !usedValues.has(valueCell)) {
821
- usedValues.add(valueCell);
822
- dataPoints.push({ value: valueCell, label: labelCell.slice(0, 40) });
823
- }
1351
+ const bulletPatterns = text.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?(?:\s*\([^)]+\))?)/gm);
1352
+ if (bulletPatterns) {
1353
+ for (const match of bulletPatterns) {
1354
+ const parsed = match.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?)/);
1355
+ if (parsed && parsed[1] && parsed[2]) {
1356
+ const label = parsed[1].trim();
1357
+ const value = parsed[2].trim();
1358
+ if (!usedValues.has(value) && label.length >= 5) {
1359
+ usedValues.add(value);
1360
+ dataPoints.push({ value, label: this.cleanMetricLabel(label) });
824
1361
  }
825
1362
  }
826
1363
  }
827
1364
  }
828
1365
  const lines = text.split("\n");
1366
+ let inTable = false;
1367
+ let tableRows = [];
1368
+ let headerRow = [];
829
1369
  for (const line of lines) {
830
- if (line.includes("|")) continue;
831
- const percentMatch = line.match(/(\w+(?:\s+\w+){0,4})\s+(?:by\s+)?(\d+(?:\.\d+)?%)/i);
832
- if (percentMatch && percentMatch[2] && percentMatch[1] && !usedValues.has(percentMatch[2])) {
833
- usedValues.add(percentMatch[2]);
834
- dataPoints.push({ value: percentMatch[2], label: percentMatch[1].slice(0, 40) });
1370
+ if (line.includes("|") && line.trim().length > 3) {
1371
+ const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
1372
+ if (cells.every((c) => /^[-:]+$/.test(c))) continue;
1373
+ if (!inTable) {
1374
+ headerRow = cells;
1375
+ inTable = true;
1376
+ } else {
1377
+ tableRows.push(cells);
1378
+ }
1379
+ } else if (inTable && tableRows.length > 0) {
1380
+ this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
1381
+ inTable = false;
1382
+ tableRows = [];
1383
+ headerRow = [];
835
1384
  }
836
- const dollarMatch = line.match(/\$(\d+(?:\.\d+)?)\s*(million|billion|M|B|K)?/i);
837
- if (dollarMatch && dataPoints.length < 6) {
838
- const fullValue = "$" + dollarMatch[1] + (dollarMatch[2] ? " " + dollarMatch[2] : "");
839
- if (!usedValues.has(fullValue)) {
840
- usedValues.add(fullValue);
841
- const label = this.extractLabelFromLine(line, fullValue);
842
- dataPoints.push({ value: fullValue, label });
1385
+ }
1386
+ if (tableRows.length > 0) {
1387
+ this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
1388
+ }
1389
+ for (const line of lines) {
1390
+ if (line.includes("|")) continue;
1391
+ if (/```/.test(line)) continue;
1392
+ const percentPattern = line.match(/(\d+(?:\.\d+)?%)\s+(reduction|increase|improvement|growth|decrease)\s+(?:in\s+)?([^.]+)/i);
1393
+ if (percentPattern && percentPattern[1] && percentPattern[3]) {
1394
+ const value = percentPattern[1];
1395
+ if (!usedValues.has(value)) {
1396
+ usedValues.add(value);
1397
+ const action = percentPattern[2] || "change";
1398
+ const subject = percentPattern[3].slice(0, 30).trim();
1399
+ dataPoints.push({ value, label: `${action} in ${subject}` });
843
1400
  }
844
1401
  }
845
1402
  }
846
1403
  return dataPoints.slice(0, 4);
847
1404
  }
1405
+ /**
1406
+ * Extract metrics from a parsed markdown table
1407
+ */
1408
+ extractMetricsFromTable(headers, rows, dataPoints, usedValues) {
1409
+ for (const row of rows) {
1410
+ const label = row[0];
1411
+ if (!label || label.length < 3) continue;
1412
+ for (let i = 1; i < row.length; i++) {
1413
+ const cell = row[i];
1414
+ if (!cell) continue;
1415
+ if (/^\$?[\d,]+\.?\d*[%MBK]?$/.test(cell.replace(/[,\s]/g, "")) || /^\d+\s*(seconds?|minutes?|hours?|ms|gb|mb)$/i.test(cell)) {
1416
+ if (!usedValues.has(cell)) {
1417
+ usedValues.add(cell);
1418
+ const colHeader = headers[i] || "";
1419
+ const fullLabel = colHeader && colHeader !== label ? `${label} (${colHeader})` : label;
1420
+ dataPoints.push({
1421
+ value: cell,
1422
+ label: this.cleanMetricLabel(fullLabel)
1423
+ });
1424
+ break;
1425
+ }
1426
+ }
1427
+ }
1428
+ }
1429
+ }
1430
+ /**
1431
+ * Clean a metric label to ensure it's complete and meaningful
1432
+ */
1433
+ cleanMetricLabel(raw) {
1434
+ let label = raw.replace(/\*\*/g, "").replace(/\|/g, " ").replace(/[:\-–—]+$/, "").replace(/\s+/g, " ").trim();
1435
+ if (label.length > 0) {
1436
+ label = label.charAt(0).toUpperCase() + label.slice(1);
1437
+ }
1438
+ if (label.length > 40) {
1439
+ const words = label.split(/\s+/);
1440
+ label = "";
1441
+ for (const word of words) {
1442
+ if ((label + " " + word).length <= 40) {
1443
+ label = label ? label + " " + word : word;
1444
+ } else {
1445
+ break;
1446
+ }
1447
+ }
1448
+ }
1449
+ return label || "Value";
1450
+ }
848
1451
  /**
849
1452
  * Extract a meaningful label from a line containing a metric
850
1453
  */
851
1454
  extractLabelFromLine(line, value) {
1455
+ const colonMatch = line.match(/([^:]+):\s*\$?[\d]/);
1456
+ if (colonMatch && colonMatch[1]) {
1457
+ return this.cleanMetricLabel(colonMatch[1]);
1458
+ }
852
1459
  const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
853
1460
  const beforeValue = cleaned.split(value)[0] || "";
854
1461
  const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
855
- return words.slice(-4).join(" ").slice(0, 40) || "Value";
1462
+ return this.cleanMetricLabel(words.slice(-4).join(" "));
856
1463
  }
857
1464
  /**
858
1465
  * Check if text contains any of the signals
@@ -877,545 +1484,958 @@ var ContentAnalyzer = class {
877
1484
  const fallback = cleaned.slice(0, 150);
878
1485
  return fallback.length >= 20 ? fallback : "";
879
1486
  }
880
- };
881
-
882
- // src/core/SlideFactory.ts
883
- var SlideFactory = class {
884
- templates;
885
- usedContent = /* @__PURE__ */ new Set();
886
- constructor() {
887
- this.templates = this.initializeTemplates();
888
- }
889
1487
  /**
890
- * Check if content has already been used (deduplication)
1488
+ * Extract metrics from natural language text
1489
+ * Handles formats like "40% productivity loss", "$2.5M investment"
891
1490
  */
892
- isContentUsed(content) {
893
- if (!content) return true;
894
- const normalized = content.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 50);
895
- if (this.usedContent.has(normalized)) {
896
- return true;
897
- }
898
- this.usedContent.add(normalized);
899
- return false;
900
- }
901
- /**
902
- * Create slides from analyzed content.
903
- *
904
- * ARCHITECTURE (per KB expert methodologies):
905
- * 1. Title slide - always first
906
- * 2. Agenda slide - business mode only with 3+ sections
907
- * 3. SCQA slides - Situation, Complication (per Minto)
908
- * 4. Content slides - from sections with bullets/content
909
- * 5. Metrics slide - if data points exist
910
- * 6. Solution slide - SCQA answer
911
- * 7. STAR moments - only if high-quality (per Duarte)
912
- * 8. CTA slide - if call to action exists
913
- * 9. Thank you slide - always last
914
- */
915
- async createSlides(analysis, mode) {
916
- const slides = [];
917
- let slideIndex = 0;
918
- this.usedContent.clear();
919
- slides.push(this.createTitleSlide(slideIndex++, analysis));
920
- this.isContentUsed(analysis.titles[0] ?? "");
921
- const substantiveSections = analysis.sections.filter(
922
- (s) => s.level === 2 && (s.bullets.length > 0 || s.content.length > 50)
923
- );
924
- if (mode === "business" && substantiveSections.length >= 3) {
925
- slides.push(this.createAgendaSlide(slideIndex++, analysis));
926
- }
927
- if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
928
- slides.push(this.createContextSlide(slideIndex++, analysis, mode));
929
- }
930
- if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
931
- slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
932
- }
933
- for (const section of substantiveSections.slice(0, 4)) {
934
- const headerUsed = this.isContentUsed(section.header);
935
- const contentUsed = section.content.length > 30 && this.isContentUsed(section.content.slice(0, 80));
936
- if (!headerUsed && !contentUsed) {
937
- if (section.bullets.length > 0) {
938
- slides.push(this.createSectionBulletSlide(slideIndex++, section, mode));
939
- } else if (section.content.length > 50) {
940
- slides.push(this.createSectionContentSlide(slideIndex++, section, mode));
941
- }
1491
+ extractMetricsFromText(text, metrics) {
1492
+ const usedValues = new Set(metrics.map((m) => m.value));
1493
+ const moneyMatches = text.matchAll(/(\$[\d,.]+[MBK]?)\s+([a-zA-Z][a-zA-Z\s]{2,30})/g);
1494
+ for (const match of moneyMatches) {
1495
+ const value = match[1];
1496
+ const rawLabel = match[2]?.trim();
1497
+ if (value && rawLabel && !usedValues.has(value)) {
1498
+ usedValues.add(value);
1499
+ metrics.push({ value, label: this.cleanMetricLabel(rawLabel) });
942
1500
  }
943
1501
  }
944
- for (const message of analysis.keyMessages) {
945
- const wordCount = message.split(/\s+/).length;
946
- if (wordCount >= 6 && !/^(The |Our |Your |Overview|Introduction|Conclusion)/i.test(message) && !this.isContentUsed(message)) {
947
- slides.push(this.createMessageSlide(slideIndex++, message, mode));
1502
+ const percentMatches = text.matchAll(/(\d+(?:\.\d+)?%)\s+([a-zA-Z][a-zA-Z\s]{2,30})/g);
1503
+ for (const match of percentMatches) {
1504
+ const value = match[1];
1505
+ const rawLabel = match[2]?.trim();
1506
+ if (value && rawLabel && !usedValues.has(value)) {
1507
+ usedValues.add(value);
1508
+ metrics.push({ value, label: this.cleanMetricLabel(rawLabel) });
948
1509
  }
949
1510
  }
950
- if (analysis.dataPoints.length >= 2) {
951
- slides.push(this.createMetricsSlide(slideIndex++, analysis.dataPoints));
952
- }
953
- if (analysis.scqa.answer && analysis.scqa.answer.length > 30 && !this.isContentUsed(analysis.scqa.answer)) {
954
- slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
955
- }
956
- const verbPattern = /\b(is|are|was|were|will|can|should|must|has|have|provides?|enables?|allows?|achieves?|exceeds?|results?|generates?|delivers?|creates?)\b/i;
957
- for (const starMoment of analysis.starMoments.slice(0, 2)) {
958
- const wordCount = starMoment.split(/\s+/).length;
959
- const hasVerb = verbPattern.test(starMoment);
960
- if (wordCount >= 6 && starMoment.length >= 40 && hasVerb && !this.isContentUsed(starMoment)) {
961
- slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
1511
+ const multMatches = text.matchAll(/(\d+[xX])\s+([a-zA-Z][a-zA-Z\s]{2,20})/g);
1512
+ for (const match of multMatches) {
1513
+ const value = match[1];
1514
+ const rawLabel = match[2]?.trim();
1515
+ if (value && rawLabel && !usedValues.has(value)) {
1516
+ usedValues.add(value);
1517
+ metrics.push({ value, label: this.cleanMetricLabel(rawLabel) });
962
1518
  }
963
1519
  }
964
- if (analysis.sparkline.callToAdventure && analysis.sparkline.callToAdventure.length > 20 && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
965
- slides.push(this.createCTASlide(slideIndex++, analysis, mode));
1520
+ const numMatches = text.matchAll(/(\d{3,}\+?)\s+([a-zA-Z][a-zA-Z\s]{2,20})/g);
1521
+ for (const match of numMatches) {
1522
+ const value = match[1];
1523
+ const rawLabel = match[2]?.trim();
1524
+ if (value && rawLabel && !usedValues.has(value)) {
1525
+ usedValues.add(value);
1526
+ metrics.push({ value, label: this.cleanMetricLabel(rawLabel) });
1527
+ }
966
1528
  }
967
- slides.push(this.createThankYouSlide(slideIndex++));
968
- return slides;
969
1529
  }
1530
+ };
1531
+
1532
+ // src/core/ContentPatternClassifier.ts
1533
+ var ContentPatternClassifier = class {
1534
+ // Regex patterns for content detection
1535
+ patterns = {
1536
+ // Big numbers: $4.88M, 23%, 10x, 500+, etc.
1537
+ bigNumber: /(\$[\d,.]+[MBK]?|\d+(?:\.\d+)?%|\d+[xX]|\d{2,}(?:\+|,\d{3})+)/,
1538
+ // Comparison language
1539
+ comparison: /\b(vs\.?|versus|compared\s+to|before\s+(?:and\s+)?after|better\s+than|worse\s+than|alternative|option\s+[A-Z1-3])\b/i,
1540
+ // Timeline markers
1541
+ timeline: /\b(phase\s*\d|week\s*\d|month\s*\d|day\s*\d|Q[1-4]|20\d{2}|step\s*\d|year\s*\d|sprint\s*\d)\b/i,
1542
+ // Process/steps language
1543
+ process: /\b(step\s*\d|stage\s*\d|pillar|phase|first|second|third|finally|then|next|workflow|process)\b/i,
1544
+ // Quote patterns
1545
+ quote: /^[""]|[""]\s*[-—–]\s*|^\s*>\s*|[""]$/,
1546
+ // Code patterns
1547
+ code: /```|`[^`]+`|function\s*\(|const\s+\w+\s*=|import\s+{|class\s+\w+|=>\s*{/,
1548
+ // Metric patterns (for detecting multiple metrics)
1549
+ metric: /(\d+(?:\.\d+)?(?:%|[xX]|[MBK])?)\s*[-–:]\s*|\b(?:increased|decreased|grew|improved|reduced)\s+(?:by\s+)?(\d+)/gi
1550
+ };
970
1551
  /**
971
- * Create a slide from a section with bullets
1552
+ * Classify a content section to determine its primary pattern.
1553
+ * Returns a ContentPattern object used by SlideFactory to select slide type.
972
1554
  */
973
- createSectionBulletSlide(index, section, mode) {
974
- const bullets = section.bullets.slice(0, mode === "keynote" ? 3 : 5);
975
- return {
976
- index,
977
- type: "bullet-points",
978
- data: {
979
- title: this.truncate(section.header, 60),
980
- bullets: bullets.map((b) => this.cleanText(b).slice(0, 80))
981
- },
982
- classes: ["slide-bullet-points"]
1555
+ classify(section) {
1556
+ const fullText = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
1557
+ const wordCount = fullText.split(/\s+/).filter((w) => w.length > 0).length;
1558
+ const hasBigNumber = this.patterns.bigNumber.test(fullText);
1559
+ const bigNumberMatch = fullText.match(this.patterns.bigNumber);
1560
+ const hasComparison = this.patterns.comparison.test(fullText);
1561
+ const hasTimeline = this.patterns.timeline.test(fullText);
1562
+ const hasProcess = this.patterns.process.test(section.header) || section.bullets.length >= 3 && this.hasNumberedItems(section.bullets);
1563
+ const hasQuote = this.patterns.quote.test(section.content);
1564
+ const hasCode = this.patterns.code.test(section.content);
1565
+ const metricMatches = fullText.match(this.patterns.metric);
1566
+ const hasMetrics = section.metrics.length >= 3 || metricMatches && metricMatches.length >= 3;
1567
+ let primaryPattern = "prose";
1568
+ if (hasQuote && section.content.length > 20) {
1569
+ primaryPattern = "quote";
1570
+ } else if (hasCode) {
1571
+ primaryPattern = "code";
1572
+ } else if (hasBigNumber && wordCount < 30) {
1573
+ primaryPattern = "big_number";
1574
+ } else if (hasMetrics) {
1575
+ primaryPattern = "metrics";
1576
+ } else if (hasComparison) {
1577
+ primaryPattern = "comparison";
1578
+ } else if (hasTimeline) {
1579
+ primaryPattern = "timeline";
1580
+ } else if (hasProcess && section.bullets.length >= 3) {
1581
+ primaryPattern = "process";
1582
+ } else if (section.bullets.length >= 2) {
1583
+ primaryPattern = "bullets";
1584
+ } else {
1585
+ primaryPattern = "prose";
1586
+ }
1587
+ const result = {
1588
+ hasBigNumber,
1589
+ hasComparison,
1590
+ hasTimeline,
1591
+ hasProcess,
1592
+ hasMetrics: !!hasMetrics,
1593
+ hasQuote,
1594
+ hasCode,
1595
+ bulletCount: section.bullets.length,
1596
+ wordCount,
1597
+ primaryPattern
983
1598
  };
1599
+ if (bigNumberMatch && bigNumberMatch[1]) {
1600
+ result.bigNumberValue = bigNumberMatch[1];
1601
+ }
1602
+ return result;
984
1603
  }
985
1604
  /**
986
- * Create a slide from a section with body content
1605
+ * Check if bullets appear to be numbered/ordered items
987
1606
  */
988
- createSectionContentSlide(index, section, mode) {
989
- if (mode === "keynote") {
990
- return {
991
- index,
992
- type: "single-statement",
993
- data: {
994
- title: this.truncate(this.extractFirstSentence(section.content), 80),
995
- keyMessage: section.header
996
- },
997
- classes: ["slide-single-statement"]
998
- };
1607
+ hasNumberedItems(bullets) {
1608
+ let numberedCount = 0;
1609
+ for (const bullet of bullets) {
1610
+ if (/^\d+[.)]\s|^[a-z][.)]\s|^step\s*\d/i.test(bullet.trim())) {
1611
+ numberedCount++;
1612
+ }
999
1613
  }
1000
- return {
1001
- index,
1002
- type: "two-column",
1003
- data: {
1004
- title: this.truncate(section.header, 60),
1005
- body: this.truncate(section.content, 200)
1006
- },
1007
- classes: ["slide-two-column"]
1008
- };
1614
+ return numberedCount >= 2;
1009
1615
  }
1010
1616
  /**
1011
- * Extract first sentence from text
1617
+ * Extract the big number value from content for display
1012
1618
  */
1013
- extractFirstSentence(text) {
1014
- const cleaned = this.cleanText(text);
1015
- const match = cleaned.match(/^[^.!?]+[.!?]/);
1016
- return match ? match[0].trim() : cleaned.slice(0, 100);
1619
+ extractBigNumber(content) {
1620
+ const match = content.match(this.patterns.bigNumber);
1621
+ if (!match || !match[1]) return null;
1622
+ const value = match[1];
1623
+ const afterNumber = content.slice(content.indexOf(value) + value.length);
1624
+ const context = afterNumber.trim().slice(0, 60).split(/[.!?]/)[0]?.trim() ?? "";
1625
+ return { value, context };
1017
1626
  }
1018
1627
  /**
1019
- * Create a metrics slide from data points
1628
+ * Extract comparison elements from content
1020
1629
  */
1021
- createMetricsSlide(index, dataPoints) {
1022
- const metrics = dataPoints.slice(0, 4).map((dp) => ({
1023
- value: dp.value,
1024
- label: this.cleanText(dp.label).slice(0, 40)
1025
- }));
1026
- return {
1027
- index,
1028
- type: "metrics-grid",
1029
- data: {
1030
- title: "Key Metrics",
1031
- metrics
1032
- },
1033
- classes: ["slide-metrics-grid"]
1034
- };
1630
+ extractComparison(section) {
1631
+ const text = section.content;
1632
+ const vsMatch = text.match(/(.{10,50})\s+(?:vs\.?|versus)\s+(.{10,50})/i);
1633
+ if (vsMatch && vsMatch[1] && vsMatch[2]) {
1634
+ return { left: vsMatch[1].trim(), right: vsMatch[2].trim() };
1635
+ }
1636
+ const beforeAfterMatch = text.match(/before[:\s]+(.{10,100})(?:after[:\s]+(.{10,100}))?/i);
1637
+ if (beforeAfterMatch && beforeAfterMatch[1]) {
1638
+ const left = beforeAfterMatch[1].trim();
1639
+ const right = beforeAfterMatch[2] ? beforeAfterMatch[2].trim() : "After";
1640
+ return { left, right };
1641
+ }
1642
+ if (section.bullets.length === 2) {
1643
+ const left = section.bullets[0];
1644
+ const right = section.bullets[1];
1645
+ if (left && right) {
1646
+ return { left, right };
1647
+ }
1648
+ }
1649
+ return null;
1035
1650
  }
1036
1651
  /**
1037
- * Create a title slide.
1652
+ * Extract timeline/process steps from content
1038
1653
  */
1039
- createTitleSlide(index, analysis) {
1040
- let subtitle = "";
1041
- if (analysis.scqa.answer && analysis.scqa.answer.length > 10) {
1042
- subtitle = analysis.scqa.answer;
1043
- } else if (analysis.starMoments.length > 0 && analysis.starMoments[0]) {
1044
- subtitle = analysis.starMoments[0];
1654
+ extractSteps(section) {
1655
+ const steps = [];
1656
+ for (const bullet of section.bullets) {
1657
+ const stepMatch = bullet.match(/^((?:step|phase|stage)\s*\d+|[1-9]\.|\d+\))\s*[-:.]?\s*(.+)/i);
1658
+ if (stepMatch && stepMatch[1] && stepMatch[2]) {
1659
+ steps.push({
1660
+ label: stepMatch[1].trim(),
1661
+ description: stepMatch[2].trim()
1662
+ });
1663
+ } else {
1664
+ steps.push({
1665
+ label: `${steps.length + 1}`,
1666
+ description: bullet
1667
+ });
1668
+ }
1045
1669
  }
1046
- return {
1047
- index,
1048
- type: "title",
1049
- data: {
1050
- title: analysis.titles[0] ?? "Presentation",
1051
- subtitle: this.truncate(subtitle, 80),
1052
- keyMessage: analysis.scqa.answer
1053
- },
1054
- classes: ["slide-title"]
1055
- };
1670
+ return steps;
1056
1671
  }
1057
1672
  /**
1058
- * Create an agenda slide.
1673
+ * Determine if content is suitable for a three-column layout
1674
+ * (3 pillars, 3 key points, etc.)
1059
1675
  */
1060
- createAgendaSlide(index, analysis) {
1061
- return {
1062
- index,
1063
- type: "agenda",
1064
- data: {
1065
- title: "Agenda",
1066
- bullets: analysis.keyMessages.map((msg, i) => `${i + 1}. ${this.truncate(msg, 50)}`)
1067
- },
1068
- classes: ["slide-agenda"]
1069
- };
1676
+ isSuitableForThreeColumn(section) {
1677
+ if (section.bullets.length === 3) return true;
1678
+ if (/three|3\s*(pillar|point|key|step)/i.test(section.header)) return true;
1679
+ const threePartPattern = /first[,.].*second[,.].*third/i;
1680
+ if (threePartPattern.test(section.content)) return true;
1681
+ return false;
1070
1682
  }
1071
1683
  /**
1072
- * Create a context/situation slide.
1684
+ * Check if content should be displayed as a quote slide
1073
1685
  */
1074
- createContextSlide(index, analysis, mode) {
1075
- if (mode === "keynote") {
1076
- return {
1077
- index,
1078
- type: "single-statement",
1079
- data: {
1080
- title: this.truncate(analysis.scqa.situation, 80),
1081
- keyMessage: "The current state"
1082
- },
1083
- classes: ["slide-single-statement"]
1084
- };
1686
+ isQuoteContent(section) {
1687
+ const content = section.content.trim();
1688
+ if (!this.patterns.quote.test(content)) {
1689
+ return { isQuote: false };
1085
1690
  }
1691
+ const attrMatch = content.match(/[-—–]\s*(.+)$/);
1692
+ if (attrMatch && attrMatch[1]) {
1693
+ return { isQuote: true, attribution: attrMatch[1].trim() };
1694
+ }
1695
+ return { isQuote: true };
1696
+ }
1697
+ };
1698
+
1699
+ // src/core/SlideFactory.ts
1700
+ var SlideFactory = class {
1701
+ kb;
1702
+ presentationType;
1703
+ classifier;
1704
+ config;
1705
+ usedContent;
1706
+ usedTitles;
1707
+ constructor(kb, type) {
1708
+ this.kb = kb;
1709
+ this.presentationType = type;
1710
+ this.classifier = new ContentPatternClassifier();
1711
+ this.usedContent = /* @__PURE__ */ new Set();
1712
+ this.usedTitles = /* @__PURE__ */ new Set();
1713
+ this.config = this.loadKBConfig(type);
1714
+ logger.step(`SlideFactory v7.1.0 initialized for ${type} (${this.config.mode} mode)`);
1715
+ logger.step(`KB Config loaded: ${this.config.allowedTypes.length} allowed types`);
1716
+ }
1717
+ /**
1718
+ * Load ALL configuration from KB - ZERO hardcoded values after this.
1719
+ */
1720
+ loadKBConfig(type) {
1086
1721
  return {
1087
- index,
1088
- type: "two-column",
1089
- data: {
1090
- title: "Current Situation",
1091
- body: analysis.scqa.situation,
1092
- bullets: analysis.sparkline.whatIs.slice(0, 3)
1093
- },
1094
- classes: ["slide-two-column"]
1722
+ defaults: this.kb.getSlideDefaults(type),
1723
+ scqaTitles: this.kb.getSCQATitles(type),
1724
+ sparklineTitles: this.kb.getSparklineTitles(type),
1725
+ rules: this.kb.getValidationRules(type),
1726
+ allowedTypes: this.kb.getAllowedSlideTypes(type),
1727
+ mode: this.kb.getModeForType(type),
1728
+ insightMarkers: this.kb.getInsightMarkers(),
1729
+ glanceTest: this.kb.getDuarteGlanceTest(),
1730
+ millersLaw: this.kb.getMillersLaw(),
1731
+ typography: this.kb.getTypographyForType(type),
1732
+ antiPatterns: this.kb.getAntiPatternsForType(type),
1733
+ requiredElements: this.kb.getRequiredElements(type)
1095
1734
  };
1096
1735
  }
1097
1736
  /**
1098
- * Create a problem/complication slide.
1737
+ * Create all slides from content analysis.
1738
+ * This is the main entry point - orchestrates the entire slide creation process.
1099
1739
  */
1100
- createProblemSlide(index, analysis, mode) {
1101
- if (mode === "keynote") {
1102
- return {
1103
- index,
1104
- type: "big-idea",
1105
- data: {
1106
- title: this.truncate(analysis.scqa.complication, 60),
1107
- keyMessage: "The challenge we face"
1108
- },
1109
- classes: ["slide-big-idea"]
1110
- };
1740
+ async createSlides(analysis) {
1741
+ const slides = [];
1742
+ const storyStructure = this.kb.getStoryStructure(this.presentationType);
1743
+ logger.step(`Using ${storyStructure.framework} story framework`);
1744
+ slides.push(this.createTitleSlide(0, analysis));
1745
+ const minSectionsForAgenda = Math.max(3, this.config.rules.bulletsPerSlide.max - 2);
1746
+ if (this.config.mode === "business" && analysis.sections.length >= minSectionsForAgenda) {
1747
+ slides.push(this.createAgendaSlide(slides.length, analysis));
1748
+ }
1749
+ if (storyStructure.framework === "scqa") {
1750
+ this.addSCQASlides(slides, analysis);
1751
+ } else if (storyStructure.framework === "sparkline") {
1752
+ this.addSparklineSlides(slides, analysis);
1753
+ }
1754
+ for (const section of analysis.sections) {
1755
+ const contentKey = this.normalizeKey(section.header);
1756
+ if (this.usedContent.has(contentKey)) continue;
1757
+ this.usedContent.add(contentKey);
1758
+ const pattern = this.classifier.classify(section);
1759
+ const slideType = this.kb.mapContentPatternToSlideType(pattern, this.config.allowedTypes);
1760
+ const slide = this.createSlideByType(slides.length, slideType, section, pattern);
1761
+ if (slide) {
1762
+ slides.push(slide);
1763
+ }
1764
+ }
1765
+ const minDataPointsForMetrics = 2;
1766
+ if (analysis.dataPoints.length >= minDataPointsForMetrics) {
1767
+ const hasMetricsSlide = slides.some(
1768
+ (s) => s.type === "metrics-grid" || s.type === "big-number"
1769
+ );
1770
+ if (!hasMetricsSlide) {
1771
+ slides.push(this.createMetricsGridSlide(slides.length, analysis.dataPoints));
1772
+ }
1773
+ }
1774
+ if (analysis.sparkline?.callToAdventure || analysis.scqa?.answer) {
1775
+ slides.push(this.createCTASlide(slides.length, analysis));
1776
+ }
1777
+ slides.push(this.createThankYouSlide(slides.length));
1778
+ const validatedSlides = this.validateAndFixSlides(slides);
1779
+ this.checkRequiredElements(validatedSlides);
1780
+ this.checkAntiPatterns(validatedSlides);
1781
+ logger.step(`Created ${validatedSlides.length} validated slides`);
1782
+ return validatedSlides;
1783
+ }
1784
+ // ===========================================================================
1785
+ // SLIDE TYPE ROUTER - Routes to appropriate creator based on KB-selected type
1786
+ // ===========================================================================
1787
+ createSlideByType(index, type, section, pattern) {
1788
+ const normalizedType = type.toLowerCase().replace(/_/g, "-");
1789
+ switch (normalizedType) {
1790
+ case "big-number":
1791
+ case "data-insight":
1792
+ return this.createBigNumberSlide(index, section, pattern);
1793
+ case "comparison":
1794
+ case "options-comparison":
1795
+ return this.createComparisonSlide(index, section);
1796
+ case "timeline":
1797
+ case "process-timeline":
1798
+ case "roadmap":
1799
+ return this.createTimelineSlide(index, section);
1800
+ case "process":
1801
+ return this.createProcessSlide(index, section);
1802
+ case "three-column":
1803
+ return this.createThreeColumnSlide(index, section);
1804
+ case "two-column":
1805
+ return this.createTwoColumnSlide(index, section);
1806
+ case "quote":
1807
+ case "testimonial":
1808
+ case "social-proof":
1809
+ return this.createQuoteSlide(index, section);
1810
+ case "metrics-grid":
1811
+ return this.createMetricsGridSlide(index, section.metrics);
1812
+ case "bullet-points":
1813
+ case "detailed-findings":
1814
+ return this.createBulletSlide(index, section);
1815
+ case "single-statement":
1816
+ case "big-idea":
1817
+ return this.createSingleStatementSlide(index, section);
1818
+ case "code-snippet":
1819
+ case "technical":
1820
+ return this.createCodeSlide(index, section);
1821
+ default:
1822
+ logger.warn(`Unknown slide type '${type}', using bullet-points`);
1823
+ return this.createBulletSlide(index, section);
1824
+ }
1825
+ }
1826
+ // ===========================================================================
1827
+ // STRUCTURAL SLIDES (title, agenda, thank you) - ALL from KB
1828
+ // ===========================================================================
1829
+ createTitleSlide(index, analysis) {
1830
+ const data = {
1831
+ title: this.truncateText(analysis.title, this.config.rules.wordsPerSlide.max)
1832
+ };
1833
+ if (analysis.keyMessages[0]) {
1834
+ data.subtitle = this.truncateText(
1835
+ analysis.keyMessages[0],
1836
+ this.config.defaults.subtitle.maxWords
1837
+ // FROM KB
1838
+ );
1111
1839
  }
1112
1840
  return {
1113
1841
  index,
1114
- type: "bullet-points",
1842
+ type: "title",
1843
+ data,
1844
+ classes: ["title-slide"]
1845
+ };
1846
+ }
1847
+ createAgendaSlide(index, analysis) {
1848
+ const agendaItems = analysis.sections.filter((s) => s.level <= 2).slice(0, this.config.rules.bulletsPerSlide.max).map((s) => this.cleanText(s.header));
1849
+ return {
1850
+ index,
1851
+ type: "agenda",
1115
1852
  data: {
1116
- title: "The Challenge",
1117
- body: analysis.scqa.complication,
1118
- bullets: this.extractBullets(analysis.scqa.complication)
1853
+ title: this.config.defaults.agenda.title,
1854
+ // FROM KB - not hardcoded 'Agenda'
1855
+ bullets: agendaItems
1119
1856
  },
1120
- classes: ["slide-bullet-points"]
1857
+ classes: ["agenda-slide"]
1121
1858
  };
1122
1859
  }
1123
- /**
1124
- * Create a key message slide.
1125
- */
1126
- createMessageSlide(index, message, mode) {
1127
- if (mode === "keynote") {
1128
- return {
1129
- index,
1130
- type: "single-statement",
1131
- data: {
1132
- title: this.truncate(message, 60),
1133
- keyMessage: message
1134
- },
1135
- classes: ["slide-single-statement"]
1136
- };
1137
- }
1860
+ createThankYouSlide(index) {
1138
1861
  return {
1139
1862
  index,
1140
- type: "bullet-points",
1863
+ type: "thank-you",
1141
1864
  data: {
1142
- title: this.extractActionTitle(message),
1143
- body: message,
1144
- bullets: this.extractBullets(message)
1865
+ title: this.config.defaults.thankYou.title,
1866
+ // FROM KB - not hardcoded 'Thank You'
1867
+ subtitle: this.config.defaults.thankYou.subtitle
1868
+ // FROM KB - not hardcoded 'Questions?'
1145
1869
  },
1146
- classes: ["slide-bullet-points"]
1870
+ classes: ["thank-you-slide"]
1147
1871
  };
1148
1872
  }
1149
- /**
1150
- * Create a STAR moment slide.
1151
- */
1152
- createStarMomentSlide(index, starMoment, mode) {
1153
- const statMatch = starMoment.match(/(\d+[%xX]|\$[\d,]+(?:\s*(?:million|billion))?)/);
1154
- if (statMatch && statMatch[1]) {
1155
- const stat = statMatch[1];
1156
- return {
1157
- index,
1158
- type: "big-number",
1873
+ // ===========================================================================
1874
+ // STORY FRAMEWORK SLIDES (SCQA, Sparkline) - ALL titles from KB
1875
+ // ===========================================================================
1876
+ addSCQASlides(slides, analysis) {
1877
+ const scqa = analysis.scqa;
1878
+ const titles = this.config.scqaTitles;
1879
+ if (scqa?.situation && !this.usedContent.has("scqa-situation")) {
1880
+ this.usedContent.add("scqa-situation");
1881
+ slides.push({
1882
+ index: slides.length,
1883
+ type: "single-statement",
1159
1884
  data: {
1160
- title: stat,
1161
- subtitle: this.removeStatistic(starMoment, stat),
1162
- keyMessage: starMoment
1885
+ title: titles.situation,
1886
+ // FROM KB - not hardcoded 'Current Situation'
1887
+ body: this.truncateText(scqa.situation, this.config.rules.wordsPerSlide.max)
1163
1888
  },
1164
- classes: ["slide-big-number"]
1165
- };
1889
+ classes: ["situation-slide"]
1890
+ });
1166
1891
  }
1167
- if (mode === "keynote") {
1168
- return {
1169
- index,
1170
- type: "big-idea",
1892
+ if (scqa?.complication && !this.usedContent.has("scqa-complication")) {
1893
+ this.usedContent.add("scqa-complication");
1894
+ slides.push({
1895
+ index: slides.length,
1896
+ type: "single-statement",
1171
1897
  data: {
1172
- title: this.truncate(starMoment, 80),
1173
- keyMessage: "A key insight"
1898
+ title: titles.complication,
1899
+ // FROM KB - not hardcoded 'The Challenge'
1900
+ body: this.truncateText(scqa.complication, this.config.rules.wordsPerSlide.max)
1174
1901
  },
1175
- classes: ["slide-big-idea"]
1176
- };
1902
+ classes: ["complication-slide"]
1903
+ });
1904
+ }
1905
+ if (scqa?.question && !this.usedContent.has("scqa-question")) {
1906
+ this.usedContent.add("scqa-question");
1907
+ slides.push({
1908
+ index: slides.length,
1909
+ type: "single-statement",
1910
+ data: {
1911
+ title: titles.question,
1912
+ // FROM KB - not hardcoded 'The Question'
1913
+ body: this.truncateText(scqa.question, this.config.rules.wordsPerSlide.max)
1914
+ },
1915
+ classes: ["question-slide"]
1916
+ });
1917
+ }
1918
+ }
1919
+ addSparklineSlides(slides, analysis) {
1920
+ const spark = analysis.sparkline;
1921
+ const titles = this.config.sparklineTitles;
1922
+ const whatIsFirst = spark?.whatIs?.[0];
1923
+ if (whatIsFirst && !this.usedContent.has("spark-what-is")) {
1924
+ this.usedContent.add("spark-what-is");
1925
+ slides.push({
1926
+ index: slides.length,
1927
+ type: "single-statement",
1928
+ data: {
1929
+ title: titles.whatIs,
1930
+ // FROM KB - not hardcoded 'Where We Are Today'
1931
+ body: this.truncateText(whatIsFirst, this.config.rules.wordsPerSlide.max)
1932
+ },
1933
+ classes: ["what-is-slide"]
1934
+ });
1935
+ }
1936
+ const whatCouldBeFirst = spark?.whatCouldBe?.[0];
1937
+ if (whatCouldBeFirst && !this.usedContent.has("spark-could-be")) {
1938
+ this.usedContent.add("spark-could-be");
1939
+ slides.push({
1940
+ index: slides.length,
1941
+ type: "single-statement",
1942
+ data: {
1943
+ title: titles.whatCouldBe,
1944
+ // FROM KB - not hardcoded 'What Could Be'
1945
+ body: this.truncateText(whatCouldBeFirst, this.config.rules.wordsPerSlide.max)
1946
+ },
1947
+ classes: ["what-could-be-slide"]
1948
+ });
1949
+ }
1950
+ }
1951
+ // ===========================================================================
1952
+ // CONTENT SLIDES - ALL values from KB
1953
+ // ===========================================================================
1954
+ createBigNumberSlide(index, section, pattern) {
1955
+ const fullContent = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
1956
+ const bigNumber = this.classifier.extractBigNumber(fullContent);
1957
+ const actualValue = bigNumber?.value || pattern.bigNumberValue;
1958
+ if (!actualValue) {
1959
+ logger.warn(`No number found for big-number slide, falling back to single-statement`);
1960
+ return this.createSingleStatementSlide(index, section);
1177
1961
  }
1178
1962
  return {
1179
1963
  index,
1180
- type: "quote",
1964
+ type: "big-number",
1181
1965
  data: {
1182
- quote: starMoment,
1183
- attribution: "Key Insight"
1966
+ title: this.createTitle(section.header, section),
1967
+ keyMessage: actualValue,
1968
+ body: bigNumber?.context || this.truncateText(
1969
+ section.content,
1970
+ this.config.defaults.context.maxWords
1971
+ // FROM KB - not hardcoded 30
1972
+ )
1184
1973
  },
1185
- classes: ["slide-quote"]
1974
+ classes: ["big-number-slide"]
1186
1975
  };
1187
1976
  }
1188
- /**
1189
- * Create a solution/answer slide.
1190
- */
1191
- createSolutionSlide(index, analysis, mode) {
1192
- if (mode === "keynote") {
1193
- return {
1194
- index,
1195
- type: "big-idea",
1196
- data: {
1197
- title: this.truncate(analysis.scqa.answer, 60),
1198
- keyMessage: "Our answer"
1199
- },
1200
- classes: ["slide-big-idea"]
1201
- };
1977
+ createComparisonSlide(index, section) {
1978
+ const comparison = this.classifier.extractComparison(section);
1979
+ const labels = this.config.defaults.comparison;
1980
+ const leftFallback = labels.optionLabels[0] ?? "Option A";
1981
+ const rightFallback = labels.optionLabels[1] ?? "Option B";
1982
+ const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
1983
+ const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
1984
+ return {
1985
+ index,
1986
+ type: "comparison",
1987
+ data: {
1988
+ title: this.createTitle(section.header, section),
1989
+ columns: [
1990
+ {
1991
+ title: labels.leftLabel,
1992
+ // FROM KB - not hardcoded 'Before'
1993
+ content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
1994
+ },
1995
+ {
1996
+ title: labels.rightLabel,
1997
+ // FROM KB - not hardcoded 'After'
1998
+ content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
1999
+ }
2000
+ ]
2001
+ },
2002
+ classes: ["comparison-slide"]
2003
+ };
2004
+ }
2005
+ createTimelineSlide(index, section) {
2006
+ const steps = this.classifier.extractSteps(section);
2007
+ const maxSteps = Math.min(
2008
+ steps.length,
2009
+ this.config.rules.bulletsPerSlide.max,
2010
+ this.config.millersLaw.maxItems
2011
+ // FROM KB - 7±2 rule
2012
+ );
2013
+ return {
2014
+ index,
2015
+ type: "timeline",
2016
+ data: {
2017
+ title: this.createTitle(section.header, section),
2018
+ steps: steps.slice(0, maxSteps).map((step) => ({
2019
+ label: step.label,
2020
+ description: this.truncateText(
2021
+ step.description,
2022
+ this.config.defaults.step.maxWords
2023
+ // FROM KB - not hardcoded 20
2024
+ )
2025
+ }))
2026
+ },
2027
+ classes: ["timeline-slide"]
2028
+ };
2029
+ }
2030
+ createProcessSlide(index, section) {
2031
+ const steps = this.classifier.extractSteps(section);
2032
+ const maxSteps = Math.min(steps.length, this.config.rules.bulletsPerSlide.max);
2033
+ return {
2034
+ index,
2035
+ type: "process",
2036
+ data: {
2037
+ title: this.createTitle(section.header, section),
2038
+ steps: steps.slice(0, maxSteps).map((step, i) => ({
2039
+ number: i + 1,
2040
+ title: step.label,
2041
+ description: this.truncateText(
2042
+ step.description,
2043
+ this.config.defaults.step.maxWords
2044
+ // FROM KB - not hardcoded 15
2045
+ )
2046
+ }))
2047
+ },
2048
+ classes: ["process-slide"]
2049
+ };
2050
+ }
2051
+ createThreeColumnSlide(index, section) {
2052
+ const columns = section.bullets.slice(0, 3);
2053
+ while (columns.length < 3) {
2054
+ columns.push("");
1202
2055
  }
2056
+ const labelTemplate = this.config.defaults.column.labelTemplate;
2057
+ return {
2058
+ index,
2059
+ type: "three-column",
2060
+ data: {
2061
+ title: this.createTitle(section.header, section),
2062
+ columns: columns.map((content, i) => ({
2063
+ title: labelTemplate.replace("{n}", String(i + 1)),
2064
+ // FROM KB - not hardcoded 'Point ${i+1}'
2065
+ content: this.truncateText(
2066
+ content,
2067
+ this.config.defaults.columnContent.maxWords
2068
+ // FROM KB - not hardcoded 25
2069
+ )
2070
+ }))
2071
+ },
2072
+ classes: ["three-column-slide"]
2073
+ };
2074
+ }
2075
+ createTwoColumnSlide(index, section) {
2076
+ const midpoint = Math.ceil(section.bullets.length / 2);
2077
+ const leftBullets = section.bullets.slice(0, midpoint);
2078
+ const rightBullets = section.bullets.slice(midpoint);
2079
+ const wordsPerBullet = Math.floor(
2080
+ this.config.rules.wordsPerSlide.max / this.config.rules.bulletsPerSlide.max
2081
+ );
1203
2082
  return {
1204
2083
  index,
1205
2084
  type: "two-column",
1206
2085
  data: {
1207
- title: "The Solution",
1208
- body: analysis.scqa.answer,
1209
- bullets: analysis.sparkline.whatCouldBe.slice(0, 4)
2086
+ title: this.createTitle(section.header, section),
2087
+ leftColumn: {
2088
+ bullets: leftBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
2089
+ },
2090
+ rightColumn: {
2091
+ bullets: rightBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
2092
+ }
1210
2093
  },
1211
- classes: ["slide-two-column"]
2094
+ classes: ["two-column-slide"]
1212
2095
  };
1213
2096
  }
1214
- /**
1215
- * Create a call-to-action slide.
1216
- */
1217
- createCTASlide(index, analysis, mode) {
2097
+ createQuoteSlide(index, section) {
2098
+ const quoteInfo = this.classifier.isQuoteContent(section);
2099
+ const quoteText = section.content.replace(/^[""]|[""]$/g, "").trim();
2100
+ const quoteWordLimit = Math.min(
2101
+ this.config.glanceTest.wordLimit * 2,
2102
+ // FROM KB - not hardcoded
2103
+ this.config.rules.wordsPerSlide.max
2104
+ );
2105
+ const data = {
2106
+ title: this.createTitle(section.header, section),
2107
+ quote: this.truncateText(quoteText, quoteWordLimit)
2108
+ };
2109
+ if (quoteInfo.attribution) {
2110
+ data.attribution = quoteInfo.attribution;
2111
+ }
1218
2112
  return {
1219
2113
  index,
1220
- type: "cta",
2114
+ type: "quote",
2115
+ data,
2116
+ classes: ["quote-slide"]
2117
+ };
2118
+ }
2119
+ createMetricsGridSlide(index, metrics) {
2120
+ const maxMetrics = this.config.defaults.metricsGrid.maxMetrics;
2121
+ const cleanedMetrics = metrics.slice(0, maxMetrics).map((m) => ({
2122
+ value: this.cleanText(m.value),
2123
+ label: this.cleanMetricLabel(m.label)
2124
+ }));
2125
+ return {
2126
+ index,
2127
+ type: "metrics-grid",
1221
2128
  data: {
1222
- title: mode === "keynote" ? "Take Action" : "Next Steps",
1223
- body: analysis.sparkline.callToAdventure,
1224
- keyMessage: "What we need from you"
2129
+ title: this.config.defaults.metricsGrid.title,
2130
+ // FROM KB - not hardcoded 'Key Metrics'
2131
+ metrics: cleanedMetrics
1225
2132
  },
1226
- classes: ["slide-cta"]
2133
+ classes: ["metrics-grid-slide"]
1227
2134
  };
1228
2135
  }
1229
- /**
1230
- * Create a thank you slide.
1231
- */
1232
- createThankYouSlide(index) {
2136
+ createBulletSlide(index, section) {
2137
+ const maxBullets = this.config.rules.bulletsPerSlide.max;
2138
+ const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
2139
+ const cleanedBullets = section.bullets.slice(0, maxBullets).map((b) => this.truncateText(this.cleanText(b), wordsPerBullet)).filter((b) => b.length > 0);
2140
+ if (cleanedBullets.length === 0 && section.content) {
2141
+ return this.createSingleStatementSlide(index, section);
2142
+ }
1233
2143
  return {
1234
2144
  index,
1235
- type: "thank-you",
2145
+ type: "bullet-points",
1236
2146
  data: {
1237
- title: "Thank You",
1238
- subtitle: "Questions?"
2147
+ title: this.createTitle(section.header, section),
2148
+ bullets: cleanedBullets
1239
2149
  },
1240
- classes: ["slide-thank-you"]
2150
+ classes: ["bullet-points-slide"]
1241
2151
  };
1242
2152
  }
1243
- /**
1244
- * Initialize slide templates with constraints.
1245
- */
1246
- initializeTemplates() {
1247
- const templates = /* @__PURE__ */ new Map();
1248
- templates.set("title", {
1249
- type: "title",
1250
- requiredFields: ["title"],
1251
- optionalFields: ["subtitle", "author", "date"],
1252
- keynoteSuitable: true,
1253
- businessSuitable: true,
1254
- maxWords: 15
1255
- });
1256
- templates.set("big-idea", {
1257
- type: "big-idea",
1258
- requiredFields: ["title"],
1259
- optionalFields: ["keyMessage"],
1260
- keynoteSuitable: true,
1261
- businessSuitable: false,
1262
- maxWords: 10
1263
- });
1264
- templates.set("single-statement", {
2153
+ createSingleStatementSlide(index, section) {
2154
+ return {
2155
+ index,
1265
2156
  type: "single-statement",
1266
- requiredFields: ["title"],
1267
- optionalFields: ["keyMessage"],
1268
- keynoteSuitable: true,
1269
- businessSuitable: false,
1270
- maxWords: 15
1271
- });
1272
- templates.set("big-number", {
1273
- type: "big-number",
1274
- requiredFields: ["title"],
1275
- optionalFields: ["subtitle", "source"],
1276
- keynoteSuitable: true,
1277
- businessSuitable: true,
1278
- maxWords: 10
1279
- });
1280
- templates.set("quote", {
1281
- type: "quote",
1282
- requiredFields: ["quote"],
1283
- optionalFields: ["attribution", "source"],
1284
- keynoteSuitable: true,
1285
- businessSuitable: true,
1286
- maxWords: 30
1287
- });
1288
- templates.set("bullet-points", {
1289
- type: "bullet-points",
1290
- requiredFields: ["title", "bullets"],
1291
- optionalFields: ["body"],
1292
- keynoteSuitable: false,
1293
- businessSuitable: true,
1294
- maxWords: 80
1295
- });
1296
- templates.set("two-column", {
2157
+ data: {
2158
+ title: this.createTitle(section.header, section),
2159
+ body: this.truncateText(
2160
+ section.content || section.bullets[0] || "",
2161
+ this.config.rules.wordsPerSlide.max
2162
+ // FROM KB
2163
+ )
2164
+ },
2165
+ classes: ["single-statement-slide"]
2166
+ };
2167
+ }
2168
+ createCodeSlide(index, section) {
2169
+ const codeMatch = section.content.match(/```[\s\S]*?```|`[^`]+`/);
2170
+ const code = codeMatch ? codeMatch[0].replace(/```/g, "").trim() : section.content;
2171
+ return {
2172
+ index,
1297
2173
  type: "two-column",
1298
- requiredFields: ["title"],
1299
- optionalFields: ["body", "bullets", "images"],
1300
- keynoteSuitable: false,
1301
- businessSuitable: true,
1302
- maxWords: 100
1303
- });
1304
- templates.set("agenda", {
1305
- type: "agenda",
1306
- requiredFields: ["title", "bullets"],
1307
- optionalFields: [],
1308
- keynoteSuitable: false,
1309
- businessSuitable: true,
1310
- maxWords: 50
1311
- });
1312
- templates.set("cta", {
2174
+ // Code slides use two-column layout
2175
+ data: {
2176
+ title: this.createTitle(section.header, section),
2177
+ leftColumn: {
2178
+ body: this.config.defaults.code.label
2179
+ // FROM KB - not hardcoded 'Code Example'
2180
+ },
2181
+ rightColumn: {
2182
+ body: code.slice(0, this.config.defaults.code.maxChars)
2183
+ // FROM KB - not hardcoded 500
2184
+ }
2185
+ },
2186
+ classes: ["code-slide"]
2187
+ };
2188
+ }
2189
+ createCTASlide(index, analysis) {
2190
+ const ctaDefaults = this.config.defaults.cta;
2191
+ const ctaText = analysis.sparkline?.callToAdventure || analysis.scqa?.answer || ctaDefaults.fallback;
2192
+ return {
2193
+ index,
1313
2194
  type: "cta",
1314
- requiredFields: ["title"],
1315
- optionalFields: ["body", "keyMessage"],
1316
- keynoteSuitable: true,
1317
- businessSuitable: true,
1318
- maxWords: 30
1319
- });
1320
- templates.set("thank-you", {
1321
- type: "thank-you",
1322
- requiredFields: ["title"],
1323
- optionalFields: ["subtitle"],
1324
- keynoteSuitable: true,
1325
- businessSuitable: true,
1326
- maxWords: 10
1327
- });
1328
- return templates;
2195
+ data: {
2196
+ title: ctaDefaults.title,
2197
+ // FROM KB - not hardcoded 'Next Steps'
2198
+ body: this.truncateText(ctaText, this.config.rules.wordsPerSlide.max),
2199
+ keyMessage: ctaDefaults.message
2200
+ // FROM KB - not hardcoded 'Ready to Begin?'
2201
+ },
2202
+ classes: ["cta-slide"]
2203
+ };
2204
+ }
2205
+ // ===========================================================================
2206
+ // VALIDATION - Ensure all slides comply with KB rules AND FIX ISSUES
2207
+ // ===========================================================================
2208
+ validateAndFixSlides(slides) {
2209
+ const validatedSlides = [];
2210
+ for (const slide of slides) {
2211
+ const wordCount = this.countWords(slide);
2212
+ const bulletCount = slide.data.bullets?.length || 0;
2213
+ const validation = this.kb.validateSlideAgainstKB(
2214
+ {
2215
+ type: slide.type,
2216
+ wordCount,
2217
+ bulletCount,
2218
+ hasActionTitle: this.isActionTitle(slide.data.title),
2219
+ hasSource: !!slide.data.source
2220
+ },
2221
+ this.presentationType
2222
+ );
2223
+ if (validation.fixes.wordCount) {
2224
+ if (slide.data.body) {
2225
+ slide.data.body = this.truncateText(slide.data.body, validation.fixes.wordCount);
2226
+ }
2227
+ if (slide.data.bullets) {
2228
+ const wordsPerBullet = Math.floor(validation.fixes.wordCount / slide.data.bullets.length);
2229
+ slide.data.bullets = slide.data.bullets.map((b) => this.truncateText(b, wordsPerBullet));
2230
+ }
2231
+ }
2232
+ if (validation.fixes.bulletCount && slide.data.bullets) {
2233
+ slide.data.bullets = slide.data.bullets.slice(0, validation.fixes.bulletCount);
2234
+ }
2235
+ if (validation.violations.length > 0) {
2236
+ logger.warn(`Slide ${slide.index} (${slide.type}): Fixed ${Object.keys(validation.fixes).length} issues, ${validation.violations.length} remaining`);
2237
+ }
2238
+ if (validation.warnings.length > 0) {
2239
+ slide.notes = (slide.notes || "") + "\nWarnings: " + validation.warnings.join(", ");
2240
+ }
2241
+ validatedSlides.push(slide);
2242
+ }
2243
+ return validatedSlides;
1329
2244
  }
1330
- // === Helper Methods ===
1331
2245
  /**
1332
- * Clean text by removing all markdown and content markers.
1333
- * CRITICAL: Must strip all formatting to prevent garbage in slides
2246
+ * Check that required elements exist in the deck (from KB)
1334
2247
  */
1335
- cleanText(text) {
1336
- if (!text) return "";
1337
- return text.replace(/^#+\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\[HEADER\]\s*/g, "").replace(/\[BULLET\]\s*/g, "").replace(/\[NUMBERED\]\s*/g, "").replace(/\[EMPHASIS\]/g, "").replace(/\[\/EMPHASIS\]/g, "").replace(/\[CODE BLOCK\]/g, "").replace(/\[IMAGE\]/g, "").replace(/\n{2,}/g, " ").replace(/\s+/g, " ").trim();
2248
+ checkRequiredElements(slides) {
2249
+ const required = this.config.requiredElements;
2250
+ if (required.length === 0) return;
2251
+ const slideTypes = slides.map((s) => s.type);
2252
+ const missingElements = [];
2253
+ for (const element of required) {
2254
+ const lowerElement = element.toLowerCase();
2255
+ if (lowerElement.includes("hook") || lowerElement.includes("opening")) {
2256
+ const titleSlide = slides.find((s) => s.type === "title");
2257
+ if (!titleSlide || !titleSlide.data.subtitle) {
2258
+ missingElements.push(element);
2259
+ }
2260
+ }
2261
+ if (lowerElement.includes("call to action") || lowerElement.includes("cta")) {
2262
+ if (!slideTypes.includes("cta")) {
2263
+ missingElements.push(element);
2264
+ }
2265
+ }
2266
+ if (lowerElement.includes("star moment") || lowerElement.includes("memorable")) {
2267
+ const hasStarMoment = slideTypes.some(
2268
+ (t) => t === "big-number" || t === "quote" || t === "single-statement"
2269
+ );
2270
+ if (!hasStarMoment) {
2271
+ missingElements.push(element);
2272
+ }
2273
+ }
2274
+ }
2275
+ if (missingElements.length > 0) {
2276
+ logger.warn(`Missing required elements: ${missingElements.join(", ")}`);
2277
+ }
1338
2278
  }
1339
2279
  /**
1340
- * Truncate text to max length at sentence boundary when possible.
1341
- * CRITICAL: Never cut mid-number (99.5% should not become 99.)
2280
+ * Check for anti-patterns from KB
1342
2281
  */
1343
- truncate(text, maxLength) {
1344
- const cleanedText = this.cleanText(text);
1345
- if (!cleanedText || cleanedText.length <= maxLength) {
1346
- return cleanedText;
1347
- }
1348
- const sentences = cleanedText.match(/[^.!?]+[.!?]/g);
1349
- if (sentences) {
1350
- let result = "";
1351
- for (const sentence of sentences) {
1352
- if ((result + sentence).length <= maxLength) {
1353
- result += sentence;
1354
- } else {
1355
- break;
2282
+ checkAntiPatterns(slides) {
2283
+ const antiPatterns = this.config.antiPatterns;
2284
+ if (antiPatterns.length === 0) return;
2285
+ const violations = [];
2286
+ for (const pattern of antiPatterns) {
2287
+ const lowerPattern = pattern.toLowerCase();
2288
+ if (lowerPattern.includes("bullet") && this.config.mode === "keynote") {
2289
+ const bulletSlides = slides.filter(
2290
+ (s) => s.type === "bullet-points" || s.data.bullets && s.data.bullets.length > 3
2291
+ );
2292
+ if (bulletSlides.length > 0) {
2293
+ violations.push(`${pattern} (${bulletSlides.length} slides)`);
1356
2294
  }
1357
2295
  }
1358
- if (result.length > maxLength * 0.5) {
1359
- return result.trim();
2296
+ if (lowerPattern.includes("text") || lowerPattern.includes("overload")) {
2297
+ const overloadedSlides = slides.filter((s) => {
2298
+ const wordCount = this.countWords(s);
2299
+ return wordCount > this.config.glanceTest.wordLimit;
2300
+ });
2301
+ if (overloadedSlides.length > 0) {
2302
+ violations.push(`${pattern} (${overloadedSlides.length} slides exceed glance test)`);
2303
+ }
1360
2304
  }
1361
2305
  }
1362
- const truncated = cleanedText.slice(0, maxLength);
1363
- let lastSpace = truncated.lastIndexOf(" ");
1364
- const afterCut = cleanedText.slice(lastSpace + 1, maxLength + 10);
1365
- if (/^[\d.,%$]+/.test(afterCut)) {
1366
- lastSpace = truncated.slice(0, lastSpace).lastIndexOf(" ");
2306
+ if (violations.length > 0) {
2307
+ logger.warn(`Anti-pattern violations: ${violations.join(", ")}`);
1367
2308
  }
1368
- if (lastSpace > maxLength * 0.5) {
1369
- return truncated.slice(0, lastSpace) + "...";
1370
- }
1371
- return truncated + "...";
1372
2309
  }
2310
+ // ===========================================================================
2311
+ // HELPER METHODS - All use KB configuration
2312
+ // ===========================================================================
1373
2313
  /**
1374
- * Extract an action title from a message.
2314
+ * Create a title for a slide - uses action titles for business mode per KB
1375
2315
  */
1376
- extractActionTitle(message) {
1377
- const cleanedMessage = this.cleanText(message);
1378
- const firstSentence = cleanedMessage.split(/[.!?]/)[0];
1379
- if (firstSentence && firstSentence.length <= 50) {
1380
- return firstSentence;
2316
+ createTitle(header, section) {
2317
+ const cleanHeader = this.cleanText(header);
2318
+ if (this.usedTitles.has(cleanHeader.toLowerCase())) {
2319
+ return `${cleanHeader} (continued)`;
2320
+ }
2321
+ this.usedTitles.add(cleanHeader.toLowerCase());
2322
+ if (this.config.rules.actionTitlesRequired) {
2323
+ return this.createActionTitle(cleanHeader, section);
1381
2324
  }
1382
- const words = cleanedMessage.split(/\s+/).slice(0, 6);
1383
- return words.join(" ");
2325
+ return cleanHeader;
1384
2326
  }
1385
2327
  /**
1386
- * Extract bullet points from text.
2328
+ * Create an action title (Minto/McKinsey style)
2329
+ * Title should communicate the conclusion, not the topic
1387
2330
  */
1388
- extractBullets(text) {
1389
- if (!text) return [];
1390
- const bulletMatches = text.match(/\[BULLET\]\s*(.+)/g);
1391
- if (bulletMatches && bulletMatches.length > 0) {
1392
- return bulletMatches.map((b) => this.cleanText(b)).slice(0, 5);
2331
+ createActionTitle(header, section) {
2332
+ const titleWordLimit = this.kb.getWordLimitForElement(this.presentationType, "title");
2333
+ const firstBullet = section.bullets[0];
2334
+ if (firstBullet && this.isInsightful(firstBullet)) {
2335
+ return this.truncateText(this.cleanText(firstBullet), titleWordLimit);
2336
+ }
2337
+ if (section.content && this.isInsightful(section.content)) {
2338
+ const sentences = section.content.split(/[.!?]/);
2339
+ const firstSentence = sentences[0];
2340
+ if (firstSentence) {
2341
+ return this.truncateText(this.cleanText(firstSentence), titleWordLimit);
2342
+ }
1393
2343
  }
1394
- const cleanedText = this.cleanText(text);
1395
- const sentences = cleanedText.split(/[.!?]+/).filter((s) => s.trim().length > 10);
1396
- return sentences.slice(0, 5).map((s) => s.trim());
2344
+ return header;
1397
2345
  }
1398
2346
  /**
1399
- * Remove a statistic from text and clean thoroughly.
2347
+ * Check if text contains an insight (not just a topic) - uses KB markers
1400
2348
  */
1401
- removeStatistic(text, stat) {
1402
- const cleaned = this.cleanText(text).replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1403
- const firstSentence = cleaned.match(/^[^.!?]+[.!?]?/);
1404
- return firstSentence ? firstSentence[0].slice(0, 80) : cleaned.slice(0, 80);
2349
+ isInsightful(text) {
2350
+ const lowerText = text.toLowerCase();
2351
+ return this.config.insightMarkers.some(
2352
+ (marker) => (
2353
+ // FROM KB - not hardcoded array
2354
+ lowerText.includes(marker.toLowerCase())
2355
+ )
2356
+ );
1405
2357
  }
1406
- };
1407
-
1408
- // src/core/TemplateEngine.ts
1409
- var import_handlebars = __toESM(require("handlebars"));
1410
- var TemplateEngine = class {
1411
- handlebars;
1412
- templates;
1413
- partials;
1414
- constructor() {
1415
- this.handlebars = import_handlebars.default.create();
1416
- this.templates = /* @__PURE__ */ new Map();
1417
- this.partials = /* @__PURE__ */ new Map();
1418
- this.registerHelpers();
2358
+ /**
2359
+ * Check if a title is an action title
2360
+ */
2361
+ isActionTitle(title) {
2362
+ if (!title) return false;
2363
+ return this.isInsightful(title);
2364
+ }
2365
+ /**
2366
+ * Count words in a slide
2367
+ */
2368
+ countWords(slide) {
2369
+ let text = "";
2370
+ const data = slide.data;
2371
+ if (data.title) text += data.title + " ";
2372
+ if (data.subtitle) text += data.subtitle + " ";
2373
+ if (data.body) text += data.body + " ";
2374
+ if (data.keyMessage) text += data.keyMessage + " ";
2375
+ if (data.bullets) text += data.bullets.join(" ") + " ";
2376
+ if (data.quote) text += data.quote + " ";
2377
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
2378
+ }
2379
+ /**
2380
+ * Truncate text to word limit at sentence boundaries
2381
+ */
2382
+ truncateText(text, maxWords) {
2383
+ const cleaned = this.cleanText(text);
2384
+ const words = cleaned.split(/\s+/);
2385
+ if (words.length <= maxWords) {
2386
+ return cleaned;
2387
+ }
2388
+ const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
2389
+ let result = "";
2390
+ for (const sentence of sentences) {
2391
+ const testResult = result + sentence;
2392
+ if (testResult.split(/\s+/).length <= maxWords) {
2393
+ result = testResult;
2394
+ } else {
2395
+ break;
2396
+ }
2397
+ }
2398
+ if (result.trim().length > 0) {
2399
+ return result.trim();
2400
+ }
2401
+ return words.slice(0, maxWords).join(" ") + "...";
2402
+ }
2403
+ /**
2404
+ * Clean text by removing content markers and normalizing
2405
+ */
2406
+ cleanText(text) {
2407
+ if (!text) return "";
2408
+ return text.replace(/\[HEADER\]\s*/g, "").replace(/\[BULLET\]\s*/g, "").replace(/\[NUMBERED\]\s*/g, "").replace(/\[EMPHASIS\]/g, "").replace(/\[\/EMPHASIS\]/g, "").replace(/\[CODE BLOCK\]/g, "").replace(/\[IMAGE\]/g, "").replace(/\*\*/g, "").replace(/\*/g, "").replace(/#{1,6}\s*/g, "").replace(/\s+/g, " ").trim();
2409
+ }
2410
+ /**
2411
+ * Clean metric labels (strip table syntax)
2412
+ */
2413
+ cleanMetricLabel(label) {
2414
+ if (!label) return "";
2415
+ return label.replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\s{2,}/g, " ").replace(/^\s*\d+\.\s*/, "").trim();
2416
+ }
2417
+ /**
2418
+ * Normalize a key for deduplication
2419
+ */
2420
+ normalizeKey(text) {
2421
+ return text.toLowerCase().replace(/[^a-z0-9]/g, "");
2422
+ }
2423
+ };
2424
+ function createSlideFactory(kb, type) {
2425
+ return new SlideFactory(kb, type);
2426
+ }
2427
+
2428
+ // src/core/TemplateEngine.ts
2429
+ var import_handlebars = __toESM(require("handlebars"));
2430
+ var TemplateEngine = class {
2431
+ handlebars;
2432
+ templates;
2433
+ partials;
2434
+ constructor() {
2435
+ this.handlebars = import_handlebars.default.create();
2436
+ this.templates = /* @__PURE__ */ new Map();
2437
+ this.partials = /* @__PURE__ */ new Map();
2438
+ this.registerHelpers();
1419
2439
  this.registerPartials();
1420
2440
  this.compileTemplates();
1421
2441
  }
@@ -1629,9 +2649,12 @@ var TemplateEngine = class {
1629
2649
  this.templates.set("big-number", this.handlebars.compile(`
1630
2650
  <section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
1631
2651
  <div class="slide-content big-number-content">
1632
- <div class="number animate-zoomIn">{{title}}</div>
1633
- {{#if subtitle}}
1634
- <p class="number-context animate-fadeIn delay-300">{{subtitle}}</p>
2652
+ {{#if title}}
2653
+ <h2 class="title animate-fadeIn">{{title}}</h2>
2654
+ {{/if}}
2655
+ <div class="number animate-zoomIn">{{keyMessage}}</div>
2656
+ {{#if body}}
2657
+ <p class="number-context animate-fadeIn delay-300">{{body}}</p>
1635
2658
  {{/if}}
1636
2659
  {{> source}}
1637
2660
  </div>
@@ -2892,21 +3915,966 @@ var QAEngine = class {
2892
3915
  });
2893
3916
  return issues;
2894
3917
  }
2895
- // ===========================================================================
2896
- // BROWSER MANAGEMENT
2897
- // ===========================================================================
2898
- async initBrowser() {
2899
- if (!this.browser) {
2900
- this.browser = await import_playwright.chromium.launch({ headless: true });
3918
+ // ===========================================================================
3919
+ // BROWSER MANAGEMENT
3920
+ // ===========================================================================
3921
+ async initBrowser() {
3922
+ if (!this.browser) {
3923
+ this.browser = await import_playwright.chromium.launch({ headless: true });
3924
+ }
3925
+ }
3926
+ async closeBrowser() {
3927
+ if (this.browser) {
3928
+ await this.browser.close();
3929
+ this.browser = null;
3930
+ }
3931
+ }
3932
+ };
3933
+
3934
+ // src/qa/SevenDimensionScorer.ts
3935
+ var DIMENSION_WEIGHTS = {
3936
+ layout: 0.15,
3937
+ contrast: 0.15,
3938
+ graphics: 0.1,
3939
+ content: 0.2,
3940
+ clarity: 0.15,
3941
+ effectiveness: 0.15,
3942
+ consistency: 0.1
3943
+ };
3944
+ var SevenDimensionScorer = class {
3945
+ kb;
3946
+ mode;
3947
+ presentationType;
3948
+ constructor(mode, presentationType) {
3949
+ this.mode = mode;
3950
+ this.presentationType = presentationType;
3951
+ }
3952
+ /**
3953
+ * Score a presentation across all 7 dimensions.
3954
+ */
3955
+ async score(slides, html, threshold = 95) {
3956
+ this.kb = await getKnowledgeGateway();
3957
+ const layout = await this.scoreLayout(slides, html);
3958
+ const contrast = await this.scoreContrast(html);
3959
+ const graphics = await this.scoreGraphics(slides);
3960
+ const content = await this.scoreContent(slides);
3961
+ const clarity = await this.scoreClarity(slides);
3962
+ const effectiveness = await this.scoreEffectiveness(slides);
3963
+ const consistency = await this.scoreConsistency(slides, html);
3964
+ const overallScore = Math.round(
3965
+ 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
3966
+ );
3967
+ const issues = [
3968
+ ...layout.issues,
3969
+ ...contrast.issues,
3970
+ ...graphics.issues,
3971
+ ...content.issues,
3972
+ ...clarity.issues,
3973
+ ...effectiveness.issues,
3974
+ ...consistency.issues
3975
+ ];
3976
+ return {
3977
+ overallScore,
3978
+ dimensions: {
3979
+ layout,
3980
+ contrast,
3981
+ graphics,
3982
+ content,
3983
+ clarity,
3984
+ effectiveness,
3985
+ consistency
3986
+ },
3987
+ issues,
3988
+ passed: overallScore >= threshold,
3989
+ threshold
3990
+ };
3991
+ }
3992
+ /**
3993
+ * Score layout dimension (whitespace, visual balance, structure)
3994
+ */
3995
+ async scoreLayout(slides, html) {
3996
+ const issues = [];
3997
+ let totalScore = 0;
3998
+ let checks = 0;
3999
+ const minWhitespace = this.mode === "keynote" ? 0.45 : 0.35;
4000
+ const maxWhitespace = 0.6;
4001
+ const slideSections = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
4002
+ for (let i = 0; i < slideSections.length; i++) {
4003
+ const section = slideSections[i];
4004
+ if (!section) continue;
4005
+ const textContent = section.replace(/<[^>]+>/g, "").trim();
4006
+ const totalArea = 1920 * 1080;
4007
+ const estimatedTextArea = textContent.length * 100;
4008
+ const whitespaceRatio = 1 - Math.min(estimatedTextArea / totalArea, 1);
4009
+ if (whitespaceRatio < minWhitespace) {
4010
+ issues.push({
4011
+ slideIndex: i,
4012
+ dimension: "layout",
4013
+ severity: "error",
4014
+ message: `Slide ${i + 1}: Insufficient whitespace (${Math.round(whitespaceRatio * 100)}% < ${Math.round(minWhitespace * 100)}%)`,
4015
+ currentValue: whitespaceRatio,
4016
+ expectedValue: minWhitespace,
4017
+ autoFixable: true,
4018
+ fixSuggestion: "Reduce content or increase margins"
4019
+ });
4020
+ totalScore += 50;
4021
+ } else if (whitespaceRatio > maxWhitespace) {
4022
+ issues.push({
4023
+ slideIndex: i,
4024
+ dimension: "layout",
4025
+ severity: "warning",
4026
+ message: `Slide ${i + 1}: Too much whitespace (${Math.round(whitespaceRatio * 100)}% > ${Math.round(maxWhitespace * 100)}%)`,
4027
+ currentValue: whitespaceRatio,
4028
+ expectedValue: maxWhitespace,
4029
+ autoFixable: false,
4030
+ fixSuggestion: "Add more content or reduce margins"
4031
+ });
4032
+ totalScore += 80;
4033
+ } else {
4034
+ totalScore += 100;
4035
+ }
4036
+ checks++;
4037
+ }
4038
+ for (let i = 0; i < slides.length; i++) {
4039
+ const slide = slides[i];
4040
+ if (!slide) continue;
4041
+ if (!["thank-you", "section-divider"].includes(slide.type)) {
4042
+ if (!slide.data.title || slide.data.title.trim().length === 0) {
4043
+ issues.push({
4044
+ slideIndex: i,
4045
+ dimension: "layout",
4046
+ severity: "warning",
4047
+ message: `Slide ${i + 1}: Missing title`,
4048
+ autoFixable: false,
4049
+ fixSuggestion: "Add a clear slide title"
4050
+ });
4051
+ totalScore += 70;
4052
+ } else {
4053
+ totalScore += 100;
4054
+ }
4055
+ checks++;
4056
+ }
4057
+ }
4058
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4059
+ return {
4060
+ name: "Layout",
4061
+ score,
4062
+ weight: DIMENSION_WEIGHTS.layout,
4063
+ issues,
4064
+ details: {
4065
+ slidesAnalyzed: slides.length,
4066
+ whitespaceTarget: `${Math.round(minWhitespace * 100)}-${Math.round(maxWhitespace * 100)}%`
4067
+ }
4068
+ };
4069
+ }
4070
+ /**
4071
+ * Score contrast dimension (WCAG compliance, readability)
4072
+ */
4073
+ async scoreContrast(html) {
4074
+ const issues = [];
4075
+ let score = 100;
4076
+ const minContrastRatio = 4.5;
4077
+ const colorMatches = html.match(/color:\s*([^;]+);/gi) || [];
4078
+ const bgColorMatches = html.match(/background(-color)?:\s*([^;]+);/gi) || [];
4079
+ const hasGoodContrast = html.includes("color: #fff") || html.includes("color: white") || html.includes("color: #18181B") || html.includes("color: #F5F5F4");
4080
+ const hasDarkBackground = html.includes("background-color: #18181B") || html.includes("background: #18181B") || html.includes("background-color: rgb(24, 24, 27)");
4081
+ if (html.includes("color: gray") || html.includes("color: #999") || html.includes("color: #888")) {
4082
+ issues.push({
4083
+ slideIndex: -1,
4084
+ dimension: "contrast",
4085
+ severity: "error",
4086
+ message: "Low contrast text color detected (gray text)",
4087
+ currentValue: "gray",
4088
+ expectedValue: "High contrast color",
4089
+ autoFixable: true,
4090
+ fixSuggestion: "Use white (#fff) or dark (#18181B) text depending on background"
4091
+ });
4092
+ score -= 20;
4093
+ }
4094
+ if (!hasGoodContrast && !hasDarkBackground) {
4095
+ issues.push({
4096
+ slideIndex: -1,
4097
+ dimension: "contrast",
4098
+ severity: "warning",
4099
+ message: "Could not verify WCAG-compliant contrast ratios",
4100
+ autoFixable: false,
4101
+ fixSuggestion: "Ensure text has 4.5:1 contrast ratio against background"
4102
+ });
4103
+ score -= 10;
4104
+ }
4105
+ const hasSmallFont = html.match(/font-size:\s*(1[0-3]|[0-9])px/i) !== null;
4106
+ if (hasSmallFont) {
4107
+ issues.push({
4108
+ slideIndex: -1,
4109
+ dimension: "contrast",
4110
+ severity: "error",
4111
+ message: "Font size too small for presentation (< 14px)",
4112
+ currentValue: "Small font",
4113
+ expectedValue: "18px minimum for body text",
4114
+ autoFixable: true,
4115
+ fixSuggestion: "Increase font size to minimum 18px"
4116
+ });
4117
+ score -= 15;
4118
+ }
4119
+ return {
4120
+ name: "Contrast",
4121
+ score: Math.max(0, score),
4122
+ weight: DIMENSION_WEIGHTS.contrast,
4123
+ issues,
4124
+ details: {
4125
+ wcagLevel: "AA",
4126
+ minContrastRatio,
4127
+ colorDefinitions: colorMatches.length,
4128
+ backgroundDefinitions: bgColorMatches.length
4129
+ }
4130
+ };
4131
+ }
4132
+ /**
4133
+ * Score graphics dimension (images, placement, relevance)
4134
+ */
4135
+ async scoreGraphics(slides) {
4136
+ const issues = [];
4137
+ let score = 100;
4138
+ let slidesWithImages = 0;
4139
+ let totalImageSlides = 0;
4140
+ for (let i = 0; i < slides.length; i++) {
4141
+ const slide = slides[i];
4142
+ if (!slide) continue;
4143
+ const shouldHaveImage = ["hero", "image", "feature"].includes(slide.type);
4144
+ if (shouldHaveImage) {
4145
+ totalImageSlides++;
4146
+ if (slide.data.image || slide.data.backgroundImage) {
4147
+ slidesWithImages++;
4148
+ } else {
4149
+ issues.push({
4150
+ slideIndex: i,
4151
+ dimension: "graphics",
4152
+ severity: "warning",
4153
+ message: `Slide ${i + 1} (${slide.type}): Missing expected image`,
4154
+ autoFixable: false,
4155
+ fixSuggestion: "Add a relevant image or change slide type"
4156
+ });
4157
+ score -= 5;
4158
+ }
4159
+ }
4160
+ }
4161
+ return {
4162
+ name: "Graphics",
4163
+ score: Math.max(0, score),
4164
+ weight: DIMENSION_WEIGHTS.graphics,
4165
+ issues,
4166
+ details: {
4167
+ slidesWithImages,
4168
+ expectedImageSlides: totalImageSlides,
4169
+ imageRatio: totalImageSlides > 0 ? slidesWithImages / totalImageSlides : 1
4170
+ }
4171
+ };
4172
+ }
4173
+ /**
4174
+ * Score content dimension (word limits, bullet counts, structure)
4175
+ * This is critical - must use KB rules exactly.
4176
+ */
4177
+ async scoreContent(slides) {
4178
+ const issues = [];
4179
+ let totalScore = 0;
4180
+ let checks = 0;
4181
+ const wordLimits = this.kb.getWordLimits(this.mode);
4182
+ const bulletLimits = this.kb.getBulletLimits(this.mode);
4183
+ const defaultMaxWords = this.mode === "keynote" ? 25 : 80;
4184
+ const defaultMinWords = this.mode === "keynote" ? 3 : 15;
4185
+ const defaultMaxBullets = 5;
4186
+ for (let i = 0; i < slides.length; i++) {
4187
+ const slide = slides[i];
4188
+ if (!slide) continue;
4189
+ const wordCount = this.countWords(slide);
4190
+ const slideType = slide.type;
4191
+ const maxWords = wordLimits[slideType] ?? defaultMaxWords;
4192
+ const minWords = defaultMinWords;
4193
+ if (wordCount > maxWords) {
4194
+ const severity = wordCount > maxWords * 1.5 ? "error" : "warning";
4195
+ issues.push({
4196
+ slideIndex: i,
4197
+ dimension: "content",
4198
+ severity,
4199
+ message: `Slide ${i + 1}: Word count ${wordCount} exceeds limit of ${maxWords} for ${this.mode} mode`,
4200
+ currentValue: wordCount,
4201
+ expectedValue: maxWords,
4202
+ autoFixable: true,
4203
+ fixSuggestion: "Condense text to key points only"
4204
+ });
4205
+ totalScore += severity === "error" ? 40 : 70;
4206
+ } else if (wordCount < minWords && !["title", "section-divider", "thank-you"].includes(slide.type)) {
4207
+ issues.push({
4208
+ slideIndex: i,
4209
+ dimension: "content",
4210
+ severity: "warning",
4211
+ message: `Slide ${i + 1}: Word count ${wordCount} may be too sparse (min: ${minWords})`,
4212
+ currentValue: wordCount,
4213
+ expectedValue: minWords,
4214
+ autoFixable: false,
4215
+ fixSuggestion: "Add supporting content"
4216
+ });
4217
+ totalScore += 80;
4218
+ } else {
4219
+ totalScore += 100;
4220
+ }
4221
+ checks++;
4222
+ if (slide.data.bullets && Array.isArray(slide.data.bullets)) {
4223
+ const bulletCount = slide.data.bullets.length;
4224
+ const maxBullets = bulletLimits[slideType] ?? defaultMaxBullets;
4225
+ if (bulletCount > maxBullets) {
4226
+ issues.push({
4227
+ slideIndex: i,
4228
+ dimension: "content",
4229
+ severity: "error",
4230
+ message: `Slide ${i + 1}: ${bulletCount} bullets exceeds limit of ${maxBullets}`,
4231
+ currentValue: bulletCount,
4232
+ expectedValue: maxBullets,
4233
+ autoFixable: true,
4234
+ fixSuggestion: "Reduce to top 3-5 key points"
4235
+ });
4236
+ totalScore += 50;
4237
+ } else {
4238
+ totalScore += 100;
4239
+ }
4240
+ checks++;
4241
+ }
4242
+ }
4243
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4244
+ return {
4245
+ name: "Content",
4246
+ score,
4247
+ weight: DIMENSION_WEIGHTS.content,
4248
+ issues,
4249
+ details: {
4250
+ mode: this.mode,
4251
+ wordLimits,
4252
+ bulletLimits,
4253
+ totalSlides: slides.length
4254
+ }
4255
+ };
4256
+ }
4257
+ /**
4258
+ * Score clarity dimension (message focus, information density)
4259
+ */
4260
+ async scoreClarity(slides) {
4261
+ const issues = [];
4262
+ let totalScore = 0;
4263
+ let checks = 0;
4264
+ for (let i = 0; i < slides.length; i++) {
4265
+ const slide = slides[i];
4266
+ if (!slide) continue;
4267
+ if (slide.data.keyMessage) {
4268
+ const keyMessageWords = slide.data.keyMessage.split(/\s+/).length;
4269
+ const maxKeyMessageWords = this.mode === "keynote" ? 15 : 25;
4270
+ if (keyMessageWords > maxKeyMessageWords) {
4271
+ issues.push({
4272
+ slideIndex: i,
4273
+ dimension: "clarity",
4274
+ severity: "warning",
4275
+ message: `Slide ${i + 1}: Key message too long (${keyMessageWords} words > ${maxKeyMessageWords})`,
4276
+ currentValue: keyMessageWords,
4277
+ expectedValue: maxKeyMessageWords,
4278
+ autoFixable: true,
4279
+ fixSuggestion: "Shorten key message to one impactful sentence"
4280
+ });
4281
+ totalScore += 70;
4282
+ } else {
4283
+ totalScore += 100;
4284
+ }
4285
+ checks++;
4286
+ }
4287
+ if (slide.data.title) {
4288
+ const title = slide.data.title;
4289
+ const titleWords = title.split(/\s+/).length;
4290
+ if (titleWords > 10) {
4291
+ issues.push({
4292
+ slideIndex: i,
4293
+ dimension: "clarity",
4294
+ severity: "warning",
4295
+ message: `Slide ${i + 1}: Title too long (${titleWords} words)`,
4296
+ currentValue: titleWords,
4297
+ expectedValue: "2-8 words",
4298
+ autoFixable: true,
4299
+ fixSuggestion: "Use action-oriented, concise title"
4300
+ });
4301
+ totalScore += 75;
4302
+ } else {
4303
+ totalScore += 100;
4304
+ }
4305
+ checks++;
4306
+ }
4307
+ 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);
4308
+ const maxElements = this.mode === "keynote" ? 4 : 6;
4309
+ if (elementCount > maxElements) {
4310
+ issues.push({
4311
+ slideIndex: i,
4312
+ dimension: "clarity",
4313
+ severity: "warning",
4314
+ message: `Slide ${i + 1}: Too many elements (${elementCount} > ${maxElements})`,
4315
+ currentValue: elementCount,
4316
+ expectedValue: maxElements,
4317
+ autoFixable: true,
4318
+ fixSuggestion: "Split into multiple slides for clarity"
4319
+ });
4320
+ totalScore += 70;
4321
+ } else {
4322
+ totalScore += 100;
4323
+ }
4324
+ checks++;
4325
+ }
4326
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4327
+ return {
4328
+ name: "Clarity",
4329
+ score,
4330
+ weight: DIMENSION_WEIGHTS.clarity,
4331
+ issues,
4332
+ details: {
4333
+ slidesAnalyzed: slides.length,
4334
+ mode: this.mode
4335
+ }
4336
+ };
4337
+ }
4338
+ /**
4339
+ * Score effectiveness dimension (expert methodology compliance)
4340
+ */
4341
+ async scoreEffectiveness(slides) {
4342
+ const issues = [];
4343
+ let score = 100;
4344
+ if (slides.length >= 3) {
4345
+ const firstSlide = slides[0];
4346
+ const lastSlide = slides[slides.length - 1];
4347
+ if (firstSlide && !["title", "hero"].includes(firstSlide.type)) {
4348
+ issues.push({
4349
+ slideIndex: 0,
4350
+ dimension: "effectiveness",
4351
+ severity: "warning",
4352
+ message: "Presentation should start with a title or hero slide",
4353
+ currentValue: firstSlide.type,
4354
+ expectedValue: "title or hero",
4355
+ autoFixable: false,
4356
+ fixSuggestion: "Add a compelling opening slide"
4357
+ });
4358
+ score -= 10;
4359
+ }
4360
+ if (lastSlide && !["thank-you", "cta", "closing"].includes(lastSlide.type)) {
4361
+ issues.push({
4362
+ slideIndex: slides.length - 1,
4363
+ dimension: "effectiveness",
4364
+ severity: "warning",
4365
+ message: "Presentation should end with a closing or CTA slide",
4366
+ currentValue: lastSlide.type,
4367
+ expectedValue: "thank-you, cta, or closing",
4368
+ autoFixable: false,
4369
+ fixSuggestion: "Add a clear call-to-action or closing"
4370
+ });
4371
+ score -= 10;
4372
+ }
4373
+ }
4374
+ const keyMessages = slides.filter((s) => s.data.keyMessage);
4375
+ if (keyMessages.length > 0 && keyMessages.length !== 3 && keyMessages.length > 4) {
4376
+ issues.push({
4377
+ slideIndex: -1,
4378
+ dimension: "effectiveness",
4379
+ severity: "info",
4380
+ message: `Consider using Rule of Three: ${keyMessages.length} key messages found`,
4381
+ currentValue: keyMessages.length,
4382
+ expectedValue: 3,
4383
+ autoFixable: false,
4384
+ fixSuggestion: "Group messages into 3 main themes"
4385
+ });
4386
+ score -= 5;
4387
+ }
4388
+ const hasScqaElements = slides.some(
4389
+ (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")
4390
+ );
4391
+ if (!hasScqaElements && this.presentationType === "consulting_deck") {
4392
+ issues.push({
4393
+ slideIndex: -1,
4394
+ dimension: "effectiveness",
4395
+ severity: "warning",
4396
+ message: "Consulting deck should follow SCQA structure (Situation, Complication, Question, Answer)",
4397
+ autoFixable: false,
4398
+ fixSuggestion: "Organize content using Barbara Minto Pyramid Principle"
4399
+ });
4400
+ score -= 10;
4401
+ }
4402
+ const firstSlideType = slides[0]?.type;
4403
+ const lastSlideType = slides[slides.length - 1]?.type;
4404
+ return {
4405
+ name: "Effectiveness",
4406
+ score: Math.max(0, score),
4407
+ weight: DIMENSION_WEIGHTS.effectiveness,
4408
+ issues,
4409
+ details: {
4410
+ presentationType: this.presentationType,
4411
+ slideCount: slides.length,
4412
+ hasOpeningSlide: firstSlideType ? ["title", "hero"].includes(firstSlideType) : false,
4413
+ hasClosingSlide: lastSlideType ? ["thank-you", "cta", "closing"].includes(lastSlideType) : false
4414
+ }
4415
+ };
4416
+ }
4417
+ /**
4418
+ * Score consistency dimension (style uniformity, design coherence)
4419
+ */
4420
+ async scoreConsistency(slides, html) {
4421
+ const issues = [];
4422
+ let score = 100;
4423
+ const hasCssVariables = html.includes("var(--") || html.includes(":root");
4424
+ if (!hasCssVariables) {
4425
+ issues.push({
4426
+ slideIndex: -1,
4427
+ dimension: "consistency",
4428
+ severity: "warning",
4429
+ message: "Presentation lacks CSS variables for consistent styling",
4430
+ autoFixable: true,
4431
+ fixSuggestion: "Use CSS variables for colors, fonts, and spacing"
4432
+ });
4433
+ score -= 10;
4434
+ }
4435
+ const titlePatterns = /* @__PURE__ */ new Set();
4436
+ for (const slide of slides) {
4437
+ if (slide.data.title) {
4438
+ const isUpperCase = slide.data.title === slide.data.title.toUpperCase();
4439
+ const words = slide.data.title.split(" ").filter((w) => w.length > 0);
4440
+ const isTitleCase = words.length > 0 && words.every(
4441
+ (w) => w.length > 0 && w[0] === w[0]?.toUpperCase()
4442
+ );
4443
+ titlePatterns.add(isUpperCase ? "UPPER" : isTitleCase ? "Title" : "sentence");
4444
+ }
4445
+ }
4446
+ if (titlePatterns.size > 1) {
4447
+ issues.push({
4448
+ slideIndex: -1,
4449
+ dimension: "consistency",
4450
+ severity: "warning",
4451
+ message: `Inconsistent title casing: ${Array.from(titlePatterns).join(", ")}`,
4452
+ autoFixable: true,
4453
+ fixSuggestion: "Use consistent title case throughout"
4454
+ });
4455
+ score -= 10;
4456
+ }
4457
+ const fontMatches = html.match(/font-family:\s*([^;]+);/gi) || [];
4458
+ const uniqueFonts = new Set(fontMatches.map((f) => f.toLowerCase()));
4459
+ if (uniqueFonts.size > 3) {
4460
+ issues.push({
4461
+ slideIndex: -1,
4462
+ dimension: "consistency",
4463
+ severity: "warning",
4464
+ message: `Too many font families (${uniqueFonts.size} > 3)`,
4465
+ autoFixable: true,
4466
+ fixSuggestion: "Use 2-3 complementary fonts max"
4467
+ });
4468
+ score -= 10;
4469
+ }
4470
+ return {
4471
+ name: "Consistency",
4472
+ score: Math.max(0, score),
4473
+ weight: DIMENSION_WEIGHTS.consistency,
4474
+ issues,
4475
+ details: {
4476
+ hasCssVariables,
4477
+ titlePatterns: Array.from(titlePatterns),
4478
+ fontFamilyCount: uniqueFonts.size
4479
+ }
4480
+ };
4481
+ }
4482
+ /**
4483
+ * Count words in a slide.
4484
+ */
4485
+ countWords(slide) {
4486
+ let text = "";
4487
+ if (slide.data.title) text += slide.data.title + " ";
4488
+ if (slide.data.subtitle) text += slide.data.subtitle + " ";
4489
+ if (slide.data.body) text += slide.data.body + " ";
4490
+ if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
4491
+ if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
4492
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
4493
+ }
4494
+ /**
4495
+ * Get a formatted report of the scoring results.
4496
+ */
4497
+ formatReport(result) {
4498
+ const lines = [];
4499
+ 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");
4500
+ lines.push("\u2551 7-DIMENSION QUALITY ASSESSMENT \u2551");
4501
+ 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");
4502
+ lines.push("");
4503
+ lines.push(`Overall Score: ${result.overallScore}/100 ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
4504
+ lines.push(`Threshold: ${result.threshold}/100`);
4505
+ lines.push("");
4506
+ lines.push("Dimension Breakdown:");
4507
+ 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");
4508
+ const dims = result.dimensions;
4509
+ const formatDim = (name, d) => {
4510
+ const bar = "\u2588".repeat(Math.floor(d.score / 10)) + "\u2591".repeat(10 - Math.floor(d.score / 10));
4511
+ const status = d.score >= 95 ? "\u2705" : d.score >= 80 ? "\u26A0\uFE0F" : "\u274C";
4512
+ return `${status} ${name.padEnd(14)} ${bar} ${d.score.toString().padStart(3)}/100 (${(d.weight * 100).toFixed(0)}%)`;
4513
+ };
4514
+ lines.push(formatDim("Layout", dims.layout));
4515
+ lines.push(formatDim("Contrast", dims.contrast));
4516
+ lines.push(formatDim("Graphics", dims.graphics));
4517
+ lines.push(formatDim("Content", dims.content));
4518
+ lines.push(formatDim("Clarity", dims.clarity));
4519
+ lines.push(formatDim("Effectiveness", dims.effectiveness));
4520
+ lines.push(formatDim("Consistency", dims.consistency));
4521
+ lines.push("");
4522
+ const errors = result.issues.filter((i) => i.severity === "error");
4523
+ const warnings = result.issues.filter((i) => i.severity === "warning");
4524
+ if (errors.length > 0) {
4525
+ lines.push("\u274C Errors:");
4526
+ errors.forEach((e) => lines.push(` \u2022 ${e.message}`));
4527
+ lines.push("");
4528
+ }
4529
+ if (warnings.length > 0) {
4530
+ lines.push("\u26A0\uFE0F Warnings:");
4531
+ warnings.slice(0, 10).forEach((w) => lines.push(` \u2022 ${w.message}`));
4532
+ if (warnings.length > 10) {
4533
+ lines.push(` ... and ${warnings.length - 10} more warnings`);
4534
+ }
4535
+ lines.push("");
4536
+ }
4537
+ const autoFixable = result.issues.filter((i) => i.autoFixable);
4538
+ if (autoFixable.length > 0) {
4539
+ lines.push(`\u{1F527} ${autoFixable.length} issues can be auto-fixed`);
4540
+ }
4541
+ return lines.join("\n");
4542
+ }
4543
+ };
4544
+
4545
+ // src/qa/AutoFixEngine.ts
4546
+ var AutoFixEngine = class {
4547
+ kb;
4548
+ mode;
4549
+ presentationType;
4550
+ constructor(mode, presentationType) {
4551
+ this.mode = mode;
4552
+ this.presentationType = presentationType;
4553
+ }
4554
+ /**
4555
+ * Apply automatic fixes to slides based on scoring results.
4556
+ */
4557
+ async fix(slides, scoringResult) {
4558
+ this.kb = await getKnowledgeGateway();
4559
+ const slidesFixed = JSON.parse(JSON.stringify(slides));
4560
+ const fixesApplied = [];
4561
+ const fixesSkipped = [];
4562
+ const autoFixableIssues = scoringResult.issues.filter((i) => i.autoFixable);
4563
+ for (const issue of autoFixableIssues) {
4564
+ const result = await this.applyFix(slidesFixed, issue);
4565
+ if (result.applied) {
4566
+ fixesApplied.push(result);
4567
+ } else {
4568
+ fixesSkipped.push(result);
4569
+ }
4570
+ }
4571
+ const summary = this.generateSummary(fixesApplied, fixesSkipped);
4572
+ return {
4573
+ slidesFixed,
4574
+ fixesApplied,
4575
+ fixesSkipped,
4576
+ summary
4577
+ };
4578
+ }
4579
+ /**
4580
+ * Apply a single fix based on the issue.
4581
+ */
4582
+ async applyFix(slides, issue) {
4583
+ const result = {
4584
+ slideIndex: issue.slideIndex,
4585
+ dimension: issue.dimension,
4586
+ originalValue: issue.currentValue,
4587
+ newValue: null,
4588
+ description: issue.message,
4589
+ applied: false
4590
+ };
4591
+ switch (issue.dimension) {
4592
+ case "content":
4593
+ return this.fixContentIssue(slides, issue, result);
4594
+ case "clarity":
4595
+ return this.fixClarityIssue(slides, issue, result);
4596
+ case "layout":
4597
+ return this.fixLayoutIssue(slides, issue, result);
4598
+ case "consistency":
4599
+ return this.fixConsistencyIssue(slides, issue, result);
4600
+ case "contrast":
4601
+ return this.fixContrastIssue(slides, issue, result);
4602
+ default:
4603
+ result.description = `No auto-fix available for ${issue.dimension}`;
4604
+ return result;
4605
+ }
4606
+ }
4607
+ /**
4608
+ * Fix content-related issues (word count, bullet count).
4609
+ */
4610
+ fixContentIssue(slides, issue, result) {
4611
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4612
+ return result;
4613
+ }
4614
+ const slide = slides[issue.slideIndex];
4615
+ if (!slide) return result;
4616
+ if (issue.message.includes("Word count") && issue.message.includes("exceeds")) {
4617
+ const maxWords = issue.expectedValue;
4618
+ result.originalValue = this.countWords(slide);
4619
+ if (slide.data.body) {
4620
+ slide.data.body = this.condenseText(slide.data.body, maxWords / 2);
4621
+ }
4622
+ if (slide.data.bullets && slide.data.bullets.length > 0) {
4623
+ const bulletCount = slide.data.bullets.length;
4624
+ slide.data.bullets = slide.data.bullets.map(
4625
+ (bullet) => this.condenseText(bullet, Math.floor(maxWords / bulletCount))
4626
+ );
4627
+ }
4628
+ if (slide.data.subtitle) {
4629
+ slide.data.subtitle = this.condenseText(slide.data.subtitle, 10);
4630
+ }
4631
+ result.newValue = this.countWords(slide);
4632
+ result.applied = result.newValue <= maxWords;
4633
+ result.description = `Condensed content from ${result.originalValue} to ${result.newValue} words`;
4634
+ }
4635
+ if (issue.message.includes("bullets exceeds")) {
4636
+ const maxBullets = issue.expectedValue;
4637
+ if (slide.data.bullets) {
4638
+ result.originalValue = slide.data.bullets.length;
4639
+ slide.data.bullets = slide.data.bullets.slice(0, maxBullets);
4640
+ result.newValue = slide.data.bullets.length;
4641
+ result.applied = true;
4642
+ result.description = `Reduced bullets from ${result.originalValue} to ${result.newValue}`;
4643
+ }
4644
+ }
4645
+ return result;
4646
+ }
4647
+ /**
4648
+ * Fix clarity-related issues (key message length, title length).
4649
+ */
4650
+ fixClarityIssue(slides, issue, result) {
4651
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4652
+ return result;
4653
+ }
4654
+ const slide = slides[issue.slideIndex];
4655
+ if (!slide) return result;
4656
+ if (issue.message.includes("Key message too long")) {
4657
+ if (slide.data.keyMessage) {
4658
+ result.originalValue = slide.data.keyMessage;
4659
+ const maxWords = issue.expectedValue;
4660
+ slide.data.keyMessage = this.condenseText(slide.data.keyMessage, maxWords);
4661
+ result.newValue = slide.data.keyMessage;
4662
+ result.applied = true;
4663
+ result.description = `Shortened key message to ${maxWords} words`;
4664
+ }
4665
+ }
4666
+ if (issue.message.includes("Title too long")) {
4667
+ if (slide.data.title) {
4668
+ result.originalValue = slide.data.title;
4669
+ const words = slide.data.title.split(/\s+/);
4670
+ const originalLength = words.length;
4671
+ if (words.length > 6) {
4672
+ slide.data.title = words.slice(0, 6).join(" ");
4673
+ }
4674
+ result.newValue = slide.data.title;
4675
+ result.applied = true;
4676
+ result.description = `Shortened title from ${originalLength} to ${slide.data.title.split(/\s+/).length} words`;
4677
+ }
4678
+ }
4679
+ if (issue.message.includes("Too many elements")) {
4680
+ result.originalValue = issue.currentValue;
4681
+ if (slide.data.subtitle && slide.data.body) {
4682
+ delete slide.data.subtitle;
4683
+ result.applied = true;
4684
+ result.description = "Removed subtitle to reduce element count";
4685
+ } else if (slide.data.body && slide.data.bullets && slide.data.bullets.length > 0) {
4686
+ delete slide.data.body;
4687
+ result.applied = true;
4688
+ result.description = "Removed body text, keeping bullets";
4689
+ }
4690
+ result.newValue = this.countElements(slide);
4691
+ }
4692
+ return result;
4693
+ }
4694
+ /**
4695
+ * Fix layout-related issues.
4696
+ */
4697
+ fixLayoutIssue(slides, issue, result) {
4698
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4699
+ return result;
4700
+ }
4701
+ const slide = slides[issue.slideIndex];
4702
+ if (!slide) return result;
4703
+ if (issue.message.includes("Insufficient whitespace")) {
4704
+ const currentWordCount = this.countWords(slide);
4705
+ const targetReduction = 0.3;
4706
+ const targetWords = Math.floor(currentWordCount * (1 - targetReduction));
4707
+ result.originalValue = currentWordCount;
4708
+ if (slide.data.body) {
4709
+ slide.data.body = this.condenseText(slide.data.body, Math.floor(targetWords * 0.5));
4710
+ }
4711
+ if (slide.data.bullets && slide.data.bullets.length > 0) {
4712
+ const wordsPerBullet = Math.floor(targetWords / (slide.data.bullets.length * 2));
4713
+ slide.data.bullets = slide.data.bullets.map((b) => this.condenseText(b, wordsPerBullet));
4714
+ }
4715
+ result.newValue = this.countWords(slide);
4716
+ result.applied = true;
4717
+ result.description = `Reduced content from ${result.originalValue} to ${result.newValue} words for better whitespace`;
4718
+ }
4719
+ return result;
4720
+ }
4721
+ /**
4722
+ * Fix consistency-related issues.
4723
+ */
4724
+ fixConsistencyIssue(slides, issue, result) {
4725
+ if (issue.message.includes("Inconsistent title casing")) {
4726
+ let fixedCount = 0;
4727
+ for (const slide of slides) {
4728
+ if (slide.data.title) {
4729
+ const original = slide.data.title;
4730
+ slide.data.title = this.toTitleCase(slide.data.title);
4731
+ if (slide.data.title !== original) fixedCount++;
4732
+ }
4733
+ }
4734
+ result.originalValue = "Mixed casing";
4735
+ result.newValue = "Title Case";
4736
+ result.applied = fixedCount > 0;
4737
+ result.description = `Applied Title Case to ${fixedCount} slide titles`;
4738
+ }
4739
+ return result;
4740
+ }
4741
+ /**
4742
+ * Fix contrast-related issues.
4743
+ * Note: These are CSS fixes, typically handled at generation time.
4744
+ */
4745
+ fixContrastIssue(slides, issue, result) {
4746
+ result.description = "Contrast issues flagged for CSS regeneration";
4747
+ result.applied = false;
4748
+ return result;
4749
+ }
4750
+ /**
4751
+ * Condense text to approximately maxWords.
4752
+ * Uses smart truncation that preserves meaning.
4753
+ */
4754
+ condenseText(text, maxWords) {
4755
+ const words = text.split(/\s+/);
4756
+ if (words.length <= maxWords) {
4757
+ return text;
4758
+ }
4759
+ const fillerWords = /* @__PURE__ */ new Set([
4760
+ "very",
4761
+ "really",
4762
+ "actually",
4763
+ "basically",
4764
+ "literally",
4765
+ "obviously",
4766
+ "clearly",
4767
+ "simply",
4768
+ "just",
4769
+ "that",
4770
+ "which",
4771
+ "would",
4772
+ "could",
4773
+ "should",
4774
+ "might"
4775
+ ]);
4776
+ let filtered = words.filter((w) => !fillerWords.has(w.toLowerCase()));
4777
+ if (filtered.length <= maxWords) {
4778
+ return filtered.join(" ");
4779
+ }
4780
+ const punctuation = [".", ",", ";", ":", "-"];
4781
+ let breakPoint = maxWords;
4782
+ for (let i = maxWords - 1; i >= maxWords - 5 && i >= 0; i--) {
4783
+ const word = filtered[i];
4784
+ if (word && punctuation.some((p) => word.endsWith(p))) {
4785
+ breakPoint = i + 1;
4786
+ break;
4787
+ }
2901
4788
  }
4789
+ filtered = filtered.slice(0, breakPoint);
4790
+ let result = filtered.join(" ");
4791
+ if (!result.endsWith(".") && !result.endsWith("!") && !result.endsWith("?")) {
4792
+ result = result.replace(/[,;:]$/, "") + "...";
4793
+ }
4794
+ return result;
2902
4795
  }
2903
- async closeBrowser() {
2904
- if (this.browser) {
2905
- await this.browser.close();
2906
- this.browser = null;
4796
+ /**
4797
+ * Convert text to Title Case.
4798
+ */
4799
+ toTitleCase(text) {
4800
+ const minorWords = /* @__PURE__ */ new Set([
4801
+ "a",
4802
+ "an",
4803
+ "the",
4804
+ "and",
4805
+ "but",
4806
+ "or",
4807
+ "for",
4808
+ "nor",
4809
+ "on",
4810
+ "at",
4811
+ "to",
4812
+ "by",
4813
+ "of",
4814
+ "in",
4815
+ "with"
4816
+ ]);
4817
+ return text.split(" ").map((word, index) => {
4818
+ if (index === 0) {
4819
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
4820
+ }
4821
+ if (minorWords.has(word.toLowerCase())) {
4822
+ return word.toLowerCase();
4823
+ }
4824
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
4825
+ }).join(" ");
4826
+ }
4827
+ /**
4828
+ * Count words in a slide.
4829
+ */
4830
+ countWords(slide) {
4831
+ let text = "";
4832
+ if (slide.data.title) text += slide.data.title + " ";
4833
+ if (slide.data.subtitle) text += slide.data.subtitle + " ";
4834
+ if (slide.data.body) text += slide.data.body + " ";
4835
+ if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
4836
+ if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
4837
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
4838
+ }
4839
+ /**
4840
+ * Count elements in a slide.
4841
+ */
4842
+ countElements(slide) {
4843
+ 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);
4844
+ }
4845
+ /**
4846
+ * Generate a summary of fixes applied.
4847
+ */
4848
+ generateSummary(applied, skipped) {
4849
+ const lines = [];
4850
+ lines.push(`
4851
+ \u{1F527} Auto-Fix Summary`);
4852
+ 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`);
4853
+ lines.push(`Fixes Applied: ${applied.length}`);
4854
+ lines.push(`Fixes Skipped: ${skipped.length}`);
4855
+ lines.push("");
4856
+ if (applied.length > 0) {
4857
+ lines.push("\u2705 Applied Fixes:");
4858
+ for (const fix of applied) {
4859
+ lines.push(` \u2022 ${fix.description}`);
4860
+ }
4861
+ lines.push("");
4862
+ }
4863
+ if (skipped.length > 0) {
4864
+ lines.push("\u26A0\uFE0F Skipped Fixes (require manual attention):");
4865
+ for (const fix of skipped.slice(0, 5)) {
4866
+ lines.push(` \u2022 ${fix.description}`);
4867
+ }
4868
+ if (skipped.length > 5) {
4869
+ lines.push(` ... and ${skipped.length - 5} more`);
4870
+ }
2907
4871
  }
4872
+ return lines.join("\n");
2908
4873
  }
2909
4874
  };
4875
+ function createAutoFixEngine(mode, presentationType) {
4876
+ return new AutoFixEngine(mode, presentationType);
4877
+ }
2910
4878
 
2911
4879
  // src/generators/html/RevealJsGenerator.ts
2912
4880
  var RevealJsGenerator = class {
@@ -3070,6 +5038,12 @@ ${slides}
3070
5038
  const highlight = palette.accent || "#e94560";
3071
5039
  const text = palette.text || "#1a1a2e";
3072
5040
  const background = palette.background || "#ffffff";
5041
+ const isDark = this.isDarkPalette(palette);
5042
+ const defaultBg = isDark ? background : "#ffffff";
5043
+ const defaultText = isDark ? text : "#1a1a2e";
5044
+ const headingColor = isDark ? primary : primary;
5045
+ const titleBg = isDark ? background : primary;
5046
+ const titleBgEnd = isDark ? this.lightenColor(background, 10) : this.darkenColor(primary, 15);
3073
5047
  return `
3074
5048
  /* Base Styles - KB-Driven Design System */
3075
5049
  :root {
@@ -3096,7 +5070,13 @@ ${slides}
3096
5070
  font-family: var(--font-body);
3097
5071
  font-size: ${fontSize};
3098
5072
  line-height: ${lineHeight};
3099
- color: var(--color-text);
5073
+ color: ${defaultText};
5074
+ background: ${defaultBg};
5075
+ }
5076
+
5077
+ /* Override reveal.js default backgrounds for dark mode */
5078
+ .reveal .slides section {
5079
+ background: ${defaultBg};
3100
5080
  }
3101
5081
 
3102
5082
  .reveal .slides {
@@ -3125,10 +5105,17 @@ ${slides}
3125
5105
  font-family: var(--font-heading);
3126
5106
  font-weight: 700;
3127
5107
  letter-spacing: -0.02em;
3128
- color: var(--color-primary);
5108
+ color: ${isDark ? primary : primary};
3129
5109
  margin-bottom: 0.5em;
3130
5110
  }
3131
5111
 
5112
+ /* Dark mode text visibility */
5113
+ ${isDark ? `
5114
+ .reveal, .reveal p, .reveal li, .reveal span {
5115
+ color: ${text};
5116
+ }
5117
+ ` : ""}
5118
+
3132
5119
  .reveal h1 { font-size: 2.5em; }
3133
5120
  .reveal h2 { font-size: 1.8em; }
3134
5121
  .reveal h3 { font-size: 1.3em; }
@@ -3306,6 +5293,124 @@ ${slides}
3306
5293
  font-weight: 600;
3307
5294
  margin-top: 1em;
3308
5295
  }
5296
+
5297
+ /* ================================================================
5298
+ SLIDE TYPE BACKGROUNDS - KB-Driven Visual Design
5299
+ Creates visual hierarchy and differentiation between slide types
5300
+ ================================================================ */
5301
+
5302
+ /* Title slide: Bold background - makes strong first impression */
5303
+ .reveal .slide-title {
5304
+ background: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
5305
+ }
5306
+ .reveal .slide-title h1,
5307
+ .reveal .slide-title h2,
5308
+ .reveal .slide-title p,
5309
+ .reveal .slide-title .subtitle {
5310
+ color: ${isDark ? primary : "#ffffff"};
5311
+ }
5312
+
5313
+ /* Section dividers: Subtle visual breaks */
5314
+ .reveal .slide-section-divider {
5315
+ background: ${isDark ? this.lightenColor(background, 8) : `linear-gradient(180deg, ${this.lightenColor(primary, 85)} 0%, ${this.lightenColor(primary, 92)} 100%)`};
5316
+ }
5317
+
5318
+ /* Big number slides: Clean with accent highlight - emphasizes the data */
5319
+ .reveal .slide-big-number {
5320
+ background: var(--color-background);
5321
+ border-left: 8px solid var(--color-highlight);
5322
+ }
5323
+ .reveal .slide-big-number .number {
5324
+ font-size: 5em;
5325
+ background: linear-gradient(135deg, var(--color-highlight) 0%, var(--color-accent) 100%);
5326
+ -webkit-background-clip: text;
5327
+ -webkit-text-fill-color: transparent;
5328
+ background-clip: text;
5329
+ }
5330
+
5331
+ /* Metrics grid: Light accent background - data-focused */
5332
+ .reveal .slide-metrics-grid {
5333
+ background: ${isDark ? this.lightenColor(background, 8) : this.lightenColor(accent, 90)};
5334
+ }
5335
+ ${isDark ? `.reveal .slide-metrics-grid { color: ${text}; }` : ""}
5336
+
5337
+ /* CTA slide: Highlight color - drives action */
5338
+ .reveal .slide-cta {
5339
+ background: linear-gradient(135deg, var(--color-highlight) 0%, var(--color-accent) 100%);
5340
+ }
5341
+ .reveal .slide-cta h2,
5342
+ .reveal .slide-cta p {
5343
+ color: #ffffff;
5344
+ }
5345
+ .reveal .slide-cta .cta-button {
5346
+ background: #ffffff;
5347
+ color: var(--color-highlight);
5348
+ }
5349
+
5350
+ /* Thank you slide: Matches title for bookend effect */
5351
+ .reveal .slide-thank-you {
5352
+ background: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
5353
+ }
5354
+ .reveal .slide-thank-you h2,
5355
+ .reveal .slide-thank-you p,
5356
+ .reveal .slide-thank-you .subtitle {
5357
+ color: ${isDark ? primary : "#ffffff"};
5358
+ }
5359
+
5360
+ /* Quote slides: Elegant subtle background */
5361
+ .reveal .slide-quote {
5362
+ background: ${isDark ? this.lightenColor(background, 5) : this.lightenColor(secondary, 92)};
5363
+ }
5364
+ .reveal .slide-quote blockquote {
5365
+ border-left-color: var(--color-accent);
5366
+ font-size: 1.3em;
5367
+ }
5368
+ ${isDark ? `.reveal .slide-quote { color: ${text}; }` : ""}
5369
+
5370
+ /* Single statement: Clean, centered, impactful */
5371
+ .reveal .slide-single-statement {
5372
+ background: var(--color-background);
5373
+ }
5374
+ .reveal .slide-single-statement .statement,
5375
+ .reveal .slide-single-statement .big-idea-text {
5376
+ font-size: 2.2em;
5377
+ max-width: 80%;
5378
+ margin: 0 auto;
5379
+ }
5380
+
5381
+ /* Comparison slides: Split visual */
5382
+ .reveal .slide-comparison .columns {
5383
+ gap: 60px;
5384
+ }
5385
+ .reveal .slide-comparison .column:first-child {
5386
+ border-right: 2px solid ${this.lightenColor(secondary, 70)};
5387
+ padding-right: 30px;
5388
+ }
5389
+
5390
+ /* Timeline/Process: Visual flow */
5391
+ .reveal .slide-timeline .steps,
5392
+ .reveal .slide-process .steps {
5393
+ display: flex;
5394
+ gap: 20px;
5395
+ }
5396
+ .reveal .slide-timeline .step,
5397
+ .reveal .slide-process .step {
5398
+ flex: 1;
5399
+ padding: 20px;
5400
+ background: ${isDark ? this.lightenColor(background, 10) : this.lightenColor(secondary, 92)};
5401
+ border-radius: 8px;
5402
+ border-top: 4px solid var(--color-accent);
5403
+ ${isDark ? `color: ${text};` : ""}
5404
+ }
5405
+
5406
+ /* Progress bar enhancement */
5407
+ .reveal .progress {
5408
+ background: ${this.lightenColor(primary, 80)};
5409
+ height: 4px;
5410
+ }
5411
+ .reveal .progress span {
5412
+ background: var(--color-highlight);
5413
+ }
3309
5414
  `;
3310
5415
  }
3311
5416
  /**
@@ -3321,6 +5426,37 @@ ${slides}
3321
5426
  b = Math.min(255, Math.floor(b + (255 - b) * (percent / 100)));
3322
5427
  return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
3323
5428
  }
5429
+ /**
5430
+ * Check if a palette is "dark mode" (dark background, light text)
5431
+ */
5432
+ isDarkPalette(palette) {
5433
+ const bgLuminance = this.getLuminance(palette.background || "#ffffff");
5434
+ const textLuminance = this.getLuminance(palette.text || "#000000");
5435
+ return bgLuminance < textLuminance;
5436
+ }
5437
+ /**
5438
+ * Get relative luminance of a hex color (0-1, 0=black, 1=white)
5439
+ */
5440
+ getLuminance(hex) {
5441
+ hex = hex.replace("#", "");
5442
+ const r = parseInt(hex.substring(0, 2), 16) / 255;
5443
+ const g = parseInt(hex.substring(2, 4), 16) / 255;
5444
+ const b = parseInt(hex.substring(4, 6), 16) / 255;
5445
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
5446
+ }
5447
+ /**
5448
+ * Darken a hex color
5449
+ */
5450
+ darkenColor(hex, percent) {
5451
+ hex = hex.replace("#", "");
5452
+ let r = parseInt(hex.substring(0, 2), 16);
5453
+ let g = parseInt(hex.substring(2, 4), 16);
5454
+ let b = parseInt(hex.substring(4, 6), 16);
5455
+ r = Math.max(0, Math.floor(r * (1 - percent / 100)));
5456
+ g = Math.max(0, Math.floor(g * (1 - percent / 100)));
5457
+ b = Math.max(0, Math.floor(b * (1 - percent / 100)));
5458
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
5459
+ }
3324
5460
  /**
3325
5461
  * Get theme-specific styles - KB-DRIVEN
3326
5462
  */
@@ -3502,6 +5638,182 @@ ${slides}
3502
5638
  }
3503
5639
  };
3504
5640
 
5641
+ // src/qa/IterativeQAEngine.ts
5642
+ var DEFAULT_OPTIONS = {
5643
+ minScore: 95,
5644
+ maxIterations: 5,
5645
+ verbose: true
5646
+ };
5647
+ var IterativeQAEngine = class {
5648
+ kb;
5649
+ scorer;
5650
+ fixer;
5651
+ generator;
5652
+ mode;
5653
+ presentationType;
5654
+ config;
5655
+ constructor(mode, presentationType, config) {
5656
+ this.mode = mode;
5657
+ this.presentationType = presentationType;
5658
+ this.config = config;
5659
+ this.scorer = new SevenDimensionScorer(mode, presentationType);
5660
+ this.fixer = new AutoFixEngine(mode, presentationType);
5661
+ this.generator = new RevealJsGenerator();
5662
+ }
5663
+ /**
5664
+ * Run the iterative QA process.
5665
+ */
5666
+ async run(initialSlides, initialHtml, options = {}) {
5667
+ const opts = { ...DEFAULT_OPTIONS, ...options };
5668
+ this.kb = await getKnowledgeGateway();
5669
+ const iterations = [];
5670
+ let currentSlides = initialSlides;
5671
+ let currentHtml = initialHtml;
5672
+ let scoringResult;
5673
+ let autoFixSummary = "";
5674
+ let totalFixesApplied = 0;
5675
+ if (opts.verbose) {
5676
+ logger.progress("\n\u{1F504} Starting Iterative QA Process");
5677
+ logger.info(` Target Score: ${opts.minScore}/100`);
5678
+ logger.info(` Max Iterations: ${opts.maxIterations}`);
5679
+ logger.progress("");
5680
+ }
5681
+ for (let i = 0; i < opts.maxIterations; i++) {
5682
+ const iterationNum = i + 1;
5683
+ if (opts.verbose) {
5684
+ logger.progress(`\u{1F4CA} Iteration ${iterationNum}/${opts.maxIterations}...`);
5685
+ }
5686
+ scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
5687
+ iterations.push({
5688
+ iteration: iterationNum,
5689
+ score: scoringResult.overallScore,
5690
+ dimensionScores: {
5691
+ layout: scoringResult.dimensions.layout.score,
5692
+ contrast: scoringResult.dimensions.contrast.score,
5693
+ graphics: scoringResult.dimensions.graphics.score,
5694
+ content: scoringResult.dimensions.content.score,
5695
+ clarity: scoringResult.dimensions.clarity.score,
5696
+ effectiveness: scoringResult.dimensions.effectiveness.score,
5697
+ consistency: scoringResult.dimensions.consistency.score
5698
+ },
5699
+ fixesApplied: 0,
5700
+ timestamp: /* @__PURE__ */ new Date()
5701
+ });
5702
+ if (opts.verbose) {
5703
+ logger.info(` Score: ${scoringResult.overallScore}/100`);
5704
+ }
5705
+ if (scoringResult.passed) {
5706
+ if (opts.verbose) {
5707
+ logger.success(`PASSED - Score meets threshold (${opts.minScore})`);
5708
+ }
5709
+ break;
5710
+ }
5711
+ if (i < opts.maxIterations - 1) {
5712
+ if (opts.verbose) {
5713
+ logger.warn("Below threshold, applying auto-fixes...");
5714
+ }
5715
+ const fixResult = await this.fixer.fix(currentSlides, scoringResult);
5716
+ if (fixResult.fixesApplied.length > 0) {
5717
+ currentSlides = fixResult.slidesFixed;
5718
+ currentHtml = await this.generator.generate(currentSlides, this.config);
5719
+ const lastIteration = iterations[iterations.length - 1];
5720
+ if (lastIteration) {
5721
+ lastIteration.fixesApplied = fixResult.fixesApplied.length;
5722
+ }
5723
+ totalFixesApplied += fixResult.fixesApplied.length;
5724
+ if (opts.verbose) {
5725
+ logger.info(` \u{1F527} Applied ${fixResult.fixesApplied.length} fixes`);
5726
+ }
5727
+ autoFixSummary += fixResult.summary + "\n";
5728
+ } else {
5729
+ if (opts.verbose) {
5730
+ logger.warn("No auto-fixes available, manual review needed");
5731
+ }
5732
+ break;
5733
+ }
5734
+ }
5735
+ }
5736
+ if (!scoringResult.passed) {
5737
+ scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
5738
+ }
5739
+ const report = this.generateReport(
5740
+ scoringResult,
5741
+ iterations,
5742
+ opts,
5743
+ totalFixesApplied
5744
+ );
5745
+ if (opts.verbose) {
5746
+ logger.info(report);
5747
+ }
5748
+ return {
5749
+ finalScore: scoringResult.overallScore,
5750
+ passed: scoringResult.passed,
5751
+ threshold: opts.minScore,
5752
+ iterations,
5753
+ totalIterations: iterations.length,
5754
+ maxIterations: opts.maxIterations,
5755
+ slides: currentSlides,
5756
+ html: currentHtml,
5757
+ finalScoring: scoringResult,
5758
+ autoFixSummary,
5759
+ report
5760
+ };
5761
+ }
5762
+ /**
5763
+ * Generate a comprehensive report.
5764
+ */
5765
+ generateReport(finalScoring, iterations, options, totalFixesApplied) {
5766
+ const lines = [];
5767
+ lines.push("");
5768
+ 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");
5769
+ lines.push("\u2551 ITERATIVE QA FINAL REPORT \u2551");
5770
+ 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");
5771
+ lines.push("");
5772
+ const passStatus = finalScoring.passed ? "\u2705 PASSED" : "\u274C FAILED";
5773
+ lines.push(`Final Score: ${finalScoring.overallScore}/100 ${passStatus}`);
5774
+ lines.push(`Threshold: ${options.minScore}/100`);
5775
+ lines.push(`Iterations: ${iterations.length}/${options.maxIterations}`);
5776
+ lines.push(`Total Fixes Applied: ${totalFixesApplied}`);
5777
+ lines.push("");
5778
+ if (iterations.length > 1) {
5779
+ lines.push("Score Progression:");
5780
+ 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");
5781
+ for (const iter of iterations) {
5782
+ const bar = "\u2588".repeat(Math.floor(iter.score / 10)) + "\u2591".repeat(10 - Math.floor(iter.score / 10));
5783
+ lines.push(` Iter ${iter.iteration}: ${bar} ${iter.score}/100 (+${iter.fixesApplied} fixes)`);
5784
+ }
5785
+ lines.push("");
5786
+ }
5787
+ lines.push(this.scorer.formatReport(finalScoring));
5788
+ lines.push("");
5789
+ lines.push("\u{1F4DA} Knowledge Base Compliance:");
5790
+ 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");
5791
+ lines.push(` Mode: ${this.mode}`);
5792
+ lines.push(` Presentation Type: ${this.presentationType}`);
5793
+ lines.push(` Word Limits: ${this.mode === "keynote" ? "6-25" : "40-80"} per slide`);
5794
+ lines.push(` Expert Frameworks: Duarte, Reynolds, Gallo, Anderson`);
5795
+ lines.push("");
5796
+ if (!finalScoring.passed) {
5797
+ lines.push("\u{1F4CB} Recommendations for Manual Review:");
5798
+ 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");
5799
+ const manualIssues = finalScoring.issues.filter((i) => !i.autoFixable);
5800
+ for (const issue of manualIssues.slice(0, 5)) {
5801
+ lines.push(` \u2022 ${issue.message}`);
5802
+ if (issue.fixSuggestion) {
5803
+ lines.push(` \u2192 ${issue.fixSuggestion}`);
5804
+ }
5805
+ }
5806
+ if (manualIssues.length > 5) {
5807
+ lines.push(` ... and ${manualIssues.length - 5} more issues`);
5808
+ }
5809
+ }
5810
+ return lines.join("\n");
5811
+ }
5812
+ };
5813
+ function createIterativeQAEngine(mode, presentationType, config) {
5814
+ return new IterativeQAEngine(mode, presentationType, config);
5815
+ }
5816
+
3505
5817
  // src/generators/pptx/PowerPointGenerator.ts
3506
5818
  var import_pptxgenjs = __toESM(require("pptxgenjs"));
3507
5819
 
@@ -4233,23 +6545,190 @@ var PowerPointGenerator = class {
4233
6545
  }
4234
6546
  };
4235
6547
 
6548
+ // src/generators/PDFGenerator.ts
6549
+ var import_puppeteer = __toESM(require("puppeteer"));
6550
+ var PDFGenerator = class {
6551
+ defaultOptions = {
6552
+ orientation: "landscape",
6553
+ printBackground: true,
6554
+ format: "Slide",
6555
+ scale: 1
6556
+ };
6557
+ /**
6558
+ * Generate PDF from HTML presentation
6559
+ * @param html - The HTML content of the presentation
6560
+ * @param options - PDF generation options
6561
+ * @returns Buffer containing the PDF file
6562
+ */
6563
+ async generate(html, options = {}) {
6564
+ const opts = { ...this.defaultOptions, ...options };
6565
+ logger.progress("\u{1F4C4} Generating PDF...");
6566
+ let browser;
6567
+ try {
6568
+ browser = await import_puppeteer.default.launch({
6569
+ headless: true,
6570
+ args: [
6571
+ "--no-sandbox",
6572
+ "--disable-setuid-sandbox",
6573
+ "--disable-dev-shm-usage"
6574
+ ]
6575
+ });
6576
+ const page = await browser.newPage();
6577
+ const printHtml = this.preparePrintHtml(html);
6578
+ await page.setViewport({
6579
+ width: 1920,
6580
+ height: 1080
6581
+ });
6582
+ await page.setContent(printHtml, {
6583
+ waitUntil: ["networkidle0", "domcontentloaded"]
6584
+ });
6585
+ await page.evaluate(() => {
6586
+ return new Promise((resolve) => {
6587
+ if (document.fonts && document.fonts.ready) {
6588
+ document.fonts.ready.then(() => resolve());
6589
+ } else {
6590
+ setTimeout(resolve, 1e3);
6591
+ }
6592
+ });
6593
+ });
6594
+ const pdfBuffer = await page.pdf({
6595
+ width: "1920px",
6596
+ height: "1080px",
6597
+ printBackground: opts.printBackground,
6598
+ scale: opts.scale,
6599
+ landscape: opts.orientation === "landscape",
6600
+ margin: {
6601
+ top: "0px",
6602
+ right: "0px",
6603
+ bottom: "0px",
6604
+ left: "0px"
6605
+ }
6606
+ });
6607
+ logger.step("PDF generated successfully");
6608
+ return Buffer.from(pdfBuffer);
6609
+ } catch (error) {
6610
+ const errorMessage = error instanceof Error ? error.message : String(error);
6611
+ logger.error(`PDF generation failed: ${errorMessage}`);
6612
+ throw new Error(`PDF generation failed: ${errorMessage}`);
6613
+ } finally {
6614
+ if (browser) {
6615
+ await browser.close();
6616
+ }
6617
+ }
6618
+ }
6619
+ /**
6620
+ * Prepare HTML for print/PDF output
6621
+ * Modifies the HTML to work better as a printed document
6622
+ */
6623
+ preparePrintHtml(html) {
6624
+ const printCss = `
6625
+ <style id="pdf-print-styles">
6626
+ /* Print-specific overrides */
6627
+ @page {
6628
+ size: 1920px 1080px;
6629
+ margin: 0;
6630
+ }
6631
+
6632
+ @media print {
6633
+ html, body {
6634
+ margin: 0;
6635
+ padding: 0;
6636
+ width: 1920px;
6637
+ height: 1080px;
6638
+ }
6639
+
6640
+ .reveal .slides {
6641
+ width: 100% !important;
6642
+ height: 100% !important;
6643
+ margin: 0 !important;
6644
+ padding: 0 !important;
6645
+ transform: none !important;
6646
+ }
6647
+
6648
+ .reveal .slides section {
6649
+ page-break-after: always;
6650
+ page-break-inside: avoid;
6651
+ width: 1920px !important;
6652
+ height: 1080px !important;
6653
+ margin: 0 !important;
6654
+ padding: 60px !important;
6655
+ box-sizing: border-box;
6656
+ position: relative !important;
6657
+ top: auto !important;
6658
+ left: auto !important;
6659
+ transform: none !important;
6660
+ display: flex !important;
6661
+ opacity: 1 !important;
6662
+ visibility: visible !important;
6663
+ }
6664
+
6665
+ .reveal .slides section:last-child {
6666
+ page-break-after: auto;
6667
+ }
6668
+
6669
+ /* Make all slides visible for printing */
6670
+ .reveal .slides section.future,
6671
+ .reveal .slides section.past {
6672
+ display: flex !important;
6673
+ opacity: 1 !important;
6674
+ visibility: visible !important;
6675
+ }
6676
+
6677
+ /* Disable animations for print */
6678
+ * {
6679
+ animation: none !important;
6680
+ transition: none !important;
6681
+ }
6682
+
6683
+ /* Hide Reveal.js controls */
6684
+ .reveal .controls,
6685
+ .reveal .progress,
6686
+ .reveal .slide-number,
6687
+ .reveal .speaker-notes,
6688
+ .reveal .navigate-left,
6689
+ .reveal .navigate-right,
6690
+ .reveal .navigate-up,
6691
+ .reveal .navigate-down {
6692
+ display: none !important;
6693
+ }
6694
+ }
6695
+
6696
+ /* Force print mode in Puppeteer */
6697
+ .reveal .slides section {
6698
+ page-break-after: always;
6699
+ page-break-inside: avoid;
6700
+ }
6701
+ </style>
6702
+ `;
6703
+ if (html.includes("</head>")) {
6704
+ return html.replace("</head>", `${printCss}</head>`);
6705
+ } else {
6706
+ return printCss + html;
6707
+ }
6708
+ }
6709
+ };
6710
+ function createPDFGenerator() {
6711
+ return new PDFGenerator();
6712
+ }
6713
+
4236
6714
  // src/core/PresentationEngine.ts
4237
6715
  var PresentationEngine = class {
4238
6716
  contentAnalyzer;
4239
- slideFactory;
4240
6717
  templateEngine;
4241
6718
  scoreCalculator;
4242
6719
  qaEngine;
4243
6720
  htmlGenerator;
4244
6721
  pptxGenerator;
6722
+ pdfGenerator;
6723
+ kb;
4245
6724
  constructor() {
4246
6725
  this.contentAnalyzer = new ContentAnalyzer();
4247
- this.slideFactory = new SlideFactory();
4248
6726
  this.templateEngine = new TemplateEngine();
4249
6727
  this.scoreCalculator = new ScoreCalculator();
4250
6728
  this.qaEngine = new QAEngine();
4251
6729
  this.htmlGenerator = new RevealJsGenerator();
4252
6730
  this.pptxGenerator = new PowerPointGenerator();
6731
+ this.pdfGenerator = createPDFGenerator();
4253
6732
  }
4254
6733
  /**
4255
6734
  * Generate a presentation from content.
@@ -4259,21 +6738,26 @@ var PresentationEngine = class {
4259
6738
  */
4260
6739
  async generate(config) {
4261
6740
  this.validateConfig(config);
4262
- console.log("\u{1F4DD} Analyzing content...");
6741
+ logger.progress("\u{1F4DA} Loading knowledge base...");
6742
+ this.kb = await getKnowledgeGateway();
6743
+ logger.progress("\u{1F4DD} Analyzing content...");
4263
6744
  const analysis = await this.contentAnalyzer.analyze(config.content, config.contentType);
4264
- console.log("\u{1F3A8} Creating slides...");
4265
- const slides = await this.slideFactory.createSlides(analysis, config.mode);
4266
- console.log("\u2705 Validating structure...");
6745
+ const presentationType = config.presentationType || analysis.detectedType;
6746
+ logger.step(`Presentation type: ${presentationType}`);
6747
+ const slideFactory = createSlideFactory(this.kb, presentationType);
6748
+ logger.progress("\u{1F3A8} Creating slides...");
6749
+ const slides = await slideFactory.createSlides(analysis);
6750
+ logger.progress("\u2705 Validating structure...");
4267
6751
  const structureErrors = this.validateStructure(slides, config.mode);
4268
6752
  if (structureErrors.length > 0) {
4269
6753
  if (config.skipQA) {
4270
- console.log("\u26A0\uFE0F Structure warnings (bypassed):");
4271
- structureErrors.forEach((e) => console.log(` \u2022 ${e}`));
6754
+ logger.warn("Structure warnings (bypassed):");
6755
+ structureErrors.forEach((e) => logger.warn(` \u2022 ${e}`));
4272
6756
  } else {
4273
6757
  throw new ValidationError(structureErrors, "Slide structure validation failed");
4274
6758
  }
4275
6759
  }
4276
- console.log("\u{1F528} Generating outputs...");
6760
+ logger.progress("\u{1F528} Generating outputs...");
4277
6761
  const outputs = {};
4278
6762
  if (config.format.includes("html")) {
4279
6763
  outputs.html = await this.htmlGenerator.generate(slides, config);
@@ -4283,23 +6767,62 @@ var PresentationEngine = class {
4283
6767
  }
4284
6768
  let qaResults;
4285
6769
  let score = 100;
6770
+ let iterativeResult = null;
6771
+ let finalSlides = slides;
6772
+ let finalHtml = outputs.html;
4286
6773
  if (!config.skipQA && outputs.html) {
4287
- console.log("\u{1F50D} Running QA validation...");
4288
- qaResults = await this.qaEngine.validate(outputs.html, {
4289
- mode: config.mode,
4290
- strictMode: true
4291
- });
4292
- score = this.scoreCalculator.calculate(qaResults);
4293
- console.log(`\u{1F4CA} QA Score: ${score}/100`);
4294
6774
  const threshold = config.qaThreshold ?? 95;
4295
- if (score < threshold) {
4296
- throw new QAFailureError(score, threshold, qaResults);
6775
+ const maxIterations = config.maxIterations ?? 5;
6776
+ const useIterativeQA = config.useIterativeQA !== false;
6777
+ if (useIterativeQA) {
6778
+ logger.progress("\u{1F50D} Running Iterative QA (7-Dimension Scoring)...");
6779
+ const iterativeEngine = createIterativeQAEngine(
6780
+ config.mode,
6781
+ presentationType,
6782
+ config
6783
+ );
6784
+ iterativeResult = await iterativeEngine.run(slides, outputs.html, {
6785
+ minScore: threshold,
6786
+ maxIterations,
6787
+ verbose: true
6788
+ });
6789
+ score = iterativeResult.finalScore;
6790
+ finalSlides = iterativeResult.slides;
6791
+ finalHtml = iterativeResult.html;
6792
+ if (outputs.html) {
6793
+ outputs.html = finalHtml;
6794
+ }
6795
+ qaResults = this.buildQAResultsFrom7Dimension(iterativeResult);
6796
+ if (!iterativeResult.passed) {
6797
+ throw new QAFailureError(score, threshold, qaResults);
6798
+ }
6799
+ } else {
6800
+ logger.progress("\u{1F50D} Running QA validation (legacy mode)...");
6801
+ qaResults = await this.qaEngine.validate(outputs.html, {
6802
+ mode: config.mode,
6803
+ strictMode: true
6804
+ });
6805
+ score = this.scoreCalculator.calculate(qaResults);
6806
+ logger.info(`\u{1F4CA} QA Score: ${score}/100`);
6807
+ if (score < threshold) {
6808
+ throw new QAFailureError(score, threshold, qaResults);
6809
+ }
4297
6810
  }
4298
6811
  } else {
4299
6812
  qaResults = this.qaEngine.createEmptyResults();
4300
- console.log("\u26A0\uFE0F QA validation skipped (NOT RECOMMENDED)");
6813
+ logger.warn("QA validation skipped (NOT RECOMMENDED)");
6814
+ }
6815
+ if (outputs.html && (config.format.includes("pdf") || config.generatePdf !== false)) {
6816
+ try {
6817
+ logger.progress("\u{1F4C4} Generating PDF from validated HTML...");
6818
+ outputs.pdf = await this.pdfGenerator.generate(outputs.html);
6819
+ logger.step("PDF generated successfully");
6820
+ } catch (pdfError) {
6821
+ const errorMsg = pdfError instanceof Error ? pdfError.message : String(pdfError);
6822
+ logger.warn(`PDF generation failed (non-critical): ${errorMsg}`);
6823
+ }
4301
6824
  }
4302
- const metadata = this.buildMetadata(config, analysis, slides);
6825
+ const metadata = this.buildMetadata(config, analysis, finalSlides, iterativeResult);
4303
6826
  return {
4304
6827
  outputs,
4305
6828
  qaResults,
@@ -4307,6 +6830,46 @@ var PresentationEngine = class {
4307
6830
  metadata
4308
6831
  };
4309
6832
  }
6833
+ /**
6834
+ * Build QA results structure from 7-dimension scoring.
6835
+ */
6836
+ buildQAResultsFrom7Dimension(iterativeResult) {
6837
+ const scoring = iterativeResult.finalScoring;
6838
+ return {
6839
+ passed: scoring.passed,
6840
+ score: scoring.overallScore,
6841
+ visual: {
6842
+ whitespacePercentage: scoring.dimensions.layout.score,
6843
+ layoutBalance: scoring.dimensions.layout.score / 100,
6844
+ colorContrast: scoring.dimensions.contrast.score / 100
6845
+ },
6846
+ content: {
6847
+ perSlide: [],
6848
+ issues: scoring.issues.filter((i) => i.dimension === "content")
6849
+ },
6850
+ accessibility: {
6851
+ wcagLevel: scoring.dimensions.contrast.score >= 95 ? "AAA" : "AA",
6852
+ issues: scoring.issues.filter((i) => i.dimension === "contrast")
6853
+ },
6854
+ issues: scoring.issues.map((issue) => ({
6855
+ severity: issue.severity,
6856
+ message: issue.message,
6857
+ slideIndex: issue.slideIndex,
6858
+ dimension: issue.dimension
6859
+ })),
6860
+ dimensions: {
6861
+ layout: scoring.dimensions.layout.score,
6862
+ contrast: scoring.dimensions.contrast.score,
6863
+ graphics: scoring.dimensions.graphics.score,
6864
+ content: scoring.dimensions.content.score,
6865
+ clarity: scoring.dimensions.clarity.score,
6866
+ effectiveness: scoring.dimensions.effectiveness.score,
6867
+ consistency: scoring.dimensions.consistency.score
6868
+ },
6869
+ iterations: iterativeResult.iterations,
6870
+ report: iterativeResult.report
6871
+ };
6872
+ }
4310
6873
  /**
4311
6874
  * Validate presentation configuration.
4312
6875
  */
@@ -4345,15 +6908,28 @@ var PresentationEngine = class {
4345
6908
  if (slides.length < 3) {
4346
6909
  errors.push("Presentation must have at least 3 slides");
4347
6910
  }
6911
+ const sparseByDesignTypes = [
6912
+ "title",
6913
+ "section-divider",
6914
+ "thank-you",
6915
+ "big-number",
6916
+ "single-statement",
6917
+ "cta",
6918
+ "agenda",
6919
+ "metrics-grid",
6920
+ "quote",
6921
+ "image-focus"
6922
+ ];
4348
6923
  slides.forEach((slide, index) => {
4349
6924
  const wordCount = this.countWords(slide);
4350
6925
  if (mode === "keynote") {
4351
- if (wordCount > 25) {
6926
+ if (wordCount > 25 && !sparseByDesignTypes.includes(slide.type)) {
4352
6927
  errors.push(`Slide ${index + 1}: ${wordCount} words exceeds keynote limit of 25`);
4353
6928
  }
4354
6929
  } else {
4355
- if (wordCount < 20 && !["title", "section-divider", "thank-you"].includes(slide.type)) {
4356
- errors.push(`Slide ${index + 1}: ${wordCount} words may be too sparse for business mode`);
6930
+ const requiresMinWords = ["bullet-points", "two-column", "process-flow"];
6931
+ if (wordCount < 20 && requiresMinWords.includes(slide.type)) {
6932
+ logger.warn(`Slide ${index + 1}: ${wordCount} words is sparse for ${slide.type} slide`);
4357
6933
  }
4358
6934
  if (wordCount > 100) {
4359
6935
  errors.push(`Slide ${index + 1}: ${wordCount} words exceeds business limit of 100`);
@@ -4377,13 +6953,13 @@ var PresentationEngine = class {
4377
6953
  /**
4378
6954
  * Build presentation metadata.
4379
6955
  */
4380
- buildMetadata(config, analysis, slides) {
6956
+ buildMetadata(config, analysis, slides, iterativeResult) {
4381
6957
  const wordCounts = slides.map((s) => this.countWords(s));
4382
6958
  const totalWords = wordCounts.reduce((sum, count) => sum + count, 0);
4383
6959
  const avgWordsPerSlide = Math.round(totalWords / slides.length);
4384
6960
  const minutesPerSlide = config.mode === "keynote" ? 1.5 : 2;
4385
6961
  const estimatedDuration = Math.round(slides.length * minutesPerSlide);
4386
- return {
6962
+ const metadata = {
4387
6963
  title: config.title,
4388
6964
  author: config.author ?? "Unknown",
4389
6965
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4392,8 +6968,15 @@ var PresentationEngine = class {
4392
6968
  wordCount: totalWords,
4393
6969
  avgWordsPerSlide,
4394
6970
  estimatedDuration,
4395
- frameworks: this.detectFrameworks(analysis)
6971
+ frameworks: this.detectFrameworks(analysis),
6972
+ presentationType: config.presentationType || analysis.detectedType
4396
6973
  };
6974
+ if (iterativeResult) {
6975
+ metadata.qaIterations = iterativeResult.totalIterations;
6976
+ metadata.qaMaxIterations = iterativeResult.maxIterations;
6977
+ metadata.dimensionScores = iterativeResult.finalScoring.dimensions;
6978
+ }
6979
+ return metadata;
4397
6980
  }
4398
6981
  /**
4399
6982
  * Detect which expert frameworks were applied.
@@ -4416,6 +6999,95 @@ var PresentationEngine = class {
4416
6999
  }
4417
7000
  };
4418
7001
 
7002
+ // src/core/SlideGenerator.ts
7003
+ var SlideGenerator = class {
7004
+ kb;
7005
+ factory;
7006
+ mode = "keynote";
7007
+ presentationType = "ted_keynote";
7008
+ async initialize() {
7009
+ this.kb = await getKnowledgeGateway();
7010
+ }
7011
+ /**
7012
+ * Generate slides from analyzed content using KB-driven SlideFactory.
7013
+ *
7014
+ * @version 7.0.0 - Now delegates ALL slide creation to SlideFactory
7015
+ * SlideFactory queries KB for every decision:
7016
+ * - Allowed slide types per presentation type
7017
+ * - Word limits per type
7018
+ * - Bullet limits per type
7019
+ * - Content pattern to slide type mapping
7020
+ * - Slide validation against KB rules
7021
+ */
7022
+ async generate(analysis, type) {
7023
+ await this.initialize();
7024
+ this.presentationType = type || analysis.detectedType;
7025
+ this.mode = this.kb.getModeForType(this.presentationType);
7026
+ logger.step(`Using ${this.mode} mode for ${this.presentationType}`);
7027
+ this.factory = createSlideFactory(this.kb, this.presentationType);
7028
+ const slides = await this.factory.createSlides(analysis);
7029
+ const legacySlides = this.convertToLegacyFormat(slides);
7030
+ logger.step(`Generated ${legacySlides.length} KB-validated slides`);
7031
+ return legacySlides;
7032
+ }
7033
+ /**
7034
+ * Convert new Slide format to legacy format for backwards compatibility
7035
+ */
7036
+ convertToLegacyFormat(slides) {
7037
+ return slides.map((slide) => ({
7038
+ index: slide.index,
7039
+ type: slide.type,
7040
+ title: slide.data.title || "",
7041
+ content: {
7042
+ subtitle: slide.data.subtitle,
7043
+ statement: slide.data.body,
7044
+ body: slide.data.body,
7045
+ bullets: slide.data.bullets,
7046
+ metrics: slide.data.metrics,
7047
+ quote: slide.data.quote ? { text: slide.data.quote, attribution: slide.data.attribution } : void 0,
7048
+ callToAction: slide.data.keyMessage
7049
+ },
7050
+ notes: slide.notes,
7051
+ template: this.mapTypeToTemplate(slide.type)
7052
+ }));
7053
+ }
7054
+ /**
7055
+ * Map slide type to template name for backwards compatibility
7056
+ */
7057
+ mapTypeToTemplate(type) {
7058
+ const templateMap = {
7059
+ "title": "Title Impact",
7060
+ "agenda": "Detailed Findings",
7061
+ "big-number": "Data Insight",
7062
+ "metrics-grid": "Data Insight",
7063
+ "bullet-points": "Detailed Findings",
7064
+ "two-column": "Two Column",
7065
+ "three-column": "Three Column",
7066
+ "comparison": "Comparison",
7067
+ "timeline": "Process Timeline",
7068
+ "process": "Process",
7069
+ "quote": "Quote",
7070
+ "single-statement": "Single Statement",
7071
+ "cta": "Call to Action",
7072
+ "thank-you": "Title Impact"
7073
+ };
7074
+ return templateMap[type] || "Detailed Findings";
7075
+ }
7076
+ // =========================================================================
7077
+ // All slide creation is now handled by SlideFactory
7078
+ // Old methods removed in v7.0.0 - KB-driven SlideFactory handles:
7079
+ // - createTitleSlide, createAgendaSlide, createSituationSlide, etc.
7080
+ // - Content pattern classification
7081
+ // - KB-based slide type selection
7082
+ // - Text truncation, cleaning, deduplication
7083
+ // =========================================================================
7084
+ };
7085
+ async function initSlideGenerator() {
7086
+ const generator = new SlideGenerator();
7087
+ await generator.initialize();
7088
+ return generator;
7089
+ }
7090
+
4419
7091
  // src/media/ImageProvider.ts
4420
7092
  var LocalImageProvider = class {
4421
7093
  name = "local";
@@ -4604,7 +7276,7 @@ async function validate(presentation, options) {
4604
7276
  score
4605
7277
  };
4606
7278
  }
4607
- var VERSION = "6.0.0";
7279
+ var VERSION = "7.1.0";
4608
7280
  var index_default = {
4609
7281
  generate,
4610
7282
  validate,
@@ -4614,10 +7286,13 @@ var index_default = {
4614
7286
  };
4615
7287
  // Annotate the CommonJS export names for ESM import in node:
4616
7288
  0 && (module.exports = {
7289
+ AutoFixEngine,
4617
7290
  ChartJsProvider,
4618
7291
  CompositeChartProvider,
4619
7292
  CompositeImageProvider,
4620
7293
  ContentAnalyzer,
7294
+ ContentPatternClassifier,
7295
+ IterativeQAEngine,
4621
7296
  KnowledgeGateway,
4622
7297
  LocalImageProvider,
4623
7298
  MermaidProvider,
@@ -4629,16 +7304,22 @@ var index_default = {
4629
7304
  QuickChartProvider,
4630
7305
  RevealJsGenerator,
4631
7306
  ScoreCalculator,
7307
+ SevenDimensionScorer,
4632
7308
  SlideFactory,
7309
+ SlideGenerator,
4633
7310
  TemplateEngine,
4634
7311
  TemplateNotFoundError,
4635
7312
  UnsplashImageProvider,
4636
7313
  VERSION,
4637
7314
  ValidationError,
7315
+ createAutoFixEngine,
4638
7316
  createDefaultChartProvider,
4639
7317
  createDefaultImageProvider,
7318
+ createIterativeQAEngine,
7319
+ createSlideFactory,
4640
7320
  generate,
4641
7321
  getKnowledgeGateway,
7322
+ initSlideGenerator,
4642
7323
  validate
4643
7324
  });
4644
7325
  /**