claude-presentation-master 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  CompositeImageProvider: () => CompositeImageProvider,
36
36
  ContentAnalyzer: () => ContentAnalyzer,
37
37
  ContentPatternClassifier: () => ContentPatternClassifier,
38
+ KnowledgeBaseError: () => KnowledgeBaseError,
38
39
  KnowledgeGateway: () => KnowledgeGateway,
39
40
  LocalImageProvider: () => LocalImageProvider,
40
41
  MermaidProvider: () => MermaidProvider,
@@ -67,32 +68,100 @@ __export(index_exports, {
67
68
  module.exports = __toCommonJS(index_exports);
68
69
 
69
70
  // src/types/index.ts
70
- var ValidationError = class extends Error {
71
+ var ValidationError = class _ValidationError extends Error {
71
72
  constructor(errors, message = "Validation failed") {
72
- super(message);
73
+ const errorList = errors.map((e) => ` \u2022 ${e}`).join("\n");
74
+ const suggestions = _ValidationError.getSuggestions(errors);
75
+ const suggestionList = suggestions.length > 0 ? "\n\nHow to fix:\n" + suggestions.map((s) => ` \u2192 ${s}`).join("\n") : "";
76
+ super(`${message}
77
+
78
+ Issues found:
79
+ ${errorList}${suggestionList}`);
73
80
  this.errors = errors;
74
81
  this.name = "ValidationError";
82
+ this.suggestions = suggestions;
83
+ }
84
+ suggestions;
85
+ static getSuggestions(errors) {
86
+ const suggestions = [];
87
+ for (const error of errors) {
88
+ if (error.includes("Content is required")) {
89
+ suggestions.push("Provide markdown content with at least one ## section header");
90
+ }
91
+ if (error.includes("Mode must be")) {
92
+ suggestions.push('Use mode: "keynote" for TED-style or mode: "business" for corporate');
93
+ }
94
+ if (error.includes("Title is required")) {
95
+ suggestions.push('Add a title: "Your Presentation Title" to your config');
96
+ }
97
+ if (error.includes("output format")) {
98
+ suggestions.push('Specify format: ["html"] or format: ["html", "pdf"]');
99
+ }
100
+ }
101
+ return [...new Set(suggestions)];
75
102
  }
76
103
  };
77
- var QAFailureError = class extends Error {
78
- constructor(score, threshold, qaResults, message = `QA score ${score} below threshold ${threshold}`) {
79
- super(message);
104
+ var QAFailureError = class _QAFailureError extends Error {
105
+ constructor(score, threshold, qaResults, message) {
106
+ const topIssues = _QAFailureError.getTopIssues(qaResults);
107
+ const issueList = topIssues.map((i) => ` \u2022 ${i}`).join("\n");
108
+ const helpText = `
109
+
110
+ Score: ${score}/100 (threshold: ${threshold})
111
+
112
+ Top issues to fix:
113
+ ${issueList}
114
+
115
+ How to improve:
116
+ \u2192 Ensure each slide has meaningful content (not just a title)
117
+ \u2192 Keep text concise - aim for <40 words per slide
118
+ \u2192 Use timeline/process slides for step-by-step content
119
+ \u2192 Add visuals for data-heavy slides`;
120
+ super(message || `QA score ${score} below threshold ${threshold}${helpText}`);
80
121
  this.score = score;
81
122
  this.threshold = threshold;
82
123
  this.qaResults = qaResults;
83
124
  this.name = "QAFailureError";
125
+ this.topIssues = topIssues;
84
126
  }
127
+ topIssues;
85
128
  getIssues() {
86
129
  return this.qaResults.issues.map((issue) => issue.message);
87
130
  }
131
+ static getTopIssues(qaResults) {
132
+ const issues = [];
133
+ if (qaResults.slideScores) {
134
+ for (const slide of qaResults.slideScores) {
135
+ if (slide.criticalIssues) {
136
+ issues.push(...slide.criticalIssues);
137
+ }
138
+ }
139
+ }
140
+ if (qaResults.issues) {
141
+ issues.push(...qaResults.issues.map((i) => i.message));
142
+ }
143
+ return [...new Set(issues)].slice(0, 5);
144
+ }
88
145
  };
89
146
  var TemplateNotFoundError = class extends Error {
90
- constructor(templatePath, message = `Template not found: ${templatePath}`) {
91
- super(message);
147
+ constructor(templatePath, message) {
148
+ super(message || `Template not found: ${templatePath}
149
+
150
+ Available templates: title, content, timeline, process, metrics, quote, cta, thank-you`);
92
151
  this.templatePath = templatePath;
93
152
  this.name = "TemplateNotFoundError";
94
153
  }
95
154
  };
155
+ var KnowledgeBaseError = class extends Error {
156
+ constructor(field, context, message) {
157
+ super(message || `Knowledge base configuration error: Missing "${field}" in ${context}
158
+
159
+ Check assets/presentation-knowledge.yaml for the correct structure.`);
160
+ this.field = field;
161
+ this.context = context;
162
+ this.name = "KnowledgeBaseError";
163
+ }
164
+ };
96
165
 
97
166
  // src/core/PresentationEngine.ts
98
167
  var fs2 = __toESM(require("fs"));
@@ -102,7 +171,6 @@ var os = __toESM(require("os"));
102
171
  // src/kb/KnowledgeGateway.ts
103
172
  var import_fs = require("fs");
104
173
  var import_path = require("path");
105
- var import_url = require("url");
106
174
  var yaml = __toESM(require("yaml"));
107
175
 
108
176
  // src/utils/Logger.ts
@@ -173,11 +241,7 @@ var logger = new Logger({
173
241
  });
174
242
 
175
243
  // src/kb/KnowledgeGateway.ts
176
- var import_meta = {};
177
244
  function getModuleDir() {
178
- if (typeof import_meta !== "undefined" && import_meta.url) {
179
- return (0, import_path.dirname)((0, import_url.fileURLToPath)(import_meta.url));
180
- }
181
245
  if (typeof __dirname !== "undefined") {
182
246
  return __dirname;
183
247
  }
@@ -847,7 +911,8 @@ var KnowledgeGateway = class {
847
911
  // Subtitle needs more room for complete phrases - at least 10 words for keynote
848
912
  subtitle: { maxWords: mode === "keynote" ? 12 : Math.min(20, Math.floor(maxWords / 2)) },
849
913
  context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
850
- step: { maxWords: Math.min(20, Math.floor(maxWords / 4)) },
914
+ // Step descriptions need minimum 8 words for meaningful content
915
+ step: { maxWords: Math.max(8, Math.min(20, Math.floor(maxWords / 3))) },
851
916
  columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
852
917
  };
853
918
  }
@@ -2866,25 +2931,43 @@ var SlideFactory = class {
2866
2931
  createComparisonSlide(index, section) {
2867
2932
  const comparison = this.classifier.extractComparison(section);
2868
2933
  const labels = this.config.defaults.comparison;
2934
+ const title = this.createTitle(section.header, section);
2935
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
2869
2936
  const leftFallback = labels.optionLabels[0] ?? "Option A";
2870
2937
  const rightFallback = labels.optionLabels[1] ?? "Option B";
2871
- const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
2872
- const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
2938
+ let leftColumn = comparison?.left || section.bullets[0] || "";
2939
+ let rightColumn = comparison?.right || section.bullets[1] || "";
2940
+ const normalizedLeft = leftColumn.toLowerCase().replace(/[^a-z0-9]/g, "");
2941
+ const normalizedRight = rightColumn.toLowerCase().replace(/[^a-z0-9]/g, "");
2942
+ const leftIsDuplicate = normalizedLeft === normalizedTitle || normalizedTitle.includes(normalizedLeft);
2943
+ const rightIsDuplicate = normalizedRight === normalizedTitle || normalizedTitle.includes(normalizedRight);
2944
+ if (leftIsDuplicate || rightIsDuplicate || !leftColumn && !rightColumn) {
2945
+ if (section.bullets.length >= 2) {
2946
+ logger.warn(`Comparison slide "${title}" has duplicate content, using bullet points instead`);
2947
+ return this.createBulletSlide(index, section);
2948
+ }
2949
+ if (leftIsDuplicate && rightIsDuplicate) {
2950
+ logger.warn(`Skipping comparison slide "${title}" - content duplicates title`);
2951
+ return null;
2952
+ }
2953
+ if (leftIsDuplicate) leftColumn = leftFallback;
2954
+ if (rightIsDuplicate) rightColumn = rightFallback;
2955
+ }
2873
2956
  return {
2874
2957
  index,
2875
2958
  type: "comparison",
2876
2959
  data: {
2877
- title: this.createTitle(section.header, section),
2960
+ title,
2878
2961
  columns: [
2879
2962
  {
2880
2963
  title: labels.leftLabel,
2881
2964
  // FROM KB - not hardcoded 'Before'
2882
- content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
2965
+ content: this.truncateText(leftColumn || leftFallback, this.config.rules.wordsPerSlide.max)
2883
2966
  },
2884
2967
  {
2885
2968
  title: labels.rightLabel,
2886
2969
  // FROM KB - not hardcoded 'After'
2887
- content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
2970
+ content: this.truncateText(rightColumn || rightFallback, this.config.rules.wordsPerSlide.max)
2888
2971
  }
2889
2972
  ]
2890
2973
  },
@@ -2895,9 +2978,8 @@ var SlideFactory = class {
2895
2978
  const steps = this.classifier.extractSteps(section);
2896
2979
  const maxSteps = Math.min(
2897
2980
  steps.length,
2898
- this.config.rules.bulletsPerSlide.max,
2899
2981
  this.config.millersLaw.maxItems
2900
- // FROM KB - 7±2 rule
2982
+ // FROM KB - 7±2 rule (max 9)
2901
2983
  );
2902
2984
  return {
2903
2985
  index,
@@ -2919,7 +3001,7 @@ var SlideFactory = class {
2919
3001
  }
2920
3002
  createProcessSlide(index, section) {
2921
3003
  const steps = this.classifier.extractSteps(section);
2922
- const maxSteps = Math.min(steps.length, this.config.rules.bulletsPerSlide.max);
3004
+ const maxSteps = Math.min(steps.length, this.config.millersLaw.maxItems);
2923
3005
  return {
2924
3006
  index,
2925
3007
  type: "process",
@@ -3156,10 +3238,21 @@ var SlideFactory = class {
3156
3238
  * Used for section headers with strong conclusions.
3157
3239
  */
3158
3240
  createTitleImpactSlide(index, section) {
3241
+ const title = this.cleanText(section.header);
3159
3242
  const supportingText = section.content || section.bullets.slice(0, 2).join(". ");
3160
3243
  const truncatedSupport = this.truncateText(supportingText, this.config.defaults.context.maxWords);
3244
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
3245
+ const normalizedBody = truncatedSupport.toLowerCase().replace(/[^a-z0-9]/g, "");
3246
+ const bodyIsDuplicate = normalizedBody === normalizedTitle || normalizedTitle.includes(normalizedBody) || normalizedBody.includes(normalizedTitle) || truncatedSupport.length < 10;
3247
+ if (bodyIsDuplicate) {
3248
+ if (section.bullets.length >= 2 && this.config.mode === "business") {
3249
+ return this.createBulletSlide(index, section);
3250
+ }
3251
+ logger.warn(`Skipping title-impact slide "${title}" - no distinct body content`);
3252
+ return null;
3253
+ }
3161
3254
  const data = {
3162
- title: this.cleanText(section.header)
3255
+ title
3163
3256
  };
3164
3257
  if (truncatedSupport) {
3165
3258
  data.body = truncatedSupport;
@@ -5326,169 +5419,422 @@ var VisualQualityEvaluator = class {
5326
5419
  const title = currentSlide.querySelector("h1, h2, .title")?.textContent?.trim() || "";
5327
5420
  const body = currentSlide.querySelector(".body, p:not(.subtitle)")?.textContent?.trim() || "";
5328
5421
  const bullets = Array.from(currentSlide.querySelectorAll("li")).map((li) => li.textContent?.trim() || "");
5422
+ const hasSteps = !!currentSlide.querySelector(".steps, .process-steps, .timeline");
5423
+ const steps = Array.from(currentSlide.querySelectorAll(".step, .process-step, .timeline-item")).map(
5424
+ (s) => s.textContent?.trim() || ""
5425
+ );
5426
+ const hasMetrics = !!currentSlide.querySelector(".metrics, .metric");
5329
5427
  const hasImage = !!currentSlide.querySelector("img");
5330
5428
  const hasChart = !!currentSlide.querySelector(".chart, svg, canvas");
5331
5429
  const classList = Array.from(currentSlide.classList);
5332
5430
  const backgroundColor = window.getComputedStyle(currentSlide).backgroundColor;
5333
5431
  const titleEl = currentSlide.querySelector("h1, h2, .title");
5334
5432
  const titleStyles = titleEl ? window.getComputedStyle(titleEl) : null;
5433
+ const truncatedElements = [];
5434
+ const contentElements = currentSlide.querySelectorAll("h1, h2, h3, p, span, li, .body, .step-desc, .step-title, .timeline-content");
5435
+ contentElements.forEach((el, idx) => {
5436
+ const styles = window.getComputedStyle(el);
5437
+ const text = el.textContent?.trim() || "";
5438
+ if (text.length < 15) return;
5439
+ const isLayoutContainer = el.classList?.contains("slide-content") || el.classList?.contains("steps") || el.classList?.contains("timeline") || el.classList?.contains("process-steps");
5440
+ if (isLayoutContainer) return;
5441
+ if (styles.textOverflow === "ellipsis") {
5442
+ if (el.scrollWidth > el.clientWidth + 5) {
5443
+ truncatedElements.push(`Element ${idx}: "${text.substring(0, 30)}..." is truncated horizontally`);
5444
+ }
5445
+ }
5446
+ const scrollHeight = el.scrollHeight;
5447
+ const clientHeight = el.clientHeight;
5448
+ if (scrollHeight > clientHeight + 20) {
5449
+ const overflow = styles.overflow || styles.overflowY;
5450
+ if (overflow === "hidden" || overflow === "clip") {
5451
+ truncatedElements.push(`Element ${idx}: "${text.substring(0, 30)}..." is truncated vertically`);
5452
+ }
5453
+ }
5454
+ });
5455
+ const allVisibleText = Array.from(currentSlide.querySelectorAll("*")).map((el) => el.textContent?.trim() || "").join(" ").trim();
5456
+ const isEmptySlide = allVisibleText.length < 10 && !hasImage && !hasChart;
5457
+ const hasOnlyTitle = title.length > 0 && body.length === 0 && bullets.length === 0 && steps.length === 0 && !hasSteps && !hasMetrics && !hasImage && !hasChart;
5458
+ const titleLower = title.toLowerCase();
5459
+ const bodyLower = body.toLowerCase();
5460
+ const isRedundant = titleLower.length > 10 && bodyLower.length > 10 && (titleLower.includes(bodyLower) || bodyLower.includes(titleLower));
5461
+ const contrastIssues = [];
5462
+ const parseRGB = (color) => {
5463
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
5464
+ if (match && match[1] !== void 0 && match[2] !== void 0 && match[3] !== void 0) {
5465
+ return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
5466
+ }
5467
+ return null;
5468
+ };
5469
+ const getLuminance = (rgb) => {
5470
+ const values = [rgb.r, rgb.g, rgb.b].map((c) => {
5471
+ c = c / 255;
5472
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
5473
+ });
5474
+ return 0.2126 * (values[0] ?? 0) + 0.7152 * (values[1] ?? 0) + 0.0722 * (values[2] ?? 0);
5475
+ };
5476
+ const getContrastRatio = (fg, bg) => {
5477
+ const l1 = getLuminance(fg);
5478
+ const l2 = getLuminance(bg);
5479
+ const lighter = Math.max(l1, l2);
5480
+ const darker = Math.min(l1, l2);
5481
+ return (lighter + 0.05) / (darker + 0.05);
5482
+ };
5483
+ const bgRGB = parseRGB(backgroundColor);
5484
+ if (titleEl && bgRGB) {
5485
+ const titleRGB = parseRGB(titleStyles?.color || "");
5486
+ if (titleRGB) {
5487
+ const contrast = getContrastRatio(titleRGB, bgRGB);
5488
+ if (contrast < 4.5) {
5489
+ contrastIssues.push(`Title contrast ${contrast.toFixed(1)}:1 (need 4.5:1)`);
5490
+ }
5491
+ }
5492
+ }
5493
+ const bodyEl = currentSlide.querySelector(".body, p:not(.subtitle)");
5494
+ if (bodyEl && bgRGB) {
5495
+ const bodyStyles2 = window.getComputedStyle(bodyEl);
5496
+ const bodyRGB = parseRGB(bodyStyles2.color);
5497
+ if (bodyRGB) {
5498
+ const contrast = getContrastRatio(bodyRGB, bgRGB);
5499
+ if (contrast < 4.5) {
5500
+ contrastIssues.push(`Body contrast ${contrast.toFixed(1)}:1 (need 4.5:1)`);
5501
+ }
5502
+ }
5503
+ }
5504
+ const hasContrastIssues = contrastIssues.length > 0;
5505
+ const layoutIssues = [];
5506
+ const slideRect = currentSlide.getBoundingClientRect();
5507
+ const layoutElements = currentSlide.querySelectorAll("h1, h2, h3, p, ul, ol, .number, .metric, img, canvas, svg");
5508
+ let topHeavy = 0;
5509
+ let bottomHeavy = 0;
5510
+ let leftHeavy = 0;
5511
+ let rightHeavy = 0;
5512
+ let centerY = slideRect.height / 2;
5513
+ let centerX = slideRect.width / 2;
5514
+ layoutElements.forEach((el) => {
5515
+ const rect = el.getBoundingClientRect();
5516
+ const elCenterY = rect.top + rect.height / 2 - slideRect.top;
5517
+ const elCenterX = rect.left + rect.width / 2 - slideRect.left;
5518
+ if (elCenterY < centerY) topHeavy++;
5519
+ else bottomHeavy++;
5520
+ if (elCenterX < centerX) leftHeavy++;
5521
+ else rightHeavy++;
5522
+ });
5523
+ const verticalBalance = Math.abs(topHeavy - bottomHeavy) / Math.max(1, topHeavy + bottomHeavy);
5524
+ const horizontalBalance = Math.abs(leftHeavy - rightHeavy) / Math.max(1, leftHeavy + rightHeavy);
5525
+ if (verticalBalance > 0.6 && contentElements.length > 2) {
5526
+ layoutIssues.push(`Vertical imbalance: ${topHeavy} top vs ${bottomHeavy} bottom`);
5527
+ }
5528
+ if (horizontalBalance > 0.6 && contentElements.length > 2) {
5529
+ layoutIssues.push(`Horizontal imbalance: ${leftHeavy} left vs ${rightHeavy} right`);
5530
+ }
5531
+ const hasLayoutIssues = layoutIssues.length > 0;
5532
+ const typographyIssues = [];
5533
+ const titleSize = parseFloat(titleStyles?.fontSize || "0");
5534
+ const bodyStyles = bodyEl ? window.getComputedStyle(bodyEl) : null;
5535
+ const bodySize = parseFloat(bodyStyles?.fontSize || "0");
5536
+ if (titleSize > 0 && bodySize > 0 && titleSize <= bodySize) {
5537
+ typographyIssues.push("Title not larger than body - hierarchy broken");
5538
+ }
5539
+ if (titleSize > 0 && bodySize > 0 && titleSize < bodySize * 1.3) {
5540
+ typographyIssues.push("Title not prominent enough vs body");
5541
+ }
5542
+ const hasTypographyIssues = typographyIssues.length > 0;
5543
+ const completenessIssues = [];
5544
+ const emptySteps = currentSlide.querySelectorAll(".steps:empty, .timeline:empty, .process-steps:empty, .steps > :empty");
5545
+ if (emptySteps.length > 0) {
5546
+ completenessIssues.push(`${emptySteps.length} empty step/timeline container(s) - content missing`);
5547
+ }
5548
+ if (body.includes("Lorem") || body.includes("placeholder") || body.includes("TODO")) {
5549
+ completenessIssues.push("Contains placeholder text");
5550
+ }
5551
+ const emptyBullets = bullets.filter((b) => b.length < 3).length;
5552
+ if (emptyBullets > 0 && bullets.length > 0) {
5553
+ completenessIssues.push(`${emptyBullets} empty/minimal bullets`);
5554
+ }
5555
+ const hasCompletenessIssues = completenessIssues.length > 0;
5556
+ const visualNeedsIssues = [];
5557
+ const visualRequiredTypes = ["data", "chart", "metrics", "comparison", "process", "timeline"];
5558
+ const visualRecommendedTypes = ["big-number", "quote", "testimonial"];
5559
+ const slideClasses = Array.from(currentSlide.classList).join(" ").toLowerCase();
5560
+ const needsVisual = visualRequiredTypes.some((t) => slideClasses.includes(t));
5561
+ const visualRecommended = visualRecommendedTypes.some((t) => slideClasses.includes(t));
5562
+ const contentText = (title + " " + body).toLowerCase();
5563
+ const dataIndicators = /\d+%|\$[\d,]+|\d+x|million|billion|percent|growth|increase|decrease|comparison|versus|vs\b|before|after/i;
5564
+ const hasDataContent = dataIndicators.test(contentText);
5565
+ const hasAnyVisual = hasImage || hasChart;
5566
+ if (needsVisual && !hasAnyVisual) {
5567
+ visualNeedsIssues.push("Data/process slide missing visual - needs chart, diagram, or image");
5568
+ } else if (hasDataContent && !hasAnyVisual && bullets.length === 0) {
5569
+ visualNeedsIssues.push("Content has numbers/data but no visualization - chart would help");
5570
+ } else if (visualRecommended && !hasAnyVisual) {
5571
+ visualNeedsIssues.push("Visual recommended for this slide type");
5572
+ }
5573
+ if (hasImage) {
5574
+ const img = currentSlide.querySelector("img");
5575
+ const imgSrc = img?.getAttribute("src") || "";
5576
+ if (imgSrc.includes("placeholder") || imgSrc.includes("picsum") || imgSrc.includes("via.placeholder")) {
5577
+ visualNeedsIssues.push("Placeholder image detected - needs real visual");
5578
+ }
5579
+ }
5580
+ const needsVisualButMissing = visualNeedsIssues.length > 0;
5581
+ const visualScore = hasAnyVisual ? 10 : needsVisual || hasDataContent ? 0 : 5;
5335
5582
  return {
5336
5583
  title,
5337
5584
  body,
5338
5585
  bullets,
5586
+ steps,
5587
+ // NEW: Timeline/process step content
5588
+ hasSteps,
5589
+ // NEW: Has .steps/.process-steps container
5590
+ hasMetrics,
5591
+ // NEW: Has metrics content
5339
5592
  hasImage,
5340
5593
  hasChart,
5341
5594
  classList,
5342
5595
  backgroundColor,
5343
5596
  titleFontSize: titleStyles?.fontSize || "",
5344
5597
  titleColor: titleStyles?.color || "",
5345
- contentLength: (title + body + bullets.join(" ")).length
5598
+ // Include steps in content length calculation
5599
+ contentLength: (title + body + bullets.join(" ") + steps.join(" ")).length,
5600
+ // Visual quality flags
5601
+ hasTruncatedText: truncatedElements.length > 0,
5602
+ truncatedElements,
5603
+ isEmptySlide,
5604
+ hasOnlyTitle,
5605
+ isRedundant,
5606
+ allVisibleTextLength: allVisibleText.length,
5607
+ // NEW: Expert quality checks
5608
+ hasContrastIssues,
5609
+ contrastIssues,
5610
+ hasLayoutIssues,
5611
+ layoutIssues,
5612
+ hasTypographyIssues,
5613
+ typographyIssues,
5614
+ hasCompletenessIssues,
5615
+ completenessIssues,
5616
+ // Visual needs analysis
5617
+ needsVisualButMissing,
5618
+ visualNeedsIssues,
5619
+ visualScore
5346
5620
  };
5347
5621
  });
5348
5622
  return this.scoreSlide(slideIndex, slideData, screenshotPath);
5349
5623
  }
5624
+ /**
5625
+ * EXPERT-LEVEL SLIDE SCORING
5626
+ *
5627
+ * This evaluates each slide like Nancy Duarte, Carmine Gallo, or a McKinsey partner would.
5628
+ * It's not about rules - it's about whether the slide WORKS.
5629
+ */
5350
5630
  scoreSlide(slideIndex, slideData, screenshotPath) {
5351
5631
  if (!slideData) {
5352
- return {
5353
- slideIndex,
5354
- slideType: "unknown",
5355
- visualImpact: 0,
5356
- visualImpactNotes: "Could not analyze slide",
5357
- contentClarity: 0,
5358
- contentClarityNotes: "Could not analyze slide",
5359
- professionalPolish: 0,
5360
- professionalPolishNotes: "Could not analyze slide",
5361
- themeCoherence: 0,
5362
- themeCoherenceNotes: "Could not analyze slide",
5363
- totalScore: 0,
5364
- screenshotPath
5365
- };
5632
+ return this.createFailedSlideScore(slideIndex, "unknown", "Could not analyze slide", screenshotPath);
5366
5633
  }
5367
5634
  const slideType = this.inferSlideType(slideData);
5368
- let visualImpact = 5;
5369
- const visualNotes = [];
5370
- if (slideData.hasImage) {
5371
- visualImpact += 2;
5372
- visualNotes.push("Has imagery");
5635
+ const criticalFailures = [];
5636
+ if (slideData.isEmptySlide) {
5637
+ criticalFailures.push("EMPTY SLIDE: No meaningful content - slide serves no purpose");
5373
5638
  }
5374
- if (slideData.hasChart) {
5375
- visualImpact += 2;
5376
- visualNotes.push("Has data visualization");
5639
+ if (slideData.hasOnlyTitle && !["title", "agenda", "thank-you", "cta"].includes(slideType)) {
5640
+ criticalFailures.push("INCOMPLETE: Slide has title but no body content - looks unfinished");
5377
5641
  }
5378
- const highImpactTypes = [
5379
- "big-number",
5380
- "big_number",
5381
- "metrics-grid",
5382
- "metrics_grid",
5383
- "three-column",
5384
- "three_column",
5385
- "three-points",
5386
- "three_points",
5387
- "title-impact",
5388
- "title_impact",
5389
- "cta",
5390
- "call-to-action",
5391
- "comparison",
5392
- "timeline",
5393
- "process",
5394
- "quote",
5395
- "testimonial"
5396
- ];
5397
- if (highImpactTypes.some((t) => slideType.includes(t.replace(/_/g, "-")) || slideType.includes(t.replace(/-/g, "_")))) {
5398
- visualImpact += 2;
5399
- visualNotes.push("High-impact slide type");
5642
+ if (slideData.hasTruncatedText) {
5643
+ criticalFailures.push(`TRUNCATED TEXT: ${slideData.truncatedElements.length} element(s) cut off - message incomplete`);
5400
5644
  }
5401
- if (slideType === "single-statement" && slideData.body.length < 50) {
5402
- visualImpact += 2;
5403
- visualNotes.push("Clean single statement");
5645
+ if (slideData.isRedundant) {
5646
+ criticalFailures.push("REDUNDANT: Title and body say the same thing - violates one-idea rule");
5404
5647
  }
5405
- if (slideType === "title" && slideData.title.length > 0 && slideData.title.length < 80) {
5406
- visualImpact += 1;
5407
- visualNotes.push("Strong title");
5648
+ if (slideData.hasCompletenessIssues) {
5649
+ slideData.completenessIssues.forEach((issue) => {
5650
+ if (issue.includes("empty")) {
5651
+ criticalFailures.push(`INCOMPLETE: ${issue}`);
5652
+ }
5653
+ });
5408
5654
  }
5409
- if (slideData.contentLength > 300) {
5410
- visualImpact -= 3;
5411
- visualNotes.push("Too much text - overwhelming");
5655
+ if (criticalFailures.length > 0) {
5656
+ return this.createFailedSlideScore(slideIndex, slideType, criticalFailures.join("; "), screenshotPath, criticalFailures);
5657
+ }
5658
+ let glanceTest = 8;
5659
+ const glanceNotes = [];
5660
+ const wordCount = slideData.contentLength / 6;
5661
+ if (wordCount > 40) {
5662
+ glanceTest = 3;
5663
+ glanceNotes.push("Too much text - fails glance test");
5664
+ } else if (wordCount > 25) {
5665
+ glanceTest -= 3;
5666
+ glanceNotes.push("Text-heavy - borderline glance test");
5667
+ } else if (wordCount < 15) {
5668
+ glanceTest += 1;
5669
+ glanceNotes.push("Clean, minimal text - passes glance test easily");
5412
5670
  }
5413
5671
  if (slideData.bullets.length > 5) {
5414
- visualImpact -= 2;
5415
- visualNotes.push("Too many bullets");
5416
- }
5417
- visualImpact = Math.max(0, Math.min(10, visualImpact));
5418
- let contentClarity = 7;
5419
- const clarityNotes = [];
5420
- if (slideData.title.length > 80) {
5421
- contentClarity -= 2;
5422
- clarityNotes.push("Title too long");
5423
- }
5424
- if (slideData.title.length === 0) {
5425
- contentClarity -= 3;
5426
- clarityNotes.push("No title");
5427
- }
5428
- if (slideData.body && slideData.body.length > 0 && slideData.body.length < 200) {
5429
- contentClarity += 1;
5430
- clarityNotes.push("Good content length");
5431
- }
5432
- const avgBulletLength = slideData.bullets.length > 0 ? slideData.bullets.reduce((sum, b) => sum + b.length, 0) / slideData.bullets.length : 0;
5433
- if (avgBulletLength > 100) {
5434
- contentClarity -= 2;
5435
- clarityNotes.push("Bullets too long - not scannable");
5436
- }
5437
- contentClarity = Math.max(0, Math.min(10, contentClarity));
5438
- let professionalPolish = 6;
5439
- const polishNotes = [];
5672
+ glanceTest -= 4;
5673
+ glanceNotes.push("Too many bullets - cannot scan in 3 seconds");
5674
+ } else if (slideData.bullets.length > 3) {
5675
+ glanceTest -= 2;
5676
+ glanceNotes.push("Multiple bullets - needs careful structuring");
5677
+ }
5678
+ if (slideData.hasImage || slideData.hasChart) {
5679
+ glanceTest += 1;
5680
+ glanceNotes.push("Visual element aids quick comprehension");
5681
+ }
5682
+ glanceTest = Math.max(0, Math.min(10, glanceTest));
5683
+ let oneIdea = 7;
5684
+ const oneIdeaNotes = [];
5685
+ if (slideData.title.length > 0 && slideData.title.length < 60) {
5686
+ oneIdea += 1;
5687
+ oneIdeaNotes.push("Clear, focused title");
5688
+ } else if (slideData.title.length > 80) {
5689
+ oneIdea -= 2;
5690
+ oneIdeaNotes.push("Title too long - multiple ideas?");
5691
+ } else if (slideData.title.length === 0) {
5692
+ oneIdea -= 4;
5693
+ oneIdeaNotes.push("No title - what is the one idea?");
5694
+ }
5695
+ const focusedTypes = ["big-number", "single-statement", "quote", "cta", "title"];
5696
+ if (focusedTypes.some((t) => slideType.includes(t))) {
5697
+ oneIdea += 2;
5698
+ oneIdeaNotes.push("Slide type naturally focuses on one idea");
5699
+ }
5700
+ if (slideData.bullets.length > 4) {
5701
+ oneIdea -= 3;
5702
+ oneIdeaNotes.push("Multiple bullets dilute the message");
5703
+ }
5704
+ oneIdea = Math.max(0, Math.min(10, oneIdea));
5705
+ let dataInkRatio = 7;
5706
+ const dataInkNotes = [];
5707
+ const structuredTypes = ["big-number", "metrics-grid", "three-column", "timeline", "process", "comparison"];
5708
+ if (structuredTypes.some((t) => slideType.includes(t))) {
5709
+ dataInkRatio += 1;
5710
+ dataInkNotes.push("Structured layout");
5711
+ }
5712
+ if (slideData.hasChart) {
5713
+ dataInkRatio += 2;
5714
+ dataInkNotes.push("Data visualization present - excellent");
5715
+ }
5716
+ if (slideData.hasImage) {
5717
+ dataInkRatio += 1;
5718
+ dataInkNotes.push("Supporting visual present");
5719
+ }
5720
+ if (slideData.needsVisualButMissing) {
5721
+ dataInkRatio -= 3;
5722
+ slideData.visualNeedsIssues.forEach((issue) => {
5723
+ dataInkNotes.push(`Visual: ${issue}`);
5724
+ });
5725
+ } else if (slideData.visualScore === 10) {
5726
+ dataInkNotes.push("Appropriate visuals for content type");
5727
+ }
5728
+ if (slideData.contentLength > 400) {
5729
+ dataInkRatio -= 4;
5730
+ dataInkNotes.push("Excessive text - low information density");
5731
+ }
5732
+ dataInkRatio = Math.max(0, Math.min(10, dataInkRatio));
5733
+ let professionalExecution = 7;
5734
+ const executionNotes = [];
5440
5735
  const titleFontSize = parseFloat(slideData.titleFontSize || "0");
5441
5736
  if (titleFontSize >= 40) {
5442
- professionalPolish += 2;
5443
- polishNotes.push("Strong title typography");
5444
- } else if (titleFontSize < 24) {
5445
- professionalPolish -= 1;
5446
- polishNotes.push("Title could be more prominent");
5737
+ professionalExecution += 1;
5738
+ executionNotes.push("Strong title typography");
5739
+ } else if (titleFontSize > 0 && titleFontSize < 24) {
5740
+ professionalExecution -= 1;
5741
+ executionNotes.push("Title could be more prominent");
5447
5742
  }
5448
5743
  if (slideData.contentLength > 10 && slideData.contentLength < 200) {
5449
- professionalPolish += 1;
5450
- polishNotes.push("Well-balanced content");
5451
- }
5452
- const polishedTypes = [
5453
- "big-number",
5454
- "metrics-grid",
5455
- "three-column",
5456
- "three-points",
5457
- "comparison",
5458
- "timeline",
5459
- "process",
5460
- "cta",
5461
- "title",
5462
- "thank-you"
5463
- ];
5464
- if (polishedTypes.some((t) => slideType.includes(t))) {
5465
- professionalPolish += 1;
5466
- polishNotes.push("Well-structured layout");
5744
+ professionalExecution += 1;
5745
+ executionNotes.push("Well-balanced content density");
5467
5746
  }
5468
- professionalPolish = Math.max(0, Math.min(10, professionalPolish));
5469
- let themeCoherence = 7;
5470
- const coherenceNotes = [];
5471
5747
  if (slideData.classList.some((c) => c.includes("slide-"))) {
5472
- themeCoherence += 1;
5473
- coherenceNotes.push("Has slide type class");
5748
+ professionalExecution += 1;
5749
+ executionNotes.push("Consistent with design system");
5474
5750
  }
5475
- themeCoherence = Math.max(0, Math.min(10, themeCoherence));
5476
- const totalScore = visualImpact + contentClarity + professionalPolish + themeCoherence;
5751
+ if (slideData.hasContrastIssues) {
5752
+ professionalExecution -= 3;
5753
+ slideData.contrastIssues.forEach((issue) => {
5754
+ executionNotes.push(`Contrast: ${issue}`);
5755
+ });
5756
+ } else {
5757
+ executionNotes.push("Good text contrast");
5758
+ }
5759
+ if (slideData.hasLayoutIssues) {
5760
+ professionalExecution -= 2;
5761
+ slideData.layoutIssues.forEach((issue) => {
5762
+ executionNotes.push(`Layout: ${issue}`);
5763
+ });
5764
+ } else if (slideData.contentLength > 50) {
5765
+ executionNotes.push("Balanced layout");
5766
+ }
5767
+ if (slideData.hasTypographyIssues) {
5768
+ professionalExecution -= 2;
5769
+ slideData.typographyIssues.forEach((issue) => {
5770
+ executionNotes.push(`Typography: ${issue}`);
5771
+ });
5772
+ } else if (titleFontSize > 0) {
5773
+ executionNotes.push("Good type hierarchy");
5774
+ }
5775
+ professionalExecution = Math.max(0, Math.min(10, professionalExecution));
5776
+ const totalScore = glanceTest + oneIdea + dataInkRatio + professionalExecution;
5777
+ const visualImpact = Math.round((glanceTest + oneIdea) / 2);
5778
+ const contentClarity = oneIdea;
5779
+ const professionalPolish = Math.round((dataInkRatio + professionalExecution) / 2);
5780
+ const themeCoherence = professionalExecution;
5477
5781
  return {
5478
5782
  slideIndex,
5479
5783
  slideType,
5784
+ // New expert dimensions
5785
+ glanceTest,
5786
+ glanceTestNotes: glanceNotes.join("; ") || "Passes glance test",
5787
+ oneIdea,
5788
+ oneIdeaNotes: oneIdeaNotes.join("; ") || "Clear single message",
5789
+ dataInkRatio,
5790
+ dataInkNotes: dataInkNotes.join("; ") || "Good information density",
5791
+ professionalExecution,
5792
+ professionalExecutionNotes: executionNotes.join("; ") || "Professional quality",
5793
+ // Critical failures
5794
+ hasCriticalFailure: false,
5795
+ criticalFailures: [],
5796
+ // Legacy dimensions (for compatibility)
5480
5797
  visualImpact,
5481
- visualImpactNotes: visualNotes.join("; ") || "Standard",
5798
+ visualImpactNotes: glanceNotes.join("; ") || "Standard",
5482
5799
  contentClarity,
5483
- contentClarityNotes: clarityNotes.join("; ") || "Good",
5800
+ contentClarityNotes: oneIdeaNotes.join("; ") || "Good",
5484
5801
  professionalPolish,
5485
- professionalPolishNotes: polishNotes.join("; ") || "Acceptable",
5802
+ professionalPolishNotes: executionNotes.join("; ") || "Acceptable",
5486
5803
  themeCoherence,
5487
- themeCoherenceNotes: coherenceNotes.join("; ") || "Consistent",
5804
+ themeCoherenceNotes: "Consistent",
5488
5805
  totalScore,
5489
5806
  screenshotPath
5490
5807
  };
5491
5808
  }
5809
+ /**
5810
+ * Create a failed slide score (for critical failures)
5811
+ */
5812
+ createFailedSlideScore(slideIndex, slideType, reason, screenshotPath, criticalFailures = []) {
5813
+ return {
5814
+ slideIndex,
5815
+ slideType,
5816
+ glanceTest: 0,
5817
+ glanceTestNotes: "CRITICAL FAILURE: " + reason,
5818
+ oneIdea: 0,
5819
+ oneIdeaNotes: "CRITICAL FAILURE: " + reason,
5820
+ dataInkRatio: 0,
5821
+ dataInkNotes: "CRITICAL FAILURE: " + reason,
5822
+ professionalExecution: 0,
5823
+ professionalExecutionNotes: "CRITICAL FAILURE: " + reason,
5824
+ hasCriticalFailure: true,
5825
+ criticalFailures: criticalFailures.length > 0 ? criticalFailures : [reason],
5826
+ visualImpact: 0,
5827
+ visualImpactNotes: "CRITICAL: " + reason,
5828
+ contentClarity: 0,
5829
+ contentClarityNotes: "CRITICAL: " + reason,
5830
+ professionalPolish: 0,
5831
+ professionalPolishNotes: "CRITICAL: " + reason,
5832
+ themeCoherence: 0,
5833
+ themeCoherenceNotes: "CRITICAL: " + reason,
5834
+ totalScore: 0,
5835
+ screenshotPath
5836
+ };
5837
+ }
5492
5838
  inferSlideType(slideData) {
5493
5839
  const classList = slideData.classList || [];
5494
5840
  for (const cls of classList) {
@@ -5600,34 +5946,44 @@ var VisualQualityEvaluator = class {
5600
5946
  evaluateContentQuality(slideScores) {
5601
5947
  let score = 25;
5602
5948
  const notes = [];
5603
- const clarityScores = slideScores.map((s) => s.contentClarity);
5604
- const avgClarity = clarityScores.reduce((a, b) => a + b, 0) / clarityScores.length;
5605
- const messagesAreClear = avgClarity >= 7;
5606
- if (!messagesAreClear) {
5607
- score -= 7;
5608
- notes.push("Messages could be clearer");
5949
+ const criticalFailureSlides = slideScores.filter((s) => s.hasCriticalFailure);
5950
+ if (criticalFailureSlides.length > 0) {
5951
+ score = Math.max(0, 25 - criticalFailureSlides.length * 8);
5952
+ notes.push(`CRITICAL: ${criticalFailureSlides.length} slide(s) have critical failures`);
5953
+ criticalFailureSlides.forEach((s) => {
5954
+ s.criticalFailures.forEach((f) => notes.push(` - Slide ${s.slideIndex}: ${f}`));
5955
+ });
5609
5956
  }
5610
- const lowClarity = slideScores.filter((s) => s.contentClarity < 5).length;
5611
- const appropriateDepth = lowClarity < slideScores.length * 0.2;
5957
+ const avgGlance = slideScores.reduce((sum, s) => sum + s.glanceTest, 0) / slideScores.length;
5958
+ const passesGlanceTest = avgGlance >= 6;
5959
+ if (!passesGlanceTest) {
5960
+ score -= 5;
5961
+ notes.push(`Glance Test: Average ${avgGlance.toFixed(1)}/10 - too text-heavy`);
5962
+ }
5963
+ const avgOneIdea = slideScores.reduce((sum, s) => sum + s.oneIdea, 0) / slideScores.length;
5964
+ const hasOneIdeaPerSlide = avgOneIdea >= 6;
5965
+ if (!hasOneIdeaPerSlide) {
5966
+ score -= 5;
5967
+ notes.push(`One Idea Rule: Average ${avgOneIdea.toFixed(1)}/10 - messages diluted`);
5968
+ }
5969
+ const messagesAreClear = avgOneIdea >= 7;
5970
+ const lowScoreSlides = slideScores.filter((s) => s.totalScore < 20).length;
5971
+ const appropriateDepth = lowScoreSlides < slideScores.length * 0.2;
5612
5972
  if (!appropriateDepth) {
5613
5973
  score -= 5;
5614
- notes.push("Some slides have content issues");
5974
+ notes.push("Some slides have quality issues");
5615
5975
  }
5616
- const overloadedSlides = slideScores.filter(
5617
- (s) => s.contentClarityNotes.includes("too long") || s.visualImpactNotes.includes("Too much")
5618
- ).length;
5976
+ const overloadedSlides = slideScores.filter((s) => s.glanceTest < 5).length;
5619
5977
  const noOverload = overloadedSlides === 0;
5620
5978
  if (!noOverload) {
5621
- score -= 5;
5622
- notes.push(`${overloadedSlides} slides have too much content`);
5979
+ score -= 3;
5980
+ notes.push(`${overloadedSlides} slides fail glance test - too dense`);
5623
5981
  }
5624
- const insightSlides = slideScores.filter(
5625
- (s) => s.visualImpact >= 7 && s.contentClarity >= 7
5626
- ).length;
5627
- const actionableInsights = insightSlides >= slideScores.length * 0.3;
5982
+ const excellentSlides = slideScores.filter((s) => s.totalScore >= 30).length;
5983
+ const actionableInsights = excellentSlides >= slideScores.length * 0.3;
5628
5984
  if (!actionableInsights) {
5629
- score -= 5;
5630
- notes.push("Need more high-impact insight slides");
5985
+ score -= 3;
5986
+ notes.push("Need more high-impact slides");
5631
5987
  }
5632
5988
  return {
5633
5989
  score: Math.max(0, score),
@@ -5641,30 +5997,50 @@ var VisualQualityEvaluator = class {
5641
5997
  evaluateExecutiveReadiness(slideScores) {
5642
5998
  let score = 25;
5643
5999
  const notes = [];
5644
- const avgVisual = slideScores.reduce((sum, s) => sum + s.visualImpact, 0) / slideScores.length;
5645
- const avgPolish = slideScores.reduce((sum, s) => sum + s.professionalPolish, 0) / slideScores.length;
5646
- const avgClarity = slideScores.reduce((sum, s) => sum + s.contentClarity, 0) / slideScores.length;
6000
+ const criticalFailureSlides = slideScores.filter((s) => s.hasCriticalFailure);
6001
+ if (criticalFailureSlides.length > 0) {
6002
+ score = 0;
6003
+ notes.push(`CRITICAL: ${criticalFailureSlides.length} slide(s) have critical failures - CANNOT show to executives`);
6004
+ criticalFailureSlides.forEach((s) => {
6005
+ notes.push(` - Slide ${s.slideIndex} (${s.slideType}): ${s.criticalFailures[0]}`);
6006
+ });
6007
+ return {
6008
+ score: 0,
6009
+ wouldImpress: false,
6010
+ readyForBoardroom: false,
6011
+ compelling: false,
6012
+ shareworthy: false,
6013
+ notes: notes.join(". ")
6014
+ };
6015
+ }
6016
+ const avgGlance = slideScores.reduce((sum, s) => sum + s.glanceTest, 0) / slideScores.length;
6017
+ const avgOneIdea = slideScores.reduce((sum, s) => sum + s.oneIdea, 0) / slideScores.length;
6018
+ const avgDataInk = slideScores.reduce((sum, s) => sum + s.dataInkRatio, 0) / slideScores.length;
6019
+ const avgExecution = slideScores.reduce((sum, s) => sum + s.professionalExecution, 0) / slideScores.length;
5647
6020
  const avgTotal = slideScores.reduce((sum, s) => sum + s.totalScore, 0) / slideScores.length;
5648
- const wouldImpress = avgTotal >= 26;
6021
+ const wouldImpress = avgGlance >= 7 && avgOneIdea >= 7 && avgExecution >= 7;
5649
6022
  if (!wouldImpress) {
5650
6023
  score -= 7;
5651
6024
  notes.push("Needs more visual impact to impress");
5652
6025
  }
5653
- const readyForBoardroom = avgPolish >= 6 && avgClarity >= 6;
6026
+ const readyForBoardroom = avgExecution >= 6 && avgDataInk >= 6;
5654
6027
  if (!readyForBoardroom) {
5655
6028
  score -= 7;
5656
- notes.push("Needs more polish for executive audience");
6029
+ notes.push(`Boardroom readiness: Execution ${avgExecution.toFixed(1)}/10, Data-Ink ${avgDataInk.toFixed(1)}/10`);
5657
6030
  }
5658
- const compelling = avgVisual >= 6;
6031
+ const compelling = avgGlance >= 6 && avgOneIdea >= 6;
5659
6032
  if (!compelling) {
5660
6033
  score -= 5;
5661
- notes.push("Could be more visually compelling");
6034
+ notes.push(`Compelling: Glance ${avgGlance.toFixed(1)}/10, OneIdea ${avgOneIdea.toFixed(1)}/10`);
5662
6035
  }
5663
6036
  const excellentSlides = slideScores.filter((s) => s.totalScore >= 30).length;
5664
6037
  const shareworthy = excellentSlides >= slideScores.length * 0.4;
5665
6038
  if (!shareworthy) {
5666
6039
  score -= 5;
5667
- notes.push("Less than 40% of slides are excellent");
6040
+ notes.push(`Shareworthy: ${excellentSlides}/${slideScores.length} excellent slides (need 40%)`);
6041
+ }
6042
+ if (wouldImpress && readyForBoardroom && compelling) {
6043
+ notes.push("Meets expert standards - McKinsey/TED quality");
5668
6044
  }
5669
6045
  return {
5670
6046
  score: Math.max(0, score),
@@ -7392,13 +7768,63 @@ var PowerPointGenerator = class {
7392
7768
 
7393
7769
  // src/generators/PDFGenerator.ts
7394
7770
  var import_puppeteer = __toESM(require("puppeteer"));
7771
+ var cachedBrowser = null;
7772
+ var browserIdleTimeout = null;
7773
+ var BROWSER_IDLE_MS = 3e4;
7395
7774
  var PDFGenerator = class {
7396
7775
  defaultOptions = {
7397
7776
  orientation: "landscape",
7398
7777
  printBackground: true,
7399
7778
  format: "Slide",
7400
- scale: 1
7779
+ scale: 1,
7780
+ reuseBrowser: true
7401
7781
  };
7782
+ /**
7783
+ * Get or create a browser instance
7784
+ */
7785
+ async getBrowser(reuse) {
7786
+ if (browserIdleTimeout) {
7787
+ clearTimeout(browserIdleTimeout);
7788
+ browserIdleTimeout = null;
7789
+ }
7790
+ if (reuse && cachedBrowser && cachedBrowser.connected) {
7791
+ logger.progress("Using cached browser instance");
7792
+ return cachedBrowser;
7793
+ }
7794
+ if (cachedBrowser && !cachedBrowser.connected) {
7795
+ cachedBrowser = null;
7796
+ }
7797
+ logger.progress("Launching new browser instance...");
7798
+ const browser = await import_puppeteer.default.launch({
7799
+ headless: true,
7800
+ args: [
7801
+ "--no-sandbox",
7802
+ "--disable-setuid-sandbox",
7803
+ "--disable-dev-shm-usage",
7804
+ "--disable-gpu"
7805
+ ]
7806
+ });
7807
+ if (reuse) {
7808
+ cachedBrowser = browser;
7809
+ }
7810
+ return browser;
7811
+ }
7812
+ /**
7813
+ * Schedule browser cleanup after idle period
7814
+ */
7815
+ scheduleBrowserCleanup() {
7816
+ if (browserIdleTimeout) {
7817
+ clearTimeout(browserIdleTimeout);
7818
+ }
7819
+ browserIdleTimeout = setTimeout(async () => {
7820
+ if (cachedBrowser) {
7821
+ logger.progress("Closing idle browser instance");
7822
+ await cachedBrowser.close().catch(() => {
7823
+ });
7824
+ cachedBrowser = null;
7825
+ }
7826
+ }, BROWSER_IDLE_MS);
7827
+ }
7402
7828
  /**
7403
7829
  * Generate PDF from HTML presentation
7404
7830
  * @param html - The HTML content of the presentation
@@ -7407,18 +7833,14 @@ var PDFGenerator = class {
7407
7833
  */
7408
7834
  async generate(html, options = {}) {
7409
7835
  const opts = { ...this.defaultOptions, ...options };
7836
+ const reuseBrowser = opts.reuseBrowser ?? true;
7410
7837
  logger.progress("\u{1F4C4} Generating PDF...");
7411
- let browser;
7838
+ let browser = null;
7839
+ let shouldCloseBrowser = !reuseBrowser;
7840
+ let page = null;
7412
7841
  try {
7413
- browser = await import_puppeteer.default.launch({
7414
- headless: true,
7415
- args: [
7416
- "--no-sandbox",
7417
- "--disable-setuid-sandbox",
7418
- "--disable-dev-shm-usage"
7419
- ]
7420
- });
7421
- const page = await browser.newPage();
7842
+ browser = await this.getBrowser(reuseBrowser);
7843
+ page = await browser.newPage();
7422
7844
  const printHtml = this.preparePrintHtml(html);
7423
7845
  await page.setViewport({
7424
7846
  width: 1920,
@@ -7454,13 +7876,38 @@ var PDFGenerator = class {
7454
7876
  } catch (error) {
7455
7877
  const errorMessage = error instanceof Error ? error.message : String(error);
7456
7878
  logger.error(`PDF generation failed: ${errorMessage}`);
7879
+ shouldCloseBrowser = true;
7457
7880
  throw new Error(`PDF generation failed: ${errorMessage}`);
7458
7881
  } finally {
7459
- if (browser) {
7460
- await browser.close();
7882
+ if (page) {
7883
+ await page.close().catch(() => {
7884
+ });
7885
+ }
7886
+ if (shouldCloseBrowser && browser) {
7887
+ await browser.close().catch(() => {
7888
+ });
7889
+ if (browser === cachedBrowser) {
7890
+ cachedBrowser = null;
7891
+ }
7892
+ } else if (reuseBrowser) {
7893
+ this.scheduleBrowserCleanup();
7461
7894
  }
7462
7895
  }
7463
7896
  }
7897
+ /**
7898
+ * Manually close the cached browser (call before process exit)
7899
+ */
7900
+ static async closeBrowser() {
7901
+ if (browserIdleTimeout) {
7902
+ clearTimeout(browserIdleTimeout);
7903
+ browserIdleTimeout = null;
7904
+ }
7905
+ if (cachedBrowser) {
7906
+ await cachedBrowser.close().catch(() => {
7907
+ });
7908
+ cachedBrowser = null;
7909
+ }
7910
+ }
7464
7911
  /**
7465
7912
  * Prepare HTML for print/PDF output
7466
7913
  * Modifies the HTML to work better as a printed document
@@ -7474,12 +7921,24 @@ var PDFGenerator = class {
7474
7921
  margin: 0;
7475
7922
  }
7476
7923
 
7924
+ /* Font rendering optimization for print */
7925
+ * {
7926
+ -webkit-font-smoothing: antialiased;
7927
+ -moz-osx-font-smoothing: grayscale;
7928
+ text-rendering: optimizeLegibility;
7929
+ font-feature-settings: "liga" 1, "kern" 1;
7930
+ }
7931
+
7477
7932
  @media print {
7478
7933
  html, body {
7479
7934
  margin: 0;
7480
7935
  padding: 0;
7481
7936
  width: 1920px;
7482
7937
  height: 1080px;
7938
+ /* Print color optimization */
7939
+ color-adjust: exact;
7940
+ -webkit-print-color-adjust: exact;
7941
+ print-color-adjust: exact;
7483
7942
  }
7484
7943
 
7485
7944
  .reveal .slides {
@@ -7496,7 +7955,8 @@ var PDFGenerator = class {
7496
7955
  width: 1920px !important;
7497
7956
  height: 1080px !important;
7498
7957
  margin: 0 !important;
7499
- padding: 60px !important;
7958
+ /* Professional safe margins: 80px (4.2%) */
7959
+ padding: 80px !important;
7500
7960
  box-sizing: border-box;
7501
7961
  position: relative !important;
7502
7962
  top: auto !important;
@@ -7520,7 +7980,7 @@ var PDFGenerator = class {
7520
7980
  }
7521
7981
 
7522
7982
  /* Disable animations for print */
7523
- * {
7983
+ *, *::before, *::after {
7524
7984
  animation: none !important;
7525
7985
  transition: none !important;
7526
7986
  }
@@ -7536,6 +7996,30 @@ var PDFGenerator = class {
7536
7996
  .reveal .navigate-down {
7537
7997
  display: none !important;
7538
7998
  }
7999
+
8000
+ /* Typography refinements for print */
8001
+ h1, h2, h3, h4 {
8002
+ orphans: 3;
8003
+ widows: 3;
8004
+ page-break-after: avoid;
8005
+ }
8006
+
8007
+ p, li {
8008
+ orphans: 2;
8009
+ widows: 2;
8010
+ }
8011
+
8012
+ /* Ensure links are readable in print */
8013
+ a {
8014
+ text-decoration: none;
8015
+ }
8016
+
8017
+ /* Prevent image overflow */
8018
+ img {
8019
+ max-width: 100%;
8020
+ height: auto;
8021
+ page-break-inside: avoid;
8022
+ }
7539
8023
  }
7540
8024
 
7541
8025
  /* Force print mode in Puppeteer */
@@ -7543,6 +8027,11 @@ var PDFGenerator = class {
7543
8027
  page-break-after: always;
7544
8028
  page-break-inside: avoid;
7545
8029
  }
8030
+
8031
+ /* High contrast mode for business presentations */
8032
+ .reveal section {
8033
+ color-scheme: light;
8034
+ }
7546
8035
  </style>
7547
8036
  `;
7548
8037
  if (html.includes("</head>")) {
@@ -8136,6 +8625,7 @@ var index_default = {
8136
8625
  CompositeImageProvider,
8137
8626
  ContentAnalyzer,
8138
8627
  ContentPatternClassifier,
8628
+ KnowledgeBaseError,
8139
8629
  KnowledgeGateway,
8140
8630
  LocalImageProvider,
8141
8631
  MermaidProvider,