claude-presentation-master 6.0.0 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -594,27 +594,65 @@ var ContentAnalyzer = class {
594
594
  }
595
595
  /**
596
596
  * Extract STAR moments (Something They'll Always Remember)
597
- * Per Nancy Duarte - these are emotional peaks in the presentation
597
+ * Per Nancy Duarte: These should be memorable, COMPLETE thoughts with impact
598
+ *
599
+ * CRITICAL REQUIREMENTS:
600
+ * - Must be complete sentences with subject + verb structure
601
+ * - Must have 5+ words minimum (no fragments!)
602
+ * - Must be 30+ characters
603
+ * - Must contain a verb
604
+ * - NOT fragments like "significant growth" or "cloud-first strategy"
605
+ * - NOT headers or topic labels
598
606
  */
599
607
  extractStarMoments(text) {
600
608
  const starMoments = [];
601
- const boldMatches = text.match(/\*\*(.+?)\*\*/g);
609
+ const usedPhrases = /* @__PURE__ */ new Set();
610
+ const fragmentPatterns = [
611
+ /^[a-z]+-[a-z]+\s+(strategy|approach|solution|platform|architecture)$/i,
612
+ // "cloud-first strategy"
613
+ /^(significant|substantial|dramatic|rapid|strong)\s+(growth|increase|decline|change)$/i,
614
+ // "significant growth"
615
+ /^(digital|cloud|data|ai)\s+(transformation|strategy|platform|solution)$/i,
616
+ // "digital transformation"
617
+ /^(our|your|the|a)\s+\w+\s*$/i,
618
+ // "our solution", "the platform"
619
+ /^[a-z]+\s+[a-z]+$/i
620
+ // Any two-word phrase
621
+ ];
622
+ const verbPatterns = /\b(is|are|was|were|will|can|could|should|must|has|have|had|does|do|provides?|enables?|allows?|offers?|delivers?|creates?|achieves?|exceeds?|results?|generates?|grows?|increases?|decreases?|transforms?|improves?|reduces?)\b/i;
623
+ const boldMatches = text.match(/\*\*([^*]+)\*\*/g);
602
624
  if (boldMatches) {
603
- for (const match of boldMatches.slice(0, 5)) {
625
+ for (const match of boldMatches) {
604
626
  const cleaned = match.replace(/\*\*/g, "").trim();
605
627
  const lowerCleaned = cleaned.toLowerCase();
606
- if (cleaned.length > 15 && cleaned.length < 100 && !this.containsSignals(lowerCleaned, this.ctaSignals) && !/^\d+[%x]?\s*$/.test(cleaned)) {
628
+ const wordCount = cleaned.split(/\s+/).length;
629
+ const normalized = cleaned.toLowerCase().slice(0, 30);
630
+ const hasVerb = verbPatterns.test(cleaned);
631
+ const isLongEnough = wordCount >= 5 && cleaned.length >= 30;
632
+ const isNotFragment = !fragmentPatterns.some((p) => p.test(cleaned));
633
+ const isNotHeader = !/^(The |Our |Your |Next |Key |Overview|Introduction|Conclusion|Summary|Background)/i.test(cleaned);
634
+ const isNotCTA = !this.containsSignals(lowerCleaned, this.ctaSignals);
635
+ const hasMetricContext = /\d+[%xX]|\$[\d,]+/.test(cleaned) && wordCount >= 5;
636
+ if ((hasVerb || hasMetricContext) && isLongEnough && isNotFragment && isNotHeader && isNotCTA && wordCount <= 25 && !usedPhrases.has(normalized)) {
607
637
  starMoments.push(cleaned);
638
+ usedPhrases.add(normalized);
639
+ if (starMoments.length >= 3) break;
608
640
  }
609
641
  }
610
642
  }
611
- const exclamations = text.match(/[^.!?]*![^.!?]*/g);
612
- if (exclamations) {
613
- for (const ex of exclamations.slice(0, 2)) {
614
- const cleaned = ex.trim();
615
- const lowerCleaned = cleaned.toLowerCase();
616
- if (cleaned.length > 10 && cleaned.length < 100 && !starMoments.includes(cleaned) && !this.containsSignals(lowerCleaned, this.ctaSignals)) {
643
+ if (starMoments.length < 3) {
644
+ const cleanedText = text.replace(/^#+\s+.+$/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\n{2,}/g, "\n").trim();
645
+ const sentences = cleanedText.match(/[^.!?]+[.!?]/g) || [];
646
+ for (const sentence of sentences) {
647
+ const cleaned = sentence.trim();
648
+ const normalized = cleaned.toLowerCase().slice(0, 30);
649
+ const wordCount = cleaned.split(/\s+/).length;
650
+ const hasHighImpact = /(\d{2,3}%|\d+x|billion|million|\$[\d,]+\s*(million|billion)?)/i.test(cleaned);
651
+ const hasVerb = verbPatterns.test(cleaned);
652
+ if (hasHighImpact && hasVerb && wordCount >= 6 && wordCount <= 25 && cleaned.length >= 40 && cleaned.length <= 200 && !usedPhrases.has(normalized) && !this.containsSignals(cleaned.toLowerCase(), this.ctaSignals)) {
617
653
  starMoments.push(cleaned);
654
+ usedPhrases.add(normalized);
655
+ if (starMoments.length >= 3) break;
618
656
  }
619
657
  }
620
658
  }
@@ -636,28 +674,65 @@ var ContentAnalyzer = class {
636
674
  return { callToAction };
637
675
  }
638
676
  /**
639
- * Extract key messages (max 3 - Rule of Three)
677
+ * Extract key messages - the actual insights from the content
678
+ * Per Carmine Gallo: Rule of Three - max 3 key messages
679
+ * Per Barbara Minto: Messages should be actionable conclusions, not topics
640
680
  */
641
681
  extractKeyMessages(text) {
642
682
  const messages = [];
643
- const h2Matches = text.match(/^##\s+(.+)$/gm);
644
- if (h2Matches) {
645
- for (const match of h2Matches.slice(0, 3)) {
646
- const msg = match.replace(/^##\s+/, "").replace(/\*\*/g, "").trim();
647
- if (msg.length > 5 && msg.length < 80) {
648
- messages.push(msg);
683
+ const usedPhrases = /* @__PURE__ */ new Set();
684
+ let cleanText = text.replace(/^#+\s+.+$/gm, "").replace(/\[.+?\]/g, "").replace(/\*\*/g, "");
685
+ cleanText = cleanText.replace(/(\d)\.(\d)/g, "$1<DECIMAL>$2");
686
+ const rawSentences = cleanText.match(/[^.!?]+[.!?]/g) || [];
687
+ const sentences = rawSentences.map((s) => s.replace(/<DECIMAL>/g, "."));
688
+ const insightSignals = [
689
+ "we recommend",
690
+ "we suggest",
691
+ "our strategy",
692
+ "the solution",
693
+ "key finding",
694
+ "result",
695
+ "achieve",
696
+ "improve",
697
+ "increase",
698
+ "decrease",
699
+ "expect",
700
+ "roi",
701
+ "benefit",
702
+ "opportunity",
703
+ "transform"
704
+ ];
705
+ for (const sentence of sentences) {
706
+ const cleaned = sentence.trim();
707
+ const normalized = cleaned.toLowerCase().slice(0, 30);
708
+ if (cleaned.length > 30 && cleaned.length < 150 && !usedPhrases.has(normalized) && !this.containsSignals(cleaned.toLowerCase(), this.ctaSignals)) {
709
+ if (this.containsSignals(cleaned.toLowerCase(), insightSignals)) {
710
+ messages.push(cleaned);
711
+ usedPhrases.add(normalized);
712
+ if (messages.length >= 3) break;
649
713
  }
650
714
  }
651
715
  }
652
716
  if (messages.length < 3) {
653
- const boldMatches = text.match(/\*\*(.+?)\*\*/g);
654
- if (boldMatches) {
655
- for (const match of boldMatches) {
656
- const msg = match.replace(/\*\*/g, "").trim();
657
- if (msg.length > 10 && msg.length < 80 && !messages.includes(msg)) {
658
- messages.push(msg);
659
- if (messages.length >= 3) break;
660
- }
717
+ for (const sentence of sentences) {
718
+ const cleaned = sentence.trim();
719
+ const normalized = cleaned.toLowerCase().slice(0, 30);
720
+ if (cleaned.length > 25 && cleaned.length < 150 && !usedPhrases.has(normalized) && /\d+[%xX]|\$[\d,]+/.test(cleaned)) {
721
+ messages.push(cleaned);
722
+ usedPhrases.add(normalized);
723
+ if (messages.length >= 3) break;
724
+ }
725
+ }
726
+ }
727
+ if (messages.length < 2) {
728
+ for (const sentence of sentences) {
729
+ const cleaned = sentence.trim();
730
+ const wordCount = cleaned.split(/\s+/).length;
731
+ const normalized = cleaned.toLowerCase().slice(0, 30);
732
+ if (wordCount >= 6 && wordCount <= 25 && !usedPhrases.has(normalized) && /\b(is|are|will|can|should|must|has|have|provides?|enables?|allows?)\b/.test(cleaned.toLowerCase())) {
733
+ messages.push(cleaned);
734
+ usedPhrases.add(normalized);
735
+ if (messages.length >= 3) break;
661
736
  }
662
737
  }
663
738
  }
@@ -665,51 +740,56 @@ var ContentAnalyzer = class {
665
740
  }
666
741
  /**
667
742
  * Extract data points (metrics with values)
743
+ * IMPROVED: Smarter label extraction that understands markdown tables
668
744
  */
669
745
  extractDataPoints(text) {
670
746
  const dataPoints = [];
671
- const dollarMatches = text.match(/\$[\d,]+(?:\.\d+)?[MBK]?(?:\s*(?:million|billion|thousand))?/gi);
672
- if (dollarMatches) {
673
- for (const match of dollarMatches.slice(0, 4)) {
674
- const context = this.getContextAroundMatch(text, match);
675
- dataPoints.push({
676
- value: match,
677
- label: this.extractLabelFromContext(context)
678
- });
747
+ const usedValues = /* @__PURE__ */ new Set();
748
+ const tableLines = text.split("\n").filter((line) => line.includes("|"));
749
+ if (tableLines.length >= 3) {
750
+ const dataRows = tableLines.filter((line) => !line.match(/^[\s|:-]+$/));
751
+ if (dataRows.length >= 2) {
752
+ for (const row of dataRows.slice(1)) {
753
+ const cells = row.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
754
+ if (cells.length >= 2) {
755
+ const valueCell = cells.find((c) => /^\$?[\d,]+\.?\d*[MBK%]?$/.test(c.replace(/[,$]/g, "")));
756
+ const labelCell = cells[0];
757
+ if (valueCell && labelCell && !usedValues.has(valueCell)) {
758
+ usedValues.add(valueCell);
759
+ dataPoints.push({ value: valueCell, label: labelCell.slice(0, 40) });
760
+ }
761
+ }
762
+ }
679
763
  }
680
764
  }
681
- const percentMatches = text.match(/\d+(?:\.\d+)?%/g);
682
- if (percentMatches) {
683
- for (const match of percentMatches.slice(0, 4)) {
684
- if (!dataPoints.some((d) => d.value === match)) {
685
- const context = this.getContextAroundMatch(text, match);
686
- dataPoints.push({
687
- value: match,
688
- label: this.extractLabelFromContext(context)
689
- });
765
+ const lines = text.split("\n");
766
+ for (const line of lines) {
767
+ if (line.includes("|")) continue;
768
+ const percentMatch = line.match(/(\w+(?:\s+\w+){0,4})\s+(?:by\s+)?(\d+(?:\.\d+)?%)/i);
769
+ if (percentMatch && percentMatch[2] && percentMatch[1] && !usedValues.has(percentMatch[2])) {
770
+ usedValues.add(percentMatch[2]);
771
+ dataPoints.push({ value: percentMatch[2], label: percentMatch[1].slice(0, 40) });
772
+ }
773
+ const dollarMatch = line.match(/\$(\d+(?:\.\d+)?)\s*(million|billion|M|B|K)?/i);
774
+ if (dollarMatch && dataPoints.length < 6) {
775
+ const fullValue = "$" + dollarMatch[1] + (dollarMatch[2] ? " " + dollarMatch[2] : "");
776
+ if (!usedValues.has(fullValue)) {
777
+ usedValues.add(fullValue);
778
+ const label = this.extractLabelFromLine(line, fullValue);
779
+ dataPoints.push({ value: fullValue, label });
690
780
  }
691
781
  }
692
782
  }
693
- return dataPoints.slice(0, 6);
783
+ return dataPoints.slice(0, 4);
694
784
  }
695
785
  /**
696
- * Get context around a match
786
+ * Extract a meaningful label from a line containing a metric
697
787
  */
698
- getContextAroundMatch(text, match) {
699
- const index = text.indexOf(match);
700
- if (index === -1) return "";
701
- const start = Math.max(0, index - 50);
702
- const end = Math.min(text.length, index + match.length + 50);
703
- return text.slice(start, end);
704
- }
705
- /**
706
- * Extract a label from surrounding context
707
- * Fixes the "Century Interactive |" garbage issue by stripping table syntax
708
- */
709
- extractLabelFromContext(context) {
710
- let cleaned = context.replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\s{2,}/g, " ").replace(/\*\*/g, "").replace(/#+\s*/g, "").trim();
711
- const words = cleaned.split(/\s+/).filter((w) => w.length > 2 && !w.match(/^\d/));
712
- return words.slice(0, 4).join(" ") || "Value";
788
+ extractLabelFromLine(line, value) {
789
+ const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
790
+ const beforeValue = cleaned.split(value)[0] || "";
791
+ const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
792
+ return words.slice(-4).join(" ").slice(0, 40) || "Value";
713
793
  }
714
794
  /**
715
795
  * Check if text contains any of the signals
@@ -719,15 +799,20 @@ var ContentAnalyzer = class {
719
799
  }
720
800
  /**
721
801
  * Extract first meaningful sentence from text
802
+ * CRITICAL: Must strip headers, CTA text, and fragments
722
803
  */
723
804
  extractFirstSentence(text) {
724
- const cleaned = text.replace(/^#+\s*/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").trim();
725
- const sentences = cleaned.split(/[.!?]+/);
726
- const first = sentences[0]?.trim();
727
- if (first && first.length > 10) {
728
- return first.slice(0, 150);
805
+ let cleaned = text.replace(/^#+\s+.+$/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\n{2,}/g, "\n").trim();
806
+ cleaned = cleaned.replace(/^(Overview|Introduction|The Problem|Our Solution|Next Steps|Conclusion|Summary|Background|Context|Recommendation)\s*:?\s*/i, "").trim();
807
+ const sentences = cleaned.match(/[^.!?]+[.!?]/g) || [];
808
+ for (const sentence of sentences) {
809
+ const trimmed = sentence.trim();
810
+ if (trimmed.length >= 20 && /\b(is|are|was|were|will|can|should|must|has|have|had|provides?|enables?|allows?|offers?|delivers?|creates?|includes?|involves?|requires?|experiencing)\b/i.test(trimmed) && !this.containsSignals(trimmed.toLowerCase(), this.ctaSignals)) {
811
+ return trimmed.slice(0, 150);
812
+ }
729
813
  }
730
- return cleaned.slice(0, 150);
814
+ const fallback = cleaned.slice(0, 150);
815
+ return fallback.length >= 20 ? fallback : "";
731
816
  }
732
817
  };
733
818
 
@@ -752,6 +837,17 @@ var SlideFactory = class {
752
837
  }
753
838
  /**
754
839
  * Create slides from analyzed content.
840
+ *
841
+ * ARCHITECTURE (per KB expert methodologies):
842
+ * 1. Title slide - always first
843
+ * 2. Agenda slide - business mode only with 3+ sections
844
+ * 3. SCQA slides - Situation, Complication (per Minto)
845
+ * 4. Content slides - from sections with bullets/content
846
+ * 5. Metrics slide - if data points exist
847
+ * 6. Solution slide - SCQA answer
848
+ * 7. STAR moments - only if high-quality (per Duarte)
849
+ * 8. CTA slide - if call to action exists
850
+ * 9. Thank you slide - always last
755
851
  */
756
852
  async createSlides(analysis, mode) {
757
853
  const slides = [];
@@ -759,34 +855,121 @@ var SlideFactory = class {
759
855
  this.usedContent.clear();
760
856
  slides.push(this.createTitleSlide(slideIndex++, analysis));
761
857
  this.isContentUsed(analysis.titles[0] ?? "");
762
- if (mode === "business" && analysis.keyMessages.length >= 2) {
858
+ const substantiveSections = analysis.sections.filter(
859
+ (s) => s.level === 2 && (s.bullets.length > 0 || s.content.length > 50)
860
+ );
861
+ if (mode === "business" && substantiveSections.length >= 3) {
763
862
  slides.push(this.createAgendaSlide(slideIndex++, analysis));
764
863
  }
765
- if (analysis.scqa.situation && !this.isContentUsed(analysis.scqa.situation)) {
864
+ if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
766
865
  slides.push(this.createContextSlide(slideIndex++, analysis, mode));
767
866
  }
768
- if (analysis.scqa.complication && !this.isContentUsed(analysis.scqa.complication)) {
867
+ if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
769
868
  slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
770
869
  }
870
+ for (const section of substantiveSections.slice(0, 4)) {
871
+ const headerUsed = this.isContentUsed(section.header);
872
+ const contentUsed = section.content.length > 30 && this.isContentUsed(section.content.slice(0, 80));
873
+ if (!headerUsed && !contentUsed) {
874
+ if (section.bullets.length > 0) {
875
+ slides.push(this.createSectionBulletSlide(slideIndex++, section, mode));
876
+ } else if (section.content.length > 50) {
877
+ slides.push(this.createSectionContentSlide(slideIndex++, section, mode));
878
+ }
879
+ }
880
+ }
771
881
  for (const message of analysis.keyMessages) {
772
- if (!this.isContentUsed(message)) {
882
+ const wordCount = message.split(/\s+/).length;
883
+ if (wordCount >= 6 && !/^(The |Our |Your |Overview|Introduction|Conclusion)/i.test(message) && !this.isContentUsed(message)) {
773
884
  slides.push(this.createMessageSlide(slideIndex++, message, mode));
774
885
  }
775
886
  }
887
+ if (analysis.dataPoints.length >= 2) {
888
+ slides.push(this.createMetricsSlide(slideIndex++, analysis.dataPoints));
889
+ }
890
+ if (analysis.scqa.answer && analysis.scqa.answer.length > 30 && !this.isContentUsed(analysis.scqa.answer)) {
891
+ slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
892
+ }
893
+ const verbPattern = /\b(is|are|was|were|will|can|should|must|has|have|provides?|enables?|allows?|achieves?|exceeds?|results?|generates?|delivers?|creates?)\b/i;
776
894
  for (const starMoment of analysis.starMoments.slice(0, 2)) {
777
- if (!this.isContentUsed(starMoment)) {
895
+ const wordCount = starMoment.split(/\s+/).length;
896
+ const hasVerb = verbPattern.test(starMoment);
897
+ if (wordCount >= 6 && starMoment.length >= 40 && hasVerb && !this.isContentUsed(starMoment)) {
778
898
  slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
779
899
  }
780
900
  }
781
- if (analysis.scqa.answer && !this.isContentUsed(analysis.scqa.answer)) {
782
- slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
783
- }
784
- if (analysis.sparkline.callToAdventure && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
901
+ if (analysis.sparkline.callToAdventure && analysis.sparkline.callToAdventure.length > 20 && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
785
902
  slides.push(this.createCTASlide(slideIndex++, analysis, mode));
786
903
  }
787
904
  slides.push(this.createThankYouSlide(slideIndex++));
788
905
  return slides;
789
906
  }
907
+ /**
908
+ * Create a slide from a section with bullets
909
+ */
910
+ createSectionBulletSlide(index, section, mode) {
911
+ const bullets = section.bullets.slice(0, mode === "keynote" ? 3 : 5);
912
+ return {
913
+ index,
914
+ type: "bullet-points",
915
+ data: {
916
+ title: this.truncate(section.header, 60),
917
+ bullets: bullets.map((b) => this.cleanText(b).slice(0, 80))
918
+ },
919
+ classes: ["slide-bullet-points"]
920
+ };
921
+ }
922
+ /**
923
+ * Create a slide from a section with body content
924
+ */
925
+ createSectionContentSlide(index, section, mode) {
926
+ if (mode === "keynote") {
927
+ return {
928
+ index,
929
+ type: "single-statement",
930
+ data: {
931
+ title: this.truncate(this.extractFirstSentence(section.content), 80),
932
+ keyMessage: section.header
933
+ },
934
+ classes: ["slide-single-statement"]
935
+ };
936
+ }
937
+ return {
938
+ index,
939
+ type: "two-column",
940
+ data: {
941
+ title: this.truncate(section.header, 60),
942
+ body: this.truncate(section.content, 200)
943
+ },
944
+ classes: ["slide-two-column"]
945
+ };
946
+ }
947
+ /**
948
+ * Extract first sentence from text
949
+ */
950
+ extractFirstSentence(text) {
951
+ const cleaned = this.cleanText(text);
952
+ const match = cleaned.match(/^[^.!?]+[.!?]/);
953
+ return match ? match[0].trim() : cleaned.slice(0, 100);
954
+ }
955
+ /**
956
+ * Create a metrics slide from data points
957
+ */
958
+ createMetricsSlide(index, dataPoints) {
959
+ const metrics = dataPoints.slice(0, 4).map((dp) => ({
960
+ value: dp.value,
961
+ label: this.cleanText(dp.label).slice(0, 40)
962
+ }));
963
+ return {
964
+ index,
965
+ type: "metrics-grid",
966
+ data: {
967
+ title: "Key Metrics",
968
+ metrics
969
+ },
970
+ classes: ["slide-metrics-grid"]
971
+ };
972
+ }
790
973
  /**
791
974
  * Create a title slide.
792
975
  */
@@ -1083,23 +1266,43 @@ var SlideFactory = class {
1083
1266
  }
1084
1267
  // === Helper Methods ===
1085
1268
  /**
1086
- * Clean text by removing all content markers.
1269
+ * Clean text by removing all markdown and content markers.
1270
+ * CRITICAL: Must strip all formatting to prevent garbage in slides
1087
1271
  */
1088
1272
  cleanText(text) {
1089
1273
  if (!text) return "";
1090
- return text.replace(/\[HEADER\]\s*/g, "").replace(/\[BULLET\]\s*/g, "").replace(/\[NUMBERED\]\s*/g, "").replace(/\[EMPHASIS\]/g, "").replace(/\[\/EMPHASIS\]/g, "").replace(/\[CODE BLOCK\]/g, "").replace(/\[IMAGE\]/g, "").replace(/\s+/g, " ").trim();
1274
+ return text.replace(/^#+\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\[HEADER\]\s*/g, "").replace(/\[BULLET\]\s*/g, "").replace(/\[NUMBERED\]\s*/g, "").replace(/\[EMPHASIS\]/g, "").replace(/\[\/EMPHASIS\]/g, "").replace(/\[CODE BLOCK\]/g, "").replace(/\[IMAGE\]/g, "").replace(/\n{2,}/g, " ").replace(/\s+/g, " ").trim();
1091
1275
  }
1092
1276
  /**
1093
- * Truncate text to max length at word boundary.
1277
+ * Truncate text to max length at sentence boundary when possible.
1278
+ * CRITICAL: Never cut mid-number (99.5% should not become 99.)
1094
1279
  */
1095
1280
  truncate(text, maxLength) {
1096
1281
  const cleanedText = this.cleanText(text);
1097
1282
  if (!cleanedText || cleanedText.length <= maxLength) {
1098
1283
  return cleanedText;
1099
1284
  }
1285
+ const sentences = cleanedText.match(/[^.!?]+[.!?]/g);
1286
+ if (sentences) {
1287
+ let result = "";
1288
+ for (const sentence of sentences) {
1289
+ if ((result + sentence).length <= maxLength) {
1290
+ result += sentence;
1291
+ } else {
1292
+ break;
1293
+ }
1294
+ }
1295
+ if (result.length > maxLength * 0.5) {
1296
+ return result.trim();
1297
+ }
1298
+ }
1100
1299
  const truncated = cleanedText.slice(0, maxLength);
1101
- const lastSpace = truncated.lastIndexOf(" ");
1102
- if (lastSpace > maxLength * 0.7) {
1300
+ let lastSpace = truncated.lastIndexOf(" ");
1301
+ const afterCut = cleanedText.slice(lastSpace + 1, maxLength + 10);
1302
+ if (/^[\d.,%$]+/.test(afterCut)) {
1303
+ lastSpace = truncated.slice(0, lastSpace).lastIndexOf(" ");
1304
+ }
1305
+ if (lastSpace > maxLength * 0.5) {
1103
1306
  return truncated.slice(0, lastSpace) + "...";
1104
1307
  }
1105
1308
  return truncated + "...";
@@ -1130,10 +1333,12 @@ var SlideFactory = class {
1130
1333
  return sentences.slice(0, 5).map((s) => s.trim());
1131
1334
  }
1132
1335
  /**
1133
- * Remove a statistic from text.
1336
+ * Remove a statistic from text and clean thoroughly.
1134
1337
  */
1135
1338
  removeStatistic(text, stat) {
1136
- return text.replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1339
+ const cleaned = this.cleanText(text).replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1340
+ const firstSentence = cleaned.match(/^[^.!?]+[.!?]?/);
1341
+ return firstSentence ? firstSentence[0].slice(0, 80) : cleaned.slice(0, 80);
1137
1342
  }
1138
1343
  };
1139
1344
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-presentation-master",
3
- "version": "6.0.0",
3
+ "version": "6.1.1",
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",