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.js CHANGED
@@ -657,27 +657,65 @@ var ContentAnalyzer = class {
657
657
  }
658
658
  /**
659
659
  * Extract STAR moments (Something They'll Always Remember)
660
- * Per Nancy Duarte - these are emotional peaks in the presentation
660
+ * Per Nancy Duarte: These should be memorable, COMPLETE thoughts with impact
661
+ *
662
+ * CRITICAL REQUIREMENTS:
663
+ * - Must be complete sentences with subject + verb structure
664
+ * - Must have 5+ words minimum (no fragments!)
665
+ * - Must be 30+ characters
666
+ * - Must contain a verb
667
+ * - NOT fragments like "significant growth" or "cloud-first strategy"
668
+ * - NOT headers or topic labels
661
669
  */
662
670
  extractStarMoments(text) {
663
671
  const starMoments = [];
664
- const boldMatches = text.match(/\*\*(.+?)\*\*/g);
672
+ const usedPhrases = /* @__PURE__ */ new Set();
673
+ const fragmentPatterns = [
674
+ /^[a-z]+-[a-z]+\s+(strategy|approach|solution|platform|architecture)$/i,
675
+ // "cloud-first strategy"
676
+ /^(significant|substantial|dramatic|rapid|strong)\s+(growth|increase|decline|change)$/i,
677
+ // "significant growth"
678
+ /^(digital|cloud|data|ai)\s+(transformation|strategy|platform|solution)$/i,
679
+ // "digital transformation"
680
+ /^(our|your|the|a)\s+\w+\s*$/i,
681
+ // "our solution", "the platform"
682
+ /^[a-z]+\s+[a-z]+$/i
683
+ // Any two-word phrase
684
+ ];
685
+ 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;
686
+ const boldMatches = text.match(/\*\*([^*]+)\*\*/g);
665
687
  if (boldMatches) {
666
- for (const match of boldMatches.slice(0, 5)) {
688
+ for (const match of boldMatches) {
667
689
  const cleaned = match.replace(/\*\*/g, "").trim();
668
690
  const lowerCleaned = cleaned.toLowerCase();
669
- if (cleaned.length > 15 && cleaned.length < 100 && !this.containsSignals(lowerCleaned, this.ctaSignals) && !/^\d+[%x]?\s*$/.test(cleaned)) {
691
+ const wordCount = cleaned.split(/\s+/).length;
692
+ const normalized = cleaned.toLowerCase().slice(0, 30);
693
+ const hasVerb = verbPatterns.test(cleaned);
694
+ const isLongEnough = wordCount >= 5 && cleaned.length >= 30;
695
+ const isNotFragment = !fragmentPatterns.some((p) => p.test(cleaned));
696
+ const isNotHeader = !/^(The |Our |Your |Next |Key |Overview|Introduction|Conclusion|Summary|Background)/i.test(cleaned);
697
+ const isNotCTA = !this.containsSignals(lowerCleaned, this.ctaSignals);
698
+ const hasMetricContext = /\d+[%xX]|\$[\d,]+/.test(cleaned) && wordCount >= 5;
699
+ if ((hasVerb || hasMetricContext) && isLongEnough && isNotFragment && isNotHeader && isNotCTA && wordCount <= 25 && !usedPhrases.has(normalized)) {
670
700
  starMoments.push(cleaned);
701
+ usedPhrases.add(normalized);
702
+ if (starMoments.length >= 3) break;
671
703
  }
672
704
  }
673
705
  }
674
- const exclamations = text.match(/[^.!?]*![^.!?]*/g);
675
- if (exclamations) {
676
- for (const ex of exclamations.slice(0, 2)) {
677
- const cleaned = ex.trim();
678
- const lowerCleaned = cleaned.toLowerCase();
679
- if (cleaned.length > 10 && cleaned.length < 100 && !starMoments.includes(cleaned) && !this.containsSignals(lowerCleaned, this.ctaSignals)) {
706
+ if (starMoments.length < 3) {
707
+ const cleanedText = text.replace(/^#+\s+.+$/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\n{2,}/g, "\n").trim();
708
+ const sentences = cleanedText.match(/[^.!?]+[.!?]/g) || [];
709
+ for (const sentence of sentences) {
710
+ const cleaned = sentence.trim();
711
+ const normalized = cleaned.toLowerCase().slice(0, 30);
712
+ const wordCount = cleaned.split(/\s+/).length;
713
+ const hasHighImpact = /(\d{2,3}%|\d+x|billion|million|\$[\d,]+\s*(million|billion)?)/i.test(cleaned);
714
+ const hasVerb = verbPatterns.test(cleaned);
715
+ if (hasHighImpact && hasVerb && wordCount >= 6 && wordCount <= 25 && cleaned.length >= 40 && cleaned.length <= 200 && !usedPhrases.has(normalized) && !this.containsSignals(cleaned.toLowerCase(), this.ctaSignals)) {
680
716
  starMoments.push(cleaned);
717
+ usedPhrases.add(normalized);
718
+ if (starMoments.length >= 3) break;
681
719
  }
682
720
  }
683
721
  }
@@ -699,28 +737,65 @@ var ContentAnalyzer = class {
699
737
  return { callToAction };
700
738
  }
701
739
  /**
702
- * Extract key messages (max 3 - Rule of Three)
740
+ * Extract key messages - the actual insights from the content
741
+ * Per Carmine Gallo: Rule of Three - max 3 key messages
742
+ * Per Barbara Minto: Messages should be actionable conclusions, not topics
703
743
  */
704
744
  extractKeyMessages(text) {
705
745
  const messages = [];
706
- const h2Matches = text.match(/^##\s+(.+)$/gm);
707
- if (h2Matches) {
708
- for (const match of h2Matches.slice(0, 3)) {
709
- const msg = match.replace(/^##\s+/, "").replace(/\*\*/g, "").trim();
710
- if (msg.length > 5 && msg.length < 80) {
711
- messages.push(msg);
746
+ const usedPhrases = /* @__PURE__ */ new Set();
747
+ let cleanText = text.replace(/^#+\s+.+$/gm, "").replace(/\[.+?\]/g, "").replace(/\*\*/g, "");
748
+ cleanText = cleanText.replace(/(\d)\.(\d)/g, "$1<DECIMAL>$2");
749
+ const rawSentences = cleanText.match(/[^.!?]+[.!?]/g) || [];
750
+ const sentences = rawSentences.map((s) => s.replace(/<DECIMAL>/g, "."));
751
+ const insightSignals = [
752
+ "we recommend",
753
+ "we suggest",
754
+ "our strategy",
755
+ "the solution",
756
+ "key finding",
757
+ "result",
758
+ "achieve",
759
+ "improve",
760
+ "increase",
761
+ "decrease",
762
+ "expect",
763
+ "roi",
764
+ "benefit",
765
+ "opportunity",
766
+ "transform"
767
+ ];
768
+ for (const sentence of sentences) {
769
+ const cleaned = sentence.trim();
770
+ const normalized = cleaned.toLowerCase().slice(0, 30);
771
+ if (cleaned.length > 30 && cleaned.length < 150 && !usedPhrases.has(normalized) && !this.containsSignals(cleaned.toLowerCase(), this.ctaSignals)) {
772
+ if (this.containsSignals(cleaned.toLowerCase(), insightSignals)) {
773
+ messages.push(cleaned);
774
+ usedPhrases.add(normalized);
775
+ if (messages.length >= 3) break;
712
776
  }
713
777
  }
714
778
  }
715
779
  if (messages.length < 3) {
716
- const boldMatches = text.match(/\*\*(.+?)\*\*/g);
717
- if (boldMatches) {
718
- for (const match of boldMatches) {
719
- const msg = match.replace(/\*\*/g, "").trim();
720
- if (msg.length > 10 && msg.length < 80 && !messages.includes(msg)) {
721
- messages.push(msg);
722
- if (messages.length >= 3) break;
723
- }
780
+ for (const sentence of sentences) {
781
+ const cleaned = sentence.trim();
782
+ const normalized = cleaned.toLowerCase().slice(0, 30);
783
+ if (cleaned.length > 25 && cleaned.length < 150 && !usedPhrases.has(normalized) && /\d+[%xX]|\$[\d,]+/.test(cleaned)) {
784
+ messages.push(cleaned);
785
+ usedPhrases.add(normalized);
786
+ if (messages.length >= 3) break;
787
+ }
788
+ }
789
+ }
790
+ if (messages.length < 2) {
791
+ for (const sentence of sentences) {
792
+ const cleaned = sentence.trim();
793
+ const wordCount = cleaned.split(/\s+/).length;
794
+ const normalized = cleaned.toLowerCase().slice(0, 30);
795
+ if (wordCount >= 6 && wordCount <= 25 && !usedPhrases.has(normalized) && /\b(is|are|will|can|should|must|has|have|provides?|enables?|allows?)\b/.test(cleaned.toLowerCase())) {
796
+ messages.push(cleaned);
797
+ usedPhrases.add(normalized);
798
+ if (messages.length >= 3) break;
724
799
  }
725
800
  }
726
801
  }
@@ -728,51 +803,56 @@ var ContentAnalyzer = class {
728
803
  }
729
804
  /**
730
805
  * Extract data points (metrics with values)
806
+ * IMPROVED: Smarter label extraction that understands markdown tables
731
807
  */
732
808
  extractDataPoints(text) {
733
809
  const dataPoints = [];
734
- const dollarMatches = text.match(/\$[\d,]+(?:\.\d+)?[MBK]?(?:\s*(?:million|billion|thousand))?/gi);
735
- if (dollarMatches) {
736
- for (const match of dollarMatches.slice(0, 4)) {
737
- const context = this.getContextAroundMatch(text, match);
738
- dataPoints.push({
739
- value: match,
740
- label: this.extractLabelFromContext(context)
741
- });
810
+ const usedValues = /* @__PURE__ */ new Set();
811
+ const tableLines = text.split("\n").filter((line) => line.includes("|"));
812
+ if (tableLines.length >= 3) {
813
+ const dataRows = tableLines.filter((line) => !line.match(/^[\s|:-]+$/));
814
+ if (dataRows.length >= 2) {
815
+ for (const row of dataRows.slice(1)) {
816
+ const cells = row.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
817
+ if (cells.length >= 2) {
818
+ const valueCell = cells.find((c) => /^\$?[\d,]+\.?\d*[MBK%]?$/.test(c.replace(/[,$]/g, "")));
819
+ const labelCell = cells[0];
820
+ if (valueCell && labelCell && !usedValues.has(valueCell)) {
821
+ usedValues.add(valueCell);
822
+ dataPoints.push({ value: valueCell, label: labelCell.slice(0, 40) });
823
+ }
824
+ }
825
+ }
742
826
  }
743
827
  }
744
- const percentMatches = text.match(/\d+(?:\.\d+)?%/g);
745
- if (percentMatches) {
746
- for (const match of percentMatches.slice(0, 4)) {
747
- if (!dataPoints.some((d) => d.value === match)) {
748
- const context = this.getContextAroundMatch(text, match);
749
- dataPoints.push({
750
- value: match,
751
- label: this.extractLabelFromContext(context)
752
- });
828
+ const lines = text.split("\n");
829
+ for (const line of lines) {
830
+ if (line.includes("|")) continue;
831
+ const percentMatch = line.match(/(\w+(?:\s+\w+){0,4})\s+(?:by\s+)?(\d+(?:\.\d+)?%)/i);
832
+ if (percentMatch && percentMatch[2] && percentMatch[1] && !usedValues.has(percentMatch[2])) {
833
+ usedValues.add(percentMatch[2]);
834
+ dataPoints.push({ value: percentMatch[2], label: percentMatch[1].slice(0, 40) });
835
+ }
836
+ const dollarMatch = line.match(/\$(\d+(?:\.\d+)?)\s*(million|billion|M|B|K)?/i);
837
+ if (dollarMatch && dataPoints.length < 6) {
838
+ const fullValue = "$" + dollarMatch[1] + (dollarMatch[2] ? " " + dollarMatch[2] : "");
839
+ if (!usedValues.has(fullValue)) {
840
+ usedValues.add(fullValue);
841
+ const label = this.extractLabelFromLine(line, fullValue);
842
+ dataPoints.push({ value: fullValue, label });
753
843
  }
754
844
  }
755
845
  }
756
- return dataPoints.slice(0, 6);
846
+ return dataPoints.slice(0, 4);
757
847
  }
758
848
  /**
759
- * Get context around a match
849
+ * Extract a meaningful label from a line containing a metric
760
850
  */
761
- getContextAroundMatch(text, match) {
762
- const index = text.indexOf(match);
763
- if (index === -1) return "";
764
- const start = Math.max(0, index - 50);
765
- const end = Math.min(text.length, index + match.length + 50);
766
- return text.slice(start, end);
767
- }
768
- /**
769
- * Extract a label from surrounding context
770
- * Fixes the "Century Interactive |" garbage issue by stripping table syntax
771
- */
772
- extractLabelFromContext(context) {
773
- let cleaned = context.replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\s{2,}/g, " ").replace(/\*\*/g, "").replace(/#+\s*/g, "").trim();
774
- const words = cleaned.split(/\s+/).filter((w) => w.length > 2 && !w.match(/^\d/));
775
- return words.slice(0, 4).join(" ") || "Value";
851
+ extractLabelFromLine(line, value) {
852
+ const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
853
+ const beforeValue = cleaned.split(value)[0] || "";
854
+ const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
855
+ return words.slice(-4).join(" ").slice(0, 40) || "Value";
776
856
  }
777
857
  /**
778
858
  * Check if text contains any of the signals
@@ -782,15 +862,20 @@ var ContentAnalyzer = class {
782
862
  }
783
863
  /**
784
864
  * Extract first meaningful sentence from text
865
+ * CRITICAL: Must strip headers, CTA text, and fragments
785
866
  */
786
867
  extractFirstSentence(text) {
787
- const cleaned = text.replace(/^#+\s*/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").trim();
788
- const sentences = cleaned.split(/[.!?]+/);
789
- const first = sentences[0]?.trim();
790
- if (first && first.length > 10) {
791
- return first.slice(0, 150);
868
+ let cleaned = text.replace(/^#+\s+.+$/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\n{2,}/g, "\n").trim();
869
+ cleaned = cleaned.replace(/^(Overview|Introduction|The Problem|Our Solution|Next Steps|Conclusion|Summary|Background|Context|Recommendation)\s*:?\s*/i, "").trim();
870
+ const sentences = cleaned.match(/[^.!?]+[.!?]/g) || [];
871
+ for (const sentence of sentences) {
872
+ const trimmed = sentence.trim();
873
+ 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)) {
874
+ return trimmed.slice(0, 150);
875
+ }
792
876
  }
793
- return cleaned.slice(0, 150);
877
+ const fallback = cleaned.slice(0, 150);
878
+ return fallback.length >= 20 ? fallback : "";
794
879
  }
795
880
  };
796
881
 
@@ -815,6 +900,17 @@ var SlideFactory = class {
815
900
  }
816
901
  /**
817
902
  * Create slides from analyzed content.
903
+ *
904
+ * ARCHITECTURE (per KB expert methodologies):
905
+ * 1. Title slide - always first
906
+ * 2. Agenda slide - business mode only with 3+ sections
907
+ * 3. SCQA slides - Situation, Complication (per Minto)
908
+ * 4. Content slides - from sections with bullets/content
909
+ * 5. Metrics slide - if data points exist
910
+ * 6. Solution slide - SCQA answer
911
+ * 7. STAR moments - only if high-quality (per Duarte)
912
+ * 8. CTA slide - if call to action exists
913
+ * 9. Thank you slide - always last
818
914
  */
819
915
  async createSlides(analysis, mode) {
820
916
  const slides = [];
@@ -822,34 +918,121 @@ var SlideFactory = class {
822
918
  this.usedContent.clear();
823
919
  slides.push(this.createTitleSlide(slideIndex++, analysis));
824
920
  this.isContentUsed(analysis.titles[0] ?? "");
825
- if (mode === "business" && analysis.keyMessages.length >= 2) {
921
+ const substantiveSections = analysis.sections.filter(
922
+ (s) => s.level === 2 && (s.bullets.length > 0 || s.content.length > 50)
923
+ );
924
+ if (mode === "business" && substantiveSections.length >= 3) {
826
925
  slides.push(this.createAgendaSlide(slideIndex++, analysis));
827
926
  }
828
- if (analysis.scqa.situation && !this.isContentUsed(analysis.scqa.situation)) {
927
+ if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
829
928
  slides.push(this.createContextSlide(slideIndex++, analysis, mode));
830
929
  }
831
- if (analysis.scqa.complication && !this.isContentUsed(analysis.scqa.complication)) {
930
+ if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
832
931
  slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
833
932
  }
933
+ for (const section of substantiveSections.slice(0, 4)) {
934
+ const headerUsed = this.isContentUsed(section.header);
935
+ const contentUsed = section.content.length > 30 && this.isContentUsed(section.content.slice(0, 80));
936
+ if (!headerUsed && !contentUsed) {
937
+ if (section.bullets.length > 0) {
938
+ slides.push(this.createSectionBulletSlide(slideIndex++, section, mode));
939
+ } else if (section.content.length > 50) {
940
+ slides.push(this.createSectionContentSlide(slideIndex++, section, mode));
941
+ }
942
+ }
943
+ }
834
944
  for (const message of analysis.keyMessages) {
835
- if (!this.isContentUsed(message)) {
945
+ const wordCount = message.split(/\s+/).length;
946
+ if (wordCount >= 6 && !/^(The |Our |Your |Overview|Introduction|Conclusion)/i.test(message) && !this.isContentUsed(message)) {
836
947
  slides.push(this.createMessageSlide(slideIndex++, message, mode));
837
948
  }
838
949
  }
950
+ if (analysis.dataPoints.length >= 2) {
951
+ slides.push(this.createMetricsSlide(slideIndex++, analysis.dataPoints));
952
+ }
953
+ if (analysis.scqa.answer && analysis.scqa.answer.length > 30 && !this.isContentUsed(analysis.scqa.answer)) {
954
+ slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
955
+ }
956
+ const verbPattern = /\b(is|are|was|were|will|can|should|must|has|have|provides?|enables?|allows?|achieves?|exceeds?|results?|generates?|delivers?|creates?)\b/i;
839
957
  for (const starMoment of analysis.starMoments.slice(0, 2)) {
840
- if (!this.isContentUsed(starMoment)) {
958
+ const wordCount = starMoment.split(/\s+/).length;
959
+ const hasVerb = verbPattern.test(starMoment);
960
+ if (wordCount >= 6 && starMoment.length >= 40 && hasVerb && !this.isContentUsed(starMoment)) {
841
961
  slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
842
962
  }
843
963
  }
844
- if (analysis.scqa.answer && !this.isContentUsed(analysis.scqa.answer)) {
845
- slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
846
- }
847
- if (analysis.sparkline.callToAdventure && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
964
+ if (analysis.sparkline.callToAdventure && analysis.sparkline.callToAdventure.length > 20 && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
848
965
  slides.push(this.createCTASlide(slideIndex++, analysis, mode));
849
966
  }
850
967
  slides.push(this.createThankYouSlide(slideIndex++));
851
968
  return slides;
852
969
  }
970
+ /**
971
+ * Create a slide from a section with bullets
972
+ */
973
+ createSectionBulletSlide(index, section, mode) {
974
+ const bullets = section.bullets.slice(0, mode === "keynote" ? 3 : 5);
975
+ return {
976
+ index,
977
+ type: "bullet-points",
978
+ data: {
979
+ title: this.truncate(section.header, 60),
980
+ bullets: bullets.map((b) => this.cleanText(b).slice(0, 80))
981
+ },
982
+ classes: ["slide-bullet-points"]
983
+ };
984
+ }
985
+ /**
986
+ * Create a slide from a section with body content
987
+ */
988
+ createSectionContentSlide(index, section, mode) {
989
+ if (mode === "keynote") {
990
+ return {
991
+ index,
992
+ type: "single-statement",
993
+ data: {
994
+ title: this.truncate(this.extractFirstSentence(section.content), 80),
995
+ keyMessage: section.header
996
+ },
997
+ classes: ["slide-single-statement"]
998
+ };
999
+ }
1000
+ return {
1001
+ index,
1002
+ type: "two-column",
1003
+ data: {
1004
+ title: this.truncate(section.header, 60),
1005
+ body: this.truncate(section.content, 200)
1006
+ },
1007
+ classes: ["slide-two-column"]
1008
+ };
1009
+ }
1010
+ /**
1011
+ * Extract first sentence from text
1012
+ */
1013
+ extractFirstSentence(text) {
1014
+ const cleaned = this.cleanText(text);
1015
+ const match = cleaned.match(/^[^.!?]+[.!?]/);
1016
+ return match ? match[0].trim() : cleaned.slice(0, 100);
1017
+ }
1018
+ /**
1019
+ * Create a metrics slide from data points
1020
+ */
1021
+ createMetricsSlide(index, dataPoints) {
1022
+ const metrics = dataPoints.slice(0, 4).map((dp) => ({
1023
+ value: dp.value,
1024
+ label: this.cleanText(dp.label).slice(0, 40)
1025
+ }));
1026
+ return {
1027
+ index,
1028
+ type: "metrics-grid",
1029
+ data: {
1030
+ title: "Key Metrics",
1031
+ metrics
1032
+ },
1033
+ classes: ["slide-metrics-grid"]
1034
+ };
1035
+ }
853
1036
  /**
854
1037
  * Create a title slide.
855
1038
  */
@@ -1146,23 +1329,43 @@ var SlideFactory = class {
1146
1329
  }
1147
1330
  // === Helper Methods ===
1148
1331
  /**
1149
- * Clean text by removing all content markers.
1332
+ * Clean text by removing all markdown and content markers.
1333
+ * CRITICAL: Must strip all formatting to prevent garbage in slides
1150
1334
  */
1151
1335
  cleanText(text) {
1152
1336
  if (!text) return "";
1153
- 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();
1337
+ 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();
1154
1338
  }
1155
1339
  /**
1156
- * Truncate text to max length at word boundary.
1340
+ * Truncate text to max length at sentence boundary when possible.
1341
+ * CRITICAL: Never cut mid-number (99.5% should not become 99.)
1157
1342
  */
1158
1343
  truncate(text, maxLength) {
1159
1344
  const cleanedText = this.cleanText(text);
1160
1345
  if (!cleanedText || cleanedText.length <= maxLength) {
1161
1346
  return cleanedText;
1162
1347
  }
1348
+ const sentences = cleanedText.match(/[^.!?]+[.!?]/g);
1349
+ if (sentences) {
1350
+ let result = "";
1351
+ for (const sentence of sentences) {
1352
+ if ((result + sentence).length <= maxLength) {
1353
+ result += sentence;
1354
+ } else {
1355
+ break;
1356
+ }
1357
+ }
1358
+ if (result.length > maxLength * 0.5) {
1359
+ return result.trim();
1360
+ }
1361
+ }
1163
1362
  const truncated = cleanedText.slice(0, maxLength);
1164
- const lastSpace = truncated.lastIndexOf(" ");
1165
- if (lastSpace > maxLength * 0.7) {
1363
+ let lastSpace = truncated.lastIndexOf(" ");
1364
+ const afterCut = cleanedText.slice(lastSpace + 1, maxLength + 10);
1365
+ if (/^[\d.,%$]+/.test(afterCut)) {
1366
+ lastSpace = truncated.slice(0, lastSpace).lastIndexOf(" ");
1367
+ }
1368
+ if (lastSpace > maxLength * 0.5) {
1166
1369
  return truncated.slice(0, lastSpace) + "...";
1167
1370
  }
1168
1371
  return truncated + "...";
@@ -1193,10 +1396,12 @@ var SlideFactory = class {
1193
1396
  return sentences.slice(0, 5).map((s) => s.trim());
1194
1397
  }
1195
1398
  /**
1196
- * Remove a statistic from text.
1399
+ * Remove a statistic from text and clean thoroughly.
1197
1400
  */
1198
1401
  removeStatistic(text, stat) {
1199
- return text.replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1402
+ const cleaned = this.cleanText(text).replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1403
+ const firstSentence = cleaned.match(/^[^.!?]+[.!?]?/);
1404
+ return firstSentence ? firstSentence[0].slice(0, 80) : cleaned.slice(0, 80);
1200
1405
  }
1201
1406
  };
1202
1407