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.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
  }
@@ -1053,7 +1117,7 @@ var ContentAnalyzer = class {
1053
1117
  logger.step(`Detected type: ${detectedType}`);
1054
1118
  const sections = this.extractSections(text);
1055
1119
  logger.step(`Found ${sections.length} sections`);
1056
- const scqa = this.extractSCQA(text);
1120
+ const scqa = this.extractSCQA(text, subtitle);
1057
1121
  const sparkline = this.extractSparkline(text);
1058
1122
  const keyMessages = this.extractKeyMessages(text);
1059
1123
  const dataPoints = this.extractDataPoints(text);
@@ -1320,15 +1384,24 @@ var ContentAnalyzer = class {
1320
1384
  /**
1321
1385
  * Extract SCQA structure (Barbara Minto)
1322
1386
  * CRITICAL: Answer must be clean prose, NOT bullet points
1387
+ * @param text - The content text to analyze
1388
+ * @param subtitle - Optional subtitle to exclude (prevents redundancy with title slide)
1323
1389
  */
1324
- extractSCQA(text) {
1390
+ extractSCQA(text, subtitle) {
1325
1391
  const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
1392
+ const normalizedSubtitle = (subtitle || "").toLowerCase().trim();
1393
+ const isSubtitle = (para) => {
1394
+ if (!normalizedSubtitle) return false;
1395
+ const normalized = para.toLowerCase().trim();
1396
+ return normalized === normalizedSubtitle || normalized.includes(normalizedSubtitle) || normalizedSubtitle.includes(normalized);
1397
+ };
1326
1398
  let situation = "";
1327
1399
  let complication = "";
1328
1400
  let question = "";
1329
1401
  let answer = "";
1330
1402
  for (const para of paragraphs.slice(0, 5)) {
1331
1403
  if (para.startsWith("-") || para.startsWith("#") || para.startsWith("|")) continue;
1404
+ if (isSubtitle(para)) continue;
1332
1405
  if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
1333
1406
  situation = this.extractFirstSentence(para);
1334
1407
  break;
@@ -1356,6 +1429,7 @@ var ContentAnalyzer = class {
1356
1429
  if (!situation && paragraphs.length > 0) {
1357
1430
  for (const para of paragraphs.slice(0, 5)) {
1358
1431
  if (!para.startsWith("-") && !para.startsWith("#") && para.length > 50) {
1432
+ if (isSubtitle(para)) continue;
1359
1433
  situation = this.extractFirstSentence(para);
1360
1434
  break;
1361
1435
  }
@@ -1802,7 +1876,8 @@ var ContentPatternClassifier = class {
1802
1876
  if (!match || !match[1]) return null;
1803
1877
  const value = match[1];
1804
1878
  const afterNumber = content.slice(content.indexOf(value) + value.length);
1805
- const context = afterNumber.trim().slice(0, 60).split(/[.!?]/)[0]?.trim() ?? "";
1879
+ const contextRaw = afterNumber.trim().split(/[\n\r]|-\s|[.!?]|(?=\d+%)|(?=\$\d)/)[0] || "";
1880
+ const context = contextRaw.trim().slice(0, 60);
1806
1881
  return { value, context };
1807
1882
  }
1808
1883
  /**
@@ -2499,6 +2574,16 @@ var SlideFactory = class {
2499
2574
  continue;
2500
2575
  }
2501
2576
  this.usedContent.add(contentKey);
2577
+ const normalizedHeader = section.header.toLowerCase().replace(/[^a-z0-9]/g, "");
2578
+ const normalizedTitle = analysis.title.toLowerCase().replace(/[^a-z0-9]/g, "");
2579
+ const normalizedSubtitle = (analysis.subtitle || "").toLowerCase().replace(/[^a-z0-9]/g, "");
2580
+ const normalizedContent = (section.content || "").toLowerCase().replace(/[^a-z0-9]/g, "");
2581
+ const headerMatchesTitle = normalizedHeader === normalizedTitle || normalizedTitle.includes(normalizedHeader) || normalizedHeader.includes(normalizedTitle);
2582
+ const contentMatchesSubtitle = normalizedContent && (normalizedContent === normalizedSubtitle || normalizedSubtitle.includes(normalizedContent) || normalizedContent.includes(normalizedSubtitle));
2583
+ if (headerMatchesTitle && (contentMatchesSubtitle || !section.content || section.content.length < 30)) {
2584
+ logger.warn(`Section "${section.header}" skipped - duplicates title slide`);
2585
+ continue;
2586
+ }
2502
2587
  const headerLower = section.header.toLowerCase();
2503
2588
  const isCTASection = headerLower.includes("call to action") || headerLower.includes("next step") || headerLower.includes("take action") || headerLower.includes("get started") || headerLower.includes("start today");
2504
2589
  if (isCTASection) {
@@ -2678,47 +2763,69 @@ var SlideFactory = class {
2678
2763
  const scqa = analysis.scqa;
2679
2764
  const titles = this.config.scqaTitles;
2680
2765
  const minBodyLength = 20;
2766
+ const normalizedTitle = analysis.title.toLowerCase().replace(/[^a-z0-9]/g, "");
2767
+ const normalizedSubtitle = (analysis.subtitle || "").toLowerCase().replace(/[^a-z0-9]/g, "");
2768
+ const duplicatesHeadline = (body) => {
2769
+ if (!body) return true;
2770
+ const normalizedBody = body.toLowerCase().replace(/[^a-z0-9]/g, "");
2771
+ if (!normalizedBody || normalizedBody.length < 10) return false;
2772
+ const titleMatch = normalizedTitle.length > 5 && (normalizedBody === normalizedTitle || normalizedTitle.includes(normalizedBody) || normalizedBody.includes(normalizedTitle));
2773
+ const subtitleMatch = normalizedSubtitle.length > 5 && (normalizedBody === normalizedSubtitle || normalizedSubtitle.includes(normalizedBody) || normalizedBody.includes(normalizedSubtitle));
2774
+ return titleMatch || subtitleMatch;
2775
+ };
2681
2776
  const situationBody = scqa?.situation ? this.truncateText(scqa.situation, this.config.rules.wordsPerSlide.max) : "";
2682
2777
  if (situationBody.length >= minBodyLength && !this.usedContent.has("scqa-situation")) {
2683
- this.usedContent.add("scqa-situation");
2684
- slides.push({
2685
- index: slides.length,
2686
- type: "single-statement",
2687
- data: {
2688
- title: titles.situation,
2689
- // FROM KB - not hardcoded 'Current Situation'
2690
- body: situationBody
2691
- },
2692
- classes: ["situation-slide"]
2693
- });
2778
+ if (duplicatesHeadline(situationBody)) {
2779
+ logger.warn(`SCQA Situation skipped - duplicates title/subtitle: "${situationBody.slice(0, 50)}..."`);
2780
+ } else {
2781
+ this.usedContent.add("scqa-situation");
2782
+ slides.push({
2783
+ index: slides.length,
2784
+ type: "single-statement",
2785
+ data: {
2786
+ title: titles.situation,
2787
+ // FROM KB - not hardcoded 'Current Situation'
2788
+ body: situationBody
2789
+ },
2790
+ classes: ["situation-slide"]
2791
+ });
2792
+ }
2694
2793
  }
2695
2794
  const complicationBody = scqa?.complication ? this.truncateText(scqa.complication, this.config.rules.wordsPerSlide.max) : "";
2696
2795
  if (complicationBody.length >= minBodyLength && !this.usedContent.has("scqa-complication")) {
2697
- this.usedContent.add("scqa-complication");
2698
- slides.push({
2699
- index: slides.length,
2700
- type: "single-statement",
2701
- data: {
2702
- title: titles.complication,
2703
- // FROM KB - not hardcoded 'The Challenge'
2704
- body: complicationBody
2705
- },
2706
- classes: ["complication-slide"]
2707
- });
2796
+ if (duplicatesHeadline(complicationBody)) {
2797
+ logger.warn(`SCQA Complication skipped - duplicates title/subtitle`);
2798
+ } else {
2799
+ this.usedContent.add("scqa-complication");
2800
+ slides.push({
2801
+ index: slides.length,
2802
+ type: "single-statement",
2803
+ data: {
2804
+ title: titles.complication,
2805
+ // FROM KB - not hardcoded 'The Challenge'
2806
+ body: complicationBody
2807
+ },
2808
+ classes: ["complication-slide"]
2809
+ });
2810
+ }
2708
2811
  }
2709
2812
  const questionBody = scqa?.question ? this.truncateText(scqa.question, this.config.rules.wordsPerSlide.max) : "";
2710
2813
  if (questionBody.length >= minBodyLength && !this.usedContent.has("scqa-question")) {
2711
- this.usedContent.add("scqa-question");
2712
- slides.push({
2713
- index: slides.length,
2714
- type: "single-statement",
2715
- data: {
2716
- title: titles.question,
2717
- // FROM KB - not hardcoded 'The Question'
2718
- body: questionBody
2719
- },
2720
- classes: ["question-slide"]
2721
- });
2814
+ if (duplicatesHeadline(questionBody)) {
2815
+ logger.warn(`SCQA Question skipped - duplicates title/subtitle`);
2816
+ } else {
2817
+ this.usedContent.add("scqa-question");
2818
+ slides.push({
2819
+ index: slides.length,
2820
+ type: "single-statement",
2821
+ data: {
2822
+ title: titles.question,
2823
+ // FROM KB - not hardcoded 'The Question'
2824
+ body: questionBody
2825
+ },
2826
+ classes: ["question-slide"]
2827
+ });
2828
+ }
2722
2829
  }
2723
2830
  }
2724
2831
  addSparklineSlides(slides, analysis) {
@@ -2771,14 +2878,41 @@ var SlideFactory = class {
2771
2878
  // CONTENT SLIDES - ALL values from KB
2772
2879
  // ===========================================================================
2773
2880
  createBigNumberSlide(index, section, pattern) {
2774
- const fullContent = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
2775
- const bigNumber = this.classifier.extractBigNumber(fullContent);
2881
+ let bigNumber = null;
2882
+ let sourceBullet = null;
2883
+ for (const bullet of section.bullets) {
2884
+ const extracted = this.classifier.extractBigNumber(bullet);
2885
+ if (extracted) {
2886
+ bigNumber = extracted;
2887
+ sourceBullet = bullet;
2888
+ break;
2889
+ }
2890
+ }
2891
+ if (!bigNumber) {
2892
+ bigNumber = this.classifier.extractBigNumber(section.header);
2893
+ }
2894
+ if (!bigNumber && section.content) {
2895
+ bigNumber = this.classifier.extractBigNumber(section.content);
2896
+ }
2776
2897
  const actualValue = bigNumber?.value || pattern.bigNumberValue;
2777
2898
  if (!actualValue) {
2778
2899
  logger.warn(`No number found for big-number slide, falling back to single-statement`);
2779
2900
  return this.createSingleStatementSlide(index, section);
2780
2901
  }
2781
- const contextContent = bigNumber?.context || section.content;
2902
+ let contextContent = bigNumber?.context || "";
2903
+ if (sourceBullet && !contextContent) {
2904
+ contextContent = sourceBullet.replace(actualValue, "").trim();
2905
+ }
2906
+ if (!contextContent) {
2907
+ contextContent = section.content || section.header;
2908
+ }
2909
+ let cleanTitle = section.header;
2910
+ if (actualValue && cleanTitle.includes(actualValue.replace(/[,$%]/g, ""))) {
2911
+ cleanTitle = cleanTitle.replace(new RegExp(`\\$?${actualValue.replace(/[,$%]/g, "").replace(/\./g, "\\.?")}[KMBkmb]?`, "gi"), "").replace(/\s+/g, " ").trim();
2912
+ }
2913
+ if (!cleanTitle || cleanTitle.length < 5) {
2914
+ cleanTitle = bigNumber?.context?.split(/[.!?]/)[0] || section.header;
2915
+ }
2782
2916
  const craftedContext = this.craftExpertContent(
2783
2917
  contextContent,
2784
2918
  "big_number",
@@ -2788,7 +2922,7 @@ var SlideFactory = class {
2788
2922
  index,
2789
2923
  type: "big-number",
2790
2924
  data: {
2791
- title: this.createTitle(section.header, section),
2925
+ title: cleanTitle,
2792
2926
  keyMessage: actualValue,
2793
2927
  body: craftedContext
2794
2928
  },
@@ -2798,25 +2932,43 @@ var SlideFactory = class {
2798
2932
  createComparisonSlide(index, section) {
2799
2933
  const comparison = this.classifier.extractComparison(section);
2800
2934
  const labels = this.config.defaults.comparison;
2935
+ const title = this.createTitle(section.header, section);
2936
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
2801
2937
  const leftFallback = labels.optionLabels[0] ?? "Option A";
2802
2938
  const rightFallback = labels.optionLabels[1] ?? "Option B";
2803
- const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
2804
- const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
2939
+ let leftColumn = comparison?.left || section.bullets[0] || "";
2940
+ let rightColumn = comparison?.right || section.bullets[1] || "";
2941
+ const normalizedLeft = leftColumn.toLowerCase().replace(/[^a-z0-9]/g, "");
2942
+ const normalizedRight = rightColumn.toLowerCase().replace(/[^a-z0-9]/g, "");
2943
+ const leftIsDuplicate = normalizedLeft === normalizedTitle || normalizedTitle.includes(normalizedLeft);
2944
+ const rightIsDuplicate = normalizedRight === normalizedTitle || normalizedTitle.includes(normalizedRight);
2945
+ if (leftIsDuplicate || rightIsDuplicate || !leftColumn && !rightColumn) {
2946
+ if (section.bullets.length >= 2) {
2947
+ logger.warn(`Comparison slide "${title}" has duplicate content, using bullet points instead`);
2948
+ return this.createBulletSlide(index, section);
2949
+ }
2950
+ if (leftIsDuplicate && rightIsDuplicate) {
2951
+ logger.warn(`Skipping comparison slide "${title}" - content duplicates title`);
2952
+ return null;
2953
+ }
2954
+ if (leftIsDuplicate) leftColumn = leftFallback;
2955
+ if (rightIsDuplicate) rightColumn = rightFallback;
2956
+ }
2805
2957
  return {
2806
2958
  index,
2807
2959
  type: "comparison",
2808
2960
  data: {
2809
- title: this.createTitle(section.header, section),
2961
+ title,
2810
2962
  columns: [
2811
2963
  {
2812
2964
  title: labels.leftLabel,
2813
2965
  // FROM KB - not hardcoded 'Before'
2814
- content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
2966
+ content: this.truncateText(leftColumn || leftFallback, this.config.rules.wordsPerSlide.max)
2815
2967
  },
2816
2968
  {
2817
2969
  title: labels.rightLabel,
2818
2970
  // FROM KB - not hardcoded 'After'
2819
- content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
2971
+ content: this.truncateText(rightColumn || rightFallback, this.config.rules.wordsPerSlide.max)
2820
2972
  }
2821
2973
  ]
2822
2974
  },
@@ -3087,10 +3239,21 @@ var SlideFactory = class {
3087
3239
  * Used for section headers with strong conclusions.
3088
3240
  */
3089
3241
  createTitleImpactSlide(index, section) {
3242
+ const title = this.cleanText(section.header);
3090
3243
  const supportingText = section.content || section.bullets.slice(0, 2).join(". ");
3091
3244
  const truncatedSupport = this.truncateText(supportingText, this.config.defaults.context.maxWords);
3245
+ const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "");
3246
+ const normalizedBody = truncatedSupport.toLowerCase().replace(/[^a-z0-9]/g, "");
3247
+ const bodyIsDuplicate = normalizedBody === normalizedTitle || normalizedTitle.includes(normalizedBody) || normalizedBody.includes(normalizedTitle) || truncatedSupport.length < 10;
3248
+ if (bodyIsDuplicate) {
3249
+ if (section.bullets.length >= 2 && this.config.mode === "business") {
3250
+ return this.createBulletSlide(index, section);
3251
+ }
3252
+ logger.warn(`Skipping title-impact slide "${title}" - no distinct body content`);
3253
+ return null;
3254
+ }
3092
3255
  const data = {
3093
- title: this.cleanText(section.header)
3256
+ title
3094
3257
  };
3095
3258
  if (truncatedSupport) {
3096
3259
  data.body = truncatedSupport;
@@ -5506,12 +5669,15 @@ var VisualQualityEvaluator = class {
5506
5669
  glanceTest += 1;
5507
5670
  glanceNotes.push("Clean, minimal text - passes glance test easily");
5508
5671
  }
5509
- if (slideData.bullets.length > 5) {
5672
+ const isAgendaSlide = slideType === "agenda" || (slideData.classes || []).includes("agenda-slide");
5673
+ const bulletLimit = isAgendaSlide ? 8 : 5;
5674
+ const bulletWarning = isAgendaSlide ? 6 : 3;
5675
+ if (slideData.bullets.length > bulletLimit) {
5510
5676
  glanceTest -= 4;
5511
5677
  glanceNotes.push("Too many bullets - cannot scan in 3 seconds");
5512
- } else if (slideData.bullets.length > 3) {
5513
- glanceTest -= 2;
5514
- glanceNotes.push("Multiple bullets - needs careful structuring");
5678
+ } else if (slideData.bullets.length > bulletWarning) {
5679
+ glanceTest -= isAgendaSlide ? 1 : 2;
5680
+ glanceNotes.push(isAgendaSlide ? "Agenda with many items" : "Multiple bullets - needs careful structuring");
5515
5681
  }
5516
5682
  if (slideData.hasImage || slideData.hasChart) {
5517
5683
  glanceTest += 1;
@@ -7606,13 +7772,63 @@ var PowerPointGenerator = class {
7606
7772
 
7607
7773
  // src/generators/PDFGenerator.ts
7608
7774
  import puppeteer from "puppeteer";
7775
+ var cachedBrowser = null;
7776
+ var browserIdleTimeout = null;
7777
+ var BROWSER_IDLE_MS = 3e4;
7609
7778
  var PDFGenerator = class {
7610
7779
  defaultOptions = {
7611
7780
  orientation: "landscape",
7612
7781
  printBackground: true,
7613
7782
  format: "Slide",
7614
- scale: 1
7783
+ scale: 1,
7784
+ reuseBrowser: true
7615
7785
  };
7786
+ /**
7787
+ * Get or create a browser instance
7788
+ */
7789
+ async getBrowser(reuse) {
7790
+ if (browserIdleTimeout) {
7791
+ clearTimeout(browserIdleTimeout);
7792
+ browserIdleTimeout = null;
7793
+ }
7794
+ if (reuse && cachedBrowser && cachedBrowser.connected) {
7795
+ logger.progress("Using cached browser instance");
7796
+ return cachedBrowser;
7797
+ }
7798
+ if (cachedBrowser && !cachedBrowser.connected) {
7799
+ cachedBrowser = null;
7800
+ }
7801
+ logger.progress("Launching new browser instance...");
7802
+ const browser = await puppeteer.launch({
7803
+ headless: true,
7804
+ args: [
7805
+ "--no-sandbox",
7806
+ "--disable-setuid-sandbox",
7807
+ "--disable-dev-shm-usage",
7808
+ "--disable-gpu"
7809
+ ]
7810
+ });
7811
+ if (reuse) {
7812
+ cachedBrowser = browser;
7813
+ }
7814
+ return browser;
7815
+ }
7816
+ /**
7817
+ * Schedule browser cleanup after idle period
7818
+ */
7819
+ scheduleBrowserCleanup() {
7820
+ if (browserIdleTimeout) {
7821
+ clearTimeout(browserIdleTimeout);
7822
+ }
7823
+ browserIdleTimeout = setTimeout(async () => {
7824
+ if (cachedBrowser) {
7825
+ logger.progress("Closing idle browser instance");
7826
+ await cachedBrowser.close().catch(() => {
7827
+ });
7828
+ cachedBrowser = null;
7829
+ }
7830
+ }, BROWSER_IDLE_MS);
7831
+ }
7616
7832
  /**
7617
7833
  * Generate PDF from HTML presentation
7618
7834
  * @param html - The HTML content of the presentation
@@ -7621,18 +7837,14 @@ var PDFGenerator = class {
7621
7837
  */
7622
7838
  async generate(html, options = {}) {
7623
7839
  const opts = { ...this.defaultOptions, ...options };
7840
+ const reuseBrowser = opts.reuseBrowser ?? true;
7624
7841
  logger.progress("\u{1F4C4} Generating PDF...");
7625
- let browser;
7842
+ let browser = null;
7843
+ let shouldCloseBrowser = !reuseBrowser;
7844
+ let page = null;
7626
7845
  try {
7627
- browser = await puppeteer.launch({
7628
- headless: true,
7629
- args: [
7630
- "--no-sandbox",
7631
- "--disable-setuid-sandbox",
7632
- "--disable-dev-shm-usage"
7633
- ]
7634
- });
7635
- const page = await browser.newPage();
7846
+ browser = await this.getBrowser(reuseBrowser);
7847
+ page = await browser.newPage();
7636
7848
  const printHtml = this.preparePrintHtml(html);
7637
7849
  await page.setViewport({
7638
7850
  width: 1920,
@@ -7668,13 +7880,38 @@ var PDFGenerator = class {
7668
7880
  } catch (error) {
7669
7881
  const errorMessage = error instanceof Error ? error.message : String(error);
7670
7882
  logger.error(`PDF generation failed: ${errorMessage}`);
7883
+ shouldCloseBrowser = true;
7671
7884
  throw new Error(`PDF generation failed: ${errorMessage}`);
7672
7885
  } finally {
7673
- if (browser) {
7674
- await browser.close();
7886
+ if (page) {
7887
+ await page.close().catch(() => {
7888
+ });
7889
+ }
7890
+ if (shouldCloseBrowser && browser) {
7891
+ await browser.close().catch(() => {
7892
+ });
7893
+ if (browser === cachedBrowser) {
7894
+ cachedBrowser = null;
7895
+ }
7896
+ } else if (reuseBrowser) {
7897
+ this.scheduleBrowserCleanup();
7675
7898
  }
7676
7899
  }
7677
7900
  }
7901
+ /**
7902
+ * Manually close the cached browser (call before process exit)
7903
+ */
7904
+ static async closeBrowser() {
7905
+ if (browserIdleTimeout) {
7906
+ clearTimeout(browserIdleTimeout);
7907
+ browserIdleTimeout = null;
7908
+ }
7909
+ if (cachedBrowser) {
7910
+ await cachedBrowser.close().catch(() => {
7911
+ });
7912
+ cachedBrowser = null;
7913
+ }
7914
+ }
7678
7915
  /**
7679
7916
  * Prepare HTML for print/PDF output
7680
7917
  * Modifies the HTML to work better as a printed document
@@ -7688,12 +7925,24 @@ var PDFGenerator = class {
7688
7925
  margin: 0;
7689
7926
  }
7690
7927
 
7928
+ /* Font rendering optimization for print */
7929
+ * {
7930
+ -webkit-font-smoothing: antialiased;
7931
+ -moz-osx-font-smoothing: grayscale;
7932
+ text-rendering: optimizeLegibility;
7933
+ font-feature-settings: "liga" 1, "kern" 1;
7934
+ }
7935
+
7691
7936
  @media print {
7692
7937
  html, body {
7693
7938
  margin: 0;
7694
7939
  padding: 0;
7695
7940
  width: 1920px;
7696
7941
  height: 1080px;
7942
+ /* Print color optimization */
7943
+ color-adjust: exact;
7944
+ -webkit-print-color-adjust: exact;
7945
+ print-color-adjust: exact;
7697
7946
  }
7698
7947
 
7699
7948
  .reveal .slides {
@@ -7710,7 +7959,8 @@ var PDFGenerator = class {
7710
7959
  width: 1920px !important;
7711
7960
  height: 1080px !important;
7712
7961
  margin: 0 !important;
7713
- padding: 60px !important;
7962
+ /* Professional safe margins: 80px (4.2%) */
7963
+ padding: 80px !important;
7714
7964
  box-sizing: border-box;
7715
7965
  position: relative !important;
7716
7966
  top: auto !important;
@@ -7734,7 +7984,7 @@ var PDFGenerator = class {
7734
7984
  }
7735
7985
 
7736
7986
  /* Disable animations for print */
7737
- * {
7987
+ *, *::before, *::after {
7738
7988
  animation: none !important;
7739
7989
  transition: none !important;
7740
7990
  }
@@ -7750,6 +8000,30 @@ var PDFGenerator = class {
7750
8000
  .reveal .navigate-down {
7751
8001
  display: none !important;
7752
8002
  }
8003
+
8004
+ /* Typography refinements for print */
8005
+ h1, h2, h3, h4 {
8006
+ orphans: 3;
8007
+ widows: 3;
8008
+ page-break-after: avoid;
8009
+ }
8010
+
8011
+ p, li {
8012
+ orphans: 2;
8013
+ widows: 2;
8014
+ }
8015
+
8016
+ /* Ensure links are readable in print */
8017
+ a {
8018
+ text-decoration: none;
8019
+ }
8020
+
8021
+ /* Prevent image overflow */
8022
+ img {
8023
+ max-width: 100%;
8024
+ height: auto;
8025
+ page-break-inside: avoid;
8026
+ }
7753
8027
  }
7754
8028
 
7755
8029
  /* Force print mode in Puppeteer */
@@ -7757,6 +8031,11 @@ var PDFGenerator = class {
7757
8031
  page-break-after: always;
7758
8032
  page-break-inside: avoid;
7759
8033
  }
8034
+
8035
+ /* High contrast mode for business presentations */
8036
+ .reveal section {
8037
+ color-scheme: light;
8038
+ }
7760
8039
  </style>
7761
8040
  `;
7762
8041
  if (html.includes("</head>")) {
@@ -8349,6 +8628,7 @@ export {
8349
8628
  CompositeImageProvider,
8350
8629
  ContentAnalyzer,
8351
8630
  ContentPatternClassifier,
8631
+ KnowledgeBaseError,
8352
8632
  KnowledgeGateway,
8353
8633
  LocalImageProvider,
8354
8634
  MermaidProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-presentation-master",
3
- "version": "8.1.0",
3
+ "version": "8.3.0",
4
4
  "description": "Generate world-class presentations using expert methodologies from Duarte, Reynolds, Gallo, and Anderson. Enforces rigorous quality standards through real visual validation.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",