using System.Text.Json; using System.Text.RegularExpressions; namespace AxCopilot.Services.Agent; public enum DeckReviewSeverity { Info, Warning, Critical, } public sealed record DeckReviewIssue(string Message, DeckReviewSeverity Severity); public sealed record DeckQualityReport( int Score, IReadOnlyList Strengths, IReadOnlyList Issues, IReadOnlyList Storyline) { public string ToToolSummary() { var strengths = Strengths.Take(3).ToList(); var issues = Issues .OrderByDescending(issue => issue.Severity) .Take(3) .Select(issue => issue.Message) .ToList(); var parts = new List { $"PPT quality {Score}/100" }; if (strengths.Count > 0) parts.Add("Strengths: " + string.Join(", ", strengths)); if (issues.Count > 0) parts.Add("Needs work: " + string.Join(", ", issues)); else parts.Add("Needs work: none"); return string.Join(" | ", parts); } } public static class DeckQualityReviewService { public static DeckQualityReport ReviewDeck( string title, JsonElement slides, bool hasTemplate, int autoRepairCount, IEnumerable? storyline = null) { var strengths = new List(); var issues = new List(); var storylineSteps = (storyline ?? []).Where(step => !string.IsNullOrWhiteSpace(step)).ToList(); const string KoreanAppendix = "\uBD80\uB85D"; if (slides.ValueKind != JsonValueKind.Array) return new DeckQualityReport(40, strengths, [new("Slides must be an array.", DeckReviewSeverity.Critical)], storylineSteps); var slidesList = slides.EnumerateArray().ToList(); var slideCount = slidesList.Count; var titleSlides = 0; var executiveSlides = 0; var recommendationSlides = 0; var roadmapSlides = 0; var appendixSlides = 0; var chartSlides = 0; var tableSlides = 0; var comparisonSlides = 0; var textHeavySlides = 0; var duplicateHeadlines = 0; var placeholderCount = 0; var headlines = new HashSet(StringComparer.OrdinalIgnoreCase); var layoutCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var slide in slidesList) { var layout = ReadString(slide, "layout", "content"); var normalizedLayout = layout.ToLowerInvariant(); layoutCounts[normalizedLayout] = layoutCounts.GetValueOrDefault(normalizedLayout) + 1; switch (normalizedLayout) { case "title": titleSlides++; break; case "executive_summary": executiveSlides++; break; case "recommendation": recommendationSlides++; break; case "roadmap": roadmapSlides++; break; case "comparison": comparisonSlides++; break; case "chart": chartSlides++; break; case "table": tableSlides++; break; } var titleText = ReadString(slide, "title"); if (titleText.Contains("appendix", StringComparison.OrdinalIgnoreCase) || titleText.Contains(KoreanAppendix, StringComparison.OrdinalIgnoreCase) || titleText.Contains("evidence", StringComparison.OrdinalIgnoreCase)) { appendixSlides++; } var headline = ReadString(slide, "headline"); if (!string.IsNullOrWhiteSpace(headline) && !headlines.Add(headline.Trim())) duplicateHeadlines++; var bulletCount = ReadStringList(slide, "summary_points").Count + ReadStringList(slide, "next_steps").Count + ReadStringList(slide, "body").Count; var bodyText = ReadString(slide, "body"); var textLength = bodyText.Length + headline.Length + ReadString(slide, "recommendation").Length + ReadString(slide, "left").Length + ReadString(slide, "right").Length; if (bulletCount >= 7 || textLength >= 420) textHeavySlides++; placeholderCount += CountPlaceholders(slide.GetRawText()); } if (hasTemplate) strengths.Add("Uses template or branded theme"); if (titleSlides > 0) strengths.Add("Includes title slide"); if (executiveSlides > 0) strengths.Add("Includes Executive Summary"); if (recommendationSlides > 0) strengths.Add("Includes Recommendation"); if (roadmapSlides > 0) strengths.Add("Includes Roadmap"); if (chartSlides + tableSlides + comparisonSlides >= 2) strengths.Add("Contains evidence-oriented comparison, table, or chart slides"); if (layoutCounts.Count >= 4) strengths.Add($"Uses {layoutCounts.Count} distinct layouts"); if (autoRepairCount > 0) strengths.Add($"Auto-repair applied {autoRepairCount} time(s)"); if (slideCount < 4) issues.Add(new("Slide count may be too low for an executive deck.", DeckReviewSeverity.Warning)); if (titleSlides == 0) issues.Add(new("Title slide is missing.", DeckReviewSeverity.Warning)); if (executiveSlides == 0) issues.Add(new("Executive Summary slide is missing.", DeckReviewSeverity.Critical)); if (recommendationSlides == 0) issues.Add(new("Recommendation or decision request slide is missing.", DeckReviewSeverity.Critical)); if (roadmapSlides == 0 && slideCount >= 5) issues.Add(new("Execution roadmap slide is missing.", DeckReviewSeverity.Warning)); if (appendixSlides == 0 && slideCount >= 6) issues.Add(new("Appendix or evidence slide is limited.", DeckReviewSeverity.Info)); if (duplicateHeadlines > 0) issues.Add(new($"Found {duplicateHeadlines} duplicate headline(s).", DeckReviewSeverity.Warning)); if (textHeavySlides > Math.Max(1, slideCount / 2)) issues.Add(new("Too many slides are text-heavy.", DeckReviewSeverity.Warning)); if (chartSlides + tableSlides + comparisonSlides == 0 && slideCount >= 4) issues.Add(new("Evidence slides such as charts, tables, or comparisons are limited.", DeckReviewSeverity.Warning)); if (placeholderCount > 0) issues.Add(new($"Found {placeholderCount} placeholder or unfinished marker(s).", DeckReviewSeverity.Critical)); var score = 70; score += Math.Min(18, strengths.Count * 3); score -= issues.Sum(issue => issue.Severity switch { DeckReviewSeverity.Critical => 12, DeckReviewSeverity.Warning => 6, _ => 2, }); score = Math.Clamp(score, 30, 98); return new DeckQualityReport(score, strengths, issues, storylineSteps); } private static string ReadString(JsonElement element, string propertyName, string fallback = "") { if (!element.SafeTryGetProperty(propertyName, out var value)) return fallback; return value.ValueKind switch { JsonValueKind.String => value.SafeGetString() ?? fallback, JsonValueKind.Number => value.ToString(), _ => fallback, }; } private static List ReadStringList(JsonElement element, string propertyName) { if (!element.SafeTryGetProperty(propertyName, out var value)) return []; return value.ValueKind switch { JsonValueKind.Array => value.EnumerateArray() .Select(item => item.ValueKind switch { JsonValueKind.String => item.SafeGetString() ?? string.Empty, JsonValueKind.Number => item.ToString(), JsonValueKind.Object => item.ToString(), _ => string.Empty, }) .Where(item => !string.IsNullOrWhiteSpace(item)) .ToList(), JsonValueKind.String => (value.SafeGetString() ?? string.Empty) .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToList(), _ => [], }; } private static int CountPlaceholders(string text) { if (string.IsNullOrWhiteSpace(text)) return 0; var patterns = new[] { @"\bTBD\b", @"\bTODO\b", @"\[(placeholder|fill me|todo)\]", @"lorem ipsum", }; return patterns.Sum(pattern => Regex.Matches(text, pattern, RegexOptions.IgnoreCase).Count); } }