claude-presentation-master 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2489,69 +2489,112 @@ function createSlideGeneratorV2(type = "consulting_deck") {
2489
2489
 
2490
2490
  // src/output/RendererV2.ts
2491
2491
  import { writeFileSync } from "fs";
2492
- var THEMES = {
2493
- mckinsey: {
2494
- style: "mckinsey",
2495
- primaryColor: "#003366",
2496
- // McKinsey blue
2497
- accentColor: "#0066cc",
2498
- description: "White background, professional hierarchy - for consulting/analysis decks"
2492
+ var FALLBACK_PALETTES = {
2493
+ consulting_classic: {
2494
+ background: "#FAFAF9",
2495
+ primary: "#0F172A",
2496
+ secondary: "#475569",
2497
+ accent: "#0369A1",
2498
+ text: "#18181B",
2499
+ name: "Consulting Classic"
2499
2500
  },
2500
- dark: {
2501
- style: "dark",
2502
- primaryColor: "#1a1a2e",
2503
- accentColor: "#4a9eff",
2504
- description: "Dark dramatic theme - for keynotes and tech presentations"
2501
+ dark_executive: {
2502
+ background: "#18181B",
2503
+ primary: "#FAFAFA",
2504
+ secondary: "#A1A1AA",
2505
+ accent: "#F59E0B",
2506
+ text: "#F4F4F5",
2507
+ name: "Dark Executive"
2505
2508
  },
2506
- minimal: {
2507
- style: "minimal",
2508
- primaryColor: "#333333",
2509
- accentColor: "#0066cc",
2510
- description: "Clean minimal white - for training and documentation"
2509
+ modern_business: {
2510
+ background: "#F8FAFC",
2511
+ primary: "#1E293B",
2512
+ secondary: "#64748B",
2513
+ accent: "#0891B2",
2514
+ text: "#0F172A",
2515
+ name: "Modern Business"
2511
2516
  },
2512
- corporate: {
2513
- style: "mckinsey",
2514
- // Uses McKinsey CSS but with different branding potential
2515
- primaryColor: "#1a365d",
2516
- accentColor: "#3182ce",
2517
- description: "Professional corporate - for executive presentations"
2517
+ executive_professional: {
2518
+ background: "#F5F5F4",
2519
+ primary: "#1E3A5F",
2520
+ secondary: "#64748B",
2521
+ accent: "#D97706",
2522
+ text: "#1F2937",
2523
+ name: "Executive Professional"
2518
2524
  },
2519
- startup: {
2520
- style: "dark",
2521
- primaryColor: "#0d1117",
2522
- accentColor: "#58a6ff",
2523
- description: "Modern dark - for pitch decks and product demos"
2525
+ strategy_growth: {
2526
+ background: "#FAF9F7",
2527
+ primary: "#292524",
2528
+ secondary: "#78716C",
2529
+ accent: "#059669",
2530
+ text: "#1C1917",
2531
+ name: "Strategy Growth"
2524
2532
  }
2525
2533
  };
2526
- var PRESENTATION_TYPE_TO_THEME = {
2527
- // Keynotes Dark dramatic style (TED talks, big stage)
2528
- ted_keynote: "dark",
2529
- // Sales → Dark dramatic style (persuasion, impact)
2530
- sales_pitch: "dark",
2531
- // Consulting/Analysis decks → McKinsey white style (data-heavy, professional)
2532
- consulting_deck: "mckinsey",
2533
- // Investment Banking → McKinsey style (financial, dense data)
2534
- investment_banking: "mckinsey",
2535
- // Investor Pitch → Startup dark style (modern, VC audiences)
2536
- investor_pitch: "startup",
2537
- // Technical → Dark style (engineering audiences like dark mode)
2538
- technical_presentation: "dark",
2539
- // All Hands → Minimal clean style (readable, accessible)
2540
- all_hands: "minimal"
2541
- };
2534
+ function getPaletteStyle(paletteName) {
2535
+ return paletteName === "dark_executive" ? "dark" : "light";
2536
+ }
2542
2537
  var RendererV2 = class {
2543
2538
  theme;
2544
2539
  presentationType;
2540
+ kb;
2545
2541
  /**
2546
- * Create renderer with theme based on presentation type.
2547
- * @param presentationType - The type of deck being created (determines theme)
2548
- * @param themeOverride - Optional explicit theme override
2542
+ * Create renderer with theme loaded from Knowledge Base.
2543
+ * @param presentationType - The type of deck being created (determines palette)
2544
+ * @param kb - Knowledge Base gateway (optional, will use global if not provided)
2549
2545
  */
2550
- constructor(presentationType = "consulting_deck", themeOverride) {
2546
+ constructor(presentationType = "consulting_deck", kb) {
2551
2547
  this.presentationType = presentationType;
2552
- const themeStyle = themeOverride || PRESENTATION_TYPE_TO_THEME[presentationType] || "mckinsey";
2553
- this.theme = THEMES[themeStyle] || THEMES.mckinsey;
2554
- console.log(`[RendererV2] Using "${this.theme.style}" theme for "${presentationType}" deck`);
2548
+ this.kb = kb || getKB();
2549
+ this.theme = this.loadThemeFromKB(presentationType);
2550
+ console.log(`[RendererV2] Using "${this.theme.paletteName}" palette (${this.theme.style} style) for "${presentationType}" deck`);
2551
+ }
2552
+ /**
2553
+ * Load theme configuration from Knowledge Base.
2554
+ * Falls back to hardcoded values only if KB fails.
2555
+ */
2556
+ loadThemeFromKB(type) {
2557
+ try {
2558
+ const paletteName = this.kb.queryOptional(`presentation_types.${type}.color_palette`).value || this.getDefaultPaletteName(type);
2559
+ const kbPalette = this.kb.queryOptional(`color_palettes.${paletteName}`).value;
2560
+ if (kbPalette) {
2561
+ console.log(`[RendererV2] Loaded "${paletteName}" palette from KB: bg=${kbPalette.background}, accent=${kbPalette.accent}`);
2562
+ return {
2563
+ style: getPaletteStyle(paletteName),
2564
+ palette: kbPalette,
2565
+ paletteName
2566
+ };
2567
+ }
2568
+ const fallback = FALLBACK_PALETTES[paletteName] ?? FALLBACK_PALETTES.consulting_classic;
2569
+ console.log(`[RendererV2] Using fallback "${paletteName}" palette`);
2570
+ return {
2571
+ style: getPaletteStyle(paletteName),
2572
+ palette: fallback,
2573
+ paletteName
2574
+ };
2575
+ } catch {
2576
+ const defaultPalette = FALLBACK_PALETTES.consulting_classic;
2577
+ return {
2578
+ style: "light",
2579
+ palette: defaultPalette,
2580
+ paletteName: "consulting_classic"
2581
+ };
2582
+ }
2583
+ }
2584
+ /**
2585
+ * Default palette mapping per presentation type (used if KB doesn't specify).
2586
+ */
2587
+ getDefaultPaletteName(type) {
2588
+ const defaults = {
2589
+ ted_keynote: "dark_executive",
2590
+ sales_pitch: "modern_business",
2591
+ consulting_deck: "consulting_classic",
2592
+ investment_banking: "executive_professional",
2593
+ investor_pitch: "modern_business",
2594
+ technical_presentation: "dark_executive",
2595
+ all_hands: "strategy_growth"
2596
+ };
2597
+ return defaults[type] || "consulting_classic";
2555
2598
  }
2556
2599
  /**
2557
2600
  * Render slides to complete HTML document.
@@ -2641,15 +2684,15 @@ ${slidesHtml}
2641
2684
  const content = this.renderSlideContent(slide);
2642
2685
  return `<div class="slide slide-${slide.type}">${content}</div>`;
2643
2686
  }).join("\n");
2644
- if (this.theme.style === "mckinsey") {
2645
- return this.getMcKinseyPrintHTML(title, slidesHtml);
2687
+ if (this.theme.style === "light") {
2688
+ return this.getLightPrintHTML(title, slidesHtml);
2646
2689
  }
2647
2690
  return this.getDarkPrintHTML(title, slidesHtml);
2648
2691
  }
2649
2692
  /**
2650
- * McKinsey-style print HTML (white background, professional).
2693
+ * Light theme print HTML (professional, white background).
2651
2694
  */
2652
- getMcKinseyPrintHTML(title, slidesHtml) {
2695
+ getLightPrintHTML(title, slidesHtml) {
2653
2696
  return `<!DOCTYPE html>
2654
2697
  <html lang="en">
2655
2698
  <head>
@@ -3305,34 +3348,60 @@ ${content}
3305
3348
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3306
3349
  }
3307
3350
  /**
3308
- * Professional CSS - McKinsey/BCG consulting style.
3351
+ * Generate CSS with colors from Knowledge Base.
3309
3352
  */
3310
3353
  getCSS() {
3311
- if (this.theme.style === "mckinsey") {
3312
- return this.getMcKinseyCSS();
3354
+ if (this.theme.style === "light") {
3355
+ return this.getLightThemeCSS();
3313
3356
  }
3314
- return this.getDarkCSS();
3357
+ return this.getDarkThemeCSS();
3315
3358
  }
3316
3359
  /**
3317
- * McKinsey/BCG Consulting Style - WHITE background, professional hierarchy.
3360
+ * Generate CSS variables from KB palette.
3318
3361
  */
3319
- getMcKinseyCSS() {
3362
+ getCSSVariables() {
3363
+ const p = this.theme.palette;
3364
+ const isLight = this.theme.style === "light";
3365
+ const bgSecondary = isLight ? this.lightenColor(p.background, -5) : this.lightenColor(p.background, 10);
3366
+ const bgAccent = isLight ? this.lightenColor(p.background, -10) : this.lightenColor(p.background, 15);
3367
+ const textMuted = isLight ? this.lightenColor(p.text, 40) : this.lightenColor(p.text, -30);
3320
3368
  return `
3321
- /* McKinsey Consulting Theme - Clean White, Strong Hierarchy */
3369
+ /* ${this.theme.paletteName} Theme - Loaded from Knowledge Base */
3322
3370
  :root {
3323
- --bg-primary: #ffffff;
3324
- --bg-secondary: #f8f9fa;
3325
- --bg-accent: #e9ecef;
3326
- --text-primary: #1a1a1a;
3327
- --text-secondary: #333333;
3328
- --text-muted: #666666;
3329
- --mckinsey-blue: #003366;
3330
- --accent-blue: #0066cc;
3371
+ --kb-palette: "${this.theme.paletteName}";
3372
+ --bg-primary: ${p.background};
3373
+ --bg-secondary: ${bgSecondary};
3374
+ --bg-accent: ${bgAccent};
3375
+ --text-primary: ${p.text};
3376
+ --text-secondary: ${p.secondary};
3377
+ --text-muted: ${textMuted};
3378
+ --color-primary: ${p.primary};
3379
+ --color-accent: ${p.accent};
3380
+ --accent-blue: ${p.accent};
3331
3381
  --accent-green: #28a745;
3332
3382
  --accent-red: #dc3545;
3333
- --border-color: #dee2e6;
3334
- --header-bar: #003366;
3335
- }
3383
+ --border-color: ${isLight ? "#dee2e6" : "rgba(255,255,255,0.1)"};
3384
+ --header-bar: ${p.primary};
3385
+ --mckinsey-blue: ${p.accent};
3386
+ }`;
3387
+ }
3388
+ /**
3389
+ * Lighten or darken a hex color.
3390
+ */
3391
+ lightenColor(hex, percent) {
3392
+ const num = parseInt(hex.replace("#", ""), 16);
3393
+ const amt = Math.round(2.55 * percent);
3394
+ const R = Math.max(0, Math.min(255, (num >> 16) + amt));
3395
+ const G = Math.max(0, Math.min(255, (num >> 8 & 255) + amt));
3396
+ const B = Math.max(0, Math.min(255, (num & 255) + amt));
3397
+ return `#${(16777216 + R * 65536 + G * 256 + B).toString(16).slice(1)}`;
3398
+ }
3399
+ /**
3400
+ * Light theme CSS (consulting style) with KB palette colors.
3401
+ */
3402
+ getLightThemeCSS() {
3403
+ return `
3404
+ ${this.getCSSVariables()}
3336
3405
 
3337
3406
  .reveal {
3338
3407
  font-family: 'Georgia', 'Times New Roman', serif;
@@ -3603,23 +3672,11 @@ ${content}
3603
3672
  `;
3604
3673
  }
3605
3674
  /**
3606
- * Dark theme CSS (legacy).
3675
+ * Dark theme CSS with KB palette colors.
3607
3676
  */
3608
- getDarkCSS() {
3677
+ getDarkThemeCSS() {
3609
3678
  return `
3610
- /* Professional Dark Theme - No Random Images */
3611
- :root {
3612
- --bg-primary: #1a1a2e;
3613
- --bg-secondary: #16213e;
3614
- --bg-accent: #0f3460;
3615
- --text-primary: #ffffff;
3616
- --text-secondary: rgba(255, 255, 255, 0.85);
3617
- --text-muted: rgba(255, 255, 255, 0.6);
3618
- --accent-blue: #4a9eff;
3619
- --accent-green: #00d4aa;
3620
- --accent-orange: #ff9f43;
3621
- --border-color: rgba(255, 255, 255, 0.1);
3622
- }
3679
+ ${this.getCSSVariables()}
3623
3680
 
3624
3681
  .reveal {
3625
3682
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
@@ -3830,18 +3887,625 @@ ${content}
3830
3887
  `;
3831
3888
  }
3832
3889
  };
3833
- function createRendererV2(presentationType = "consulting_deck", themeOverride) {
3834
- return new RendererV2(presentationType, themeOverride);
3890
+ function createRendererV2(presentationType = "consulting_deck", kb) {
3891
+ return new RendererV2(presentationType, kb);
3892
+ }
3893
+
3894
+ // src/qa/VisualQAEngine.ts
3895
+ import { writeFileSync as writeFileSync2, unlinkSync, mkdtempSync } from "fs";
3896
+ import { join as join2 } from "path";
3897
+ import { tmpdir } from "os";
3898
+ var VisualQAEngine = class {
3899
+ kb;
3900
+ browser = null;
3901
+ initialized = false;
3902
+ // KB-loaded thresholds
3903
+ minWhitespace = 0.35;
3904
+ minContrast = 4.5;
3905
+ // WCAG AA
3906
+ maxWordsPerSlide = 50;
3907
+ targetScore = 95;
3908
+ constructor() {
3909
+ this.kb = getKB();
3910
+ }
3911
+ /**
3912
+ * Initialize Playwright browser.
3913
+ */
3914
+ async initialize() {
3915
+ if (this.initialized) return;
3916
+ const playwright = await import("playwright");
3917
+ this.browser = await playwright.chromium.launch({
3918
+ headless: true,
3919
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
3920
+ });
3921
+ await this.loadKBThresholds();
3922
+ this.initialized = true;
3923
+ console.log("[VisualQA] Initialized with Playwright");
3924
+ }
3925
+ /**
3926
+ * Load quality thresholds from Knowledge Base.
3927
+ */
3928
+ async loadKBThresholds() {
3929
+ try {
3930
+ const whitespaceResult = this.kb.queryOptional("design_system.whitespace_rules");
3931
+ if (whitespaceResult.value?.min) {
3932
+ this.minWhitespace = whitespaceResult.value.min;
3933
+ }
3934
+ } catch {
3935
+ }
3936
+ }
3937
+ /**
3938
+ * Capture screenshots of all slides in a presentation.
3939
+ */
3940
+ async captureSlides(html) {
3941
+ await this.initialize();
3942
+ if (!this.browser) throw new Error("Browser not initialized");
3943
+ const tempDir = mkdtempSync(join2(tmpdir(), "visual-qa-"));
3944
+ const tempFile = join2(tempDir, "presentation.html");
3945
+ const modifiedHtml = html.replace(
3946
+ /hash:\s*true/g,
3947
+ "hash: false"
3948
+ );
3949
+ writeFileSync2(tempFile, modifiedHtml);
3950
+ const page = await this.browser.newPage();
3951
+ await page.setViewportSize({ width: 1920, height: 1080 });
3952
+ await page.goto(`file://${tempFile}`, { waitUntil: "networkidle" });
3953
+ await page.waitForTimeout(1500);
3954
+ const slideCount = await page.evaluate(() => {
3955
+ const reveal = window.Reveal;
3956
+ if (reveal) {
3957
+ return reveal.getTotalSlides();
3958
+ }
3959
+ return document.querySelectorAll(".slides > section").length;
3960
+ });
3961
+ console.log(`[VisualQA] Capturing ${slideCount} slides...`);
3962
+ const screenshots = [];
3963
+ for (let i = 0; i < slideCount; i++) {
3964
+ await page.evaluate((index) => {
3965
+ const reveal = window.Reveal;
3966
+ if (reveal) {
3967
+ reveal.slide(index, 0, 0);
3968
+ }
3969
+ }, i);
3970
+ await page.waitForTimeout(400);
3971
+ const screenshot = await page.screenshot({
3972
+ type: "png",
3973
+ fullPage: false
3974
+ });
3975
+ screenshots.push(screenshot);
3976
+ console.log(`[VisualQA] Captured slide ${i + 1}/${slideCount}`);
3977
+ }
3978
+ await page.close();
3979
+ try {
3980
+ unlinkSync(tempFile);
3981
+ } catch {
3982
+ }
3983
+ return screenshots;
3984
+ }
3985
+ /**
3986
+ * Analyze a single slide screenshot.
3987
+ */
3988
+ async analyzeSlide(screenshot, slideIndex, slideTitle, presentationType) {
3989
+ const issues = [];
3990
+ let score = 100;
3991
+ const typeThresholds = this.getTypeThresholds(presentationType);
3992
+ const whitespacePercentage = await this.measureWhitespace(screenshot);
3993
+ if (whitespacePercentage < typeThresholds.minWhitespace) {
3994
+ const penalty = Math.round((typeThresholds.minWhitespace - whitespacePercentage) * 100);
3995
+ score -= penalty;
3996
+ issues.push({
3997
+ severity: penalty > 15 ? "critical" : "major",
3998
+ category: "whitespace",
3999
+ issue: `Whitespace is ${(whitespacePercentage * 100).toFixed(1)}% (need ${typeThresholds.minWhitespace * 100}%+)`,
4000
+ measurement: whitespacePercentage,
4001
+ threshold: typeThresholds.minWhitespace,
4002
+ fix: "Reduce content or increase margins"
4003
+ });
4004
+ }
4005
+ const colorContrast = await this.measureContrast(screenshot);
4006
+ if (colorContrast < this.minContrast) {
4007
+ score -= 20;
4008
+ issues.push({
4009
+ severity: "critical",
4010
+ category: "contrast",
4011
+ issue: `Contrast ratio is ${colorContrast.toFixed(1)}:1 (WCAG AA requires 4.5:1)`,
4012
+ measurement: colorContrast,
4013
+ threshold: this.minContrast,
4014
+ fix: "Increase contrast between text and background"
4015
+ });
4016
+ }
4017
+ const textDensity = await this.measureTextDensity(screenshot);
4018
+ const maxDensity = typeThresholds.maxWordsPerSlide / 100;
4019
+ if (textDensity > maxDensity) {
4020
+ score -= 15;
4021
+ issues.push({
4022
+ severity: "major",
4023
+ category: "density",
4024
+ issue: `Text is too dense for ${presentationType}`,
4025
+ measurement: textDensity,
4026
+ threshold: maxDensity,
4027
+ fix: "Split content across multiple slides"
4028
+ });
4029
+ }
4030
+ const visualBalance = await this.measureBalance(screenshot);
4031
+ const balanceThreshold = ["ted_keynote", "sales_pitch"].includes(presentationType) ? 20 : 60;
4032
+ if (visualBalance < balanceThreshold) {
4033
+ score -= 10;
4034
+ issues.push({
4035
+ severity: "minor",
4036
+ category: "balance",
4037
+ issue: `Visual balance score is ${visualBalance}/100`,
4038
+ measurement: visualBalance,
4039
+ threshold: balanceThreshold,
4040
+ fix: "Redistribute content for better visual balance"
4041
+ });
4042
+ }
4043
+ const hierarchyClear = await this.checkHierarchy(screenshot);
4044
+ if (!hierarchyClear) {
4045
+ score -= 10;
4046
+ issues.push({
4047
+ severity: "major",
4048
+ category: "hierarchy",
4049
+ issue: "No clear visual hierarchy detected",
4050
+ fix: "Add distinct title styling or increase heading size"
4051
+ });
4052
+ }
4053
+ const glanceTestPassed = textDensity < 0.3 && hierarchyClear;
4054
+ if (!glanceTestPassed && presentationType !== "investment_banking") {
4055
+ score -= 10;
4056
+ issues.push({
4057
+ severity: "major",
4058
+ category: "density",
4059
+ issue: "Fails 3-second glance test",
4060
+ fix: "Simplify to one clear message per slide"
4061
+ });
4062
+ }
4063
+ const minBalanceForPro = ["ted_keynote", "sales_pitch"].includes(presentationType) ? 20 : 50;
4064
+ const professionalLook = whitespacePercentage >= typeThresholds.minWhitespace * 0.9 && colorContrast >= this.minContrast * 0.9 && visualBalance >= minBalanceForPro;
4065
+ if (!professionalLook) {
4066
+ score -= 5;
4067
+ issues.push({
4068
+ severity: "minor",
4069
+ category: "professional",
4070
+ issue: "Slide lacks professional polish",
4071
+ fix: "Review spacing, colors, and alignment"
4072
+ });
4073
+ }
4074
+ return {
4075
+ slideIndex,
4076
+ title: slideTitle,
4077
+ screenshot,
4078
+ whitespacePercentage,
4079
+ textDensity,
4080
+ colorContrast,
4081
+ visualBalance,
4082
+ glanceTestPassed,
4083
+ hierarchyClear,
4084
+ professionalLook,
4085
+ score: Math.max(0, Math.min(100, score)),
4086
+ issues,
4087
+ passed: score >= this.targetScore
4088
+ };
4089
+ }
4090
+ /**
4091
+ * Analyze entire deck visually.
4092
+ */
4093
+ async analyzeDeck(html, slides, presentationType) {
4094
+ console.log(`[VisualQA] Starting visual analysis of ${slides.length} slides...`);
4095
+ const screenshots = await this.captureSlides(html);
4096
+ const slideAnalyses = [];
4097
+ for (let i = 0; i < screenshots.length; i++) {
4098
+ const slide = slides[i];
4099
+ const analysis = await this.analyzeSlide(
4100
+ screenshots[i],
4101
+ i,
4102
+ slide?.title || `Slide ${i + 1}`,
4103
+ presentationType
4104
+ );
4105
+ slideAnalyses.push(analysis);
4106
+ console.log(`[VisualQA] Slide ${i + 1}: ${analysis.score}/100 ${analysis.passed ? "\u2713" : "\u2717"}`);
4107
+ }
4108
+ const consistency = this.calculateConsistency(slideAnalyses);
4109
+ const deckIssues = [];
4110
+ const failingSlides = slideAnalyses.filter((s) => !s.passed);
4111
+ if (failingSlides.length > 0) {
4112
+ deckIssues.push(`${failingSlides.length} slides below quality threshold`);
4113
+ }
4114
+ if (consistency < 80) {
4115
+ deckIssues.push(`Visual inconsistency across slides (${consistency}/100)`);
4116
+ }
4117
+ const avgScore = slideAnalyses.reduce((sum, s) => sum + s.score, 0) / slideAnalyses.length;
4118
+ const consistencyBonus = consistency >= 90 ? 2 : consistency >= 80 ? 0 : -3;
4119
+ const overallScore = Math.round(Math.min(100, Math.max(0, avgScore + consistencyBonus)));
4120
+ const passed = overallScore >= this.targetScore && failingSlides.length === 0;
4121
+ const summary = passed ? `\u2705 Visual QA PASSED: ${overallScore}/100 with ${consistency}% consistency` : `\u274C Visual QA FAILED: ${overallScore}/100, ${failingSlides.length} slides need work`;
4122
+ console.log(`[VisualQA] ${summary}`);
4123
+ return {
4124
+ slides: slideAnalyses,
4125
+ overallScore,
4126
+ passed,
4127
+ consistency,
4128
+ deckIssues,
4129
+ summary
4130
+ };
4131
+ }
4132
+ /**
4133
+ * Generate remediation feedback for failing slides.
4134
+ */
4135
+ generateRemediationFeedback(analysis) {
4136
+ const feedback = [];
4137
+ for (const slide of analysis.slides) {
4138
+ if (!slide.passed) {
4139
+ const fixes = [];
4140
+ const problems = [];
4141
+ for (const issue of slide.issues) {
4142
+ problems.push(issue.issue);
4143
+ fixes.push(issue.fix);
4144
+ }
4145
+ feedback.push({
4146
+ slideIndex: slide.slideIndex,
4147
+ currentScore: slide.score,
4148
+ targetScore: this.targetScore,
4149
+ specificFixes: fixes,
4150
+ visualProblems: problems
4151
+ });
4152
+ }
4153
+ }
4154
+ return feedback;
4155
+ }
4156
+ /**
4157
+ * Run the full QA loop until passing or max iterations.
4158
+ */
4159
+ async runQALoop(generateFn, remediateFn, presentationType, maxIterations = 3) {
4160
+ console.log(`[VisualQA] Starting QA loop (max ${maxIterations} iterations)...`);
4161
+ let iteration = 0;
4162
+ let currentAnalysis = null;
4163
+ let initialScore = 0;
4164
+ const improvements = [];
4165
+ while (iteration < maxIterations) {
4166
+ iteration++;
4167
+ console.log(`
4168
+ [VisualQA] === Iteration ${iteration} ===`);
4169
+ const { html, slides } = await generateFn();
4170
+ currentAnalysis = await this.analyzeDeck(html, slides, presentationType);
4171
+ if (iteration === 1) {
4172
+ initialScore = currentAnalysis.overallScore;
4173
+ }
4174
+ if (currentAnalysis.passed) {
4175
+ console.log(`[VisualQA] \u2705 PASSED on iteration ${iteration}!`);
4176
+ break;
4177
+ }
4178
+ const feedback = this.generateRemediationFeedback(currentAnalysis);
4179
+ console.log(`[VisualQA] ${feedback.length} slides need remediation`);
4180
+ for (const fb of feedback) {
4181
+ console.log(` Slide ${fb.slideIndex + 1}: ${fb.currentScore}\u2192${fb.targetScore}`);
4182
+ for (const fix of fb.specificFixes.slice(0, 2)) {
4183
+ console.log(` - ${fix}`);
4184
+ }
4185
+ }
4186
+ improvements.push(`Iteration ${iteration}: ${currentAnalysis.overallScore}/100`);
4187
+ if (iteration < maxIterations) {
4188
+ await remediateFn(feedback);
4189
+ }
4190
+ }
4191
+ if (!currentAnalysis) {
4192
+ throw new Error("No analysis performed");
4193
+ }
4194
+ return {
4195
+ finalScore: currentAnalysis.overallScore,
4196
+ passed: currentAnalysis.passed,
4197
+ iterations: iteration,
4198
+ initialScore,
4199
+ improvements,
4200
+ finalAnalysis: currentAnalysis
4201
+ };
4202
+ }
4203
+ /**
4204
+ * Close browser and cleanup.
4205
+ */
4206
+ async cleanup() {
4207
+ if (this.browser) {
4208
+ await this.browser.close();
4209
+ this.browser = null;
4210
+ this.initialized = false;
4211
+ }
4212
+ }
4213
+ // =============================================================================
4214
+ // PIXEL ANALYSIS METHODS
4215
+ // =============================================================================
4216
+ /**
4217
+ * Measure whitespace percentage from screenshot.
4218
+ * Uses actual pixel analysis.
4219
+ */
4220
+ async measureWhitespace(screenshot) {
4221
+ try {
4222
+ const sharp = await import("sharp");
4223
+ const image = sharp.default(screenshot);
4224
+ const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
4225
+ const totalPixels = info.width * info.height;
4226
+ let backgroundPixels = 0;
4227
+ const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
4228
+ for (let i = 0; i < data.length; i += info.channels) {
4229
+ const r = data[i];
4230
+ const g = data[i + 1];
4231
+ const b = data[i + 2];
4232
+ if (this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 30)) {
4233
+ backgroundPixels++;
4234
+ }
4235
+ }
4236
+ return backgroundPixels / totalPixels;
4237
+ } catch {
4238
+ return 0.4;
4239
+ }
4240
+ }
4241
+ /**
4242
+ * Measure color contrast ratio.
4243
+ */
4244
+ async measureContrast(screenshot) {
4245
+ try {
4246
+ const sharp = await import("sharp");
4247
+ const image = sharp.default(screenshot);
4248
+ const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
4249
+ const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
4250
+ const textColor = this.detectTextColor(data, info.width, info.height, info.channels, bgColor);
4251
+ const bgLuminance = this.relativeLuminance(bgColor.r, bgColor.g, bgColor.b);
4252
+ const textLuminance = this.relativeLuminance(textColor.r, textColor.g, textColor.b);
4253
+ const lighter = Math.max(bgLuminance, textLuminance);
4254
+ const darker = Math.min(bgLuminance, textLuminance);
4255
+ return (lighter + 0.05) / (darker + 0.05);
4256
+ } catch {
4257
+ return 7;
4258
+ }
4259
+ }
4260
+ /**
4261
+ * Measure text density (approximation based on non-background pixels).
4262
+ */
4263
+ async measureTextDensity(screenshot) {
4264
+ try {
4265
+ const sharp = await import("sharp");
4266
+ const image = sharp.default(screenshot);
4267
+ const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
4268
+ const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
4269
+ let contentPixels = 0;
4270
+ const totalPixels = info.width * info.height;
4271
+ for (let i = 0; i < data.length; i += info.channels) {
4272
+ const r = data[i];
4273
+ const g = data[i + 1];
4274
+ const b = data[i + 2];
4275
+ if (!this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 30)) {
4276
+ contentPixels++;
4277
+ }
4278
+ }
4279
+ return contentPixels / totalPixels;
4280
+ } catch {
4281
+ return 0.2;
4282
+ }
4283
+ }
4284
+ /**
4285
+ * Measure visual balance (how evenly distributed is the content).
4286
+ * Uses a 3x3 grid to properly detect centered layouts (common in keynotes).
4287
+ */
4288
+ async measureBalance(screenshot) {
4289
+ try {
4290
+ const sharp = await import("sharp");
4291
+ const image = sharp.default(screenshot);
4292
+ const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
4293
+ const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
4294
+ const grid = [0, 0, 0, 0, 0, 0, 0, 0, 0];
4295
+ const thirdW = Math.floor(info.width / 3);
4296
+ const thirdH = Math.floor(info.height / 3);
4297
+ for (let y = 0; y < info.height; y++) {
4298
+ for (let x = 0; x < info.width; x++) {
4299
+ const i = (y * info.width + x) * info.channels;
4300
+ const r = data[i];
4301
+ const g = data[i + 1];
4302
+ const b = data[i + 2];
4303
+ if (!this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 30)) {
4304
+ const col = x < thirdW ? 0 : x < thirdW * 2 ? 1 : 2;
4305
+ const row = y < thirdH ? 0 : y < thirdH * 2 ? 1 : 2;
4306
+ const cellIndex = row * 3 + col;
4307
+ grid[cellIndex] = (grid[cellIndex] ?? 0) + 1;
4308
+ }
4309
+ }
4310
+ }
4311
+ const total = grid.reduce((a, b) => a + b, 0);
4312
+ if (total === 0) return 100;
4313
+ const centerColumn = (grid[1] ?? 0) + (grid[4] ?? 0) + (grid[7] ?? 0);
4314
+ const leftColumn = (grid[0] ?? 0) + (grid[3] ?? 0) + (grid[6] ?? 0);
4315
+ const rightColumn = (grid[2] ?? 0) + (grid[5] ?? 0) + (grid[8] ?? 0);
4316
+ const centerRatio = centerColumn / total;
4317
+ if (centerRatio > 0.6) {
4318
+ const sideBalance = leftColumn > 0 && rightColumn > 0 ? Math.min(leftColumn, rightColumn) / Math.max(leftColumn, rightColumn) : 1;
4319
+ return Math.round(70 + sideBalance * 30);
4320
+ }
4321
+ const leftTotal = leftColumn + (grid[1] ?? 0) / 2 + (grid[4] ?? 0) / 2 + (grid[7] ?? 0) / 2;
4322
+ const rightTotal = rightColumn + (grid[1] ?? 0) / 2 + (grid[4] ?? 0) / 2 + (grid[7] ?? 0) / 2;
4323
+ const topRow = (grid[0] ?? 0) + (grid[1] ?? 0) + (grid[2] ?? 0);
4324
+ const bottomRow = (grid[6] ?? 0) + (grid[7] ?? 0) + (grid[8] ?? 0);
4325
+ const hBalance = leftTotal > 0 && rightTotal > 0 ? Math.min(leftTotal, rightTotal) / Math.max(leftTotal, rightTotal) : 0.5;
4326
+ const vBalance = topRow > 0 && bottomRow > 0 ? Math.min(topRow, bottomRow) / Math.max(topRow, bottomRow) : 0.5;
4327
+ const score = (hBalance * 0.6 + vBalance * 0.4) * 100;
4328
+ return Math.max(0, Math.min(100, Math.round(score)));
4329
+ } catch {
4330
+ return 75;
4331
+ }
4332
+ }
4333
+ /**
4334
+ * Check if there's clear visual hierarchy.
4335
+ */
4336
+ async checkHierarchy(screenshot) {
4337
+ try {
4338
+ const sharp = await import("sharp");
4339
+ const image = sharp.default(screenshot);
4340
+ const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
4341
+ const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
4342
+ const topRegionHeight = Math.floor(info.height * 0.2);
4343
+ let topContentPixels = 0;
4344
+ let bottomContentPixels = 0;
4345
+ for (let y = 0; y < info.height; y++) {
4346
+ for (let x = 0; x < info.width; x++) {
4347
+ const i = (y * info.width + x) * info.channels;
4348
+ const r = data[i];
4349
+ const g = data[i + 1];
4350
+ const b = data[i + 2];
4351
+ if (!this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 30)) {
4352
+ if (y < topRegionHeight) {
4353
+ topContentPixels++;
4354
+ } else {
4355
+ bottomContentPixels++;
4356
+ }
4357
+ }
4358
+ }
4359
+ }
4360
+ const topDensity = topContentPixels / (info.width * topRegionHeight);
4361
+ const bottomDensity = bottomContentPixels / (info.width * (info.height - topRegionHeight));
4362
+ return topDensity > 0.01 && topDensity < 0.3;
4363
+ } catch {
4364
+ return true;
4365
+ }
4366
+ }
4367
+ // =============================================================================
4368
+ // HELPER METHODS
4369
+ // =============================================================================
4370
+ getTypeThresholds(type) {
4371
+ const thresholds = {
4372
+ ted_keynote: { minWhitespace: 0.6, maxWordsPerSlide: 15 },
4373
+ sales_pitch: { minWhitespace: 0.5, maxWordsPerSlide: 30 },
4374
+ investor_pitch: { minWhitespace: 0.5, maxWordsPerSlide: 35 },
4375
+ consulting_deck: { minWhitespace: 0.35, maxWordsPerSlide: 70 },
4376
+ investment_banking: { minWhitespace: 0.3, maxWordsPerSlide: 100 },
4377
+ technical_presentation: { minWhitespace: 0.45, maxWordsPerSlide: 45 },
4378
+ all_hands: { minWhitespace: 0.4, maxWordsPerSlide: 50 }
4379
+ };
4380
+ return thresholds[type] || { minWhitespace: 0.4, maxWordsPerSlide: 50 };
4381
+ }
4382
+ detectBackgroundColor(data, width, height, channels) {
4383
+ const colorBuckets = /* @__PURE__ */ new Map();
4384
+ for (let y = 0; y < height; y += 4) {
4385
+ for (let x = 0; x < width; x += 4) {
4386
+ const i = (y * width + x) * channels;
4387
+ const r = data[i];
4388
+ const g = data[i + 1];
4389
+ const b = data[i + 2];
4390
+ const key = `${Math.floor(r / 16)},${Math.floor(g / 16)},${Math.floor(b / 16)}`;
4391
+ const existing = colorBuckets.get(key);
4392
+ if (existing) {
4393
+ existing.count++;
4394
+ } else {
4395
+ colorBuckets.set(key, { r, g, b, count: 1 });
4396
+ }
4397
+ }
4398
+ }
4399
+ let maxCount = 0;
4400
+ let dominantColor = { r: 240, g: 240, b: 240 };
4401
+ for (const color of colorBuckets.values()) {
4402
+ if (color.count > maxCount) {
4403
+ maxCount = color.count;
4404
+ dominantColor = { r: color.r, g: color.g, b: color.b };
4405
+ }
4406
+ }
4407
+ return dominantColor;
4408
+ }
4409
+ detectTextColor(data, width, height, channels, bgColor) {
4410
+ const bgLuminance = this.relativeLuminance(bgColor.r, bgColor.g, bgColor.b);
4411
+ const isDarkBg = bgLuminance < 0.5;
4412
+ const luminanceBuckets = /* @__PURE__ */ new Map();
4413
+ const startY = Math.floor(height * 0.05);
4414
+ const endY = Math.floor(height * 0.95);
4415
+ const startX = Math.floor(width * 0.05);
4416
+ const endX = Math.floor(width * 0.95);
4417
+ for (let y = startY; y < endY; y += 2) {
4418
+ for (let x = startX; x < endX; x += 2) {
4419
+ const i = (y * width + x) * channels;
4420
+ const r = data[i];
4421
+ const g = data[i + 1];
4422
+ const b = data[i + 2];
4423
+ if (this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 25)) {
4424
+ continue;
4425
+ }
4426
+ const lum = this.relativeLuminance(r, g, b);
4427
+ const bucket = Math.floor(lum * 10);
4428
+ const existing = luminanceBuckets.get(bucket);
4429
+ if (existing) {
4430
+ const total = existing.count + 1;
4431
+ existing.r = Math.round((existing.r * existing.count + r) / total);
4432
+ existing.g = Math.round((existing.g * existing.count + g) / total);
4433
+ existing.b = Math.round((existing.b * existing.count + b) / total);
4434
+ existing.count = total;
4435
+ } else {
4436
+ luminanceBuckets.set(bucket, { r, g, b, count: 1 });
4437
+ }
4438
+ }
4439
+ }
4440
+ const totalPixels = Array.from(luminanceBuckets.values()).reduce((sum, b) => sum + b.count, 0);
4441
+ const minCount = Math.max(50, totalPixels * 5e-3);
4442
+ let textColor = isDarkBg ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 };
4443
+ if (isDarkBg) {
4444
+ for (let bucket = 10; bucket >= 0; bucket--) {
4445
+ const colors = luminanceBuckets.get(bucket);
4446
+ if (colors && colors.count >= minCount) {
4447
+ textColor = { r: colors.r, g: colors.g, b: colors.b };
4448
+ break;
4449
+ }
4450
+ }
4451
+ } else {
4452
+ for (let bucket = 0; bucket <= 10; bucket++) {
4453
+ const colors = luminanceBuckets.get(bucket);
4454
+ if (colors && colors.count >= minCount) {
4455
+ textColor = { r: colors.r, g: colors.g, b: colors.b };
4456
+ break;
4457
+ }
4458
+ }
4459
+ }
4460
+ return textColor;
4461
+ }
4462
+ isSimilarColor(r1, g1, b1, r2, g2, b2, threshold) {
4463
+ return Math.abs(r1 - r2) < threshold && Math.abs(g1 - g2) < threshold && Math.abs(b1 - b2) < threshold;
4464
+ }
4465
+ relativeLuminance(r, g, b) {
4466
+ const sRGB = [r, g, b].map((c) => {
4467
+ c = c / 255;
4468
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
4469
+ });
4470
+ return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
4471
+ }
4472
+ calculateConsistency(slides) {
4473
+ if (slides.length < 2) return 100;
4474
+ const whitespaces = slides.map((s) => s.whitespacePercentage);
4475
+ const densities = slides.map((s) => s.textDensity);
4476
+ const wsVariance = this.variance(whitespaces);
4477
+ const densityVariance = this.variance(densities);
4478
+ const maxVariance = 0.1;
4479
+ const wsScore = Math.max(0, 100 - wsVariance / maxVariance * 50);
4480
+ const densityScore = Math.max(0, 100 - densityVariance / maxVariance * 50);
4481
+ return Math.round((wsScore + densityScore) / 2);
4482
+ }
4483
+ variance(values) {
4484
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
4485
+ return values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
4486
+ }
4487
+ };
4488
+ var instance5 = null;
4489
+ function getVisualQAEngine() {
4490
+ if (!instance5) {
4491
+ instance5 = new VisualQAEngine();
4492
+ }
4493
+ return instance5;
4494
+ }
4495
+ async function initVisualQAEngine() {
4496
+ const engine = getVisualQAEngine();
4497
+ await engine.initialize();
4498
+ return engine;
3835
4499
  }
3836
4500
 
3837
4501
  // src/core/PresentationEngineV2.ts
3838
- import { writeFileSync as writeFileSync2 } from "fs";
4502
+ import { writeFileSync as writeFileSync3 } from "fs";
3839
4503
  var PALETTE_TO_THEME = {
3840
4504
  dark_executive: "dark",
3841
- modern_business: "startup",
3842
- consulting_classic: "mckinsey",
3843
- executive_professional: "mckinsey",
3844
- strategy_growth: "minimal"
4505
+ modern_business: "light",
4506
+ consulting_classic: "light",
4507
+ executive_professional: "light",
4508
+ strategy_growth: "light"
3845
4509
  };
3846
4510
  async function loadDesignSpecsFromKB(kb, type) {
3847
4511
  const typeConfig = kb.queryRequired(`presentation_types.${type}`);
@@ -3852,7 +4516,7 @@ async function loadDesignSpecsFromKB(kb, type) {
3852
4516
  const typography = typeConfig.value.typography;
3853
4517
  const primaryExperts = typeConfig.value.primary_experts;
3854
4518
  const colorPalette = typeConfig.value.color_palette || "consulting_classic";
3855
- const theme = PALETTE_TO_THEME[colorPalette] || "mckinsey";
4519
+ const theme = PALETTE_TO_THEME[colorPalette] || "light";
3856
4520
  const scoringWeights = typeConfig.value.scoring_weights;
3857
4521
  let structure = "General presentation structure";
3858
4522
  if (primaryExperts.some((e) => e.includes("Minto") || e.includes("McKinsey"))) {
@@ -3896,6 +4560,80 @@ function getQualityCriteria(specs, type) {
3896
4560
  requireDataSources: specs.sourcesRequired
3897
4561
  };
3898
4562
  }
4563
+ function convertV1SlideToV2(slide) {
4564
+ const data = slide.data;
4565
+ const typeMap = {
4566
+ title: "title",
4567
+ title_impact: "title",
4568
+ agenda: "bullets",
4569
+ section_divider: "section",
4570
+ thank_you: "thank_you",
4571
+ single_statement: "statement",
4572
+ big_idea: "statement",
4573
+ big_number: "metrics",
4574
+ quote: "statement",
4575
+ star_moment: "statement",
4576
+ call_to_action: "call_to_action",
4577
+ three_points: "bullets",
4578
+ bullet_points: "bullets",
4579
+ two_column: "bullets",
4580
+ comparison: "bullets",
4581
+ metrics_grid: "metrics",
4582
+ data_insight: "metrics",
4583
+ problem_statement: "statement",
4584
+ solution_overview: "bullets",
4585
+ recommendation: "bullets",
4586
+ next_steps: "bullets"
4587
+ };
4588
+ const v2Type = typeMap[slide.type] || "bullets";
4589
+ const content = {};
4590
+ if (data.bullets && data.bullets.length > 0) {
4591
+ content.bullets = data.bullets;
4592
+ }
4593
+ if (data.metrics && data.metrics.length > 0) {
4594
+ content.metrics = data.metrics.map((m) => {
4595
+ const metric = {
4596
+ value: String(m.value),
4597
+ label: m.label
4598
+ };
4599
+ if (m.trend) {
4600
+ metric.trend = m.trend;
4601
+ }
4602
+ return metric;
4603
+ });
4604
+ }
4605
+ if (v2Type === "statement") {
4606
+ const statement = data.keyMessage || data.quote || data.body || "";
4607
+ if (statement) {
4608
+ content.statement = statement;
4609
+ }
4610
+ if (data.attribution) {
4611
+ content.subtext = data.attribution;
4612
+ }
4613
+ }
4614
+ if (data.body && v2Type !== "statement") {
4615
+ content.body = data.body;
4616
+ }
4617
+ if (data.source) {
4618
+ content.source = data.source;
4619
+ }
4620
+ if (data.subtitle) {
4621
+ content.subtext = data.subtitle;
4622
+ }
4623
+ const v2Slide = {
4624
+ index: slide.index,
4625
+ type: v2Type,
4626
+ title: data.title || "",
4627
+ content
4628
+ };
4629
+ if (slide.notes) {
4630
+ v2Slide.notes = slide.notes;
4631
+ }
4632
+ return v2Slide;
4633
+ }
4634
+ function convertV1SlidesToV2(slides) {
4635
+ return slides.map(convertV1SlideToV2);
4636
+ }
3899
4637
  var PresentationEngineV2 = class {
3900
4638
  options;
3901
4639
  kb = null;
@@ -3906,7 +4644,10 @@ var PresentationEngineV2 = class {
3906
4644
  imageApiKey: options.imageApiKey || process.env.GEMINI_API_KEY || process.env.OPENAI_API_KEY || "",
3907
4645
  minScore: options.minScore ?? 95,
3908
4646
  verbose: options.verbose ?? false,
3909
- maxRemediationAttempts: options.maxRemediationAttempts ?? 3
4647
+ maxRemediationAttempts: options.maxRemediationAttempts ?? 3,
4648
+ runVisualQA: options.runVisualQA ?? false,
4649
+ useRichPipeline: options.useRichPipeline ?? true
4650
+ // Default to V1's rich pipeline
3910
4651
  };
3911
4652
  }
3912
4653
  log(message) {
@@ -3938,8 +4679,21 @@ var PresentationEngineV2 = class {
3938
4679
  this.log(` Words/slide: ${designSpecs.wordsPerSlide.min}-${designSpecs.wordsPerSlide.max}`);
3939
4680
  this.log(` Experts: ${designSpecs.experts.slice(0, 2).join(", ")}...`);
3940
4681
  this.log("Step 3: Generating slides...");
3941
- const generator = createSlideGeneratorV2(presentationType);
3942
- let slides = generator.generate(markdown, title);
4682
+ let slides;
4683
+ if (this.options.useRichPipeline) {
4684
+ this.log(" Using V1 rich pipeline...");
4685
+ const contentAnalyzer = await initContentAnalyzer();
4686
+ const slideGenerator = await initSlideGenerator();
4687
+ const analysis = await contentAnalyzer.analyze(markdown, "markdown");
4688
+ this.log(` Content analysis: ${analysis.sections.length} sections, ${analysis.dataPoints.length} data points`);
4689
+ const v1Slides = await slideGenerator.generate(analysis, presentationType);
4690
+ this.log(` V1 generated ${v1Slides.length} slides`);
4691
+ slides = convertV1SlidesToV2(v1Slides);
4692
+ } else {
4693
+ this.log(" Using V2 minimalist pipeline...");
4694
+ const generator = createSlideGeneratorV2(presentationType);
4695
+ slides = generator.generate(markdown, title);
4696
+ }
3943
4697
  this.log(` Generated ${slides.length} slides`);
3944
4698
  if (!this.options.imageApiKey) {
3945
4699
  const shouldUseImages = this.shouldRecommendImages(slides, presentationType);
@@ -3989,12 +4743,31 @@ Summary: ${review.summary}`
3989
4743
  );
3990
4744
  }
3991
4745
  this.log("Step 7: Rendering output...");
3992
- const renderer = createRendererV2(presentationType, this.options.themeOverride);
4746
+ const renderer = createRendererV2(presentationType, this.kb);
3993
4747
  const html = renderer.render(slides, title || "Presentation");
4748
+ let visualQA;
4749
+ if (this.options.runVisualQA) {
4750
+ this.log("Step 8: Running Visual QA analysis...");
4751
+ try {
4752
+ const visualQAEngine = await initVisualQAEngine();
4753
+ visualQA = await visualQAEngine.analyzeDeck(html, slides, presentationType);
4754
+ await visualQAEngine.cleanup();
4755
+ if (!visualQA.passed) {
4756
+ warnings.push(`Visual QA detected issues: ${visualQA.summary}`);
4757
+ this.log(`Visual QA: ${visualQA.overallScore}/100 - ${visualQA.deckIssues.length} issues found`);
4758
+ } else {
4759
+ this.log(`Visual QA PASSED: ${visualQA.overallScore}/100`);
4760
+ }
4761
+ } catch (error) {
4762
+ const errMsg = error instanceof Error ? error.message : "Unknown error";
4763
+ warnings.push(`Visual QA failed: ${errMsg}`);
4764
+ this.log(`Visual QA error: ${errMsg}`);
4765
+ }
4766
+ }
3994
4767
  this.log(`
3995
4768
  SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
3996
4769
  this.log(`Flow: ${review.flow}`);
3997
- return {
4770
+ const result = {
3998
4771
  slides,
3999
4772
  html,
4000
4773
  presentationType,
@@ -4002,6 +4775,10 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
4002
4775
  review,
4003
4776
  warnings
4004
4777
  };
4778
+ if (visualQA) {
4779
+ result.visualQA = visualQA;
4780
+ }
4781
+ return result;
4005
4782
  }
4006
4783
  /**
4007
4784
  * Detect presentation type from content analysis.
@@ -4096,7 +4873,7 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
4096
4873
  const issues = [];
4097
4874
  const suggestions = [];
4098
4875
  let score = 100;
4099
- if (slide.type === "title" || slide.type === "thank_you") {
4876
+ if (slide.type === "title" || slide.type === "thank_you" || slide.type === "section") {
4100
4877
  return {
4101
4878
  slideIndex: index,
4102
4879
  title: slide.title,
@@ -4154,7 +4931,7 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
4154
4931
  score -= 10;
4155
4932
  }
4156
4933
  }
4157
- const hasContent = slide.content.bullets && slide.content.bullets.length > 0 || slide.content.table || slide.content.statement || slide.content.body;
4934
+ const hasContent = slide.content.bullets && slide.content.bullets.length > 0 || slide.content.table || slide.content.metrics && slide.content.metrics.length > 0 || slide.content.statement || slide.content.body;
4158
4935
  if (!hasContent) {
4159
4936
  issues.push("Slide has no content");
4160
4937
  score -= 30;
@@ -4337,7 +5114,7 @@ async function generatePresentation(markdown, options) {
4337
5114
  const result = await engine.generate(markdown, options?.title);
4338
5115
  if (options?.outputPath) {
4339
5116
  const htmlPath = options.outputPath.replace(/\.[^.]+$/, "") + ".html";
4340
- writeFileSync2(htmlPath, result.html);
5117
+ writeFileSync3(htmlPath, result.html);
4341
5118
  console.log(`
4342
5119
  Output: ${htmlPath}`);
4343
5120
  }
@@ -5081,12 +5858,12 @@ var SlideQualityReviewer = class {
5081
5858
  };
5082
5859
  }
5083
5860
  };
5084
- var instance5 = null;
5861
+ var instance6 = null;
5085
5862
  function getSlideQualityReviewer() {
5086
- if (!instance5) {
5087
- instance5 = new SlideQualityReviewer();
5863
+ if (!instance6) {
5864
+ instance6 = new SlideQualityReviewer();
5088
5865
  }
5089
- return instance5;
5866
+ return instance6;
5090
5867
  }
5091
5868
  async function initSlideQualityReviewer() {
5092
5869
  const reviewer = getSlideQualityReviewer();
@@ -5616,12 +6393,12 @@ var DeckQualityReviewer = class {
5616
6393
  return Math.max(0, 100 - deduction);
5617
6394
  }
5618
6395
  };
5619
- var instance6 = null;
6396
+ var instance7 = null;
5620
6397
  function getDeckQualityReviewer() {
5621
- if (!instance6) {
5622
- instance6 = new DeckQualityReviewer();
6398
+ if (!instance7) {
6399
+ instance7 = new DeckQualityReviewer();
5623
6400
  }
5624
- return instance6;
6401
+ return instance7;
5625
6402
  }
5626
6403
  async function initDeckQualityReviewer() {
5627
6404
  const reviewer = getDeckQualityReviewer();
@@ -5926,12 +6703,12 @@ var Remediator = class {
5926
6703
  return text.split(/\s+/).filter((w) => w.length > 0).length;
5927
6704
  }
5928
6705
  };
5929
- var instance7 = null;
6706
+ var instance8 = null;
5930
6707
  function getRemediator() {
5931
- if (!instance7) {
5932
- instance7 = new Remediator();
6708
+ if (!instance8) {
6709
+ instance8 = new Remediator();
5933
6710
  }
5934
- return instance7;
6711
+ return instance8;
5935
6712
  }
5936
6713
  async function initRemediator() {
5937
6714
  const remediator = getRemediator();
@@ -6903,12 +7680,12 @@ ${content}
6903
7680
  return html;
6904
7681
  }
6905
7682
  };
6906
- var instance8 = null;
7683
+ var instance9 = null;
6907
7684
  function getRenderer() {
6908
- if (!instance8) {
6909
- instance8 = new Renderer();
7685
+ if (!instance9) {
7686
+ instance9 = new Renderer();
6910
7687
  }
6911
- return instance8;
7688
+ return instance9;
6912
7689
  }
6913
7690
  async function initRenderer() {
6914
7691
  const renderer = getRenderer();
@@ -7778,7 +8555,7 @@ if (typeof process !== "undefined" && process.argv[1]?.includes("CodeQualityVali
7778
8555
  }
7779
8556
 
7780
8557
  // src/index.ts
7781
- var VERSION = "4.1.0";
8558
+ var VERSION = "4.2.0";
7782
8559
  async function generate(options) {
7783
8560
  const {
7784
8561
  content,
@@ -7965,6 +8742,7 @@ export {
7965
8742
  SlideQualityReviewer,
7966
8743
  VERSION,
7967
8744
  VisualDesignSystem,
8745
+ VisualQAEngine,
7968
8746
  createNanoBananaProvider,
7969
8747
  createPresentationEngineV2,
7970
8748
  createRendererV2,
@@ -7980,6 +8758,7 @@ export {
7980
8758
  getSlideGenerator,
7981
8759
  getSlideQualityReviewer,
7982
8760
  getVisualDesignSystem,
8761
+ getVisualQAEngine,
7983
8762
  initContentAnalyzer,
7984
8763
  initDeckQualityReviewer,
7985
8764
  initKB,
@@ -7988,6 +8767,7 @@ export {
7988
8767
  initSlideGenerator,
7989
8768
  initSlideQualityReviewer,
7990
8769
  initVisualDesignSystem,
8770
+ initVisualQAEngine,
7991
8771
  runCodeQualityCheck,
7992
8772
  validateCodeQuality
7993
8773
  };