claude-presentation-master 8.1.0 → 8.3.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
  }
@@ -1122,7 +1186,7 @@ var ContentAnalyzer = class {
1122
1186
  logger.step(`Detected type: ${detectedType}`);
1123
1187
  const sections = this.extractSections(text);
1124
1188
  logger.step(`Found ${sections.length} sections`);
1125
- const scqa = this.extractSCQA(text);
1189
+ const scqa = this.extractSCQA(text, subtitle);
1126
1190
  const sparkline = this.extractSparkline(text);
1127
1191
  const keyMessages = this.extractKeyMessages(text);
1128
1192
  const dataPoints = this.extractDataPoints(text);
@@ -1389,15 +1453,24 @@ var ContentAnalyzer = class {
1389
1453
  /**
1390
1454
  * Extract SCQA structure (Barbara Minto)
1391
1455
  * CRITICAL: Answer must be clean prose, NOT bullet points
1456
+ * @param text - The content text to analyze
1457
+ * @param subtitle - Optional subtitle to exclude (prevents redundancy with title slide)
1392
1458
  */
1393
- extractSCQA(text) {
1459
+ extractSCQA(text, subtitle) {
1394
1460
  const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
1461
+ const normalizedSubtitle = (subtitle || "").toLowerCase().trim();
1462
+ const isSubtitle = (para) => {
1463
+ if (!normalizedSubtitle) return false;
1464
+ const normalized = para.toLowerCase().trim();
1465
+ return normalized === normalizedSubtitle || normalized.includes(normalizedSubtitle) || normalizedSubtitle.includes(normalized);
1466
+ };
1395
1467
  let situation = "";
1396
1468
  let complication = "";
1397
1469
  let question = "";
1398
1470
  let answer = "";
1399
1471
  for (const para of paragraphs.slice(0, 5)) {
1400
1472
  if (para.startsWith("-") || para.startsWith("#") || para.startsWith("|")) continue;
1473
+ if (isSubtitle(para)) continue;
1401
1474
  if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
1402
1475
  situation = this.extractFirstSentence(para);
1403
1476
  break;
@@ -1425,6 +1498,7 @@ var ContentAnalyzer = class {
1425
1498
  if (!situation && paragraphs.length > 0) {
1426
1499
  for (const para of paragraphs.slice(0, 5)) {
1427
1500
  if (!para.startsWith("-") && !para.startsWith("#") && para.length > 50) {
1501
+ if (isSubtitle(para)) continue;
1428
1502
  situation = this.extractFirstSentence(para);
1429
1503
  break;
1430
1504
  }
@@ -1871,7 +1945,8 @@ var ContentPatternClassifier = class {
1871
1945
  if (!match || !match[1]) return null;
1872
1946
  const value = match[1];
1873
1947
  const afterNumber = content.slice(content.indexOf(value) + value.length);
1874
- const context = afterNumber.trim().slice(0, 60).split(/[.!?]/)[0]?.trim() ?? "";
1948
+ const contextRaw = afterNumber.trim().split(/[\n\r]|-\s|[.!?]|(?=\d+%)|(?=\$\d)/)[0] || "";
1949
+ const context = contextRaw.trim().slice(0, 60);
1875
1950
  return { value, context };
1876
1951
  }
1877
1952
  /**
@@ -2568,6 +2643,16 @@ var SlideFactory = class {
2568
2643
  continue;
2569
2644
  }
2570
2645
  this.usedContent.add(contentKey);
2646
+ const normalizedHeader = section.header.toLowerCase().replace(/[^a-z0-9]/g, "");
2647
+ const normalizedTitle = analysis.title.toLowerCase().replace(/[^a-z0-9]/g, "");
2648
+ const normalizedSubtitle = (analysis.subtitle || "").toLowerCase().replace(/[^a-z0-9]/g, "");
2649
+ const normalizedContent = (section.content || "").toLowerCase().replace(/[^a-z0-9]/g, "");
2650
+ const headerMatchesTitle = normalizedHeader === normalizedTitle || normalizedTitle.includes(normalizedHeader) || normalizedHeader.includes(normalizedTitle);
2651
+ const contentMatchesSubtitle = normalizedContent && (normalizedContent === normalizedSubtitle || normalizedSubtitle.includes(normalizedContent) || normalizedContent.includes(normalizedSubtitle));
2652
+ if (headerMatchesTitle && (contentMatchesSubtitle || !section.content || section.content.length < 30)) {
2653
+ logger.warn(`Section "${section.header}" skipped - duplicates title slide`);
2654
+ continue;
2655
+ }
2571
2656
  const headerLower = section.header.toLowerCase();
2572
2657
  const isCTASection = headerLower.includes("call to action") || headerLower.includes("next step") || headerLower.includes("take action") || headerLower.includes("get started") || headerLower.includes("start today");
2573
2658
  if (isCTASection) {
@@ -2747,47 +2832,69 @@ var SlideFactory = class {
2747
2832
  const scqa = analysis.scqa;
2748
2833
  const titles = this.config.scqaTitles;
2749
2834
  const minBodyLength = 20;
2835
+ const normalizedTitle = analysis.title.toLowerCase().replace(/[^a-z0-9]/g, "");
2836
+ const normalizedSubtitle = (analysis.subtitle || "").toLowerCase().replace(/[^a-z0-9]/g, "");
2837
+ const duplicatesHeadline = (body) => {
2838
+ if (!body) return true;
2839
+ const normalizedBody = body.toLowerCase().replace(/[^a-z0-9]/g, "");
2840
+ if (!normalizedBody || normalizedBody.length < 10) return false;
2841
+ const titleMatch = normalizedTitle.length > 5 && (normalizedBody === normalizedTitle || normalizedTitle.includes(normalizedBody) || normalizedBody.includes(normalizedTitle));
2842
+ const subtitleMatch = normalizedSubtitle.length > 5 && (normalizedBody === normalizedSubtitle || normalizedSubtitle.includes(normalizedBody) || normalizedBody.includes(normalizedSubtitle));
2843
+ return titleMatch || subtitleMatch;
2844
+ };
2750
2845
  const situationBody = scqa?.situation ? this.truncateText(scqa.situation, this.config.rules.wordsPerSlide.max) : "";
2751
2846
  if (situationBody.length >= minBodyLength && !this.usedContent.has("scqa-situation")) {
2752
- this.usedContent.add("scqa-situation");
2753
- slides.push({
2754
- index: slides.length,
2755
- type: "single-statement",
2756
- data: {
2757
- title: titles.situation,
2758
- // FROM KB - not hardcoded 'Current Situation'
2759
- body: situationBody
2760
- },
2761
- classes: ["situation-slide"]
2762
- });
2847
+ if (duplicatesHeadline(situationBody)) {
2848
+ logger.warn(`SCQA Situation skipped - duplicates title/subtitle: "${situationBody.slice(0, 50)}..."`);
2849
+ } else {
2850
+ this.usedContent.add("scqa-situation");
2851
+ slides.push({
2852
+ index: slides.length,
2853
+ type: "single-statement",
2854
+ data: {
2855
+ title: titles.situation,
2856
+ // FROM KB - not hardcoded 'Current Situation'
2857
+ body: situationBody
2858
+ },
2859
+ classes: ["situation-slide"]
2860
+ });
2861
+ }
2763
2862
  }
2764
2863
  const complicationBody = scqa?.complication ? this.truncateText(scqa.complication, this.config.rules.wordsPerSlide.max) : "";
2765
2864
  if (complicationBody.length >= minBodyLength && !this.usedContent.has("scqa-complication")) {
2766
- this.usedContent.add("scqa-complication");
2767
- slides.push({
2768
- index: slides.length,
2769
- type: "single-statement",
2770
- data: {
2771
- title: titles.complication,
2772
- // FROM KB - not hardcoded 'The Challenge'
2773
- body: complicationBody
2774
- },
2775
- classes: ["complication-slide"]
2776
- });
2865
+ if (duplicatesHeadline(complicationBody)) {
2866
+ logger.warn(`SCQA Complication skipped - duplicates title/subtitle`);
2867
+ } else {
2868
+ this.usedContent.add("scqa-complication");
2869
+ slides.push({
2870
+ index: slides.length,
2871
+ type: "single-statement",
2872
+ data: {
2873
+ title: titles.complication,
2874
+ // FROM KB - not hardcoded 'The Challenge'
2875
+ body: complicationBody
2876
+ },
2877
+ classes: ["complication-slide"]
2878
+ });
2879
+ }
2777
2880
  }
2778
2881
  const questionBody = scqa?.question ? this.truncateText(scqa.question, this.config.rules.wordsPerSlide.max) : "";
2779
2882
  if (questionBody.length >= minBodyLength && !this.usedContent.has("scqa-question")) {
2780
- this.usedContent.add("scqa-question");
2781
- slides.push({
2782
- index: slides.length,
2783
- type: "single-statement",
2784
- data: {
2785
- title: titles.question,
2786
- // FROM KB - not hardcoded 'The Question'
2787
- body: questionBody
2788
- },
2789
- classes: ["question-slide"]
2790
- });
2883
+ if (duplicatesHeadline(questionBody)) {
2884
+ logger.warn(`SCQA Question skipped - duplicates title/subtitle`);
2885
+ } else {
2886
+ this.usedContent.add("scqa-question");
2887
+ slides.push({
2888
+ index: slides.length,
2889
+ type: "single-statement",
2890
+ data: {
2891
+ title: titles.question,
2892
+ // FROM KB - not hardcoded 'The Question'
2893
+ body: questionBody
2894
+ },
2895
+ classes: ["question-slide"]
2896
+ });
2897
+ }
2791
2898
  }
2792
2899
  }
2793
2900
  addSparklineSlides(slides, analysis) {
@@ -2840,14 +2947,41 @@ var SlideFactory = class {
2840
2947
  // CONTENT SLIDES - ALL values from KB
2841
2948
  // ===========================================================================
2842
2949
  createBigNumberSlide(index, section, pattern) {
2843
- const fullContent = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
2844
- const bigNumber = this.classifier.extractBigNumber(fullContent);
2950
+ let bigNumber = null;
2951
+ let sourceBullet = null;
2952
+ for (const bullet of section.bullets) {
2953
+ const extracted = this.classifier.extractBigNumber(bullet);
2954
+ if (extracted) {
2955
+ bigNumber = extracted;
2956
+ sourceBullet = bullet;
2957
+ break;
2958
+ }
2959
+ }
2960
+ if (!bigNumber) {
2961
+ bigNumber = this.classifier.extractBigNumber(section.header);
2962
+ }
2963
+ if (!bigNumber && section.content) {
2964
+ bigNumber = this.classifier.extractBigNumber(section.content);
2965
+ }
2845
2966
  const actualValue = bigNumber?.value || pattern.bigNumberValue;
2846
2967
  if (!actualValue) {
2847
2968
  logger.warn(`No number found for big-number slide, falling back to single-statement`);
2848
2969
  return this.createSingleStatementSlide(index, section);
2849
2970
  }
2850
- const contextContent = bigNumber?.context || section.content;
2971
+ let contextContent = bigNumber?.context || "";
2972
+ if (sourceBullet && !contextContent) {
2973
+ contextContent = sourceBullet.replace(actualValue, "").trim();
2974
+ }
2975
+ if (!contextContent) {
2976
+ contextContent = section.content || section.header;
2977
+ }
2978
+ let cleanTitle = section.header;
2979
+ if (actualValue && cleanTitle.includes(actualValue.replace(/[,$%]/g, ""))) {
2980
+ cleanTitle = cleanTitle.replace(new RegExp(`\\$?${actualValue.replace(/[,$%]/g, "").replace(/\./g, "\\.?")}[KMBkmb]?`, "gi"), "").replace(/\s+/g, " ").trim();
2981
+ }
2982
+ if (!cleanTitle || cleanTitle.length < 5) {
2983
+ cleanTitle = bigNumber?.context?.split(/[.!?]/)[0] || section.header;
2984
+ }
2851
2985
  const craftedContext = this.craftExpertContent(
2852
2986
  contextContent,
2853
2987
  "big_number",
@@ -2857,7 +2991,7 @@ var SlideFactory = class {
2857
2991
  index,
2858
2992
  type: "big-number",
2859
2993
  data: {
2860
- title: this.createTitle(section.header, section),
2994
+ title: cleanTitle,
2861
2995
  keyMessage: actualValue,
2862
2996
  body: craftedContext
2863
2997
  },
@@ -2867,25 +3001,43 @@ var SlideFactory = class {
2867
3001
  createComparisonSlide(index, section) {
2868
3002
  const comparison = this.classifier.extractComparison(section);
2869
3003
  const labels = this.config.defaults.comparison;
3004
+ const title = this.createTitle(section.header, section);
3005
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
2870
3006
  const leftFallback = labels.optionLabels[0] ?? "Option A";
2871
3007
  const rightFallback = labels.optionLabels[1] ?? "Option B";
2872
- const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
2873
- const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
3008
+ let leftColumn = comparison?.left || section.bullets[0] || "";
3009
+ let rightColumn = comparison?.right || section.bullets[1] || "";
3010
+ const normalizedLeft = leftColumn.toLowerCase().replace(/[^a-z0-9]/g, "");
3011
+ const normalizedRight = rightColumn.toLowerCase().replace(/[^a-z0-9]/g, "");
3012
+ const leftIsDuplicate = normalizedLeft === normalizedTitle || normalizedTitle.includes(normalizedLeft);
3013
+ const rightIsDuplicate = normalizedRight === normalizedTitle || normalizedTitle.includes(normalizedRight);
3014
+ if (leftIsDuplicate || rightIsDuplicate || !leftColumn && !rightColumn) {
3015
+ if (section.bullets.length >= 2) {
3016
+ logger.warn(`Comparison slide "${title}" has duplicate content, using bullet points instead`);
3017
+ return this.createBulletSlide(index, section);
3018
+ }
3019
+ if (leftIsDuplicate && rightIsDuplicate) {
3020
+ logger.warn(`Skipping comparison slide "${title}" - content duplicates title`);
3021
+ return null;
3022
+ }
3023
+ if (leftIsDuplicate) leftColumn = leftFallback;
3024
+ if (rightIsDuplicate) rightColumn = rightFallback;
3025
+ }
2874
3026
  return {
2875
3027
  index,
2876
3028
  type: "comparison",
2877
3029
  data: {
2878
- title: this.createTitle(section.header, section),
3030
+ title,
2879
3031
  columns: [
2880
3032
  {
2881
3033
  title: labels.leftLabel,
2882
3034
  // FROM KB - not hardcoded 'Before'
2883
- content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
3035
+ content: this.truncateText(leftColumn || leftFallback, this.config.rules.wordsPerSlide.max)
2884
3036
  },
2885
3037
  {
2886
3038
  title: labels.rightLabel,
2887
3039
  // FROM KB - not hardcoded 'After'
2888
- content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
3040
+ content: this.truncateText(rightColumn || rightFallback, this.config.rules.wordsPerSlide.max)
2889
3041
  }
2890
3042
  ]
2891
3043
  },
@@ -3156,10 +3308,21 @@ var SlideFactory = class {
3156
3308
  * Used for section headers with strong conclusions.
3157
3309
  */
3158
3310
  createTitleImpactSlide(index, section) {
3311
+ const title = this.cleanText(section.header);
3159
3312
  const supportingText = section.content || section.bullets.slice(0, 2).join(". ");
3160
3313
  const truncatedSupport = this.truncateText(supportingText, this.config.defaults.context.maxWords);
3314
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
3315
+ const normalizedBody = truncatedSupport.toLowerCase().replace(/[^a-z0-9]/g, "");
3316
+ const bodyIsDuplicate = normalizedBody === normalizedTitle || normalizedTitle.includes(normalizedBody) || normalizedBody.includes(normalizedTitle) || truncatedSupport.length < 10;
3317
+ if (bodyIsDuplicate) {
3318
+ if (section.bullets.length >= 2 && this.config.mode === "business") {
3319
+ return this.createBulletSlide(index, section);
3320
+ }
3321
+ logger.warn(`Skipping title-impact slide "${title}" - no distinct body content`);
3322
+ return null;
3323
+ }
3161
3324
  const data = {
3162
- title: this.cleanText(section.header)
3325
+ title
3163
3326
  };
3164
3327
  if (truncatedSupport) {
3165
3328
  data.body = truncatedSupport;
@@ -5575,12 +5738,15 @@ var VisualQualityEvaluator = class {
5575
5738
  glanceTest += 1;
5576
5739
  glanceNotes.push("Clean, minimal text - passes glance test easily");
5577
5740
  }
5578
- if (slideData.bullets.length > 5) {
5741
+ const isAgendaSlide = slideType === "agenda" || (slideData.classes || []).includes("agenda-slide");
5742
+ const bulletLimit = isAgendaSlide ? 8 : 5;
5743
+ const bulletWarning = isAgendaSlide ? 6 : 3;
5744
+ if (slideData.bullets.length > bulletLimit) {
5579
5745
  glanceTest -= 4;
5580
5746
  glanceNotes.push("Too many bullets - cannot scan in 3 seconds");
5581
- } else if (slideData.bullets.length > 3) {
5582
- glanceTest -= 2;
5583
- glanceNotes.push("Multiple bullets - needs careful structuring");
5747
+ } else if (slideData.bullets.length > bulletWarning) {
5748
+ glanceTest -= isAgendaSlide ? 1 : 2;
5749
+ glanceNotes.push(isAgendaSlide ? "Agenda with many items" : "Multiple bullets - needs careful structuring");
5584
5750
  }
5585
5751
  if (slideData.hasImage || slideData.hasChart) {
5586
5752
  glanceTest += 1;
@@ -7675,13 +7841,63 @@ var PowerPointGenerator = class {
7675
7841
 
7676
7842
  // src/generators/PDFGenerator.ts
7677
7843
  var import_puppeteer = __toESM(require("puppeteer"));
7844
+ var cachedBrowser = null;
7845
+ var browserIdleTimeout = null;
7846
+ var BROWSER_IDLE_MS = 3e4;
7678
7847
  var PDFGenerator = class {
7679
7848
  defaultOptions = {
7680
7849
  orientation: "landscape",
7681
7850
  printBackground: true,
7682
7851
  format: "Slide",
7683
- scale: 1
7852
+ scale: 1,
7853
+ reuseBrowser: true
7684
7854
  };
7855
+ /**
7856
+ * Get or create a browser instance
7857
+ */
7858
+ async getBrowser(reuse) {
7859
+ if (browserIdleTimeout) {
7860
+ clearTimeout(browserIdleTimeout);
7861
+ browserIdleTimeout = null;
7862
+ }
7863
+ if (reuse && cachedBrowser && cachedBrowser.connected) {
7864
+ logger.progress("Using cached browser instance");
7865
+ return cachedBrowser;
7866
+ }
7867
+ if (cachedBrowser && !cachedBrowser.connected) {
7868
+ cachedBrowser = null;
7869
+ }
7870
+ logger.progress("Launching new browser instance...");
7871
+ const browser = await import_puppeteer.default.launch({
7872
+ headless: true,
7873
+ args: [
7874
+ "--no-sandbox",
7875
+ "--disable-setuid-sandbox",
7876
+ "--disable-dev-shm-usage",
7877
+ "--disable-gpu"
7878
+ ]
7879
+ });
7880
+ if (reuse) {
7881
+ cachedBrowser = browser;
7882
+ }
7883
+ return browser;
7884
+ }
7885
+ /**
7886
+ * Schedule browser cleanup after idle period
7887
+ */
7888
+ scheduleBrowserCleanup() {
7889
+ if (browserIdleTimeout) {
7890
+ clearTimeout(browserIdleTimeout);
7891
+ }
7892
+ browserIdleTimeout = setTimeout(async () => {
7893
+ if (cachedBrowser) {
7894
+ logger.progress("Closing idle browser instance");
7895
+ await cachedBrowser.close().catch(() => {
7896
+ });
7897
+ cachedBrowser = null;
7898
+ }
7899
+ }, BROWSER_IDLE_MS);
7900
+ }
7685
7901
  /**
7686
7902
  * Generate PDF from HTML presentation
7687
7903
  * @param html - The HTML content of the presentation
@@ -7690,18 +7906,14 @@ var PDFGenerator = class {
7690
7906
  */
7691
7907
  async generate(html, options = {}) {
7692
7908
  const opts = { ...this.defaultOptions, ...options };
7909
+ const reuseBrowser = opts.reuseBrowser ?? true;
7693
7910
  logger.progress("\u{1F4C4} Generating PDF...");
7694
- let browser;
7911
+ let browser = null;
7912
+ let shouldCloseBrowser = !reuseBrowser;
7913
+ let page = null;
7695
7914
  try {
7696
- browser = await import_puppeteer.default.launch({
7697
- headless: true,
7698
- args: [
7699
- "--no-sandbox",
7700
- "--disable-setuid-sandbox",
7701
- "--disable-dev-shm-usage"
7702
- ]
7703
- });
7704
- const page = await browser.newPage();
7915
+ browser = await this.getBrowser(reuseBrowser);
7916
+ page = await browser.newPage();
7705
7917
  const printHtml = this.preparePrintHtml(html);
7706
7918
  await page.setViewport({
7707
7919
  width: 1920,
@@ -7737,13 +7949,38 @@ var PDFGenerator = class {
7737
7949
  } catch (error) {
7738
7950
  const errorMessage = error instanceof Error ? error.message : String(error);
7739
7951
  logger.error(`PDF generation failed: ${errorMessage}`);
7952
+ shouldCloseBrowser = true;
7740
7953
  throw new Error(`PDF generation failed: ${errorMessage}`);
7741
7954
  } finally {
7742
- if (browser) {
7743
- await browser.close();
7955
+ if (page) {
7956
+ await page.close().catch(() => {
7957
+ });
7958
+ }
7959
+ if (shouldCloseBrowser && browser) {
7960
+ await browser.close().catch(() => {
7961
+ });
7962
+ if (browser === cachedBrowser) {
7963
+ cachedBrowser = null;
7964
+ }
7965
+ } else if (reuseBrowser) {
7966
+ this.scheduleBrowserCleanup();
7744
7967
  }
7745
7968
  }
7746
7969
  }
7970
+ /**
7971
+ * Manually close the cached browser (call before process exit)
7972
+ */
7973
+ static async closeBrowser() {
7974
+ if (browserIdleTimeout) {
7975
+ clearTimeout(browserIdleTimeout);
7976
+ browserIdleTimeout = null;
7977
+ }
7978
+ if (cachedBrowser) {
7979
+ await cachedBrowser.close().catch(() => {
7980
+ });
7981
+ cachedBrowser = null;
7982
+ }
7983
+ }
7747
7984
  /**
7748
7985
  * Prepare HTML for print/PDF output
7749
7986
  * Modifies the HTML to work better as a printed document
@@ -7757,12 +7994,24 @@ var PDFGenerator = class {
7757
7994
  margin: 0;
7758
7995
  }
7759
7996
 
7997
+ /* Font rendering optimization for print */
7998
+ * {
7999
+ -webkit-font-smoothing: antialiased;
8000
+ -moz-osx-font-smoothing: grayscale;
8001
+ text-rendering: optimizeLegibility;
8002
+ font-feature-settings: "liga" 1, "kern" 1;
8003
+ }
8004
+
7760
8005
  @media print {
7761
8006
  html, body {
7762
8007
  margin: 0;
7763
8008
  padding: 0;
7764
8009
  width: 1920px;
7765
8010
  height: 1080px;
8011
+ /* Print color optimization */
8012
+ color-adjust: exact;
8013
+ -webkit-print-color-adjust: exact;
8014
+ print-color-adjust: exact;
7766
8015
  }
7767
8016
 
7768
8017
  .reveal .slides {
@@ -7779,7 +8028,8 @@ var PDFGenerator = class {
7779
8028
  width: 1920px !important;
7780
8029
  height: 1080px !important;
7781
8030
  margin: 0 !important;
7782
- padding: 60px !important;
8031
+ /* Professional safe margins: 80px (4.2%) */
8032
+ padding: 80px !important;
7783
8033
  box-sizing: border-box;
7784
8034
  position: relative !important;
7785
8035
  top: auto !important;
@@ -7803,7 +8053,7 @@ var PDFGenerator = class {
7803
8053
  }
7804
8054
 
7805
8055
  /* Disable animations for print */
7806
- * {
8056
+ *, *::before, *::after {
7807
8057
  animation: none !important;
7808
8058
  transition: none !important;
7809
8059
  }
@@ -7819,6 +8069,30 @@ var PDFGenerator = class {
7819
8069
  .reveal .navigate-down {
7820
8070
  display: none !important;
7821
8071
  }
8072
+
8073
+ /* Typography refinements for print */
8074
+ h1, h2, h3, h4 {
8075
+ orphans: 3;
8076
+ widows: 3;
8077
+ page-break-after: avoid;
8078
+ }
8079
+
8080
+ p, li {
8081
+ orphans: 2;
8082
+ widows: 2;
8083
+ }
8084
+
8085
+ /* Ensure links are readable in print */
8086
+ a {
8087
+ text-decoration: none;
8088
+ }
8089
+
8090
+ /* Prevent image overflow */
8091
+ img {
8092
+ max-width: 100%;
8093
+ height: auto;
8094
+ page-break-inside: avoid;
8095
+ }
7822
8096
  }
7823
8097
 
7824
8098
  /* Force print mode in Puppeteer */
@@ -7826,6 +8100,11 @@ var PDFGenerator = class {
7826
8100
  page-break-after: always;
7827
8101
  page-break-inside: avoid;
7828
8102
  }
8103
+
8104
+ /* High contrast mode for business presentations */
8105
+ .reveal section {
8106
+ color-scheme: light;
8107
+ }
7829
8108
  </style>
7830
8109
  `;
7831
8110
  if (html.includes("</head>")) {
@@ -8419,6 +8698,7 @@ var index_default = {
8419
8698
  CompositeImageProvider,
8420
8699
  ContentAnalyzer,
8421
8700
  ContentPatternClassifier,
8701
+ KnowledgeBaseError,
8422
8702
  KnowledgeGateway,
8423
8703
  LocalImageProvider,
8424
8704
  MermaidProvider,