claude-presentation-master 6.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -562
- package/bin/auto-fix-presentation.cjs +446 -0
- package/bin/cli.js +95 -32
- package/bin/qa-presentation.cjs +279 -0
- package/bin/score-presentation.cjs +488 -0
- package/dist/index.d.mts +744 -66
- package/dist/index.d.ts +744 -66
- package/dist/index.js +2745 -549
- package/dist/index.mjs +2736 -549
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -287,6 +287,439 @@ var KnowledgeGateway = class {
|
|
|
287
287
|
this.ensureLoaded();
|
|
288
288
|
return this.kb;
|
|
289
289
|
}
|
|
290
|
+
// ==========================================================================
|
|
291
|
+
// KB-DRIVEN SLIDEFACTORY METHODS (v7.0.0)
|
|
292
|
+
// ==========================================================================
|
|
293
|
+
/**
|
|
294
|
+
* Get allowed slide types for a specific presentation type.
|
|
295
|
+
* CRITICAL: SlideFactory must ONLY use types from this list.
|
|
296
|
+
*/
|
|
297
|
+
getAllowedSlideTypes(type) {
|
|
298
|
+
this.ensureLoaded();
|
|
299
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
300
|
+
if (!typeConfig?.slide_types_allowed) {
|
|
301
|
+
const mode = this.getModeForType(type);
|
|
302
|
+
const templates = this.getSlideTemplates(mode);
|
|
303
|
+
return templates.map((t) => t.name.toLowerCase().replace(/\s+/g, "_"));
|
|
304
|
+
}
|
|
305
|
+
return typeConfig.slide_types_allowed;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get validation rules for a specific presentation type.
|
|
309
|
+
* Returns word limits, bullet limits, whitespace requirements, etc.
|
|
310
|
+
*/
|
|
311
|
+
getValidationRules(type) {
|
|
312
|
+
this.ensureLoaded();
|
|
313
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
314
|
+
if (!typeConfig?.validation_rules) {
|
|
315
|
+
const mode = this.getModeForType(type);
|
|
316
|
+
return mode === "keynote" ? {
|
|
317
|
+
wordsPerSlide: { min: 1, max: 25, ideal: 10 },
|
|
318
|
+
whitespace: { min: 40, ideal: 50 },
|
|
319
|
+
bulletsPerSlide: { max: 3 },
|
|
320
|
+
actionTitlesRequired: false,
|
|
321
|
+
sourcesRequired: false
|
|
322
|
+
} : {
|
|
323
|
+
wordsPerSlide: { min: 40, max: 80, ideal: 60 },
|
|
324
|
+
whitespace: { min: 25, ideal: 30 },
|
|
325
|
+
bulletsPerSlide: { max: 5 },
|
|
326
|
+
actionTitlesRequired: true,
|
|
327
|
+
sourcesRequired: true
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const rules = typeConfig.validation_rules;
|
|
331
|
+
return {
|
|
332
|
+
wordsPerSlide: {
|
|
333
|
+
min: rules.words_per_slide?.min ?? 1,
|
|
334
|
+
max: rules.words_per_slide?.max ?? 80,
|
|
335
|
+
ideal: rules.words_per_slide?.ideal ?? 40
|
|
336
|
+
},
|
|
337
|
+
whitespace: {
|
|
338
|
+
min: rules.whitespace?.min ?? 25,
|
|
339
|
+
ideal: rules.whitespace?.ideal ?? 30
|
|
340
|
+
},
|
|
341
|
+
bulletsPerSlide: {
|
|
342
|
+
max: rules.bullets_per_slide?.max ?? 5
|
|
343
|
+
},
|
|
344
|
+
actionTitlesRequired: rules.action_titles_required ?? false,
|
|
345
|
+
sourcesRequired: rules.sources_required ?? false,
|
|
346
|
+
calloutsRequired: rules.callouts_required,
|
|
347
|
+
denseDataAllowed: rules.dense_data_allowed,
|
|
348
|
+
codeBlocksAllowed: rules.code_blocks_allowed,
|
|
349
|
+
diagramsRequired: rules.diagrams_required
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get required elements that MUST be in the deck for this type.
|
|
354
|
+
*/
|
|
355
|
+
getRequiredElements(type) {
|
|
356
|
+
this.ensureLoaded();
|
|
357
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
358
|
+
return typeConfig?.required_elements ?? [];
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get anti-patterns to avoid for this presentation type.
|
|
362
|
+
*/
|
|
363
|
+
getAntiPatternsForType(type) {
|
|
364
|
+
this.ensureLoaded();
|
|
365
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
366
|
+
return typeConfig?.anti_patterns ?? [];
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Get typography specifications for this presentation type.
|
|
370
|
+
*/
|
|
371
|
+
getTypographyForType(type) {
|
|
372
|
+
this.ensureLoaded();
|
|
373
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
374
|
+
const typo = typeConfig?.typography;
|
|
375
|
+
if (!typo) {
|
|
376
|
+
const mode = this.getModeForType(type);
|
|
377
|
+
const modeTypo = this.getTypography(mode);
|
|
378
|
+
const maxFontsValue = modeTypo.max_fonts;
|
|
379
|
+
return {
|
|
380
|
+
titles: modeTypo.titles ?? "48px, Bold",
|
|
381
|
+
body: modeTypo.body_text ?? "24px",
|
|
382
|
+
maxFonts: typeof maxFontsValue === "number" ? maxFontsValue : 2
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
titles: typo.titles ?? "48px, Bold",
|
|
387
|
+
body: typo.body ?? "24px",
|
|
388
|
+
maxFonts: typeof typo.max_fonts === "number" ? typo.max_fonts : 2,
|
|
389
|
+
actionTitle: typo.action_title,
|
|
390
|
+
sectionHeaders: typo.section_headers,
|
|
391
|
+
dataLabels: typo.data_labels,
|
|
392
|
+
code: typo.code
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get CSS variables recipe for this presentation type.
|
|
397
|
+
*/
|
|
398
|
+
getCSSVariablesForType(type) {
|
|
399
|
+
this.ensureLoaded();
|
|
400
|
+
const recipes = this.kb.type_visual_recipes;
|
|
401
|
+
if (recipes?.[type]?.css_variables) {
|
|
402
|
+
return recipes[type].css_variables;
|
|
403
|
+
}
|
|
404
|
+
return `:root {
|
|
405
|
+
--color-background: #FAFAF9;
|
|
406
|
+
--color-primary: #0F172A;
|
|
407
|
+
--color-secondary: #475569;
|
|
408
|
+
--color-accent: #0369A1;
|
|
409
|
+
--color-text: #18181B;
|
|
410
|
+
--font-display: 'Inter', sans-serif;
|
|
411
|
+
--font-body: 'Inter', sans-serif;
|
|
412
|
+
}`;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get scoring weights for QA evaluation for this type.
|
|
416
|
+
*/
|
|
417
|
+
getScoringWeights(type) {
|
|
418
|
+
this.ensureLoaded();
|
|
419
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
420
|
+
const weights = typeConfig?.scoring_weights;
|
|
421
|
+
if (!weights) {
|
|
422
|
+
return {
|
|
423
|
+
visualQuality: 30,
|
|
424
|
+
contentQuality: 30,
|
|
425
|
+
expertCompliance: 30,
|
|
426
|
+
accessibility: 10
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
visualQuality: weights.visual_quality ?? 30,
|
|
431
|
+
contentQuality: weights.content_quality ?? 30,
|
|
432
|
+
expertCompliance: weights.expert_compliance ?? 30,
|
|
433
|
+
accessibility: weights.accessibility ?? 10
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Get story structure framework for this presentation type.
|
|
438
|
+
*/
|
|
439
|
+
getStoryStructure(type) {
|
|
440
|
+
this.ensureLoaded();
|
|
441
|
+
const typeConfig = this.kb.presentation_types?.[type];
|
|
442
|
+
const mode = this.getModeForType(type);
|
|
443
|
+
if (mode === "business") {
|
|
444
|
+
return {
|
|
445
|
+
framework: "scqa",
|
|
446
|
+
requiredElements: ["situation", "complication", "answer"],
|
|
447
|
+
optionalElements: ["question", "evidence", "recommendation"]
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
framework: "sparkline",
|
|
452
|
+
requiredElements: ["what_is", "what_could_be", "call_to_action"],
|
|
453
|
+
optionalElements: ["star_moment", "hook"]
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Map a content pattern to the best allowed slide type.
|
|
458
|
+
* CRITICAL: This is how SlideFactory decides which slide type to use.
|
|
459
|
+
*/
|
|
460
|
+
mapContentPatternToSlideType(pattern, allowedTypes) {
|
|
461
|
+
const patternToTypes = {
|
|
462
|
+
big_number: ["big_number", "big-number", "data_insight", "metrics_grid", "metrics-grid"],
|
|
463
|
+
comparison: ["comparison", "options_comparison", "two_column", "two-column"],
|
|
464
|
+
timeline: ["timeline", "process_timeline", "process", "roadmap"],
|
|
465
|
+
process: ["process", "process_timeline", "timeline", "three_column", "three-column"],
|
|
466
|
+
metrics: ["metrics_grid", "metrics-grid", "data_insight", "big_number", "big-number"],
|
|
467
|
+
quote: ["quote", "testimonial", "social_proof", "social-proof"],
|
|
468
|
+
code: ["code_snippet", "technical", "two_column", "two-column"],
|
|
469
|
+
bullets: ["bullet_points", "bullet-points", "detailed_findings", "two_column", "two-column"],
|
|
470
|
+
prose: ["two_column", "two-column", "bullet_points", "bullet-points", "single_statement"]
|
|
471
|
+
};
|
|
472
|
+
const preferredTypes = patternToTypes[pattern.primaryPattern] || patternToTypes.prose || [];
|
|
473
|
+
for (const preferred of preferredTypes) {
|
|
474
|
+
const underscoreVersion = preferred.replace(/-/g, "_");
|
|
475
|
+
const dashVersion = preferred.replace(/_/g, "-");
|
|
476
|
+
if (allowedTypes.includes(preferred)) return preferred;
|
|
477
|
+
if (allowedTypes.includes(underscoreVersion)) return underscoreVersion;
|
|
478
|
+
if (allowedTypes.includes(dashVersion)) return dashVersion;
|
|
479
|
+
}
|
|
480
|
+
const contentTypes = [
|
|
481
|
+
"bullet_points",
|
|
482
|
+
"bullet-points",
|
|
483
|
+
"two_column",
|
|
484
|
+
"two-column",
|
|
485
|
+
"three_column",
|
|
486
|
+
"three-column",
|
|
487
|
+
"data_insight"
|
|
488
|
+
];
|
|
489
|
+
for (const ct of contentTypes) {
|
|
490
|
+
if (allowedTypes.includes(ct)) return ct;
|
|
491
|
+
}
|
|
492
|
+
return allowedTypes[0] ?? "bullet-points";
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Get a specific slide template by name from the KB.
|
|
496
|
+
*/
|
|
497
|
+
getSlideTemplateByName(name) {
|
|
498
|
+
this.ensureLoaded();
|
|
499
|
+
const keynoteTemplates = this.kb.slide_templates?.keynote_mode ?? [];
|
|
500
|
+
const businessTemplates = this.kb.slide_templates?.business_mode ?? [];
|
|
501
|
+
const normalizedName = name.toLowerCase().replace(/[-_]/g, " ");
|
|
502
|
+
for (const template of [...keynoteTemplates, ...businessTemplates]) {
|
|
503
|
+
const templateName = template.name.toLowerCase().replace(/[-_]/g, " ");
|
|
504
|
+
if (templateName.includes(normalizedName) || normalizedName.includes(templateName)) {
|
|
505
|
+
return {
|
|
506
|
+
name: template.name,
|
|
507
|
+
purpose: template.purpose,
|
|
508
|
+
elements: template.elements,
|
|
509
|
+
components: template.components,
|
|
510
|
+
wordLimit: template.word_limit ?? 60
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return void 0;
|
|
515
|
+
}
|
|
516
|
+
// ==========================================================================
|
|
517
|
+
// SLIDE DEFAULTS & LABELS (v7.1.0 - Replace ALL Hardcoded Values)
|
|
518
|
+
// ==========================================================================
|
|
519
|
+
/**
|
|
520
|
+
* Get slide defaults for structural slides (titles, messages, labels).
|
|
521
|
+
* CRITICAL: SlideFactory must use these instead of hardcoded strings.
|
|
522
|
+
*/
|
|
523
|
+
getSlideDefaults(type) {
|
|
524
|
+
this.ensureLoaded();
|
|
525
|
+
const mode = this.getModeForType(type);
|
|
526
|
+
const rules = this.getValidationRules(type);
|
|
527
|
+
const maxWords = rules.wordsPerSlide.max;
|
|
528
|
+
return {
|
|
529
|
+
agenda: { title: mode === "business" ? "Agenda" : "What We'll Cover" },
|
|
530
|
+
thankYou: {
|
|
531
|
+
title: mode === "business" ? "Thank You" : "Thank You",
|
|
532
|
+
subtitle: mode === "business" ? "Questions?" : "Let's Connect"
|
|
533
|
+
},
|
|
534
|
+
cta: {
|
|
535
|
+
title: mode === "business" ? "Next Steps" : "Take Action",
|
|
536
|
+
message: mode === "business" ? "Recommended Actions" : "Ready to Begin?",
|
|
537
|
+
fallback: mode === "business" ? "Contact us to learn more" : "Take the next step"
|
|
538
|
+
},
|
|
539
|
+
metricsGrid: {
|
|
540
|
+
title: mode === "business" ? "Key Metrics" : "The Numbers",
|
|
541
|
+
maxMetrics: 4
|
|
542
|
+
},
|
|
543
|
+
code: {
|
|
544
|
+
label: "Code Example",
|
|
545
|
+
maxChars: 500
|
|
546
|
+
},
|
|
547
|
+
comparison: {
|
|
548
|
+
leftLabel: mode === "business" ? "Current State" : "Before",
|
|
549
|
+
rightLabel: mode === "business" ? "Future State" : "After",
|
|
550
|
+
optionLabels: ["Option A", "Option B", "Option C", "Option D"]
|
|
551
|
+
},
|
|
552
|
+
column: { labelTemplate: "Point {n}" },
|
|
553
|
+
subtitle: { maxWords: Math.min(15, Math.floor(maxWords / 3)) },
|
|
554
|
+
context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
|
|
555
|
+
step: { maxWords: Math.min(20, Math.floor(maxWords / 4)) },
|
|
556
|
+
columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get SCQA framework titles from KB (for business mode).
|
|
561
|
+
*/
|
|
562
|
+
getSCQATitles(type) {
|
|
563
|
+
this.ensureLoaded();
|
|
564
|
+
const mode = this.getModeForType(type);
|
|
565
|
+
if (mode === "business") {
|
|
566
|
+
return {
|
|
567
|
+
situation: "Current Situation",
|
|
568
|
+
complication: "The Challenge",
|
|
569
|
+
question: "The Critical Question",
|
|
570
|
+
answer: "Our Recommendation"
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
situation: "Where We Are",
|
|
575
|
+
complication: "What's At Stake",
|
|
576
|
+
question: "The Question We Face",
|
|
577
|
+
answer: "The Path Forward"
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get Sparkline framework titles from KB (for keynote mode).
|
|
582
|
+
*/
|
|
583
|
+
getSparklineTitles(type) {
|
|
584
|
+
this.ensureLoaded();
|
|
585
|
+
return {
|
|
586
|
+
whatIs: "Where We Are Today",
|
|
587
|
+
whatCouldBe: "What Could Be",
|
|
588
|
+
callToAdventure: "The Call to Adventure",
|
|
589
|
+
newBliss: "The New World"
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Get insight markers for detecting action titles.
|
|
594
|
+
* These are words/phrases that indicate an insight vs a topic.
|
|
595
|
+
*/
|
|
596
|
+
getInsightMarkers() {
|
|
597
|
+
this.ensureLoaded();
|
|
598
|
+
return [
|
|
599
|
+
// Trend indicators
|
|
600
|
+
"increase",
|
|
601
|
+
"decrease",
|
|
602
|
+
"grew",
|
|
603
|
+
"declined",
|
|
604
|
+
"achieve",
|
|
605
|
+
"exceed",
|
|
606
|
+
"improve",
|
|
607
|
+
"reduce",
|
|
608
|
+
"save",
|
|
609
|
+
"gain",
|
|
610
|
+
"lost",
|
|
611
|
+
"doubled",
|
|
612
|
+
"tripled",
|
|
613
|
+
"outperform",
|
|
614
|
+
"underperform",
|
|
615
|
+
"accelerate",
|
|
616
|
+
"decelerate",
|
|
617
|
+
// Quantitative markers
|
|
618
|
+
"%",
|
|
619
|
+
"percent",
|
|
620
|
+
"million",
|
|
621
|
+
"billion",
|
|
622
|
+
"thousand",
|
|
623
|
+
"$",
|
|
624
|
+
"\u20AC",
|
|
625
|
+
"\xA3",
|
|
626
|
+
"ROI",
|
|
627
|
+
"revenue",
|
|
628
|
+
"cost",
|
|
629
|
+
"margin",
|
|
630
|
+
"growth",
|
|
631
|
+
"decline",
|
|
632
|
+
// Causality markers
|
|
633
|
+
"due to",
|
|
634
|
+
"because",
|
|
635
|
+
"resulting in",
|
|
636
|
+
"leading to",
|
|
637
|
+
"enabling",
|
|
638
|
+
"driving",
|
|
639
|
+
"caused by",
|
|
640
|
+
"attributed to",
|
|
641
|
+
"as a result of",
|
|
642
|
+
// Action verbs (Minto Pyramid style)
|
|
643
|
+
"should",
|
|
644
|
+
"must",
|
|
645
|
+
"need to",
|
|
646
|
+
"recommend",
|
|
647
|
+
"propose",
|
|
648
|
+
"suggest",
|
|
649
|
+
"requires",
|
|
650
|
+
"demands",
|
|
651
|
+
"enables",
|
|
652
|
+
"prevents",
|
|
653
|
+
"ensures"
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Get word limit for a specific slide element type.
|
|
658
|
+
*/
|
|
659
|
+
getWordLimitForElement(type, element) {
|
|
660
|
+
const rules = this.getValidationRules(type);
|
|
661
|
+
const maxWords = rules.wordsPerSlide.max;
|
|
662
|
+
const bulletsMax = rules.bulletsPerSlide.max;
|
|
663
|
+
switch (element) {
|
|
664
|
+
case "title":
|
|
665
|
+
return Math.min(15, Math.floor(maxWords / 4));
|
|
666
|
+
case "subtitle":
|
|
667
|
+
return Math.min(15, Math.floor(maxWords / 5));
|
|
668
|
+
case "bullet":
|
|
669
|
+
return Math.floor(maxWords / (bulletsMax || 5));
|
|
670
|
+
case "body":
|
|
671
|
+
return maxWords;
|
|
672
|
+
case "quote":
|
|
673
|
+
return Math.min(40, maxWords);
|
|
674
|
+
case "step":
|
|
675
|
+
return Math.min(20, Math.floor(maxWords / 4));
|
|
676
|
+
default:
|
|
677
|
+
return maxWords;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Validate a slide against KB rules for the given presentation type.
|
|
682
|
+
*/
|
|
683
|
+
validateSlideAgainstKB(slide, type) {
|
|
684
|
+
const rules = this.getValidationRules(type);
|
|
685
|
+
const allowedTypes = this.getAllowedSlideTypes(type);
|
|
686
|
+
const violations = [];
|
|
687
|
+
const warnings = [];
|
|
688
|
+
const normalizedType = slide.type.replace(/-/g, "_");
|
|
689
|
+
const isAllowed = allowedTypes.some(
|
|
690
|
+
(t) => t === slide.type || t === normalizedType || t.replace(/_/g, "-") === slide.type
|
|
691
|
+
);
|
|
692
|
+
if (!isAllowed) {
|
|
693
|
+
violations.push(`Slide type '${slide.type}' not allowed for ${type}. Allowed: ${allowedTypes.join(", ")}`);
|
|
694
|
+
}
|
|
695
|
+
if (slide.wordCount > rules.wordsPerSlide.max) {
|
|
696
|
+
violations.push(`Word count ${slide.wordCount} exceeds max ${rules.wordsPerSlide.max}`);
|
|
697
|
+
} else if (slide.wordCount < rules.wordsPerSlide.min) {
|
|
698
|
+
warnings.push(`Word count ${slide.wordCount} below min ${rules.wordsPerSlide.min}`);
|
|
699
|
+
}
|
|
700
|
+
if (slide.bulletCount > rules.bulletsPerSlide.max) {
|
|
701
|
+
violations.push(`Bullet count ${slide.bulletCount} exceeds max ${rules.bulletsPerSlide.max}`);
|
|
702
|
+
}
|
|
703
|
+
if (rules.actionTitlesRequired && !slide.hasActionTitle) {
|
|
704
|
+
violations.push(`Action title required for ${type} but not present`);
|
|
705
|
+
}
|
|
706
|
+
if (rules.sourcesRequired && !slide.hasSource) {
|
|
707
|
+
warnings.push(`Source citation recommended for ${type}`);
|
|
708
|
+
}
|
|
709
|
+
const fixes = {};
|
|
710
|
+
if (slide.wordCount > rules.wordsPerSlide.max) {
|
|
711
|
+
fixes.wordCount = rules.wordsPerSlide.max;
|
|
712
|
+
}
|
|
713
|
+
if (slide.bulletCount > rules.bulletsPerSlide.max) {
|
|
714
|
+
fixes.bulletCount = rules.bulletsPerSlide.max;
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
valid: violations.length === 0,
|
|
718
|
+
violations,
|
|
719
|
+
warnings,
|
|
720
|
+
fixes
|
|
721
|
+
};
|
|
722
|
+
}
|
|
290
723
|
};
|
|
291
724
|
var gatewayInstance = null;
|
|
292
725
|
async function getKnowledgeGateway() {
|
|
@@ -424,12 +857,22 @@ var ContentAnalyzer = class {
|
|
|
424
857
|
}
|
|
425
858
|
/**
|
|
426
859
|
* Parse content based on type
|
|
860
|
+
* CRITICAL: Strip code blocks FIRST to prevent code from becoming slides
|
|
427
861
|
*/
|
|
428
862
|
parseContent(content, contentType) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
863
|
+
let text = content;
|
|
864
|
+
text = text.replace(/```[\s\S]*?```/g, "");
|
|
865
|
+
text = text.replace(/`[^`]{50,}`/g, "");
|
|
866
|
+
text = text.split("\n").filter((line) => {
|
|
867
|
+
const trimmed = line.trim();
|
|
868
|
+
if (/^(import|export|const|let|var|function|class|interface|type|async|await|return|if|for|while)\s/.test(trimmed)) return false;
|
|
869
|
+
if (/^(\/\/|\/\*|\*|#!)/.test(trimmed)) return false;
|
|
870
|
+
if (/^\s*[{}\[\]();]/.test(trimmed)) return false;
|
|
871
|
+
if (/^[\w.]+\s*\(.*\)\s*;?\s*$/.test(trimmed)) return false;
|
|
872
|
+
if (/^[\w.]+\s*=\s*/.test(trimmed) && /[{(\[]/.test(trimmed)) return false;
|
|
873
|
+
return true;
|
|
874
|
+
}).join("\n");
|
|
875
|
+
return text;
|
|
433
876
|
}
|
|
434
877
|
/**
|
|
435
878
|
* Extract the main title from content
|
|
@@ -547,6 +990,7 @@ var ContentAnalyzer = class {
|
|
|
547
990
|
}
|
|
548
991
|
/**
|
|
549
992
|
* Extract SCQA structure (Barbara Minto)
|
|
993
|
+
* CRITICAL: Answer must be clean prose, NOT bullet points
|
|
550
994
|
*/
|
|
551
995
|
extractSCQA(text) {
|
|
552
996
|
const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
|
|
@@ -554,34 +998,45 @@ var ContentAnalyzer = class {
|
|
|
554
998
|
let complication = "";
|
|
555
999
|
let question = "";
|
|
556
1000
|
let answer = "";
|
|
557
|
-
for (const para of paragraphs.slice(0,
|
|
1001
|
+
for (const para of paragraphs.slice(0, 5)) {
|
|
1002
|
+
if (para.startsWith("-") || para.startsWith("#") || para.startsWith("|")) continue;
|
|
558
1003
|
if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
|
|
559
1004
|
situation = this.extractFirstSentence(para);
|
|
560
1005
|
break;
|
|
561
1006
|
}
|
|
562
1007
|
}
|
|
563
1008
|
for (const para of paragraphs) {
|
|
1009
|
+
if (para.startsWith("-") || para.startsWith("|")) continue;
|
|
564
1010
|
if (this.containsSignals(para.toLowerCase(), this.complicationSignals)) {
|
|
565
1011
|
complication = this.extractFirstSentence(para);
|
|
566
1012
|
break;
|
|
567
1013
|
}
|
|
568
1014
|
}
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
1015
|
+
for (const para of paragraphs) {
|
|
1016
|
+
if (para.startsWith("-") || para.includes("\n-") || para.startsWith("|")) continue;
|
|
1017
|
+
if (para.startsWith("*") && !para.startsWith("**")) continue;
|
|
572
1018
|
const lowerPara = para.toLowerCase();
|
|
573
1019
|
if (this.containsSignals(lowerPara, this.answerSignals) && !this.containsSignals(lowerPara, this.ctaSignals)) {
|
|
574
|
-
|
|
575
|
-
|
|
1020
|
+
const sentence = this.extractFirstSentence(para);
|
|
1021
|
+
if (sentence.split(/\s+/).length >= 8 && /\b(is|are|will|can|provides?|delivers?|enables?)\b/i.test(sentence)) {
|
|
1022
|
+
answer = sentence;
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
576
1025
|
}
|
|
577
1026
|
}
|
|
578
|
-
if (!situation && paragraphs.length > 0
|
|
579
|
-
|
|
1027
|
+
if (!situation && paragraphs.length > 0) {
|
|
1028
|
+
for (const para of paragraphs.slice(0, 5)) {
|
|
1029
|
+
if (!para.startsWith("-") && !para.startsWith("#") && para.length > 50) {
|
|
1030
|
+
situation = this.extractFirstSentence(para);
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
580
1034
|
}
|
|
581
|
-
if (!answer
|
|
582
|
-
for (const para of paragraphs
|
|
1035
|
+
if (!answer) {
|
|
1036
|
+
for (const para of paragraphs) {
|
|
1037
|
+
if (para.startsWith("-") || para.includes("\n-")) continue;
|
|
583
1038
|
const lowerPara = para.toLowerCase();
|
|
584
|
-
if (lowerPara.includes("
|
|
1039
|
+
if (lowerPara.includes("bottom line") || lowerPara.includes("delivers both") || lowerPara.includes("the result") || lowerPara.includes("in summary")) {
|
|
585
1040
|
answer = this.extractFirstSentence(para);
|
|
586
1041
|
break;
|
|
587
1042
|
}
|
|
@@ -740,56 +1195,123 @@ var ContentAnalyzer = class {
|
|
|
740
1195
|
}
|
|
741
1196
|
/**
|
|
742
1197
|
* Extract data points (metrics with values)
|
|
743
|
-
*
|
|
1198
|
+
* REWRITTEN: Proper markdown table parsing and meaningful label extraction
|
|
744
1199
|
*/
|
|
745
1200
|
extractDataPoints(text) {
|
|
746
1201
|
const dataPoints = [];
|
|
747
1202
|
const usedValues = /* @__PURE__ */ new Set();
|
|
748
|
-
const
|
|
749
|
-
if (
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
usedValues.add(valueCell);
|
|
759
|
-
dataPoints.push({ value: valueCell, label: labelCell.slice(0, 40) });
|
|
760
|
-
}
|
|
1203
|
+
const bulletPatterns = text.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?(?:\s*\([^)]+\))?)/gm);
|
|
1204
|
+
if (bulletPatterns) {
|
|
1205
|
+
for (const match of bulletPatterns) {
|
|
1206
|
+
const parsed = match.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?)/);
|
|
1207
|
+
if (parsed && parsed[1] && parsed[2]) {
|
|
1208
|
+
const label = parsed[1].trim();
|
|
1209
|
+
const value = parsed[2].trim();
|
|
1210
|
+
if (!usedValues.has(value) && label.length >= 5) {
|
|
1211
|
+
usedValues.add(value);
|
|
1212
|
+
dataPoints.push({ value, label: this.cleanMetricLabel(label) });
|
|
761
1213
|
}
|
|
762
1214
|
}
|
|
763
1215
|
}
|
|
764
1216
|
}
|
|
765
1217
|
const lines = text.split("\n");
|
|
1218
|
+
let inTable = false;
|
|
1219
|
+
let tableRows = [];
|
|
1220
|
+
let headerRow = [];
|
|
766
1221
|
for (const line of lines) {
|
|
767
|
-
if (line.includes("|"))
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1222
|
+
if (line.includes("|") && line.trim().length > 3) {
|
|
1223
|
+
const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
1224
|
+
if (cells.every((c) => /^[-:]+$/.test(c))) continue;
|
|
1225
|
+
if (!inTable) {
|
|
1226
|
+
headerRow = cells;
|
|
1227
|
+
inTable = true;
|
|
1228
|
+
} else {
|
|
1229
|
+
tableRows.push(cells);
|
|
1230
|
+
}
|
|
1231
|
+
} else if (inTable && tableRows.length > 0) {
|
|
1232
|
+
this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
|
|
1233
|
+
inTable = false;
|
|
1234
|
+
tableRows = [];
|
|
1235
|
+
headerRow = [];
|
|
772
1236
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1237
|
+
}
|
|
1238
|
+
if (tableRows.length > 0) {
|
|
1239
|
+
this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
|
|
1240
|
+
}
|
|
1241
|
+
for (const line of lines) {
|
|
1242
|
+
if (line.includes("|")) continue;
|
|
1243
|
+
if (/```/.test(line)) continue;
|
|
1244
|
+
const percentPattern = line.match(/(\d+(?:\.\d+)?%)\s+(reduction|increase|improvement|growth|decrease)\s+(?:in\s+)?([^.]+)/i);
|
|
1245
|
+
if (percentPattern && percentPattern[1] && percentPattern[3]) {
|
|
1246
|
+
const value = percentPattern[1];
|
|
1247
|
+
if (!usedValues.has(value)) {
|
|
1248
|
+
usedValues.add(value);
|
|
1249
|
+
const action = percentPattern[2] || "change";
|
|
1250
|
+
const subject = percentPattern[3].slice(0, 30).trim();
|
|
1251
|
+
dataPoints.push({ value, label: `${action} in ${subject}` });
|
|
780
1252
|
}
|
|
781
1253
|
}
|
|
782
1254
|
}
|
|
783
1255
|
return dataPoints.slice(0, 4);
|
|
784
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Extract metrics from a parsed markdown table
|
|
1259
|
+
*/
|
|
1260
|
+
extractMetricsFromTable(headers, rows, dataPoints, usedValues) {
|
|
1261
|
+
for (const row of rows) {
|
|
1262
|
+
const label = row[0];
|
|
1263
|
+
if (!label || label.length < 3) continue;
|
|
1264
|
+
for (let i = 1; i < row.length; i++) {
|
|
1265
|
+
const cell = row[i];
|
|
1266
|
+
if (!cell) continue;
|
|
1267
|
+
if (/^\$?[\d,]+\.?\d*[%MBK]?$/.test(cell.replace(/[,\s]/g, "")) || /^\d+\s*(seconds?|minutes?|hours?|ms|gb|mb)$/i.test(cell)) {
|
|
1268
|
+
if (!usedValues.has(cell)) {
|
|
1269
|
+
usedValues.add(cell);
|
|
1270
|
+
const colHeader = headers[i] || "";
|
|
1271
|
+
const fullLabel = colHeader && colHeader !== label ? `${label} (${colHeader})` : label;
|
|
1272
|
+
dataPoints.push({
|
|
1273
|
+
value: cell,
|
|
1274
|
+
label: this.cleanMetricLabel(fullLabel)
|
|
1275
|
+
});
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Clean a metric label to ensure it's complete and meaningful
|
|
1284
|
+
*/
|
|
1285
|
+
cleanMetricLabel(raw) {
|
|
1286
|
+
let label = raw.replace(/\*\*/g, "").replace(/\|/g, " ").replace(/[:\-–—]+$/, "").replace(/\s+/g, " ").trim();
|
|
1287
|
+
if (label.length > 0) {
|
|
1288
|
+
label = label.charAt(0).toUpperCase() + label.slice(1);
|
|
1289
|
+
}
|
|
1290
|
+
if (label.length > 40) {
|
|
1291
|
+
const words = label.split(/\s+/);
|
|
1292
|
+
label = "";
|
|
1293
|
+
for (const word of words) {
|
|
1294
|
+
if ((label + " " + word).length <= 40) {
|
|
1295
|
+
label = label ? label + " " + word : word;
|
|
1296
|
+
} else {
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return label || "Value";
|
|
1302
|
+
}
|
|
785
1303
|
/**
|
|
786
1304
|
* Extract a meaningful label from a line containing a metric
|
|
787
1305
|
*/
|
|
788
1306
|
extractLabelFromLine(line, value) {
|
|
1307
|
+
const colonMatch = line.match(/([^:]+):\s*\$?[\d]/);
|
|
1308
|
+
if (colonMatch && colonMatch[1]) {
|
|
1309
|
+
return this.cleanMetricLabel(colonMatch[1]);
|
|
1310
|
+
}
|
|
789
1311
|
const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
|
|
790
1312
|
const beforeValue = cleaned.split(value)[0] || "";
|
|
791
1313
|
const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
|
|
792
|
-
return words.slice(-4).join(" ")
|
|
1314
|
+
return this.cleanMetricLabel(words.slice(-4).join(" "));
|
|
793
1315
|
}
|
|
794
1316
|
/**
|
|
795
1317
|
* Check if text contains any of the signals
|
|
@@ -816,531 +1338,897 @@ var ContentAnalyzer = class {
|
|
|
816
1338
|
}
|
|
817
1339
|
};
|
|
818
1340
|
|
|
819
|
-
// src/core/
|
|
820
|
-
var
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1341
|
+
// src/core/ContentPatternClassifier.ts
|
|
1342
|
+
var ContentPatternClassifier = class {
|
|
1343
|
+
// Regex patterns for content detection
|
|
1344
|
+
patterns = {
|
|
1345
|
+
// Big numbers: $4.88M, 23%, 10x, 500+, etc.
|
|
1346
|
+
bigNumber: /(\$[\d,.]+[MBK]?|\d+(?:\.\d+)?%|\d+[xX]|\d{2,}(?:\+|,\d{3})+)/,
|
|
1347
|
+
// Comparison language
|
|
1348
|
+
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,
|
|
1349
|
+
// Timeline markers
|
|
1350
|
+
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,
|
|
1351
|
+
// Process/steps language
|
|
1352
|
+
process: /\b(step\s*\d|stage\s*\d|pillar|phase|first|second|third|finally|then|next|workflow|process)\b/i,
|
|
1353
|
+
// Quote patterns
|
|
1354
|
+
quote: /^[""]|[""]\s*[-—–]\s*|^\s*>\s*|[""]$/,
|
|
1355
|
+
// Code patterns
|
|
1356
|
+
code: /```|`[^`]+`|function\s*\(|const\s+\w+\s*=|import\s+{|class\s+\w+|=>\s*{/,
|
|
1357
|
+
// Metric patterns (for detecting multiple metrics)
|
|
1358
|
+
metric: /(\d+(?:\.\d+)?(?:%|[xX]|[MBK])?)\s*[-–:]\s*|\b(?:increased|decreased|grew|improved|reduced)\s+(?:by\s+)?(\d+)/gi
|
|
1359
|
+
};
|
|
826
1360
|
/**
|
|
827
|
-
*
|
|
1361
|
+
* Classify a content section to determine its primary pattern.
|
|
1362
|
+
* Returns a ContentPattern object used by SlideFactory to select slide type.
|
|
828
1363
|
*/
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
1364
|
+
classify(section) {
|
|
1365
|
+
const fullText = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
|
|
1366
|
+
const wordCount = fullText.split(/\s+/).filter((w) => w.length > 0).length;
|
|
1367
|
+
const hasBigNumber = this.patterns.bigNumber.test(fullText);
|
|
1368
|
+
const bigNumberMatch = fullText.match(this.patterns.bigNumber);
|
|
1369
|
+
const hasComparison = this.patterns.comparison.test(fullText);
|
|
1370
|
+
const hasTimeline = this.patterns.timeline.test(fullText);
|
|
1371
|
+
const hasProcess = this.patterns.process.test(section.header) || section.bullets.length >= 3 && this.hasNumberedItems(section.bullets);
|
|
1372
|
+
const hasQuote = this.patterns.quote.test(section.content);
|
|
1373
|
+
const hasCode = this.patterns.code.test(section.content);
|
|
1374
|
+
const metricMatches = fullText.match(this.patterns.metric);
|
|
1375
|
+
const hasMetrics = section.metrics.length >= 3 || metricMatches && metricMatches.length >= 3;
|
|
1376
|
+
let primaryPattern = "prose";
|
|
1377
|
+
if (hasQuote && section.content.length > 20) {
|
|
1378
|
+
primaryPattern = "quote";
|
|
1379
|
+
} else if (hasCode) {
|
|
1380
|
+
primaryPattern = "code";
|
|
1381
|
+
} else if (hasBigNumber && wordCount < 30) {
|
|
1382
|
+
primaryPattern = "big_number";
|
|
1383
|
+
} else if (hasMetrics) {
|
|
1384
|
+
primaryPattern = "metrics";
|
|
1385
|
+
} else if (hasComparison) {
|
|
1386
|
+
primaryPattern = "comparison";
|
|
1387
|
+
} else if (hasTimeline) {
|
|
1388
|
+
primaryPattern = "timeline";
|
|
1389
|
+
} else if (hasProcess && section.bullets.length >= 3) {
|
|
1390
|
+
primaryPattern = "process";
|
|
1391
|
+
} else if (section.bullets.length >= 2) {
|
|
1392
|
+
primaryPattern = "bullets";
|
|
1393
|
+
} else {
|
|
1394
|
+
primaryPattern = "prose";
|
|
834
1395
|
}
|
|
835
|
-
|
|
836
|
-
|
|
1396
|
+
const result = {
|
|
1397
|
+
hasBigNumber,
|
|
1398
|
+
hasComparison,
|
|
1399
|
+
hasTimeline,
|
|
1400
|
+
hasProcess,
|
|
1401
|
+
hasMetrics: !!hasMetrics,
|
|
1402
|
+
hasQuote,
|
|
1403
|
+
hasCode,
|
|
1404
|
+
bulletCount: section.bullets.length,
|
|
1405
|
+
wordCount,
|
|
1406
|
+
primaryPattern
|
|
1407
|
+
};
|
|
1408
|
+
if (bigNumberMatch && bigNumberMatch[1]) {
|
|
1409
|
+
result.bigNumberValue = bigNumberMatch[1];
|
|
1410
|
+
}
|
|
1411
|
+
return result;
|
|
837
1412
|
}
|
|
838
1413
|
/**
|
|
839
|
-
*
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
* 5. Metrics slide - if data points exist
|
|
847
|
-
* 6. Solution slide - SCQA answer
|
|
848
|
-
* 7. STAR moments - only if high-quality (per Duarte)
|
|
849
|
-
* 8. CTA slide - if call to action exists
|
|
850
|
-
* 9. Thank you slide - always last
|
|
851
|
-
*/
|
|
852
|
-
async createSlides(analysis, mode) {
|
|
853
|
-
const slides = [];
|
|
854
|
-
let slideIndex = 0;
|
|
855
|
-
this.usedContent.clear();
|
|
856
|
-
slides.push(this.createTitleSlide(slideIndex++, analysis));
|
|
857
|
-
this.isContentUsed(analysis.titles[0] ?? "");
|
|
858
|
-
const substantiveSections = analysis.sections.filter(
|
|
859
|
-
(s) => s.level === 2 && (s.bullets.length > 0 || s.content.length > 50)
|
|
860
|
-
);
|
|
861
|
-
if (mode === "business" && substantiveSections.length >= 3) {
|
|
862
|
-
slides.push(this.createAgendaSlide(slideIndex++, analysis));
|
|
863
|
-
}
|
|
864
|
-
if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
|
|
865
|
-
slides.push(this.createContextSlide(slideIndex++, analysis, mode));
|
|
866
|
-
}
|
|
867
|
-
if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
|
|
868
|
-
slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
|
|
869
|
-
}
|
|
870
|
-
for (const section of substantiveSections.slice(0, 4)) {
|
|
871
|
-
const headerUsed = this.isContentUsed(section.header);
|
|
872
|
-
const contentUsed = section.content.length > 30 && this.isContentUsed(section.content.slice(0, 80));
|
|
873
|
-
if (!headerUsed && !contentUsed) {
|
|
874
|
-
if (section.bullets.length > 0) {
|
|
875
|
-
slides.push(this.createSectionBulletSlide(slideIndex++, section, mode));
|
|
876
|
-
} else if (section.content.length > 50) {
|
|
877
|
-
slides.push(this.createSectionContentSlide(slideIndex++, section, mode));
|
|
878
|
-
}
|
|
1414
|
+
* Check if bullets appear to be numbered/ordered items
|
|
1415
|
+
*/
|
|
1416
|
+
hasNumberedItems(bullets) {
|
|
1417
|
+
let numberedCount = 0;
|
|
1418
|
+
for (const bullet of bullets) {
|
|
1419
|
+
if (/^\d+[.)]\s|^[a-z][.)]\s|^step\s*\d/i.test(bullet.trim())) {
|
|
1420
|
+
numberedCount++;
|
|
879
1421
|
}
|
|
880
1422
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1423
|
+
return numberedCount >= 2;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Extract the big number value from content for display
|
|
1427
|
+
*/
|
|
1428
|
+
extractBigNumber(content) {
|
|
1429
|
+
const match = content.match(this.patterns.bigNumber);
|
|
1430
|
+
if (!match || !match[1]) return null;
|
|
1431
|
+
const value = match[1];
|
|
1432
|
+
const afterNumber = content.slice(content.indexOf(value) + value.length);
|
|
1433
|
+
const context = afterNumber.trim().slice(0, 60).split(/[.!?]/)[0]?.trim() ?? "";
|
|
1434
|
+
return { value, context };
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Extract comparison elements from content
|
|
1438
|
+
*/
|
|
1439
|
+
extractComparison(section) {
|
|
1440
|
+
const text = section.content;
|
|
1441
|
+
const vsMatch = text.match(/(.{10,50})\s+(?:vs\.?|versus)\s+(.{10,50})/i);
|
|
1442
|
+
if (vsMatch && vsMatch[1] && vsMatch[2]) {
|
|
1443
|
+
return { left: vsMatch[1].trim(), right: vsMatch[2].trim() };
|
|
1444
|
+
}
|
|
1445
|
+
const beforeAfterMatch = text.match(/before[:\s]+(.{10,100})(?:after[:\s]+(.{10,100}))?/i);
|
|
1446
|
+
if (beforeAfterMatch && beforeAfterMatch[1]) {
|
|
1447
|
+
const left = beforeAfterMatch[1].trim();
|
|
1448
|
+
const right = beforeAfterMatch[2] ? beforeAfterMatch[2].trim() : "After";
|
|
1449
|
+
return { left, right };
|
|
1450
|
+
}
|
|
1451
|
+
if (section.bullets.length === 2) {
|
|
1452
|
+
const left = section.bullets[0];
|
|
1453
|
+
const right = section.bullets[1];
|
|
1454
|
+
if (left && right) {
|
|
1455
|
+
return { left, right };
|
|
885
1456
|
}
|
|
886
1457
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
for (const
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Extract timeline/process steps from content
|
|
1462
|
+
*/
|
|
1463
|
+
extractSteps(section) {
|
|
1464
|
+
const steps = [];
|
|
1465
|
+
for (const bullet of section.bullets) {
|
|
1466
|
+
const stepMatch = bullet.match(/^((?:step|phase|stage)\s*\d+|[1-9]\.|\d+\))\s*[-:.]?\s*(.+)/i);
|
|
1467
|
+
if (stepMatch && stepMatch[1] && stepMatch[2]) {
|
|
1468
|
+
steps.push({
|
|
1469
|
+
label: stepMatch[1].trim(),
|
|
1470
|
+
description: stepMatch[2].trim()
|
|
1471
|
+
});
|
|
1472
|
+
} else {
|
|
1473
|
+
steps.push({
|
|
1474
|
+
label: `${steps.length + 1}`,
|
|
1475
|
+
description: bullet
|
|
1476
|
+
});
|
|
899
1477
|
}
|
|
900
1478
|
}
|
|
901
|
-
|
|
902
|
-
slides.push(this.createCTASlide(slideIndex++, analysis, mode));
|
|
903
|
-
}
|
|
904
|
-
slides.push(this.createThankYouSlide(slideIndex++));
|
|
905
|
-
return slides;
|
|
1479
|
+
return steps;
|
|
906
1480
|
}
|
|
907
1481
|
/**
|
|
908
|
-
*
|
|
1482
|
+
* Determine if content is suitable for a three-column layout
|
|
1483
|
+
* (3 pillars, 3 key points, etc.)
|
|
909
1484
|
*/
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
return
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
title: this.truncate(section.header, 60),
|
|
917
|
-
bullets: bullets.map((b) => this.cleanText(b).slice(0, 80))
|
|
918
|
-
},
|
|
919
|
-
classes: ["slide-bullet-points"]
|
|
920
|
-
};
|
|
1485
|
+
isSuitableForThreeColumn(section) {
|
|
1486
|
+
if (section.bullets.length === 3) return true;
|
|
1487
|
+
if (/three|3\s*(pillar|point|key|step)/i.test(section.header)) return true;
|
|
1488
|
+
const threePartPattern = /first[,.].*second[,.].*third/i;
|
|
1489
|
+
if (threePartPattern.test(section.content)) return true;
|
|
1490
|
+
return false;
|
|
921
1491
|
}
|
|
922
1492
|
/**
|
|
923
|
-
*
|
|
1493
|
+
* Check if content should be displayed as a quote slide
|
|
924
1494
|
*/
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
type: "single-statement",
|
|
930
|
-
data: {
|
|
931
|
-
title: this.truncate(this.extractFirstSentence(section.content), 80),
|
|
932
|
-
keyMessage: section.header
|
|
933
|
-
},
|
|
934
|
-
classes: ["slide-single-statement"]
|
|
935
|
-
};
|
|
1495
|
+
isQuoteContent(section) {
|
|
1496
|
+
const content = section.content.trim();
|
|
1497
|
+
if (!this.patterns.quote.test(content)) {
|
|
1498
|
+
return { isQuote: false };
|
|
936
1499
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
body: this.truncate(section.content, 200)
|
|
943
|
-
},
|
|
944
|
-
classes: ["slide-two-column"]
|
|
945
|
-
};
|
|
1500
|
+
const attrMatch = content.match(/[-—–]\s*(.+)$/);
|
|
1501
|
+
if (attrMatch && attrMatch[1]) {
|
|
1502
|
+
return { isQuote: true, attribution: attrMatch[1].trim() };
|
|
1503
|
+
}
|
|
1504
|
+
return { isQuote: true };
|
|
946
1505
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
// src/core/SlideFactory.ts
|
|
1509
|
+
var SlideFactory = class {
|
|
1510
|
+
kb;
|
|
1511
|
+
presentationType;
|
|
1512
|
+
classifier;
|
|
1513
|
+
config;
|
|
1514
|
+
usedContent;
|
|
1515
|
+
usedTitles;
|
|
1516
|
+
constructor(kb, type) {
|
|
1517
|
+
this.kb = kb;
|
|
1518
|
+
this.presentationType = type;
|
|
1519
|
+
this.classifier = new ContentPatternClassifier();
|
|
1520
|
+
this.usedContent = /* @__PURE__ */ new Set();
|
|
1521
|
+
this.usedTitles = /* @__PURE__ */ new Set();
|
|
1522
|
+
this.config = this.loadKBConfig(type);
|
|
1523
|
+
console.log(` \u2713 SlideFactory v7.1.0 initialized for ${type} (${this.config.mode} mode)`);
|
|
1524
|
+
console.log(` \u2713 KB Config loaded: ${this.config.allowedTypes.length} allowed types`);
|
|
954
1525
|
}
|
|
955
1526
|
/**
|
|
956
|
-
*
|
|
1527
|
+
* Load ALL configuration from KB - ZERO hardcoded values after this.
|
|
957
1528
|
*/
|
|
958
|
-
|
|
959
|
-
const metrics = dataPoints.slice(0, 4).map((dp) => ({
|
|
960
|
-
value: dp.value,
|
|
961
|
-
label: this.cleanText(dp.label).slice(0, 40)
|
|
962
|
-
}));
|
|
1529
|
+
loadKBConfig(type) {
|
|
963
1530
|
return {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1531
|
+
defaults: this.kb.getSlideDefaults(type),
|
|
1532
|
+
scqaTitles: this.kb.getSCQATitles(type),
|
|
1533
|
+
sparklineTitles: this.kb.getSparklineTitles(type),
|
|
1534
|
+
rules: this.kb.getValidationRules(type),
|
|
1535
|
+
allowedTypes: this.kb.getAllowedSlideTypes(type),
|
|
1536
|
+
mode: this.kb.getModeForType(type),
|
|
1537
|
+
insightMarkers: this.kb.getInsightMarkers(),
|
|
1538
|
+
glanceTest: this.kb.getDuarteGlanceTest(),
|
|
1539
|
+
millersLaw: this.kb.getMillersLaw(),
|
|
1540
|
+
typography: this.kb.getTypographyForType(type),
|
|
1541
|
+
antiPatterns: this.kb.getAntiPatternsForType(type),
|
|
1542
|
+
requiredElements: this.kb.getRequiredElements(type)
|
|
971
1543
|
};
|
|
972
1544
|
}
|
|
973
1545
|
/**
|
|
974
|
-
* Create
|
|
1546
|
+
* Create all slides from content analysis.
|
|
1547
|
+
* This is the main entry point - orchestrates the entire slide creation process.
|
|
975
1548
|
*/
|
|
1549
|
+
async createSlides(analysis) {
|
|
1550
|
+
const slides = [];
|
|
1551
|
+
const storyStructure = this.kb.getStoryStructure(this.presentationType);
|
|
1552
|
+
console.log(` \u2713 Using ${storyStructure.framework} story framework`);
|
|
1553
|
+
slides.push(this.createTitleSlide(0, analysis));
|
|
1554
|
+
const minSectionsForAgenda = Math.max(3, this.config.rules.bulletsPerSlide.max - 2);
|
|
1555
|
+
if (this.config.mode === "business" && analysis.sections.length >= minSectionsForAgenda) {
|
|
1556
|
+
slides.push(this.createAgendaSlide(slides.length, analysis));
|
|
1557
|
+
}
|
|
1558
|
+
if (storyStructure.framework === "scqa") {
|
|
1559
|
+
this.addSCQASlides(slides, analysis);
|
|
1560
|
+
} else if (storyStructure.framework === "sparkline") {
|
|
1561
|
+
this.addSparklineSlides(slides, analysis);
|
|
1562
|
+
}
|
|
1563
|
+
for (const section of analysis.sections) {
|
|
1564
|
+
const contentKey = this.normalizeKey(section.header);
|
|
1565
|
+
if (this.usedContent.has(contentKey)) continue;
|
|
1566
|
+
this.usedContent.add(contentKey);
|
|
1567
|
+
const pattern = this.classifier.classify(section);
|
|
1568
|
+
const slideType = this.kb.mapContentPatternToSlideType(pattern, this.config.allowedTypes);
|
|
1569
|
+
const slide = this.createSlideByType(slides.length, slideType, section, pattern);
|
|
1570
|
+
if (slide) {
|
|
1571
|
+
slides.push(slide);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const minDataPointsForMetrics = 2;
|
|
1575
|
+
if (analysis.dataPoints.length >= minDataPointsForMetrics) {
|
|
1576
|
+
const hasMetricsSlide = slides.some(
|
|
1577
|
+
(s) => s.type === "metrics-grid" || s.type === "big-number"
|
|
1578
|
+
);
|
|
1579
|
+
if (!hasMetricsSlide) {
|
|
1580
|
+
slides.push(this.createMetricsGridSlide(slides.length, analysis.dataPoints));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (analysis.sparkline?.callToAdventure || analysis.scqa?.answer) {
|
|
1584
|
+
slides.push(this.createCTASlide(slides.length, analysis));
|
|
1585
|
+
}
|
|
1586
|
+
slides.push(this.createThankYouSlide(slides.length));
|
|
1587
|
+
const validatedSlides = this.validateAndFixSlides(slides);
|
|
1588
|
+
this.checkRequiredElements(validatedSlides);
|
|
1589
|
+
this.checkAntiPatterns(validatedSlides);
|
|
1590
|
+
console.log(` \u2713 Created ${validatedSlides.length} validated slides`);
|
|
1591
|
+
return validatedSlides;
|
|
1592
|
+
}
|
|
1593
|
+
// ===========================================================================
|
|
1594
|
+
// SLIDE TYPE ROUTER - Routes to appropriate creator based on KB-selected type
|
|
1595
|
+
// ===========================================================================
|
|
1596
|
+
createSlideByType(index, type, section, pattern) {
|
|
1597
|
+
const normalizedType = type.toLowerCase().replace(/_/g, "-");
|
|
1598
|
+
switch (normalizedType) {
|
|
1599
|
+
case "big-number":
|
|
1600
|
+
case "data-insight":
|
|
1601
|
+
return this.createBigNumberSlide(index, section, pattern);
|
|
1602
|
+
case "comparison":
|
|
1603
|
+
case "options-comparison":
|
|
1604
|
+
return this.createComparisonSlide(index, section);
|
|
1605
|
+
case "timeline":
|
|
1606
|
+
case "process-timeline":
|
|
1607
|
+
case "roadmap":
|
|
1608
|
+
return this.createTimelineSlide(index, section);
|
|
1609
|
+
case "process":
|
|
1610
|
+
return this.createProcessSlide(index, section);
|
|
1611
|
+
case "three-column":
|
|
1612
|
+
return this.createThreeColumnSlide(index, section);
|
|
1613
|
+
case "two-column":
|
|
1614
|
+
return this.createTwoColumnSlide(index, section);
|
|
1615
|
+
case "quote":
|
|
1616
|
+
case "testimonial":
|
|
1617
|
+
case "social-proof":
|
|
1618
|
+
return this.createQuoteSlide(index, section);
|
|
1619
|
+
case "metrics-grid":
|
|
1620
|
+
return this.createMetricsGridSlide(index, section.metrics);
|
|
1621
|
+
case "bullet-points":
|
|
1622
|
+
case "detailed-findings":
|
|
1623
|
+
return this.createBulletSlide(index, section);
|
|
1624
|
+
case "single-statement":
|
|
1625
|
+
case "big-idea":
|
|
1626
|
+
return this.createSingleStatementSlide(index, section);
|
|
1627
|
+
case "code-snippet":
|
|
1628
|
+
case "technical":
|
|
1629
|
+
return this.createCodeSlide(index, section);
|
|
1630
|
+
default:
|
|
1631
|
+
console.log(` \u26A0 Unknown slide type '${type}', using bullet-points`);
|
|
1632
|
+
return this.createBulletSlide(index, section);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
// ===========================================================================
|
|
1636
|
+
// STRUCTURAL SLIDES (title, agenda, thank you) - ALL from KB
|
|
1637
|
+
// ===========================================================================
|
|
976
1638
|
createTitleSlide(index, analysis) {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
subtitle =
|
|
1639
|
+
const data = {
|
|
1640
|
+
title: this.truncateText(analysis.title, this.config.rules.wordsPerSlide.max)
|
|
1641
|
+
};
|
|
1642
|
+
if (analysis.keyMessages[0]) {
|
|
1643
|
+
data.subtitle = this.truncateText(
|
|
1644
|
+
analysis.keyMessages[0],
|
|
1645
|
+
this.config.defaults.subtitle.maxWords
|
|
1646
|
+
// FROM KB
|
|
1647
|
+
);
|
|
982
1648
|
}
|
|
983
1649
|
return {
|
|
984
1650
|
index,
|
|
985
1651
|
type: "title",
|
|
986
|
-
data
|
|
987
|
-
|
|
988
|
-
subtitle: this.truncate(subtitle, 80),
|
|
989
|
-
keyMessage: analysis.scqa.answer
|
|
990
|
-
},
|
|
991
|
-
classes: ["slide-title"]
|
|
1652
|
+
data,
|
|
1653
|
+
classes: ["title-slide"]
|
|
992
1654
|
};
|
|
993
1655
|
}
|
|
994
|
-
/**
|
|
995
|
-
* Create an agenda slide.
|
|
996
|
-
*/
|
|
997
1656
|
createAgendaSlide(index, analysis) {
|
|
1657
|
+
const agendaItems = analysis.sections.filter((s) => s.level <= 2).slice(0, this.config.rules.bulletsPerSlide.max).map((s) => this.cleanText(s.header));
|
|
998
1658
|
return {
|
|
999
1659
|
index,
|
|
1000
1660
|
type: "agenda",
|
|
1001
1661
|
data: {
|
|
1002
|
-
title:
|
|
1003
|
-
|
|
1662
|
+
title: this.config.defaults.agenda.title,
|
|
1663
|
+
// FROM KB - not hardcoded 'Agenda'
|
|
1664
|
+
bullets: agendaItems
|
|
1004
1665
|
},
|
|
1005
|
-
classes: ["slide
|
|
1666
|
+
classes: ["agenda-slide"]
|
|
1006
1667
|
};
|
|
1007
1668
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1669
|
+
createThankYouSlide(index) {
|
|
1670
|
+
return {
|
|
1671
|
+
index,
|
|
1672
|
+
type: "thank-you",
|
|
1673
|
+
data: {
|
|
1674
|
+
title: this.config.defaults.thankYou.title,
|
|
1675
|
+
// FROM KB - not hardcoded 'Thank You'
|
|
1676
|
+
subtitle: this.config.defaults.thankYou.subtitle
|
|
1677
|
+
// FROM KB - not hardcoded 'Questions?'
|
|
1678
|
+
},
|
|
1679
|
+
classes: ["thank-you-slide"]
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
// ===========================================================================
|
|
1683
|
+
// STORY FRAMEWORK SLIDES (SCQA, Sparkline) - ALL titles from KB
|
|
1684
|
+
// ===========================================================================
|
|
1685
|
+
addSCQASlides(slides, analysis) {
|
|
1686
|
+
const scqa = analysis.scqa;
|
|
1687
|
+
const titles = this.config.scqaTitles;
|
|
1688
|
+
if (scqa?.situation && !this.usedContent.has("scqa-situation")) {
|
|
1689
|
+
this.usedContent.add("scqa-situation");
|
|
1690
|
+
slides.push({
|
|
1691
|
+
index: slides.length,
|
|
1015
1692
|
type: "single-statement",
|
|
1016
1693
|
data: {
|
|
1017
|
-
title:
|
|
1018
|
-
|
|
1694
|
+
title: titles.situation,
|
|
1695
|
+
// FROM KB - not hardcoded 'Current Situation'
|
|
1696
|
+
body: this.truncateText(scqa.situation, this.config.rules.wordsPerSlide.max)
|
|
1019
1697
|
},
|
|
1020
|
-
classes: ["slide
|
|
1021
|
-
};
|
|
1698
|
+
classes: ["situation-slide"]
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
if (scqa?.complication && !this.usedContent.has("scqa-complication")) {
|
|
1702
|
+
this.usedContent.add("scqa-complication");
|
|
1703
|
+
slides.push({
|
|
1704
|
+
index: slides.length,
|
|
1705
|
+
type: "single-statement",
|
|
1706
|
+
data: {
|
|
1707
|
+
title: titles.complication,
|
|
1708
|
+
// FROM KB - not hardcoded 'The Challenge'
|
|
1709
|
+
body: this.truncateText(scqa.complication, this.config.rules.wordsPerSlide.max)
|
|
1710
|
+
},
|
|
1711
|
+
classes: ["complication-slide"]
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
if (scqa?.question && !this.usedContent.has("scqa-question")) {
|
|
1715
|
+
this.usedContent.add("scqa-question");
|
|
1716
|
+
slides.push({
|
|
1717
|
+
index: slides.length,
|
|
1718
|
+
type: "single-statement",
|
|
1719
|
+
data: {
|
|
1720
|
+
title: titles.question,
|
|
1721
|
+
// FROM KB - not hardcoded 'The Question'
|
|
1722
|
+
body: this.truncateText(scqa.question, this.config.rules.wordsPerSlide.max)
|
|
1723
|
+
},
|
|
1724
|
+
classes: ["question-slide"]
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
addSparklineSlides(slides, analysis) {
|
|
1729
|
+
const spark = analysis.sparkline;
|
|
1730
|
+
const titles = this.config.sparklineTitles;
|
|
1731
|
+
const whatIsFirst = spark?.whatIs?.[0];
|
|
1732
|
+
if (whatIsFirst && !this.usedContent.has("spark-what-is")) {
|
|
1733
|
+
this.usedContent.add("spark-what-is");
|
|
1734
|
+
slides.push({
|
|
1735
|
+
index: slides.length,
|
|
1736
|
+
type: "single-statement",
|
|
1737
|
+
data: {
|
|
1738
|
+
title: titles.whatIs,
|
|
1739
|
+
// FROM KB - not hardcoded 'Where We Are Today'
|
|
1740
|
+
body: this.truncateText(whatIsFirst, this.config.rules.wordsPerSlide.max)
|
|
1741
|
+
},
|
|
1742
|
+
classes: ["what-is-slide"]
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
const whatCouldBeFirst = spark?.whatCouldBe?.[0];
|
|
1746
|
+
if (whatCouldBeFirst && !this.usedContent.has("spark-could-be")) {
|
|
1747
|
+
this.usedContent.add("spark-could-be");
|
|
1748
|
+
slides.push({
|
|
1749
|
+
index: slides.length,
|
|
1750
|
+
type: "single-statement",
|
|
1751
|
+
data: {
|
|
1752
|
+
title: titles.whatCouldBe,
|
|
1753
|
+
// FROM KB - not hardcoded 'What Could Be'
|
|
1754
|
+
body: this.truncateText(whatCouldBeFirst, this.config.rules.wordsPerSlide.max)
|
|
1755
|
+
},
|
|
1756
|
+
classes: ["what-could-be-slide"]
|
|
1757
|
+
});
|
|
1022
1758
|
}
|
|
1759
|
+
}
|
|
1760
|
+
// ===========================================================================
|
|
1761
|
+
// CONTENT SLIDES - ALL values from KB
|
|
1762
|
+
// ===========================================================================
|
|
1763
|
+
createBigNumberSlide(index, section, pattern) {
|
|
1764
|
+
const bigNumber = this.classifier.extractBigNumber(
|
|
1765
|
+
`${section.header} ${section.content} ${section.bullets.join(" ")}`
|
|
1766
|
+
);
|
|
1023
1767
|
return {
|
|
1024
1768
|
index,
|
|
1025
|
-
type: "
|
|
1769
|
+
type: "big-number",
|
|
1026
1770
|
data: {
|
|
1027
|
-
title:
|
|
1028
|
-
|
|
1029
|
-
|
|
1771
|
+
title: this.createTitle(section.header, section),
|
|
1772
|
+
keyMessage: bigNumber?.value || pattern.bigNumberValue || "0",
|
|
1773
|
+
body: bigNumber?.context || this.truncateText(
|
|
1774
|
+
section.content,
|
|
1775
|
+
this.config.defaults.context.maxWords
|
|
1776
|
+
// FROM KB - not hardcoded 30
|
|
1777
|
+
)
|
|
1030
1778
|
},
|
|
1031
|
-
classes: ["
|
|
1779
|
+
classes: ["big-number-slide"]
|
|
1032
1780
|
};
|
|
1033
1781
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
type: "big-idea",
|
|
1042
|
-
data: {
|
|
1043
|
-
title: this.truncate(analysis.scqa.complication, 60),
|
|
1044
|
-
keyMessage: "The challenge we face"
|
|
1045
|
-
},
|
|
1046
|
-
classes: ["slide-big-idea"]
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1782
|
+
createComparisonSlide(index, section) {
|
|
1783
|
+
const comparison = this.classifier.extractComparison(section);
|
|
1784
|
+
const labels = this.config.defaults.comparison;
|
|
1785
|
+
const leftFallback = labels.optionLabels[0] ?? "Option A";
|
|
1786
|
+
const rightFallback = labels.optionLabels[1] ?? "Option B";
|
|
1787
|
+
const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
|
|
1788
|
+
const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
|
|
1049
1789
|
return {
|
|
1050
1790
|
index,
|
|
1051
|
-
type: "
|
|
1791
|
+
type: "comparison",
|
|
1052
1792
|
data: {
|
|
1053
|
-
title:
|
|
1054
|
-
|
|
1055
|
-
|
|
1793
|
+
title: this.createTitle(section.header, section),
|
|
1794
|
+
columns: [
|
|
1795
|
+
{
|
|
1796
|
+
title: labels.leftLabel,
|
|
1797
|
+
// FROM KB - not hardcoded 'Before'
|
|
1798
|
+
content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
title: labels.rightLabel,
|
|
1802
|
+
// FROM KB - not hardcoded 'After'
|
|
1803
|
+
content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
|
|
1804
|
+
}
|
|
1805
|
+
]
|
|
1056
1806
|
},
|
|
1057
|
-
classes: ["slide
|
|
1807
|
+
classes: ["comparison-slide"]
|
|
1058
1808
|
};
|
|
1059
1809
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
data: {
|
|
1069
|
-
title: this.truncate(message, 60),
|
|
1070
|
-
keyMessage: message
|
|
1071
|
-
},
|
|
1072
|
-
classes: ["slide-single-statement"]
|
|
1073
|
-
};
|
|
1074
|
-
}
|
|
1810
|
+
createTimelineSlide(index, section) {
|
|
1811
|
+
const steps = this.classifier.extractSteps(section);
|
|
1812
|
+
const maxSteps = Math.min(
|
|
1813
|
+
steps.length,
|
|
1814
|
+
this.config.rules.bulletsPerSlide.max,
|
|
1815
|
+
this.config.millersLaw.maxItems
|
|
1816
|
+
// FROM KB - 7±2 rule
|
|
1817
|
+
);
|
|
1075
1818
|
return {
|
|
1076
1819
|
index,
|
|
1077
|
-
type: "
|
|
1820
|
+
type: "timeline",
|
|
1078
1821
|
data: {
|
|
1079
|
-
title: this.
|
|
1080
|
-
|
|
1081
|
-
|
|
1822
|
+
title: this.createTitle(section.header, section),
|
|
1823
|
+
steps: steps.slice(0, maxSteps).map((step) => ({
|
|
1824
|
+
label: step.label,
|
|
1825
|
+
description: this.truncateText(
|
|
1826
|
+
step.description,
|
|
1827
|
+
this.config.defaults.step.maxWords
|
|
1828
|
+
// FROM KB - not hardcoded 20
|
|
1829
|
+
)
|
|
1830
|
+
}))
|
|
1082
1831
|
},
|
|
1083
|
-
classes: ["slide
|
|
1832
|
+
classes: ["timeline-slide"]
|
|
1084
1833
|
};
|
|
1085
1834
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1835
|
+
createProcessSlide(index, section) {
|
|
1836
|
+
const steps = this.classifier.extractSteps(section);
|
|
1837
|
+
const maxSteps = Math.min(steps.length, this.config.rules.bulletsPerSlide.max);
|
|
1838
|
+
return {
|
|
1839
|
+
index,
|
|
1840
|
+
type: "process",
|
|
1841
|
+
data: {
|
|
1842
|
+
title: this.createTitle(section.header, section),
|
|
1843
|
+
steps: steps.slice(0, maxSteps).map((step, i) => ({
|
|
1844
|
+
number: i + 1,
|
|
1845
|
+
title: step.label,
|
|
1846
|
+
description: this.truncateText(
|
|
1847
|
+
step.description,
|
|
1848
|
+
this.config.defaults.step.maxWords
|
|
1849
|
+
// FROM KB - not hardcoded 15
|
|
1850
|
+
)
|
|
1851
|
+
}))
|
|
1852
|
+
},
|
|
1853
|
+
classes: ["process-slide"]
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
createThreeColumnSlide(index, section) {
|
|
1857
|
+
const columns = section.bullets.slice(0, 3);
|
|
1858
|
+
while (columns.length < 3) {
|
|
1859
|
+
columns.push("");
|
|
1103
1860
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1861
|
+
const labelTemplate = this.config.defaults.column.labelTemplate;
|
|
1862
|
+
return {
|
|
1863
|
+
index,
|
|
1864
|
+
type: "three-column",
|
|
1865
|
+
data: {
|
|
1866
|
+
title: this.createTitle(section.header, section),
|
|
1867
|
+
columns: columns.map((content, i) => ({
|
|
1868
|
+
title: labelTemplate.replace("{n}", String(i + 1)),
|
|
1869
|
+
// FROM KB - not hardcoded 'Point ${i+1}'
|
|
1870
|
+
content: this.truncateText(
|
|
1871
|
+
content,
|
|
1872
|
+
this.config.defaults.columnContent.maxWords
|
|
1873
|
+
// FROM KB - not hardcoded 25
|
|
1874
|
+
)
|
|
1875
|
+
}))
|
|
1876
|
+
},
|
|
1877
|
+
classes: ["three-column-slide"]
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
createTwoColumnSlide(index, section) {
|
|
1881
|
+
const midpoint = Math.ceil(section.bullets.length / 2);
|
|
1882
|
+
const leftBullets = section.bullets.slice(0, midpoint);
|
|
1883
|
+
const rightBullets = section.bullets.slice(midpoint);
|
|
1884
|
+
const wordsPerBullet = Math.floor(
|
|
1885
|
+
this.config.rules.wordsPerSlide.max / this.config.rules.bulletsPerSlide.max
|
|
1886
|
+
);
|
|
1887
|
+
return {
|
|
1888
|
+
index,
|
|
1889
|
+
type: "two-column",
|
|
1890
|
+
data: {
|
|
1891
|
+
title: this.createTitle(section.header, section),
|
|
1892
|
+
leftColumn: {
|
|
1893
|
+
bullets: leftBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
|
|
1111
1894
|
},
|
|
1112
|
-
|
|
1113
|
-
|
|
1895
|
+
rightColumn: {
|
|
1896
|
+
bullets: rightBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
|
|
1897
|
+
}
|
|
1898
|
+
},
|
|
1899
|
+
classes: ["two-column-slide"]
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
createQuoteSlide(index, section) {
|
|
1903
|
+
const quoteInfo = this.classifier.isQuoteContent(section);
|
|
1904
|
+
const quoteText = section.content.replace(/^[""]|[""]$/g, "").trim();
|
|
1905
|
+
const quoteWordLimit = Math.min(
|
|
1906
|
+
this.config.glanceTest.wordLimit * 2,
|
|
1907
|
+
// FROM KB - not hardcoded
|
|
1908
|
+
this.config.rules.wordsPerSlide.max
|
|
1909
|
+
);
|
|
1910
|
+
const data = {
|
|
1911
|
+
title: this.createTitle(section.header, section),
|
|
1912
|
+
quote: this.truncateText(quoteText, quoteWordLimit)
|
|
1913
|
+
};
|
|
1914
|
+
if (quoteInfo.attribution) {
|
|
1915
|
+
data.attribution = quoteInfo.attribution;
|
|
1114
1916
|
}
|
|
1115
1917
|
return {
|
|
1116
1918
|
index,
|
|
1117
1919
|
type: "quote",
|
|
1920
|
+
data,
|
|
1921
|
+
classes: ["quote-slide"]
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
createMetricsGridSlide(index, metrics) {
|
|
1925
|
+
const maxMetrics = this.config.defaults.metricsGrid.maxMetrics;
|
|
1926
|
+
const cleanedMetrics = metrics.slice(0, maxMetrics).map((m) => ({
|
|
1927
|
+
value: this.cleanText(m.value),
|
|
1928
|
+
label: this.cleanMetricLabel(m.label)
|
|
1929
|
+
}));
|
|
1930
|
+
return {
|
|
1931
|
+
index,
|
|
1932
|
+
type: "metrics-grid",
|
|
1118
1933
|
data: {
|
|
1119
|
-
|
|
1120
|
-
|
|
1934
|
+
title: this.config.defaults.metricsGrid.title,
|
|
1935
|
+
// FROM KB - not hardcoded 'Key Metrics'
|
|
1936
|
+
metrics: cleanedMetrics
|
|
1121
1937
|
},
|
|
1122
|
-
classes: ["slide
|
|
1938
|
+
classes: ["metrics-grid-slide"]
|
|
1123
1939
|
};
|
|
1124
1940
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
if (
|
|
1130
|
-
return
|
|
1131
|
-
index,
|
|
1132
|
-
type: "big-idea",
|
|
1133
|
-
data: {
|
|
1134
|
-
title: this.truncate(analysis.scqa.answer, 60),
|
|
1135
|
-
keyMessage: "Our answer"
|
|
1136
|
-
},
|
|
1137
|
-
classes: ["slide-big-idea"]
|
|
1138
|
-
};
|
|
1941
|
+
createBulletSlide(index, section) {
|
|
1942
|
+
const maxBullets = this.config.rules.bulletsPerSlide.max;
|
|
1943
|
+
const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
|
|
1944
|
+
const cleanedBullets = section.bullets.slice(0, maxBullets).map((b) => this.truncateText(this.cleanText(b), wordsPerBullet)).filter((b) => b.length > 0);
|
|
1945
|
+
if (cleanedBullets.length === 0 && section.content) {
|
|
1946
|
+
return this.createSingleStatementSlide(index, section);
|
|
1139
1947
|
}
|
|
1140
1948
|
return {
|
|
1141
1949
|
index,
|
|
1142
|
-
type: "
|
|
1950
|
+
type: "bullet-points",
|
|
1143
1951
|
data: {
|
|
1144
|
-
title:
|
|
1145
|
-
|
|
1146
|
-
bullets: analysis.sparkline.whatCouldBe.slice(0, 4)
|
|
1952
|
+
title: this.createTitle(section.header, section),
|
|
1953
|
+
bullets: cleanedBullets
|
|
1147
1954
|
},
|
|
1148
|
-
classes: ["
|
|
1955
|
+
classes: ["bullet-points-slide"]
|
|
1149
1956
|
};
|
|
1150
1957
|
}
|
|
1151
|
-
|
|
1152
|
-
* Create a call-to-action slide.
|
|
1153
|
-
*/
|
|
1154
|
-
createCTASlide(index, analysis, mode) {
|
|
1958
|
+
createSingleStatementSlide(index, section) {
|
|
1155
1959
|
return {
|
|
1156
1960
|
index,
|
|
1157
|
-
type: "
|
|
1961
|
+
type: "single-statement",
|
|
1158
1962
|
data: {
|
|
1159
|
-
title:
|
|
1160
|
-
body:
|
|
1161
|
-
|
|
1963
|
+
title: this.createTitle(section.header, section),
|
|
1964
|
+
body: this.truncateText(
|
|
1965
|
+
section.content || section.bullets[0] || "",
|
|
1966
|
+
this.config.rules.wordsPerSlide.max
|
|
1967
|
+
// FROM KB
|
|
1968
|
+
)
|
|
1162
1969
|
},
|
|
1163
|
-
classes: ["slide
|
|
1970
|
+
classes: ["single-statement-slide"]
|
|
1164
1971
|
};
|
|
1165
1972
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
createThankYouSlide(index) {
|
|
1973
|
+
createCodeSlide(index, section) {
|
|
1974
|
+
const codeMatch = section.content.match(/```[\s\S]*?```|`[^`]+`/);
|
|
1975
|
+
const code = codeMatch ? codeMatch[0].replace(/```/g, "").trim() : section.content;
|
|
1170
1976
|
return {
|
|
1171
1977
|
index,
|
|
1172
|
-
type: "
|
|
1978
|
+
type: "two-column",
|
|
1979
|
+
// Code slides use two-column layout
|
|
1173
1980
|
data: {
|
|
1174
|
-
title:
|
|
1175
|
-
|
|
1981
|
+
title: this.createTitle(section.header, section),
|
|
1982
|
+
leftColumn: {
|
|
1983
|
+
body: this.config.defaults.code.label
|
|
1984
|
+
// FROM KB - not hardcoded 'Code Example'
|
|
1985
|
+
},
|
|
1986
|
+
rightColumn: {
|
|
1987
|
+
body: code.slice(0, this.config.defaults.code.maxChars)
|
|
1988
|
+
// FROM KB - not hardcoded 500
|
|
1989
|
+
}
|
|
1176
1990
|
},
|
|
1177
|
-
classes: ["slide
|
|
1991
|
+
classes: ["code-slide"]
|
|
1178
1992
|
};
|
|
1179
1993
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
templates.set("title", {
|
|
1186
|
-
type: "title",
|
|
1187
|
-
requiredFields: ["title"],
|
|
1188
|
-
optionalFields: ["subtitle", "author", "date"],
|
|
1189
|
-
keynoteSuitable: true,
|
|
1190
|
-
businessSuitable: true,
|
|
1191
|
-
maxWords: 15
|
|
1192
|
-
});
|
|
1193
|
-
templates.set("big-idea", {
|
|
1194
|
-
type: "big-idea",
|
|
1195
|
-
requiredFields: ["title"],
|
|
1196
|
-
optionalFields: ["keyMessage"],
|
|
1197
|
-
keynoteSuitable: true,
|
|
1198
|
-
businessSuitable: false,
|
|
1199
|
-
maxWords: 10
|
|
1200
|
-
});
|
|
1201
|
-
templates.set("single-statement", {
|
|
1202
|
-
type: "single-statement",
|
|
1203
|
-
requiredFields: ["title"],
|
|
1204
|
-
optionalFields: ["keyMessage"],
|
|
1205
|
-
keynoteSuitable: true,
|
|
1206
|
-
businessSuitable: false,
|
|
1207
|
-
maxWords: 15
|
|
1208
|
-
});
|
|
1209
|
-
templates.set("big-number", {
|
|
1210
|
-
type: "big-number",
|
|
1211
|
-
requiredFields: ["title"],
|
|
1212
|
-
optionalFields: ["subtitle", "source"],
|
|
1213
|
-
keynoteSuitable: true,
|
|
1214
|
-
businessSuitable: true,
|
|
1215
|
-
maxWords: 10
|
|
1216
|
-
});
|
|
1217
|
-
templates.set("quote", {
|
|
1218
|
-
type: "quote",
|
|
1219
|
-
requiredFields: ["quote"],
|
|
1220
|
-
optionalFields: ["attribution", "source"],
|
|
1221
|
-
keynoteSuitable: true,
|
|
1222
|
-
businessSuitable: true,
|
|
1223
|
-
maxWords: 30
|
|
1224
|
-
});
|
|
1225
|
-
templates.set("bullet-points", {
|
|
1226
|
-
type: "bullet-points",
|
|
1227
|
-
requiredFields: ["title", "bullets"],
|
|
1228
|
-
optionalFields: ["body"],
|
|
1229
|
-
keynoteSuitable: false,
|
|
1230
|
-
businessSuitable: true,
|
|
1231
|
-
maxWords: 80
|
|
1232
|
-
});
|
|
1233
|
-
templates.set("two-column", {
|
|
1234
|
-
type: "two-column",
|
|
1235
|
-
requiredFields: ["title"],
|
|
1236
|
-
optionalFields: ["body", "bullets", "images"],
|
|
1237
|
-
keynoteSuitable: false,
|
|
1238
|
-
businessSuitable: true,
|
|
1239
|
-
maxWords: 100
|
|
1240
|
-
});
|
|
1241
|
-
templates.set("agenda", {
|
|
1242
|
-
type: "agenda",
|
|
1243
|
-
requiredFields: ["title", "bullets"],
|
|
1244
|
-
optionalFields: [],
|
|
1245
|
-
keynoteSuitable: false,
|
|
1246
|
-
businessSuitable: true,
|
|
1247
|
-
maxWords: 50
|
|
1248
|
-
});
|
|
1249
|
-
templates.set("cta", {
|
|
1994
|
+
createCTASlide(index, analysis) {
|
|
1995
|
+
const ctaDefaults = this.config.defaults.cta;
|
|
1996
|
+
const ctaText = analysis.sparkline?.callToAdventure || analysis.scqa?.answer || ctaDefaults.fallback;
|
|
1997
|
+
return {
|
|
1998
|
+
index,
|
|
1250
1999
|
type: "cta",
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
2000
|
+
data: {
|
|
2001
|
+
title: ctaDefaults.title,
|
|
2002
|
+
// FROM KB - not hardcoded 'Next Steps'
|
|
2003
|
+
body: this.truncateText(ctaText, this.config.rules.wordsPerSlide.max),
|
|
2004
|
+
keyMessage: ctaDefaults.message
|
|
2005
|
+
// FROM KB - not hardcoded 'Ready to Begin?'
|
|
2006
|
+
},
|
|
2007
|
+
classes: ["cta-slide"]
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
// ===========================================================================
|
|
2011
|
+
// VALIDATION - Ensure all slides comply with KB rules AND FIX ISSUES
|
|
2012
|
+
// ===========================================================================
|
|
2013
|
+
validateAndFixSlides(slides) {
|
|
2014
|
+
const validatedSlides = [];
|
|
2015
|
+
for (const slide of slides) {
|
|
2016
|
+
const wordCount = this.countWords(slide);
|
|
2017
|
+
const bulletCount = slide.data.bullets?.length || 0;
|
|
2018
|
+
const validation = this.kb.validateSlideAgainstKB(
|
|
2019
|
+
{
|
|
2020
|
+
type: slide.type,
|
|
2021
|
+
wordCount,
|
|
2022
|
+
bulletCount,
|
|
2023
|
+
hasActionTitle: this.isActionTitle(slide.data.title),
|
|
2024
|
+
hasSource: !!slide.data.source
|
|
2025
|
+
},
|
|
2026
|
+
this.presentationType
|
|
2027
|
+
);
|
|
2028
|
+
if (validation.fixes.wordCount) {
|
|
2029
|
+
if (slide.data.body) {
|
|
2030
|
+
slide.data.body = this.truncateText(slide.data.body, validation.fixes.wordCount);
|
|
2031
|
+
}
|
|
2032
|
+
if (slide.data.bullets) {
|
|
2033
|
+
const wordsPerBullet = Math.floor(validation.fixes.wordCount / slide.data.bullets.length);
|
|
2034
|
+
slide.data.bullets = slide.data.bullets.map((b) => this.truncateText(b, wordsPerBullet));
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (validation.fixes.bulletCount && slide.data.bullets) {
|
|
2038
|
+
slide.data.bullets = slide.data.bullets.slice(0, validation.fixes.bulletCount);
|
|
2039
|
+
}
|
|
2040
|
+
if (validation.violations.length > 0) {
|
|
2041
|
+
console.log(` \u26A0 Slide ${slide.index} (${slide.type}): Fixed ${Object.keys(validation.fixes).length} issues, ${validation.violations.length} remaining`);
|
|
2042
|
+
}
|
|
2043
|
+
if (validation.warnings.length > 0) {
|
|
2044
|
+
slide.notes = (slide.notes || "") + "\nWarnings: " + validation.warnings.join(", ");
|
|
2045
|
+
}
|
|
2046
|
+
validatedSlides.push(slide);
|
|
2047
|
+
}
|
|
2048
|
+
return validatedSlides;
|
|
1266
2049
|
}
|
|
1267
|
-
// === Helper Methods ===
|
|
1268
2050
|
/**
|
|
1269
|
-
*
|
|
1270
|
-
* CRITICAL: Must strip all formatting to prevent garbage in slides
|
|
2051
|
+
* Check that required elements exist in the deck (from KB)
|
|
1271
2052
|
*/
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
2053
|
+
checkRequiredElements(slides) {
|
|
2054
|
+
const required = this.config.requiredElements;
|
|
2055
|
+
if (required.length === 0) return;
|
|
2056
|
+
const slideTypes = slides.map((s) => s.type);
|
|
2057
|
+
const missingElements = [];
|
|
2058
|
+
for (const element of required) {
|
|
2059
|
+
const lowerElement = element.toLowerCase();
|
|
2060
|
+
if (lowerElement.includes("hook") || lowerElement.includes("opening")) {
|
|
2061
|
+
const titleSlide = slides.find((s) => s.type === "title");
|
|
2062
|
+
if (!titleSlide || !titleSlide.data.subtitle) {
|
|
2063
|
+
missingElements.push(element);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
if (lowerElement.includes("call to action") || lowerElement.includes("cta")) {
|
|
2067
|
+
if (!slideTypes.includes("cta")) {
|
|
2068
|
+
missingElements.push(element);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
if (lowerElement.includes("star moment") || lowerElement.includes("memorable")) {
|
|
2072
|
+
const hasStarMoment = slideTypes.some(
|
|
2073
|
+
(t) => t === "big-number" || t === "quote" || t === "single-statement"
|
|
2074
|
+
);
|
|
2075
|
+
if (!hasStarMoment) {
|
|
2076
|
+
missingElements.push(element);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (missingElements.length > 0) {
|
|
2081
|
+
console.log(` \u26A0 Missing required elements: ${missingElements.join(", ")}`);
|
|
2082
|
+
}
|
|
1275
2083
|
}
|
|
1276
2084
|
/**
|
|
1277
|
-
*
|
|
1278
|
-
* CRITICAL: Never cut mid-number (99.5% should not become 99.)
|
|
2085
|
+
* Check for anti-patterns from KB
|
|
1279
2086
|
*/
|
|
1280
|
-
|
|
1281
|
-
const
|
|
1282
|
-
if (
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
break;
|
|
2087
|
+
checkAntiPatterns(slides) {
|
|
2088
|
+
const antiPatterns = this.config.antiPatterns;
|
|
2089
|
+
if (antiPatterns.length === 0) return;
|
|
2090
|
+
const violations = [];
|
|
2091
|
+
for (const pattern of antiPatterns) {
|
|
2092
|
+
const lowerPattern = pattern.toLowerCase();
|
|
2093
|
+
if (lowerPattern.includes("bullet") && this.config.mode === "keynote") {
|
|
2094
|
+
const bulletSlides = slides.filter(
|
|
2095
|
+
(s) => s.type === "bullet-points" || s.data.bullets && s.data.bullets.length > 3
|
|
2096
|
+
);
|
|
2097
|
+
if (bulletSlides.length > 0) {
|
|
2098
|
+
violations.push(`${pattern} (${bulletSlides.length} slides)`);
|
|
1293
2099
|
}
|
|
1294
2100
|
}
|
|
1295
|
-
if (
|
|
1296
|
-
|
|
2101
|
+
if (lowerPattern.includes("text") || lowerPattern.includes("overload")) {
|
|
2102
|
+
const overloadedSlides = slides.filter((s) => {
|
|
2103
|
+
const wordCount = this.countWords(s);
|
|
2104
|
+
return wordCount > this.config.glanceTest.wordLimit;
|
|
2105
|
+
});
|
|
2106
|
+
if (overloadedSlides.length > 0) {
|
|
2107
|
+
violations.push(`${pattern} (${overloadedSlides.length} slides exceed glance test)`);
|
|
2108
|
+
}
|
|
1297
2109
|
}
|
|
1298
2110
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
2111
|
+
if (violations.length > 0) {
|
|
2112
|
+
console.log(` \u26A0 Anti-pattern violations: ${violations.join(", ")}`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
// ===========================================================================
|
|
2116
|
+
// HELPER METHODS - All use KB configuration
|
|
2117
|
+
// ===========================================================================
|
|
2118
|
+
/**
|
|
2119
|
+
* Create a title for a slide - uses action titles for business mode per KB
|
|
2120
|
+
*/
|
|
2121
|
+
createTitle(header, section) {
|
|
2122
|
+
const cleanHeader = this.cleanText(header);
|
|
2123
|
+
if (this.usedTitles.has(cleanHeader.toLowerCase())) {
|
|
2124
|
+
return `${cleanHeader} (continued)`;
|
|
1304
2125
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
2126
|
+
this.usedTitles.add(cleanHeader.toLowerCase());
|
|
2127
|
+
if (this.config.rules.actionTitlesRequired) {
|
|
2128
|
+
return this.createActionTitle(cleanHeader, section);
|
|
1307
2129
|
}
|
|
1308
|
-
return
|
|
2130
|
+
return cleanHeader;
|
|
1309
2131
|
}
|
|
1310
2132
|
/**
|
|
1311
|
-
*
|
|
2133
|
+
* Create an action title (Minto/McKinsey style)
|
|
2134
|
+
* Title should communicate the conclusion, not the topic
|
|
1312
2135
|
*/
|
|
1313
|
-
|
|
1314
|
-
const
|
|
1315
|
-
const
|
|
1316
|
-
if (
|
|
1317
|
-
return
|
|
2136
|
+
createActionTitle(header, section) {
|
|
2137
|
+
const titleWordLimit = this.kb.getWordLimitForElement(this.presentationType, "title");
|
|
2138
|
+
const firstBullet = section.bullets[0];
|
|
2139
|
+
if (firstBullet && this.isInsightful(firstBullet)) {
|
|
2140
|
+
return this.truncateText(this.cleanText(firstBullet), titleWordLimit);
|
|
2141
|
+
}
|
|
2142
|
+
if (section.content && this.isInsightful(section.content)) {
|
|
2143
|
+
const sentences = section.content.split(/[.!?]/);
|
|
2144
|
+
const firstSentence = sentences[0];
|
|
2145
|
+
if (firstSentence) {
|
|
2146
|
+
return this.truncateText(this.cleanText(firstSentence), titleWordLimit);
|
|
2147
|
+
}
|
|
1318
2148
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
2149
|
+
return header;
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Check if text contains an insight (not just a topic) - uses KB markers
|
|
2153
|
+
*/
|
|
2154
|
+
isInsightful(text) {
|
|
2155
|
+
const lowerText = text.toLowerCase();
|
|
2156
|
+
return this.config.insightMarkers.some(
|
|
2157
|
+
(marker) => (
|
|
2158
|
+
// FROM KB - not hardcoded array
|
|
2159
|
+
lowerText.includes(marker.toLowerCase())
|
|
2160
|
+
)
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Check if a title is an action title
|
|
2165
|
+
*/
|
|
2166
|
+
isActionTitle(title) {
|
|
2167
|
+
if (!title) return false;
|
|
2168
|
+
return this.isInsightful(title);
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Count words in a slide
|
|
2172
|
+
*/
|
|
2173
|
+
countWords(slide) {
|
|
2174
|
+
let text = "";
|
|
2175
|
+
const data = slide.data;
|
|
2176
|
+
if (data.title) text += data.title + " ";
|
|
2177
|
+
if (data.subtitle) text += data.subtitle + " ";
|
|
2178
|
+
if (data.body) text += data.body + " ";
|
|
2179
|
+
if (data.keyMessage) text += data.keyMessage + " ";
|
|
2180
|
+
if (data.bullets) text += data.bullets.join(" ") + " ";
|
|
2181
|
+
if (data.quote) text += data.quote + " ";
|
|
2182
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
1321
2183
|
}
|
|
1322
2184
|
/**
|
|
1323
|
-
*
|
|
2185
|
+
* Truncate text to word limit at sentence boundaries
|
|
1324
2186
|
*/
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
if (
|
|
1329
|
-
return
|
|
2187
|
+
truncateText(text, maxWords) {
|
|
2188
|
+
const cleaned = this.cleanText(text);
|
|
2189
|
+
const words = cleaned.split(/\s+/);
|
|
2190
|
+
if (words.length <= maxWords) {
|
|
2191
|
+
return cleaned;
|
|
2192
|
+
}
|
|
2193
|
+
const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
|
|
2194
|
+
let result = "";
|
|
2195
|
+
for (const sentence of sentences) {
|
|
2196
|
+
const testResult = result + sentence;
|
|
2197
|
+
if (testResult.split(/\s+/).length <= maxWords) {
|
|
2198
|
+
result = testResult;
|
|
2199
|
+
} else {
|
|
2200
|
+
break;
|
|
2201
|
+
}
|
|
1330
2202
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2203
|
+
if (result.trim().length > 0) {
|
|
2204
|
+
return result.trim();
|
|
2205
|
+
}
|
|
2206
|
+
return words.slice(0, maxWords).join(" ") + "...";
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Clean text by removing content markers and normalizing
|
|
2210
|
+
*/
|
|
2211
|
+
cleanText(text) {
|
|
2212
|
+
if (!text) return "";
|
|
2213
|
+
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();
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Clean metric labels (strip table syntax)
|
|
2217
|
+
*/
|
|
2218
|
+
cleanMetricLabel(label) {
|
|
2219
|
+
if (!label) return "";
|
|
2220
|
+
return label.replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\s{2,}/g, " ").replace(/^\s*\d+\.\s*/, "").trim();
|
|
1334
2221
|
}
|
|
1335
2222
|
/**
|
|
1336
|
-
*
|
|
2223
|
+
* Normalize a key for deduplication
|
|
1337
2224
|
*/
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const firstSentence = cleaned.match(/^[^.!?]+[.!?]?/);
|
|
1341
|
-
return firstSentence ? firstSentence[0].slice(0, 80) : cleaned.slice(0, 80);
|
|
2225
|
+
normalizeKey(text) {
|
|
2226
|
+
return text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1342
2227
|
}
|
|
1343
2228
|
};
|
|
2229
|
+
function createSlideFactory(kb, type) {
|
|
2230
|
+
return new SlideFactory(kb, type);
|
|
2231
|
+
}
|
|
1344
2232
|
|
|
1345
2233
|
// src/core/TemplateEngine.ts
|
|
1346
2234
|
import Handlebars from "handlebars";
|
|
@@ -2751,99 +3639,1044 @@ var QAEngine = class {
|
|
|
2751
3639
|
score -= Math.round((1 - signalPass / slideCount) * 30);
|
|
2752
3640
|
return Math.max(0, score);
|
|
2753
3641
|
}
|
|
2754
|
-
calculateExpertScore(results) {
|
|
2755
|
-
const experts = [results.duarte, results.reynolds, results.gallo, results.anderson];
|
|
2756
|
-
const totalScore = experts.reduce((sum, e) => sum + e.score, 0);
|
|
2757
|
-
return totalScore / experts.length;
|
|
3642
|
+
calculateExpertScore(results) {
|
|
3643
|
+
const experts = [results.duarte, results.reynolds, results.gallo, results.anderson];
|
|
3644
|
+
const totalScore = experts.reduce((sum, e) => sum + e.score, 0);
|
|
3645
|
+
return totalScore / experts.length;
|
|
3646
|
+
}
|
|
3647
|
+
calculateA11yScore(results) {
|
|
3648
|
+
let score = 100;
|
|
3649
|
+
score -= Math.min(40, results.fontSizeIssues.length * 10);
|
|
3650
|
+
score -= Math.min(40, results.contrastIssues.length * 10);
|
|
3651
|
+
if (results.focusCoverage < 1) score -= 20;
|
|
3652
|
+
return Math.max(0, score);
|
|
3653
|
+
}
|
|
3654
|
+
// ===========================================================================
|
|
3655
|
+
// ISSUE COLLECTION
|
|
3656
|
+
// ===========================================================================
|
|
3657
|
+
collectIssues(visual, content, expert, accessibility) {
|
|
3658
|
+
const issues = [];
|
|
3659
|
+
visual.perSlide.forEach((slide) => {
|
|
3660
|
+
slide.issues.forEach((issue) => {
|
|
3661
|
+
issues.push({
|
|
3662
|
+
severity: slide.whitespace > 70 || slide.whitespace < 20 ? "error" : "warning",
|
|
3663
|
+
category: "visual",
|
|
3664
|
+
slideIndex: slide.slideIndex,
|
|
3665
|
+
message: issue
|
|
3666
|
+
});
|
|
3667
|
+
});
|
|
3668
|
+
});
|
|
3669
|
+
content.perSlide.forEach((slide) => {
|
|
3670
|
+
slide.issues.forEach((issue) => {
|
|
3671
|
+
issues.push({
|
|
3672
|
+
severity: "warning",
|
|
3673
|
+
category: "content",
|
|
3674
|
+
slideIndex: slide.slideIndex,
|
|
3675
|
+
message: issue
|
|
3676
|
+
});
|
|
3677
|
+
});
|
|
3678
|
+
});
|
|
3679
|
+
content.glanceTest.filter((g) => !g.passed).forEach((g) => {
|
|
3680
|
+
const issue = {
|
|
3681
|
+
severity: "warning",
|
|
3682
|
+
category: "content",
|
|
3683
|
+
slideIndex: g.slideIndex,
|
|
3684
|
+
message: `Glance test failed: "${g.keyMessage.substring(0, 50)}..." takes ${g.readingTime}s to read`
|
|
3685
|
+
};
|
|
3686
|
+
if (g.recommendation) {
|
|
3687
|
+
issue.suggestion = g.recommendation;
|
|
3688
|
+
}
|
|
3689
|
+
issues.push(issue);
|
|
3690
|
+
});
|
|
3691
|
+
[expert.duarte, expert.reynolds, expert.gallo, expert.anderson].forEach((e) => {
|
|
3692
|
+
e.violations.forEach((v) => {
|
|
3693
|
+
issues.push({
|
|
3694
|
+
severity: "warning",
|
|
3695
|
+
category: "expert",
|
|
3696
|
+
message: `${e.expertName}: ${v}`
|
|
3697
|
+
});
|
|
3698
|
+
});
|
|
3699
|
+
});
|
|
3700
|
+
accessibility.fontSizeIssues.forEach((issue) => {
|
|
3701
|
+
issues.push({
|
|
3702
|
+
severity: "error",
|
|
3703
|
+
category: "accessibility",
|
|
3704
|
+
slideIndex: issue.slideIndex,
|
|
3705
|
+
message: `Font size ${issue.actualSize}px below minimum ${issue.minimumSize}px`,
|
|
3706
|
+
suggestion: `Increase font size to at least ${issue.minimumSize}px`
|
|
3707
|
+
});
|
|
3708
|
+
});
|
|
3709
|
+
accessibility.contrastIssues.forEach((issue) => {
|
|
3710
|
+
issues.push({
|
|
3711
|
+
severity: "error",
|
|
3712
|
+
category: "accessibility",
|
|
3713
|
+
slideIndex: issue.slideIndex,
|
|
3714
|
+
message: `Contrast ratio ${issue.ratio.toFixed(2)} below required ${issue.required}`,
|
|
3715
|
+
suggestion: "Increase contrast between text and background"
|
|
3716
|
+
});
|
|
3717
|
+
});
|
|
3718
|
+
return issues;
|
|
3719
|
+
}
|
|
3720
|
+
// ===========================================================================
|
|
3721
|
+
// BROWSER MANAGEMENT
|
|
3722
|
+
// ===========================================================================
|
|
3723
|
+
async initBrowser() {
|
|
3724
|
+
if (!this.browser) {
|
|
3725
|
+
this.browser = await chromium.launch({ headless: true });
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
async closeBrowser() {
|
|
3729
|
+
if (this.browser) {
|
|
3730
|
+
await this.browser.close();
|
|
3731
|
+
this.browser = null;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
};
|
|
3735
|
+
|
|
3736
|
+
// src/qa/SevenDimensionScorer.ts
|
|
3737
|
+
var DIMENSION_WEIGHTS = {
|
|
3738
|
+
layout: 0.15,
|
|
3739
|
+
contrast: 0.15,
|
|
3740
|
+
graphics: 0.1,
|
|
3741
|
+
content: 0.2,
|
|
3742
|
+
clarity: 0.15,
|
|
3743
|
+
effectiveness: 0.15,
|
|
3744
|
+
consistency: 0.1
|
|
3745
|
+
};
|
|
3746
|
+
var SevenDimensionScorer = class {
|
|
3747
|
+
kb;
|
|
3748
|
+
mode;
|
|
3749
|
+
presentationType;
|
|
3750
|
+
constructor(mode, presentationType) {
|
|
3751
|
+
this.mode = mode;
|
|
3752
|
+
this.presentationType = presentationType;
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Score a presentation across all 7 dimensions.
|
|
3756
|
+
*/
|
|
3757
|
+
async score(slides, html, threshold = 95) {
|
|
3758
|
+
this.kb = await getKnowledgeGateway();
|
|
3759
|
+
const layout = await this.scoreLayout(slides, html);
|
|
3760
|
+
const contrast = await this.scoreContrast(html);
|
|
3761
|
+
const graphics = await this.scoreGraphics(slides);
|
|
3762
|
+
const content = await this.scoreContent(slides);
|
|
3763
|
+
const clarity = await this.scoreClarity(slides);
|
|
3764
|
+
const effectiveness = await this.scoreEffectiveness(slides);
|
|
3765
|
+
const consistency = await this.scoreConsistency(slides, html);
|
|
3766
|
+
const overallScore = Math.round(
|
|
3767
|
+
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
|
|
3768
|
+
);
|
|
3769
|
+
const issues = [
|
|
3770
|
+
...layout.issues,
|
|
3771
|
+
...contrast.issues,
|
|
3772
|
+
...graphics.issues,
|
|
3773
|
+
...content.issues,
|
|
3774
|
+
...clarity.issues,
|
|
3775
|
+
...effectiveness.issues,
|
|
3776
|
+
...consistency.issues
|
|
3777
|
+
];
|
|
3778
|
+
return {
|
|
3779
|
+
overallScore,
|
|
3780
|
+
dimensions: {
|
|
3781
|
+
layout,
|
|
3782
|
+
contrast,
|
|
3783
|
+
graphics,
|
|
3784
|
+
content,
|
|
3785
|
+
clarity,
|
|
3786
|
+
effectiveness,
|
|
3787
|
+
consistency
|
|
3788
|
+
},
|
|
3789
|
+
issues,
|
|
3790
|
+
passed: overallScore >= threshold,
|
|
3791
|
+
threshold
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
/**
|
|
3795
|
+
* Score layout dimension (whitespace, visual balance, structure)
|
|
3796
|
+
*/
|
|
3797
|
+
async scoreLayout(slides, html) {
|
|
3798
|
+
const issues = [];
|
|
3799
|
+
let totalScore = 0;
|
|
3800
|
+
let checks = 0;
|
|
3801
|
+
const minWhitespace = this.mode === "keynote" ? 0.45 : 0.35;
|
|
3802
|
+
const maxWhitespace = 0.6;
|
|
3803
|
+
const slideSections = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
|
|
3804
|
+
for (let i = 0; i < slideSections.length; i++) {
|
|
3805
|
+
const section = slideSections[i];
|
|
3806
|
+
if (!section) continue;
|
|
3807
|
+
const textContent = section.replace(/<[^>]+>/g, "").trim();
|
|
3808
|
+
const totalArea = 1920 * 1080;
|
|
3809
|
+
const estimatedTextArea = textContent.length * 100;
|
|
3810
|
+
const whitespaceRatio = 1 - Math.min(estimatedTextArea / totalArea, 1);
|
|
3811
|
+
if (whitespaceRatio < minWhitespace) {
|
|
3812
|
+
issues.push({
|
|
3813
|
+
slideIndex: i,
|
|
3814
|
+
dimension: "layout",
|
|
3815
|
+
severity: "error",
|
|
3816
|
+
message: `Slide ${i + 1}: Insufficient whitespace (${Math.round(whitespaceRatio * 100)}% < ${Math.round(minWhitespace * 100)}%)`,
|
|
3817
|
+
currentValue: whitespaceRatio,
|
|
3818
|
+
expectedValue: minWhitespace,
|
|
3819
|
+
autoFixable: true,
|
|
3820
|
+
fixSuggestion: "Reduce content or increase margins"
|
|
3821
|
+
});
|
|
3822
|
+
totalScore += 50;
|
|
3823
|
+
} else if (whitespaceRatio > maxWhitespace) {
|
|
3824
|
+
issues.push({
|
|
3825
|
+
slideIndex: i,
|
|
3826
|
+
dimension: "layout",
|
|
3827
|
+
severity: "warning",
|
|
3828
|
+
message: `Slide ${i + 1}: Too much whitespace (${Math.round(whitespaceRatio * 100)}% > ${Math.round(maxWhitespace * 100)}%)`,
|
|
3829
|
+
currentValue: whitespaceRatio,
|
|
3830
|
+
expectedValue: maxWhitespace,
|
|
3831
|
+
autoFixable: false,
|
|
3832
|
+
fixSuggestion: "Add more content or reduce margins"
|
|
3833
|
+
});
|
|
3834
|
+
totalScore += 80;
|
|
3835
|
+
} else {
|
|
3836
|
+
totalScore += 100;
|
|
3837
|
+
}
|
|
3838
|
+
checks++;
|
|
3839
|
+
}
|
|
3840
|
+
for (let i = 0; i < slides.length; i++) {
|
|
3841
|
+
const slide = slides[i];
|
|
3842
|
+
if (!slide) continue;
|
|
3843
|
+
if (!["thank-you", "section-divider"].includes(slide.type)) {
|
|
3844
|
+
if (!slide.data.title || slide.data.title.trim().length === 0) {
|
|
3845
|
+
issues.push({
|
|
3846
|
+
slideIndex: i,
|
|
3847
|
+
dimension: "layout",
|
|
3848
|
+
severity: "warning",
|
|
3849
|
+
message: `Slide ${i + 1}: Missing title`,
|
|
3850
|
+
autoFixable: false,
|
|
3851
|
+
fixSuggestion: "Add a clear slide title"
|
|
3852
|
+
});
|
|
3853
|
+
totalScore += 70;
|
|
3854
|
+
} else {
|
|
3855
|
+
totalScore += 100;
|
|
3856
|
+
}
|
|
3857
|
+
checks++;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
const score = checks > 0 ? Math.round(totalScore / checks) : 100;
|
|
3861
|
+
return {
|
|
3862
|
+
name: "Layout",
|
|
3863
|
+
score,
|
|
3864
|
+
weight: DIMENSION_WEIGHTS.layout,
|
|
3865
|
+
issues,
|
|
3866
|
+
details: {
|
|
3867
|
+
slidesAnalyzed: slides.length,
|
|
3868
|
+
whitespaceTarget: `${Math.round(minWhitespace * 100)}-${Math.round(maxWhitespace * 100)}%`
|
|
3869
|
+
}
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Score contrast dimension (WCAG compliance, readability)
|
|
3874
|
+
*/
|
|
3875
|
+
async scoreContrast(html) {
|
|
3876
|
+
const issues = [];
|
|
3877
|
+
let score = 100;
|
|
3878
|
+
const minContrastRatio = 4.5;
|
|
3879
|
+
const colorMatches = html.match(/color:\s*([^;]+);/gi) || [];
|
|
3880
|
+
const bgColorMatches = html.match(/background(-color)?:\s*([^;]+);/gi) || [];
|
|
3881
|
+
const hasGoodContrast = html.includes("color: #fff") || html.includes("color: white") || html.includes("color: #18181B") || html.includes("color: #F5F5F4");
|
|
3882
|
+
const hasDarkBackground = html.includes("background-color: #18181B") || html.includes("background: #18181B") || html.includes("background-color: rgb(24, 24, 27)");
|
|
3883
|
+
if (html.includes("color: gray") || html.includes("color: #999") || html.includes("color: #888")) {
|
|
3884
|
+
issues.push({
|
|
3885
|
+
slideIndex: -1,
|
|
3886
|
+
dimension: "contrast",
|
|
3887
|
+
severity: "error",
|
|
3888
|
+
message: "Low contrast text color detected (gray text)",
|
|
3889
|
+
currentValue: "gray",
|
|
3890
|
+
expectedValue: "High contrast color",
|
|
3891
|
+
autoFixable: true,
|
|
3892
|
+
fixSuggestion: "Use white (#fff) or dark (#18181B) text depending on background"
|
|
3893
|
+
});
|
|
3894
|
+
score -= 20;
|
|
3895
|
+
}
|
|
3896
|
+
if (!hasGoodContrast && !hasDarkBackground) {
|
|
3897
|
+
issues.push({
|
|
3898
|
+
slideIndex: -1,
|
|
3899
|
+
dimension: "contrast",
|
|
3900
|
+
severity: "warning",
|
|
3901
|
+
message: "Could not verify WCAG-compliant contrast ratios",
|
|
3902
|
+
autoFixable: false,
|
|
3903
|
+
fixSuggestion: "Ensure text has 4.5:1 contrast ratio against background"
|
|
3904
|
+
});
|
|
3905
|
+
score -= 10;
|
|
3906
|
+
}
|
|
3907
|
+
const hasSmallFont = html.match(/font-size:\s*(1[0-3]|[0-9])px/i) !== null;
|
|
3908
|
+
if (hasSmallFont) {
|
|
3909
|
+
issues.push({
|
|
3910
|
+
slideIndex: -1,
|
|
3911
|
+
dimension: "contrast",
|
|
3912
|
+
severity: "error",
|
|
3913
|
+
message: "Font size too small for presentation (< 14px)",
|
|
3914
|
+
currentValue: "Small font",
|
|
3915
|
+
expectedValue: "18px minimum for body text",
|
|
3916
|
+
autoFixable: true,
|
|
3917
|
+
fixSuggestion: "Increase font size to minimum 18px"
|
|
3918
|
+
});
|
|
3919
|
+
score -= 15;
|
|
3920
|
+
}
|
|
3921
|
+
return {
|
|
3922
|
+
name: "Contrast",
|
|
3923
|
+
score: Math.max(0, score),
|
|
3924
|
+
weight: DIMENSION_WEIGHTS.contrast,
|
|
3925
|
+
issues,
|
|
3926
|
+
details: {
|
|
3927
|
+
wcagLevel: "AA",
|
|
3928
|
+
minContrastRatio,
|
|
3929
|
+
colorDefinitions: colorMatches.length,
|
|
3930
|
+
backgroundDefinitions: bgColorMatches.length
|
|
3931
|
+
}
|
|
3932
|
+
};
|
|
3933
|
+
}
|
|
3934
|
+
/**
|
|
3935
|
+
* Score graphics dimension (images, placement, relevance)
|
|
3936
|
+
*/
|
|
3937
|
+
async scoreGraphics(slides) {
|
|
3938
|
+
const issues = [];
|
|
3939
|
+
let score = 100;
|
|
3940
|
+
let slidesWithImages = 0;
|
|
3941
|
+
let totalImageSlides = 0;
|
|
3942
|
+
for (let i = 0; i < slides.length; i++) {
|
|
3943
|
+
const slide = slides[i];
|
|
3944
|
+
if (!slide) continue;
|
|
3945
|
+
const shouldHaveImage = ["hero", "image", "feature"].includes(slide.type);
|
|
3946
|
+
if (shouldHaveImage) {
|
|
3947
|
+
totalImageSlides++;
|
|
3948
|
+
if (slide.data.image || slide.data.backgroundImage) {
|
|
3949
|
+
slidesWithImages++;
|
|
3950
|
+
} else {
|
|
3951
|
+
issues.push({
|
|
3952
|
+
slideIndex: i,
|
|
3953
|
+
dimension: "graphics",
|
|
3954
|
+
severity: "warning",
|
|
3955
|
+
message: `Slide ${i + 1} (${slide.type}): Missing expected image`,
|
|
3956
|
+
autoFixable: false,
|
|
3957
|
+
fixSuggestion: "Add a relevant image or change slide type"
|
|
3958
|
+
});
|
|
3959
|
+
score -= 5;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
return {
|
|
3964
|
+
name: "Graphics",
|
|
3965
|
+
score: Math.max(0, score),
|
|
3966
|
+
weight: DIMENSION_WEIGHTS.graphics,
|
|
3967
|
+
issues,
|
|
3968
|
+
details: {
|
|
3969
|
+
slidesWithImages,
|
|
3970
|
+
expectedImageSlides: totalImageSlides,
|
|
3971
|
+
imageRatio: totalImageSlides > 0 ? slidesWithImages / totalImageSlides : 1
|
|
3972
|
+
}
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Score content dimension (word limits, bullet counts, structure)
|
|
3977
|
+
* This is critical - must use KB rules exactly.
|
|
3978
|
+
*/
|
|
3979
|
+
async scoreContent(slides) {
|
|
3980
|
+
const issues = [];
|
|
3981
|
+
let totalScore = 0;
|
|
3982
|
+
let checks = 0;
|
|
3983
|
+
const wordLimits = this.kb.getWordLimits(this.mode);
|
|
3984
|
+
const bulletLimits = this.kb.getBulletLimits(this.mode);
|
|
3985
|
+
const defaultMaxWords = this.mode === "keynote" ? 25 : 80;
|
|
3986
|
+
const defaultMinWords = this.mode === "keynote" ? 3 : 15;
|
|
3987
|
+
const defaultMaxBullets = 5;
|
|
3988
|
+
for (let i = 0; i < slides.length; i++) {
|
|
3989
|
+
const slide = slides[i];
|
|
3990
|
+
if (!slide) continue;
|
|
3991
|
+
const wordCount = this.countWords(slide);
|
|
3992
|
+
const slideType = slide.type;
|
|
3993
|
+
const maxWords = wordLimits[slideType] ?? defaultMaxWords;
|
|
3994
|
+
const minWords = defaultMinWords;
|
|
3995
|
+
if (wordCount > maxWords) {
|
|
3996
|
+
const severity = wordCount > maxWords * 1.5 ? "error" : "warning";
|
|
3997
|
+
issues.push({
|
|
3998
|
+
slideIndex: i,
|
|
3999
|
+
dimension: "content",
|
|
4000
|
+
severity,
|
|
4001
|
+
message: `Slide ${i + 1}: Word count ${wordCount} exceeds limit of ${maxWords} for ${this.mode} mode`,
|
|
4002
|
+
currentValue: wordCount,
|
|
4003
|
+
expectedValue: maxWords,
|
|
4004
|
+
autoFixable: true,
|
|
4005
|
+
fixSuggestion: "Condense text to key points only"
|
|
4006
|
+
});
|
|
4007
|
+
totalScore += severity === "error" ? 40 : 70;
|
|
4008
|
+
} else if (wordCount < minWords && !["title", "section-divider", "thank-you"].includes(slide.type)) {
|
|
4009
|
+
issues.push({
|
|
4010
|
+
slideIndex: i,
|
|
4011
|
+
dimension: "content",
|
|
4012
|
+
severity: "warning",
|
|
4013
|
+
message: `Slide ${i + 1}: Word count ${wordCount} may be too sparse (min: ${minWords})`,
|
|
4014
|
+
currentValue: wordCount,
|
|
4015
|
+
expectedValue: minWords,
|
|
4016
|
+
autoFixable: false,
|
|
4017
|
+
fixSuggestion: "Add supporting content"
|
|
4018
|
+
});
|
|
4019
|
+
totalScore += 80;
|
|
4020
|
+
} else {
|
|
4021
|
+
totalScore += 100;
|
|
4022
|
+
}
|
|
4023
|
+
checks++;
|
|
4024
|
+
if (slide.data.bullets && Array.isArray(slide.data.bullets)) {
|
|
4025
|
+
const bulletCount = slide.data.bullets.length;
|
|
4026
|
+
const maxBullets = bulletLimits[slideType] ?? defaultMaxBullets;
|
|
4027
|
+
if (bulletCount > maxBullets) {
|
|
4028
|
+
issues.push({
|
|
4029
|
+
slideIndex: i,
|
|
4030
|
+
dimension: "content",
|
|
4031
|
+
severity: "error",
|
|
4032
|
+
message: `Slide ${i + 1}: ${bulletCount} bullets exceeds limit of ${maxBullets}`,
|
|
4033
|
+
currentValue: bulletCount,
|
|
4034
|
+
expectedValue: maxBullets,
|
|
4035
|
+
autoFixable: true,
|
|
4036
|
+
fixSuggestion: "Reduce to top 3-5 key points"
|
|
4037
|
+
});
|
|
4038
|
+
totalScore += 50;
|
|
4039
|
+
} else {
|
|
4040
|
+
totalScore += 100;
|
|
4041
|
+
}
|
|
4042
|
+
checks++;
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
const score = checks > 0 ? Math.round(totalScore / checks) : 100;
|
|
4046
|
+
return {
|
|
4047
|
+
name: "Content",
|
|
4048
|
+
score,
|
|
4049
|
+
weight: DIMENSION_WEIGHTS.content,
|
|
4050
|
+
issues,
|
|
4051
|
+
details: {
|
|
4052
|
+
mode: this.mode,
|
|
4053
|
+
wordLimits,
|
|
4054
|
+
bulletLimits,
|
|
4055
|
+
totalSlides: slides.length
|
|
4056
|
+
}
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
4059
|
+
/**
|
|
4060
|
+
* Score clarity dimension (message focus, information density)
|
|
4061
|
+
*/
|
|
4062
|
+
async scoreClarity(slides) {
|
|
4063
|
+
const issues = [];
|
|
4064
|
+
let totalScore = 0;
|
|
4065
|
+
let checks = 0;
|
|
4066
|
+
for (let i = 0; i < slides.length; i++) {
|
|
4067
|
+
const slide = slides[i];
|
|
4068
|
+
if (!slide) continue;
|
|
4069
|
+
if (slide.data.keyMessage) {
|
|
4070
|
+
const keyMessageWords = slide.data.keyMessage.split(/\s+/).length;
|
|
4071
|
+
const maxKeyMessageWords = this.mode === "keynote" ? 15 : 25;
|
|
4072
|
+
if (keyMessageWords > maxKeyMessageWords) {
|
|
4073
|
+
issues.push({
|
|
4074
|
+
slideIndex: i,
|
|
4075
|
+
dimension: "clarity",
|
|
4076
|
+
severity: "warning",
|
|
4077
|
+
message: `Slide ${i + 1}: Key message too long (${keyMessageWords} words > ${maxKeyMessageWords})`,
|
|
4078
|
+
currentValue: keyMessageWords,
|
|
4079
|
+
expectedValue: maxKeyMessageWords,
|
|
4080
|
+
autoFixable: true,
|
|
4081
|
+
fixSuggestion: "Shorten key message to one impactful sentence"
|
|
4082
|
+
});
|
|
4083
|
+
totalScore += 70;
|
|
4084
|
+
} else {
|
|
4085
|
+
totalScore += 100;
|
|
4086
|
+
}
|
|
4087
|
+
checks++;
|
|
4088
|
+
}
|
|
4089
|
+
if (slide.data.title) {
|
|
4090
|
+
const title = slide.data.title;
|
|
4091
|
+
const titleWords = title.split(/\s+/).length;
|
|
4092
|
+
if (titleWords > 10) {
|
|
4093
|
+
issues.push({
|
|
4094
|
+
slideIndex: i,
|
|
4095
|
+
dimension: "clarity",
|
|
4096
|
+
severity: "warning",
|
|
4097
|
+
message: `Slide ${i + 1}: Title too long (${titleWords} words)`,
|
|
4098
|
+
currentValue: titleWords,
|
|
4099
|
+
expectedValue: "2-8 words",
|
|
4100
|
+
autoFixable: true,
|
|
4101
|
+
fixSuggestion: "Use action-oriented, concise title"
|
|
4102
|
+
});
|
|
4103
|
+
totalScore += 75;
|
|
4104
|
+
} else {
|
|
4105
|
+
totalScore += 100;
|
|
4106
|
+
}
|
|
4107
|
+
checks++;
|
|
4108
|
+
}
|
|
4109
|
+
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);
|
|
4110
|
+
const maxElements = this.mode === "keynote" ? 4 : 6;
|
|
4111
|
+
if (elementCount > maxElements) {
|
|
4112
|
+
issues.push({
|
|
4113
|
+
slideIndex: i,
|
|
4114
|
+
dimension: "clarity",
|
|
4115
|
+
severity: "warning",
|
|
4116
|
+
message: `Slide ${i + 1}: Too many elements (${elementCount} > ${maxElements})`,
|
|
4117
|
+
currentValue: elementCount,
|
|
4118
|
+
expectedValue: maxElements,
|
|
4119
|
+
autoFixable: true,
|
|
4120
|
+
fixSuggestion: "Split into multiple slides for clarity"
|
|
4121
|
+
});
|
|
4122
|
+
totalScore += 70;
|
|
4123
|
+
} else {
|
|
4124
|
+
totalScore += 100;
|
|
4125
|
+
}
|
|
4126
|
+
checks++;
|
|
4127
|
+
}
|
|
4128
|
+
const score = checks > 0 ? Math.round(totalScore / checks) : 100;
|
|
4129
|
+
return {
|
|
4130
|
+
name: "Clarity",
|
|
4131
|
+
score,
|
|
4132
|
+
weight: DIMENSION_WEIGHTS.clarity,
|
|
4133
|
+
issues,
|
|
4134
|
+
details: {
|
|
4135
|
+
slidesAnalyzed: slides.length,
|
|
4136
|
+
mode: this.mode
|
|
4137
|
+
}
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
/**
|
|
4141
|
+
* Score effectiveness dimension (expert methodology compliance)
|
|
4142
|
+
*/
|
|
4143
|
+
async scoreEffectiveness(slides) {
|
|
4144
|
+
const issues = [];
|
|
4145
|
+
let score = 100;
|
|
4146
|
+
if (slides.length >= 3) {
|
|
4147
|
+
const firstSlide = slides[0];
|
|
4148
|
+
const lastSlide = slides[slides.length - 1];
|
|
4149
|
+
if (firstSlide && !["title", "hero"].includes(firstSlide.type)) {
|
|
4150
|
+
issues.push({
|
|
4151
|
+
slideIndex: 0,
|
|
4152
|
+
dimension: "effectiveness",
|
|
4153
|
+
severity: "warning",
|
|
4154
|
+
message: "Presentation should start with a title or hero slide",
|
|
4155
|
+
currentValue: firstSlide.type,
|
|
4156
|
+
expectedValue: "title or hero",
|
|
4157
|
+
autoFixable: false,
|
|
4158
|
+
fixSuggestion: "Add a compelling opening slide"
|
|
4159
|
+
});
|
|
4160
|
+
score -= 10;
|
|
4161
|
+
}
|
|
4162
|
+
if (lastSlide && !["thank-you", "cta", "closing"].includes(lastSlide.type)) {
|
|
4163
|
+
issues.push({
|
|
4164
|
+
slideIndex: slides.length - 1,
|
|
4165
|
+
dimension: "effectiveness",
|
|
4166
|
+
severity: "warning",
|
|
4167
|
+
message: "Presentation should end with a closing or CTA slide",
|
|
4168
|
+
currentValue: lastSlide.type,
|
|
4169
|
+
expectedValue: "thank-you, cta, or closing",
|
|
4170
|
+
autoFixable: false,
|
|
4171
|
+
fixSuggestion: "Add a clear call-to-action or closing"
|
|
4172
|
+
});
|
|
4173
|
+
score -= 10;
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
const keyMessages = slides.filter((s) => s.data.keyMessage);
|
|
4177
|
+
if (keyMessages.length > 0 && keyMessages.length !== 3 && keyMessages.length > 4) {
|
|
4178
|
+
issues.push({
|
|
4179
|
+
slideIndex: -1,
|
|
4180
|
+
dimension: "effectiveness",
|
|
4181
|
+
severity: "info",
|
|
4182
|
+
message: `Consider using Rule of Three: ${keyMessages.length} key messages found`,
|
|
4183
|
+
currentValue: keyMessages.length,
|
|
4184
|
+
expectedValue: 3,
|
|
4185
|
+
autoFixable: false,
|
|
4186
|
+
fixSuggestion: "Group messages into 3 main themes"
|
|
4187
|
+
});
|
|
4188
|
+
score -= 5;
|
|
4189
|
+
}
|
|
4190
|
+
const hasScqaElements = slides.some(
|
|
4191
|
+
(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")
|
|
4192
|
+
);
|
|
4193
|
+
if (!hasScqaElements && this.presentationType === "consulting_deck") {
|
|
4194
|
+
issues.push({
|
|
4195
|
+
slideIndex: -1,
|
|
4196
|
+
dimension: "effectiveness",
|
|
4197
|
+
severity: "warning",
|
|
4198
|
+
message: "Consulting deck should follow SCQA structure (Situation, Complication, Question, Answer)",
|
|
4199
|
+
autoFixable: false,
|
|
4200
|
+
fixSuggestion: "Organize content using Barbara Minto Pyramid Principle"
|
|
4201
|
+
});
|
|
4202
|
+
score -= 10;
|
|
4203
|
+
}
|
|
4204
|
+
const firstSlideType = slides[0]?.type;
|
|
4205
|
+
const lastSlideType = slides[slides.length - 1]?.type;
|
|
4206
|
+
return {
|
|
4207
|
+
name: "Effectiveness",
|
|
4208
|
+
score: Math.max(0, score),
|
|
4209
|
+
weight: DIMENSION_WEIGHTS.effectiveness,
|
|
4210
|
+
issues,
|
|
4211
|
+
details: {
|
|
4212
|
+
presentationType: this.presentationType,
|
|
4213
|
+
slideCount: slides.length,
|
|
4214
|
+
hasOpeningSlide: firstSlideType ? ["title", "hero"].includes(firstSlideType) : false,
|
|
4215
|
+
hasClosingSlide: lastSlideType ? ["thank-you", "cta", "closing"].includes(lastSlideType) : false
|
|
4216
|
+
}
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
/**
|
|
4220
|
+
* Score consistency dimension (style uniformity, design coherence)
|
|
4221
|
+
*/
|
|
4222
|
+
async scoreConsistency(slides, html) {
|
|
4223
|
+
const issues = [];
|
|
4224
|
+
let score = 100;
|
|
4225
|
+
const hasCssVariables = html.includes("var(--") || html.includes(":root");
|
|
4226
|
+
if (!hasCssVariables) {
|
|
4227
|
+
issues.push({
|
|
4228
|
+
slideIndex: -1,
|
|
4229
|
+
dimension: "consistency",
|
|
4230
|
+
severity: "warning",
|
|
4231
|
+
message: "Presentation lacks CSS variables for consistent styling",
|
|
4232
|
+
autoFixable: true,
|
|
4233
|
+
fixSuggestion: "Use CSS variables for colors, fonts, and spacing"
|
|
4234
|
+
});
|
|
4235
|
+
score -= 10;
|
|
4236
|
+
}
|
|
4237
|
+
const titlePatterns = /* @__PURE__ */ new Set();
|
|
4238
|
+
for (const slide of slides) {
|
|
4239
|
+
if (slide.data.title) {
|
|
4240
|
+
const isUpperCase = slide.data.title === slide.data.title.toUpperCase();
|
|
4241
|
+
const words = slide.data.title.split(" ").filter((w) => w.length > 0);
|
|
4242
|
+
const isTitleCase = words.length > 0 && words.every(
|
|
4243
|
+
(w) => w.length > 0 && w[0] === w[0]?.toUpperCase()
|
|
4244
|
+
);
|
|
4245
|
+
titlePatterns.add(isUpperCase ? "UPPER" : isTitleCase ? "Title" : "sentence");
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
if (titlePatterns.size > 1) {
|
|
4249
|
+
issues.push({
|
|
4250
|
+
slideIndex: -1,
|
|
4251
|
+
dimension: "consistency",
|
|
4252
|
+
severity: "warning",
|
|
4253
|
+
message: `Inconsistent title casing: ${Array.from(titlePatterns).join(", ")}`,
|
|
4254
|
+
autoFixable: true,
|
|
4255
|
+
fixSuggestion: "Use consistent title case throughout"
|
|
4256
|
+
});
|
|
4257
|
+
score -= 10;
|
|
4258
|
+
}
|
|
4259
|
+
const fontMatches = html.match(/font-family:\s*([^;]+);/gi) || [];
|
|
4260
|
+
const uniqueFonts = new Set(fontMatches.map((f) => f.toLowerCase()));
|
|
4261
|
+
if (uniqueFonts.size > 3) {
|
|
4262
|
+
issues.push({
|
|
4263
|
+
slideIndex: -1,
|
|
4264
|
+
dimension: "consistency",
|
|
4265
|
+
severity: "warning",
|
|
4266
|
+
message: `Too many font families (${uniqueFonts.size} > 3)`,
|
|
4267
|
+
autoFixable: true,
|
|
4268
|
+
fixSuggestion: "Use 2-3 complementary fonts max"
|
|
4269
|
+
});
|
|
4270
|
+
score -= 10;
|
|
4271
|
+
}
|
|
4272
|
+
return {
|
|
4273
|
+
name: "Consistency",
|
|
4274
|
+
score: Math.max(0, score),
|
|
4275
|
+
weight: DIMENSION_WEIGHTS.consistency,
|
|
4276
|
+
issues,
|
|
4277
|
+
details: {
|
|
4278
|
+
hasCssVariables,
|
|
4279
|
+
titlePatterns: Array.from(titlePatterns),
|
|
4280
|
+
fontFamilyCount: uniqueFonts.size
|
|
4281
|
+
}
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
/**
|
|
4285
|
+
* Count words in a slide.
|
|
4286
|
+
*/
|
|
4287
|
+
countWords(slide) {
|
|
4288
|
+
let text = "";
|
|
4289
|
+
if (slide.data.title) text += slide.data.title + " ";
|
|
4290
|
+
if (slide.data.subtitle) text += slide.data.subtitle + " ";
|
|
4291
|
+
if (slide.data.body) text += slide.data.body + " ";
|
|
4292
|
+
if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
|
|
4293
|
+
if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
|
|
4294
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
4295
|
+
}
|
|
4296
|
+
/**
|
|
4297
|
+
* Get a formatted report of the scoring results.
|
|
4298
|
+
*/
|
|
4299
|
+
formatReport(result) {
|
|
4300
|
+
const lines = [];
|
|
4301
|
+
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");
|
|
4302
|
+
lines.push("\u2551 7-DIMENSION QUALITY ASSESSMENT \u2551");
|
|
4303
|
+
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");
|
|
4304
|
+
lines.push("");
|
|
4305
|
+
lines.push(`Overall Score: ${result.overallScore}/100 ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
4306
|
+
lines.push(`Threshold: ${result.threshold}/100`);
|
|
4307
|
+
lines.push("");
|
|
4308
|
+
lines.push("Dimension Breakdown:");
|
|
4309
|
+
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");
|
|
4310
|
+
const dims = result.dimensions;
|
|
4311
|
+
const formatDim = (name, d) => {
|
|
4312
|
+
const bar = "\u2588".repeat(Math.floor(d.score / 10)) + "\u2591".repeat(10 - Math.floor(d.score / 10));
|
|
4313
|
+
const status = d.score >= 95 ? "\u2705" : d.score >= 80 ? "\u26A0\uFE0F" : "\u274C";
|
|
4314
|
+
return `${status} ${name.padEnd(14)} ${bar} ${d.score.toString().padStart(3)}/100 (${(d.weight * 100).toFixed(0)}%)`;
|
|
4315
|
+
};
|
|
4316
|
+
lines.push(formatDim("Layout", dims.layout));
|
|
4317
|
+
lines.push(formatDim("Contrast", dims.contrast));
|
|
4318
|
+
lines.push(formatDim("Graphics", dims.graphics));
|
|
4319
|
+
lines.push(formatDim("Content", dims.content));
|
|
4320
|
+
lines.push(formatDim("Clarity", dims.clarity));
|
|
4321
|
+
lines.push(formatDim("Effectiveness", dims.effectiveness));
|
|
4322
|
+
lines.push(formatDim("Consistency", dims.consistency));
|
|
4323
|
+
lines.push("");
|
|
4324
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
4325
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
4326
|
+
if (errors.length > 0) {
|
|
4327
|
+
lines.push("\u274C Errors:");
|
|
4328
|
+
errors.forEach((e) => lines.push(` \u2022 ${e.message}`));
|
|
4329
|
+
lines.push("");
|
|
4330
|
+
}
|
|
4331
|
+
if (warnings.length > 0) {
|
|
4332
|
+
lines.push("\u26A0\uFE0F Warnings:");
|
|
4333
|
+
warnings.slice(0, 10).forEach((w) => lines.push(` \u2022 ${w.message}`));
|
|
4334
|
+
if (warnings.length > 10) {
|
|
4335
|
+
lines.push(` ... and ${warnings.length - 10} more warnings`);
|
|
4336
|
+
}
|
|
4337
|
+
lines.push("");
|
|
4338
|
+
}
|
|
4339
|
+
const autoFixable = result.issues.filter((i) => i.autoFixable);
|
|
4340
|
+
if (autoFixable.length > 0) {
|
|
4341
|
+
lines.push(`\u{1F527} ${autoFixable.length} issues can be auto-fixed`);
|
|
4342
|
+
}
|
|
4343
|
+
return lines.join("\n");
|
|
4344
|
+
}
|
|
4345
|
+
};
|
|
4346
|
+
|
|
4347
|
+
// src/qa/AutoFixEngine.ts
|
|
4348
|
+
var AutoFixEngine = class {
|
|
4349
|
+
kb;
|
|
4350
|
+
mode;
|
|
4351
|
+
presentationType;
|
|
4352
|
+
constructor(mode, presentationType) {
|
|
4353
|
+
this.mode = mode;
|
|
4354
|
+
this.presentationType = presentationType;
|
|
4355
|
+
}
|
|
4356
|
+
/**
|
|
4357
|
+
* Apply automatic fixes to slides based on scoring results.
|
|
4358
|
+
*/
|
|
4359
|
+
async fix(slides, scoringResult) {
|
|
4360
|
+
this.kb = await getKnowledgeGateway();
|
|
4361
|
+
const slidesFixed = JSON.parse(JSON.stringify(slides));
|
|
4362
|
+
const fixesApplied = [];
|
|
4363
|
+
const fixesSkipped = [];
|
|
4364
|
+
const autoFixableIssues = scoringResult.issues.filter((i) => i.autoFixable);
|
|
4365
|
+
for (const issue of autoFixableIssues) {
|
|
4366
|
+
const result = await this.applyFix(slidesFixed, issue);
|
|
4367
|
+
if (result.applied) {
|
|
4368
|
+
fixesApplied.push(result);
|
|
4369
|
+
} else {
|
|
4370
|
+
fixesSkipped.push(result);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
const summary = this.generateSummary(fixesApplied, fixesSkipped);
|
|
4374
|
+
return {
|
|
4375
|
+
slidesFixed,
|
|
4376
|
+
fixesApplied,
|
|
4377
|
+
fixesSkipped,
|
|
4378
|
+
summary
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4381
|
+
/**
|
|
4382
|
+
* Apply a single fix based on the issue.
|
|
4383
|
+
*/
|
|
4384
|
+
async applyFix(slides, issue) {
|
|
4385
|
+
const result = {
|
|
4386
|
+
slideIndex: issue.slideIndex,
|
|
4387
|
+
dimension: issue.dimension,
|
|
4388
|
+
originalValue: issue.currentValue,
|
|
4389
|
+
newValue: null,
|
|
4390
|
+
description: issue.message,
|
|
4391
|
+
applied: false
|
|
4392
|
+
};
|
|
4393
|
+
switch (issue.dimension) {
|
|
4394
|
+
case "content":
|
|
4395
|
+
return this.fixContentIssue(slides, issue, result);
|
|
4396
|
+
case "clarity":
|
|
4397
|
+
return this.fixClarityIssue(slides, issue, result);
|
|
4398
|
+
case "layout":
|
|
4399
|
+
return this.fixLayoutIssue(slides, issue, result);
|
|
4400
|
+
case "consistency":
|
|
4401
|
+
return this.fixConsistencyIssue(slides, issue, result);
|
|
4402
|
+
case "contrast":
|
|
4403
|
+
return this.fixContrastIssue(slides, issue, result);
|
|
4404
|
+
default:
|
|
4405
|
+
result.description = `No auto-fix available for ${issue.dimension}`;
|
|
4406
|
+
return result;
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
/**
|
|
4410
|
+
* Fix content-related issues (word count, bullet count).
|
|
4411
|
+
*/
|
|
4412
|
+
fixContentIssue(slides, issue, result) {
|
|
4413
|
+
if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
|
|
4414
|
+
return result;
|
|
4415
|
+
}
|
|
4416
|
+
const slide = slides[issue.slideIndex];
|
|
4417
|
+
if (!slide) return result;
|
|
4418
|
+
if (issue.message.includes("Word count") && issue.message.includes("exceeds")) {
|
|
4419
|
+
const maxWords = issue.expectedValue;
|
|
4420
|
+
result.originalValue = this.countWords(slide);
|
|
4421
|
+
if (slide.data.body) {
|
|
4422
|
+
slide.data.body = this.condenseText(slide.data.body, maxWords / 2);
|
|
4423
|
+
}
|
|
4424
|
+
if (slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4425
|
+
const bulletCount = slide.data.bullets.length;
|
|
4426
|
+
slide.data.bullets = slide.data.bullets.map(
|
|
4427
|
+
(bullet) => this.condenseText(bullet, Math.floor(maxWords / bulletCount))
|
|
4428
|
+
);
|
|
4429
|
+
}
|
|
4430
|
+
if (slide.data.subtitle) {
|
|
4431
|
+
slide.data.subtitle = this.condenseText(slide.data.subtitle, 10);
|
|
4432
|
+
}
|
|
4433
|
+
result.newValue = this.countWords(slide);
|
|
4434
|
+
result.applied = result.newValue <= maxWords;
|
|
4435
|
+
result.description = `Condensed content from ${result.originalValue} to ${result.newValue} words`;
|
|
4436
|
+
}
|
|
4437
|
+
if (issue.message.includes("bullets exceeds")) {
|
|
4438
|
+
const maxBullets = issue.expectedValue;
|
|
4439
|
+
if (slide.data.bullets) {
|
|
4440
|
+
result.originalValue = slide.data.bullets.length;
|
|
4441
|
+
slide.data.bullets = slide.data.bullets.slice(0, maxBullets);
|
|
4442
|
+
result.newValue = slide.data.bullets.length;
|
|
4443
|
+
result.applied = true;
|
|
4444
|
+
result.description = `Reduced bullets from ${result.originalValue} to ${result.newValue}`;
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
return result;
|
|
4448
|
+
}
|
|
4449
|
+
/**
|
|
4450
|
+
* Fix clarity-related issues (key message length, title length).
|
|
4451
|
+
*/
|
|
4452
|
+
fixClarityIssue(slides, issue, result) {
|
|
4453
|
+
if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
|
|
4454
|
+
return result;
|
|
4455
|
+
}
|
|
4456
|
+
const slide = slides[issue.slideIndex];
|
|
4457
|
+
if (!slide) return result;
|
|
4458
|
+
if (issue.message.includes("Key message too long")) {
|
|
4459
|
+
if (slide.data.keyMessage) {
|
|
4460
|
+
result.originalValue = slide.data.keyMessage;
|
|
4461
|
+
const maxWords = issue.expectedValue;
|
|
4462
|
+
slide.data.keyMessage = this.condenseText(slide.data.keyMessage, maxWords);
|
|
4463
|
+
result.newValue = slide.data.keyMessage;
|
|
4464
|
+
result.applied = true;
|
|
4465
|
+
result.description = `Shortened key message to ${maxWords} words`;
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
if (issue.message.includes("Title too long")) {
|
|
4469
|
+
if (slide.data.title) {
|
|
4470
|
+
result.originalValue = slide.data.title;
|
|
4471
|
+
const words = slide.data.title.split(/\s+/);
|
|
4472
|
+
const originalLength = words.length;
|
|
4473
|
+
if (words.length > 6) {
|
|
4474
|
+
slide.data.title = words.slice(0, 6).join(" ");
|
|
4475
|
+
}
|
|
4476
|
+
result.newValue = slide.data.title;
|
|
4477
|
+
result.applied = true;
|
|
4478
|
+
result.description = `Shortened title from ${originalLength} to ${slide.data.title.split(/\s+/).length} words`;
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
if (issue.message.includes("Too many elements")) {
|
|
4482
|
+
result.originalValue = issue.currentValue;
|
|
4483
|
+
if (slide.data.subtitle && slide.data.body) {
|
|
4484
|
+
delete slide.data.subtitle;
|
|
4485
|
+
result.applied = true;
|
|
4486
|
+
result.description = "Removed subtitle to reduce element count";
|
|
4487
|
+
} else if (slide.data.body && slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4488
|
+
delete slide.data.body;
|
|
4489
|
+
result.applied = true;
|
|
4490
|
+
result.description = "Removed body text, keeping bullets";
|
|
4491
|
+
}
|
|
4492
|
+
result.newValue = this.countElements(slide);
|
|
4493
|
+
}
|
|
4494
|
+
return result;
|
|
2758
4495
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
if (
|
|
2764
|
-
|
|
4496
|
+
/**
|
|
4497
|
+
* Fix layout-related issues.
|
|
4498
|
+
*/
|
|
4499
|
+
fixLayoutIssue(slides, issue, result) {
|
|
4500
|
+
if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
|
|
4501
|
+
return result;
|
|
4502
|
+
}
|
|
4503
|
+
const slide = slides[issue.slideIndex];
|
|
4504
|
+
if (!slide) return result;
|
|
4505
|
+
if (issue.message.includes("Insufficient whitespace")) {
|
|
4506
|
+
const currentWordCount = this.countWords(slide);
|
|
4507
|
+
const targetReduction = 0.3;
|
|
4508
|
+
const targetWords = Math.floor(currentWordCount * (1 - targetReduction));
|
|
4509
|
+
result.originalValue = currentWordCount;
|
|
4510
|
+
if (slide.data.body) {
|
|
4511
|
+
slide.data.body = this.condenseText(slide.data.body, Math.floor(targetWords * 0.5));
|
|
4512
|
+
}
|
|
4513
|
+
if (slide.data.bullets && slide.data.bullets.length > 0) {
|
|
4514
|
+
const wordsPerBullet = Math.floor(targetWords / (slide.data.bullets.length * 2));
|
|
4515
|
+
slide.data.bullets = slide.data.bullets.map((b) => this.condenseText(b, wordsPerBullet));
|
|
4516
|
+
}
|
|
4517
|
+
result.newValue = this.countWords(slide);
|
|
4518
|
+
result.applied = true;
|
|
4519
|
+
result.description = `Reduced content from ${result.originalValue} to ${result.newValue} words for better whitespace`;
|
|
4520
|
+
}
|
|
4521
|
+
return result;
|
|
2765
4522
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
slide
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
});
|
|
2779
|
-
});
|
|
2780
|
-
});
|
|
2781
|
-
content.perSlide.forEach((slide) => {
|
|
2782
|
-
slide.issues.forEach((issue) => {
|
|
2783
|
-
issues.push({
|
|
2784
|
-
severity: "warning",
|
|
2785
|
-
category: "content",
|
|
2786
|
-
slideIndex: slide.slideIndex,
|
|
2787
|
-
message: issue
|
|
2788
|
-
});
|
|
2789
|
-
});
|
|
2790
|
-
});
|
|
2791
|
-
content.glanceTest.filter((g) => !g.passed).forEach((g) => {
|
|
2792
|
-
const issue = {
|
|
2793
|
-
severity: "warning",
|
|
2794
|
-
category: "content",
|
|
2795
|
-
slideIndex: g.slideIndex,
|
|
2796
|
-
message: `Glance test failed: "${g.keyMessage.substring(0, 50)}..." takes ${g.readingTime}s to read`
|
|
2797
|
-
};
|
|
2798
|
-
if (g.recommendation) {
|
|
2799
|
-
issue.suggestion = g.recommendation;
|
|
4523
|
+
/**
|
|
4524
|
+
* Fix consistency-related issues.
|
|
4525
|
+
*/
|
|
4526
|
+
fixConsistencyIssue(slides, issue, result) {
|
|
4527
|
+
if (issue.message.includes("Inconsistent title casing")) {
|
|
4528
|
+
let fixedCount = 0;
|
|
4529
|
+
for (const slide of slides) {
|
|
4530
|
+
if (slide.data.title) {
|
|
4531
|
+
const original = slide.data.title;
|
|
4532
|
+
slide.data.title = this.toTitleCase(slide.data.title);
|
|
4533
|
+
if (slide.data.title !== original) fixedCount++;
|
|
4534
|
+
}
|
|
2800
4535
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
category: "expert",
|
|
2808
|
-
message: `${e.expertName}: ${v}`
|
|
2809
|
-
});
|
|
2810
|
-
});
|
|
2811
|
-
});
|
|
2812
|
-
accessibility.fontSizeIssues.forEach((issue) => {
|
|
2813
|
-
issues.push({
|
|
2814
|
-
severity: "error",
|
|
2815
|
-
category: "accessibility",
|
|
2816
|
-
slideIndex: issue.slideIndex,
|
|
2817
|
-
message: `Font size ${issue.actualSize}px below minimum ${issue.minimumSize}px`,
|
|
2818
|
-
suggestion: `Increase font size to at least ${issue.minimumSize}px`
|
|
2819
|
-
});
|
|
2820
|
-
});
|
|
2821
|
-
accessibility.contrastIssues.forEach((issue) => {
|
|
2822
|
-
issues.push({
|
|
2823
|
-
severity: "error",
|
|
2824
|
-
category: "accessibility",
|
|
2825
|
-
slideIndex: issue.slideIndex,
|
|
2826
|
-
message: `Contrast ratio ${issue.ratio.toFixed(2)} below required ${issue.required}`,
|
|
2827
|
-
suggestion: "Increase contrast between text and background"
|
|
2828
|
-
});
|
|
2829
|
-
});
|
|
2830
|
-
return issues;
|
|
4536
|
+
result.originalValue = "Mixed casing";
|
|
4537
|
+
result.newValue = "Title Case";
|
|
4538
|
+
result.applied = fixedCount > 0;
|
|
4539
|
+
result.description = `Applied Title Case to ${fixedCount} slide titles`;
|
|
4540
|
+
}
|
|
4541
|
+
return result;
|
|
2831
4542
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
4543
|
+
/**
|
|
4544
|
+
* Fix contrast-related issues.
|
|
4545
|
+
* Note: These are CSS fixes, typically handled at generation time.
|
|
4546
|
+
*/
|
|
4547
|
+
fixContrastIssue(slides, issue, result) {
|
|
4548
|
+
result.description = "Contrast issues flagged for CSS regeneration";
|
|
4549
|
+
result.applied = false;
|
|
4550
|
+
return result;
|
|
4551
|
+
}
|
|
4552
|
+
/**
|
|
4553
|
+
* Condense text to approximately maxWords.
|
|
4554
|
+
* Uses smart truncation that preserves meaning.
|
|
4555
|
+
*/
|
|
4556
|
+
condenseText(text, maxWords) {
|
|
4557
|
+
const words = text.split(/\s+/);
|
|
4558
|
+
if (words.length <= maxWords) {
|
|
4559
|
+
return text;
|
|
4560
|
+
}
|
|
4561
|
+
const fillerWords = /* @__PURE__ */ new Set([
|
|
4562
|
+
"very",
|
|
4563
|
+
"really",
|
|
4564
|
+
"actually",
|
|
4565
|
+
"basically",
|
|
4566
|
+
"literally",
|
|
4567
|
+
"obviously",
|
|
4568
|
+
"clearly",
|
|
4569
|
+
"simply",
|
|
4570
|
+
"just",
|
|
4571
|
+
"that",
|
|
4572
|
+
"which",
|
|
4573
|
+
"would",
|
|
4574
|
+
"could",
|
|
4575
|
+
"should",
|
|
4576
|
+
"might"
|
|
4577
|
+
]);
|
|
4578
|
+
let filtered = words.filter((w) => !fillerWords.has(w.toLowerCase()));
|
|
4579
|
+
if (filtered.length <= maxWords) {
|
|
4580
|
+
return filtered.join(" ");
|
|
4581
|
+
}
|
|
4582
|
+
const punctuation = [".", ",", ";", ":", "-"];
|
|
4583
|
+
let breakPoint = maxWords;
|
|
4584
|
+
for (let i = maxWords - 1; i >= maxWords - 5 && i >= 0; i--) {
|
|
4585
|
+
const word = filtered[i];
|
|
4586
|
+
if (word && punctuation.some((p) => word.endsWith(p))) {
|
|
4587
|
+
breakPoint = i + 1;
|
|
4588
|
+
break;
|
|
4589
|
+
}
|
|
2838
4590
|
}
|
|
4591
|
+
filtered = filtered.slice(0, breakPoint);
|
|
4592
|
+
let result = filtered.join(" ");
|
|
4593
|
+
if (!result.endsWith(".") && !result.endsWith("!") && !result.endsWith("?")) {
|
|
4594
|
+
result = result.replace(/[,;:]$/, "") + "...";
|
|
4595
|
+
}
|
|
4596
|
+
return result;
|
|
2839
4597
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
4598
|
+
/**
|
|
4599
|
+
* Convert text to Title Case.
|
|
4600
|
+
*/
|
|
4601
|
+
toTitleCase(text) {
|
|
4602
|
+
const minorWords = /* @__PURE__ */ new Set([
|
|
4603
|
+
"a",
|
|
4604
|
+
"an",
|
|
4605
|
+
"the",
|
|
4606
|
+
"and",
|
|
4607
|
+
"but",
|
|
4608
|
+
"or",
|
|
4609
|
+
"for",
|
|
4610
|
+
"nor",
|
|
4611
|
+
"on",
|
|
4612
|
+
"at",
|
|
4613
|
+
"to",
|
|
4614
|
+
"by",
|
|
4615
|
+
"of",
|
|
4616
|
+
"in",
|
|
4617
|
+
"with"
|
|
4618
|
+
]);
|
|
4619
|
+
return text.split(" ").map((word, index) => {
|
|
4620
|
+
if (index === 0) {
|
|
4621
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
4622
|
+
}
|
|
4623
|
+
if (minorWords.has(word.toLowerCase())) {
|
|
4624
|
+
return word.toLowerCase();
|
|
4625
|
+
}
|
|
4626
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
4627
|
+
}).join(" ");
|
|
4628
|
+
}
|
|
4629
|
+
/**
|
|
4630
|
+
* Count words in a slide.
|
|
4631
|
+
*/
|
|
4632
|
+
countWords(slide) {
|
|
4633
|
+
let text = "";
|
|
4634
|
+
if (slide.data.title) text += slide.data.title + " ";
|
|
4635
|
+
if (slide.data.subtitle) text += slide.data.subtitle + " ";
|
|
4636
|
+
if (slide.data.body) text += slide.data.body + " ";
|
|
4637
|
+
if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
|
|
4638
|
+
if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
|
|
4639
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
4640
|
+
}
|
|
4641
|
+
/**
|
|
4642
|
+
* Count elements in a slide.
|
|
4643
|
+
*/
|
|
4644
|
+
countElements(slide) {
|
|
4645
|
+
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);
|
|
4646
|
+
}
|
|
4647
|
+
/**
|
|
4648
|
+
* Generate a summary of fixes applied.
|
|
4649
|
+
*/
|
|
4650
|
+
generateSummary(applied, skipped) {
|
|
4651
|
+
const lines = [];
|
|
4652
|
+
lines.push(`
|
|
4653
|
+
\u{1F527} Auto-Fix Summary`);
|
|
4654
|
+
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`);
|
|
4655
|
+
lines.push(`Fixes Applied: ${applied.length}`);
|
|
4656
|
+
lines.push(`Fixes Skipped: ${skipped.length}`);
|
|
4657
|
+
lines.push("");
|
|
4658
|
+
if (applied.length > 0) {
|
|
4659
|
+
lines.push("\u2705 Applied Fixes:");
|
|
4660
|
+
for (const fix of applied) {
|
|
4661
|
+
lines.push(` \u2022 ${fix.description}`);
|
|
4662
|
+
}
|
|
4663
|
+
lines.push("");
|
|
4664
|
+
}
|
|
4665
|
+
if (skipped.length > 0) {
|
|
4666
|
+
lines.push("\u26A0\uFE0F Skipped Fixes (require manual attention):");
|
|
4667
|
+
for (const fix of skipped.slice(0, 5)) {
|
|
4668
|
+
lines.push(` \u2022 ${fix.description}`);
|
|
4669
|
+
}
|
|
4670
|
+
if (skipped.length > 5) {
|
|
4671
|
+
lines.push(` ... and ${skipped.length - 5} more`);
|
|
4672
|
+
}
|
|
2844
4673
|
}
|
|
4674
|
+
return lines.join("\n");
|
|
2845
4675
|
}
|
|
2846
4676
|
};
|
|
4677
|
+
function createAutoFixEngine(mode, presentationType) {
|
|
4678
|
+
return new AutoFixEngine(mode, presentationType);
|
|
4679
|
+
}
|
|
2847
4680
|
|
|
2848
4681
|
// src/generators/html/RevealJsGenerator.ts
|
|
2849
4682
|
var RevealJsGenerator = class {
|
|
@@ -3439,6 +5272,182 @@ ${slides}
|
|
|
3439
5272
|
}
|
|
3440
5273
|
};
|
|
3441
5274
|
|
|
5275
|
+
// src/qa/IterativeQAEngine.ts
|
|
5276
|
+
var DEFAULT_OPTIONS = {
|
|
5277
|
+
minScore: 95,
|
|
5278
|
+
maxIterations: 5,
|
|
5279
|
+
verbose: true
|
|
5280
|
+
};
|
|
5281
|
+
var IterativeQAEngine = class {
|
|
5282
|
+
kb;
|
|
5283
|
+
scorer;
|
|
5284
|
+
fixer;
|
|
5285
|
+
generator;
|
|
5286
|
+
mode;
|
|
5287
|
+
presentationType;
|
|
5288
|
+
config;
|
|
5289
|
+
constructor(mode, presentationType, config) {
|
|
5290
|
+
this.mode = mode;
|
|
5291
|
+
this.presentationType = presentationType;
|
|
5292
|
+
this.config = config;
|
|
5293
|
+
this.scorer = new SevenDimensionScorer(mode, presentationType);
|
|
5294
|
+
this.fixer = new AutoFixEngine(mode, presentationType);
|
|
5295
|
+
this.generator = new RevealJsGenerator();
|
|
5296
|
+
}
|
|
5297
|
+
/**
|
|
5298
|
+
* Run the iterative QA process.
|
|
5299
|
+
*/
|
|
5300
|
+
async run(initialSlides, initialHtml, options = {}) {
|
|
5301
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
5302
|
+
this.kb = await getKnowledgeGateway();
|
|
5303
|
+
const iterations = [];
|
|
5304
|
+
let currentSlides = initialSlides;
|
|
5305
|
+
let currentHtml = initialHtml;
|
|
5306
|
+
let scoringResult;
|
|
5307
|
+
let autoFixSummary = "";
|
|
5308
|
+
let totalFixesApplied = 0;
|
|
5309
|
+
if (opts.verbose) {
|
|
5310
|
+
console.log("\n\u{1F504} Starting Iterative QA Process");
|
|
5311
|
+
console.log(` Target Score: ${opts.minScore}/100`);
|
|
5312
|
+
console.log(` Max Iterations: ${opts.maxIterations}`);
|
|
5313
|
+
console.log("");
|
|
5314
|
+
}
|
|
5315
|
+
for (let i = 0; i < opts.maxIterations; i++) {
|
|
5316
|
+
const iterationNum = i + 1;
|
|
5317
|
+
if (opts.verbose) {
|
|
5318
|
+
console.log(`\u{1F4CA} Iteration ${iterationNum}/${opts.maxIterations}...`);
|
|
5319
|
+
}
|
|
5320
|
+
scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
|
|
5321
|
+
iterations.push({
|
|
5322
|
+
iteration: iterationNum,
|
|
5323
|
+
score: scoringResult.overallScore,
|
|
5324
|
+
dimensionScores: {
|
|
5325
|
+
layout: scoringResult.dimensions.layout.score,
|
|
5326
|
+
contrast: scoringResult.dimensions.contrast.score,
|
|
5327
|
+
graphics: scoringResult.dimensions.graphics.score,
|
|
5328
|
+
content: scoringResult.dimensions.content.score,
|
|
5329
|
+
clarity: scoringResult.dimensions.clarity.score,
|
|
5330
|
+
effectiveness: scoringResult.dimensions.effectiveness.score,
|
|
5331
|
+
consistency: scoringResult.dimensions.consistency.score
|
|
5332
|
+
},
|
|
5333
|
+
fixesApplied: 0,
|
|
5334
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
5335
|
+
});
|
|
5336
|
+
if (opts.verbose) {
|
|
5337
|
+
console.log(` Score: ${scoringResult.overallScore}/100`);
|
|
5338
|
+
}
|
|
5339
|
+
if (scoringResult.passed) {
|
|
5340
|
+
if (opts.verbose) {
|
|
5341
|
+
console.log(` \u2705 PASSED - Score meets threshold (${opts.minScore})`);
|
|
5342
|
+
}
|
|
5343
|
+
break;
|
|
5344
|
+
}
|
|
5345
|
+
if (i < opts.maxIterations - 1) {
|
|
5346
|
+
if (opts.verbose) {
|
|
5347
|
+
console.log(` \u26A0\uFE0F Below threshold, applying auto-fixes...`);
|
|
5348
|
+
}
|
|
5349
|
+
const fixResult = await this.fixer.fix(currentSlides, scoringResult);
|
|
5350
|
+
if (fixResult.fixesApplied.length > 0) {
|
|
5351
|
+
currentSlides = fixResult.slidesFixed;
|
|
5352
|
+
currentHtml = await this.generator.generate(currentSlides, this.config);
|
|
5353
|
+
const lastIteration = iterations[iterations.length - 1];
|
|
5354
|
+
if (lastIteration) {
|
|
5355
|
+
lastIteration.fixesApplied = fixResult.fixesApplied.length;
|
|
5356
|
+
}
|
|
5357
|
+
totalFixesApplied += fixResult.fixesApplied.length;
|
|
5358
|
+
if (opts.verbose) {
|
|
5359
|
+
console.log(` \u{1F527} Applied ${fixResult.fixesApplied.length} fixes`);
|
|
5360
|
+
}
|
|
5361
|
+
autoFixSummary += fixResult.summary + "\n";
|
|
5362
|
+
} else {
|
|
5363
|
+
if (opts.verbose) {
|
|
5364
|
+
console.log(` \u26A0\uFE0F No auto-fixes available, manual review needed`);
|
|
5365
|
+
}
|
|
5366
|
+
break;
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
if (!scoringResult.passed) {
|
|
5371
|
+
scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
|
|
5372
|
+
}
|
|
5373
|
+
const report = this.generateReport(
|
|
5374
|
+
scoringResult,
|
|
5375
|
+
iterations,
|
|
5376
|
+
opts,
|
|
5377
|
+
totalFixesApplied
|
|
5378
|
+
);
|
|
5379
|
+
if (opts.verbose) {
|
|
5380
|
+
console.log(report);
|
|
5381
|
+
}
|
|
5382
|
+
return {
|
|
5383
|
+
finalScore: scoringResult.overallScore,
|
|
5384
|
+
passed: scoringResult.passed,
|
|
5385
|
+
threshold: opts.minScore,
|
|
5386
|
+
iterations,
|
|
5387
|
+
totalIterations: iterations.length,
|
|
5388
|
+
maxIterations: opts.maxIterations,
|
|
5389
|
+
slides: currentSlides,
|
|
5390
|
+
html: currentHtml,
|
|
5391
|
+
finalScoring: scoringResult,
|
|
5392
|
+
autoFixSummary,
|
|
5393
|
+
report
|
|
5394
|
+
};
|
|
5395
|
+
}
|
|
5396
|
+
/**
|
|
5397
|
+
* Generate a comprehensive report.
|
|
5398
|
+
*/
|
|
5399
|
+
generateReport(finalScoring, iterations, options, totalFixesApplied) {
|
|
5400
|
+
const lines = [];
|
|
5401
|
+
lines.push("");
|
|
5402
|
+
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");
|
|
5403
|
+
lines.push("\u2551 ITERATIVE QA FINAL REPORT \u2551");
|
|
5404
|
+
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");
|
|
5405
|
+
lines.push("");
|
|
5406
|
+
const passStatus = finalScoring.passed ? "\u2705 PASSED" : "\u274C FAILED";
|
|
5407
|
+
lines.push(`Final Score: ${finalScoring.overallScore}/100 ${passStatus}`);
|
|
5408
|
+
lines.push(`Threshold: ${options.minScore}/100`);
|
|
5409
|
+
lines.push(`Iterations: ${iterations.length}/${options.maxIterations}`);
|
|
5410
|
+
lines.push(`Total Fixes Applied: ${totalFixesApplied}`);
|
|
5411
|
+
lines.push("");
|
|
5412
|
+
if (iterations.length > 1) {
|
|
5413
|
+
lines.push("Score Progression:");
|
|
5414
|
+
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");
|
|
5415
|
+
for (const iter of iterations) {
|
|
5416
|
+
const bar = "\u2588".repeat(Math.floor(iter.score / 10)) + "\u2591".repeat(10 - Math.floor(iter.score / 10));
|
|
5417
|
+
lines.push(` Iter ${iter.iteration}: ${bar} ${iter.score}/100 (+${iter.fixesApplied} fixes)`);
|
|
5418
|
+
}
|
|
5419
|
+
lines.push("");
|
|
5420
|
+
}
|
|
5421
|
+
lines.push(this.scorer.formatReport(finalScoring));
|
|
5422
|
+
lines.push("");
|
|
5423
|
+
lines.push("\u{1F4DA} Knowledge Base Compliance:");
|
|
5424
|
+
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");
|
|
5425
|
+
lines.push(` Mode: ${this.mode}`);
|
|
5426
|
+
lines.push(` Presentation Type: ${this.presentationType}`);
|
|
5427
|
+
lines.push(` Word Limits: ${this.mode === "keynote" ? "6-25" : "40-80"} per slide`);
|
|
5428
|
+
lines.push(` Expert Frameworks: Duarte, Reynolds, Gallo, Anderson`);
|
|
5429
|
+
lines.push("");
|
|
5430
|
+
if (!finalScoring.passed) {
|
|
5431
|
+
lines.push("\u{1F4CB} Recommendations for Manual Review:");
|
|
5432
|
+
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");
|
|
5433
|
+
const manualIssues = finalScoring.issues.filter((i) => !i.autoFixable);
|
|
5434
|
+
for (const issue of manualIssues.slice(0, 5)) {
|
|
5435
|
+
lines.push(` \u2022 ${issue.message}`);
|
|
5436
|
+
if (issue.fixSuggestion) {
|
|
5437
|
+
lines.push(` \u2192 ${issue.fixSuggestion}`);
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
if (manualIssues.length > 5) {
|
|
5441
|
+
lines.push(` ... and ${manualIssues.length - 5} more issues`);
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
return lines.join("\n");
|
|
5445
|
+
}
|
|
5446
|
+
};
|
|
5447
|
+
function createIterativeQAEngine(mode, presentationType, config) {
|
|
5448
|
+
return new IterativeQAEngine(mode, presentationType, config);
|
|
5449
|
+
}
|
|
5450
|
+
|
|
3442
5451
|
// src/generators/pptx/PowerPointGenerator.ts
|
|
3443
5452
|
import PptxGenJS from "pptxgenjs";
|
|
3444
5453
|
|
|
@@ -4173,15 +6182,14 @@ var PowerPointGenerator = class {
|
|
|
4173
6182
|
// src/core/PresentationEngine.ts
|
|
4174
6183
|
var PresentationEngine = class {
|
|
4175
6184
|
contentAnalyzer;
|
|
4176
|
-
slideFactory;
|
|
4177
6185
|
templateEngine;
|
|
4178
6186
|
scoreCalculator;
|
|
4179
6187
|
qaEngine;
|
|
4180
6188
|
htmlGenerator;
|
|
4181
6189
|
pptxGenerator;
|
|
6190
|
+
kb;
|
|
4182
6191
|
constructor() {
|
|
4183
6192
|
this.contentAnalyzer = new ContentAnalyzer();
|
|
4184
|
-
this.slideFactory = new SlideFactory();
|
|
4185
6193
|
this.templateEngine = new TemplateEngine();
|
|
4186
6194
|
this.scoreCalculator = new ScoreCalculator();
|
|
4187
6195
|
this.qaEngine = new QAEngine();
|
|
@@ -4196,10 +6204,15 @@ var PresentationEngine = class {
|
|
|
4196
6204
|
*/
|
|
4197
6205
|
async generate(config) {
|
|
4198
6206
|
this.validateConfig(config);
|
|
6207
|
+
console.log("\u{1F4DA} Loading knowledge base...");
|
|
6208
|
+
this.kb = await getKnowledgeGateway();
|
|
4199
6209
|
console.log("\u{1F4DD} Analyzing content...");
|
|
4200
6210
|
const analysis = await this.contentAnalyzer.analyze(config.content, config.contentType);
|
|
6211
|
+
const presentationType = config.presentationType || analysis.detectedType;
|
|
6212
|
+
console.log(` \u2713 Presentation type: ${presentationType}`);
|
|
6213
|
+
const slideFactory = createSlideFactory(this.kb, presentationType);
|
|
4201
6214
|
console.log("\u{1F3A8} Creating slides...");
|
|
4202
|
-
const slides = await
|
|
6215
|
+
const slides = await slideFactory.createSlides(analysis);
|
|
4203
6216
|
console.log("\u2705 Validating structure...");
|
|
4204
6217
|
const structureErrors = this.validateStructure(slides, config.mode);
|
|
4205
6218
|
if (structureErrors.length > 0) {
|
|
@@ -4220,23 +6233,52 @@ var PresentationEngine = class {
|
|
|
4220
6233
|
}
|
|
4221
6234
|
let qaResults;
|
|
4222
6235
|
let score = 100;
|
|
6236
|
+
let iterativeResult = null;
|
|
6237
|
+
let finalSlides = slides;
|
|
6238
|
+
let finalHtml = outputs.html;
|
|
4223
6239
|
if (!config.skipQA && outputs.html) {
|
|
4224
|
-
console.log("\u{1F50D} Running QA validation...");
|
|
4225
|
-
qaResults = await this.qaEngine.validate(outputs.html, {
|
|
4226
|
-
mode: config.mode,
|
|
4227
|
-
strictMode: true
|
|
4228
|
-
});
|
|
4229
|
-
score = this.scoreCalculator.calculate(qaResults);
|
|
4230
|
-
console.log(`\u{1F4CA} QA Score: ${score}/100`);
|
|
4231
6240
|
const threshold = config.qaThreshold ?? 95;
|
|
4232
|
-
|
|
4233
|
-
|
|
6241
|
+
const maxIterations = config.maxIterations ?? 5;
|
|
6242
|
+
const useIterativeQA = config.useIterativeQA !== false;
|
|
6243
|
+
if (useIterativeQA) {
|
|
6244
|
+
console.log("\u{1F50D} Running Iterative QA (7-Dimension Scoring)...");
|
|
6245
|
+
const iterativeEngine = createIterativeQAEngine(
|
|
6246
|
+
config.mode,
|
|
6247
|
+
presentationType,
|
|
6248
|
+
config
|
|
6249
|
+
);
|
|
6250
|
+
iterativeResult = await iterativeEngine.run(slides, outputs.html, {
|
|
6251
|
+
minScore: threshold,
|
|
6252
|
+
maxIterations,
|
|
6253
|
+
verbose: true
|
|
6254
|
+
});
|
|
6255
|
+
score = iterativeResult.finalScore;
|
|
6256
|
+
finalSlides = iterativeResult.slides;
|
|
6257
|
+
finalHtml = iterativeResult.html;
|
|
6258
|
+
if (outputs.html) {
|
|
6259
|
+
outputs.html = finalHtml;
|
|
6260
|
+
}
|
|
6261
|
+
qaResults = this.buildQAResultsFrom7Dimension(iterativeResult);
|
|
6262
|
+
if (!iterativeResult.passed) {
|
|
6263
|
+
throw new QAFailureError(score, threshold, qaResults);
|
|
6264
|
+
}
|
|
6265
|
+
} else {
|
|
6266
|
+
console.log("\u{1F50D} Running QA validation (legacy mode)...");
|
|
6267
|
+
qaResults = await this.qaEngine.validate(outputs.html, {
|
|
6268
|
+
mode: config.mode,
|
|
6269
|
+
strictMode: true
|
|
6270
|
+
});
|
|
6271
|
+
score = this.scoreCalculator.calculate(qaResults);
|
|
6272
|
+
console.log(`\u{1F4CA} QA Score: ${score}/100`);
|
|
6273
|
+
if (score < threshold) {
|
|
6274
|
+
throw new QAFailureError(score, threshold, qaResults);
|
|
6275
|
+
}
|
|
4234
6276
|
}
|
|
4235
6277
|
} else {
|
|
4236
6278
|
qaResults = this.qaEngine.createEmptyResults();
|
|
4237
6279
|
console.log("\u26A0\uFE0F QA validation skipped (NOT RECOMMENDED)");
|
|
4238
6280
|
}
|
|
4239
|
-
const metadata = this.buildMetadata(config, analysis,
|
|
6281
|
+
const metadata = this.buildMetadata(config, analysis, finalSlides, iterativeResult);
|
|
4240
6282
|
return {
|
|
4241
6283
|
outputs,
|
|
4242
6284
|
qaResults,
|
|
@@ -4244,6 +6286,46 @@ var PresentationEngine = class {
|
|
|
4244
6286
|
metadata
|
|
4245
6287
|
};
|
|
4246
6288
|
}
|
|
6289
|
+
/**
|
|
6290
|
+
* Build QA results structure from 7-dimension scoring.
|
|
6291
|
+
*/
|
|
6292
|
+
buildQAResultsFrom7Dimension(iterativeResult) {
|
|
6293
|
+
const scoring = iterativeResult.finalScoring;
|
|
6294
|
+
return {
|
|
6295
|
+
passed: scoring.passed,
|
|
6296
|
+
score: scoring.overallScore,
|
|
6297
|
+
visual: {
|
|
6298
|
+
whitespacePercentage: scoring.dimensions.layout.score,
|
|
6299
|
+
layoutBalance: scoring.dimensions.layout.score / 100,
|
|
6300
|
+
colorContrast: scoring.dimensions.contrast.score / 100
|
|
6301
|
+
},
|
|
6302
|
+
content: {
|
|
6303
|
+
perSlide: [],
|
|
6304
|
+
issues: scoring.issues.filter((i) => i.dimension === "content")
|
|
6305
|
+
},
|
|
6306
|
+
accessibility: {
|
|
6307
|
+
wcagLevel: scoring.dimensions.contrast.score >= 95 ? "AAA" : "AA",
|
|
6308
|
+
issues: scoring.issues.filter((i) => i.dimension === "contrast")
|
|
6309
|
+
},
|
|
6310
|
+
issues: scoring.issues.map((issue) => ({
|
|
6311
|
+
severity: issue.severity,
|
|
6312
|
+
message: issue.message,
|
|
6313
|
+
slideIndex: issue.slideIndex,
|
|
6314
|
+
dimension: issue.dimension
|
|
6315
|
+
})),
|
|
6316
|
+
dimensions: {
|
|
6317
|
+
layout: scoring.dimensions.layout.score,
|
|
6318
|
+
contrast: scoring.dimensions.contrast.score,
|
|
6319
|
+
graphics: scoring.dimensions.graphics.score,
|
|
6320
|
+
content: scoring.dimensions.content.score,
|
|
6321
|
+
clarity: scoring.dimensions.clarity.score,
|
|
6322
|
+
effectiveness: scoring.dimensions.effectiveness.score,
|
|
6323
|
+
consistency: scoring.dimensions.consistency.score
|
|
6324
|
+
},
|
|
6325
|
+
iterations: iterativeResult.iterations,
|
|
6326
|
+
report: iterativeResult.report
|
|
6327
|
+
};
|
|
6328
|
+
}
|
|
4247
6329
|
/**
|
|
4248
6330
|
* Validate presentation configuration.
|
|
4249
6331
|
*/
|
|
@@ -4314,13 +6396,13 @@ var PresentationEngine = class {
|
|
|
4314
6396
|
/**
|
|
4315
6397
|
* Build presentation metadata.
|
|
4316
6398
|
*/
|
|
4317
|
-
buildMetadata(config, analysis, slides) {
|
|
6399
|
+
buildMetadata(config, analysis, slides, iterativeResult) {
|
|
4318
6400
|
const wordCounts = slides.map((s) => this.countWords(s));
|
|
4319
6401
|
const totalWords = wordCounts.reduce((sum, count) => sum + count, 0);
|
|
4320
6402
|
const avgWordsPerSlide = Math.round(totalWords / slides.length);
|
|
4321
6403
|
const minutesPerSlide = config.mode === "keynote" ? 1.5 : 2;
|
|
4322
6404
|
const estimatedDuration = Math.round(slides.length * minutesPerSlide);
|
|
4323
|
-
|
|
6405
|
+
const metadata = {
|
|
4324
6406
|
title: config.title,
|
|
4325
6407
|
author: config.author ?? "Unknown",
|
|
4326
6408
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4329,8 +6411,15 @@ var PresentationEngine = class {
|
|
|
4329
6411
|
wordCount: totalWords,
|
|
4330
6412
|
avgWordsPerSlide,
|
|
4331
6413
|
estimatedDuration,
|
|
4332
|
-
frameworks: this.detectFrameworks(analysis)
|
|
6414
|
+
frameworks: this.detectFrameworks(analysis),
|
|
6415
|
+
presentationType: config.presentationType || analysis.detectedType
|
|
4333
6416
|
};
|
|
6417
|
+
if (iterativeResult) {
|
|
6418
|
+
metadata.qaIterations = iterativeResult.totalIterations;
|
|
6419
|
+
metadata.qaMaxIterations = iterativeResult.maxIterations;
|
|
6420
|
+
metadata.dimensionScores = iterativeResult.finalScoring.dimensions;
|
|
6421
|
+
}
|
|
6422
|
+
return metadata;
|
|
4334
6423
|
}
|
|
4335
6424
|
/**
|
|
4336
6425
|
* Detect which expert frameworks were applied.
|
|
@@ -4353,6 +6442,95 @@ var PresentationEngine = class {
|
|
|
4353
6442
|
}
|
|
4354
6443
|
};
|
|
4355
6444
|
|
|
6445
|
+
// src/core/SlideGenerator.ts
|
|
6446
|
+
var SlideGenerator = class {
|
|
6447
|
+
kb;
|
|
6448
|
+
factory;
|
|
6449
|
+
mode = "keynote";
|
|
6450
|
+
presentationType = "ted_keynote";
|
|
6451
|
+
async initialize() {
|
|
6452
|
+
this.kb = await getKnowledgeGateway();
|
|
6453
|
+
}
|
|
6454
|
+
/**
|
|
6455
|
+
* Generate slides from analyzed content using KB-driven SlideFactory.
|
|
6456
|
+
*
|
|
6457
|
+
* @version 7.0.0 - Now delegates ALL slide creation to SlideFactory
|
|
6458
|
+
* SlideFactory queries KB for every decision:
|
|
6459
|
+
* - Allowed slide types per presentation type
|
|
6460
|
+
* - Word limits per type
|
|
6461
|
+
* - Bullet limits per type
|
|
6462
|
+
* - Content pattern to slide type mapping
|
|
6463
|
+
* - Slide validation against KB rules
|
|
6464
|
+
*/
|
|
6465
|
+
async generate(analysis, type) {
|
|
6466
|
+
await this.initialize();
|
|
6467
|
+
this.presentationType = type || analysis.detectedType;
|
|
6468
|
+
this.mode = this.kb.getModeForType(this.presentationType);
|
|
6469
|
+
console.log(` \u2713 Using ${this.mode} mode for ${this.presentationType}`);
|
|
6470
|
+
this.factory = createSlideFactory(this.kb, this.presentationType);
|
|
6471
|
+
const slides = await this.factory.createSlides(analysis);
|
|
6472
|
+
const legacySlides = this.convertToLegacyFormat(slides);
|
|
6473
|
+
console.log(` \u2713 Generated ${legacySlides.length} KB-validated slides`);
|
|
6474
|
+
return legacySlides;
|
|
6475
|
+
}
|
|
6476
|
+
/**
|
|
6477
|
+
* Convert new Slide format to legacy format for backwards compatibility
|
|
6478
|
+
*/
|
|
6479
|
+
convertToLegacyFormat(slides) {
|
|
6480
|
+
return slides.map((slide) => ({
|
|
6481
|
+
index: slide.index,
|
|
6482
|
+
type: slide.type,
|
|
6483
|
+
title: slide.data.title || "",
|
|
6484
|
+
content: {
|
|
6485
|
+
subtitle: slide.data.subtitle,
|
|
6486
|
+
statement: slide.data.body,
|
|
6487
|
+
body: slide.data.body,
|
|
6488
|
+
bullets: slide.data.bullets,
|
|
6489
|
+
metrics: slide.data.metrics,
|
|
6490
|
+
quote: slide.data.quote ? { text: slide.data.quote, attribution: slide.data.attribution } : void 0,
|
|
6491
|
+
callToAction: slide.data.keyMessage
|
|
6492
|
+
},
|
|
6493
|
+
notes: slide.notes,
|
|
6494
|
+
template: this.mapTypeToTemplate(slide.type)
|
|
6495
|
+
}));
|
|
6496
|
+
}
|
|
6497
|
+
/**
|
|
6498
|
+
* Map slide type to template name for backwards compatibility
|
|
6499
|
+
*/
|
|
6500
|
+
mapTypeToTemplate(type) {
|
|
6501
|
+
const templateMap = {
|
|
6502
|
+
"title": "Title Impact",
|
|
6503
|
+
"agenda": "Detailed Findings",
|
|
6504
|
+
"big-number": "Data Insight",
|
|
6505
|
+
"metrics-grid": "Data Insight",
|
|
6506
|
+
"bullet-points": "Detailed Findings",
|
|
6507
|
+
"two-column": "Two Column",
|
|
6508
|
+
"three-column": "Three Column",
|
|
6509
|
+
"comparison": "Comparison",
|
|
6510
|
+
"timeline": "Process Timeline",
|
|
6511
|
+
"process": "Process",
|
|
6512
|
+
"quote": "Quote",
|
|
6513
|
+
"single-statement": "Single Statement",
|
|
6514
|
+
"cta": "Call to Action",
|
|
6515
|
+
"thank-you": "Title Impact"
|
|
6516
|
+
};
|
|
6517
|
+
return templateMap[type] || "Detailed Findings";
|
|
6518
|
+
}
|
|
6519
|
+
// =========================================================================
|
|
6520
|
+
// All slide creation is now handled by SlideFactory
|
|
6521
|
+
// Old methods removed in v7.0.0 - KB-driven SlideFactory handles:
|
|
6522
|
+
// - createTitleSlide, createAgendaSlide, createSituationSlide, etc.
|
|
6523
|
+
// - Content pattern classification
|
|
6524
|
+
// - KB-based slide type selection
|
|
6525
|
+
// - Text truncation, cleaning, deduplication
|
|
6526
|
+
// =========================================================================
|
|
6527
|
+
};
|
|
6528
|
+
async function initSlideGenerator() {
|
|
6529
|
+
const generator = new SlideGenerator();
|
|
6530
|
+
await generator.initialize();
|
|
6531
|
+
return generator;
|
|
6532
|
+
}
|
|
6533
|
+
|
|
4356
6534
|
// src/media/ImageProvider.ts
|
|
4357
6535
|
var LocalImageProvider = class {
|
|
4358
6536
|
name = "local";
|
|
@@ -4541,7 +6719,7 @@ async function validate(presentation, options) {
|
|
|
4541
6719
|
score
|
|
4542
6720
|
};
|
|
4543
6721
|
}
|
|
4544
|
-
var VERSION = "
|
|
6722
|
+
var VERSION = "7.1.0";
|
|
4545
6723
|
var index_default = {
|
|
4546
6724
|
generate,
|
|
4547
6725
|
validate,
|
|
@@ -4550,10 +6728,13 @@ var index_default = {
|
|
|
4550
6728
|
VERSION
|
|
4551
6729
|
};
|
|
4552
6730
|
export {
|
|
6731
|
+
AutoFixEngine,
|
|
4553
6732
|
ChartJsProvider,
|
|
4554
6733
|
CompositeChartProvider,
|
|
4555
6734
|
CompositeImageProvider,
|
|
4556
6735
|
ContentAnalyzer,
|
|
6736
|
+
ContentPatternClassifier,
|
|
6737
|
+
IterativeQAEngine,
|
|
4557
6738
|
KnowledgeGateway,
|
|
4558
6739
|
LocalImageProvider,
|
|
4559
6740
|
MermaidProvider,
|
|
@@ -4565,17 +6746,23 @@ export {
|
|
|
4565
6746
|
QuickChartProvider,
|
|
4566
6747
|
RevealJsGenerator,
|
|
4567
6748
|
ScoreCalculator,
|
|
6749
|
+
SevenDimensionScorer,
|
|
4568
6750
|
SlideFactory,
|
|
6751
|
+
SlideGenerator,
|
|
4569
6752
|
TemplateEngine,
|
|
4570
6753
|
TemplateNotFoundError,
|
|
4571
6754
|
UnsplashImageProvider,
|
|
4572
6755
|
VERSION,
|
|
4573
6756
|
ValidationError,
|
|
6757
|
+
createAutoFixEngine,
|
|
4574
6758
|
createDefaultChartProvider,
|
|
4575
6759
|
createDefaultImageProvider,
|
|
6760
|
+
createIterativeQAEngine,
|
|
6761
|
+
createSlideFactory,
|
|
4576
6762
|
index_default as default,
|
|
4577
6763
|
generate,
|
|
4578
6764
|
getKnowledgeGateway,
|
|
6765
|
+
initSlideGenerator,
|
|
4579
6766
|
validate
|
|
4580
6767
|
};
|
|
4581
6768
|
/**
|