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.mjs CHANGED
@@ -1,30 +1,98 @@
1
1
  // src/types/index.ts
2
- var ValidationError = class extends Error {
2
+ var ValidationError = class _ValidationError extends Error {
3
3
  constructor(errors, message = "Validation failed") {
4
- super(message);
4
+ const errorList = errors.map((e) => ` \u2022 ${e}`).join("\n");
5
+ const suggestions = _ValidationError.getSuggestions(errors);
6
+ const suggestionList = suggestions.length > 0 ? "\n\nHow to fix:\n" + suggestions.map((s) => ` \u2192 ${s}`).join("\n") : "";
7
+ super(`${message}
8
+
9
+ Issues found:
10
+ ${errorList}${suggestionList}`);
5
11
  this.errors = errors;
6
12
  this.name = "ValidationError";
13
+ this.suggestions = suggestions;
14
+ }
15
+ suggestions;
16
+ static getSuggestions(errors) {
17
+ const suggestions = [];
18
+ for (const error of errors) {
19
+ if (error.includes("Content is required")) {
20
+ suggestions.push("Provide markdown content with at least one ## section header");
21
+ }
22
+ if (error.includes("Mode must be")) {
23
+ suggestions.push('Use mode: "keynote" for TED-style or mode: "business" for corporate');
24
+ }
25
+ if (error.includes("Title is required")) {
26
+ suggestions.push('Add a title: "Your Presentation Title" to your config');
27
+ }
28
+ if (error.includes("output format")) {
29
+ suggestions.push('Specify format: ["html"] or format: ["html", "pdf"]');
30
+ }
31
+ }
32
+ return [...new Set(suggestions)];
7
33
  }
8
34
  };
9
- var QAFailureError = class extends Error {
10
- constructor(score, threshold, qaResults, message = `QA score ${score} below threshold ${threshold}`) {
11
- super(message);
35
+ var QAFailureError = class _QAFailureError extends Error {
36
+ constructor(score, threshold, qaResults, message) {
37
+ const topIssues = _QAFailureError.getTopIssues(qaResults);
38
+ const issueList = topIssues.map((i) => ` \u2022 ${i}`).join("\n");
39
+ const helpText = `
40
+
41
+ Score: ${score}/100 (threshold: ${threshold})
42
+
43
+ Top issues to fix:
44
+ ${issueList}
45
+
46
+ How to improve:
47
+ \u2192 Ensure each slide has meaningful content (not just a title)
48
+ \u2192 Keep text concise - aim for <40 words per slide
49
+ \u2192 Use timeline/process slides for step-by-step content
50
+ \u2192 Add visuals for data-heavy slides`;
51
+ super(message || `QA score ${score} below threshold ${threshold}${helpText}`);
12
52
  this.score = score;
13
53
  this.threshold = threshold;
14
54
  this.qaResults = qaResults;
15
55
  this.name = "QAFailureError";
56
+ this.topIssues = topIssues;
16
57
  }
58
+ topIssues;
17
59
  getIssues() {
18
60
  return this.qaResults.issues.map((issue) => issue.message);
19
61
  }
62
+ static getTopIssues(qaResults) {
63
+ const issues = [];
64
+ if (qaResults.slideScores) {
65
+ for (const slide of qaResults.slideScores) {
66
+ if (slide.criticalIssues) {
67
+ issues.push(...slide.criticalIssues);
68
+ }
69
+ }
70
+ }
71
+ if (qaResults.issues) {
72
+ issues.push(...qaResults.issues.map((i) => i.message));
73
+ }
74
+ return [...new Set(issues)].slice(0, 5);
75
+ }
20
76
  };
21
77
  var TemplateNotFoundError = class extends Error {
22
- constructor(templatePath, message = `Template not found: ${templatePath}`) {
23
- super(message);
78
+ constructor(templatePath, message) {
79
+ super(message || `Template not found: ${templatePath}
80
+
81
+ Available templates: title, content, timeline, process, metrics, quote, cta, thank-you`);
24
82
  this.templatePath = templatePath;
25
83
  this.name = "TemplateNotFoundError";
26
84
  }
27
85
  };
86
+ var KnowledgeBaseError = class extends Error {
87
+ constructor(field, context, message) {
88
+ super(message || `Knowledge base configuration error: Missing "${field}" in ${context}
89
+
90
+ Check assets/presentation-knowledge.yaml for the correct structure.`);
91
+ this.field = field;
92
+ this.context = context;
93
+ this.name = "KnowledgeBaseError";
94
+ }
95
+ };
28
96
 
29
97
  // src/core/PresentationEngine.ts
30
98
  import * as fs2 from "fs";
@@ -33,8 +101,7 @@ import * as os from "os";
33
101
 
34
102
  // src/kb/KnowledgeGateway.ts
35
103
  import { readFileSync } from "fs";
36
- import { join, dirname } from "path";
37
- import { fileURLToPath } from "url";
104
+ import { join } from "path";
38
105
  import * as yaml from "yaml";
39
106
 
40
107
  // src/utils/Logger.ts
@@ -106,9 +173,6 @@ var logger = new Logger({
106
173
 
107
174
  // src/kb/KnowledgeGateway.ts
108
175
  function getModuleDir() {
109
- if (typeof import.meta !== "undefined" && import.meta.url) {
110
- return dirname(fileURLToPath(import.meta.url));
111
- }
112
176
  if (typeof __dirname !== "undefined") {
113
177
  return __dirname;
114
178
  }
@@ -778,7 +842,8 @@ var KnowledgeGateway = class {
778
842
  // Subtitle needs more room for complete phrases - at least 10 words for keynote
779
843
  subtitle: { maxWords: mode === "keynote" ? 12 : Math.min(20, Math.floor(maxWords / 2)) },
780
844
  context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
781
- step: { maxWords: Math.min(20, Math.floor(maxWords / 4)) },
845
+ // Step descriptions need minimum 8 words for meaningful content
846
+ step: { maxWords: Math.max(8, Math.min(20, Math.floor(maxWords / 3))) },
782
847
  columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
783
848
  };
784
849
  }
@@ -2797,25 +2862,43 @@ var SlideFactory = class {
2797
2862
  createComparisonSlide(index, section) {
2798
2863
  const comparison = this.classifier.extractComparison(section);
2799
2864
  const labels = this.config.defaults.comparison;
2865
+ const title = this.createTitle(section.header, section);
2866
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
2800
2867
  const leftFallback = labels.optionLabels[0] ?? "Option A";
2801
2868
  const rightFallback = labels.optionLabels[1] ?? "Option B";
2802
- const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
2803
- const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
2869
+ let leftColumn = comparison?.left || section.bullets[0] || "";
2870
+ let rightColumn = comparison?.right || section.bullets[1] || "";
2871
+ const normalizedLeft = leftColumn.toLowerCase().replace(/[^a-z0-9]/g, "");
2872
+ const normalizedRight = rightColumn.toLowerCase().replace(/[^a-z0-9]/g, "");
2873
+ const leftIsDuplicate = normalizedLeft === normalizedTitle || normalizedTitle.includes(normalizedLeft);
2874
+ const rightIsDuplicate = normalizedRight === normalizedTitle || normalizedTitle.includes(normalizedRight);
2875
+ if (leftIsDuplicate || rightIsDuplicate || !leftColumn && !rightColumn) {
2876
+ if (section.bullets.length >= 2) {
2877
+ logger.warn(`Comparison slide "${title}" has duplicate content, using bullet points instead`);
2878
+ return this.createBulletSlide(index, section);
2879
+ }
2880
+ if (leftIsDuplicate && rightIsDuplicate) {
2881
+ logger.warn(`Skipping comparison slide "${title}" - content duplicates title`);
2882
+ return null;
2883
+ }
2884
+ if (leftIsDuplicate) leftColumn = leftFallback;
2885
+ if (rightIsDuplicate) rightColumn = rightFallback;
2886
+ }
2804
2887
  return {
2805
2888
  index,
2806
2889
  type: "comparison",
2807
2890
  data: {
2808
- title: this.createTitle(section.header, section),
2891
+ title,
2809
2892
  columns: [
2810
2893
  {
2811
2894
  title: labels.leftLabel,
2812
2895
  // FROM KB - not hardcoded 'Before'
2813
- content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
2896
+ content: this.truncateText(leftColumn || leftFallback, this.config.rules.wordsPerSlide.max)
2814
2897
  },
2815
2898
  {
2816
2899
  title: labels.rightLabel,
2817
2900
  // FROM KB - not hardcoded 'After'
2818
- content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
2901
+ content: this.truncateText(rightColumn || rightFallback, this.config.rules.wordsPerSlide.max)
2819
2902
  }
2820
2903
  ]
2821
2904
  },
@@ -2826,9 +2909,8 @@ var SlideFactory = class {
2826
2909
  const steps = this.classifier.extractSteps(section);
2827
2910
  const maxSteps = Math.min(
2828
2911
  steps.length,
2829
- this.config.rules.bulletsPerSlide.max,
2830
2912
  this.config.millersLaw.maxItems
2831
- // FROM KB - 7±2 rule
2913
+ // FROM KB - 7±2 rule (max 9)
2832
2914
  );
2833
2915
  return {
2834
2916
  index,
@@ -2850,7 +2932,7 @@ var SlideFactory = class {
2850
2932
  }
2851
2933
  createProcessSlide(index, section) {
2852
2934
  const steps = this.classifier.extractSteps(section);
2853
- const maxSteps = Math.min(steps.length, this.config.rules.bulletsPerSlide.max);
2935
+ const maxSteps = Math.min(steps.length, this.config.millersLaw.maxItems);
2854
2936
  return {
2855
2937
  index,
2856
2938
  type: "process",
@@ -3087,10 +3169,21 @@ var SlideFactory = class {
3087
3169
  * Used for section headers with strong conclusions.
3088
3170
  */
3089
3171
  createTitleImpactSlide(index, section) {
3172
+ const title = this.cleanText(section.header);
3090
3173
  const supportingText = section.content || section.bullets.slice(0, 2).join(". ");
3091
3174
  const truncatedSupport = this.truncateText(supportingText, this.config.defaults.context.maxWords);
3175
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
3176
+ const normalizedBody = truncatedSupport.toLowerCase().replace(/[^a-z0-9]/g, "");
3177
+ const bodyIsDuplicate = normalizedBody === normalizedTitle || normalizedTitle.includes(normalizedBody) || normalizedBody.includes(normalizedTitle) || truncatedSupport.length < 10;
3178
+ if (bodyIsDuplicate) {
3179
+ if (section.bullets.length >= 2 && this.config.mode === "business") {
3180
+ return this.createBulletSlide(index, section);
3181
+ }
3182
+ logger.warn(`Skipping title-impact slide "${title}" - no distinct body content`);
3183
+ return null;
3184
+ }
3092
3185
  const data = {
3093
- title: this.cleanText(section.header)
3186
+ title
3094
3187
  };
3095
3188
  if (truncatedSupport) {
3096
3189
  data.body = truncatedSupport;
@@ -5257,169 +5350,422 @@ var VisualQualityEvaluator = class {
5257
5350
  const title = currentSlide.querySelector("h1, h2, .title")?.textContent?.trim() || "";
5258
5351
  const body = currentSlide.querySelector(".body, p:not(.subtitle)")?.textContent?.trim() || "";
5259
5352
  const bullets = Array.from(currentSlide.querySelectorAll("li")).map((li) => li.textContent?.trim() || "");
5353
+ const hasSteps = !!currentSlide.querySelector(".steps, .process-steps, .timeline");
5354
+ const steps = Array.from(currentSlide.querySelectorAll(".step, .process-step, .timeline-item")).map(
5355
+ (s) => s.textContent?.trim() || ""
5356
+ );
5357
+ const hasMetrics = !!currentSlide.querySelector(".metrics, .metric");
5260
5358
  const hasImage = !!currentSlide.querySelector("img");
5261
5359
  const hasChart = !!currentSlide.querySelector(".chart, svg, canvas");
5262
5360
  const classList = Array.from(currentSlide.classList);
5263
5361
  const backgroundColor = window.getComputedStyle(currentSlide).backgroundColor;
5264
5362
  const titleEl = currentSlide.querySelector("h1, h2, .title");
5265
5363
  const titleStyles = titleEl ? window.getComputedStyle(titleEl) : null;
5364
+ const truncatedElements = [];
5365
+ const contentElements = currentSlide.querySelectorAll("h1, h2, h3, p, span, li, .body, .step-desc, .step-title, .timeline-content");
5366
+ contentElements.forEach((el, idx) => {
5367
+ const styles = window.getComputedStyle(el);
5368
+ const text = el.textContent?.trim() || "";
5369
+ if (text.length < 15) return;
5370
+ const isLayoutContainer = el.classList?.contains("slide-content") || el.classList?.contains("steps") || el.classList?.contains("timeline") || el.classList?.contains("process-steps");
5371
+ if (isLayoutContainer) return;
5372
+ if (styles.textOverflow === "ellipsis") {
5373
+ if (el.scrollWidth > el.clientWidth + 5) {
5374
+ truncatedElements.push(`Element ${idx}: "${text.substring(0, 30)}..." is truncated horizontally`);
5375
+ }
5376
+ }
5377
+ const scrollHeight = el.scrollHeight;
5378
+ const clientHeight = el.clientHeight;
5379
+ if (scrollHeight > clientHeight + 20) {
5380
+ const overflow = styles.overflow || styles.overflowY;
5381
+ if (overflow === "hidden" || overflow === "clip") {
5382
+ truncatedElements.push(`Element ${idx}: "${text.substring(0, 30)}..." is truncated vertically`);
5383
+ }
5384
+ }
5385
+ });
5386
+ const allVisibleText = Array.from(currentSlide.querySelectorAll("*")).map((el) => el.textContent?.trim() || "").join(" ").trim();
5387
+ const isEmptySlide = allVisibleText.length < 10 && !hasImage && !hasChart;
5388
+ const hasOnlyTitle = title.length > 0 && body.length === 0 && bullets.length === 0 && steps.length === 0 && !hasSteps && !hasMetrics && !hasImage && !hasChart;
5389
+ const titleLower = title.toLowerCase();
5390
+ const bodyLower = body.toLowerCase();
5391
+ const isRedundant = titleLower.length > 10 && bodyLower.length > 10 && (titleLower.includes(bodyLower) || bodyLower.includes(titleLower));
5392
+ const contrastIssues = [];
5393
+ const parseRGB = (color) => {
5394
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
5395
+ if (match && match[1] !== void 0 && match[2] !== void 0 && match[3] !== void 0) {
5396
+ return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
5397
+ }
5398
+ return null;
5399
+ };
5400
+ const getLuminance = (rgb) => {
5401
+ const values = [rgb.r, rgb.g, rgb.b].map((c) => {
5402
+ c = c / 255;
5403
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
5404
+ });
5405
+ return 0.2126 * (values[0] ?? 0) + 0.7152 * (values[1] ?? 0) + 0.0722 * (values[2] ?? 0);
5406
+ };
5407
+ const getContrastRatio = (fg, bg) => {
5408
+ const l1 = getLuminance(fg);
5409
+ const l2 = getLuminance(bg);
5410
+ const lighter = Math.max(l1, l2);
5411
+ const darker = Math.min(l1, l2);
5412
+ return (lighter + 0.05) / (darker + 0.05);
5413
+ };
5414
+ const bgRGB = parseRGB(backgroundColor);
5415
+ if (titleEl && bgRGB) {
5416
+ const titleRGB = parseRGB(titleStyles?.color || "");
5417
+ if (titleRGB) {
5418
+ const contrast = getContrastRatio(titleRGB, bgRGB);
5419
+ if (contrast < 4.5) {
5420
+ contrastIssues.push(`Title contrast ${contrast.toFixed(1)}:1 (need 4.5:1)`);
5421
+ }
5422
+ }
5423
+ }
5424
+ const bodyEl = currentSlide.querySelector(".body, p:not(.subtitle)");
5425
+ if (bodyEl && bgRGB) {
5426
+ const bodyStyles2 = window.getComputedStyle(bodyEl);
5427
+ const bodyRGB = parseRGB(bodyStyles2.color);
5428
+ if (bodyRGB) {
5429
+ const contrast = getContrastRatio(bodyRGB, bgRGB);
5430
+ if (contrast < 4.5) {
5431
+ contrastIssues.push(`Body contrast ${contrast.toFixed(1)}:1 (need 4.5:1)`);
5432
+ }
5433
+ }
5434
+ }
5435
+ const hasContrastIssues = contrastIssues.length > 0;
5436
+ const layoutIssues = [];
5437
+ const slideRect = currentSlide.getBoundingClientRect();
5438
+ const layoutElements = currentSlide.querySelectorAll("h1, h2, h3, p, ul, ol, .number, .metric, img, canvas, svg");
5439
+ let topHeavy = 0;
5440
+ let bottomHeavy = 0;
5441
+ let leftHeavy = 0;
5442
+ let rightHeavy = 0;
5443
+ let centerY = slideRect.height / 2;
5444
+ let centerX = slideRect.width / 2;
5445
+ layoutElements.forEach((el) => {
5446
+ const rect = el.getBoundingClientRect();
5447
+ const elCenterY = rect.top + rect.height / 2 - slideRect.top;
5448
+ const elCenterX = rect.left + rect.width / 2 - slideRect.left;
5449
+ if (elCenterY < centerY) topHeavy++;
5450
+ else bottomHeavy++;
5451
+ if (elCenterX < centerX) leftHeavy++;
5452
+ else rightHeavy++;
5453
+ });
5454
+ const verticalBalance = Math.abs(topHeavy - bottomHeavy) / Math.max(1, topHeavy + bottomHeavy);
5455
+ const horizontalBalance = Math.abs(leftHeavy - rightHeavy) / Math.max(1, leftHeavy + rightHeavy);
5456
+ if (verticalBalance > 0.6 && contentElements.length > 2) {
5457
+ layoutIssues.push(`Vertical imbalance: ${topHeavy} top vs ${bottomHeavy} bottom`);
5458
+ }
5459
+ if (horizontalBalance > 0.6 && contentElements.length > 2) {
5460
+ layoutIssues.push(`Horizontal imbalance: ${leftHeavy} left vs ${rightHeavy} right`);
5461
+ }
5462
+ const hasLayoutIssues = layoutIssues.length > 0;
5463
+ const typographyIssues = [];
5464
+ const titleSize = parseFloat(titleStyles?.fontSize || "0");
5465
+ const bodyStyles = bodyEl ? window.getComputedStyle(bodyEl) : null;
5466
+ const bodySize = parseFloat(bodyStyles?.fontSize || "0");
5467
+ if (titleSize > 0 && bodySize > 0 && titleSize <= bodySize) {
5468
+ typographyIssues.push("Title not larger than body - hierarchy broken");
5469
+ }
5470
+ if (titleSize > 0 && bodySize > 0 && titleSize < bodySize * 1.3) {
5471
+ typographyIssues.push("Title not prominent enough vs body");
5472
+ }
5473
+ const hasTypographyIssues = typographyIssues.length > 0;
5474
+ const completenessIssues = [];
5475
+ const emptySteps = currentSlide.querySelectorAll(".steps:empty, .timeline:empty, .process-steps:empty, .steps > :empty");
5476
+ if (emptySteps.length > 0) {
5477
+ completenessIssues.push(`${emptySteps.length} empty step/timeline container(s) - content missing`);
5478
+ }
5479
+ if (body.includes("Lorem") || body.includes("placeholder") || body.includes("TODO")) {
5480
+ completenessIssues.push("Contains placeholder text");
5481
+ }
5482
+ const emptyBullets = bullets.filter((b) => b.length < 3).length;
5483
+ if (emptyBullets > 0 && bullets.length > 0) {
5484
+ completenessIssues.push(`${emptyBullets} empty/minimal bullets`);
5485
+ }
5486
+ const hasCompletenessIssues = completenessIssues.length > 0;
5487
+ const visualNeedsIssues = [];
5488
+ const visualRequiredTypes = ["data", "chart", "metrics", "comparison", "process", "timeline"];
5489
+ const visualRecommendedTypes = ["big-number", "quote", "testimonial"];
5490
+ const slideClasses = Array.from(currentSlide.classList).join(" ").toLowerCase();
5491
+ const needsVisual = visualRequiredTypes.some((t) => slideClasses.includes(t));
5492
+ const visualRecommended = visualRecommendedTypes.some((t) => slideClasses.includes(t));
5493
+ const contentText = (title + " " + body).toLowerCase();
5494
+ const dataIndicators = /\d+%|\$[\d,]+|\d+x|million|billion|percent|growth|increase|decrease|comparison|versus|vs\b|before|after/i;
5495
+ const hasDataContent = dataIndicators.test(contentText);
5496
+ const hasAnyVisual = hasImage || hasChart;
5497
+ if (needsVisual && !hasAnyVisual) {
5498
+ visualNeedsIssues.push("Data/process slide missing visual - needs chart, diagram, or image");
5499
+ } else if (hasDataContent && !hasAnyVisual && bullets.length === 0) {
5500
+ visualNeedsIssues.push("Content has numbers/data but no visualization - chart would help");
5501
+ } else if (visualRecommended && !hasAnyVisual) {
5502
+ visualNeedsIssues.push("Visual recommended for this slide type");
5503
+ }
5504
+ if (hasImage) {
5505
+ const img = currentSlide.querySelector("img");
5506
+ const imgSrc = img?.getAttribute("src") || "";
5507
+ if (imgSrc.includes("placeholder") || imgSrc.includes("picsum") || imgSrc.includes("via.placeholder")) {
5508
+ visualNeedsIssues.push("Placeholder image detected - needs real visual");
5509
+ }
5510
+ }
5511
+ const needsVisualButMissing = visualNeedsIssues.length > 0;
5512
+ const visualScore = hasAnyVisual ? 10 : needsVisual || hasDataContent ? 0 : 5;
5266
5513
  return {
5267
5514
  title,
5268
5515
  body,
5269
5516
  bullets,
5517
+ steps,
5518
+ // NEW: Timeline/process step content
5519
+ hasSteps,
5520
+ // NEW: Has .steps/.process-steps container
5521
+ hasMetrics,
5522
+ // NEW: Has metrics content
5270
5523
  hasImage,
5271
5524
  hasChart,
5272
5525
  classList,
5273
5526
  backgroundColor,
5274
5527
  titleFontSize: titleStyles?.fontSize || "",
5275
5528
  titleColor: titleStyles?.color || "",
5276
- contentLength: (title + body + bullets.join(" ")).length
5529
+ // Include steps in content length calculation
5530
+ contentLength: (title + body + bullets.join(" ") + steps.join(" ")).length,
5531
+ // Visual quality flags
5532
+ hasTruncatedText: truncatedElements.length > 0,
5533
+ truncatedElements,
5534
+ isEmptySlide,
5535
+ hasOnlyTitle,
5536
+ isRedundant,
5537
+ allVisibleTextLength: allVisibleText.length,
5538
+ // NEW: Expert quality checks
5539
+ hasContrastIssues,
5540
+ contrastIssues,
5541
+ hasLayoutIssues,
5542
+ layoutIssues,
5543
+ hasTypographyIssues,
5544
+ typographyIssues,
5545
+ hasCompletenessIssues,
5546
+ completenessIssues,
5547
+ // Visual needs analysis
5548
+ needsVisualButMissing,
5549
+ visualNeedsIssues,
5550
+ visualScore
5277
5551
  };
5278
5552
  });
5279
5553
  return this.scoreSlide(slideIndex, slideData, screenshotPath);
5280
5554
  }
5555
+ /**
5556
+ * EXPERT-LEVEL SLIDE SCORING
5557
+ *
5558
+ * This evaluates each slide like Nancy Duarte, Carmine Gallo, or a McKinsey partner would.
5559
+ * It's not about rules - it's about whether the slide WORKS.
5560
+ */
5281
5561
  scoreSlide(slideIndex, slideData, screenshotPath) {
5282
5562
  if (!slideData) {
5283
- return {
5284
- slideIndex,
5285
- slideType: "unknown",
5286
- visualImpact: 0,
5287
- visualImpactNotes: "Could not analyze slide",
5288
- contentClarity: 0,
5289
- contentClarityNotes: "Could not analyze slide",
5290
- professionalPolish: 0,
5291
- professionalPolishNotes: "Could not analyze slide",
5292
- themeCoherence: 0,
5293
- themeCoherenceNotes: "Could not analyze slide",
5294
- totalScore: 0,
5295
- screenshotPath
5296
- };
5563
+ return this.createFailedSlideScore(slideIndex, "unknown", "Could not analyze slide", screenshotPath);
5297
5564
  }
5298
5565
  const slideType = this.inferSlideType(slideData);
5299
- let visualImpact = 5;
5300
- const visualNotes = [];
5301
- if (slideData.hasImage) {
5302
- visualImpact += 2;
5303
- visualNotes.push("Has imagery");
5566
+ const criticalFailures = [];
5567
+ if (slideData.isEmptySlide) {
5568
+ criticalFailures.push("EMPTY SLIDE: No meaningful content - slide serves no purpose");
5304
5569
  }
5305
- if (slideData.hasChart) {
5306
- visualImpact += 2;
5307
- visualNotes.push("Has data visualization");
5570
+ if (slideData.hasOnlyTitle && !["title", "agenda", "thank-you", "cta"].includes(slideType)) {
5571
+ criticalFailures.push("INCOMPLETE: Slide has title but no body content - looks unfinished");
5308
5572
  }
5309
- const highImpactTypes = [
5310
- "big-number",
5311
- "big_number",
5312
- "metrics-grid",
5313
- "metrics_grid",
5314
- "three-column",
5315
- "three_column",
5316
- "three-points",
5317
- "three_points",
5318
- "title-impact",
5319
- "title_impact",
5320
- "cta",
5321
- "call-to-action",
5322
- "comparison",
5323
- "timeline",
5324
- "process",
5325
- "quote",
5326
- "testimonial"
5327
- ];
5328
- if (highImpactTypes.some((t) => slideType.includes(t.replace(/_/g, "-")) || slideType.includes(t.replace(/-/g, "_")))) {
5329
- visualImpact += 2;
5330
- visualNotes.push("High-impact slide type");
5573
+ if (slideData.hasTruncatedText) {
5574
+ criticalFailures.push(`TRUNCATED TEXT: ${slideData.truncatedElements.length} element(s) cut off - message incomplete`);
5331
5575
  }
5332
- if (slideType === "single-statement" && slideData.body.length < 50) {
5333
- visualImpact += 2;
5334
- visualNotes.push("Clean single statement");
5576
+ if (slideData.isRedundant) {
5577
+ criticalFailures.push("REDUNDANT: Title and body say the same thing - violates one-idea rule");
5335
5578
  }
5336
- if (slideType === "title" && slideData.title.length > 0 && slideData.title.length < 80) {
5337
- visualImpact += 1;
5338
- visualNotes.push("Strong title");
5579
+ if (slideData.hasCompletenessIssues) {
5580
+ slideData.completenessIssues.forEach((issue) => {
5581
+ if (issue.includes("empty")) {
5582
+ criticalFailures.push(`INCOMPLETE: ${issue}`);
5583
+ }
5584
+ });
5339
5585
  }
5340
- if (slideData.contentLength > 300) {
5341
- visualImpact -= 3;
5342
- visualNotes.push("Too much text - overwhelming");
5586
+ if (criticalFailures.length > 0) {
5587
+ return this.createFailedSlideScore(slideIndex, slideType, criticalFailures.join("; "), screenshotPath, criticalFailures);
5588
+ }
5589
+ let glanceTest = 8;
5590
+ const glanceNotes = [];
5591
+ const wordCount = slideData.contentLength / 6;
5592
+ if (wordCount > 40) {
5593
+ glanceTest = 3;
5594
+ glanceNotes.push("Too much text - fails glance test");
5595
+ } else if (wordCount > 25) {
5596
+ glanceTest -= 3;
5597
+ glanceNotes.push("Text-heavy - borderline glance test");
5598
+ } else if (wordCount < 15) {
5599
+ glanceTest += 1;
5600
+ glanceNotes.push("Clean, minimal text - passes glance test easily");
5343
5601
  }
5344
5602
  if (slideData.bullets.length > 5) {
5345
- visualImpact -= 2;
5346
- visualNotes.push("Too many bullets");
5347
- }
5348
- visualImpact = Math.max(0, Math.min(10, visualImpact));
5349
- let contentClarity = 7;
5350
- const clarityNotes = [];
5351
- if (slideData.title.length > 80) {
5352
- contentClarity -= 2;
5353
- clarityNotes.push("Title too long");
5354
- }
5355
- if (slideData.title.length === 0) {
5356
- contentClarity -= 3;
5357
- clarityNotes.push("No title");
5358
- }
5359
- if (slideData.body && slideData.body.length > 0 && slideData.body.length < 200) {
5360
- contentClarity += 1;
5361
- clarityNotes.push("Good content length");
5362
- }
5363
- const avgBulletLength = slideData.bullets.length > 0 ? slideData.bullets.reduce((sum, b) => sum + b.length, 0) / slideData.bullets.length : 0;
5364
- if (avgBulletLength > 100) {
5365
- contentClarity -= 2;
5366
- clarityNotes.push("Bullets too long - not scannable");
5367
- }
5368
- contentClarity = Math.max(0, Math.min(10, contentClarity));
5369
- let professionalPolish = 6;
5370
- const polishNotes = [];
5603
+ glanceTest -= 4;
5604
+ glanceNotes.push("Too many bullets - cannot scan in 3 seconds");
5605
+ } else if (slideData.bullets.length > 3) {
5606
+ glanceTest -= 2;
5607
+ glanceNotes.push("Multiple bullets - needs careful structuring");
5608
+ }
5609
+ if (slideData.hasImage || slideData.hasChart) {
5610
+ glanceTest += 1;
5611
+ glanceNotes.push("Visual element aids quick comprehension");
5612
+ }
5613
+ glanceTest = Math.max(0, Math.min(10, glanceTest));
5614
+ let oneIdea = 7;
5615
+ const oneIdeaNotes = [];
5616
+ if (slideData.title.length > 0 && slideData.title.length < 60) {
5617
+ oneIdea += 1;
5618
+ oneIdeaNotes.push("Clear, focused title");
5619
+ } else if (slideData.title.length > 80) {
5620
+ oneIdea -= 2;
5621
+ oneIdeaNotes.push("Title too long - multiple ideas?");
5622
+ } else if (slideData.title.length === 0) {
5623
+ oneIdea -= 4;
5624
+ oneIdeaNotes.push("No title - what is the one idea?");
5625
+ }
5626
+ const focusedTypes = ["big-number", "single-statement", "quote", "cta", "title"];
5627
+ if (focusedTypes.some((t) => slideType.includes(t))) {
5628
+ oneIdea += 2;
5629
+ oneIdeaNotes.push("Slide type naturally focuses on one idea");
5630
+ }
5631
+ if (slideData.bullets.length > 4) {
5632
+ oneIdea -= 3;
5633
+ oneIdeaNotes.push("Multiple bullets dilute the message");
5634
+ }
5635
+ oneIdea = Math.max(0, Math.min(10, oneIdea));
5636
+ let dataInkRatio = 7;
5637
+ const dataInkNotes = [];
5638
+ const structuredTypes = ["big-number", "metrics-grid", "three-column", "timeline", "process", "comparison"];
5639
+ if (structuredTypes.some((t) => slideType.includes(t))) {
5640
+ dataInkRatio += 1;
5641
+ dataInkNotes.push("Structured layout");
5642
+ }
5643
+ if (slideData.hasChart) {
5644
+ dataInkRatio += 2;
5645
+ dataInkNotes.push("Data visualization present - excellent");
5646
+ }
5647
+ if (slideData.hasImage) {
5648
+ dataInkRatio += 1;
5649
+ dataInkNotes.push("Supporting visual present");
5650
+ }
5651
+ if (slideData.needsVisualButMissing) {
5652
+ dataInkRatio -= 3;
5653
+ slideData.visualNeedsIssues.forEach((issue) => {
5654
+ dataInkNotes.push(`Visual: ${issue}`);
5655
+ });
5656
+ } else if (slideData.visualScore === 10) {
5657
+ dataInkNotes.push("Appropriate visuals for content type");
5658
+ }
5659
+ if (slideData.contentLength > 400) {
5660
+ dataInkRatio -= 4;
5661
+ dataInkNotes.push("Excessive text - low information density");
5662
+ }
5663
+ dataInkRatio = Math.max(0, Math.min(10, dataInkRatio));
5664
+ let professionalExecution = 7;
5665
+ const executionNotes = [];
5371
5666
  const titleFontSize = parseFloat(slideData.titleFontSize || "0");
5372
5667
  if (titleFontSize >= 40) {
5373
- professionalPolish += 2;
5374
- polishNotes.push("Strong title typography");
5375
- } else if (titleFontSize < 24) {
5376
- professionalPolish -= 1;
5377
- polishNotes.push("Title could be more prominent");
5668
+ professionalExecution += 1;
5669
+ executionNotes.push("Strong title typography");
5670
+ } else if (titleFontSize > 0 && titleFontSize < 24) {
5671
+ professionalExecution -= 1;
5672
+ executionNotes.push("Title could be more prominent");
5378
5673
  }
5379
5674
  if (slideData.contentLength > 10 && slideData.contentLength < 200) {
5380
- professionalPolish += 1;
5381
- polishNotes.push("Well-balanced content");
5382
- }
5383
- const polishedTypes = [
5384
- "big-number",
5385
- "metrics-grid",
5386
- "three-column",
5387
- "three-points",
5388
- "comparison",
5389
- "timeline",
5390
- "process",
5391
- "cta",
5392
- "title",
5393
- "thank-you"
5394
- ];
5395
- if (polishedTypes.some((t) => slideType.includes(t))) {
5396
- professionalPolish += 1;
5397
- polishNotes.push("Well-structured layout");
5675
+ professionalExecution += 1;
5676
+ executionNotes.push("Well-balanced content density");
5398
5677
  }
5399
- professionalPolish = Math.max(0, Math.min(10, professionalPolish));
5400
- let themeCoherence = 7;
5401
- const coherenceNotes = [];
5402
5678
  if (slideData.classList.some((c) => c.includes("slide-"))) {
5403
- themeCoherence += 1;
5404
- coherenceNotes.push("Has slide type class");
5679
+ professionalExecution += 1;
5680
+ executionNotes.push("Consistent with design system");
5405
5681
  }
5406
- themeCoherence = Math.max(0, Math.min(10, themeCoherence));
5407
- const totalScore = visualImpact + contentClarity + professionalPolish + themeCoherence;
5682
+ if (slideData.hasContrastIssues) {
5683
+ professionalExecution -= 3;
5684
+ slideData.contrastIssues.forEach((issue) => {
5685
+ executionNotes.push(`Contrast: ${issue}`);
5686
+ });
5687
+ } else {
5688
+ executionNotes.push("Good text contrast");
5689
+ }
5690
+ if (slideData.hasLayoutIssues) {
5691
+ professionalExecution -= 2;
5692
+ slideData.layoutIssues.forEach((issue) => {
5693
+ executionNotes.push(`Layout: ${issue}`);
5694
+ });
5695
+ } else if (slideData.contentLength > 50) {
5696
+ executionNotes.push("Balanced layout");
5697
+ }
5698
+ if (slideData.hasTypographyIssues) {
5699
+ professionalExecution -= 2;
5700
+ slideData.typographyIssues.forEach((issue) => {
5701
+ executionNotes.push(`Typography: ${issue}`);
5702
+ });
5703
+ } else if (titleFontSize > 0) {
5704
+ executionNotes.push("Good type hierarchy");
5705
+ }
5706
+ professionalExecution = Math.max(0, Math.min(10, professionalExecution));
5707
+ const totalScore = glanceTest + oneIdea + dataInkRatio + professionalExecution;
5708
+ const visualImpact = Math.round((glanceTest + oneIdea) / 2);
5709
+ const contentClarity = oneIdea;
5710
+ const professionalPolish = Math.round((dataInkRatio + professionalExecution) / 2);
5711
+ const themeCoherence = professionalExecution;
5408
5712
  return {
5409
5713
  slideIndex,
5410
5714
  slideType,
5715
+ // New expert dimensions
5716
+ glanceTest,
5717
+ glanceTestNotes: glanceNotes.join("; ") || "Passes glance test",
5718
+ oneIdea,
5719
+ oneIdeaNotes: oneIdeaNotes.join("; ") || "Clear single message",
5720
+ dataInkRatio,
5721
+ dataInkNotes: dataInkNotes.join("; ") || "Good information density",
5722
+ professionalExecution,
5723
+ professionalExecutionNotes: executionNotes.join("; ") || "Professional quality",
5724
+ // Critical failures
5725
+ hasCriticalFailure: false,
5726
+ criticalFailures: [],
5727
+ // Legacy dimensions (for compatibility)
5411
5728
  visualImpact,
5412
- visualImpactNotes: visualNotes.join("; ") || "Standard",
5729
+ visualImpactNotes: glanceNotes.join("; ") || "Standard",
5413
5730
  contentClarity,
5414
- contentClarityNotes: clarityNotes.join("; ") || "Good",
5731
+ contentClarityNotes: oneIdeaNotes.join("; ") || "Good",
5415
5732
  professionalPolish,
5416
- professionalPolishNotes: polishNotes.join("; ") || "Acceptable",
5733
+ professionalPolishNotes: executionNotes.join("; ") || "Acceptable",
5417
5734
  themeCoherence,
5418
- themeCoherenceNotes: coherenceNotes.join("; ") || "Consistent",
5735
+ themeCoherenceNotes: "Consistent",
5419
5736
  totalScore,
5420
5737
  screenshotPath
5421
5738
  };
5422
5739
  }
5740
+ /**
5741
+ * Create a failed slide score (for critical failures)
5742
+ */
5743
+ createFailedSlideScore(slideIndex, slideType, reason, screenshotPath, criticalFailures = []) {
5744
+ return {
5745
+ slideIndex,
5746
+ slideType,
5747
+ glanceTest: 0,
5748
+ glanceTestNotes: "CRITICAL FAILURE: " + reason,
5749
+ oneIdea: 0,
5750
+ oneIdeaNotes: "CRITICAL FAILURE: " + reason,
5751
+ dataInkRatio: 0,
5752
+ dataInkNotes: "CRITICAL FAILURE: " + reason,
5753
+ professionalExecution: 0,
5754
+ professionalExecutionNotes: "CRITICAL FAILURE: " + reason,
5755
+ hasCriticalFailure: true,
5756
+ criticalFailures: criticalFailures.length > 0 ? criticalFailures : [reason],
5757
+ visualImpact: 0,
5758
+ visualImpactNotes: "CRITICAL: " + reason,
5759
+ contentClarity: 0,
5760
+ contentClarityNotes: "CRITICAL: " + reason,
5761
+ professionalPolish: 0,
5762
+ professionalPolishNotes: "CRITICAL: " + reason,
5763
+ themeCoherence: 0,
5764
+ themeCoherenceNotes: "CRITICAL: " + reason,
5765
+ totalScore: 0,
5766
+ screenshotPath
5767
+ };
5768
+ }
5423
5769
  inferSlideType(slideData) {
5424
5770
  const classList = slideData.classList || [];
5425
5771
  for (const cls of classList) {
@@ -5531,34 +5877,44 @@ var VisualQualityEvaluator = class {
5531
5877
  evaluateContentQuality(slideScores) {
5532
5878
  let score = 25;
5533
5879
  const notes = [];
5534
- const clarityScores = slideScores.map((s) => s.contentClarity);
5535
- const avgClarity = clarityScores.reduce((a, b) => a + b, 0) / clarityScores.length;
5536
- const messagesAreClear = avgClarity >= 7;
5537
- if (!messagesAreClear) {
5538
- score -= 7;
5539
- notes.push("Messages could be clearer");
5880
+ const criticalFailureSlides = slideScores.filter((s) => s.hasCriticalFailure);
5881
+ if (criticalFailureSlides.length > 0) {
5882
+ score = Math.max(0, 25 - criticalFailureSlides.length * 8);
5883
+ notes.push(`CRITICAL: ${criticalFailureSlides.length} slide(s) have critical failures`);
5884
+ criticalFailureSlides.forEach((s) => {
5885
+ s.criticalFailures.forEach((f) => notes.push(` - Slide ${s.slideIndex}: ${f}`));
5886
+ });
5540
5887
  }
5541
- const lowClarity = slideScores.filter((s) => s.contentClarity < 5).length;
5542
- const appropriateDepth = lowClarity < slideScores.length * 0.2;
5888
+ const avgGlance = slideScores.reduce((sum, s) => sum + s.glanceTest, 0) / slideScores.length;
5889
+ const passesGlanceTest = avgGlance >= 6;
5890
+ if (!passesGlanceTest) {
5891
+ score -= 5;
5892
+ notes.push(`Glance Test: Average ${avgGlance.toFixed(1)}/10 - too text-heavy`);
5893
+ }
5894
+ const avgOneIdea = slideScores.reduce((sum, s) => sum + s.oneIdea, 0) / slideScores.length;
5895
+ const hasOneIdeaPerSlide = avgOneIdea >= 6;
5896
+ if (!hasOneIdeaPerSlide) {
5897
+ score -= 5;
5898
+ notes.push(`One Idea Rule: Average ${avgOneIdea.toFixed(1)}/10 - messages diluted`);
5899
+ }
5900
+ const messagesAreClear = avgOneIdea >= 7;
5901
+ const lowScoreSlides = slideScores.filter((s) => s.totalScore < 20).length;
5902
+ const appropriateDepth = lowScoreSlides < slideScores.length * 0.2;
5543
5903
  if (!appropriateDepth) {
5544
5904
  score -= 5;
5545
- notes.push("Some slides have content issues");
5905
+ notes.push("Some slides have quality issues");
5546
5906
  }
5547
- const overloadedSlides = slideScores.filter(
5548
- (s) => s.contentClarityNotes.includes("too long") || s.visualImpactNotes.includes("Too much")
5549
- ).length;
5907
+ const overloadedSlides = slideScores.filter((s) => s.glanceTest < 5).length;
5550
5908
  const noOverload = overloadedSlides === 0;
5551
5909
  if (!noOverload) {
5552
- score -= 5;
5553
- notes.push(`${overloadedSlides} slides have too much content`);
5910
+ score -= 3;
5911
+ notes.push(`${overloadedSlides} slides fail glance test - too dense`);
5554
5912
  }
5555
- const insightSlides = slideScores.filter(
5556
- (s) => s.visualImpact >= 7 && s.contentClarity >= 7
5557
- ).length;
5558
- const actionableInsights = insightSlides >= slideScores.length * 0.3;
5913
+ const excellentSlides = slideScores.filter((s) => s.totalScore >= 30).length;
5914
+ const actionableInsights = excellentSlides >= slideScores.length * 0.3;
5559
5915
  if (!actionableInsights) {
5560
- score -= 5;
5561
- notes.push("Need more high-impact insight slides");
5916
+ score -= 3;
5917
+ notes.push("Need more high-impact slides");
5562
5918
  }
5563
5919
  return {
5564
5920
  score: Math.max(0, score),
@@ -5572,30 +5928,50 @@ var VisualQualityEvaluator = class {
5572
5928
  evaluateExecutiveReadiness(slideScores) {
5573
5929
  let score = 25;
5574
5930
  const notes = [];
5575
- const avgVisual = slideScores.reduce((sum, s) => sum + s.visualImpact, 0) / slideScores.length;
5576
- const avgPolish = slideScores.reduce((sum, s) => sum + s.professionalPolish, 0) / slideScores.length;
5577
- const avgClarity = slideScores.reduce((sum, s) => sum + s.contentClarity, 0) / slideScores.length;
5931
+ const criticalFailureSlides = slideScores.filter((s) => s.hasCriticalFailure);
5932
+ if (criticalFailureSlides.length > 0) {
5933
+ score = 0;
5934
+ notes.push(`CRITICAL: ${criticalFailureSlides.length} slide(s) have critical failures - CANNOT show to executives`);
5935
+ criticalFailureSlides.forEach((s) => {
5936
+ notes.push(` - Slide ${s.slideIndex} (${s.slideType}): ${s.criticalFailures[0]}`);
5937
+ });
5938
+ return {
5939
+ score: 0,
5940
+ wouldImpress: false,
5941
+ readyForBoardroom: false,
5942
+ compelling: false,
5943
+ shareworthy: false,
5944
+ notes: notes.join(". ")
5945
+ };
5946
+ }
5947
+ const avgGlance = slideScores.reduce((sum, s) => sum + s.glanceTest, 0) / slideScores.length;
5948
+ const avgOneIdea = slideScores.reduce((sum, s) => sum + s.oneIdea, 0) / slideScores.length;
5949
+ const avgDataInk = slideScores.reduce((sum, s) => sum + s.dataInkRatio, 0) / slideScores.length;
5950
+ const avgExecution = slideScores.reduce((sum, s) => sum + s.professionalExecution, 0) / slideScores.length;
5578
5951
  const avgTotal = slideScores.reduce((sum, s) => sum + s.totalScore, 0) / slideScores.length;
5579
- const wouldImpress = avgTotal >= 26;
5952
+ const wouldImpress = avgGlance >= 7 && avgOneIdea >= 7 && avgExecution >= 7;
5580
5953
  if (!wouldImpress) {
5581
5954
  score -= 7;
5582
5955
  notes.push("Needs more visual impact to impress");
5583
5956
  }
5584
- const readyForBoardroom = avgPolish >= 6 && avgClarity >= 6;
5957
+ const readyForBoardroom = avgExecution >= 6 && avgDataInk >= 6;
5585
5958
  if (!readyForBoardroom) {
5586
5959
  score -= 7;
5587
- notes.push("Needs more polish for executive audience");
5960
+ notes.push(`Boardroom readiness: Execution ${avgExecution.toFixed(1)}/10, Data-Ink ${avgDataInk.toFixed(1)}/10`);
5588
5961
  }
5589
- const compelling = avgVisual >= 6;
5962
+ const compelling = avgGlance >= 6 && avgOneIdea >= 6;
5590
5963
  if (!compelling) {
5591
5964
  score -= 5;
5592
- notes.push("Could be more visually compelling");
5965
+ notes.push(`Compelling: Glance ${avgGlance.toFixed(1)}/10, OneIdea ${avgOneIdea.toFixed(1)}/10`);
5593
5966
  }
5594
5967
  const excellentSlides = slideScores.filter((s) => s.totalScore >= 30).length;
5595
5968
  const shareworthy = excellentSlides >= slideScores.length * 0.4;
5596
5969
  if (!shareworthy) {
5597
5970
  score -= 5;
5598
- notes.push("Less than 40% of slides are excellent");
5971
+ notes.push(`Shareworthy: ${excellentSlides}/${slideScores.length} excellent slides (need 40%)`);
5972
+ }
5973
+ if (wouldImpress && readyForBoardroom && compelling) {
5974
+ notes.push("Meets expert standards - McKinsey/TED quality");
5599
5975
  }
5600
5976
  return {
5601
5977
  score: Math.max(0, score),
@@ -7323,13 +7699,63 @@ var PowerPointGenerator = class {
7323
7699
 
7324
7700
  // src/generators/PDFGenerator.ts
7325
7701
  import puppeteer from "puppeteer";
7702
+ var cachedBrowser = null;
7703
+ var browserIdleTimeout = null;
7704
+ var BROWSER_IDLE_MS = 3e4;
7326
7705
  var PDFGenerator = class {
7327
7706
  defaultOptions = {
7328
7707
  orientation: "landscape",
7329
7708
  printBackground: true,
7330
7709
  format: "Slide",
7331
- scale: 1
7710
+ scale: 1,
7711
+ reuseBrowser: true
7332
7712
  };
7713
+ /**
7714
+ * Get or create a browser instance
7715
+ */
7716
+ async getBrowser(reuse) {
7717
+ if (browserIdleTimeout) {
7718
+ clearTimeout(browserIdleTimeout);
7719
+ browserIdleTimeout = null;
7720
+ }
7721
+ if (reuse && cachedBrowser && cachedBrowser.connected) {
7722
+ logger.progress("Using cached browser instance");
7723
+ return cachedBrowser;
7724
+ }
7725
+ if (cachedBrowser && !cachedBrowser.connected) {
7726
+ cachedBrowser = null;
7727
+ }
7728
+ logger.progress("Launching new browser instance...");
7729
+ const browser = await puppeteer.launch({
7730
+ headless: true,
7731
+ args: [
7732
+ "--no-sandbox",
7733
+ "--disable-setuid-sandbox",
7734
+ "--disable-dev-shm-usage",
7735
+ "--disable-gpu"
7736
+ ]
7737
+ });
7738
+ if (reuse) {
7739
+ cachedBrowser = browser;
7740
+ }
7741
+ return browser;
7742
+ }
7743
+ /**
7744
+ * Schedule browser cleanup after idle period
7745
+ */
7746
+ scheduleBrowserCleanup() {
7747
+ if (browserIdleTimeout) {
7748
+ clearTimeout(browserIdleTimeout);
7749
+ }
7750
+ browserIdleTimeout = setTimeout(async () => {
7751
+ if (cachedBrowser) {
7752
+ logger.progress("Closing idle browser instance");
7753
+ await cachedBrowser.close().catch(() => {
7754
+ });
7755
+ cachedBrowser = null;
7756
+ }
7757
+ }, BROWSER_IDLE_MS);
7758
+ }
7333
7759
  /**
7334
7760
  * Generate PDF from HTML presentation
7335
7761
  * @param html - The HTML content of the presentation
@@ -7338,18 +7764,14 @@ var PDFGenerator = class {
7338
7764
  */
7339
7765
  async generate(html, options = {}) {
7340
7766
  const opts = { ...this.defaultOptions, ...options };
7767
+ const reuseBrowser = opts.reuseBrowser ?? true;
7341
7768
  logger.progress("\u{1F4C4} Generating PDF...");
7342
- let browser;
7769
+ let browser = null;
7770
+ let shouldCloseBrowser = !reuseBrowser;
7771
+ let page = null;
7343
7772
  try {
7344
- browser = await puppeteer.launch({
7345
- headless: true,
7346
- args: [
7347
- "--no-sandbox",
7348
- "--disable-setuid-sandbox",
7349
- "--disable-dev-shm-usage"
7350
- ]
7351
- });
7352
- const page = await browser.newPage();
7773
+ browser = await this.getBrowser(reuseBrowser);
7774
+ page = await browser.newPage();
7353
7775
  const printHtml = this.preparePrintHtml(html);
7354
7776
  await page.setViewport({
7355
7777
  width: 1920,
@@ -7385,13 +7807,38 @@ var PDFGenerator = class {
7385
7807
  } catch (error) {
7386
7808
  const errorMessage = error instanceof Error ? error.message : String(error);
7387
7809
  logger.error(`PDF generation failed: ${errorMessage}`);
7810
+ shouldCloseBrowser = true;
7388
7811
  throw new Error(`PDF generation failed: ${errorMessage}`);
7389
7812
  } finally {
7390
- if (browser) {
7391
- await browser.close();
7813
+ if (page) {
7814
+ await page.close().catch(() => {
7815
+ });
7816
+ }
7817
+ if (shouldCloseBrowser && browser) {
7818
+ await browser.close().catch(() => {
7819
+ });
7820
+ if (browser === cachedBrowser) {
7821
+ cachedBrowser = null;
7822
+ }
7823
+ } else if (reuseBrowser) {
7824
+ this.scheduleBrowserCleanup();
7392
7825
  }
7393
7826
  }
7394
7827
  }
7828
+ /**
7829
+ * Manually close the cached browser (call before process exit)
7830
+ */
7831
+ static async closeBrowser() {
7832
+ if (browserIdleTimeout) {
7833
+ clearTimeout(browserIdleTimeout);
7834
+ browserIdleTimeout = null;
7835
+ }
7836
+ if (cachedBrowser) {
7837
+ await cachedBrowser.close().catch(() => {
7838
+ });
7839
+ cachedBrowser = null;
7840
+ }
7841
+ }
7395
7842
  /**
7396
7843
  * Prepare HTML for print/PDF output
7397
7844
  * Modifies the HTML to work better as a printed document
@@ -7405,12 +7852,24 @@ var PDFGenerator = class {
7405
7852
  margin: 0;
7406
7853
  }
7407
7854
 
7855
+ /* Font rendering optimization for print */
7856
+ * {
7857
+ -webkit-font-smoothing: antialiased;
7858
+ -moz-osx-font-smoothing: grayscale;
7859
+ text-rendering: optimizeLegibility;
7860
+ font-feature-settings: "liga" 1, "kern" 1;
7861
+ }
7862
+
7408
7863
  @media print {
7409
7864
  html, body {
7410
7865
  margin: 0;
7411
7866
  padding: 0;
7412
7867
  width: 1920px;
7413
7868
  height: 1080px;
7869
+ /* Print color optimization */
7870
+ color-adjust: exact;
7871
+ -webkit-print-color-adjust: exact;
7872
+ print-color-adjust: exact;
7414
7873
  }
7415
7874
 
7416
7875
  .reveal .slides {
@@ -7427,7 +7886,8 @@ var PDFGenerator = class {
7427
7886
  width: 1920px !important;
7428
7887
  height: 1080px !important;
7429
7888
  margin: 0 !important;
7430
- padding: 60px !important;
7889
+ /* Professional safe margins: 80px (4.2%) */
7890
+ padding: 80px !important;
7431
7891
  box-sizing: border-box;
7432
7892
  position: relative !important;
7433
7893
  top: auto !important;
@@ -7451,7 +7911,7 @@ var PDFGenerator = class {
7451
7911
  }
7452
7912
 
7453
7913
  /* Disable animations for print */
7454
- * {
7914
+ *, *::before, *::after {
7455
7915
  animation: none !important;
7456
7916
  transition: none !important;
7457
7917
  }
@@ -7467,6 +7927,30 @@ var PDFGenerator = class {
7467
7927
  .reveal .navigate-down {
7468
7928
  display: none !important;
7469
7929
  }
7930
+
7931
+ /* Typography refinements for print */
7932
+ h1, h2, h3, h4 {
7933
+ orphans: 3;
7934
+ widows: 3;
7935
+ page-break-after: avoid;
7936
+ }
7937
+
7938
+ p, li {
7939
+ orphans: 2;
7940
+ widows: 2;
7941
+ }
7942
+
7943
+ /* Ensure links are readable in print */
7944
+ a {
7945
+ text-decoration: none;
7946
+ }
7947
+
7948
+ /* Prevent image overflow */
7949
+ img {
7950
+ max-width: 100%;
7951
+ height: auto;
7952
+ page-break-inside: avoid;
7953
+ }
7470
7954
  }
7471
7955
 
7472
7956
  /* Force print mode in Puppeteer */
@@ -7474,6 +7958,11 @@ var PDFGenerator = class {
7474
7958
  page-break-after: always;
7475
7959
  page-break-inside: avoid;
7476
7960
  }
7961
+
7962
+ /* High contrast mode for business presentations */
7963
+ .reveal section {
7964
+ color-scheme: light;
7965
+ }
7477
7966
  </style>
7478
7967
  `;
7479
7968
  if (html.includes("</head>")) {
@@ -8066,6 +8555,7 @@ export {
8066
8555
  CompositeImageProvider,
8067
8556
  ContentAnalyzer,
8068
8557
  ContentPatternClassifier,
8558
+ KnowledgeBaseError,
8069
8559
  KnowledgeGateway,
8070
8560
  LocalImageProvider,
8071
8561
  MermaidProvider,