claude-presentation-master 6.1.1 → 7.2.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/README.md +298 -562
- package/bin/auto-fix-presentation.cjs +446 -0
- package/bin/cli.js +101 -32
- package/bin/qa-presentation.cjs +279 -0
- package/bin/score-presentation.cjs +488 -0
- package/dist/index.d.mts +838 -74
- package/dist/index.d.ts +838 -74
- package/dist/index.js +3203 -522
- package/dist/index.mjs +3195 -523
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AutoFixEngine: () => AutoFixEngine,
|
|
33
34
|
ChartJsProvider: () => ChartJsProvider,
|
|
34
35
|
CompositeChartProvider: () => CompositeChartProvider,
|
|
35
36
|
CompositeImageProvider: () => CompositeImageProvider,
|
|
36
37
|
ContentAnalyzer: () => ContentAnalyzer,
|
|
38
|
+
ContentPatternClassifier: () => ContentPatternClassifier,
|
|
39
|
+
IterativeQAEngine: () => IterativeQAEngine,
|
|
37
40
|
KnowledgeGateway: () => KnowledgeGateway,
|
|
38
41
|
LocalImageProvider: () => LocalImageProvider,
|
|
39
42
|
MermaidProvider: () => MermaidProvider,
|
|
@@ -45,17 +48,23 @@ __export(index_exports, {
|
|
|
45
48
|
QuickChartProvider: () => QuickChartProvider,
|
|
46
49
|
RevealJsGenerator: () => RevealJsGenerator,
|
|
47
50
|
ScoreCalculator: () => ScoreCalculator,
|
|
51
|
+
SevenDimensionScorer: () => SevenDimensionScorer,
|
|
48
52
|
SlideFactory: () => SlideFactory,
|
|
53
|
+
SlideGenerator: () => SlideGenerator,
|
|
49
54
|
TemplateEngine: () => TemplateEngine,
|
|
50
55
|
TemplateNotFoundError: () => TemplateNotFoundError,
|
|
51
56
|
UnsplashImageProvider: () => UnsplashImageProvider,
|
|
52
57
|
VERSION: () => VERSION,
|
|
53
58
|
ValidationError: () => ValidationError,
|
|
59
|
+
createAutoFixEngine: () => createAutoFixEngine,
|
|
54
60
|
createDefaultChartProvider: () => createDefaultChartProvider,
|
|
55
61
|
createDefaultImageProvider: () => createDefaultImageProvider,
|
|
62
|
+
createIterativeQAEngine: () => createIterativeQAEngine,
|
|
63
|
+
createSlideFactory: () => createSlideFactory,
|
|
56
64
|
default: () => index_default,
|
|
57
65
|
generate: () => generate,
|
|
58
66
|
getKnowledgeGateway: () => getKnowledgeGateway,
|
|
67
|
+
initSlideGenerator: () => initSlideGenerator,
|
|
59
68
|
validate: () => validate
|
|
60
69
|
});
|
|
61
70
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -93,6 +102,75 @@ var import_fs = require("fs");
|
|
|
93
102
|
var import_path = require("path");
|
|
94
103
|
var import_url = require("url");
|
|
95
104
|
var yaml = __toESM(require("yaml"));
|
|
105
|
+
|
|
106
|
+
// src/utils/Logger.ts
|
|
107
|
+
var Logger = class {
|
|
108
|
+
level;
|
|
109
|
+
prefix;
|
|
110
|
+
timestamps;
|
|
111
|
+
constructor(options = {}) {
|
|
112
|
+
this.level = options.level ?? 1 /* INFO */;
|
|
113
|
+
this.prefix = options.prefix ?? "";
|
|
114
|
+
this.timestamps = options.timestamps ?? false;
|
|
115
|
+
}
|
|
116
|
+
setLevel(level) {
|
|
117
|
+
this.level = level;
|
|
118
|
+
}
|
|
119
|
+
formatMessage(message) {
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (this.timestamps) {
|
|
122
|
+
parts.push(`[${(/* @__PURE__ */ new Date()).toISOString()}]`);
|
|
123
|
+
}
|
|
124
|
+
if (this.prefix) {
|
|
125
|
+
parts.push(`[${this.prefix}]`);
|
|
126
|
+
}
|
|
127
|
+
parts.push(message);
|
|
128
|
+
return parts.join(" ");
|
|
129
|
+
}
|
|
130
|
+
debug(message, ...args) {
|
|
131
|
+
if (this.level <= 0 /* DEBUG */) {
|
|
132
|
+
console.debug(this.formatMessage(message), ...args);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
info(message, ...args) {
|
|
136
|
+
if (this.level <= 1 /* INFO */) {
|
|
137
|
+
console.info(this.formatMessage(message), ...args);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
warn(message, ...args) {
|
|
141
|
+
if (this.level <= 2 /* WARN */) {
|
|
142
|
+
console.warn(this.formatMessage(message), ...args);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
error(message, ...args) {
|
|
146
|
+
if (this.level <= 3 /* ERROR */) {
|
|
147
|
+
console.error(this.formatMessage(message), ...args);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Progress messages (always shown unless silent)
|
|
151
|
+
progress(message) {
|
|
152
|
+
if (this.level < 4 /* SILENT */) {
|
|
153
|
+
console.info(message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Success messages
|
|
157
|
+
success(message) {
|
|
158
|
+
if (this.level < 4 /* SILENT */) {
|
|
159
|
+
console.info(`\u2705 ${message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Step messages for workflow progress
|
|
163
|
+
step(message) {
|
|
164
|
+
if (this.level <= 1 /* INFO */) {
|
|
165
|
+
console.info(` \u2713 ${message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var logger = new Logger({
|
|
170
|
+
level: process.env.LOG_LEVEL ? parseInt(process.env.LOG_LEVEL) : 1 /* INFO */
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// src/kb/KnowledgeGateway.ts
|
|
96
174
|
var import_meta = {};
|
|
97
175
|
function getModuleDir() {
|
|
98
176
|
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
@@ -104,7 +182,7 @@ function getModuleDir() {
|
|
|
104
182
|
return process.cwd();
|
|
105
183
|
}
|
|
106
184
|
var KnowledgeGateway = class {
|
|
107
|
-
kb;
|
|
185
|
+
kb = {};
|
|
108
186
|
loaded = false;
|
|
109
187
|
constructor() {
|
|
110
188
|
}
|
|
@@ -127,7 +205,7 @@ var KnowledgeGateway = class {
|
|
|
127
205
|
const content = (0, import_fs.readFileSync)(path, "utf-8");
|
|
128
206
|
this.kb = yaml.parse(content);
|
|
129
207
|
this.loaded = true;
|
|
130
|
-
|
|
208
|
+
logger.step(`Knowledge base loaded from ${path}`);
|
|
131
209
|
return;
|
|
132
210
|
} catch {
|
|
133
211
|
}
|
|
@@ -318,7 +396,7 @@ var KnowledgeGateway = class {
|
|
|
318
396
|
*/
|
|
319
397
|
getChartGuidance(purpose) {
|
|
320
398
|
this.ensureLoaded();
|
|
321
|
-
return this.kb.chart_selection_guide.by_purpose[purpose];
|
|
399
|
+
return this.kb.chart_selection_guide.by_purpose?.[purpose];
|
|
322
400
|
}
|
|
323
401
|
/**
|
|
324
402
|
* Get charts to avoid
|
|
@@ -333,7 +411,7 @@ var KnowledgeGateway = class {
|
|
|
333
411
|
*/
|
|
334
412
|
getIBPitchBookStructure(type) {
|
|
335
413
|
this.ensureLoaded();
|
|
336
|
-
return this.kb.investment_banking?.pitch_book_types[type];
|
|
414
|
+
return this.kb.investment_banking?.pitch_book_types?.[type];
|
|
337
415
|
}
|
|
338
416
|
/**
|
|
339
417
|
* Check if knowledge base is loaded
|
|
@@ -350,6 +428,439 @@ var KnowledgeGateway = class {
|
|
|
350
428
|
this.ensureLoaded();
|
|
351
429
|
return this.kb;
|
|
352
430
|
}
|
|
431
|
+
// ==========================================================================
|
|
432
|
+
// KB-DRIVEN SLIDEFACTORY METHODS (v7.0.0)
|
|
433
|
+
// ==========================================================================
|
|
434
|
+
/**
|
|
435
|
+
* Get allowed slide types for a specific presentation type.
|
|
436
|
+
* CRITICAL: SlideFactory must ONLY use types from this list.
|
|
437
|
+
*/
|
|
438
|
+
getAllowedSlideTypes(type) {
|
|
439
|
+
this.ensureLoaded();
|
|
440
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
441
|
+
if (!typeConfig?.slide_types_allowed) {
|
|
442
|
+
const mode = this.getModeForType(type);
|
|
443
|
+
const templates = this.getSlideTemplates(mode);
|
|
444
|
+
return templates.map((t) => t.name.toLowerCase().replace(/\s+/g, "_"));
|
|
445
|
+
}
|
|
446
|
+
return typeConfig.slide_types_allowed;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get validation rules for a specific presentation type.
|
|
450
|
+
* Returns word limits, bullet limits, whitespace requirements, etc.
|
|
451
|
+
*/
|
|
452
|
+
getValidationRules(type) {
|
|
453
|
+
this.ensureLoaded();
|
|
454
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
455
|
+
if (!typeConfig?.validation_rules) {
|
|
456
|
+
const mode = this.getModeForType(type);
|
|
457
|
+
return mode === "keynote" ? {
|
|
458
|
+
wordsPerSlide: { min: 1, max: 25, ideal: 10 },
|
|
459
|
+
whitespace: { min: 40, ideal: 50 },
|
|
460
|
+
bulletsPerSlide: { max: 3 },
|
|
461
|
+
actionTitlesRequired: false,
|
|
462
|
+
sourcesRequired: false
|
|
463
|
+
} : {
|
|
464
|
+
wordsPerSlide: { min: 40, max: 80, ideal: 60 },
|
|
465
|
+
whitespace: { min: 25, ideal: 30 },
|
|
466
|
+
bulletsPerSlide: { max: 5 },
|
|
467
|
+
actionTitlesRequired: true,
|
|
468
|
+
sourcesRequired: true
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const rules = typeConfig.validation_rules;
|
|
472
|
+
return {
|
|
473
|
+
wordsPerSlide: {
|
|
474
|
+
min: rules.words_per_slide?.min ?? 1,
|
|
475
|
+
max: rules.words_per_slide?.max ?? 80,
|
|
476
|
+
ideal: rules.words_per_slide?.ideal ?? 40
|
|
477
|
+
},
|
|
478
|
+
whitespace: {
|
|
479
|
+
min: rules.whitespace?.min ?? 25,
|
|
480
|
+
ideal: rules.whitespace?.ideal ?? 30
|
|
481
|
+
},
|
|
482
|
+
bulletsPerSlide: {
|
|
483
|
+
max: rules.bullets_per_slide?.max ?? 5
|
|
484
|
+
},
|
|
485
|
+
actionTitlesRequired: rules.action_titles_required ?? false,
|
|
486
|
+
sourcesRequired: rules.sources_required ?? false,
|
|
487
|
+
calloutsRequired: rules.callouts_required,
|
|
488
|
+
denseDataAllowed: rules.dense_data_allowed,
|
|
489
|
+
codeBlocksAllowed: rules.code_blocks_allowed,
|
|
490
|
+
diagramsRequired: rules.diagrams_required
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Get required elements that MUST be in the deck for this type.
|
|
495
|
+
*/
|
|
496
|
+
getRequiredElements(type) {
|
|
497
|
+
this.ensureLoaded();
|
|
498
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
499
|
+
return typeConfig?.required_elements ?? [];
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Get anti-patterns to avoid for this presentation type.
|
|
503
|
+
*/
|
|
504
|
+
getAntiPatternsForType(type) {
|
|
505
|
+
this.ensureLoaded();
|
|
506
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
507
|
+
return typeConfig?.anti_patterns ?? [];
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Get typography specifications for this presentation type.
|
|
511
|
+
*/
|
|
512
|
+
getTypographyForType(type) {
|
|
513
|
+
this.ensureLoaded();
|
|
514
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
515
|
+
const typo = typeConfig?.typography;
|
|
516
|
+
if (!typo) {
|
|
517
|
+
const mode = this.getModeForType(type);
|
|
518
|
+
const modeTypo = this.getTypography(mode);
|
|
519
|
+
const maxFontsValue = modeTypo.max_fonts;
|
|
520
|
+
return {
|
|
521
|
+
titles: modeTypo.titles ?? "48px, Bold",
|
|
522
|
+
body: modeTypo.body_text ?? "24px",
|
|
523
|
+
maxFonts: typeof maxFontsValue === "number" ? maxFontsValue : 2
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
titles: typo.titles ?? "48px, Bold",
|
|
528
|
+
body: typo.body ?? "24px",
|
|
529
|
+
maxFonts: typeof typo.max_fonts === "number" ? typo.max_fonts : 2,
|
|
530
|
+
actionTitle: typo.action_title,
|
|
531
|
+
sectionHeaders: typo.section_headers,
|
|
532
|
+
dataLabels: typo.data_labels,
|
|
533
|
+
code: typo.code
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get CSS variables recipe for this presentation type.
|
|
538
|
+
*/
|
|
539
|
+
getCSSVariablesForType(type) {
|
|
540
|
+
this.ensureLoaded();
|
|
541
|
+
const recipes = this.kb.type_visual_recipes;
|
|
542
|
+
if (recipes?.[type]?.css_variables) {
|
|
543
|
+
return recipes[type].css_variables;
|
|
544
|
+
}
|
|
545
|
+
return `:root {
|
|
546
|
+
--color-background: #FAFAF9;
|
|
547
|
+
--color-primary: #0F172A;
|
|
548
|
+
--color-secondary: #475569;
|
|
549
|
+
--color-accent: #0369A1;
|
|
550
|
+
--color-text: #18181B;
|
|
551
|
+
--font-display: 'Inter', sans-serif;
|
|
552
|
+
--font-body: 'Inter', sans-serif;
|
|
553
|
+
}`;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Get scoring weights for QA evaluation for this type.
|
|
557
|
+
*/
|
|
558
|
+
getScoringWeights(type) {
|
|
559
|
+
this.ensureLoaded();
|
|
560
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
561
|
+
const weights = typeConfig?.scoring_weights;
|
|
562
|
+
if (!weights) {
|
|
563
|
+
return {
|
|
564
|
+
visualQuality: 30,
|
|
565
|
+
contentQuality: 30,
|
|
566
|
+
expertCompliance: 30,
|
|
567
|
+
accessibility: 10
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
visualQuality: weights.visual_quality ?? 30,
|
|
572
|
+
contentQuality: weights.content_quality ?? 30,
|
|
573
|
+
expertCompliance: weights.expert_compliance ?? 30,
|
|
574
|
+
accessibility: weights.accessibility ?? 10
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get story structure framework for this presentation type.
|
|
579
|
+
*/
|
|
580
|
+
getStoryStructure(type) {
|
|
581
|
+
this.ensureLoaded();
|
|
582
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
583
|
+
const mode = this.getModeForType(type);
|
|
584
|
+
if (mode === "business") {
|
|
585
|
+
return {
|
|
586
|
+
framework: "scqa",
|
|
587
|
+
requiredElements: ["situation", "complication", "answer"],
|
|
588
|
+
optionalElements: ["question", "evidence", "recommendation"]
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
framework: "sparkline",
|
|
593
|
+
requiredElements: ["what_is", "what_could_be", "call_to_action"],
|
|
594
|
+
optionalElements: ["star_moment", "hook"]
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Map a content pattern to the best allowed slide type.
|
|
599
|
+
* CRITICAL: This is how SlideFactory decides which slide type to use.
|
|
600
|
+
*/
|
|
601
|
+
mapContentPatternToSlideType(pattern, allowedTypes) {
|
|
602
|
+
const patternToTypes = {
|
|
603
|
+
big_number: ["big_number", "big-number", "data_insight", "metrics_grid", "metrics-grid"],
|
|
604
|
+
comparison: ["comparison", "options_comparison", "two_column", "two-column"],
|
|
605
|
+
timeline: ["timeline", "process_timeline", "process", "roadmap"],
|
|
606
|
+
process: ["process", "process_timeline", "timeline", "three_column", "three-column"],
|
|
607
|
+
metrics: ["metrics_grid", "metrics-grid", "data_insight", "big_number", "big-number"],
|
|
608
|
+
quote: ["quote", "testimonial", "social_proof", "social-proof"],
|
|
609
|
+
code: ["code_snippet", "technical", "two_column", "two-column"],
|
|
610
|
+
bullets: ["bullet_points", "bullet-points", "detailed_findings", "two_column", "two-column"],
|
|
611
|
+
prose: ["two_column", "two-column", "bullet_points", "bullet-points", "single_statement"]
|
|
612
|
+
};
|
|
613
|
+
const preferredTypes = patternToTypes[pattern.primaryPattern] || patternToTypes.prose || [];
|
|
614
|
+
for (const preferred of preferredTypes) {
|
|
615
|
+
const underscoreVersion = preferred.replace(/-/g, "_");
|
|
616
|
+
const dashVersion = preferred.replace(/_/g, "-");
|
|
617
|
+
if (allowedTypes.includes(preferred)) return preferred;
|
|
618
|
+
if (allowedTypes.includes(underscoreVersion)) return underscoreVersion;
|
|
619
|
+
if (allowedTypes.includes(dashVersion)) return dashVersion;
|
|
620
|
+
}
|
|
621
|
+
const contentTypes = [
|
|
622
|
+
"bullet_points",
|
|
623
|
+
"bullet-points",
|
|
624
|
+
"two_column",
|
|
625
|
+
"two-column",
|
|
626
|
+
"three_column",
|
|
627
|
+
"three-column",
|
|
628
|
+
"data_insight"
|
|
629
|
+
];
|
|
630
|
+
for (const ct of contentTypes) {
|
|
631
|
+
if (allowedTypes.includes(ct)) return ct;
|
|
632
|
+
}
|
|
633
|
+
return allowedTypes[0] ?? "bullet-points";
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Get a specific slide template by name from the KB.
|
|
637
|
+
*/
|
|
638
|
+
getSlideTemplateByName(name) {
|
|
639
|
+
this.ensureLoaded();
|
|
640
|
+
const keynoteTemplates = this.kb.slide_templates?.keynote_mode ?? [];
|
|
641
|
+
const businessTemplates = this.kb.slide_templates?.business_mode ?? [];
|
|
642
|
+
const normalizedName = name.toLowerCase().replace(/[-_]/g, " ");
|
|
643
|
+
for (const template of [...keynoteTemplates, ...businessTemplates]) {
|
|
644
|
+
const templateName = template.name.toLowerCase().replace(/[-_]/g, " ");
|
|
645
|
+
if (templateName.includes(normalizedName) || normalizedName.includes(templateName)) {
|
|
646
|
+
return {
|
|
647
|
+
name: template.name,
|
|
648
|
+
purpose: template.purpose,
|
|
649
|
+
elements: template.elements,
|
|
650
|
+
components: template.components,
|
|
651
|
+
wordLimit: template.word_limit ?? 60
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return void 0;
|
|
656
|
+
}
|
|
657
|
+
// ==========================================================================
|
|
658
|
+
// SLIDE DEFAULTS & LABELS (v7.1.0 - Replace ALL Hardcoded Values)
|
|
659
|
+
// ==========================================================================
|
|
660
|
+
/**
|
|
661
|
+
* Get slide defaults for structural slides (titles, messages, labels).
|
|
662
|
+
* CRITICAL: SlideFactory must use these instead of hardcoded strings.
|
|
663
|
+
*/
|
|
664
|
+
getSlideDefaults(type) {
|
|
665
|
+
this.ensureLoaded();
|
|
666
|
+
const mode = this.getModeForType(type);
|
|
667
|
+
const rules = this.getValidationRules(type);
|
|
668
|
+
const maxWords = rules.wordsPerSlide.max;
|
|
669
|
+
return {
|
|
670
|
+
agenda: { title: mode === "business" ? "Agenda" : "What We'll Cover" },
|
|
671
|
+
thankYou: {
|
|
672
|
+
title: mode === "business" ? "Thank You" : "Thank You",
|
|
673
|
+
subtitle: mode === "business" ? "Questions?" : "Let's Connect"
|
|
674
|
+
},
|
|
675
|
+
cta: {
|
|
676
|
+
title: mode === "business" ? "Next Steps" : "Take Action",
|
|
677
|
+
message: mode === "business" ? "Recommended Actions" : "Ready to Begin?",
|
|
678
|
+
fallback: mode === "business" ? "Contact us to learn more" : "Take the next step"
|
|
679
|
+
},
|
|
680
|
+
metricsGrid: {
|
|
681
|
+
title: mode === "business" ? "Key Metrics" : "The Numbers",
|
|
682
|
+
maxMetrics: 4
|
|
683
|
+
},
|
|
684
|
+
code: {
|
|
685
|
+
label: "Code Example",
|
|
686
|
+
maxChars: 500
|
|
687
|
+
},
|
|
688
|
+
comparison: {
|
|
689
|
+
leftLabel: mode === "business" ? "Current State" : "Before",
|
|
690
|
+
rightLabel: mode === "business" ? "Future State" : "After",
|
|
691
|
+
optionLabels: ["Option A", "Option B", "Option C", "Option D"]
|
|
692
|
+
},
|
|
693
|
+
column: { labelTemplate: "Point {n}" },
|
|
694
|
+
subtitle: { maxWords: Math.min(15, Math.floor(maxWords / 3)) },
|
|
695
|
+
context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
|
|
696
|
+
step: { maxWords: Math.min(20, Math.floor(maxWords / 4)) },
|
|
697
|
+
columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Get SCQA framework titles from KB (for business mode).
|
|
702
|
+
*/
|
|
703
|
+
getSCQATitles(type) {
|
|
704
|
+
this.ensureLoaded();
|
|
705
|
+
const mode = this.getModeForType(type);
|
|
706
|
+
if (mode === "business") {
|
|
707
|
+
return {
|
|
708
|
+
situation: "Current Situation",
|
|
709
|
+
complication: "The Challenge",
|
|
710
|
+
question: "The Critical Question",
|
|
711
|
+
answer: "Our Recommendation"
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
situation: "Where We Are",
|
|
716
|
+
complication: "What's At Stake",
|
|
717
|
+
question: "The Question We Face",
|
|
718
|
+
answer: "The Path Forward"
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Get Sparkline framework titles from KB (for keynote mode).
|
|
723
|
+
*/
|
|
724
|
+
getSparklineTitles(type) {
|
|
725
|
+
this.ensureLoaded();
|
|
726
|
+
return {
|
|
727
|
+
whatIs: "Where We Are Today",
|
|
728
|
+
whatCouldBe: "What Could Be",
|
|
729
|
+
callToAdventure: "The Call to Adventure",
|
|
730
|
+
newBliss: "The New World"
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Get insight markers for detecting action titles.
|
|
735
|
+
* These are words/phrases that indicate an insight vs a topic.
|
|
736
|
+
*/
|
|
737
|
+
getInsightMarkers() {
|
|
738
|
+
this.ensureLoaded();
|
|
739
|
+
return [
|
|
740
|
+
// Trend indicators
|
|
741
|
+
"increase",
|
|
742
|
+
"decrease",
|
|
743
|
+
"grew",
|
|
744
|
+
"declined",
|
|
745
|
+
"achieve",
|
|
746
|
+
"exceed",
|
|
747
|
+
"improve",
|
|
748
|
+
"reduce",
|
|
749
|
+
"save",
|
|
750
|
+
"gain",
|
|
751
|
+
"lost",
|
|
752
|
+
"doubled",
|
|
753
|
+
"tripled",
|
|
754
|
+
"outperform",
|
|
755
|
+
"underperform",
|
|
756
|
+
"accelerate",
|
|
757
|
+
"decelerate",
|
|
758
|
+
// Quantitative markers
|
|
759
|
+
"%",
|
|
760
|
+
"percent",
|
|
761
|
+
"million",
|
|
762
|
+
"billion",
|
|
763
|
+
"thousand",
|
|
764
|
+
"$",
|
|
765
|
+
"\u20AC",
|
|
766
|
+
"\xA3",
|
|
767
|
+
"ROI",
|
|
768
|
+
"revenue",
|
|
769
|
+
"cost",
|
|
770
|
+
"margin",
|
|
771
|
+
"growth",
|
|
772
|
+
"decline",
|
|
773
|
+
// Causality markers
|
|
774
|
+
"due to",
|
|
775
|
+
"because",
|
|
776
|
+
"resulting in",
|
|
777
|
+
"leading to",
|
|
778
|
+
"enabling",
|
|
779
|
+
"driving",
|
|
780
|
+
"caused by",
|
|
781
|
+
"attributed to",
|
|
782
|
+
"as a result of",
|
|
783
|
+
// Action verbs (Minto Pyramid style)
|
|
784
|
+
"should",
|
|
785
|
+
"must",
|
|
786
|
+
"need to",
|
|
787
|
+
"recommend",
|
|
788
|
+
"propose",
|
|
789
|
+
"suggest",
|
|
790
|
+
"requires",
|
|
791
|
+
"demands",
|
|
792
|
+
"enables",
|
|
793
|
+
"prevents",
|
|
794
|
+
"ensures"
|
|
795
|
+
];
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Get word limit for a specific slide element type.
|
|
799
|
+
*/
|
|
800
|
+
getWordLimitForElement(type, element) {
|
|
801
|
+
const rules = this.getValidationRules(type);
|
|
802
|
+
const maxWords = rules.wordsPerSlide.max;
|
|
803
|
+
const bulletsMax = rules.bulletsPerSlide.max;
|
|
804
|
+
switch (element) {
|
|
805
|
+
case "title":
|
|
806
|
+
return Math.min(15, Math.floor(maxWords / 4));
|
|
807
|
+
case "subtitle":
|
|
808
|
+
return Math.min(15, Math.floor(maxWords / 5));
|
|
809
|
+
case "bullet":
|
|
810
|
+
return Math.floor(maxWords / (bulletsMax || 5));
|
|
811
|
+
case "body":
|
|
812
|
+
return maxWords;
|
|
813
|
+
case "quote":
|
|
814
|
+
return Math.min(40, maxWords);
|
|
815
|
+
case "step":
|
|
816
|
+
return Math.min(20, Math.floor(maxWords / 4));
|
|
817
|
+
default:
|
|
818
|
+
return maxWords;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Validate a slide against KB rules for the given presentation type.
|
|
823
|
+
*/
|
|
824
|
+
validateSlideAgainstKB(slide, type) {
|
|
825
|
+
const rules = this.getValidationRules(type);
|
|
826
|
+
const allowedTypes = this.getAllowedSlideTypes(type);
|
|
827
|
+
const violations = [];
|
|
828
|
+
const warnings = [];
|
|
829
|
+
const normalizedType = slide.type.replace(/-/g, "_");
|
|
830
|
+
const isAllowed = allowedTypes.some(
|
|
831
|
+
(t) => t === slide.type || t === normalizedType || t.replace(/_/g, "-") === slide.type
|
|
832
|
+
);
|
|
833
|
+
if (!isAllowed) {
|
|
834
|
+
violations.push(`Slide type '${slide.type}' not allowed for ${type}. Allowed: ${allowedTypes.join(", ")}`);
|
|
835
|
+
}
|
|
836
|
+
if (slide.wordCount > rules.wordsPerSlide.max) {
|
|
837
|
+
violations.push(`Word count ${slide.wordCount} exceeds max ${rules.wordsPerSlide.max}`);
|
|
838
|
+
} else if (slide.wordCount < rules.wordsPerSlide.min) {
|
|
839
|
+
warnings.push(`Word count ${slide.wordCount} below min ${rules.wordsPerSlide.min}`);
|
|
840
|
+
}
|
|
841
|
+
if (slide.bulletCount > rules.bulletsPerSlide.max) {
|
|
842
|
+
violations.push(`Bullet count ${slide.bulletCount} exceeds max ${rules.bulletsPerSlide.max}`);
|
|
843
|
+
}
|
|
844
|
+
if (rules.actionTitlesRequired && !slide.hasActionTitle) {
|
|
845
|
+
violations.push(`Action title required for ${type} but not present`);
|
|
846
|
+
}
|
|
847
|
+
if (rules.sourcesRequired && !slide.hasSource) {
|
|
848
|
+
warnings.push(`Source citation recommended for ${type}`);
|
|
849
|
+
}
|
|
850
|
+
const fixes = {};
|
|
851
|
+
if (slide.wordCount > rules.wordsPerSlide.max) {
|
|
852
|
+
fixes.wordCount = rules.wordsPerSlide.max;
|
|
853
|
+
}
|
|
854
|
+
if (slide.bulletCount > rules.bulletsPerSlide.max) {
|
|
855
|
+
fixes.bulletCount = rules.bulletsPerSlide.max;
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
valid: violations.length === 0,
|
|
859
|
+
violations,
|
|
860
|
+
warnings,
|
|
861
|
+
fixes
|
|
862
|
+
};
|
|
863
|
+
}
|
|
353
864
|
};
|
|
354
865
|
var gatewayInstance = null;
|
|
355
866
|
async function getKnowledgeGateway() {
|
|
@@ -452,14 +963,14 @@ var ContentAnalyzer = class {
|
|
|
452
963
|
const text = this.parseContent(content, contentType);
|
|
453
964
|
const title = this.extractTitle(text);
|
|
454
965
|
const detectedType = this.detectPresentationType(text);
|
|
455
|
-
|
|
966
|
+
logger.step(`Detected type: ${detectedType}`);
|
|
456
967
|
const sections = this.extractSections(text);
|
|
457
|
-
|
|
968
|
+
logger.step(`Found ${sections.length} sections`);
|
|
458
969
|
const scqa = this.extractSCQA(text);
|
|
459
970
|
const sparkline = this.extractSparkline(text);
|
|
460
971
|
const keyMessages = this.extractKeyMessages(text);
|
|
461
972
|
const dataPoints = this.extractDataPoints(text);
|
|
462
|
-
|
|
973
|
+
logger.step(`Found ${dataPoints.length} data points`);
|
|
463
974
|
const titles = sections.map((s) => s.header).filter((h) => h.length > 0);
|
|
464
975
|
const starMoments = this.extractStarMoments(text);
|
|
465
976
|
const estimatedSlideCount = Math.max(5, sections.length + 3);
|
|
@@ -487,12 +998,22 @@ var ContentAnalyzer = class {
|
|
|
487
998
|
}
|
|
488
999
|
/**
|
|
489
1000
|
* Parse content based on type
|
|
1001
|
+
* CRITICAL: Strip code blocks FIRST to prevent code from becoming slides
|
|
490
1002
|
*/
|
|
491
1003
|
parseContent(content, contentType) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
|
|
1004
|
+
let text = content;
|
|
1005
|
+
text = text.replace(/```[\s\S]*?```/g, "");
|
|
1006
|
+
text = text.replace(/`[^`]{50,}`/g, "");
|
|
1007
|
+
text = text.split("\n").filter((line) => {
|
|
1008
|
+
const trimmed = line.trim();
|
|
1009
|
+
if (/^(import|export|const|let|var|function|class|interface|type|async|await|return|if|for|while)\s/.test(trimmed)) return false;
|
|
1010
|
+
if (/^(\/\/|\/\*|\*|#!)/.test(trimmed)) return false;
|
|
1011
|
+
if (/^\s*[{}\[\]();]/.test(trimmed)) return false;
|
|
1012
|
+
if (/^[\w.]+\s*\(.*\)\s*;?\s*$/.test(trimmed)) return false;
|
|
1013
|
+
if (/^[\w.]+\s*=\s*/.test(trimmed) && /[{(\[]/.test(trimmed)) return false;
|
|
1014
|
+
return true;
|
|
1015
|
+
}).join("\n");
|
|
1016
|
+
return text;
|
|
496
1017
|
}
|
|
497
1018
|
/**
|
|
498
1019
|
* Extract the main title from content
|
|
@@ -569,12 +1090,16 @@ var ContentAnalyzer = class {
|
|
|
569
1090
|
}
|
|
570
1091
|
const bulletMatch = trimmedLine.match(/^[-*+]\s+(.+)$/);
|
|
571
1092
|
if (bulletMatch && bulletMatch[1] && currentSection) {
|
|
572
|
-
|
|
1093
|
+
const bulletText = bulletMatch[1];
|
|
1094
|
+
currentSection.bullets.push(bulletText);
|
|
1095
|
+
this.extractMetricsFromText(bulletText, currentSection.metrics);
|
|
573
1096
|
continue;
|
|
574
1097
|
}
|
|
575
1098
|
const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/);
|
|
576
1099
|
if (numberedMatch && numberedMatch[1] && currentSection) {
|
|
577
|
-
|
|
1100
|
+
const itemText = numberedMatch[1];
|
|
1101
|
+
currentSection.bullets.push(itemText);
|
|
1102
|
+
this.extractMetricsFromText(itemText, currentSection.metrics);
|
|
578
1103
|
continue;
|
|
579
1104
|
}
|
|
580
1105
|
const metricMatch = trimmedLine.match(/\$?([\d,]+\.?\d*)[%]?\s*[-–—:]\s*(.+)/);
|
|
@@ -585,6 +1110,9 @@ var ContentAnalyzer = class {
|
|
|
585
1110
|
});
|
|
586
1111
|
continue;
|
|
587
1112
|
}
|
|
1113
|
+
if (currentSection && trimmedLine) {
|
|
1114
|
+
this.extractMetricsFromText(trimmedLine, currentSection.metrics);
|
|
1115
|
+
}
|
|
588
1116
|
if (trimmedLine.includes("|") && currentSection) {
|
|
589
1117
|
const cells = trimmedLine.split("|").map((c) => c.trim()).filter((c) => c && !c.match(/^-+$/));
|
|
590
1118
|
if (cells.length >= 2) {
|
|
@@ -610,6 +1138,7 @@ var ContentAnalyzer = class {
|
|
|
610
1138
|
}
|
|
611
1139
|
/**
|
|
612
1140
|
* Extract SCQA structure (Barbara Minto)
|
|
1141
|
+
* CRITICAL: Answer must be clean prose, NOT bullet points
|
|
613
1142
|
*/
|
|
614
1143
|
extractSCQA(text) {
|
|
615
1144
|
const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
|
|
@@ -617,34 +1146,45 @@ var ContentAnalyzer = class {
|
|
|
617
1146
|
let complication = "";
|
|
618
1147
|
let question = "";
|
|
619
1148
|
let answer = "";
|
|
620
|
-
for (const para of paragraphs.slice(0,
|
|
1149
|
+
for (const para of paragraphs.slice(0, 5)) {
|
|
1150
|
+
if (para.startsWith("-") || para.startsWith("#") || para.startsWith("|")) continue;
|
|
621
1151
|
if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
|
|
622
1152
|
situation = this.extractFirstSentence(para);
|
|
623
1153
|
break;
|
|
624
1154
|
}
|
|
625
1155
|
}
|
|
626
1156
|
for (const para of paragraphs) {
|
|
1157
|
+
if (para.startsWith("-") || para.startsWith("|")) continue;
|
|
627
1158
|
if (this.containsSignals(para.toLowerCase(), this.complicationSignals)) {
|
|
628
1159
|
complication = this.extractFirstSentence(para);
|
|
629
1160
|
break;
|
|
630
1161
|
}
|
|
631
1162
|
}
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
1163
|
+
for (const para of paragraphs) {
|
|
1164
|
+
if (para.startsWith("-") || para.includes("\n-") || para.startsWith("|")) continue;
|
|
1165
|
+
if (para.startsWith("*") && !para.startsWith("**")) continue;
|
|
635
1166
|
const lowerPara = para.toLowerCase();
|
|
636
1167
|
if (this.containsSignals(lowerPara, this.answerSignals) && !this.containsSignals(lowerPara, this.ctaSignals)) {
|
|
637
|
-
|
|
638
|
-
|
|
1168
|
+
const sentence = this.extractFirstSentence(para);
|
|
1169
|
+
if (sentence.split(/\s+/).length >= 8 && /\b(is|are|will|can|provides?|delivers?|enables?)\b/i.test(sentence)) {
|
|
1170
|
+
answer = sentence;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
639
1173
|
}
|
|
640
1174
|
}
|
|
641
|
-
if (!situation && paragraphs.length > 0
|
|
642
|
-
|
|
1175
|
+
if (!situation && paragraphs.length > 0) {
|
|
1176
|
+
for (const para of paragraphs.slice(0, 5)) {
|
|
1177
|
+
if (!para.startsWith("-") && !para.startsWith("#") && para.length > 50) {
|
|
1178
|
+
situation = this.extractFirstSentence(para);
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
643
1182
|
}
|
|
644
|
-
if (!answer
|
|
645
|
-
for (const para of paragraphs
|
|
1183
|
+
if (!answer) {
|
|
1184
|
+
for (const para of paragraphs) {
|
|
1185
|
+
if (para.startsWith("-") || para.includes("\n-")) continue;
|
|
646
1186
|
const lowerPara = para.toLowerCase();
|
|
647
|
-
if (lowerPara.includes("
|
|
1187
|
+
if (lowerPara.includes("bottom line") || lowerPara.includes("delivers both") || lowerPara.includes("the result") || lowerPara.includes("in summary")) {
|
|
648
1188
|
answer = this.extractFirstSentence(para);
|
|
649
1189
|
break;
|
|
650
1190
|
}
|
|
@@ -803,56 +1343,123 @@ var ContentAnalyzer = class {
|
|
|
803
1343
|
}
|
|
804
1344
|
/**
|
|
805
1345
|
* Extract data points (metrics with values)
|
|
806
|
-
*
|
|
1346
|
+
* REWRITTEN: Proper markdown table parsing and meaningful label extraction
|
|
807
1347
|
*/
|
|
808
1348
|
extractDataPoints(text) {
|
|
809
1349
|
const dataPoints = [];
|
|
810
1350
|
const usedValues = /* @__PURE__ */ new Set();
|
|
811
|
-
const
|
|
812
|
-
if (
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
usedValues.add(valueCell);
|
|
822
|
-
dataPoints.push({ value: valueCell, label: labelCell.slice(0, 40) });
|
|
823
|
-
}
|
|
1351
|
+
const bulletPatterns = text.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?(?:\s*\([^)]+\))?)/gm);
|
|
1352
|
+
if (bulletPatterns) {
|
|
1353
|
+
for (const match of bulletPatterns) {
|
|
1354
|
+
const parsed = match.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?)/);
|
|
1355
|
+
if (parsed && parsed[1] && parsed[2]) {
|
|
1356
|
+
const label = parsed[1].trim();
|
|
1357
|
+
const value = parsed[2].trim();
|
|
1358
|
+
if (!usedValues.has(value) && label.length >= 5) {
|
|
1359
|
+
usedValues.add(value);
|
|
1360
|
+
dataPoints.push({ value, label: this.cleanMetricLabel(label) });
|
|
824
1361
|
}
|
|
825
1362
|
}
|
|
826
1363
|
}
|
|
827
1364
|
}
|
|
828
1365
|
const lines = text.split("\n");
|
|
1366
|
+
let inTable = false;
|
|
1367
|
+
let tableRows = [];
|
|
1368
|
+
let headerRow = [];
|
|
829
1369
|
for (const line of lines) {
|
|
830
|
-
if (line.includes("|"))
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1370
|
+
if (line.includes("|") && line.trim().length > 3) {
|
|
1371
|
+
const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
1372
|
+
if (cells.every((c) => /^[-:]+$/.test(c))) continue;
|
|
1373
|
+
if (!inTable) {
|
|
1374
|
+
headerRow = cells;
|
|
1375
|
+
inTable = true;
|
|
1376
|
+
} else {
|
|
1377
|
+
tableRows.push(cells);
|
|
1378
|
+
}
|
|
1379
|
+
} else if (inTable && tableRows.length > 0) {
|
|
1380
|
+
this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
|
|
1381
|
+
inTable = false;
|
|
1382
|
+
tableRows = [];
|
|
1383
|
+
headerRow = [];
|
|
835
1384
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1385
|
+
}
|
|
1386
|
+
if (tableRows.length > 0) {
|
|
1387
|
+
this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
|
|
1388
|
+
}
|
|
1389
|
+
for (const line of lines) {
|
|
1390
|
+
if (line.includes("|")) continue;
|
|
1391
|
+
if (/```/.test(line)) continue;
|
|
1392
|
+
const percentPattern = line.match(/(\d+(?:\.\d+)?%)\s+(reduction|increase|improvement|growth|decrease)\s+(?:in\s+)?([^.]+)/i);
|
|
1393
|
+
if (percentPattern && percentPattern[1] && percentPattern[3]) {
|
|
1394
|
+
const value = percentPattern[1];
|
|
1395
|
+
if (!usedValues.has(value)) {
|
|
1396
|
+
usedValues.add(value);
|
|
1397
|
+
const action = percentPattern[2] || "change";
|
|
1398
|
+
const subject = percentPattern[3].slice(0, 30).trim();
|
|
1399
|
+
dataPoints.push({ value, label: `${action} in ${subject}` });
|
|
843
1400
|
}
|
|
844
1401
|
}
|
|
845
1402
|
}
|
|
846
1403
|
return dataPoints.slice(0, 4);
|
|
847
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Extract metrics from a parsed markdown table
|
|
1407
|
+
*/
|
|
1408
|
+
extractMetricsFromTable(headers, rows, dataPoints, usedValues) {
|
|
1409
|
+
for (const row of rows) {
|
|
1410
|
+
const label = row[0];
|
|
1411
|
+
if (!label || label.length < 3) continue;
|
|
1412
|
+
for (let i = 1; i < row.length; i++) {
|
|
1413
|
+
const cell = row[i];
|
|
1414
|
+
if (!cell) continue;
|
|
1415
|
+
if (/^\$?[\d,]+\.?\d*[%MBK]?$/.test(cell.replace(/[,\s]/g, "")) || /^\d+\s*(seconds?|minutes?|hours?|ms|gb|mb)$/i.test(cell)) {
|
|
1416
|
+
if (!usedValues.has(cell)) {
|
|
1417
|
+
usedValues.add(cell);
|
|
1418
|
+
const colHeader = headers[i] || "";
|
|
1419
|
+
const fullLabel = colHeader && colHeader !== label ? `${label} (${colHeader})` : label;
|
|
1420
|
+
dataPoints.push({
|
|
1421
|
+
value: cell,
|
|
1422
|
+
label: this.cleanMetricLabel(fullLabel)
|
|
1423
|
+
});
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Clean a metric label to ensure it's complete and meaningful
|
|
1432
|
+
*/
|
|
1433
|
+
cleanMetricLabel(raw) {
|
|
1434
|
+
let label = raw.replace(/\*\*/g, "").replace(/\|/g, " ").replace(/[:\-–—]+$/, "").replace(/\s+/g, " ").trim();
|
|
1435
|
+
if (label.length > 0) {
|
|
1436
|
+
label = label.charAt(0).toUpperCase() + label.slice(1);
|
|
1437
|
+
}
|
|
1438
|
+
if (label.length > 40) {
|
|
1439
|
+
const words = label.split(/\s+/);
|
|
1440
|
+
label = "";
|
|
1441
|
+
for (const word of words) {
|
|
1442
|
+
if ((label + " " + word).length <= 40) {
|
|
1443
|
+
label = label ? label + " " + word : word;
|
|
1444
|
+
} else {
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return label || "Value";
|
|
1450
|
+
}
|
|
848
1451
|
/**
|
|
849
1452
|
* Extract a meaningful label from a line containing a metric
|
|
850
1453
|
*/
|
|
851
1454
|
extractLabelFromLine(line, value) {
|
|
1455
|
+
const colonMatch = line.match(/([^:]+):\s*\$?[\d]/);
|
|
1456
|
+
if (colonMatch && colonMatch[1]) {
|
|
1457
|
+
return this.cleanMetricLabel(colonMatch[1]);
|
|
1458
|
+
}
|
|
852
1459
|
const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
|
|
853
1460
|
const beforeValue = cleaned.split(value)[0] || "";
|
|
854
1461
|
const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
|
|
855
|
-
return words.slice(-4).join(" ")
|
|
1462
|
+
return this.cleanMetricLabel(words.slice(-4).join(" "));
|
|
856
1463
|
}
|
|
857
1464
|
/**
|
|
858
1465
|
* Check if text contains any of the signals
|
|
@@ -877,545 +1484,958 @@ var ContentAnalyzer = class {
|
|
|
877
1484
|
const fallback = cleaned.slice(0, 150);
|
|
878
1485
|
return fallback.length >= 20 ? fallback : "";
|
|
879
1486
|
}
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
// src/core/SlideFactory.ts
|
|
883
|
-
var SlideFactory = class {
|
|
884
|
-
templates;
|
|
885
|
-
usedContent = /* @__PURE__ */ new Set();
|
|
886
|
-
constructor() {
|
|
887
|
-
this.templates = this.initializeTemplates();
|
|
888
|
-
}
|
|
889
1487
|
/**
|
|
890
|
-
*
|
|
1488
|
+
* Extract metrics from natural language text
|
|
1489
|
+
* Handles formats like "40% productivity loss", "$2.5M investment"
|
|
891
1490
|
*/
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
/**
|
|
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
|
|
914
|
-
*/
|
|
915
|
-
async createSlides(analysis, mode) {
|
|
916
|
-
const slides = [];
|
|
917
|
-
let slideIndex = 0;
|
|
918
|
-
this.usedContent.clear();
|
|
919
|
-
slides.push(this.createTitleSlide(slideIndex++, analysis));
|
|
920
|
-
this.isContentUsed(analysis.titles[0] ?? "");
|
|
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) {
|
|
925
|
-
slides.push(this.createAgendaSlide(slideIndex++, analysis));
|
|
926
|
-
}
|
|
927
|
-
if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
|
|
928
|
-
slides.push(this.createContextSlide(slideIndex++, analysis, mode));
|
|
929
|
-
}
|
|
930
|
-
if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
|
|
931
|
-
slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
|
|
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
|
-
}
|
|
1491
|
+
extractMetricsFromText(text, metrics) {
|
|
1492
|
+
const usedValues = new Set(metrics.map((m) => m.value));
|
|
1493
|
+
const moneyMatches = text.matchAll(/(\$[\d,.]+[MBK]?)\s+([a-zA-Z][a-zA-Z\s]{2,30})/g);
|
|
1494
|
+
for (const match of moneyMatches) {
|
|
1495
|
+
const value = match[1];
|
|
1496
|
+
const rawLabel = match[2]?.trim();
|
|
1497
|
+
if (value && rawLabel && !usedValues.has(value)) {
|
|
1498
|
+
usedValues.add(value);
|
|
1499
|
+
metrics.push({ value, label: this.cleanMetricLabel(rawLabel) });
|
|
942
1500
|
}
|
|
943
1501
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1502
|
+
const percentMatches = text.matchAll(/(\d+(?:\.\d+)?%)\s+([a-zA-Z][a-zA-Z\s]{2,30})/g);
|
|
1503
|
+
for (const match of percentMatches) {
|
|
1504
|
+
const value = match[1];
|
|
1505
|
+
const rawLabel = match[2]?.trim();
|
|
1506
|
+
if (value && rawLabel && !usedValues.has(value)) {
|
|
1507
|
+
usedValues.add(value);
|
|
1508
|
+
metrics.push({ value, label: this.cleanMetricLabel(rawLabel) });
|
|
948
1509
|
}
|
|
949
1510
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
for (const starMoment of analysis.starMoments.slice(0, 2)) {
|
|
958
|
-
const wordCount = starMoment.split(/\s+/).length;
|
|
959
|
-
const hasVerb = verbPattern.test(starMoment);
|
|
960
|
-
if (wordCount >= 6 && starMoment.length >= 40 && hasVerb && !this.isContentUsed(starMoment)) {
|
|
961
|
-
slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
|
|
1511
|
+
const multMatches = text.matchAll(/(\d+[xX])\s+([a-zA-Z][a-zA-Z\s]{2,20})/g);
|
|
1512
|
+
for (const match of multMatches) {
|
|
1513
|
+
const value = match[1];
|
|
1514
|
+
const rawLabel = match[2]?.trim();
|
|
1515
|
+
if (value && rawLabel && !usedValues.has(value)) {
|
|
1516
|
+
usedValues.add(value);
|
|
1517
|
+
metrics.push({ value, label: this.cleanMetricLabel(rawLabel) });
|
|
962
1518
|
}
|
|
963
1519
|
}
|
|
964
|
-
|
|
965
|
-
|
|
1520
|
+
const numMatches = text.matchAll(/(\d{3,}\+?)\s+([a-zA-Z][a-zA-Z\s]{2,20})/g);
|
|
1521
|
+
for (const match of numMatches) {
|
|
1522
|
+
const value = match[1];
|
|
1523
|
+
const rawLabel = match[2]?.trim();
|
|
1524
|
+
if (value && rawLabel && !usedValues.has(value)) {
|
|
1525
|
+
usedValues.add(value);
|
|
1526
|
+
metrics.push({ value, label: this.cleanMetricLabel(rawLabel) });
|
|
1527
|
+
}
|
|
966
1528
|
}
|
|
967
|
-
slides.push(this.createThankYouSlide(slideIndex++));
|
|
968
|
-
return slides;
|
|
969
1529
|
}
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
// src/core/ContentPatternClassifier.ts
|
|
1533
|
+
var ContentPatternClassifier = class {
|
|
1534
|
+
// Regex patterns for content detection
|
|
1535
|
+
patterns = {
|
|
1536
|
+
// Big numbers: $4.88M, 23%, 10x, 500+, etc.
|
|
1537
|
+
bigNumber: /(\$[\d,.]+[MBK]?|\d+(?:\.\d+)?%|\d+[xX]|\d{2,}(?:\+|,\d{3})+)/,
|
|
1538
|
+
// Comparison language
|
|
1539
|
+
comparison: /\b(vs\.?|versus|compared\s+to|before\s+(?:and\s+)?after|better\s+than|worse\s+than|alternative|option\s+[A-Z1-3])\b/i,
|
|
1540
|
+
// Timeline markers
|
|
1541
|
+
timeline: /\b(phase\s*\d|week\s*\d|month\s*\d|day\s*\d|Q[1-4]|20\d{2}|step\s*\d|year\s*\d|sprint\s*\d)\b/i,
|
|
1542
|
+
// Process/steps language
|
|
1543
|
+
process: /\b(step\s*\d|stage\s*\d|pillar|phase|first|second|third|finally|then|next|workflow|process)\b/i,
|
|
1544
|
+
// Quote patterns
|
|
1545
|
+
quote: /^[""]|[""]\s*[-—–]\s*|^\s*>\s*|[""]$/,
|
|
1546
|
+
// Code patterns
|
|
1547
|
+
code: /```|`[^`]+`|function\s*\(|const\s+\w+\s*=|import\s+{|class\s+\w+|=>\s*{/,
|
|
1548
|
+
// Metric patterns (for detecting multiple metrics)
|
|
1549
|
+
metric: /(\d+(?:\.\d+)?(?:%|[xX]|[MBK])?)\s*[-–:]\s*|\b(?:increased|decreased|grew|improved|reduced)\s+(?:by\s+)?(\d+)/gi
|
|
1550
|
+
};
|
|
970
1551
|
/**
|
|
971
|
-
*
|
|
1552
|
+
* Classify a content section to determine its primary pattern.
|
|
1553
|
+
* Returns a ContentPattern object used by SlideFactory to select slide type.
|
|
972
1554
|
*/
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1555
|
+
classify(section) {
|
|
1556
|
+
const fullText = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
|
|
1557
|
+
const wordCount = fullText.split(/\s+/).filter((w) => w.length > 0).length;
|
|
1558
|
+
const hasBigNumber = this.patterns.bigNumber.test(fullText);
|
|
1559
|
+
const bigNumberMatch = fullText.match(this.patterns.bigNumber);
|
|
1560
|
+
const hasComparison = this.patterns.comparison.test(fullText);
|
|
1561
|
+
const hasTimeline = this.patterns.timeline.test(fullText);
|
|
1562
|
+
const hasProcess = this.patterns.process.test(section.header) || section.bullets.length >= 3 && this.hasNumberedItems(section.bullets);
|
|
1563
|
+
const hasQuote = this.patterns.quote.test(section.content);
|
|
1564
|
+
const hasCode = this.patterns.code.test(section.content);
|
|
1565
|
+
const metricMatches = fullText.match(this.patterns.metric);
|
|
1566
|
+
const hasMetrics = section.metrics.length >= 3 || metricMatches && metricMatches.length >= 3;
|
|
1567
|
+
let primaryPattern = "prose";
|
|
1568
|
+
if (hasQuote && section.content.length > 20) {
|
|
1569
|
+
primaryPattern = "quote";
|
|
1570
|
+
} else if (hasCode) {
|
|
1571
|
+
primaryPattern = "code";
|
|
1572
|
+
} else if (hasBigNumber && wordCount < 30) {
|
|
1573
|
+
primaryPattern = "big_number";
|
|
1574
|
+
} else if (hasMetrics) {
|
|
1575
|
+
primaryPattern = "metrics";
|
|
1576
|
+
} else if (hasComparison) {
|
|
1577
|
+
primaryPattern = "comparison";
|
|
1578
|
+
} else if (hasTimeline) {
|
|
1579
|
+
primaryPattern = "timeline";
|
|
1580
|
+
} else if (hasProcess && section.bullets.length >= 3) {
|
|
1581
|
+
primaryPattern = "process";
|
|
1582
|
+
} else if (section.bullets.length >= 2) {
|
|
1583
|
+
primaryPattern = "bullets";
|
|
1584
|
+
} else {
|
|
1585
|
+
primaryPattern = "prose";
|
|
1586
|
+
}
|
|
1587
|
+
const result = {
|
|
1588
|
+
hasBigNumber,
|
|
1589
|
+
hasComparison,
|
|
1590
|
+
hasTimeline,
|
|
1591
|
+
hasProcess,
|
|
1592
|
+
hasMetrics: !!hasMetrics,
|
|
1593
|
+
hasQuote,
|
|
1594
|
+
hasCode,
|
|
1595
|
+
bulletCount: section.bullets.length,
|
|
1596
|
+
wordCount,
|
|
1597
|
+
primaryPattern
|
|
983
1598
|
};
|
|
1599
|
+
if (bigNumberMatch && bigNumberMatch[1]) {
|
|
1600
|
+
result.bigNumberValue = bigNumberMatch[1];
|
|
1601
|
+
}
|
|
1602
|
+
return result;
|
|
984
1603
|
}
|
|
985
1604
|
/**
|
|
986
|
-
*
|
|
1605
|
+
* Check if bullets appear to be numbered/ordered items
|
|
987
1606
|
*/
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
title: this.truncate(this.extractFirstSentence(section.content), 80),
|
|
995
|
-
keyMessage: section.header
|
|
996
|
-
},
|
|
997
|
-
classes: ["slide-single-statement"]
|
|
998
|
-
};
|
|
1607
|
+
hasNumberedItems(bullets) {
|
|
1608
|
+
let numberedCount = 0;
|
|
1609
|
+
for (const bullet of bullets) {
|
|
1610
|
+
if (/^\d+[.)]\s|^[a-z][.)]\s|^step\s*\d/i.test(bullet.trim())) {
|
|
1611
|
+
numberedCount++;
|
|
1612
|
+
}
|
|
999
1613
|
}
|
|
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
|
-
};
|
|
1614
|
+
return numberedCount >= 2;
|
|
1009
1615
|
}
|
|
1010
1616
|
/**
|
|
1011
|
-
* Extract
|
|
1617
|
+
* Extract the big number value from content for display
|
|
1012
1618
|
*/
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1619
|
+
extractBigNumber(content) {
|
|
1620
|
+
const match = content.match(this.patterns.bigNumber);
|
|
1621
|
+
if (!match || !match[1]) return null;
|
|
1622
|
+
const value = match[1];
|
|
1623
|
+
const afterNumber = content.slice(content.indexOf(value) + value.length);
|
|
1624
|
+
const context = afterNumber.trim().slice(0, 60).split(/[.!?]/)[0]?.trim() ?? "";
|
|
1625
|
+
return { value, context };
|
|
1017
1626
|
}
|
|
1018
1627
|
/**
|
|
1019
|
-
*
|
|
1628
|
+
* Extract comparison elements from content
|
|
1020
1629
|
*/
|
|
1021
|
-
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1630
|
+
extractComparison(section) {
|
|
1631
|
+
const text = section.content;
|
|
1632
|
+
const vsMatch = text.match(/(.{10,50})\s+(?:vs\.?|versus)\s+(.{10,50})/i);
|
|
1633
|
+
if (vsMatch && vsMatch[1] && vsMatch[2]) {
|
|
1634
|
+
return { left: vsMatch[1].trim(), right: vsMatch[2].trim() };
|
|
1635
|
+
}
|
|
1636
|
+
const beforeAfterMatch = text.match(/before[:\s]+(.{10,100})(?:after[:\s]+(.{10,100}))?/i);
|
|
1637
|
+
if (beforeAfterMatch && beforeAfterMatch[1]) {
|
|
1638
|
+
const left = beforeAfterMatch[1].trim();
|
|
1639
|
+
const right = beforeAfterMatch[2] ? beforeAfterMatch[2].trim() : "After";
|
|
1640
|
+
return { left, right };
|
|
1641
|
+
}
|
|
1642
|
+
if (section.bullets.length === 2) {
|
|
1643
|
+
const left = section.bullets[0];
|
|
1644
|
+
const right = section.bullets[1];
|
|
1645
|
+
if (left && right) {
|
|
1646
|
+
return { left, right };
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return null;
|
|
1035
1650
|
}
|
|
1036
1651
|
/**
|
|
1037
|
-
*
|
|
1652
|
+
* Extract timeline/process steps from content
|
|
1038
1653
|
*/
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1654
|
+
extractSteps(section) {
|
|
1655
|
+
const steps = [];
|
|
1656
|
+
for (const bullet of section.bullets) {
|
|
1657
|
+
const stepMatch = bullet.match(/^((?:step|phase|stage)\s*\d+|[1-9]\.|\d+\))\s*[-:.]?\s*(.+)/i);
|
|
1658
|
+
if (stepMatch && stepMatch[1] && stepMatch[2]) {
|
|
1659
|
+
steps.push({
|
|
1660
|
+
label: stepMatch[1].trim(),
|
|
1661
|
+
description: stepMatch[2].trim()
|
|
1662
|
+
});
|
|
1663
|
+
} else {
|
|
1664
|
+
steps.push({
|
|
1665
|
+
label: `${steps.length + 1}`,
|
|
1666
|
+
description: bullet
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1045
1669
|
}
|
|
1046
|
-
return
|
|
1047
|
-
index,
|
|
1048
|
-
type: "title",
|
|
1049
|
-
data: {
|
|
1050
|
-
title: analysis.titles[0] ?? "Presentation",
|
|
1051
|
-
subtitle: this.truncate(subtitle, 80),
|
|
1052
|
-
keyMessage: analysis.scqa.answer
|
|
1053
|
-
},
|
|
1054
|
-
classes: ["slide-title"]
|
|
1055
|
-
};
|
|
1670
|
+
return steps;
|
|
1056
1671
|
}
|
|
1057
1672
|
/**
|
|
1058
|
-
*
|
|
1673
|
+
* Determine if content is suitable for a three-column layout
|
|
1674
|
+
* (3 pillars, 3 key points, etc.)
|
|
1059
1675
|
*/
|
|
1060
|
-
|
|
1061
|
-
return
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
bullets: analysis.keyMessages.map((msg, i) => `${i + 1}. ${this.truncate(msg, 50)}`)
|
|
1067
|
-
},
|
|
1068
|
-
classes: ["slide-agenda"]
|
|
1069
|
-
};
|
|
1676
|
+
isSuitableForThreeColumn(section) {
|
|
1677
|
+
if (section.bullets.length === 3) return true;
|
|
1678
|
+
if (/three|3\s*(pillar|point|key|step)/i.test(section.header)) return true;
|
|
1679
|
+
const threePartPattern = /first[,.].*second[,.].*third/i;
|
|
1680
|
+
if (threePartPattern.test(section.content)) return true;
|
|
1681
|
+
return false;
|
|
1070
1682
|
}
|
|
1071
1683
|
/**
|
|
1072
|
-
*
|
|
1684
|
+
* Check if content should be displayed as a quote slide
|
|
1073
1685
|
*/
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
type: "single-statement",
|
|
1079
|
-
data: {
|
|
1080
|
-
title: this.truncate(analysis.scqa.situation, 80),
|
|
1081
|
-
keyMessage: "The current state"
|
|
1082
|
-
},
|
|
1083
|
-
classes: ["slide-single-statement"]
|
|
1084
|
-
};
|
|
1686
|
+
isQuoteContent(section) {
|
|
1687
|
+
const content = section.content.trim();
|
|
1688
|
+
if (!this.patterns.quote.test(content)) {
|
|
1689
|
+
return { isQuote: false };
|
|
1085
1690
|
}
|
|
1691
|
+
const attrMatch = content.match(/[-—–]\s*(.+)$/);
|
|
1692
|
+
if (attrMatch && attrMatch[1]) {
|
|
1693
|
+
return { isQuote: true, attribution: attrMatch[1].trim() };
|
|
1694
|
+
}
|
|
1695
|
+
return { isQuote: true };
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
// src/core/SlideFactory.ts
|
|
1700
|
+
var SlideFactory = class {
|
|
1701
|
+
kb;
|
|
1702
|
+
presentationType;
|
|
1703
|
+
classifier;
|
|
1704
|
+
config;
|
|
1705
|
+
usedContent;
|
|
1706
|
+
usedTitles;
|
|
1707
|
+
constructor(kb, type) {
|
|
1708
|
+
this.kb = kb;
|
|
1709
|
+
this.presentationType = type;
|
|
1710
|
+
this.classifier = new ContentPatternClassifier();
|
|
1711
|
+
this.usedContent = /* @__PURE__ */ new Set();
|
|
1712
|
+
this.usedTitles = /* @__PURE__ */ new Set();
|
|
1713
|
+
this.config = this.loadKBConfig(type);
|
|
1714
|
+
logger.step(`SlideFactory v7.1.0 initialized for ${type} (${this.config.mode} mode)`);
|
|
1715
|
+
logger.step(`KB Config loaded: ${this.config.allowedTypes.length} allowed types`);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Load ALL configuration from KB - ZERO hardcoded values after this.
|
|
1719
|
+
*/
|
|
1720
|
+
loadKBConfig(type) {
|
|
1086
1721
|
return {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1722
|
+
defaults: this.kb.getSlideDefaults(type),
|
|
1723
|
+
scqaTitles: this.kb.getSCQATitles(type),
|
|
1724
|
+
sparklineTitles: this.kb.getSparklineTitles(type),
|
|
1725
|
+
rules: this.kb.getValidationRules(type),
|
|
1726
|
+
allowedTypes: this.kb.getAllowedSlideTypes(type),
|
|
1727
|
+
mode: this.kb.getModeForType(type),
|
|
1728
|
+
insightMarkers: this.kb.getInsightMarkers(),
|
|
1729
|
+
glanceTest: this.kb.getDuarteGlanceTest(),
|
|
1730
|
+
millersLaw: this.kb.getMillersLaw(),
|
|
1731
|
+
typography: this.kb.getTypographyForType(type),
|
|
1732
|
+
antiPatterns: this.kb.getAntiPatternsForType(type),
|
|
1733
|
+
requiredElements: this.kb.getRequiredElements(type)
|
|
1095
1734
|
};
|
|
1096
1735
|
}
|
|
1097
1736
|
/**
|
|
1098
|
-
* Create
|
|
1737
|
+
* Create all slides from content analysis.
|
|
1738
|
+
* This is the main entry point - orchestrates the entire slide creation process.
|
|
1099
1739
|
*/
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1740
|
+
async createSlides(analysis) {
|
|
1741
|
+
const slides = [];
|
|
1742
|
+
const storyStructure = this.kb.getStoryStructure(this.presentationType);
|
|
1743
|
+
logger.step(`Using ${storyStructure.framework} story framework`);
|
|
1744
|
+
slides.push(this.createTitleSlide(0, analysis));
|
|
1745
|
+
const minSectionsForAgenda = Math.max(3, this.config.rules.bulletsPerSlide.max - 2);
|
|
1746
|
+
if (this.config.mode === "business" && analysis.sections.length >= minSectionsForAgenda) {
|
|
1747
|
+
slides.push(this.createAgendaSlide(slides.length, analysis));
|
|
1748
|
+
}
|
|
1749
|
+
if (storyStructure.framework === "scqa") {
|
|
1750
|
+
this.addSCQASlides(slides, analysis);
|
|
1751
|
+
} else if (storyStructure.framework === "sparkline") {
|
|
1752
|
+
this.addSparklineSlides(slides, analysis);
|
|
1753
|
+
}
|
|
1754
|
+
for (const section of analysis.sections) {
|
|
1755
|
+
const contentKey = this.normalizeKey(section.header);
|
|
1756
|
+
if (this.usedContent.has(contentKey)) continue;
|
|
1757
|
+
this.usedContent.add(contentKey);
|
|
1758
|
+
const pattern = this.classifier.classify(section);
|
|
1759
|
+
const slideType = this.kb.mapContentPatternToSlideType(pattern, this.config.allowedTypes);
|
|
1760
|
+
const slide = this.createSlideByType(slides.length, slideType, section, pattern);
|
|
1761
|
+
if (slide) {
|
|
1762
|
+
slides.push(slide);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
const minDataPointsForMetrics = 2;
|
|
1766
|
+
if (analysis.dataPoints.length >= minDataPointsForMetrics) {
|
|
1767
|
+
const hasMetricsSlide = slides.some(
|
|
1768
|
+
(s) => s.type === "metrics-grid" || s.type === "big-number"
|
|
1769
|
+
);
|
|
1770
|
+
if (!hasMetricsSlide) {
|
|
1771
|
+
slides.push(this.createMetricsGridSlide(slides.length, analysis.dataPoints));
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (analysis.sparkline?.callToAdventure || analysis.scqa?.answer) {
|
|
1775
|
+
slides.push(this.createCTASlide(slides.length, analysis));
|
|
1776
|
+
}
|
|
1777
|
+
slides.push(this.createThankYouSlide(slides.length));
|
|
1778
|
+
const validatedSlides = this.validateAndFixSlides(slides);
|
|
1779
|
+
this.checkRequiredElements(validatedSlides);
|
|
1780
|
+
this.checkAntiPatterns(validatedSlides);
|
|
1781
|
+
logger.step(`Created ${validatedSlides.length} validated slides`);
|
|
1782
|
+
return validatedSlides;
|
|
1783
|
+
}
|
|
1784
|
+
// ===========================================================================
|
|
1785
|
+
// SLIDE TYPE ROUTER - Routes to appropriate creator based on KB-selected type
|
|
1786
|
+
// ===========================================================================
|
|
1787
|
+
createSlideByType(index, type, section, pattern) {
|
|
1788
|
+
const normalizedType = type.toLowerCase().replace(/_/g, "-");
|
|
1789
|
+
switch (normalizedType) {
|
|
1790
|
+
case "big-number":
|
|
1791
|
+
case "data-insight":
|
|
1792
|
+
return this.createBigNumberSlide(index, section, pattern);
|
|
1793
|
+
case "comparison":
|
|
1794
|
+
case "options-comparison":
|
|
1795
|
+
return this.createComparisonSlide(index, section);
|
|
1796
|
+
case "timeline":
|
|
1797
|
+
case "process-timeline":
|
|
1798
|
+
case "roadmap":
|
|
1799
|
+
return this.createTimelineSlide(index, section);
|
|
1800
|
+
case "process":
|
|
1801
|
+
return this.createProcessSlide(index, section);
|
|
1802
|
+
case "three-column":
|
|
1803
|
+
return this.createThreeColumnSlide(index, section);
|
|
1804
|
+
case "two-column":
|
|
1805
|
+
return this.createTwoColumnSlide(index, section);
|
|
1806
|
+
case "quote":
|
|
1807
|
+
case "testimonial":
|
|
1808
|
+
case "social-proof":
|
|
1809
|
+
return this.createQuoteSlide(index, section);
|
|
1810
|
+
case "metrics-grid":
|
|
1811
|
+
return this.createMetricsGridSlide(index, section.metrics);
|
|
1812
|
+
case "bullet-points":
|
|
1813
|
+
case "detailed-findings":
|
|
1814
|
+
return this.createBulletSlide(index, section);
|
|
1815
|
+
case "single-statement":
|
|
1816
|
+
case "big-idea":
|
|
1817
|
+
return this.createSingleStatementSlide(index, section);
|
|
1818
|
+
case "code-snippet":
|
|
1819
|
+
case "technical":
|
|
1820
|
+
return this.createCodeSlide(index, section);
|
|
1821
|
+
default:
|
|
1822
|
+
logger.warn(`Unknown slide type '${type}', using bullet-points`);
|
|
1823
|
+
return this.createBulletSlide(index, section);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
// ===========================================================================
|
|
1827
|
+
// STRUCTURAL SLIDES (title, agenda, thank you) - ALL from KB
|
|
1828
|
+
// ===========================================================================
|
|
1829
|
+
createTitleSlide(index, analysis) {
|
|
1830
|
+
const data = {
|
|
1831
|
+
title: this.truncateText(analysis.title, this.config.rules.wordsPerSlide.max)
|
|
1832
|
+
};
|
|
1833
|
+
if (analysis.keyMessages[0]) {
|
|
1834
|
+
data.subtitle = this.truncateText(
|
|
1835
|
+
analysis.keyMessages[0],
|
|
1836
|
+
this.config.defaults.subtitle.maxWords
|
|
1837
|
+
// FROM KB
|
|
1838
|
+
);
|
|
1111
1839
|
}
|
|
1112
1840
|
return {
|
|
1113
1841
|
index,
|
|
1114
|
-
type: "
|
|
1842
|
+
type: "title",
|
|
1843
|
+
data,
|
|
1844
|
+
classes: ["title-slide"]
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
createAgendaSlide(index, analysis) {
|
|
1848
|
+
const agendaItems = analysis.sections.filter((s) => s.level <= 2).slice(0, this.config.rules.bulletsPerSlide.max).map((s) => this.cleanText(s.header));
|
|
1849
|
+
return {
|
|
1850
|
+
index,
|
|
1851
|
+
type: "agenda",
|
|
1115
1852
|
data: {
|
|
1116
|
-
title:
|
|
1117
|
-
|
|
1118
|
-
bullets:
|
|
1853
|
+
title: this.config.defaults.agenda.title,
|
|
1854
|
+
// FROM KB - not hardcoded 'Agenda'
|
|
1855
|
+
bullets: agendaItems
|
|
1119
1856
|
},
|
|
1120
|
-
classes: ["slide
|
|
1857
|
+
classes: ["agenda-slide"]
|
|
1121
1858
|
};
|
|
1122
1859
|
}
|
|
1123
|
-
|
|
1124
|
-
* Create a key message slide.
|
|
1125
|
-
*/
|
|
1126
|
-
createMessageSlide(index, message, mode) {
|
|
1127
|
-
if (mode === "keynote") {
|
|
1128
|
-
return {
|
|
1129
|
-
index,
|
|
1130
|
-
type: "single-statement",
|
|
1131
|
-
data: {
|
|
1132
|
-
title: this.truncate(message, 60),
|
|
1133
|
-
keyMessage: message
|
|
1134
|
-
},
|
|
1135
|
-
classes: ["slide-single-statement"]
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1860
|
+
createThankYouSlide(index) {
|
|
1138
1861
|
return {
|
|
1139
1862
|
index,
|
|
1140
|
-
type: "
|
|
1863
|
+
type: "thank-you",
|
|
1141
1864
|
data: {
|
|
1142
|
-
title: this.
|
|
1143
|
-
|
|
1144
|
-
|
|
1865
|
+
title: this.config.defaults.thankYou.title,
|
|
1866
|
+
// FROM KB - not hardcoded 'Thank You'
|
|
1867
|
+
subtitle: this.config.defaults.thankYou.subtitle
|
|
1868
|
+
// FROM KB - not hardcoded 'Questions?'
|
|
1145
1869
|
},
|
|
1146
|
-
classes: ["
|
|
1870
|
+
classes: ["thank-you-slide"]
|
|
1147
1871
|
};
|
|
1148
1872
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1873
|
+
// ===========================================================================
|
|
1874
|
+
// STORY FRAMEWORK SLIDES (SCQA, Sparkline) - ALL titles from KB
|
|
1875
|
+
// ===========================================================================
|
|
1876
|
+
addSCQASlides(slides, analysis) {
|
|
1877
|
+
const scqa = analysis.scqa;
|
|
1878
|
+
const titles = this.config.scqaTitles;
|
|
1879
|
+
if (scqa?.situation && !this.usedContent.has("scqa-situation")) {
|
|
1880
|
+
this.usedContent.add("scqa-situation");
|
|
1881
|
+
slides.push({
|
|
1882
|
+
index: slides.length,
|
|
1883
|
+
type: "single-statement",
|
|
1159
1884
|
data: {
|
|
1160
|
-
title:
|
|
1161
|
-
|
|
1162
|
-
|
|
1885
|
+
title: titles.situation,
|
|
1886
|
+
// FROM KB - not hardcoded 'Current Situation'
|
|
1887
|
+
body: this.truncateText(scqa.situation, this.config.rules.wordsPerSlide.max)
|
|
1163
1888
|
},
|
|
1164
|
-
classes: ["slide
|
|
1165
|
-
};
|
|
1889
|
+
classes: ["situation-slide"]
|
|
1890
|
+
});
|
|
1166
1891
|
}
|
|
1167
|
-
if (
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1892
|
+
if (scqa?.complication && !this.usedContent.has("scqa-complication")) {
|
|
1893
|
+
this.usedContent.add("scqa-complication");
|
|
1894
|
+
slides.push({
|
|
1895
|
+
index: slides.length,
|
|
1896
|
+
type: "single-statement",
|
|
1171
1897
|
data: {
|
|
1172
|
-
title:
|
|
1173
|
-
|
|
1898
|
+
title: titles.complication,
|
|
1899
|
+
// FROM KB - not hardcoded 'The Challenge'
|
|
1900
|
+
body: this.truncateText(scqa.complication, this.config.rules.wordsPerSlide.max)
|
|
1174
1901
|
},
|
|
1175
|
-
classes: ["slide
|
|
1176
|
-
};
|
|
1902
|
+
classes: ["complication-slide"]
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
if (scqa?.question && !this.usedContent.has("scqa-question")) {
|
|
1906
|
+
this.usedContent.add("scqa-question");
|
|
1907
|
+
slides.push({
|
|
1908
|
+
index: slides.length,
|
|
1909
|
+
type: "single-statement",
|
|
1910
|
+
data: {
|
|
1911
|
+
title: titles.question,
|
|
1912
|
+
// FROM KB - not hardcoded 'The Question'
|
|
1913
|
+
body: this.truncateText(scqa.question, this.config.rules.wordsPerSlide.max)
|
|
1914
|
+
},
|
|
1915
|
+
classes: ["question-slide"]
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
addSparklineSlides(slides, analysis) {
|
|
1920
|
+
const spark = analysis.sparkline;
|
|
1921
|
+
const titles = this.config.sparklineTitles;
|
|
1922
|
+
const whatIsFirst = spark?.whatIs?.[0];
|
|
1923
|
+
if (whatIsFirst && !this.usedContent.has("spark-what-is")) {
|
|
1924
|
+
this.usedContent.add("spark-what-is");
|
|
1925
|
+
slides.push({
|
|
1926
|
+
index: slides.length,
|
|
1927
|
+
type: "single-statement",
|
|
1928
|
+
data: {
|
|
1929
|
+
title: titles.whatIs,
|
|
1930
|
+
// FROM KB - not hardcoded 'Where We Are Today'
|
|
1931
|
+
body: this.truncateText(whatIsFirst, this.config.rules.wordsPerSlide.max)
|
|
1932
|
+
},
|
|
1933
|
+
classes: ["what-is-slide"]
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
const whatCouldBeFirst = spark?.whatCouldBe?.[0];
|
|
1937
|
+
if (whatCouldBeFirst && !this.usedContent.has("spark-could-be")) {
|
|
1938
|
+
this.usedContent.add("spark-could-be");
|
|
1939
|
+
slides.push({
|
|
1940
|
+
index: slides.length,
|
|
1941
|
+
type: "single-statement",
|
|
1942
|
+
data: {
|
|
1943
|
+
title: titles.whatCouldBe,
|
|
1944
|
+
// FROM KB - not hardcoded 'What Could Be'
|
|
1945
|
+
body: this.truncateText(whatCouldBeFirst, this.config.rules.wordsPerSlide.max)
|
|
1946
|
+
},
|
|
1947
|
+
classes: ["what-could-be-slide"]
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
// ===========================================================================
|
|
1952
|
+
// CONTENT SLIDES - ALL values from KB
|
|
1953
|
+
// ===========================================================================
|
|
1954
|
+
createBigNumberSlide(index, section, pattern) {
|
|
1955
|
+
const fullContent = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
|
|
1956
|
+
const bigNumber = this.classifier.extractBigNumber(fullContent);
|
|
1957
|
+
const actualValue = bigNumber?.value || pattern.bigNumberValue;
|
|
1958
|
+
if (!actualValue) {
|
|
1959
|
+
logger.warn(`No number found for big-number slide, falling back to single-statement`);
|
|
1960
|
+
return this.createSingleStatementSlide(index, section);
|
|
1177
1961
|
}
|
|
1178
1962
|
return {
|
|
1179
1963
|
index,
|
|
1180
|
-
type: "
|
|
1964
|
+
type: "big-number",
|
|
1181
1965
|
data: {
|
|
1182
|
-
|
|
1183
|
-
|
|
1966
|
+
title: this.createTitle(section.header, section),
|
|
1967
|
+
keyMessage: actualValue,
|
|
1968
|
+
body: bigNumber?.context || this.truncateText(
|
|
1969
|
+
section.content,
|
|
1970
|
+
this.config.defaults.context.maxWords
|
|
1971
|
+
// FROM KB - not hardcoded 30
|
|
1972
|
+
)
|
|
1184
1973
|
},
|
|
1185
|
-
classes: ["slide
|
|
1974
|
+
classes: ["big-number-slide"]
|
|
1186
1975
|
};
|
|
1187
1976
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1977
|
+
createComparisonSlide(index, section) {
|
|
1978
|
+
const comparison = this.classifier.extractComparison(section);
|
|
1979
|
+
const labels = this.config.defaults.comparison;
|
|
1980
|
+
const leftFallback = labels.optionLabels[0] ?? "Option A";
|
|
1981
|
+
const rightFallback = labels.optionLabels[1] ?? "Option B";
|
|
1982
|
+
const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
|
|
1983
|
+
const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
|
|
1984
|
+
return {
|
|
1985
|
+
index,
|
|
1986
|
+
type: "comparison",
|
|
1987
|
+
data: {
|
|
1988
|
+
title: this.createTitle(section.header, section),
|
|
1989
|
+
columns: [
|
|
1990
|
+
{
|
|
1991
|
+
title: labels.leftLabel,
|
|
1992
|
+
// FROM KB - not hardcoded 'Before'
|
|
1993
|
+
content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
title: labels.rightLabel,
|
|
1997
|
+
// FROM KB - not hardcoded 'After'
|
|
1998
|
+
content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
|
|
1999
|
+
}
|
|
2000
|
+
]
|
|
2001
|
+
},
|
|
2002
|
+
classes: ["comparison-slide"]
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
createTimelineSlide(index, section) {
|
|
2006
|
+
const steps = this.classifier.extractSteps(section);
|
|
2007
|
+
const maxSteps = Math.min(
|
|
2008
|
+
steps.length,
|
|
2009
|
+
this.config.rules.bulletsPerSlide.max,
|
|
2010
|
+
this.config.millersLaw.maxItems
|
|
2011
|
+
// FROM KB - 7±2 rule
|
|
2012
|
+
);
|
|
2013
|
+
return {
|
|
2014
|
+
index,
|
|
2015
|
+
type: "timeline",
|
|
2016
|
+
data: {
|
|
2017
|
+
title: this.createTitle(section.header, section),
|
|
2018
|
+
steps: steps.slice(0, maxSteps).map((step) => ({
|
|
2019
|
+
label: step.label,
|
|
2020
|
+
description: this.truncateText(
|
|
2021
|
+
step.description,
|
|
2022
|
+
this.config.defaults.step.maxWords
|
|
2023
|
+
// FROM KB - not hardcoded 20
|
|
2024
|
+
)
|
|
2025
|
+
}))
|
|
2026
|
+
},
|
|
2027
|
+
classes: ["timeline-slide"]
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
createProcessSlide(index, section) {
|
|
2031
|
+
const steps = this.classifier.extractSteps(section);
|
|
2032
|
+
const maxSteps = Math.min(steps.length, this.config.rules.bulletsPerSlide.max);
|
|
2033
|
+
return {
|
|
2034
|
+
index,
|
|
2035
|
+
type: "process",
|
|
2036
|
+
data: {
|
|
2037
|
+
title: this.createTitle(section.header, section),
|
|
2038
|
+
steps: steps.slice(0, maxSteps).map((step, i) => ({
|
|
2039
|
+
number: i + 1,
|
|
2040
|
+
title: step.label,
|
|
2041
|
+
description: this.truncateText(
|
|
2042
|
+
step.description,
|
|
2043
|
+
this.config.defaults.step.maxWords
|
|
2044
|
+
// FROM KB - not hardcoded 15
|
|
2045
|
+
)
|
|
2046
|
+
}))
|
|
2047
|
+
},
|
|
2048
|
+
classes: ["process-slide"]
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
createThreeColumnSlide(index, section) {
|
|
2052
|
+
const columns = section.bullets.slice(0, 3);
|
|
2053
|
+
while (columns.length < 3) {
|
|
2054
|
+
columns.push("");
|
|
1202
2055
|
}
|
|
2056
|
+
const labelTemplate = this.config.defaults.column.labelTemplate;
|
|
2057
|
+
return {
|
|
2058
|
+
index,
|
|
2059
|
+
type: "three-column",
|
|
2060
|
+
data: {
|
|
2061
|
+
title: this.createTitle(section.header, section),
|
|
2062
|
+
columns: columns.map((content, i) => ({
|
|
2063
|
+
title: labelTemplate.replace("{n}", String(i + 1)),
|
|
2064
|
+
// FROM KB - not hardcoded 'Point ${i+1}'
|
|
2065
|
+
content: this.truncateText(
|
|
2066
|
+
content,
|
|
2067
|
+
this.config.defaults.columnContent.maxWords
|
|
2068
|
+
// FROM KB - not hardcoded 25
|
|
2069
|
+
)
|
|
2070
|
+
}))
|
|
2071
|
+
},
|
|
2072
|
+
classes: ["three-column-slide"]
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
createTwoColumnSlide(index, section) {
|
|
2076
|
+
const midpoint = Math.ceil(section.bullets.length / 2);
|
|
2077
|
+
const leftBullets = section.bullets.slice(0, midpoint);
|
|
2078
|
+
const rightBullets = section.bullets.slice(midpoint);
|
|
2079
|
+
const wordsPerBullet = Math.floor(
|
|
2080
|
+
this.config.rules.wordsPerSlide.max / this.config.rules.bulletsPerSlide.max
|
|
2081
|
+
);
|
|
1203
2082
|
return {
|
|
1204
2083
|
index,
|
|
1205
2084
|
type: "two-column",
|
|
1206
2085
|
data: {
|
|
1207
|
-
title:
|
|
1208
|
-
|
|
1209
|
-
|
|
2086
|
+
title: this.createTitle(section.header, section),
|
|
2087
|
+
leftColumn: {
|
|
2088
|
+
bullets: leftBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
|
|
2089
|
+
},
|
|
2090
|
+
rightColumn: {
|
|
2091
|
+
bullets: rightBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
|
|
2092
|
+
}
|
|
1210
2093
|
},
|
|
1211
|
-
classes: ["
|
|
2094
|
+
classes: ["two-column-slide"]
|
|
1212
2095
|
};
|
|
1213
2096
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
2097
|
+
createQuoteSlide(index, section) {
|
|
2098
|
+
const quoteInfo = this.classifier.isQuoteContent(section);
|
|
2099
|
+
const quoteText = section.content.replace(/^[""]|[""]$/g, "").trim();
|
|
2100
|
+
const quoteWordLimit = Math.min(
|
|
2101
|
+
this.config.glanceTest.wordLimit * 2,
|
|
2102
|
+
// FROM KB - not hardcoded
|
|
2103
|
+
this.config.rules.wordsPerSlide.max
|
|
2104
|
+
);
|
|
2105
|
+
const data = {
|
|
2106
|
+
title: this.createTitle(section.header, section),
|
|
2107
|
+
quote: this.truncateText(quoteText, quoteWordLimit)
|
|
2108
|
+
};
|
|
2109
|
+
if (quoteInfo.attribution) {
|
|
2110
|
+
data.attribution = quoteInfo.attribution;
|
|
2111
|
+
}
|
|
1218
2112
|
return {
|
|
1219
2113
|
index,
|
|
1220
|
-
type: "
|
|
2114
|
+
type: "quote",
|
|
2115
|
+
data,
|
|
2116
|
+
classes: ["quote-slide"]
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
createMetricsGridSlide(index, metrics) {
|
|
2120
|
+
const maxMetrics = this.config.defaults.metricsGrid.maxMetrics;
|
|
2121
|
+
const cleanedMetrics = metrics.slice(0, maxMetrics).map((m) => ({
|
|
2122
|
+
value: this.cleanText(m.value),
|
|
2123
|
+
label: this.cleanMetricLabel(m.label)
|
|
2124
|
+
}));
|
|
2125
|
+
return {
|
|
2126
|
+
index,
|
|
2127
|
+
type: "metrics-grid",
|
|
1221
2128
|
data: {
|
|
1222
|
-
title:
|
|
1223
|
-
|
|
1224
|
-
|
|
2129
|
+
title: this.config.defaults.metricsGrid.title,
|
|
2130
|
+
// FROM KB - not hardcoded 'Key Metrics'
|
|
2131
|
+
metrics: cleanedMetrics
|
|
1225
2132
|
},
|
|
1226
|
-
classes: ["slide
|
|
2133
|
+
classes: ["metrics-grid-slide"]
|
|
1227
2134
|
};
|
|
1228
2135
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
2136
|
+
createBulletSlide(index, section) {
|
|
2137
|
+
const maxBullets = this.config.rules.bulletsPerSlide.max;
|
|
2138
|
+
const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
|
|
2139
|
+
const cleanedBullets = section.bullets.slice(0, maxBullets).map((b) => this.truncateText(this.cleanText(b), wordsPerBullet)).filter((b) => b.length > 0);
|
|
2140
|
+
if (cleanedBullets.length === 0 && section.content) {
|
|
2141
|
+
return this.createSingleStatementSlide(index, section);
|
|
2142
|
+
}
|
|
1233
2143
|
return {
|
|
1234
2144
|
index,
|
|
1235
|
-
type: "
|
|
2145
|
+
type: "bullet-points",
|
|
1236
2146
|
data: {
|
|
1237
|
-
title:
|
|
1238
|
-
|
|
2147
|
+
title: this.createTitle(section.header, section),
|
|
2148
|
+
bullets: cleanedBullets
|
|
1239
2149
|
},
|
|
1240
|
-
classes: ["
|
|
2150
|
+
classes: ["bullet-points-slide"]
|
|
1241
2151
|
};
|
|
1242
2152
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
initializeTemplates() {
|
|
1247
|
-
const templates = /* @__PURE__ */ new Map();
|
|
1248
|
-
templates.set("title", {
|
|
1249
|
-
type: "title",
|
|
1250
|
-
requiredFields: ["title"],
|
|
1251
|
-
optionalFields: ["subtitle", "author", "date"],
|
|
1252
|
-
keynoteSuitable: true,
|
|
1253
|
-
businessSuitable: true,
|
|
1254
|
-
maxWords: 15
|
|
1255
|
-
});
|
|
1256
|
-
templates.set("big-idea", {
|
|
1257
|
-
type: "big-idea",
|
|
1258
|
-
requiredFields: ["title"],
|
|
1259
|
-
optionalFields: ["keyMessage"],
|
|
1260
|
-
keynoteSuitable: true,
|
|
1261
|
-
businessSuitable: false,
|
|
1262
|
-
maxWords: 10
|
|
1263
|
-
});
|
|
1264
|
-
templates.set("single-statement", {
|
|
2153
|
+
createSingleStatementSlide(index, section) {
|
|
2154
|
+
return {
|
|
2155
|
+
index,
|
|
1265
2156
|
type: "single-statement",
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
requiredFields: ["quote"],
|
|
1283
|
-
optionalFields: ["attribution", "source"],
|
|
1284
|
-
keynoteSuitable: true,
|
|
1285
|
-
businessSuitable: true,
|
|
1286
|
-
maxWords: 30
|
|
1287
|
-
});
|
|
1288
|
-
templates.set("bullet-points", {
|
|
1289
|
-
type: "bullet-points",
|
|
1290
|
-
requiredFields: ["title", "bullets"],
|
|
1291
|
-
optionalFields: ["body"],
|
|
1292
|
-
keynoteSuitable: false,
|
|
1293
|
-
businessSuitable: true,
|
|
1294
|
-
maxWords: 80
|
|
1295
|
-
});
|
|
1296
|
-
templates.set("two-column", {
|
|
2157
|
+
data: {
|
|
2158
|
+
title: this.createTitle(section.header, section),
|
|
2159
|
+
body: this.truncateText(
|
|
2160
|
+
section.content || section.bullets[0] || "",
|
|
2161
|
+
this.config.rules.wordsPerSlide.max
|
|
2162
|
+
// FROM KB
|
|
2163
|
+
)
|
|
2164
|
+
},
|
|
2165
|
+
classes: ["single-statement-slide"]
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
createCodeSlide(index, section) {
|
|
2169
|
+
const codeMatch = section.content.match(/```[\s\S]*?```|`[^`]+`/);
|
|
2170
|
+
const code = codeMatch ? codeMatch[0].replace(/```/g, "").trim() : section.content;
|
|
2171
|
+
return {
|
|
2172
|
+
index,
|
|
1297
2173
|
type: "two-column",
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
}
|
|
1312
|
-
|
|
2174
|
+
// Code slides use two-column layout
|
|
2175
|
+
data: {
|
|
2176
|
+
title: this.createTitle(section.header, section),
|
|
2177
|
+
leftColumn: {
|
|
2178
|
+
body: this.config.defaults.code.label
|
|
2179
|
+
// FROM KB - not hardcoded 'Code Example'
|
|
2180
|
+
},
|
|
2181
|
+
rightColumn: {
|
|
2182
|
+
body: code.slice(0, this.config.defaults.code.maxChars)
|
|
2183
|
+
// FROM KB - not hardcoded 500
|
|
2184
|
+
}
|
|
2185
|
+
},
|
|
2186
|
+
classes: ["code-slide"]
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
createCTASlide(index, analysis) {
|
|
2190
|
+
const ctaDefaults = this.config.defaults.cta;
|
|
2191
|
+
const ctaText = analysis.sparkline?.callToAdventure || analysis.scqa?.answer || ctaDefaults.fallback;
|
|
2192
|
+
return {
|
|
2193
|
+
index,
|
|
1313
2194
|
type: "cta",
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
2195
|
+
data: {
|
|
2196
|
+
title: ctaDefaults.title,
|
|
2197
|
+
// FROM KB - not hardcoded 'Next Steps'
|
|
2198
|
+
body: this.truncateText(ctaText, this.config.rules.wordsPerSlide.max),
|
|
2199
|
+
keyMessage: ctaDefaults.message
|
|
2200
|
+
// FROM KB - not hardcoded 'Ready to Begin?'
|
|
2201
|
+
},
|
|
2202
|
+
classes: ["cta-slide"]
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
// ===========================================================================
|
|
2206
|
+
// VALIDATION - Ensure all slides comply with KB rules AND FIX ISSUES
|
|
2207
|
+
// ===========================================================================
|
|
2208
|
+
validateAndFixSlides(slides) {
|
|
2209
|
+
const validatedSlides = [];
|
|
2210
|
+
for (const slide of slides) {
|
|
2211
|
+
const wordCount = this.countWords(slide);
|
|
2212
|
+
const bulletCount = slide.data.bullets?.length || 0;
|
|
2213
|
+
const validation = this.kb.validateSlideAgainstKB(
|
|
2214
|
+
{
|
|
2215
|
+
type: slide.type,
|
|
2216
|
+
wordCount,
|
|
2217
|
+
bulletCount,
|
|
2218
|
+
hasActionTitle: this.isActionTitle(slide.data.title),
|
|
2219
|
+
hasSource: !!slide.data.source
|
|
2220
|
+
},
|
|
2221
|
+
this.presentationType
|
|
2222
|
+
);
|
|
2223
|
+
if (validation.fixes.wordCount) {
|
|
2224
|
+
if (slide.data.body) {
|
|
2225
|
+
slide.data.body = this.truncateText(slide.data.body, validation.fixes.wordCount);
|
|
2226
|
+
}
|
|
2227
|
+
if (slide.data.bullets) {
|
|
2228
|
+
const wordsPerBullet = Math.floor(validation.fixes.wordCount / slide.data.bullets.length);
|
|
2229
|
+
slide.data.bullets = slide.data.bullets.map((b) => this.truncateText(b, wordsPerBullet));
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
if (validation.fixes.bulletCount && slide.data.bullets) {
|
|
2233
|
+
slide.data.bullets = slide.data.bullets.slice(0, validation.fixes.bulletCount);
|
|
2234
|
+
}
|
|
2235
|
+
if (validation.violations.length > 0) {
|
|
2236
|
+
logger.warn(`Slide ${slide.index} (${slide.type}): Fixed ${Object.keys(validation.fixes).length} issues, ${validation.violations.length} remaining`);
|
|
2237
|
+
}
|
|
2238
|
+
if (validation.warnings.length > 0) {
|
|
2239
|
+
slide.notes = (slide.notes || "") + "\nWarnings: " + validation.warnings.join(", ");
|
|
2240
|
+
}
|
|
2241
|
+
validatedSlides.push(slide);
|
|
2242
|
+
}
|
|
2243
|
+
return validatedSlides;
|
|
1329
2244
|
}
|
|
1330
|
-
// === Helper Methods ===
|
|
1331
2245
|
/**
|
|
1332
|
-
*
|
|
1333
|
-
* CRITICAL: Must strip all formatting to prevent garbage in slides
|
|
2246
|
+
* Check that required elements exist in the deck (from KB)
|
|
1334
2247
|
*/
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
2248
|
+
checkRequiredElements(slides) {
|
|
2249
|
+
const required = this.config.requiredElements;
|
|
2250
|
+
if (required.length === 0) return;
|
|
2251
|
+
const slideTypes = slides.map((s) => s.type);
|
|
2252
|
+
const missingElements = [];
|
|
2253
|
+
for (const element of required) {
|
|
2254
|
+
const lowerElement = element.toLowerCase();
|
|
2255
|
+
if (lowerElement.includes("hook") || lowerElement.includes("opening")) {
|
|
2256
|
+
const titleSlide = slides.find((s) => s.type === "title");
|
|
2257
|
+
if (!titleSlide || !titleSlide.data.subtitle) {
|
|
2258
|
+
missingElements.push(element);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (lowerElement.includes("call to action") || lowerElement.includes("cta")) {
|
|
2262
|
+
if (!slideTypes.includes("cta")) {
|
|
2263
|
+
missingElements.push(element);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (lowerElement.includes("star moment") || lowerElement.includes("memorable")) {
|
|
2267
|
+
const hasStarMoment = slideTypes.some(
|
|
2268
|
+
(t) => t === "big-number" || t === "quote" || t === "single-statement"
|
|
2269
|
+
);
|
|
2270
|
+
if (!hasStarMoment) {
|
|
2271
|
+
missingElements.push(element);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
if (missingElements.length > 0) {
|
|
2276
|
+
logger.warn(`Missing required elements: ${missingElements.join(", ")}`);
|
|
2277
|
+
}
|
|
1338
2278
|
}
|
|
1339
2279
|
/**
|
|
1340
|
-
*
|
|
1341
|
-
* CRITICAL: Never cut mid-number (99.5% should not become 99.)
|
|
2280
|
+
* Check for anti-patterns from KB
|
|
1342
2281
|
*/
|
|
1343
|
-
|
|
1344
|
-
const
|
|
1345
|
-
if (
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
break;
|
|
2282
|
+
checkAntiPatterns(slides) {
|
|
2283
|
+
const antiPatterns = this.config.antiPatterns;
|
|
2284
|
+
if (antiPatterns.length === 0) return;
|
|
2285
|
+
const violations = [];
|
|
2286
|
+
for (const pattern of antiPatterns) {
|
|
2287
|
+
const lowerPattern = pattern.toLowerCase();
|
|
2288
|
+
if (lowerPattern.includes("bullet") && this.config.mode === "keynote") {
|
|
2289
|
+
const bulletSlides = slides.filter(
|
|
2290
|
+
(s) => s.type === "bullet-points" || s.data.bullets && s.data.bullets.length > 3
|
|
2291
|
+
);
|
|
2292
|
+
if (bulletSlides.length > 0) {
|
|
2293
|
+
violations.push(`${pattern} (${bulletSlides.length} slides)`);
|
|
1356
2294
|
}
|
|
1357
2295
|
}
|
|
1358
|
-
if (
|
|
1359
|
-
|
|
2296
|
+
if (lowerPattern.includes("text") || lowerPattern.includes("overload")) {
|
|
2297
|
+
const overloadedSlides = slides.filter((s) => {
|
|
2298
|
+
const wordCount = this.countWords(s);
|
|
2299
|
+
return wordCount > this.config.glanceTest.wordLimit;
|
|
2300
|
+
});
|
|
2301
|
+
if (overloadedSlides.length > 0) {
|
|
2302
|
+
violations.push(`${pattern} (${overloadedSlides.length} slides exceed glance test)`);
|
|
2303
|
+
}
|
|
1360
2304
|
}
|
|
1361
2305
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
const afterCut = cleanedText.slice(lastSpace + 1, maxLength + 10);
|
|
1365
|
-
if (/^[\d.,%$]+/.test(afterCut)) {
|
|
1366
|
-
lastSpace = truncated.slice(0, lastSpace).lastIndexOf(" ");
|
|
2306
|
+
if (violations.length > 0) {
|
|
2307
|
+
logger.warn(`Anti-pattern violations: ${violations.join(", ")}`);
|
|
1367
2308
|
}
|
|
1368
|
-
if (lastSpace > maxLength * 0.5) {
|
|
1369
|
-
return truncated.slice(0, lastSpace) + "...";
|
|
1370
|
-
}
|
|
1371
|
-
return truncated + "...";
|
|
1372
2309
|
}
|
|
2310
|
+
// ===========================================================================
|
|
2311
|
+
// HELPER METHODS - All use KB configuration
|
|
2312
|
+
// ===========================================================================
|
|
1373
2313
|
/**
|
|
1374
|
-
*
|
|
2314
|
+
* Create a title for a slide - uses action titles for business mode per KB
|
|
1375
2315
|
*/
|
|
1376
|
-
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
2316
|
+
createTitle(header, section) {
|
|
2317
|
+
const cleanHeader = this.cleanText(header);
|
|
2318
|
+
if (this.usedTitles.has(cleanHeader.toLowerCase())) {
|
|
2319
|
+
return `${cleanHeader} (continued)`;
|
|
2320
|
+
}
|
|
2321
|
+
this.usedTitles.add(cleanHeader.toLowerCase());
|
|
2322
|
+
if (this.config.rules.actionTitlesRequired) {
|
|
2323
|
+
return this.createActionTitle(cleanHeader, section);
|
|
1381
2324
|
}
|
|
1382
|
-
|
|
1383
|
-
return words.join(" ");
|
|
2325
|
+
return cleanHeader;
|
|
1384
2326
|
}
|
|
1385
2327
|
/**
|
|
1386
|
-
*
|
|
2328
|
+
* Create an action title (Minto/McKinsey style)
|
|
2329
|
+
* Title should communicate the conclusion, not the topic
|
|
1387
2330
|
*/
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
const
|
|
1391
|
-
if (
|
|
1392
|
-
return
|
|
2331
|
+
createActionTitle(header, section) {
|
|
2332
|
+
const titleWordLimit = this.kb.getWordLimitForElement(this.presentationType, "title");
|
|
2333
|
+
const firstBullet = section.bullets[0];
|
|
2334
|
+
if (firstBullet && this.isInsightful(firstBullet)) {
|
|
2335
|
+
return this.truncateText(this.cleanText(firstBullet), titleWordLimit);
|
|
2336
|
+
}
|
|
2337
|
+
if (section.content && this.isInsightful(section.content)) {
|
|
2338
|
+
const sentences = section.content.split(/[.!?]/);
|
|
2339
|
+
const firstSentence = sentences[0];
|
|
2340
|
+
if (firstSentence) {
|
|
2341
|
+
return this.truncateText(this.cleanText(firstSentence), titleWordLimit);
|
|
2342
|
+
}
|
|
1393
2343
|
}
|
|
1394
|
-
|
|
1395
|
-
const sentences = cleanedText.split(/[.!?]+/).filter((s) => s.trim().length > 10);
|
|
1396
|
-
return sentences.slice(0, 5).map((s) => s.trim());
|
|
2344
|
+
return header;
|
|
1397
2345
|
}
|
|
1398
2346
|
/**
|
|
1399
|
-
*
|
|
2347
|
+
* Check if text contains an insight (not just a topic) - uses KB markers
|
|
1400
2348
|
*/
|
|
1401
|
-
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
2349
|
+
isInsightful(text) {
|
|
2350
|
+
const lowerText = text.toLowerCase();
|
|
2351
|
+
return this.config.insightMarkers.some(
|
|
2352
|
+
(marker) => (
|
|
2353
|
+
// FROM KB - not hardcoded array
|
|
2354
|
+
lowerText.includes(marker.toLowerCase())
|
|
2355
|
+
)
|
|
2356
|
+
);
|
|
1405
2357
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2358
|
+
/**
|
|
2359
|
+
* Check if a title is an action title
|
|
2360
|
+
*/
|
|
2361
|
+
isActionTitle(title) {
|
|
2362
|
+
if (!title) return false;
|
|
2363
|
+
return this.isInsightful(title);
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Count words in a slide
|
|
2367
|
+
*/
|
|
2368
|
+
countWords(slide) {
|
|
2369
|
+
let text = "";
|
|
2370
|
+
const data = slide.data;
|
|
2371
|
+
if (data.title) text += data.title + " ";
|
|
2372
|
+
if (data.subtitle) text += data.subtitle + " ";
|
|
2373
|
+
if (data.body) text += data.body + " ";
|
|
2374
|
+
if (data.keyMessage) text += data.keyMessage + " ";
|
|
2375
|
+
if (data.bullets) text += data.bullets.join(" ") + " ";
|
|
2376
|
+
if (data.quote) text += data.quote + " ";
|
|
2377
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Truncate text to word limit at sentence boundaries
|
|
2381
|
+
*/
|
|
2382
|
+
truncateText(text, maxWords) {
|
|
2383
|
+
const cleaned = this.cleanText(text);
|
|
2384
|
+
const words = cleaned.split(/\s+/);
|
|
2385
|
+
if (words.length <= maxWords) {
|
|
2386
|
+
return cleaned;
|
|
2387
|
+
}
|
|
2388
|
+
const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
|
|
2389
|
+
let result = "";
|
|
2390
|
+
for (const sentence of sentences) {
|
|
2391
|
+
const testResult = result + sentence;
|
|
2392
|
+
if (testResult.split(/\s+/).length <= maxWords) {
|
|
2393
|
+
result = testResult;
|
|
2394
|
+
} else {
|
|
2395
|
+
break;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (result.trim().length > 0) {
|
|
2399
|
+
return result.trim();
|
|
2400
|
+
}
|
|
2401
|
+
return words.slice(0, maxWords).join(" ") + "...";
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Clean text by removing content markers and normalizing
|
|
2405
|
+
*/
|
|
2406
|
+
cleanText(text) {
|
|
2407
|
+
if (!text) return "";
|
|
2408
|
+
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(/\*\*/g, "").replace(/\*/g, "").replace(/#{1,6}\s*/g, "").replace(/\s+/g, " ").trim();
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Clean metric labels (strip table syntax)
|
|
2412
|
+
*/
|
|
2413
|
+
cleanMetricLabel(label) {
|
|
2414
|
+
if (!label) return "";
|
|
2415
|
+
return label.replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\s{2,}/g, " ").replace(/^\s*\d+\.\s*/, "").trim();
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Normalize a key for deduplication
|
|
2419
|
+
*/
|
|
2420
|
+
normalizeKey(text) {
|
|
2421
|
+
return text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
function createSlideFactory(kb, type) {
|
|
2425
|
+
return new SlideFactory(kb, type);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/core/TemplateEngine.ts
|
|
2429
|
+
var import_handlebars = __toESM(require("handlebars"));
|
|
2430
|
+
var TemplateEngine = class {
|
|
2431
|
+
handlebars;
|
|
2432
|
+
templates;
|
|
2433
|
+
partials;
|
|
2434
|
+
constructor() {
|
|
2435
|
+
this.handlebars = import_handlebars.default.create();
|
|
2436
|
+
this.templates = /* @__PURE__ */ new Map();
|
|
2437
|
+
this.partials = /* @__PURE__ */ new Map();
|
|
2438
|
+
this.registerHelpers();
|
|
1419
2439
|
this.registerPartials();
|
|
1420
2440
|
this.compileTemplates();
|
|
1421
2441
|
}
|
|
@@ -1629,9 +2649,12 @@ var TemplateEngine = class {
|
|
|
1629
2649
|
this.templates.set("big-number", this.handlebars.compile(`
|
|
1630
2650
|
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1631
2651
|
<div class="slide-content big-number-content">
|
|
1632
|
-
|
|
1633
|
-
{{
|
|
1634
|
-
|
|
2652
|
+
{{#if title}}
|
|
2653
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
2654
|
+
{{/if}}
|
|
2655
|
+
<div class="number animate-zoomIn">{{keyMessage}}</div>
|
|
2656
|
+
{{#if body}}
|
|
2657
|
+
<p class="number-context animate-fadeIn delay-300">{{body}}</p>
|
|
1635
2658
|
{{/if}}
|
|
1636
2659
|
{{> source}}
|
|
1637
2660
|
</div>
|
|
@@ -2892,21 +3915,966 @@ var QAEngine = class {
|
|
|
2892
3915
|
});
|
|
2893
3916
|
return issues;
|
|
2894
3917
|
}
|
|
2895
|
-
// ===========================================================================
|
|
2896
|
-
// BROWSER MANAGEMENT
|
|
2897
|
-
// ===========================================================================
|
|
2898
|
-
async initBrowser() {
|
|
2899
|
-
if (!this.browser) {
|
|
2900
|
-
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
3918
|
+
// ===========================================================================
|
|
3919
|
+
// BROWSER MANAGEMENT
|
|
3920
|
+
// ===========================================================================
|
|
3921
|
+
async initBrowser() {
|
|
3922
|
+
if (!this.browser) {
|
|
3923
|
+
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
async closeBrowser() {
|
|
3927
|
+
if (this.browser) {
|
|
3928
|
+
await this.browser.close();
|
|
3929
|
+
this.browser = null;
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
};
|
|
3933
|
+
|
|
3934
|
+
// src/qa/SevenDimensionScorer.ts
|
|
3935
|
+
var DIMENSION_WEIGHTS = {
|
|
3936
|
+
layout: 0.15,
|
|
3937
|
+
contrast: 0.15,
|
|
3938
|
+
graphics: 0.1,
|
|
3939
|
+
content: 0.2,
|
|
3940
|
+
clarity: 0.15,
|
|
3941
|
+
effectiveness: 0.15,
|
|
3942
|
+
consistency: 0.1
|
|
3943
|
+
};
|
|
3944
|
+
var SevenDimensionScorer = class {
|
|
3945
|
+
kb;
|
|
3946
|
+
mode;
|
|
3947
|
+
presentationType;
|
|
3948
|
+
constructor(mode, presentationType) {
|
|
3949
|
+
this.mode = mode;
|
|
3950
|
+
this.presentationType = presentationType;
|
|
3951
|
+
}
|
|
3952
|
+
/**
|
|
3953
|
+
* Score a presentation across all 7 dimensions.
|
|
3954
|
+
*/
|
|
3955
|
+
async score(slides, html, threshold = 95) {
|
|
3956
|
+
this.kb = await getKnowledgeGateway();
|
|
3957
|
+
const layout = await this.scoreLayout(slides, html);
|
|
3958
|
+
const contrast = await this.scoreContrast(html);
|
|
3959
|
+
const graphics = await this.scoreGraphics(slides);
|
|
3960
|
+
const content = await this.scoreContent(slides);
|
|
3961
|
+
const clarity = await this.scoreClarity(slides);
|
|
3962
|
+
const effectiveness = await this.scoreEffectiveness(slides);
|
|
3963
|
+
const consistency = await this.scoreConsistency(slides, html);
|
|
3964
|
+
const overallScore = Math.round(
|
|
3965
|
+
layout.score * DIMENSION_WEIGHTS.layout + contrast.score * DIMENSION_WEIGHTS.contrast + graphics.score * DIMENSION_WEIGHTS.graphics + content.score * DIMENSION_WEIGHTS.content + clarity.score * DIMENSION_WEIGHTS.clarity + effectiveness.score * DIMENSION_WEIGHTS.effectiveness + consistency.score * DIMENSION_WEIGHTS.consistency
|
|
3966
|
+
);
|
|
3967
|
+
const issues = [
|
|
3968
|
+
...layout.issues,
|
|
3969
|
+
...contrast.issues,
|
|
3970
|
+
...graphics.issues,
|
|
3971
|
+
...content.issues,
|
|
3972
|
+
...clarity.issues,
|
|
3973
|
+
...effectiveness.issues,
|
|
3974
|
+
...consistency.issues
|
|
3975
|
+
];
|
|
3976
|
+
return {
|
|
3977
|
+
overallScore,
|
|
3978
|
+
dimensions: {
|
|
3979
|
+
layout,
|
|
3980
|
+
contrast,
|
|
3981
|
+
graphics,
|
|
3982
|
+
content,
|
|
3983
|
+
clarity,
|
|
3984
|
+
effectiveness,
|
|
3985
|
+
consistency
|
|
3986
|
+
},
|
|
3987
|
+
issues,
|
|
3988
|
+
passed: overallScore >= threshold,
|
|
3989
|
+
threshold
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
/**
|
|
3993
|
+
* Score layout dimension (whitespace, visual balance, structure)
|
|
3994
|
+
*/
|
|
3995
|
+
async scoreLayout(slides, html) {
|
|
3996
|
+
const issues = [];
|
|
3997
|
+
let totalScore = 0;
|
|
3998
|
+
let checks = 0;
|
|
3999
|
+
const minWhitespace = this.mode === "keynote" ? 0.45 : 0.35;
|
|
4000
|
+
const maxWhitespace = 0.6;
|
|
4001
|
+
const slideSections = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
|
|
4002
|
+
for (let i = 0; i < slideSections.length; i++) {
|
|
4003
|
+
const section = slideSections[i];
|
|
4004
|
+
if (!section) continue;
|
|
4005
|
+
const textContent = section.replace(/<[^>]+>/g, "").trim();
|
|
4006
|
+
const totalArea = 1920 * 1080;
|
|
4007
|
+
const estimatedTextArea = textContent.length * 100;
|
|
4008
|
+
const whitespaceRatio = 1 - Math.min(estimatedTextArea / totalArea, 1);
|
|
4009
|
+
if (whitespaceRatio < minWhitespace) {
|
|
4010
|
+
issues.push({
|
|
4011
|
+
slideIndex: i,
|
|
4012
|
+
dimension: "layout",
|
|
4013
|
+
severity: "error",
|
|
4014
|
+
message: `Slide ${i + 1}: Insufficient whitespace (${Math.round(whitespaceRatio * 100)}% < ${Math.round(minWhitespace * 100)}%)`,
|
|
4015
|
+
currentValue: whitespaceRatio,
|
|
4016
|
+
expectedValue: minWhitespace,
|
|
4017
|
+
autoFixable: true,
|
|
4018
|
+
fixSuggestion: "Reduce content or increase margins"
|
|
4019
|
+
});
|
|
4020
|
+
totalScore += 50;
|
|
4021
|
+
} else if (whitespaceRatio > maxWhitespace) {
|
|
4022
|
+
issues.push({
|
|
4023
|
+
slideIndex: i,
|
|
4024
|
+
dimension: "layout",
|
|
4025
|
+
severity: "warning",
|
|
4026
|
+
message: `Slide ${i + 1}: Too much whitespace (${Math.round(whitespaceRatio * 100)}% > ${Math.round(maxWhitespace * 100)}%)`,
|
|
4027
|
+
currentValue: whitespaceRatio,
|
|
4028
|
+
expectedValue: maxWhitespace,
|
|
4029
|
+
autoFixable: false,
|
|
4030
|
+
fixSuggestion: "Add more content or reduce margins"
|
|
4031
|
+
});
|
|
4032
|
+
totalScore += 80;
|
|
4033
|
+
} else {
|
|
4034
|
+
totalScore += 100;
|
|
4035
|
+
}
|
|
4036
|
+
checks++;
|
|
4037
|
+
}
|
|
4038
|
+
for (let i = 0; i < slides.length; i++) {
|
|
4039
|
+
const slide = slides[i];
|
|
4040
|
+
if (!slide) continue;
|
|
4041
|
+
if (!["thank-you", "section-divider"].includes(slide.type)) {
|
|
4042
|
+
if (!slide.data.title || slide.data.title.trim().length === 0) {
|
|
4043
|
+
issues.push({
|
|
4044
|
+
slideIndex: i,
|
|
4045
|
+
dimension: "layout",
|
|
4046
|
+
severity: "warning",
|
|
4047
|
+
message: `Slide ${i + 1}: Missing title`,
|
|
4048
|
+
autoFixable: false,
|
|
4049
|
+
fixSuggestion: "Add a clear slide title"
|
|
4050
|
+
});
|
|
4051
|
+
totalScore += 70;
|
|
4052
|
+
} else {
|
|
4053
|
+
totalScore += 100;
|
|
4054
|
+
}
|
|
4055
|
+
checks++;
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
const score = checks > 0 ? Math.round(totalScore / checks) : 100;
|
|
4059
|
+
return {
|
|
4060
|
+
name: "Layout",
|
|
4061
|
+
score,
|
|
4062
|
+
weight: DIMENSION_WEIGHTS.layout,
|
|
4063
|
+
issues,
|
|
4064
|
+
details: {
|
|
4065
|
+
slidesAnalyzed: slides.length,
|
|
4066
|
+
whitespaceTarget: `${Math.round(minWhitespace * 100)}-${Math.round(maxWhitespace * 100)}%`
|
|
4067
|
+
}
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
4070
|
+
/**
|
|
4071
|
+
* Score contrast dimension (WCAG compliance, readability)
|
|
4072
|
+
*/
|
|
4073
|
+
async scoreContrast(html) {
|
|
4074
|
+
const issues = [];
|
|
4075
|
+
let score = 100;
|
|
4076
|
+
const minContrastRatio = 4.5;
|
|
4077
|
+
const colorMatches = html.match(/color:\s*([^;]+);/gi) || [];
|
|
4078
|
+
const bgColorMatches = html.match(/background(-color)?:\s*([^;]+);/gi) || [];
|
|
4079
|
+
const hasGoodContrast = html.includes("color: #fff") || html.includes("color: white") || html.includes("color: #18181B") || html.includes("color: #F5F5F4");
|
|
4080
|
+
const hasDarkBackground = html.includes("background-color: #18181B") || html.includes("background: #18181B") || html.includes("background-color: rgb(24, 24, 27)");
|
|
4081
|
+
if (html.includes("color: gray") || html.includes("color: #999") || html.includes("color: #888")) {
|
|
4082
|
+
issues.push({
|
|
4083
|
+
slideIndex: -1,
|
|
4084
|
+
dimension: "contrast",
|
|
4085
|
+
severity: "error",
|
|
4086
|
+
message: "Low contrast text color detected (gray text)",
|
|
4087
|
+
currentValue: "gray",
|
|
4088
|
+
expectedValue: "High contrast color",
|
|
4089
|
+
autoFixable: true,
|
|
4090
|
+
fixSuggestion: "Use white (#fff) or dark (#18181B) text depending on background"
|
|
4091
|
+
});
|
|
4092
|
+
score -= 20;
|
|
4093
|
+
}
|
|
4094
|
+
if (!hasGoodContrast && !hasDarkBackground) {
|
|
4095
|
+
issues.push({
|
|
4096
|
+
slideIndex: -1,
|
|
4097
|
+
dimension: "contrast",
|
|
4098
|
+
severity: "warning",
|
|
4099
|
+
message: "Could not verify WCAG-compliant contrast ratios",
|
|
4100
|
+
autoFixable: false,
|
|
4101
|
+
fixSuggestion: "Ensure text has 4.5:1 contrast ratio against background"
|
|
4102
|
+
});
|
|
4103
|
+
score -= 10;
|
|
4104
|
+
}
|
|
4105
|
+
const hasSmallFont = html.match(/font-size:\s*(1[0-3]|[0-9])px/i) !== null;
|
|
4106
|
+
if (hasSmallFont) {
|
|
4107
|
+
issues.push({
|
|
4108
|
+
slideIndex: -1,
|
|
4109
|
+
dimension: "contrast",
|
|
4110
|
+
severity: "error",
|
|
4111
|
+
message: "Font size too small for presentation (< 14px)",
|
|
4112
|
+
currentValue: "Small font",
|
|
4113
|
+
expectedValue: "18px minimum for body text",
|
|
4114
|
+
autoFixable: true,
|
|
4115
|
+
fixSuggestion: "Increase font size to minimum 18px"
|
|
4116
|
+
});
|
|
4117
|
+
score -= 15;
|
|
4118
|
+
}
|
|
4119
|
+
return {
|
|
4120
|
+
name: "Contrast",
|
|
4121
|
+
score: Math.max(0, score),
|
|
4122
|
+
weight: DIMENSION_WEIGHTS.contrast,
|
|
4123
|
+
issues,
|
|
4124
|
+
details: {
|
|
4125
|
+
wcagLevel: "AA",
|
|
4126
|
+
minContrastRatio,
|
|
4127
|
+
colorDefinitions: colorMatches.length,
|
|
4128
|
+
backgroundDefinitions: bgColorMatches.length
|
|
4129
|
+
}
|
|
4130
|
+
};
|
|
4131
|
+
}
|
|
4132
|
+
/**
|
|
4133
|
+
* Score graphics dimension (images, placement, relevance)
|
|
4134
|
+
*/
|
|
4135
|
+
async scoreGraphics(slides) {
|
|
4136
|
+
const issues = [];
|
|
4137
|
+
let score = 100;
|
|
4138
|
+
let slidesWithImages = 0;
|
|
4139
|
+
let totalImageSlides = 0;
|
|
4140
|
+
for (let i = 0; i < slides.length; i++) {
|
|
4141
|
+
const slide = slides[i];
|
|
4142
|
+
if (!slide) continue;
|
|
4143
|
+
const shouldHaveImage = ["hero", "image", "feature"].includes(slide.type);
|
|
4144
|
+
if (shouldHaveImage) {
|
|
4145
|
+
totalImageSlides++;
|
|
4146
|
+
if (slide.data.image || slide.data.backgroundImage) {
|
|
4147
|
+
slidesWithImages++;
|
|
4148
|
+
} else {
|
|
4149
|
+
issues.push({
|
|
4150
|
+
slideIndex: i,
|
|
4151
|
+
dimension: "graphics",
|
|
4152
|
+
severity: "warning",
|
|
4153
|
+
message: `Slide ${i + 1} (${slide.type}): Missing expected image`,
|
|
4154
|
+
autoFixable: false,
|
|
4155
|
+
fixSuggestion: "Add a relevant image or change slide type"
|
|
4156
|
+
});
|
|
4157
|
+
score -= 5;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
return {
|
|
4162
|
+
name: "Graphics",
|
|
4163
|
+
score: Math.max(0, score),
|
|
4164
|
+
weight: DIMENSION_WEIGHTS.graphics,
|
|
4165
|
+
issues,
|
|
4166
|
+
details: {
|
|
4167
|
+
slidesWithImages,
|
|
4168
|
+
expectedImageSlides: totalImageSlides,
|
|
4169
|
+
imageRatio: totalImageSlides > 0 ? slidesWithImages / totalImageSlides : 1
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
/**
|
|
4174
|
+
* Score content dimension (word limits, bullet counts, structure)
|
|
4175
|
+
* This is critical - must use KB rules exactly.
|
|
4176
|
+
*/
|
|
4177
|
+
async scoreContent(slides) {
|
|
4178
|
+
const issues = [];
|
|
4179
|
+
let totalScore = 0;
|
|
4180
|
+
let checks = 0;
|
|
4181
|
+
const wordLimits = this.kb.getWordLimits(this.mode);
|
|
4182
|
+
const bulletLimits = this.kb.getBulletLimits(this.mode);
|
|
4183
|
+
const defaultMaxWords = this.mode === "keynote" ? 25 : 80;
|
|
4184
|
+
const defaultMinWords = this.mode === "keynote" ? 3 : 15;
|
|
4185
|
+
const defaultMaxBullets = 5;
|
|
4186
|
+
for (let i = 0; i < slides.length; i++) {
|
|
4187
|
+
const slide = slides[i];
|
|
4188
|
+
if (!slide) continue;
|
|
4189
|
+
const wordCount = this.countWords(slide);
|
|
4190
|
+
const slideType = slide.type;
|
|
4191
|
+
const maxWords = wordLimits[slideType] ?? defaultMaxWords;
|
|
4192
|
+
const minWords = defaultMinWords;
|
|
4193
|
+
if (wordCount > maxWords) {
|
|
4194
|
+
const severity = wordCount > maxWords * 1.5 ? "error" : "warning";
|
|
4195
|
+
issues.push({
|
|
4196
|
+
slideIndex: i,
|
|
4197
|
+
dimension: "content",
|
|
4198
|
+
severity,
|
|
4199
|
+
message: `Slide ${i + 1}: Word count ${wordCount} exceeds limit of ${maxWords} for ${this.mode} mode`,
|
|
4200
|
+
currentValue: wordCount,
|
|
4201
|
+
expectedValue: maxWords,
|
|
4202
|
+
autoFixable: true,
|
|
4203
|
+
fixSuggestion: "Condense text to key points only"
|
|
4204
|
+
});
|
|
4205
|
+
totalScore += severity === "error" ? 40 : 70;
|
|
4206
|
+
} else if (wordCount < minWords && !["title", "section-divider", "thank-you"].includes(slide.type)) {
|
|
4207
|
+
issues.push({
|
|
4208
|
+
slideIndex: i,
|
|
4209
|
+
dimension: "content",
|
|
4210
|
+
severity: "warning",
|
|
4211
|
+
message: `Slide ${i + 1}: Word count ${wordCount} may be too sparse (min: ${minWords})`,
|
|
4212
|
+
currentValue: wordCount,
|
|
4213
|
+
expectedValue: minWords,
|
|
4214
|
+
autoFixable: false,
|
|
4215
|
+
fixSuggestion: "Add supporting content"
|
|
4216
|
+
});
|
|
4217
|
+
totalScore += 80;
|
|
4218
|
+
} else {
|
|
4219
|
+
totalScore += 100;
|
|
4220
|
+
}
|
|
4221
|
+
checks++;
|
|
4222
|
+
if (slide.data.bullets && Array.isArray(slide.data.bullets)) {
|
|
4223
|
+
const bulletCount = slide.data.bullets.length;
|
|
4224
|
+
const maxBullets = bulletLimits[slideType] ?? defaultMaxBullets;
|
|
4225
|
+
if (bulletCount > maxBullets) {
|
|
4226
|
+
issues.push({
|
|
4227
|
+
slideIndex: i,
|
|
4228
|
+
dimension: "content",
|
|
4229
|
+
severity: "error",
|
|
4230
|
+
message: `Slide ${i + 1}: ${bulletCount} bullets exceeds limit of ${maxBullets}`,
|
|
4231
|
+
currentValue: bulletCount,
|
|
4232
|
+
expectedValue: maxBullets,
|
|
4233
|
+
autoFixable: true,
|
|
4234
|
+
fixSuggestion: "Reduce to top 3-5 key points"
|
|
4235
|
+
});
|
|
4236
|
+
totalScore += 50;
|
|
4237
|
+
} else {
|
|
4238
|
+
totalScore += 100;
|
|
4239
|
+
}
|
|
4240
|
+
checks++;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
const score = checks > 0 ? Math.round(totalScore / checks) : 100;
|
|
4244
|
+
return {
|
|
4245
|
+
name: "Content",
|
|
4246
|
+
score,
|
|
4247
|
+
weight: DIMENSION_WEIGHTS.content,
|
|
4248
|
+
issues,
|
|
4249
|
+
details: {
|
|
4250
|
+
mode: this.mode,
|
|
4251
|
+
wordLimits,
|
|
4252
|
+
bulletLimits,
|
|
4253
|
+
totalSlides: slides.length
|
|
4254
|
+
}
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Score clarity dimension (message focus, information density)
|
|
4259
|
+
*/
|
|
4260
|
+
async scoreClarity(slides) {
|
|
4261
|
+
const issues = [];
|
|
4262
|
+
let totalScore = 0;
|
|
4263
|
+
let checks = 0;
|
|
4264
|
+
for (let i = 0; i < slides.length; i++) {
|
|
4265
|
+
const slide = slides[i];
|
|
4266
|
+
if (!slide) continue;
|
|
4267
|
+
if (slide.data.keyMessage) {
|
|
4268
|
+
const keyMessageWords = slide.data.keyMessage.split(/\s+/).length;
|
|
4269
|
+
const maxKeyMessageWords = this.mode === "keynote" ? 15 : 25;
|
|
4270
|
+
if (keyMessageWords > maxKeyMessageWords) {
|
|
4271
|
+
issues.push({
|
|
4272
|
+
slideIndex: i,
|
|
4273
|
+
dimension: "clarity",
|
|
4274
|
+
severity: "warning",
|
|
4275
|
+
message: `Slide ${i + 1}: Key message too long (${keyMessageWords} words > ${maxKeyMessageWords})`,
|
|
4276
|
+
currentValue: keyMessageWords,
|
|
4277
|
+
expectedValue: maxKeyMessageWords,
|
|
4278
|
+
autoFixable: true,
|
|
4279
|
+
fixSuggestion: "Shorten key message to one impactful sentence"
|
|
4280
|
+
});
|
|
4281
|
+
totalScore += 70;
|
|
4282
|
+
} else {
|
|
4283
|
+
totalScore += 100;
|
|
4284
|
+
}
|
|
4285
|
+
checks++;
|
|
4286
|
+
}
|
|
4287
|
+
if (slide.data.title) {
|
|
4288
|
+
const title = slide.data.title;
|
|
4289
|
+
const titleWords = title.split(/\s+/).length;
|
|
4290
|
+
if (titleWords > 10) {
|
|
4291
|
+
issues.push({
|
|
4292
|
+
slideIndex: i,
|
|
4293
|
+
dimension: "clarity",
|
|
4294
|
+
severity: "warning",
|
|
4295
|
+
message: `Slide ${i + 1}: Title too long (${titleWords} words)`,
|
|
4296
|
+
currentValue: titleWords,
|
|
4297
|
+
expectedValue: "2-8 words",
|
|
4298
|
+
autoFixable: true,
|
|
4299
|
+
fixSuggestion: "Use action-oriented, concise title"
|
|
4300
|
+
});
|
|
4301
|
+
totalScore += 75;
|
|
4302
|
+
} else {
|
|
4303
|
+
totalScore += 100;
|
|
4304
|
+
}
|
|
4305
|
+
checks++;
|
|
4306
|
+
}
|
|
4307
|
+
const elementCount = (slide.data.title ? 1 : 0) + (slide.data.subtitle ? 1 : 0) + (slide.data.body ? 1 : 0) + (slide.data.bullets?.length ?? 0) + (slide.data.keyMessage ? 1 : 0) + (slide.data.image ? 1 : 0);
|
|
4308
|
+
const maxElements = this.mode === "keynote" ? 4 : 6;
|
|
4309
|
+
if (elementCount > maxElements) {
|
|
4310
|
+
issues.push({
|
|
4311
|
+
slideIndex: i,
|
|
4312
|
+
dimension: "clarity",
|
|
4313
|
+
severity: "warning",
|
|
4314
|
+
message: `Slide ${i + 1}: Too many elements (${elementCount} > ${maxElements})`,
|
|
4315
|
+
currentValue: elementCount,
|
|
4316
|
+
expectedValue: maxElements,
|
|
4317
|
+
autoFixable: true,
|
|
4318
|
+
fixSuggestion: "Split into multiple slides for clarity"
|
|
4319
|
+
});
|
|
4320
|
+
totalScore += 70;
|
|
4321
|
+
} else {
|
|
4322
|
+
totalScore += 100;
|
|
4323
|
+
}
|
|
4324
|
+
checks++;
|
|
4325
|
+
}
|
|
4326
|
+
const score = checks > 0 ? Math.round(totalScore / checks) : 100;
|
|
4327
|
+
return {
|
|
4328
|
+
name: "Clarity",
|
|
4329
|
+
score,
|
|
4330
|
+
weight: DIMENSION_WEIGHTS.clarity,
|
|
4331
|
+
issues,
|
|
4332
|
+
details: {
|
|
4333
|
+
slidesAnalyzed: slides.length,
|
|
4334
|
+
mode: this.mode
|
|
4335
|
+
}
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
/**
|
|
4339
|
+
* Score effectiveness dimension (expert methodology compliance)
|
|
4340
|
+
*/
|
|
4341
|
+
async scoreEffectiveness(slides) {
|
|
4342
|
+
const issues = [];
|
|
4343
|
+
let score = 100;
|
|
4344
|
+
if (slides.length >= 3) {
|
|
4345
|
+
const firstSlide = slides[0];
|
|
4346
|
+
const lastSlide = slides[slides.length - 1];
|
|
4347
|
+
if (firstSlide && !["title", "hero"].includes(firstSlide.type)) {
|
|
4348
|
+
issues.push({
|
|
4349
|
+
slideIndex: 0,
|
|
4350
|
+
dimension: "effectiveness",
|
|
4351
|
+
severity: "warning",
|
|
4352
|
+
message: "Presentation should start with a title or hero slide",
|
|
4353
|
+
currentValue: firstSlide.type,
|
|
4354
|
+
expectedValue: "title or hero",
|
|
4355
|
+
autoFixable: false,
|
|
4356
|
+
fixSuggestion: "Add a compelling opening slide"
|
|
4357
|
+
});
|
|
4358
|
+
score -= 10;
|
|
4359
|
+
}
|
|
4360
|
+
if (lastSlide && !["thank-you", "cta", "closing"].includes(lastSlide.type)) {
|
|
4361
|
+
issues.push({
|
|
4362
|
+
slideIndex: slides.length - 1,
|
|
4363
|
+
dimension: "effectiveness",
|
|
4364
|
+
severity: "warning",
|
|
4365
|
+
message: "Presentation should end with a closing or CTA slide",
|
|
4366
|
+
currentValue: lastSlide.type,
|
|
4367
|
+
expectedValue: "thank-you, cta, or closing",
|
|
4368
|
+
autoFixable: false,
|
|
4369
|
+
fixSuggestion: "Add a clear call-to-action or closing"
|
|
4370
|
+
});
|
|
4371
|
+
score -= 10;
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
const keyMessages = slides.filter((s) => s.data.keyMessage);
|
|
4375
|
+
if (keyMessages.length > 0 && keyMessages.length !== 3 && keyMessages.length > 4) {
|
|
4376
|
+
issues.push({
|
|
4377
|
+
slideIndex: -1,
|
|
4378
|
+
dimension: "effectiveness",
|
|
4379
|
+
severity: "info",
|
|
4380
|
+
message: `Consider using Rule of Three: ${keyMessages.length} key messages found`,
|
|
4381
|
+
currentValue: keyMessages.length,
|
|
4382
|
+
expectedValue: 3,
|
|
4383
|
+
autoFixable: false,
|
|
4384
|
+
fixSuggestion: "Group messages into 3 main themes"
|
|
4385
|
+
});
|
|
4386
|
+
score -= 5;
|
|
4387
|
+
}
|
|
4388
|
+
const hasScqaElements = slides.some(
|
|
4389
|
+
(s) => s.data.scqaType || s.data.title && s.data.title.toLowerCase().includes("challenge") || s.data.title && s.data.title.toLowerCase().includes("solution") || s.data.title && s.data.title.toLowerCase().includes("recommendation")
|
|
4390
|
+
);
|
|
4391
|
+
if (!hasScqaElements && this.presentationType === "consulting_deck") {
|
|
4392
|
+
issues.push({
|
|
4393
|
+
slideIndex: -1,
|
|
4394
|
+
dimension: "effectiveness",
|
|
4395
|
+
severity: "warning",
|
|
4396
|
+
message: "Consulting deck should follow SCQA structure (Situation, Complication, Question, Answer)",
|
|
4397
|
+
autoFixable: false,
|
|
4398
|
+
fixSuggestion: "Organize content using Barbara Minto Pyramid Principle"
|
|
4399
|
+
});
|
|
4400
|
+
score -= 10;
|
|
4401
|
+
}
|
|
4402
|
+
const firstSlideType = slides[0]?.type;
|
|
4403
|
+
const lastSlideType = slides[slides.length - 1]?.type;
|
|
4404
|
+
return {
|
|
4405
|
+
name: "Effectiveness",
|
|
4406
|
+
score: Math.max(0, score),
|
|
4407
|
+
weight: DIMENSION_WEIGHTS.effectiveness,
|
|
4408
|
+
issues,
|
|
4409
|
+
details: {
|
|
4410
|
+
presentationType: this.presentationType,
|
|
4411
|
+
slideCount: slides.length,
|
|
4412
|
+
hasOpeningSlide: firstSlideType ? ["title", "hero"].includes(firstSlideType) : false,
|
|
4413
|
+
hasClosingSlide: lastSlideType ? ["thank-you", "cta", "closing"].includes(lastSlideType) : false
|
|
4414
|
+
}
|
|
4415
|
+
};
|
|
4416
|
+
}
|
|
4417
|
+
/**
|
|
4418
|
+
* Score consistency dimension (style uniformity, design coherence)
|
|
4419
|
+
*/
|
|
4420
|
+
async scoreConsistency(slides, html) {
|
|
4421
|
+
const issues = [];
|
|
4422
|
+
let score = 100;
|
|
4423
|
+
const hasCssVariables = html.includes("var(--") || html.includes(":root");
|
|
4424
|
+
if (!hasCssVariables) {
|
|
4425
|
+
issues.push({
|
|
4426
|
+
slideIndex: -1,
|
|
4427
|
+
dimension: "consistency",
|
|
4428
|
+
severity: "warning",
|
|
4429
|
+
message: "Presentation lacks CSS variables for consistent styling",
|
|
4430
|
+
autoFixable: true,
|
|
4431
|
+
fixSuggestion: "Use CSS variables for colors, fonts, and spacing"
|
|
4432
|
+
});
|
|
4433
|
+
score -= 10;
|
|
4434
|
+
}
|
|
4435
|
+
const titlePatterns = /* @__PURE__ */ new Set();
|
|
4436
|
+
for (const slide of slides) {
|
|
4437
|
+
if (slide.data.title) {
|
|
4438
|
+
const isUpperCase = slide.data.title === slide.data.title.toUpperCase();
|
|
4439
|
+
const words = slide.data.title.split(" ").filter((w) => w.length > 0);
|
|
4440
|
+
const isTitleCase = words.length > 0 && words.every(
|
|
4441
|
+
(w) => w.length > 0 && w[0] === w[0]?.toUpperCase()
|
|
4442
|
+
);
|
|
4443
|
+
titlePatterns.add(isUpperCase ? "UPPER" : isTitleCase ? "Title" : "sentence");
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
if (titlePatterns.size > 1) {
|
|
4447
|
+
issues.push({
|
|
4448
|
+
slideIndex: -1,
|
|
4449
|
+
dimension: "consistency",
|
|
4450
|
+
severity: "warning",
|
|
4451
|
+
message: `Inconsistent title casing: ${Array.from(titlePatterns).join(", ")}`,
|
|
4452
|
+
autoFixable: true,
|
|
4453
|
+
fixSuggestion: "Use consistent title case throughout"
|
|
4454
|
+
});
|
|
4455
|
+
score -= 10;
|
|
4456
|
+
}
|
|
4457
|
+
const fontMatches = html.match(/font-family:\s*([^;]+);/gi) || [];
|
|
4458
|
+
const uniqueFonts = new Set(fontMatches.map((f) => f.toLowerCase()));
|
|
4459
|
+
if (uniqueFonts.size > 3) {
|
|
4460
|
+
issues.push({
|
|
4461
|
+
slideIndex: -1,
|
|
4462
|
+
dimension: "consistency",
|
|
4463
|
+
severity: "warning",
|
|
4464
|
+
message: `Too many font families (${uniqueFonts.size} > 3)`,
|
|
4465
|
+
autoFixable: true,
|
|
4466
|
+
fixSuggestion: "Use 2-3 complementary fonts max"
|
|
4467
|
+
});
|
|
4468
|
+
score -= 10;
|
|
4469
|
+
}
|
|
4470
|
+
return {
|
|
4471
|
+
name: "Consistency",
|
|
4472
|
+
score: Math.max(0, score),
|
|
4473
|
+
weight: DIMENSION_WEIGHTS.consistency,
|
|
4474
|
+
issues,
|
|
4475
|
+
details: {
|
|
4476
|
+
hasCssVariables,
|
|
4477
|
+
titlePatterns: Array.from(titlePatterns),
|
|
4478
|
+
fontFamilyCount: uniqueFonts.size
|
|
4479
|
+
}
|
|
4480
|
+
};
|
|
4481
|
+
}
|
|
4482
|
+
/**
|
|
4483
|
+
* Count words in a slide.
|
|
4484
|
+
*/
|
|
4485
|
+
countWords(slide) {
|
|
4486
|
+
let text = "";
|
|
4487
|
+
if (slide.data.title) text += slide.data.title + " ";
|
|
4488
|
+
if (slide.data.subtitle) text += slide.data.subtitle + " ";
|
|
4489
|
+
if (slide.data.body) text += slide.data.body + " ";
|
|
4490
|
+
if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
|
|
4491
|
+
if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
|
|
4492
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
4493
|
+
}
|
|
4494
|
+
/**
|
|
4495
|
+
* Get a formatted report of the scoring results.
|
|
4496
|
+
*/
|
|
4497
|
+
formatReport(result) {
|
|
4498
|
+
const lines = [];
|
|
4499
|
+
lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
4500
|
+
lines.push("\u2551 7-DIMENSION QUALITY ASSESSMENT \u2551");
|
|
4501
|
+
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
4502
|
+
lines.push("");
|
|
4503
|
+
lines.push(`Overall Score: ${result.overallScore}/100 ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
4504
|
+
lines.push(`Threshold: ${result.threshold}/100`);
|
|
4505
|
+
lines.push("");
|
|
4506
|
+
lines.push("Dimension Breakdown:");
|
|
4507
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
4508
|
+
const dims = result.dimensions;
|
|
4509
|
+
const formatDim = (name, d) => {
|
|
4510
|
+
const bar = "\u2588".repeat(Math.floor(d.score / 10)) + "\u2591".repeat(10 - Math.floor(d.score / 10));
|
|
4511
|
+
const status = d.score >= 95 ? "\u2705" : d.score >= 80 ? "\u26A0\uFE0F" : "\u274C";
|
|
4512
|
+
return `${status} ${name.padEnd(14)} ${bar} ${d.score.toString().padStart(3)}/100 (${(d.weight * 100).toFixed(0)}%)`;
|
|
4513
|
+
};
|
|
4514
|
+
lines.push(formatDim("Layout", dims.layout));
|
|
4515
|
+
lines.push(formatDim("Contrast", dims.contrast));
|
|
4516
|
+
lines.push(formatDim("Graphics", dims.graphics));
|
|
4517
|
+
lines.push(formatDim("Content", dims.content));
|
|
4518
|
+
lines.push(formatDim("Clarity", dims.clarity));
|
|
4519
|
+
lines.push(formatDim("Effectiveness", dims.effectiveness));
|
|
4520
|
+
lines.push(formatDim("Consistency", dims.consistency));
|
|
4521
|
+
lines.push("");
|
|
4522
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
4523
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
4524
|
+
if (errors.length > 0) {
|
|
4525
|
+
lines.push("\u274C Errors:");
|
|
4526
|
+
errors.forEach((e) => lines.push(` \u2022 ${e.message}`));
|
|
4527
|
+
lines.push("");
|
|
4528
|
+
}
|
|
4529
|
+
if (warnings.length > 0) {
|
|
4530
|
+
lines.push("\u26A0\uFE0F Warnings:");
|
|
4531
|
+
warnings.slice(0, 10).forEach((w) => lines.push(` \u2022 ${w.message}`));
|
|
4532
|
+
if (warnings.length > 10) {
|
|
4533
|
+
lines.push(` ... and ${warnings.length - 10} more warnings`);
|
|
4534
|
+
}
|
|
4535
|
+
lines.push("");
|
|
4536
|
+
}
|
|
4537
|
+
const autoFixable = result.issues.filter((i) => i.autoFixable);
|
|
4538
|
+
if (autoFixable.length > 0) {
|
|
4539
|
+
lines.push(`\u{1F527} ${autoFixable.length} issues can be auto-fixed`);
|
|
4540
|
+
}
|
|
4541
|
+
return lines.join("\n");
|
|
4542
|
+
}
|
|
4543
|
+
};
|
|
4544
|
+
|
|
4545
|
+
// src/qa/AutoFixEngine.ts
|
|
4546
|
+
var AutoFixEngine = class {
|
|
4547
|
+
kb;
|
|
4548
|
+
mode;
|
|
4549
|
+
presentationType;
|
|
4550
|
+
constructor(mode, presentationType) {
|
|
4551
|
+
this.mode = mode;
|
|
4552
|
+
this.presentationType = presentationType;
|
|
4553
|
+
}
|
|
4554
|
+
/**
|
|
4555
|
+
* Apply automatic fixes to slides based on scoring results.
|
|
4556
|
+
*/
|
|
4557
|
+
async fix(slides, scoringResult) {
|
|
4558
|
+
this.kb = await getKnowledgeGateway();
|
|
4559
|
+
const slidesFixed = JSON.parse(JSON.stringify(slides));
|
|
4560
|
+
const fixesApplied = [];
|
|
4561
|
+
const fixesSkipped = [];
|
|
4562
|
+
const autoFixableIssues = scoringResult.issues.filter((i) => i.autoFixable);
|
|
4563
|
+
for (const issue of autoFixableIssues) {
|
|
4564
|
+
const result = await this.applyFix(slidesFixed, issue);
|
|
4565
|
+
if (result.applied) {
|
|
4566
|
+
fixesApplied.push(result);
|
|
4567
|
+
} else {
|
|
4568
|
+
fixesSkipped.push(result);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
const summary = this.generateSummary(fixesApplied, fixesSkipped);
|
|
4572
|
+
return {
|
|
4573
|
+
slidesFixed,
|
|
4574
|
+
fixesApplied,
|
|
4575
|
+
fixesSkipped,
|
|
4576
|
+
summary
|
|
4577
|
+
};
|
|
4578
|
+
}
|
|
4579
|
+
/**
|
|
4580
|
+
* Apply a single fix based on the issue.
|
|
4581
|
+
*/
|
|
4582
|
+
async applyFix(slides, issue) {
|
|
4583
|
+
const result = {
|
|
4584
|
+
slideIndex: issue.slideIndex,
|
|
4585
|
+
dimension: issue.dimension,
|
|
4586
|
+
originalValue: issue.currentValue,
|
|
4587
|
+
newValue: null,
|
|
4588
|
+
description: issue.message,
|
|
4589
|
+
applied: false
|
|
4590
|
+
};
|
|
4591
|
+
switch (issue.dimension) {
|
|
4592
|
+
case "content":
|
|
4593
|
+
return this.fixContentIssue(slides, issue, result);
|
|
4594
|
+
case "clarity":
|
|
4595
|
+
return this.fixClarityIssue(slides, issue, result);
|
|
4596
|
+
case "layout":
|
|
4597
|
+
return this.fixLayoutIssue(slides, issue, result);
|
|
4598
|
+
case "consistency":
|
|
4599
|
+
return this.fixConsistencyIssue(slides, issue, result);
|
|
4600
|
+
case "contrast":
|
|
4601
|
+
return this.fixContrastIssue(slides, issue, result);
|
|
4602
|
+
default:
|
|
4603
|
+
result.description = `No auto-fix available for ${issue.dimension}`;
|
|
4604
|
+
return result;
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Fix content-related issues (word count, bullet count).
|
|
4609
|
+
*/
|
|
4610
|
+
fixContentIssue(slides, issue, result) {
|
|
4611
|
+
if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
|
|
4612
|
+
return result;
|
|
4613
|
+
}
|
|
4614
|
+
const slide = slides[issue.slideIndex];
|
|
4615
|
+
if (!slide) return result;
|
|
4616
|
+
if (issue.message.includes("Word count") && issue.message.includes("exceeds")) {
|
|
4617
|
+
const maxWords = issue.expectedValue;
|
|
4618
|
+
result.originalValue = this.countWords(slide);
|
|
4619
|
+
if (slide.data.body) {
|
|
4620
|
+
slide.data.body = this.condenseText(slide.data.body, maxWords / 2);
|
|
4621
|
+
}
|
|
4622
|
+
if (slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4623
|
+
const bulletCount = slide.data.bullets.length;
|
|
4624
|
+
slide.data.bullets = slide.data.bullets.map(
|
|
4625
|
+
(bullet) => this.condenseText(bullet, Math.floor(maxWords / bulletCount))
|
|
4626
|
+
);
|
|
4627
|
+
}
|
|
4628
|
+
if (slide.data.subtitle) {
|
|
4629
|
+
slide.data.subtitle = this.condenseText(slide.data.subtitle, 10);
|
|
4630
|
+
}
|
|
4631
|
+
result.newValue = this.countWords(slide);
|
|
4632
|
+
result.applied = result.newValue <= maxWords;
|
|
4633
|
+
result.description = `Condensed content from ${result.originalValue} to ${result.newValue} words`;
|
|
4634
|
+
}
|
|
4635
|
+
if (issue.message.includes("bullets exceeds")) {
|
|
4636
|
+
const maxBullets = issue.expectedValue;
|
|
4637
|
+
if (slide.data.bullets) {
|
|
4638
|
+
result.originalValue = slide.data.bullets.length;
|
|
4639
|
+
slide.data.bullets = slide.data.bullets.slice(0, maxBullets);
|
|
4640
|
+
result.newValue = slide.data.bullets.length;
|
|
4641
|
+
result.applied = true;
|
|
4642
|
+
result.description = `Reduced bullets from ${result.originalValue} to ${result.newValue}`;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
return result;
|
|
4646
|
+
}
|
|
4647
|
+
/**
|
|
4648
|
+
* Fix clarity-related issues (key message length, title length).
|
|
4649
|
+
*/
|
|
4650
|
+
fixClarityIssue(slides, issue, result) {
|
|
4651
|
+
if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
|
|
4652
|
+
return result;
|
|
4653
|
+
}
|
|
4654
|
+
const slide = slides[issue.slideIndex];
|
|
4655
|
+
if (!slide) return result;
|
|
4656
|
+
if (issue.message.includes("Key message too long")) {
|
|
4657
|
+
if (slide.data.keyMessage) {
|
|
4658
|
+
result.originalValue = slide.data.keyMessage;
|
|
4659
|
+
const maxWords = issue.expectedValue;
|
|
4660
|
+
slide.data.keyMessage = this.condenseText(slide.data.keyMessage, maxWords);
|
|
4661
|
+
result.newValue = slide.data.keyMessage;
|
|
4662
|
+
result.applied = true;
|
|
4663
|
+
result.description = `Shortened key message to ${maxWords} words`;
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
if (issue.message.includes("Title too long")) {
|
|
4667
|
+
if (slide.data.title) {
|
|
4668
|
+
result.originalValue = slide.data.title;
|
|
4669
|
+
const words = slide.data.title.split(/\s+/);
|
|
4670
|
+
const originalLength = words.length;
|
|
4671
|
+
if (words.length > 6) {
|
|
4672
|
+
slide.data.title = words.slice(0, 6).join(" ");
|
|
4673
|
+
}
|
|
4674
|
+
result.newValue = slide.data.title;
|
|
4675
|
+
result.applied = true;
|
|
4676
|
+
result.description = `Shortened title from ${originalLength} to ${slide.data.title.split(/\s+/).length} words`;
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
if (issue.message.includes("Too many elements")) {
|
|
4680
|
+
result.originalValue = issue.currentValue;
|
|
4681
|
+
if (slide.data.subtitle && slide.data.body) {
|
|
4682
|
+
delete slide.data.subtitle;
|
|
4683
|
+
result.applied = true;
|
|
4684
|
+
result.description = "Removed subtitle to reduce element count";
|
|
4685
|
+
} else if (slide.data.body && slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4686
|
+
delete slide.data.body;
|
|
4687
|
+
result.applied = true;
|
|
4688
|
+
result.description = "Removed body text, keeping bullets";
|
|
4689
|
+
}
|
|
4690
|
+
result.newValue = this.countElements(slide);
|
|
4691
|
+
}
|
|
4692
|
+
return result;
|
|
4693
|
+
}
|
|
4694
|
+
/**
|
|
4695
|
+
* Fix layout-related issues.
|
|
4696
|
+
*/
|
|
4697
|
+
fixLayoutIssue(slides, issue, result) {
|
|
4698
|
+
if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
|
|
4699
|
+
return result;
|
|
4700
|
+
}
|
|
4701
|
+
const slide = slides[issue.slideIndex];
|
|
4702
|
+
if (!slide) return result;
|
|
4703
|
+
if (issue.message.includes("Insufficient whitespace")) {
|
|
4704
|
+
const currentWordCount = this.countWords(slide);
|
|
4705
|
+
const targetReduction = 0.3;
|
|
4706
|
+
const targetWords = Math.floor(currentWordCount * (1 - targetReduction));
|
|
4707
|
+
result.originalValue = currentWordCount;
|
|
4708
|
+
if (slide.data.body) {
|
|
4709
|
+
slide.data.body = this.condenseText(slide.data.body, Math.floor(targetWords * 0.5));
|
|
4710
|
+
}
|
|
4711
|
+
if (slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4712
|
+
const wordsPerBullet = Math.floor(targetWords / (slide.data.bullets.length * 2));
|
|
4713
|
+
slide.data.bullets = slide.data.bullets.map((b) => this.condenseText(b, wordsPerBullet));
|
|
4714
|
+
}
|
|
4715
|
+
result.newValue = this.countWords(slide);
|
|
4716
|
+
result.applied = true;
|
|
4717
|
+
result.description = `Reduced content from ${result.originalValue} to ${result.newValue} words for better whitespace`;
|
|
4718
|
+
}
|
|
4719
|
+
return result;
|
|
4720
|
+
}
|
|
4721
|
+
/**
|
|
4722
|
+
* Fix consistency-related issues.
|
|
4723
|
+
*/
|
|
4724
|
+
fixConsistencyIssue(slides, issue, result) {
|
|
4725
|
+
if (issue.message.includes("Inconsistent title casing")) {
|
|
4726
|
+
let fixedCount = 0;
|
|
4727
|
+
for (const slide of slides) {
|
|
4728
|
+
if (slide.data.title) {
|
|
4729
|
+
const original = slide.data.title;
|
|
4730
|
+
slide.data.title = this.toTitleCase(slide.data.title);
|
|
4731
|
+
if (slide.data.title !== original) fixedCount++;
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
result.originalValue = "Mixed casing";
|
|
4735
|
+
result.newValue = "Title Case";
|
|
4736
|
+
result.applied = fixedCount > 0;
|
|
4737
|
+
result.description = `Applied Title Case to ${fixedCount} slide titles`;
|
|
4738
|
+
}
|
|
4739
|
+
return result;
|
|
4740
|
+
}
|
|
4741
|
+
/**
|
|
4742
|
+
* Fix contrast-related issues.
|
|
4743
|
+
* Note: These are CSS fixes, typically handled at generation time.
|
|
4744
|
+
*/
|
|
4745
|
+
fixContrastIssue(slides, issue, result) {
|
|
4746
|
+
result.description = "Contrast issues flagged for CSS regeneration";
|
|
4747
|
+
result.applied = false;
|
|
4748
|
+
return result;
|
|
4749
|
+
}
|
|
4750
|
+
/**
|
|
4751
|
+
* Condense text to approximately maxWords.
|
|
4752
|
+
* Uses smart truncation that preserves meaning.
|
|
4753
|
+
*/
|
|
4754
|
+
condenseText(text, maxWords) {
|
|
4755
|
+
const words = text.split(/\s+/);
|
|
4756
|
+
if (words.length <= maxWords) {
|
|
4757
|
+
return text;
|
|
4758
|
+
}
|
|
4759
|
+
const fillerWords = /* @__PURE__ */ new Set([
|
|
4760
|
+
"very",
|
|
4761
|
+
"really",
|
|
4762
|
+
"actually",
|
|
4763
|
+
"basically",
|
|
4764
|
+
"literally",
|
|
4765
|
+
"obviously",
|
|
4766
|
+
"clearly",
|
|
4767
|
+
"simply",
|
|
4768
|
+
"just",
|
|
4769
|
+
"that",
|
|
4770
|
+
"which",
|
|
4771
|
+
"would",
|
|
4772
|
+
"could",
|
|
4773
|
+
"should",
|
|
4774
|
+
"might"
|
|
4775
|
+
]);
|
|
4776
|
+
let filtered = words.filter((w) => !fillerWords.has(w.toLowerCase()));
|
|
4777
|
+
if (filtered.length <= maxWords) {
|
|
4778
|
+
return filtered.join(" ");
|
|
4779
|
+
}
|
|
4780
|
+
const punctuation = [".", ",", ";", ":", "-"];
|
|
4781
|
+
let breakPoint = maxWords;
|
|
4782
|
+
for (let i = maxWords - 1; i >= maxWords - 5 && i >= 0; i--) {
|
|
4783
|
+
const word = filtered[i];
|
|
4784
|
+
if (word && punctuation.some((p) => word.endsWith(p))) {
|
|
4785
|
+
breakPoint = i + 1;
|
|
4786
|
+
break;
|
|
4787
|
+
}
|
|
2901
4788
|
}
|
|
4789
|
+
filtered = filtered.slice(0, breakPoint);
|
|
4790
|
+
let result = filtered.join(" ");
|
|
4791
|
+
if (!result.endsWith(".") && !result.endsWith("!") && !result.endsWith("?")) {
|
|
4792
|
+
result = result.replace(/[,;:]$/, "") + "...";
|
|
4793
|
+
}
|
|
4794
|
+
return result;
|
|
2902
4795
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
4796
|
+
/**
|
|
4797
|
+
* Convert text to Title Case.
|
|
4798
|
+
*/
|
|
4799
|
+
toTitleCase(text) {
|
|
4800
|
+
const minorWords = /* @__PURE__ */ new Set([
|
|
4801
|
+
"a",
|
|
4802
|
+
"an",
|
|
4803
|
+
"the",
|
|
4804
|
+
"and",
|
|
4805
|
+
"but",
|
|
4806
|
+
"or",
|
|
4807
|
+
"for",
|
|
4808
|
+
"nor",
|
|
4809
|
+
"on",
|
|
4810
|
+
"at",
|
|
4811
|
+
"to",
|
|
4812
|
+
"by",
|
|
4813
|
+
"of",
|
|
4814
|
+
"in",
|
|
4815
|
+
"with"
|
|
4816
|
+
]);
|
|
4817
|
+
return text.split(" ").map((word, index) => {
|
|
4818
|
+
if (index === 0) {
|
|
4819
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
4820
|
+
}
|
|
4821
|
+
if (minorWords.has(word.toLowerCase())) {
|
|
4822
|
+
return word.toLowerCase();
|
|
4823
|
+
}
|
|
4824
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
4825
|
+
}).join(" ");
|
|
4826
|
+
}
|
|
4827
|
+
/**
|
|
4828
|
+
* Count words in a slide.
|
|
4829
|
+
*/
|
|
4830
|
+
countWords(slide) {
|
|
4831
|
+
let text = "";
|
|
4832
|
+
if (slide.data.title) text += slide.data.title + " ";
|
|
4833
|
+
if (slide.data.subtitle) text += slide.data.subtitle + " ";
|
|
4834
|
+
if (slide.data.body) text += slide.data.body + " ";
|
|
4835
|
+
if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
|
|
4836
|
+
if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
|
|
4837
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
4838
|
+
}
|
|
4839
|
+
/**
|
|
4840
|
+
* Count elements in a slide.
|
|
4841
|
+
*/
|
|
4842
|
+
countElements(slide) {
|
|
4843
|
+
return (slide.data.title ? 1 : 0) + (slide.data.subtitle ? 1 : 0) + (slide.data.body ? 1 : 0) + (slide.data.bullets?.length || 0) + (slide.data.keyMessage ? 1 : 0) + (slide.data.image ? 1 : 0);
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4846
|
+
* Generate a summary of fixes applied.
|
|
4847
|
+
*/
|
|
4848
|
+
generateSummary(applied, skipped) {
|
|
4849
|
+
const lines = [];
|
|
4850
|
+
lines.push(`
|
|
4851
|
+
\u{1F527} Auto-Fix Summary`);
|
|
4852
|
+
lines.push(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
4853
|
+
lines.push(`Fixes Applied: ${applied.length}`);
|
|
4854
|
+
lines.push(`Fixes Skipped: ${skipped.length}`);
|
|
4855
|
+
lines.push("");
|
|
4856
|
+
if (applied.length > 0) {
|
|
4857
|
+
lines.push("\u2705 Applied Fixes:");
|
|
4858
|
+
for (const fix of applied) {
|
|
4859
|
+
lines.push(` \u2022 ${fix.description}`);
|
|
4860
|
+
}
|
|
4861
|
+
lines.push("");
|
|
4862
|
+
}
|
|
4863
|
+
if (skipped.length > 0) {
|
|
4864
|
+
lines.push("\u26A0\uFE0F Skipped Fixes (require manual attention):");
|
|
4865
|
+
for (const fix of skipped.slice(0, 5)) {
|
|
4866
|
+
lines.push(` \u2022 ${fix.description}`);
|
|
4867
|
+
}
|
|
4868
|
+
if (skipped.length > 5) {
|
|
4869
|
+
lines.push(` ... and ${skipped.length - 5} more`);
|
|
4870
|
+
}
|
|
2907
4871
|
}
|
|
4872
|
+
return lines.join("\n");
|
|
2908
4873
|
}
|
|
2909
4874
|
};
|
|
4875
|
+
function createAutoFixEngine(mode, presentationType) {
|
|
4876
|
+
return new AutoFixEngine(mode, presentationType);
|
|
4877
|
+
}
|
|
2910
4878
|
|
|
2911
4879
|
// src/generators/html/RevealJsGenerator.ts
|
|
2912
4880
|
var RevealJsGenerator = class {
|
|
@@ -3070,6 +5038,12 @@ ${slides}
|
|
|
3070
5038
|
const highlight = palette.accent || "#e94560";
|
|
3071
5039
|
const text = palette.text || "#1a1a2e";
|
|
3072
5040
|
const background = palette.background || "#ffffff";
|
|
5041
|
+
const isDark = this.isDarkPalette(palette);
|
|
5042
|
+
const defaultBg = isDark ? background : "#ffffff";
|
|
5043
|
+
const defaultText = isDark ? text : "#1a1a2e";
|
|
5044
|
+
const headingColor = isDark ? primary : primary;
|
|
5045
|
+
const titleBg = isDark ? background : primary;
|
|
5046
|
+
const titleBgEnd = isDark ? this.lightenColor(background, 10) : this.darkenColor(primary, 15);
|
|
3073
5047
|
return `
|
|
3074
5048
|
/* Base Styles - KB-Driven Design System */
|
|
3075
5049
|
:root {
|
|
@@ -3096,7 +5070,13 @@ ${slides}
|
|
|
3096
5070
|
font-family: var(--font-body);
|
|
3097
5071
|
font-size: ${fontSize};
|
|
3098
5072
|
line-height: ${lineHeight};
|
|
3099
|
-
color:
|
|
5073
|
+
color: ${defaultText};
|
|
5074
|
+
background: ${defaultBg};
|
|
5075
|
+
}
|
|
5076
|
+
|
|
5077
|
+
/* Override reveal.js default backgrounds for dark mode */
|
|
5078
|
+
.reveal .slides section {
|
|
5079
|
+
background: ${defaultBg};
|
|
3100
5080
|
}
|
|
3101
5081
|
|
|
3102
5082
|
.reveal .slides {
|
|
@@ -3125,10 +5105,17 @@ ${slides}
|
|
|
3125
5105
|
font-family: var(--font-heading);
|
|
3126
5106
|
font-weight: 700;
|
|
3127
5107
|
letter-spacing: -0.02em;
|
|
3128
|
-
color:
|
|
5108
|
+
color: ${isDark ? primary : primary};
|
|
3129
5109
|
margin-bottom: 0.5em;
|
|
3130
5110
|
}
|
|
3131
5111
|
|
|
5112
|
+
/* Dark mode text visibility */
|
|
5113
|
+
${isDark ? `
|
|
5114
|
+
.reveal, .reveal p, .reveal li, .reveal span {
|
|
5115
|
+
color: ${text};
|
|
5116
|
+
}
|
|
5117
|
+
` : ""}
|
|
5118
|
+
|
|
3132
5119
|
.reveal h1 { font-size: 2.5em; }
|
|
3133
5120
|
.reveal h2 { font-size: 1.8em; }
|
|
3134
5121
|
.reveal h3 { font-size: 1.3em; }
|
|
@@ -3306,6 +5293,124 @@ ${slides}
|
|
|
3306
5293
|
font-weight: 600;
|
|
3307
5294
|
margin-top: 1em;
|
|
3308
5295
|
}
|
|
5296
|
+
|
|
5297
|
+
/* ================================================================
|
|
5298
|
+
SLIDE TYPE BACKGROUNDS - KB-Driven Visual Design
|
|
5299
|
+
Creates visual hierarchy and differentiation between slide types
|
|
5300
|
+
================================================================ */
|
|
5301
|
+
|
|
5302
|
+
/* Title slide: Bold background - makes strong first impression */
|
|
5303
|
+
.reveal .slide-title {
|
|
5304
|
+
background: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
|
|
5305
|
+
}
|
|
5306
|
+
.reveal .slide-title h1,
|
|
5307
|
+
.reveal .slide-title h2,
|
|
5308
|
+
.reveal .slide-title p,
|
|
5309
|
+
.reveal .slide-title .subtitle {
|
|
5310
|
+
color: ${isDark ? primary : "#ffffff"};
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
/* Section dividers: Subtle visual breaks */
|
|
5314
|
+
.reveal .slide-section-divider {
|
|
5315
|
+
background: ${isDark ? this.lightenColor(background, 8) : `linear-gradient(180deg, ${this.lightenColor(primary, 85)} 0%, ${this.lightenColor(primary, 92)} 100%)`};
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5318
|
+
/* Big number slides: Clean with accent highlight - emphasizes the data */
|
|
5319
|
+
.reveal .slide-big-number {
|
|
5320
|
+
background: var(--color-background);
|
|
5321
|
+
border-left: 8px solid var(--color-highlight);
|
|
5322
|
+
}
|
|
5323
|
+
.reveal .slide-big-number .number {
|
|
5324
|
+
font-size: 5em;
|
|
5325
|
+
background: linear-gradient(135deg, var(--color-highlight) 0%, var(--color-accent) 100%);
|
|
5326
|
+
-webkit-background-clip: text;
|
|
5327
|
+
-webkit-text-fill-color: transparent;
|
|
5328
|
+
background-clip: text;
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5331
|
+
/* Metrics grid: Light accent background - data-focused */
|
|
5332
|
+
.reveal .slide-metrics-grid {
|
|
5333
|
+
background: ${isDark ? this.lightenColor(background, 8) : this.lightenColor(accent, 90)};
|
|
5334
|
+
}
|
|
5335
|
+
${isDark ? `.reveal .slide-metrics-grid { color: ${text}; }` : ""}
|
|
5336
|
+
|
|
5337
|
+
/* CTA slide: Highlight color - drives action */
|
|
5338
|
+
.reveal .slide-cta {
|
|
5339
|
+
background: linear-gradient(135deg, var(--color-highlight) 0%, var(--color-accent) 100%);
|
|
5340
|
+
}
|
|
5341
|
+
.reveal .slide-cta h2,
|
|
5342
|
+
.reveal .slide-cta p {
|
|
5343
|
+
color: #ffffff;
|
|
5344
|
+
}
|
|
5345
|
+
.reveal .slide-cta .cta-button {
|
|
5346
|
+
background: #ffffff;
|
|
5347
|
+
color: var(--color-highlight);
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
/* Thank you slide: Matches title for bookend effect */
|
|
5351
|
+
.reveal .slide-thank-you {
|
|
5352
|
+
background: linear-gradient(135deg, ${titleBg} 0%, ${titleBgEnd} 100%);
|
|
5353
|
+
}
|
|
5354
|
+
.reveal .slide-thank-you h2,
|
|
5355
|
+
.reveal .slide-thank-you p,
|
|
5356
|
+
.reveal .slide-thank-you .subtitle {
|
|
5357
|
+
color: ${isDark ? primary : "#ffffff"};
|
|
5358
|
+
}
|
|
5359
|
+
|
|
5360
|
+
/* Quote slides: Elegant subtle background */
|
|
5361
|
+
.reveal .slide-quote {
|
|
5362
|
+
background: ${isDark ? this.lightenColor(background, 5) : this.lightenColor(secondary, 92)};
|
|
5363
|
+
}
|
|
5364
|
+
.reveal .slide-quote blockquote {
|
|
5365
|
+
border-left-color: var(--color-accent);
|
|
5366
|
+
font-size: 1.3em;
|
|
5367
|
+
}
|
|
5368
|
+
${isDark ? `.reveal .slide-quote { color: ${text}; }` : ""}
|
|
5369
|
+
|
|
5370
|
+
/* Single statement: Clean, centered, impactful */
|
|
5371
|
+
.reveal .slide-single-statement {
|
|
5372
|
+
background: var(--color-background);
|
|
5373
|
+
}
|
|
5374
|
+
.reveal .slide-single-statement .statement,
|
|
5375
|
+
.reveal .slide-single-statement .big-idea-text {
|
|
5376
|
+
font-size: 2.2em;
|
|
5377
|
+
max-width: 80%;
|
|
5378
|
+
margin: 0 auto;
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
/* Comparison slides: Split visual */
|
|
5382
|
+
.reveal .slide-comparison .columns {
|
|
5383
|
+
gap: 60px;
|
|
5384
|
+
}
|
|
5385
|
+
.reveal .slide-comparison .column:first-child {
|
|
5386
|
+
border-right: 2px solid ${this.lightenColor(secondary, 70)};
|
|
5387
|
+
padding-right: 30px;
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5390
|
+
/* Timeline/Process: Visual flow */
|
|
5391
|
+
.reveal .slide-timeline .steps,
|
|
5392
|
+
.reveal .slide-process .steps {
|
|
5393
|
+
display: flex;
|
|
5394
|
+
gap: 20px;
|
|
5395
|
+
}
|
|
5396
|
+
.reveal .slide-timeline .step,
|
|
5397
|
+
.reveal .slide-process .step {
|
|
5398
|
+
flex: 1;
|
|
5399
|
+
padding: 20px;
|
|
5400
|
+
background: ${isDark ? this.lightenColor(background, 10) : this.lightenColor(secondary, 92)};
|
|
5401
|
+
border-radius: 8px;
|
|
5402
|
+
border-top: 4px solid var(--color-accent);
|
|
5403
|
+
${isDark ? `color: ${text};` : ""}
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
/* Progress bar enhancement */
|
|
5407
|
+
.reveal .progress {
|
|
5408
|
+
background: ${this.lightenColor(primary, 80)};
|
|
5409
|
+
height: 4px;
|
|
5410
|
+
}
|
|
5411
|
+
.reveal .progress span {
|
|
5412
|
+
background: var(--color-highlight);
|
|
5413
|
+
}
|
|
3309
5414
|
`;
|
|
3310
5415
|
}
|
|
3311
5416
|
/**
|
|
@@ -3321,6 +5426,37 @@ ${slides}
|
|
|
3321
5426
|
b = Math.min(255, Math.floor(b + (255 - b) * (percent / 100)));
|
|
3322
5427
|
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
3323
5428
|
}
|
|
5429
|
+
/**
|
|
5430
|
+
* Check if a palette is "dark mode" (dark background, light text)
|
|
5431
|
+
*/
|
|
5432
|
+
isDarkPalette(palette) {
|
|
5433
|
+
const bgLuminance = this.getLuminance(palette.background || "#ffffff");
|
|
5434
|
+
const textLuminance = this.getLuminance(palette.text || "#000000");
|
|
5435
|
+
return bgLuminance < textLuminance;
|
|
5436
|
+
}
|
|
5437
|
+
/**
|
|
5438
|
+
* Get relative luminance of a hex color (0-1, 0=black, 1=white)
|
|
5439
|
+
*/
|
|
5440
|
+
getLuminance(hex) {
|
|
5441
|
+
hex = hex.replace("#", "");
|
|
5442
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
5443
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
5444
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
5445
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
5446
|
+
}
|
|
5447
|
+
/**
|
|
5448
|
+
* Darken a hex color
|
|
5449
|
+
*/
|
|
5450
|
+
darkenColor(hex, percent) {
|
|
5451
|
+
hex = hex.replace("#", "");
|
|
5452
|
+
let r = parseInt(hex.substring(0, 2), 16);
|
|
5453
|
+
let g = parseInt(hex.substring(2, 4), 16);
|
|
5454
|
+
let b = parseInt(hex.substring(4, 6), 16);
|
|
5455
|
+
r = Math.max(0, Math.floor(r * (1 - percent / 100)));
|
|
5456
|
+
g = Math.max(0, Math.floor(g * (1 - percent / 100)));
|
|
5457
|
+
b = Math.max(0, Math.floor(b * (1 - percent / 100)));
|
|
5458
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
5459
|
+
}
|
|
3324
5460
|
/**
|
|
3325
5461
|
* Get theme-specific styles - KB-DRIVEN
|
|
3326
5462
|
*/
|
|
@@ -3502,6 +5638,182 @@ ${slides}
|
|
|
3502
5638
|
}
|
|
3503
5639
|
};
|
|
3504
5640
|
|
|
5641
|
+
// src/qa/IterativeQAEngine.ts
|
|
5642
|
+
var DEFAULT_OPTIONS = {
|
|
5643
|
+
minScore: 95,
|
|
5644
|
+
maxIterations: 5,
|
|
5645
|
+
verbose: true
|
|
5646
|
+
};
|
|
5647
|
+
var IterativeQAEngine = class {
|
|
5648
|
+
kb;
|
|
5649
|
+
scorer;
|
|
5650
|
+
fixer;
|
|
5651
|
+
generator;
|
|
5652
|
+
mode;
|
|
5653
|
+
presentationType;
|
|
5654
|
+
config;
|
|
5655
|
+
constructor(mode, presentationType, config) {
|
|
5656
|
+
this.mode = mode;
|
|
5657
|
+
this.presentationType = presentationType;
|
|
5658
|
+
this.config = config;
|
|
5659
|
+
this.scorer = new SevenDimensionScorer(mode, presentationType);
|
|
5660
|
+
this.fixer = new AutoFixEngine(mode, presentationType);
|
|
5661
|
+
this.generator = new RevealJsGenerator();
|
|
5662
|
+
}
|
|
5663
|
+
/**
|
|
5664
|
+
* Run the iterative QA process.
|
|
5665
|
+
*/
|
|
5666
|
+
async run(initialSlides, initialHtml, options = {}) {
|
|
5667
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
5668
|
+
this.kb = await getKnowledgeGateway();
|
|
5669
|
+
const iterations = [];
|
|
5670
|
+
let currentSlides = initialSlides;
|
|
5671
|
+
let currentHtml = initialHtml;
|
|
5672
|
+
let scoringResult;
|
|
5673
|
+
let autoFixSummary = "";
|
|
5674
|
+
let totalFixesApplied = 0;
|
|
5675
|
+
if (opts.verbose) {
|
|
5676
|
+
logger.progress("\n\u{1F504} Starting Iterative QA Process");
|
|
5677
|
+
logger.info(` Target Score: ${opts.minScore}/100`);
|
|
5678
|
+
logger.info(` Max Iterations: ${opts.maxIterations}`);
|
|
5679
|
+
logger.progress("");
|
|
5680
|
+
}
|
|
5681
|
+
for (let i = 0; i < opts.maxIterations; i++) {
|
|
5682
|
+
const iterationNum = i + 1;
|
|
5683
|
+
if (opts.verbose) {
|
|
5684
|
+
logger.progress(`\u{1F4CA} Iteration ${iterationNum}/${opts.maxIterations}...`);
|
|
5685
|
+
}
|
|
5686
|
+
scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
|
|
5687
|
+
iterations.push({
|
|
5688
|
+
iteration: iterationNum,
|
|
5689
|
+
score: scoringResult.overallScore,
|
|
5690
|
+
dimensionScores: {
|
|
5691
|
+
layout: scoringResult.dimensions.layout.score,
|
|
5692
|
+
contrast: scoringResult.dimensions.contrast.score,
|
|
5693
|
+
graphics: scoringResult.dimensions.graphics.score,
|
|
5694
|
+
content: scoringResult.dimensions.content.score,
|
|
5695
|
+
clarity: scoringResult.dimensions.clarity.score,
|
|
5696
|
+
effectiveness: scoringResult.dimensions.effectiveness.score,
|
|
5697
|
+
consistency: scoringResult.dimensions.consistency.score
|
|
5698
|
+
},
|
|
5699
|
+
fixesApplied: 0,
|
|
5700
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
5701
|
+
});
|
|
5702
|
+
if (opts.verbose) {
|
|
5703
|
+
logger.info(` Score: ${scoringResult.overallScore}/100`);
|
|
5704
|
+
}
|
|
5705
|
+
if (scoringResult.passed) {
|
|
5706
|
+
if (opts.verbose) {
|
|
5707
|
+
logger.success(`PASSED - Score meets threshold (${opts.minScore})`);
|
|
5708
|
+
}
|
|
5709
|
+
break;
|
|
5710
|
+
}
|
|
5711
|
+
if (i < opts.maxIterations - 1) {
|
|
5712
|
+
if (opts.verbose) {
|
|
5713
|
+
logger.warn("Below threshold, applying auto-fixes...");
|
|
5714
|
+
}
|
|
5715
|
+
const fixResult = await this.fixer.fix(currentSlides, scoringResult);
|
|
5716
|
+
if (fixResult.fixesApplied.length > 0) {
|
|
5717
|
+
currentSlides = fixResult.slidesFixed;
|
|
5718
|
+
currentHtml = await this.generator.generate(currentSlides, this.config);
|
|
5719
|
+
const lastIteration = iterations[iterations.length - 1];
|
|
5720
|
+
if (lastIteration) {
|
|
5721
|
+
lastIteration.fixesApplied = fixResult.fixesApplied.length;
|
|
5722
|
+
}
|
|
5723
|
+
totalFixesApplied += fixResult.fixesApplied.length;
|
|
5724
|
+
if (opts.verbose) {
|
|
5725
|
+
logger.info(` \u{1F527} Applied ${fixResult.fixesApplied.length} fixes`);
|
|
5726
|
+
}
|
|
5727
|
+
autoFixSummary += fixResult.summary + "\n";
|
|
5728
|
+
} else {
|
|
5729
|
+
if (opts.verbose) {
|
|
5730
|
+
logger.warn("No auto-fixes available, manual review needed");
|
|
5731
|
+
}
|
|
5732
|
+
break;
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
if (!scoringResult.passed) {
|
|
5737
|
+
scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
|
|
5738
|
+
}
|
|
5739
|
+
const report = this.generateReport(
|
|
5740
|
+
scoringResult,
|
|
5741
|
+
iterations,
|
|
5742
|
+
opts,
|
|
5743
|
+
totalFixesApplied
|
|
5744
|
+
);
|
|
5745
|
+
if (opts.verbose) {
|
|
5746
|
+
logger.info(report);
|
|
5747
|
+
}
|
|
5748
|
+
return {
|
|
5749
|
+
finalScore: scoringResult.overallScore,
|
|
5750
|
+
passed: scoringResult.passed,
|
|
5751
|
+
threshold: opts.minScore,
|
|
5752
|
+
iterations,
|
|
5753
|
+
totalIterations: iterations.length,
|
|
5754
|
+
maxIterations: opts.maxIterations,
|
|
5755
|
+
slides: currentSlides,
|
|
5756
|
+
html: currentHtml,
|
|
5757
|
+
finalScoring: scoringResult,
|
|
5758
|
+
autoFixSummary,
|
|
5759
|
+
report
|
|
5760
|
+
};
|
|
5761
|
+
}
|
|
5762
|
+
/**
|
|
5763
|
+
* Generate a comprehensive report.
|
|
5764
|
+
*/
|
|
5765
|
+
generateReport(finalScoring, iterations, options, totalFixesApplied) {
|
|
5766
|
+
const lines = [];
|
|
5767
|
+
lines.push("");
|
|
5768
|
+
lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
5769
|
+
lines.push("\u2551 ITERATIVE QA FINAL REPORT \u2551");
|
|
5770
|
+
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
5771
|
+
lines.push("");
|
|
5772
|
+
const passStatus = finalScoring.passed ? "\u2705 PASSED" : "\u274C FAILED";
|
|
5773
|
+
lines.push(`Final Score: ${finalScoring.overallScore}/100 ${passStatus}`);
|
|
5774
|
+
lines.push(`Threshold: ${options.minScore}/100`);
|
|
5775
|
+
lines.push(`Iterations: ${iterations.length}/${options.maxIterations}`);
|
|
5776
|
+
lines.push(`Total Fixes Applied: ${totalFixesApplied}`);
|
|
5777
|
+
lines.push("");
|
|
5778
|
+
if (iterations.length > 1) {
|
|
5779
|
+
lines.push("Score Progression:");
|
|
5780
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5781
|
+
for (const iter of iterations) {
|
|
5782
|
+
const bar = "\u2588".repeat(Math.floor(iter.score / 10)) + "\u2591".repeat(10 - Math.floor(iter.score / 10));
|
|
5783
|
+
lines.push(` Iter ${iter.iteration}: ${bar} ${iter.score}/100 (+${iter.fixesApplied} fixes)`);
|
|
5784
|
+
}
|
|
5785
|
+
lines.push("");
|
|
5786
|
+
}
|
|
5787
|
+
lines.push(this.scorer.formatReport(finalScoring));
|
|
5788
|
+
lines.push("");
|
|
5789
|
+
lines.push("\u{1F4DA} Knowledge Base Compliance:");
|
|
5790
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5791
|
+
lines.push(` Mode: ${this.mode}`);
|
|
5792
|
+
lines.push(` Presentation Type: ${this.presentationType}`);
|
|
5793
|
+
lines.push(` Word Limits: ${this.mode === "keynote" ? "6-25" : "40-80"} per slide`);
|
|
5794
|
+
lines.push(` Expert Frameworks: Duarte, Reynolds, Gallo, Anderson`);
|
|
5795
|
+
lines.push("");
|
|
5796
|
+
if (!finalScoring.passed) {
|
|
5797
|
+
lines.push("\u{1F4CB} Recommendations for Manual Review:");
|
|
5798
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5799
|
+
const manualIssues = finalScoring.issues.filter((i) => !i.autoFixable);
|
|
5800
|
+
for (const issue of manualIssues.slice(0, 5)) {
|
|
5801
|
+
lines.push(` \u2022 ${issue.message}`);
|
|
5802
|
+
if (issue.fixSuggestion) {
|
|
5803
|
+
lines.push(` \u2192 ${issue.fixSuggestion}`);
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
if (manualIssues.length > 5) {
|
|
5807
|
+
lines.push(` ... and ${manualIssues.length - 5} more issues`);
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
return lines.join("\n");
|
|
5811
|
+
}
|
|
5812
|
+
};
|
|
5813
|
+
function createIterativeQAEngine(mode, presentationType, config) {
|
|
5814
|
+
return new IterativeQAEngine(mode, presentationType, config);
|
|
5815
|
+
}
|
|
5816
|
+
|
|
3505
5817
|
// src/generators/pptx/PowerPointGenerator.ts
|
|
3506
5818
|
var import_pptxgenjs = __toESM(require("pptxgenjs"));
|
|
3507
5819
|
|
|
@@ -4233,23 +6545,190 @@ var PowerPointGenerator = class {
|
|
|
4233
6545
|
}
|
|
4234
6546
|
};
|
|
4235
6547
|
|
|
6548
|
+
// src/generators/PDFGenerator.ts
|
|
6549
|
+
var import_puppeteer = __toESM(require("puppeteer"));
|
|
6550
|
+
var PDFGenerator = class {
|
|
6551
|
+
defaultOptions = {
|
|
6552
|
+
orientation: "landscape",
|
|
6553
|
+
printBackground: true,
|
|
6554
|
+
format: "Slide",
|
|
6555
|
+
scale: 1
|
|
6556
|
+
};
|
|
6557
|
+
/**
|
|
6558
|
+
* Generate PDF from HTML presentation
|
|
6559
|
+
* @param html - The HTML content of the presentation
|
|
6560
|
+
* @param options - PDF generation options
|
|
6561
|
+
* @returns Buffer containing the PDF file
|
|
6562
|
+
*/
|
|
6563
|
+
async generate(html, options = {}) {
|
|
6564
|
+
const opts = { ...this.defaultOptions, ...options };
|
|
6565
|
+
logger.progress("\u{1F4C4} Generating PDF...");
|
|
6566
|
+
let browser;
|
|
6567
|
+
try {
|
|
6568
|
+
browser = await import_puppeteer.default.launch({
|
|
6569
|
+
headless: true,
|
|
6570
|
+
args: [
|
|
6571
|
+
"--no-sandbox",
|
|
6572
|
+
"--disable-setuid-sandbox",
|
|
6573
|
+
"--disable-dev-shm-usage"
|
|
6574
|
+
]
|
|
6575
|
+
});
|
|
6576
|
+
const page = await browser.newPage();
|
|
6577
|
+
const printHtml = this.preparePrintHtml(html);
|
|
6578
|
+
await page.setViewport({
|
|
6579
|
+
width: 1920,
|
|
6580
|
+
height: 1080
|
|
6581
|
+
});
|
|
6582
|
+
await page.setContent(printHtml, {
|
|
6583
|
+
waitUntil: ["networkidle0", "domcontentloaded"]
|
|
6584
|
+
});
|
|
6585
|
+
await page.evaluate(() => {
|
|
6586
|
+
return new Promise((resolve) => {
|
|
6587
|
+
if (document.fonts && document.fonts.ready) {
|
|
6588
|
+
document.fonts.ready.then(() => resolve());
|
|
6589
|
+
} else {
|
|
6590
|
+
setTimeout(resolve, 1e3);
|
|
6591
|
+
}
|
|
6592
|
+
});
|
|
6593
|
+
});
|
|
6594
|
+
const pdfBuffer = await page.pdf({
|
|
6595
|
+
width: "1920px",
|
|
6596
|
+
height: "1080px",
|
|
6597
|
+
printBackground: opts.printBackground,
|
|
6598
|
+
scale: opts.scale,
|
|
6599
|
+
landscape: opts.orientation === "landscape",
|
|
6600
|
+
margin: {
|
|
6601
|
+
top: "0px",
|
|
6602
|
+
right: "0px",
|
|
6603
|
+
bottom: "0px",
|
|
6604
|
+
left: "0px"
|
|
6605
|
+
}
|
|
6606
|
+
});
|
|
6607
|
+
logger.step("PDF generated successfully");
|
|
6608
|
+
return Buffer.from(pdfBuffer);
|
|
6609
|
+
} catch (error) {
|
|
6610
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6611
|
+
logger.error(`PDF generation failed: ${errorMessage}`);
|
|
6612
|
+
throw new Error(`PDF generation failed: ${errorMessage}`);
|
|
6613
|
+
} finally {
|
|
6614
|
+
if (browser) {
|
|
6615
|
+
await browser.close();
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
6619
|
+
/**
|
|
6620
|
+
* Prepare HTML for print/PDF output
|
|
6621
|
+
* Modifies the HTML to work better as a printed document
|
|
6622
|
+
*/
|
|
6623
|
+
preparePrintHtml(html) {
|
|
6624
|
+
const printCss = `
|
|
6625
|
+
<style id="pdf-print-styles">
|
|
6626
|
+
/* Print-specific overrides */
|
|
6627
|
+
@page {
|
|
6628
|
+
size: 1920px 1080px;
|
|
6629
|
+
margin: 0;
|
|
6630
|
+
}
|
|
6631
|
+
|
|
6632
|
+
@media print {
|
|
6633
|
+
html, body {
|
|
6634
|
+
margin: 0;
|
|
6635
|
+
padding: 0;
|
|
6636
|
+
width: 1920px;
|
|
6637
|
+
height: 1080px;
|
|
6638
|
+
}
|
|
6639
|
+
|
|
6640
|
+
.reveal .slides {
|
|
6641
|
+
width: 100% !important;
|
|
6642
|
+
height: 100% !important;
|
|
6643
|
+
margin: 0 !important;
|
|
6644
|
+
padding: 0 !important;
|
|
6645
|
+
transform: none !important;
|
|
6646
|
+
}
|
|
6647
|
+
|
|
6648
|
+
.reveal .slides section {
|
|
6649
|
+
page-break-after: always;
|
|
6650
|
+
page-break-inside: avoid;
|
|
6651
|
+
width: 1920px !important;
|
|
6652
|
+
height: 1080px !important;
|
|
6653
|
+
margin: 0 !important;
|
|
6654
|
+
padding: 60px !important;
|
|
6655
|
+
box-sizing: border-box;
|
|
6656
|
+
position: relative !important;
|
|
6657
|
+
top: auto !important;
|
|
6658
|
+
left: auto !important;
|
|
6659
|
+
transform: none !important;
|
|
6660
|
+
display: flex !important;
|
|
6661
|
+
opacity: 1 !important;
|
|
6662
|
+
visibility: visible !important;
|
|
6663
|
+
}
|
|
6664
|
+
|
|
6665
|
+
.reveal .slides section:last-child {
|
|
6666
|
+
page-break-after: auto;
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
/* Make all slides visible for printing */
|
|
6670
|
+
.reveal .slides section.future,
|
|
6671
|
+
.reveal .slides section.past {
|
|
6672
|
+
display: flex !important;
|
|
6673
|
+
opacity: 1 !important;
|
|
6674
|
+
visibility: visible !important;
|
|
6675
|
+
}
|
|
6676
|
+
|
|
6677
|
+
/* Disable animations for print */
|
|
6678
|
+
* {
|
|
6679
|
+
animation: none !important;
|
|
6680
|
+
transition: none !important;
|
|
6681
|
+
}
|
|
6682
|
+
|
|
6683
|
+
/* Hide Reveal.js controls */
|
|
6684
|
+
.reveal .controls,
|
|
6685
|
+
.reveal .progress,
|
|
6686
|
+
.reveal .slide-number,
|
|
6687
|
+
.reveal .speaker-notes,
|
|
6688
|
+
.reveal .navigate-left,
|
|
6689
|
+
.reveal .navigate-right,
|
|
6690
|
+
.reveal .navigate-up,
|
|
6691
|
+
.reveal .navigate-down {
|
|
6692
|
+
display: none !important;
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
|
|
6696
|
+
/* Force print mode in Puppeteer */
|
|
6697
|
+
.reveal .slides section {
|
|
6698
|
+
page-break-after: always;
|
|
6699
|
+
page-break-inside: avoid;
|
|
6700
|
+
}
|
|
6701
|
+
</style>
|
|
6702
|
+
`;
|
|
6703
|
+
if (html.includes("</head>")) {
|
|
6704
|
+
return html.replace("</head>", `${printCss}</head>`);
|
|
6705
|
+
} else {
|
|
6706
|
+
return printCss + html;
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
};
|
|
6710
|
+
function createPDFGenerator() {
|
|
6711
|
+
return new PDFGenerator();
|
|
6712
|
+
}
|
|
6713
|
+
|
|
4236
6714
|
// src/core/PresentationEngine.ts
|
|
4237
6715
|
var PresentationEngine = class {
|
|
4238
6716
|
contentAnalyzer;
|
|
4239
|
-
slideFactory;
|
|
4240
6717
|
templateEngine;
|
|
4241
6718
|
scoreCalculator;
|
|
4242
6719
|
qaEngine;
|
|
4243
6720
|
htmlGenerator;
|
|
4244
6721
|
pptxGenerator;
|
|
6722
|
+
pdfGenerator;
|
|
6723
|
+
kb;
|
|
4245
6724
|
constructor() {
|
|
4246
6725
|
this.contentAnalyzer = new ContentAnalyzer();
|
|
4247
|
-
this.slideFactory = new SlideFactory();
|
|
4248
6726
|
this.templateEngine = new TemplateEngine();
|
|
4249
6727
|
this.scoreCalculator = new ScoreCalculator();
|
|
4250
6728
|
this.qaEngine = new QAEngine();
|
|
4251
6729
|
this.htmlGenerator = new RevealJsGenerator();
|
|
4252
6730
|
this.pptxGenerator = new PowerPointGenerator();
|
|
6731
|
+
this.pdfGenerator = createPDFGenerator();
|
|
4253
6732
|
}
|
|
4254
6733
|
/**
|
|
4255
6734
|
* Generate a presentation from content.
|
|
@@ -4259,21 +6738,26 @@ var PresentationEngine = class {
|
|
|
4259
6738
|
*/
|
|
4260
6739
|
async generate(config) {
|
|
4261
6740
|
this.validateConfig(config);
|
|
4262
|
-
|
|
6741
|
+
logger.progress("\u{1F4DA} Loading knowledge base...");
|
|
6742
|
+
this.kb = await getKnowledgeGateway();
|
|
6743
|
+
logger.progress("\u{1F4DD} Analyzing content...");
|
|
4263
6744
|
const analysis = await this.contentAnalyzer.analyze(config.content, config.contentType);
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
6745
|
+
const presentationType = config.presentationType || analysis.detectedType;
|
|
6746
|
+
logger.step(`Presentation type: ${presentationType}`);
|
|
6747
|
+
const slideFactory = createSlideFactory(this.kb, presentationType);
|
|
6748
|
+
logger.progress("\u{1F3A8} Creating slides...");
|
|
6749
|
+
const slides = await slideFactory.createSlides(analysis);
|
|
6750
|
+
logger.progress("\u2705 Validating structure...");
|
|
4267
6751
|
const structureErrors = this.validateStructure(slides, config.mode);
|
|
4268
6752
|
if (structureErrors.length > 0) {
|
|
4269
6753
|
if (config.skipQA) {
|
|
4270
|
-
|
|
4271
|
-
structureErrors.forEach((e) =>
|
|
6754
|
+
logger.warn("Structure warnings (bypassed):");
|
|
6755
|
+
structureErrors.forEach((e) => logger.warn(` \u2022 ${e}`));
|
|
4272
6756
|
} else {
|
|
4273
6757
|
throw new ValidationError(structureErrors, "Slide structure validation failed");
|
|
4274
6758
|
}
|
|
4275
6759
|
}
|
|
4276
|
-
|
|
6760
|
+
logger.progress("\u{1F528} Generating outputs...");
|
|
4277
6761
|
const outputs = {};
|
|
4278
6762
|
if (config.format.includes("html")) {
|
|
4279
6763
|
outputs.html = await this.htmlGenerator.generate(slides, config);
|
|
@@ -4283,23 +6767,62 @@ var PresentationEngine = class {
|
|
|
4283
6767
|
}
|
|
4284
6768
|
let qaResults;
|
|
4285
6769
|
let score = 100;
|
|
6770
|
+
let iterativeResult = null;
|
|
6771
|
+
let finalSlides = slides;
|
|
6772
|
+
let finalHtml = outputs.html;
|
|
4286
6773
|
if (!config.skipQA && outputs.html) {
|
|
4287
|
-
console.log("\u{1F50D} Running QA validation...");
|
|
4288
|
-
qaResults = await this.qaEngine.validate(outputs.html, {
|
|
4289
|
-
mode: config.mode,
|
|
4290
|
-
strictMode: true
|
|
4291
|
-
});
|
|
4292
|
-
score = this.scoreCalculator.calculate(qaResults);
|
|
4293
|
-
console.log(`\u{1F4CA} QA Score: ${score}/100`);
|
|
4294
6774
|
const threshold = config.qaThreshold ?? 95;
|
|
4295
|
-
|
|
4296
|
-
|
|
6775
|
+
const maxIterations = config.maxIterations ?? 5;
|
|
6776
|
+
const useIterativeQA = config.useIterativeQA !== false;
|
|
6777
|
+
if (useIterativeQA) {
|
|
6778
|
+
logger.progress("\u{1F50D} Running Iterative QA (7-Dimension Scoring)...");
|
|
6779
|
+
const iterativeEngine = createIterativeQAEngine(
|
|
6780
|
+
config.mode,
|
|
6781
|
+
presentationType,
|
|
6782
|
+
config
|
|
6783
|
+
);
|
|
6784
|
+
iterativeResult = await iterativeEngine.run(slides, outputs.html, {
|
|
6785
|
+
minScore: threshold,
|
|
6786
|
+
maxIterations,
|
|
6787
|
+
verbose: true
|
|
6788
|
+
});
|
|
6789
|
+
score = iterativeResult.finalScore;
|
|
6790
|
+
finalSlides = iterativeResult.slides;
|
|
6791
|
+
finalHtml = iterativeResult.html;
|
|
6792
|
+
if (outputs.html) {
|
|
6793
|
+
outputs.html = finalHtml;
|
|
6794
|
+
}
|
|
6795
|
+
qaResults = this.buildQAResultsFrom7Dimension(iterativeResult);
|
|
6796
|
+
if (!iterativeResult.passed) {
|
|
6797
|
+
throw new QAFailureError(score, threshold, qaResults);
|
|
6798
|
+
}
|
|
6799
|
+
} else {
|
|
6800
|
+
logger.progress("\u{1F50D} Running QA validation (legacy mode)...");
|
|
6801
|
+
qaResults = await this.qaEngine.validate(outputs.html, {
|
|
6802
|
+
mode: config.mode,
|
|
6803
|
+
strictMode: true
|
|
6804
|
+
});
|
|
6805
|
+
score = this.scoreCalculator.calculate(qaResults);
|
|
6806
|
+
logger.info(`\u{1F4CA} QA Score: ${score}/100`);
|
|
6807
|
+
if (score < threshold) {
|
|
6808
|
+
throw new QAFailureError(score, threshold, qaResults);
|
|
6809
|
+
}
|
|
4297
6810
|
}
|
|
4298
6811
|
} else {
|
|
4299
6812
|
qaResults = this.qaEngine.createEmptyResults();
|
|
4300
|
-
|
|
6813
|
+
logger.warn("QA validation skipped (NOT RECOMMENDED)");
|
|
6814
|
+
}
|
|
6815
|
+
if (outputs.html && (config.format.includes("pdf") || config.generatePdf !== false)) {
|
|
6816
|
+
try {
|
|
6817
|
+
logger.progress("\u{1F4C4} Generating PDF from validated HTML...");
|
|
6818
|
+
outputs.pdf = await this.pdfGenerator.generate(outputs.html);
|
|
6819
|
+
logger.step("PDF generated successfully");
|
|
6820
|
+
} catch (pdfError) {
|
|
6821
|
+
const errorMsg = pdfError instanceof Error ? pdfError.message : String(pdfError);
|
|
6822
|
+
logger.warn(`PDF generation failed (non-critical): ${errorMsg}`);
|
|
6823
|
+
}
|
|
4301
6824
|
}
|
|
4302
|
-
const metadata = this.buildMetadata(config, analysis,
|
|
6825
|
+
const metadata = this.buildMetadata(config, analysis, finalSlides, iterativeResult);
|
|
4303
6826
|
return {
|
|
4304
6827
|
outputs,
|
|
4305
6828
|
qaResults,
|
|
@@ -4307,6 +6830,46 @@ var PresentationEngine = class {
|
|
|
4307
6830
|
metadata
|
|
4308
6831
|
};
|
|
4309
6832
|
}
|
|
6833
|
+
/**
|
|
6834
|
+
* Build QA results structure from 7-dimension scoring.
|
|
6835
|
+
*/
|
|
6836
|
+
buildQAResultsFrom7Dimension(iterativeResult) {
|
|
6837
|
+
const scoring = iterativeResult.finalScoring;
|
|
6838
|
+
return {
|
|
6839
|
+
passed: scoring.passed,
|
|
6840
|
+
score: scoring.overallScore,
|
|
6841
|
+
visual: {
|
|
6842
|
+
whitespacePercentage: scoring.dimensions.layout.score,
|
|
6843
|
+
layoutBalance: scoring.dimensions.layout.score / 100,
|
|
6844
|
+
colorContrast: scoring.dimensions.contrast.score / 100
|
|
6845
|
+
},
|
|
6846
|
+
content: {
|
|
6847
|
+
perSlide: [],
|
|
6848
|
+
issues: scoring.issues.filter((i) => i.dimension === "content")
|
|
6849
|
+
},
|
|
6850
|
+
accessibility: {
|
|
6851
|
+
wcagLevel: scoring.dimensions.contrast.score >= 95 ? "AAA" : "AA",
|
|
6852
|
+
issues: scoring.issues.filter((i) => i.dimension === "contrast")
|
|
6853
|
+
},
|
|
6854
|
+
issues: scoring.issues.map((issue) => ({
|
|
6855
|
+
severity: issue.severity,
|
|
6856
|
+
message: issue.message,
|
|
6857
|
+
slideIndex: issue.slideIndex,
|
|
6858
|
+
dimension: issue.dimension
|
|
6859
|
+
})),
|
|
6860
|
+
dimensions: {
|
|
6861
|
+
layout: scoring.dimensions.layout.score,
|
|
6862
|
+
contrast: scoring.dimensions.contrast.score,
|
|
6863
|
+
graphics: scoring.dimensions.graphics.score,
|
|
6864
|
+
content: scoring.dimensions.content.score,
|
|
6865
|
+
clarity: scoring.dimensions.clarity.score,
|
|
6866
|
+
effectiveness: scoring.dimensions.effectiveness.score,
|
|
6867
|
+
consistency: scoring.dimensions.consistency.score
|
|
6868
|
+
},
|
|
6869
|
+
iterations: iterativeResult.iterations,
|
|
6870
|
+
report: iterativeResult.report
|
|
6871
|
+
};
|
|
6872
|
+
}
|
|
4310
6873
|
/**
|
|
4311
6874
|
* Validate presentation configuration.
|
|
4312
6875
|
*/
|
|
@@ -4345,15 +6908,28 @@ var PresentationEngine = class {
|
|
|
4345
6908
|
if (slides.length < 3) {
|
|
4346
6909
|
errors.push("Presentation must have at least 3 slides");
|
|
4347
6910
|
}
|
|
6911
|
+
const sparseByDesignTypes = [
|
|
6912
|
+
"title",
|
|
6913
|
+
"section-divider",
|
|
6914
|
+
"thank-you",
|
|
6915
|
+
"big-number",
|
|
6916
|
+
"single-statement",
|
|
6917
|
+
"cta",
|
|
6918
|
+
"agenda",
|
|
6919
|
+
"metrics-grid",
|
|
6920
|
+
"quote",
|
|
6921
|
+
"image-focus"
|
|
6922
|
+
];
|
|
4348
6923
|
slides.forEach((slide, index) => {
|
|
4349
6924
|
const wordCount = this.countWords(slide);
|
|
4350
6925
|
if (mode === "keynote") {
|
|
4351
|
-
if (wordCount > 25) {
|
|
6926
|
+
if (wordCount > 25 && !sparseByDesignTypes.includes(slide.type)) {
|
|
4352
6927
|
errors.push(`Slide ${index + 1}: ${wordCount} words exceeds keynote limit of 25`);
|
|
4353
6928
|
}
|
|
4354
6929
|
} else {
|
|
4355
|
-
|
|
4356
|
-
|
|
6930
|
+
const requiresMinWords = ["bullet-points", "two-column", "process-flow"];
|
|
6931
|
+
if (wordCount < 20 && requiresMinWords.includes(slide.type)) {
|
|
6932
|
+
logger.warn(`Slide ${index + 1}: ${wordCount} words is sparse for ${slide.type} slide`);
|
|
4357
6933
|
}
|
|
4358
6934
|
if (wordCount > 100) {
|
|
4359
6935
|
errors.push(`Slide ${index + 1}: ${wordCount} words exceeds business limit of 100`);
|
|
@@ -4377,13 +6953,13 @@ var PresentationEngine = class {
|
|
|
4377
6953
|
/**
|
|
4378
6954
|
* Build presentation metadata.
|
|
4379
6955
|
*/
|
|
4380
|
-
buildMetadata(config, analysis, slides) {
|
|
6956
|
+
buildMetadata(config, analysis, slides, iterativeResult) {
|
|
4381
6957
|
const wordCounts = slides.map((s) => this.countWords(s));
|
|
4382
6958
|
const totalWords = wordCounts.reduce((sum, count) => sum + count, 0);
|
|
4383
6959
|
const avgWordsPerSlide = Math.round(totalWords / slides.length);
|
|
4384
6960
|
const minutesPerSlide = config.mode === "keynote" ? 1.5 : 2;
|
|
4385
6961
|
const estimatedDuration = Math.round(slides.length * minutesPerSlide);
|
|
4386
|
-
|
|
6962
|
+
const metadata = {
|
|
4387
6963
|
title: config.title,
|
|
4388
6964
|
author: config.author ?? "Unknown",
|
|
4389
6965
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4392,8 +6968,15 @@ var PresentationEngine = class {
|
|
|
4392
6968
|
wordCount: totalWords,
|
|
4393
6969
|
avgWordsPerSlide,
|
|
4394
6970
|
estimatedDuration,
|
|
4395
|
-
frameworks: this.detectFrameworks(analysis)
|
|
6971
|
+
frameworks: this.detectFrameworks(analysis),
|
|
6972
|
+
presentationType: config.presentationType || analysis.detectedType
|
|
4396
6973
|
};
|
|
6974
|
+
if (iterativeResult) {
|
|
6975
|
+
metadata.qaIterations = iterativeResult.totalIterations;
|
|
6976
|
+
metadata.qaMaxIterations = iterativeResult.maxIterations;
|
|
6977
|
+
metadata.dimensionScores = iterativeResult.finalScoring.dimensions;
|
|
6978
|
+
}
|
|
6979
|
+
return metadata;
|
|
4397
6980
|
}
|
|
4398
6981
|
/**
|
|
4399
6982
|
* Detect which expert frameworks were applied.
|
|
@@ -4416,6 +6999,95 @@ var PresentationEngine = class {
|
|
|
4416
6999
|
}
|
|
4417
7000
|
};
|
|
4418
7001
|
|
|
7002
|
+
// src/core/SlideGenerator.ts
|
|
7003
|
+
var SlideGenerator = class {
|
|
7004
|
+
kb;
|
|
7005
|
+
factory;
|
|
7006
|
+
mode = "keynote";
|
|
7007
|
+
presentationType = "ted_keynote";
|
|
7008
|
+
async initialize() {
|
|
7009
|
+
this.kb = await getKnowledgeGateway();
|
|
7010
|
+
}
|
|
7011
|
+
/**
|
|
7012
|
+
* Generate slides from analyzed content using KB-driven SlideFactory.
|
|
7013
|
+
*
|
|
7014
|
+
* @version 7.0.0 - Now delegates ALL slide creation to SlideFactory
|
|
7015
|
+
* SlideFactory queries KB for every decision:
|
|
7016
|
+
* - Allowed slide types per presentation type
|
|
7017
|
+
* - Word limits per type
|
|
7018
|
+
* - Bullet limits per type
|
|
7019
|
+
* - Content pattern to slide type mapping
|
|
7020
|
+
* - Slide validation against KB rules
|
|
7021
|
+
*/
|
|
7022
|
+
async generate(analysis, type) {
|
|
7023
|
+
await this.initialize();
|
|
7024
|
+
this.presentationType = type || analysis.detectedType;
|
|
7025
|
+
this.mode = this.kb.getModeForType(this.presentationType);
|
|
7026
|
+
logger.step(`Using ${this.mode} mode for ${this.presentationType}`);
|
|
7027
|
+
this.factory = createSlideFactory(this.kb, this.presentationType);
|
|
7028
|
+
const slides = await this.factory.createSlides(analysis);
|
|
7029
|
+
const legacySlides = this.convertToLegacyFormat(slides);
|
|
7030
|
+
logger.step(`Generated ${legacySlides.length} KB-validated slides`);
|
|
7031
|
+
return legacySlides;
|
|
7032
|
+
}
|
|
7033
|
+
/**
|
|
7034
|
+
* Convert new Slide format to legacy format for backwards compatibility
|
|
7035
|
+
*/
|
|
7036
|
+
convertToLegacyFormat(slides) {
|
|
7037
|
+
return slides.map((slide) => ({
|
|
7038
|
+
index: slide.index,
|
|
7039
|
+
type: slide.type,
|
|
7040
|
+
title: slide.data.title || "",
|
|
7041
|
+
content: {
|
|
7042
|
+
subtitle: slide.data.subtitle,
|
|
7043
|
+
statement: slide.data.body,
|
|
7044
|
+
body: slide.data.body,
|
|
7045
|
+
bullets: slide.data.bullets,
|
|
7046
|
+
metrics: slide.data.metrics,
|
|
7047
|
+
quote: slide.data.quote ? { text: slide.data.quote, attribution: slide.data.attribution } : void 0,
|
|
7048
|
+
callToAction: slide.data.keyMessage
|
|
7049
|
+
},
|
|
7050
|
+
notes: slide.notes,
|
|
7051
|
+
template: this.mapTypeToTemplate(slide.type)
|
|
7052
|
+
}));
|
|
7053
|
+
}
|
|
7054
|
+
/**
|
|
7055
|
+
* Map slide type to template name for backwards compatibility
|
|
7056
|
+
*/
|
|
7057
|
+
mapTypeToTemplate(type) {
|
|
7058
|
+
const templateMap = {
|
|
7059
|
+
"title": "Title Impact",
|
|
7060
|
+
"agenda": "Detailed Findings",
|
|
7061
|
+
"big-number": "Data Insight",
|
|
7062
|
+
"metrics-grid": "Data Insight",
|
|
7063
|
+
"bullet-points": "Detailed Findings",
|
|
7064
|
+
"two-column": "Two Column",
|
|
7065
|
+
"three-column": "Three Column",
|
|
7066
|
+
"comparison": "Comparison",
|
|
7067
|
+
"timeline": "Process Timeline",
|
|
7068
|
+
"process": "Process",
|
|
7069
|
+
"quote": "Quote",
|
|
7070
|
+
"single-statement": "Single Statement",
|
|
7071
|
+
"cta": "Call to Action",
|
|
7072
|
+
"thank-you": "Title Impact"
|
|
7073
|
+
};
|
|
7074
|
+
return templateMap[type] || "Detailed Findings";
|
|
7075
|
+
}
|
|
7076
|
+
// =========================================================================
|
|
7077
|
+
// All slide creation is now handled by SlideFactory
|
|
7078
|
+
// Old methods removed in v7.0.0 - KB-driven SlideFactory handles:
|
|
7079
|
+
// - createTitleSlide, createAgendaSlide, createSituationSlide, etc.
|
|
7080
|
+
// - Content pattern classification
|
|
7081
|
+
// - KB-based slide type selection
|
|
7082
|
+
// - Text truncation, cleaning, deduplication
|
|
7083
|
+
// =========================================================================
|
|
7084
|
+
};
|
|
7085
|
+
async function initSlideGenerator() {
|
|
7086
|
+
const generator = new SlideGenerator();
|
|
7087
|
+
await generator.initialize();
|
|
7088
|
+
return generator;
|
|
7089
|
+
}
|
|
7090
|
+
|
|
4419
7091
|
// src/media/ImageProvider.ts
|
|
4420
7092
|
var LocalImageProvider = class {
|
|
4421
7093
|
name = "local";
|
|
@@ -4604,7 +7276,7 @@ async function validate(presentation, options) {
|
|
|
4604
7276
|
score
|
|
4605
7277
|
};
|
|
4606
7278
|
}
|
|
4607
|
-
var VERSION = "
|
|
7279
|
+
var VERSION = "7.1.0";
|
|
4608
7280
|
var index_default = {
|
|
4609
7281
|
generate,
|
|
4610
7282
|
validate,
|
|
@@ -4614,10 +7286,13 @@ var index_default = {
|
|
|
4614
7286
|
};
|
|
4615
7287
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4616
7288
|
0 && (module.exports = {
|
|
7289
|
+
AutoFixEngine,
|
|
4617
7290
|
ChartJsProvider,
|
|
4618
7291
|
CompositeChartProvider,
|
|
4619
7292
|
CompositeImageProvider,
|
|
4620
7293
|
ContentAnalyzer,
|
|
7294
|
+
ContentPatternClassifier,
|
|
7295
|
+
IterativeQAEngine,
|
|
4621
7296
|
KnowledgeGateway,
|
|
4622
7297
|
LocalImageProvider,
|
|
4623
7298
|
MermaidProvider,
|
|
@@ -4629,16 +7304,22 @@ var index_default = {
|
|
|
4629
7304
|
QuickChartProvider,
|
|
4630
7305
|
RevealJsGenerator,
|
|
4631
7306
|
ScoreCalculator,
|
|
7307
|
+
SevenDimensionScorer,
|
|
4632
7308
|
SlideFactory,
|
|
7309
|
+
SlideGenerator,
|
|
4633
7310
|
TemplateEngine,
|
|
4634
7311
|
TemplateNotFoundError,
|
|
4635
7312
|
UnsplashImageProvider,
|
|
4636
7313
|
VERSION,
|
|
4637
7314
|
ValidationError,
|
|
7315
|
+
createAutoFixEngine,
|
|
4638
7316
|
createDefaultChartProvider,
|
|
4639
7317
|
createDefaultImageProvider,
|
|
7318
|
+
createIterativeQAEngine,
|
|
7319
|
+
createSlideFactory,
|
|
4640
7320
|
generate,
|
|
4641
7321
|
getKnowledgeGateway,
|
|
7322
|
+
initSlideGenerator,
|
|
4642
7323
|
validate
|
|
4643
7324
|
});
|
|
4644
7325
|
/**
|