diff --git a/documentation/ModComms_Features.md b/documentation/ModComms_Features.md new file mode 100644 index 0000000..af91159 --- /dev/null +++ b/documentation/ModComms_Features.md @@ -0,0 +1,244 @@ +# Mod Comms — Features Overview + +## Product Overview + +**Mod Comms** is an AI-powered proof review tool built for Barclays by OLIVER Agency. It automates the review of marketing materials (proofs) for legal compliance, brand adherence, tone of voice, and channel suitability — replacing slow, inconsistent manual review processes with fast, structured AI analysis. + +### The Problem + +Marketing teams at Barclays produce hundreds of proofs across social, display, email, and print channels. Each proof must be checked against: + +- Legal and regulatory requirements (FCA, ASA/CAP) +- Barclays or Barclaycard brand guidelines +- Channel-specific best practices +- Platform technical specifications + +Manual review is time-consuming, error-prone, and creates bottlenecks that delay campaign launches. + +### The Solution + +Mod Comms deploys four specialist AI agents that analyse every proof in parallel, delivering structured feedback in seconds rather than days. A Lead Agent synthesises the results into an overall pass/fail status with clear, actionable recommendations. + +--- + +## Multi-Agent AI System + +Mod Comms uses a multi-agent architecture powered by Google Gemini 2.5 Flash. Four specialist agents run in parallel, each with deep domain knowledge loaded from a managed Knowledge Base. + +### Legal Agent + +- Detects financial promotions (interest rates, APR, credit products) +- Checks advertising standards compliance (ASA/CAP code) +- Verifies required disclaimers are present and legible +- Assesses FCA regulatory compliance +- Reviews terms and conditions placement +- Checks third-party content permissions and disclosures + +### Brand Agent + +- Validates logo usage, minimum size, clear space, and placement +- Checks colour palette against approved masterbrand colours +- Verifies typography (Barclays Effra / Arial fallback, correct weights) +- Assesses adherence to design principles and sacred assets +- Dynamically loads Barclays or Barclaycard brand specifications based on campaign settings + +### Channel Best Practices Agent + +- Evaluates content strategy and messaging clarity for the target platform +- Checks creative best practices (visual hierarchy, layout, engagement patterns) +- Assesses platform optimisation (algorithm, safe zones, text-to-image ratios) +- Reviews mobile-first design (legibility, touch targets, thumb-zone navigation) + +### Channel Tech Specs Agent + +- Verifies dimensions, resolution, and aspect ratios against platform requirements +- Checks file format, size limits, and compression +- Validates typography specifications (minimum font sizes, character counts) +- Confirms digital grid system compliance (12-column desktop, 6-column mobile) +- Checks WCAG accessibility requirements (colour contrast, legibility) + +### Lead Agent + +The Lead Agent synthesises all specialist reviews into a final overall status and a professional summary. It does not analyse the proof directly — it interprets and consolidates the specialist results. + +--- + +## RAG Status System + +Every agent returns a RAG (Red / Amber / Green) status: + +| Status | Meaning | +|--------|---------| +| **Green** | Fully compliant, no issues found | +| **Amber** | Minor issues that should be addressed | +| **Red** | Significant issues that must be resolved | +| **Error** | Agent could not analyse with confidence | + +### Overall Status Decision Logic + +The Lead Agent determines the overall proof status: + +1. **Requires Manual Legal Review** — Financial promotion detected +2. **Analysis Error** — Any agent returned an Error status +3. **Failed** — Any agent returned Red +4. **Passed** — All agents returned Green or Amber + +--- + +## Campaign Management + +### Campaigns + +- Campaign table with name, proof count, status, creator, owning agency, and last modified date +- Create campaigns with: name, brand guidelines (Barclays / Barclaycard), campaign ID, client lead +- Agency and agency lead are pre-filled from the user's profile +- Manual status toggle between "In Progress" and "Completed" +- Show/Hide Completed toggle and "My Campaigns Only" filter +- Sortable and filterable columns + +### Proofs + +- Upload proofs with: name, channel, sub-channel (dependent), proof type (dependent), file +- Supported channels: Social (Meta, YouTube — 12 formats), Display (Google, Barclays.co.uk — 10 formats), Copy (AD Copy) +- Version management with full version history +- Download, delete, and re-upload capabilities +- Real-time analysis progress with agent-by-agent status updates + +--- + +## Real-Time Analysis + +Mod Comms uses WebSocket connections for live analysis: + +1. User uploads a proof (image or multi-page PDF) +2. File is sent via WebSocket with campaign metadata +3. Backend rasterises PDFs to PNG images (up to 10 pages) +4. All four agents analyse in parallel via `asyncio.gather()` +5. Real-time callbacks show each agent starting and completing +6. Lead Agent synthesises results into overall status and summary +7. Complete result returned to the frontend + +### Revision-Aware Analysis + +When uploading a new version, the system provides previous review context to each agent. Agents can then report: + +- **Resolved Issues** — Previously flagged issues that have been fixed +- **Outstanding Issues** — Issues from the previous version that remain +- **New Issues** — Issues not present in the previous version + +--- + +## Feedback Reports & PDF Export + +### Asset Detail View + +- Two-column layout: proof preview (left) and agent feedback (right) +- Each agent's feedback includes RAG status badge, detailed text feedback, and actionable issues +- Issues can be marked as resolved with a resolution note (visual strikethrough) +- Issues can be flagged as incorrect feedback for audit tracking + +### PDF Export + +- **Single Proof Export**: Cover page with branding, proof details, preview, Lead Agent summary, and all agent feedback with RAG status +- **Campaign Export**: Consolidated report for all proofs in a campaign + +--- + +## Knowledge Base Management + +Admins can manage the reference documentation that powers each agent: + +1. **Upload Source Documents** — Upload PDF/markdown brand guidelines, legal specs, or channel documentation +2. **Document Parsing** — System converts uploaded documents to structured markdown +3. **Spec Generation** — AI processes parsed documents into a unified specification +4. **Version Control** — Each processing run creates a new spec version with diff comparison +5. **Activation** — Admins can activate any spec version, which agents then use for analysis + +Five knowledge bases correspond to the five agent contexts: +- Legal +- Brand (Barclays) +- Brand (Barclaycard) +- Channel Best Practices +- Channel Tech Specs + +--- + +## Analytics Dashboard + +Admin-only dashboard with key performance indicators: + +- **Proofs Uploaded** — Total count +- **Pass Rate** — Percentage of proofs that passed +- **Issues Found** — Total issues across all agents +- **Time Saved** — Estimated hours saved (based on versions created from AI feedback) +- **AI Performance Summary** — AI-generated weekly trends and insights +- **Agent Performance Table** — Per-agent pass rate and average issues per proof + +--- + +## Auditing Dashboard + +Admin-only audit trail with three tabs: + +- **Flags** — User-reported incorrect agent feedback (proof, agent, user comments, timestamp) +- **Resolutions** — User-resolved issues (proof, agent, original issue, resolution note, timestamp) +- **Errors** — Analysis errors (proof, error summary, timestamp) + +Each audit entry links back to the specific proof and version for investigation. + +--- + +## User Roles & Access Control + +| Role | Write Access | Analytics | Auditing | Knowledge Base | Settings | User Management | Agency Filter | +|------|-------------|-----------|----------|----------------|----------|-----------------|---------------| +| Super Admin | Yes | Yes | Yes | Yes | Full | Yes | Yes | +| Oversight Admin | No | Yes | Yes | No | Read-only | No | Yes | +| Agency Admin | Yes | Yes | No | No | Full | No | No | +| Basic User | Yes | No | No | No | No | No | No | + +- Azure AD / O365 SSO authentication (MSAL) +- Users without an assigned agency see a message to contact their administrator + +--- + +## Settings & Configuration + +Admin-accessible settings for managing dropdown options used across the application: + +- **Channels** — Add/remove marketing channels (Social, Display, Copy) +- **Sub-Channels** — Platform-specific sub-channels dependent on parent channel +- **Proof Types** — Format types dependent on sub-channel selection +- Changes propagate immediately to all user dropdowns + +--- + +## Technical Architecture + +### Frontend +- React + TypeScript + Vite +- Tailwind CSS with Barclays design system +- MSAL (Azure AD) authentication +- WebSocket client for real-time analysis +- REST API client for CRUD operations +- localStorage persistence with versioned keys + +### Backend +- FastAPI (Python) with async support +- Google Gemini 2.5 Flash API for AI analysis +- SQLAlchemy async ORM + asyncpg +- Alembic database migrations +- WebSocket endpoint for real-time analysis +- PDF rasterisation service + +### Database +- PostgreSQL +- Core tables: Agencies, Users, Campaigns, Proofs, ProofVersions +- Audit tables: FlaggedItems, ResolvedItems, ErrorItems +- Configuration: DropdownOptions +- Knowledge Base: KnowledgeBases, SourceDocuments, SpecVersions, ProcessingJobs + +### Infrastructure +- Azure AD for authentication +- File storage with unique keys +- CORS-configured for frontend-backend communication diff --git a/documentation/ModComms_Presentation.pdf b/documentation/ModComms_Presentation.pdf new file mode 100644 index 0000000..2c2d231 Binary files /dev/null and b/documentation/ModComms_Presentation.pdf differ diff --git a/documentation/ModComms_Presentation.pptx b/documentation/ModComms_Presentation.pptx new file mode 100644 index 0000000..b91f147 Binary files /dev/null and b/documentation/ModComms_Presentation.pptx differ diff --git a/documentation/generate_presentation.py b/documentation/generate_presentation.py new file mode 100644 index 0000000..03b4a6c --- /dev/null +++ b/documentation/generate_presentation.py @@ -0,0 +1,1470 @@ +""" +Mod Comms — PowerPoint Presentation Generator +Generates a professional 16:9 widescreen presentation showcasing all features. +""" + +import os +from pathlib import Path +from pptx import Presentation +from pptx.util import Inches, Pt, Emu +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN, MSO_ANCHOR +from pptx.enum.shapes import MSO_SHAPE + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- +SCRIPT_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = SCRIPT_DIR.parent +LOGO_PATH = PROJECT_ROOT / "UI_guidance" / "Barclays-Modcomms.png" +OUTPUT_PPTX = SCRIPT_DIR / "ModComms_Presentation.pptx" + +# --------------------------------------------------------------------------- +# Design tokens +# --------------------------------------------------------------------------- +DARK_NAVY = RGBColor(0x1A, 0x21, 0x42) +ACTIVE_BLUE = RGBColor(0x00, 0x6D, 0xE3) +ELECTRIC_VIOLET = RGBColor(0x7A, 0x0F, 0xF9) +LIME = RGBColor(0xC3, 0xFB, 0x5A) +TEAL = RGBColor(0x01, 0xA1, 0xA2) +CYAN_BRAND = RGBColor(0x00, 0xAE, 0xEF) + +WHITE = RGBColor(0xFF, 0xFF, 0xFF) +BLACK_TITLE = RGBColor(0x27, 0x27, 0x27) +GREY_700 = RGBColor(0x8E, 0x8E, 0x8E) +GREY_300 = RGBColor(0xE2, 0xE2, 0xE2) +GREY_100 = RGBColor(0xF6, 0xF6, 0xF6) + +RAG_GREEN = RGBColor(0x09, 0x82, 0x1F) +RAG_AMBER = RGBColor(0xFF, 0xBA, 0x00) +RAG_RED = RGBColor(0xE3, 0x00, 0x0F) + +FONT_NAME = "Arial" + +# Slide dimensions (16:9) +SLIDE_WIDTH = Inches(13.333) +SLIDE_HEIGHT = Inches(7.5) + +# Margins +MARGIN_LEFT = Inches(0.8) +MARGIN_RIGHT = Inches(0.8) +MARGIN_TOP = Inches(0.6) +CONTENT_WIDTH = SLIDE_WIDTH - MARGIN_LEFT - MARGIN_RIGHT + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def set_slide_bg(slide, color): + """Set a solid background colour for a slide.""" + bg = slide.background + fill = bg.fill + fill.solid() + fill.fore_color.rgb = color + + +def add_textbox(slide, left, top, width, height, text, font_size=18, + color=BLACK_TITLE, bold=False, alignment=PP_ALIGN.LEFT, + font_name=FONT_NAME, line_spacing=1.2): + """Add a simple single-run text box and return the shape.""" + txBox = slide.shapes.add_textbox(left, top, width, height) + tf = txBox.text_frame + tf.word_wrap = True + p = tf.paragraphs[0] + p.text = text + p.font.size = Pt(font_size) + p.font.color.rgb = color + p.font.bold = bold + p.font.name = font_name + p.alignment = alignment + p.space_after = Pt(0) + p.space_before = Pt(0) + if line_spacing != 1.0: + p.line_spacing = Pt(font_size * line_spacing) + return txBox + + +def add_bullet_list(slide, left, top, width, height, items, font_size=14, + color=BLACK_TITLE, bullet_color=ACTIVE_BLUE, line_spacing=1.5): + """Add a bulleted list text box. Each item is a separate paragraph.""" + txBox = slide.shapes.add_textbox(left, top, width, height) + tf = txBox.text_frame + tf.word_wrap = True + + for i, item in enumerate(items): + if i == 0: + p = tf.paragraphs[0] + else: + p = tf.add_paragraph() + p.text = item + p.font.size = Pt(font_size) + p.font.color.rgb = color + p.font.name = FONT_NAME + p.space_after = Pt(4) + p.space_before = Pt(2) + p.line_spacing = Pt(font_size * line_spacing) + # Bullet + pPr = p._pPr + if pPr is None: + from pptx.oxml.ns import qn + pPr = p._p.get_or_add_pPr() + from pptx.oxml.ns import qn + buNone = pPr.find(qn("a:buNone")) + if buNone is not None: + pPr.remove(buNone) + buChar = pPr.makeelement(qn("a:buChar"), {"char": "\u2022"}) + pPr.append(buChar) + buClr = pPr.makeelement(qn("a:buClr"), {}) + srgbClr = buClr.makeelement(qn("a:srgbClr"), {"val": f"{bullet_color}"}) + buClr.append(srgbClr) + pPr.append(buClr) + buSzPct = pPr.makeelement(qn("a:buSzPct"), {"val": "120000"}) + pPr.append(buSzPct) + # Indent + pPr.set("marL", str(Emu(Inches(0.3)))) + pPr.set("indent", str(Emu(Inches(-0.25)))) + + return txBox + + +def add_rounded_rect(slide, left, top, width, height, fill_color, text="", + font_size=12, font_color=WHITE, bold=False, line_color=None): + """Add a rounded rectangle shape with optional text.""" + shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height) + shape.fill.solid() + shape.fill.fore_color.rgb = fill_color + if line_color: + shape.line.color.rgb = line_color + shape.line.width = Pt(1.5) + else: + shape.line.fill.background() + # Smaller corner radius + shape.adjustments[0] = 0.1 + if text: + tf = shape.text_frame + tf.word_wrap = True + tf.paragraphs[0].alignment = PP_ALIGN.CENTER + p = tf.paragraphs[0] + p.text = text + p.font.size = Pt(font_size) + p.font.color.rgb = font_color + p.font.bold = bold + p.font.name = FONT_NAME + tf.margin_left = Pt(6) + tf.margin_right = Pt(6) + tf.margin_top = Pt(4) + tf.margin_bottom = Pt(4) + return shape + + +def add_circle(slide, left, top, diameter, fill_color, text="", font_size=11, + font_color=WHITE, bold=True): + """Add a circle shape with centered text.""" + shape = slide.shapes.add_shape(MSO_SHAPE.OVAL, left, top, diameter, diameter) + shape.fill.solid() + shape.fill.fore_color.rgb = fill_color + shape.line.fill.background() + if text: + tf = shape.text_frame + tf.word_wrap = True + tf.paragraphs[0].alignment = PP_ALIGN.CENTER + p = tf.paragraphs[0] + p.text = text + p.font.size = Pt(font_size) + p.font.color.rgb = font_color + p.font.bold = bold + p.font.name = FONT_NAME + tf.margin_left = Pt(2) + tf.margin_right = Pt(2) + tf.margin_top = Pt(2) + tf.margin_bottom = Pt(2) + return shape + + +def add_connector_line(slide, start_x, start_y, end_x, end_y, color=GREY_300, width=1.5): + """Add a straight connector line.""" + connector = slide.shapes.add_connector( + 1, # MSO_CONNECTOR.STRAIGHT + start_x, start_y, end_x, end_y + ) + connector.line.color.rgb = color + connector.line.width = Pt(width) + return connector + + +def add_rich_textbox(slide, left, top, width, height): + """Add a text box and return the text frame for manual paragraph building.""" + txBox = slide.shapes.add_textbox(left, top, width, height) + tf = txBox.text_frame + tf.word_wrap = True + return tf + + +# --------------------------------------------------------------------------- +# Slide builders +# --------------------------------------------------------------------------- + +def make_title_slide(prs): + """Slide 1: Title slide with logo.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank + set_slide_bg(slide, DARK_NAVY) + + # Logo + if LOGO_PATH.exists(): + slide.shapes.add_picture( + str(LOGO_PATH), + Inches(0.8), Inches(1.0), + height=Inches(1.8) + ) + + # Title + add_textbox(slide, Inches(0.8), Inches(3.2), Inches(10), Inches(1.0), + "AI-Powered Proof Review", font_size=40, color=WHITE, bold=True) + + # Subtitle + tf = add_rich_textbox(slide, Inches(0.8), Inches(4.2), Inches(10), Inches(0.8)) + p = tf.paragraphs[0] + run1 = p.add_run() + run1.text = "Automated compliance, brand, and channel analysis for " + run1.font.size = Pt(20) + run1.font.color.rgb = GREY_300 + run1.font.name = FONT_NAME + run2 = p.add_run() + run2.text = "Barclays" + run2.font.size = Pt(20) + run2.font.color.rgb = LIME + run2.font.bold = True + run2.font.name = FONT_NAME + run3 = p.add_run() + run3.text = " marketing materials" + run3.font.size = Pt(20) + run3.font.color.rgb = GREY_300 + run3.font.name = FONT_NAME + + # Built by line + add_textbox(slide, Inches(0.8), Inches(5.4), Inches(6), Inches(0.5), + "Built by OLIVER Agency", font_size=14, color=GREY_700) + + # Decorative accent bar + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.05), + Inches(2.0), Pt(4)) + bar.fill.solid() + bar.fill.fore_color.rgb = LIME + bar.line.fill.background() + + +def make_section_divider(prs, section_number, title, subtitle=""): + """Section divider slide — dark navy with large number.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, DARK_NAVY) + + # Large section number + add_textbox(slide, Inches(0.8), Inches(1.0), Inches(3), Inches(2.5), + f"{section_number:02d}", font_size=96, color=LIME, bold=True) + + # Accent bar + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.6), + Inches(2.0), Pt(4)) + bar.fill.solid() + bar.fill.fore_color.rgb = LIME + bar.line.fill.background() + + # Title + add_textbox(slide, Inches(0.8), Inches(3.9), Inches(10), Inches(1.2), + title, font_size=36, color=WHITE, bold=True) + + if subtitle: + add_textbox(slide, Inches(0.8), Inches(5.1), Inches(10), Inches(0.8), + subtitle, font_size=18, color=GREY_300) + + +def make_content_slide(prs, title, bullets, subtitle=""): + """Standard content slide with heading and bullet list.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + # Title + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + title, font_size=28, color=DARK_NAVY, bold=True) + + # Accent underline + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + y_start = Inches(1.3) + if subtitle: + add_textbox(slide, MARGIN_LEFT, y_start, CONTENT_WIDTH, Inches(0.5), + subtitle, font_size=15, color=GREY_700) + y_start = Inches(1.8) + + add_bullet_list(slide, MARGIN_LEFT, y_start, CONTENT_WIDTH, Inches(5.5), + bullets, font_size=15, color=BLACK_TITLE) + + return slide + + +def make_two_column_slide(prs, title, left_title, left_items, right_title, right_items): + """Two-column content slide.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + # Title + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + title, font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + col_width = Inches(5.5) + gap = Inches(0.8) + + # Left column + add_textbox(slide, MARGIN_LEFT, Inches(1.4), col_width, Inches(0.4), + left_title, font_size=18, color=TEAL, bold=True) + add_bullet_list(slide, MARGIN_LEFT, Inches(1.85), col_width, Inches(5.0), + left_items, font_size=14) + + # Right column + right_left = MARGIN_LEFT + col_width + gap + add_textbox(slide, right_left, Inches(1.4), col_width, Inches(0.4), + right_title, font_size=18, color=TEAL, bold=True) + add_bullet_list(slide, right_left, Inches(1.85), col_width, Inches(5.0), + right_items, font_size=14) + + return slide + + +# --------------------------------------------------------------------------- +# Specific slides +# --------------------------------------------------------------------------- + +def make_agenda_slide(prs): + """Slide 2: Agenda.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "Agenda", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + agenda_items = [ + ("01", "The Challenge", "Why manual proof review doesn't scale"), + ("02", "Introducing Mod Comms", "AI-powered proof review at a glance"), + ("03", "Multi-Agent AI System", "Four specialist agents + Lead Agent"), + ("04", "Campaign Management", "Organising and tracking marketing proofs"), + ("05", "Real-Time Analysis", "Live WebSocket-powered review"), + ("06", "Feedback & Reporting", "Structured results and PDF export"), + ("07", "Knowledge Base & Admin", "Managing guidelines, analytics, and access"), + ("08", "Technical Architecture", "How it all fits together"), + ] + + y = Inches(1.4) + for num, title, desc in agenda_items: + # Number + add_textbox(slide, MARGIN_LEFT, y, Inches(0.6), Inches(0.45), + num, font_size=20, color=ACTIVE_BLUE, bold=True) + # Title + add_textbox(slide, Inches(1.5), y, Inches(4), Inches(0.3), + title, font_size=16, color=DARK_NAVY, bold=True) + # Description + add_textbox(slide, Inches(5.6), y + Inches(0.02), Inches(6), Inches(0.3), + desc, font_size=14, color=GREY_700) + # Divider line + if num != "08": + line = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, + MARGIN_LEFT, y + Inches(0.48), + CONTENT_WIDTH, Pt(0.75)) + line.fill.solid() + line.fill.fore_color.rgb = GREY_300 + line.line.fill.background() + y += Inches(0.58) + + +def make_problem_slide(prs): + """Slide 4: Problem statement.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "The Manual Review Bottleneck", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + # Problem cards — 2x2 grid + cards = [ + ("Slow Turnaround", "Manual review of each proof takes hours.\nCampaign launches are delayed\nwaiting for feedback."), + ("Inconsistent Quality", "Different reviewers apply guidelines\ndifferently. Critical compliance\nissues are sometimes missed."), + ("Scaling Challenges", "Hundreds of proofs across Social,\nDisplay, Email, and Print channels\noverwhelm review teams."), + ("Knowledge Silos", "Brand guidelines, legal requirements,\nand channel specs live in separate\ndocuments — hard to cross-reference."), + ] + + card_w = Inches(5.5) + card_h = Inches(2.3) + x_positions = [MARGIN_LEFT, MARGIN_LEFT + card_w + Inches(0.6)] + y_positions = [Inches(1.5), Inches(4.1)] + + for idx, (card_title, card_desc) in enumerate(cards): + x = x_positions[idx % 2] + y = y_positions[idx // 2] + + # Card background + card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, card_w, card_h) + card.fill.solid() + card.fill.fore_color.rgb = GREY_100 + card.line.color.rgb = GREY_300 + card.line.width = Pt(1) + card.adjustments[0] = 0.05 + + # Red accent bar at top of card + accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x + Inches(0.2), + y + Inches(0.2), Inches(0.3), Pt(4)) + accent.fill.solid() + accent.fill.fore_color.rgb = RAG_RED + accent.line.fill.background() + + # Title + add_textbox(slide, x + Inches(0.7), y + Inches(0.1), card_w - Inches(1), Inches(0.4), + card_title, font_size=16, color=DARK_NAVY, bold=True) + + # Description + add_textbox(slide, x + Inches(0.3), y + Inches(0.6), card_w - Inches(0.6), Inches(1.5), + card_desc, font_size=13, color=GREY_700, line_spacing=1.4) + + +def make_solution_slide(prs): + """Slide 6: Solution overview with value proposition cards.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "How Mod Comms Solves This", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + cards = [ + (ACTIVE_BLUE, "Instant Analysis", + "Four AI agents review every\nproof in parallel — results in\nseconds, not days."), + (ELECTRIC_VIOLET, "Consistent Standards", + "Every proof is checked against\nthe same guidelines. No more\nhuman inconsistency."), + (TEAL, "Full Coverage", + "Legal, Brand, Channel Best\nPractices, and Tech Specs —\nall checked simultaneously."), + (RAG_GREEN, "Actionable Feedback", + "Clear RAG status with specific,\nconstructive recommendations\nfor every issue found."), + ] + + card_w = Inches(2.7) + card_h = Inches(4.0) + total_w = card_w * 4 + Inches(0.4) * 3 + start_x = (SLIDE_WIDTH - total_w) / 2 + + for idx, (color, title, desc) in enumerate(cards): + x = start_x + idx * (card_w + Inches(0.4)) + y = Inches(1.8) + + # Card + card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, card_w, card_h) + card.fill.solid() + card.fill.fore_color.rgb = DARK_NAVY + card.line.fill.background() + card.adjustments[0] = 0.06 + + # Top accent bar + accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, + x + Inches(0.3), y + Inches(0.3), + Inches(0.8), Pt(4)) + accent.fill.solid() + accent.fill.fore_color.rgb = color + accent.line.fill.background() + + # Title + add_textbox(slide, x + Inches(0.3), y + Inches(0.6), card_w - Inches(0.6), Inches(0.5), + title, font_size=16, color=LIME, bold=True) + + # Description + add_textbox(slide, x + Inches(0.3), y + Inches(1.2), card_w - Inches(0.6), Inches(2.5), + desc, font_size=13, color=GREY_300, line_spacing=1.5) + + +def make_agent_architecture_slide(prs): + """Slide 8: Agent architecture diagram.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.3), CONTENT_WIDTH, Inches(0.6), + "Multi-Agent Architecture", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(0.88), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + # --- Proof Upload box --- + upload_x = Inches(0.5) + upload_y = Inches(2.8) + upload_w = Inches(1.8) + upload_h = Inches(1.2) + add_rounded_rect(slide, upload_x, upload_y, upload_w, upload_h, + GREY_100, "Proof\nUpload", font_size=14, font_color=DARK_NAVY, + bold=True, line_color=GREY_300) + + # --- Arrow from upload to agents area --- + add_connector_line(slide, upload_x + upload_w, upload_y + upload_h / 2, + Inches(2.8), upload_y + upload_h / 2, ACTIVE_BLUE, 2) + + # --- Four specialist agents --- + agent_x = Inches(2.8) + agents = [ + (ACTIVE_BLUE, "Legal\nAgent"), + (ELECTRIC_VIOLET, "Brand\nAgent"), + (TEAL, "Channel Best\nPractices Agent"), + (CYAN_BRAND, "Channel Tech\nSpecs Agent"), + ] + agent_w = Inches(2.0) + agent_h = Inches(0.9) + agent_gap = Inches(0.2) + agents_total_h = len(agents) * agent_h + (len(agents) - 1) * agent_gap + agent_start_y = Inches(1.2) + + for idx, (color, name) in enumerate(agents): + y = agent_start_y + idx * (agent_h + agent_gap) + add_rounded_rect(slide, agent_x, y, agent_w, agent_h, + color, name, font_size=12, font_color=WHITE, bold=True) + # Arrow to lead agent + add_connector_line(slide, agent_x + agent_w, y + agent_h / 2, + Inches(5.6), y + agent_h / 2, GREY_300, 1.5) + + # --- Gemini API box (below agents) --- + gemini_x = Inches(2.8) + gemini_y = agent_start_y + agents_total_h + Inches(0.4) + add_rounded_rect(slide, gemini_x, gemini_y, agent_w, Inches(0.7), + DARK_NAVY, "Google Gemini 2.5 Flash", + font_size=11, font_color=LIME, bold=True) + + # Connector from Gemini to agents (vertical line on the left side) + gem_center_x = gemini_x + agent_w / 2 + add_connector_line(slide, gem_center_x, agent_start_y + agents_total_h, + gem_center_x, gemini_y, GREY_300, 1.5) + + # --- Lead Agent (central) --- + lead_x = Inches(5.6) + lead_y = Inches(2.4) + lead_w = Inches(2.2) + lead_h = Inches(1.8) + lead = add_rounded_rect(slide, lead_x, lead_y, lead_w, lead_h, + DARK_NAVY, "", line_color=LIME) + + # Lead agent text + add_textbox(slide, lead_x + Inches(0.15), lead_y + Inches(0.2), + lead_w - Inches(0.3), Inches(0.4), + "Lead Agent", font_size=16, color=LIME, bold=True, + alignment=PP_ALIGN.CENTER) + add_textbox(slide, lead_x + Inches(0.15), lead_y + Inches(0.65), + lead_w - Inches(0.3), Inches(0.9), + "Synthesises all\nreviews into final\nstatus & summary", + font_size=11, color=WHITE, alignment=PP_ALIGN.CENTER) + + # --- Arrow from lead to output --- + add_connector_line(slide, lead_x + lead_w, lead_y + lead_h / 2, + Inches(8.5), lead_y + lead_h / 2, ACTIVE_BLUE, 2) + + # --- Knowledge Base box (below lead) --- + kb_x = Inches(5.6) + kb_y = gemini_y + add_rounded_rect(slide, kb_x, kb_y, lead_w, Inches(0.7), + DARK_NAVY, "Knowledge Base", + font_size=11, font_color=LIME, bold=True) + add_connector_line(slide, kb_x + lead_w / 2, lead_y + lead_h, + kb_x + lead_w / 2, kb_y, GREY_300, 1.5) + + # --- Output: RAG Status --- + output_x = Inches(8.5) + output_y = Inches(1.5) + output_w = Inches(4.2) + output_h = Inches(4.8) + + # Output container + out_card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, + output_x, output_y, output_w, output_h) + out_card.fill.solid() + out_card.fill.fore_color.rgb = GREY_100 + out_card.line.color.rgb = GREY_300 + out_card.line.width = Pt(1) + out_card.adjustments[0] = 0.04 + + add_textbox(slide, output_x + Inches(0.2), output_y + Inches(0.15), + output_w - Inches(0.4), Inches(0.35), + "Analysis Output", font_size=14, color=DARK_NAVY, bold=True, + alignment=PP_ALIGN.CENTER) + + # RAG circles + rag_items = [ + (RAG_GREEN, "Green", "Passed — compliant"), + (RAG_AMBER, "Amber", "Minor issues to address"), + (RAG_RED, "Red", "Failed — must resolve"), + ] + circle_d = Inches(0.4) + rag_y = output_y + Inches(0.7) + for idx, (color, label, desc) in enumerate(rag_items): + cy = rag_y + idx * Inches(0.6) + add_circle(slide, output_x + Inches(0.3), cy, circle_d, color, + "", font_size=9, bold=True) + add_textbox(slide, output_x + Inches(0.85), cy + Inches(0.02), + Inches(1), Inches(0.35), + label, font_size=12, color=DARK_NAVY, bold=True) + add_textbox(slide, output_x + Inches(1.8), cy + Inches(0.02), + Inches(2.2), Inches(0.35), + desc, font_size=11, color=GREY_700) + + # Overall status labels + status_y = rag_y + Inches(2.0) + statuses = [ + (RAG_GREEN, "Passed"), + (RAG_AMBER, "Requires Manual Legal Review"), + (RAG_RED, "Failed"), + (GREY_700, "Analysis Error"), + ] + add_textbox(slide, output_x + Inches(0.2), status_y - Inches(0.3), + output_w - Inches(0.4), Inches(0.3), + "Overall Status:", font_size=12, color=DARK_NAVY, bold=True) + for idx, (color, label) in enumerate(statuses): + sy = status_y + idx * Inches(0.35) + dot = slide.shapes.add_shape(MSO_SHAPE.OVAL, + output_x + Inches(0.3), sy + Inches(0.05), + Inches(0.15), Inches(0.15)) + dot.fill.solid() + dot.fill.fore_color.rgb = color + dot.line.fill.background() + add_textbox(slide, output_x + Inches(0.6), sy, + Inches(3.2), Inches(0.3), + label, font_size=11, color=DARK_NAVY) + + +def make_legal_agent_slide(prs): + """Slide 9: Legal Agent deep dive.""" + make_content_slide(prs, "Legal Agent", + [ + "Detects financial promotions — interest rates, APR, credit products, savings rates", + "Checks advertising standards compliance against ASA/CAP code", + "Verifies required disclaimers are present, legible, and properly placed", + "Assesses FCA regulatory compliance for financial services marketing", + "Reviews terms and conditions — referenced where necessary, qualifying text clear", + "Checks third-party content — permissions, attributions, influencer disclosures", + "Financial promotion detected → overall status becomes 'Requires Manual Legal Review'", + "Uses British English and constructive language throughout all feedback", + ], + subtitle="Compliance specialist ensuring all marketing materials meet legal and regulatory requirements") + + +def make_brand_agent_slide(prs): + """Slide 10: Brand Agent deep dive.""" + make_two_column_slide(prs, + "Brand Agent", + "Barclays Brand Checks", + [ + "Logo usage — correct version, minimum size, clear space, placement", + "Colour palette — approved masterbrand colours, WCAG-compliant pairings", + "Typography — Barclays Effra (Arial fallback), correct weights and scale", + "Design principles — overall design reflects brand expression", + "Sacred assets — present and unaltered", + "Accessibility — legible font sizes, proper contrast ratios", + ], + "Barclaycard Specifics", + [ + "Card Portal — stroke weight, corner radius, border colour, rotation limits", + "Barclaycard-specific core principles and guidelines", + "Experiential and email-specific guidelines applied", + "Social media guidelines for Barclaycard-branded content", + "Brand selection is per-campaign — agents load the correct spec dynamically", + "15+ brand guideline documents in the Knowledge Base", + ]) + + +def make_channel_agents_slide(prs): + """Slide 11: Channel agents deep dive.""" + make_two_column_slide(prs, + "Channel Agents", + "Best Practices Agent", + [ + "Content strategy — messaging clarity, CTA effectiveness", + "Creative best practices — visual hierarchy, engagement patterns", + "Platform optimisation — algorithm tips, safe zones, text-to-image ratios", + "Engagement — hashtags, mentions, tone suitability", + "Mobile-first design — legibility, touch targets, thumb-zone navigation", + ], + "Tech Specs Agent", + [ + "Dimensions & resolution — platform-specific sizes, DPI/PPI, aspect ratios", + "File format — type, size limits, compression quality", + "Typography specs — minimum font sizes, character counts", + "Digital grid system — 12-col desktop, 6-col mobile, 8px baseline", + "WCAG accessibility — colour contrast, documented pairings", + "Platform-specific specs — safe zones, video formats, frame rates", + ]) + + +def make_rag_status_slide(prs): + """Slide 12: RAG Status & Decision Logic.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "RAG Status & Decision Logic", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + # Agent RAG section + add_textbox(slide, MARGIN_LEFT, Inches(1.3), Inches(5), Inches(0.4), + "Per-Agent RAG Status", font_size=18, color=TEAL, bold=True) + + rag_items = [ + (RAG_GREEN, "Green", "Fully compliant — no issues found"), + (RAG_AMBER, "Amber", "Minor issues that should be addressed"), + (RAG_RED, "Red", "Significant issues that must be resolved"), + (GREY_700, "Error", "Agent could not analyse with confidence"), + ] + + for idx, (color, label, desc) in enumerate(rag_items): + y = Inches(1.85) + idx * Inches(0.55) + add_circle(slide, MARGIN_LEFT + Inches(0.2), y, Inches(0.35), color) + add_textbox(slide, MARGIN_LEFT + Inches(0.75), y + Inches(0.02), + Inches(1), Inches(0.3), label, font_size=14, color=DARK_NAVY, bold=True) + add_textbox(slide, MARGIN_LEFT + Inches(1.8), y + Inches(0.02), + Inches(4), Inches(0.3), desc, font_size=13, color=GREY_700) + + # Decision logic section + add_textbox(slide, Inches(7.0), Inches(1.3), Inches(5.5), Inches(0.4), + "Lead Agent Decision Logic", font_size=18, color=TEAL, bold=True) + + # Decision flow as cards + decisions = [ + ("1", RAG_AMBER, "Financial promotion detected?", + "→ Requires Manual Legal Review"), + ("2", RAG_RED, "Any agent returned Error?", + "→ Analysis Error"), + ("3", RAG_RED, "Any agent returned Red?", + "→ Failed"), + ("4", RAG_GREEN, "Otherwise", + "→ Passed"), + ] + + for idx, (num, color, question, result) in enumerate(decisions): + y = Inches(1.85) + idx * Inches(1.15) + x = Inches(7.0) + + # Decision card + card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, + x, y, Inches(5.5), Inches(0.95)) + card.fill.solid() + card.fill.fore_color.rgb = GREY_100 + card.line.color.rgb = GREY_300 + card.line.width = Pt(1) + card.adjustments[0] = 0.08 + + # Step number + add_circle(slide, x + Inches(0.15), y + Inches(0.2), Inches(0.45), + color, num, font_size=14, font_color=WHITE) + + add_textbox(slide, x + Inches(0.75), y + Inches(0.1), + Inches(4.5), Inches(0.35), + question, font_size=13, color=DARK_NAVY, bold=True) + add_textbox(slide, x + Inches(0.75), y + Inches(0.5), + Inches(4.5), Inches(0.35), + result, font_size=13, color=ACTIVE_BLUE, bold=True) + + +def make_campaign_lifecycle_slide(prs): + """Slide 14: Campaign & proof lifecycle.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "Campaign & Proof Lifecycle", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + # Flow steps + steps = [ + ("Create\nCampaign", "Set brand, agency,\nclient lead"), + ("Upload\nProof", "Name, channel,\nsub-channel, type"), + ("AI\nAnalysis", "4 agents analyse\nin parallel"), + ("Review\nFeedback", "RAG status per\nagent + summary"), + ("Resolve or\nRevise", "Mark issues resolved\nor upload new version"), + ("Export\nReport", "PDF with full\nfeedback details"), + ] + + step_w = Inches(1.7) + step_h = Inches(1.3) + desc_h = Inches(0.9) + gap = Inches(0.25) + total_w = len(steps) * step_w + (len(steps) - 1) * gap + start_x = (SLIDE_WIDTH - total_w) / 2 + step_y = Inches(1.8) + + colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL, ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL] + + for idx, (title, desc) in enumerate(steps): + x = start_x + idx * (step_w + gap) + + # Step box + add_rounded_rect(slide, x, step_y, step_w, step_h, + colors[idx], title, font_size=13, font_color=WHITE, bold=True) + + # Description below + add_textbox(slide, x, step_y + step_h + Inches(0.15), step_w, desc_h, + desc, font_size=11, color=GREY_700, alignment=PP_ALIGN.CENTER, + line_spacing=1.4) + + # Arrow + if idx < len(steps) - 1: + arrow_x = x + step_w + arrow_y = step_y + step_h / 2 + add_connector_line(slide, arrow_x, arrow_y, + arrow_x + gap, arrow_y, GREY_300, 2) + + # Bottom section: Campaign table details + add_textbox(slide, MARGIN_LEFT, Inches(4.4), CONTENT_WIDTH, Inches(0.4), + "Campaign Table Features", font_size=16, color=TEAL, bold=True) + + add_bullet_list(slide, MARGIN_LEFT, Inches(4.9), Inches(5.5), Inches(2.5), + [ + "Sortable, filterable columns — name, status, proof count, agency", + "\"My Campaigns Only\" toggle for personal workspace", + "Show/Hide Completed toggle", + "Quick-create modal with brand guideline selection", + ], font_size=13) + + add_textbox(slide, Inches(7.0), Inches(4.4), Inches(5.5), Inches(0.4), + "Proof Management", font_size=16, color=TEAL, bold=True) + + add_bullet_list(slide, Inches(7.0), Inches(4.9), Inches(5.5), Inches(2.5), + [ + "Dependent dropdowns: Channel → Sub-Channel → Proof Type", + "Version history with download and comparison", + "Duplicate file detection via MD5 hash", + "Supported: Social, Display, Copy channels (22+ formats)", + ], font_size=13) + + +def make_websocket_slide(prs): + """Slide 16: WebSocket live analysis.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "Real-Time WebSocket Analysis", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + # Message flow diagram + messages = [ + ("Client", ACTIVE_BLUE, "analyze", "File (base64) + metadata sent via WebSocket"), + ("Server", TEAL, "agent_started", "\"Legal Agent is analysing...\""), + ("Server", TEAL, "agent_completed", "Legal Agent review returned (RAG + feedback)"), + ("Server", TEAL, "agent_started", "\"Brand Agent is analysing...\""), + ("Server", TEAL, "agent_completed", "Brand Agent review returned"), + ("Server", ELECTRIC_VIOLET, "summary", "Lead Agent summary generated"), + ("Server", RAG_GREEN, "complete", "Full AgentReview + proof ID + version ID"), + ] + + y = Inches(1.5) + for sender, color, msg_type, desc in messages: + # Direction indicator + if sender == "Client": + arrow_text = "→" + label_color = ACTIVE_BLUE + else: + arrow_text = "←" + label_color = TEAL + + # Message type badge + add_rounded_rect(slide, MARGIN_LEFT, y, Inches(0.8), Inches(0.4), + GREY_100, sender, font_size=10, font_color=GREY_700, + line_color=GREY_300) + + add_textbox(slide, Inches(1.75), y + Inches(0.03), Inches(0.3), Inches(0.35), + arrow_text, font_size=16, color=label_color, bold=True) + + add_rounded_rect(slide, Inches(2.2), y, Inches(2.0), Inches(0.4), + color, msg_type, font_size=11, font_color=WHITE, bold=True) + + add_textbox(slide, Inches(4.4), y + Inches(0.05), Inches(5), Inches(0.35), + desc, font_size=12, color=GREY_700) + + y += Inches(0.55) + + # Right side: Key features + add_textbox(slide, Inches(9.8), Inches(1.3), Inches(3), Inches(0.4), + "Key Capabilities", font_size=16, color=TEAL, bold=True) + + add_bullet_list(slide, Inches(9.8), Inches(1.8), Inches(3.2), Inches(4.5), + [ + "Parallel agent execution via asyncio.gather()", + "Real-time progress — agents report as they finish", + "PDF rasterisation (up to 10 pages)", + "Revision-aware analysis with previous review context", + "Authenticated via MSAL bearer token", + "Automatic proof persistence to database", + ], font_size=12) + + +def make_feedback_reports_slide(prs): + """Slide 18: Feedback reports & PDF export.""" + make_two_column_slide(prs, + "Feedback Reports & PDF Export", + "Asset Detail View", + [ + "Two-column layout: proof preview (left) + agent feedback (right)", + "RAG status badge per agent with colour-coded indicators", + "Detailed text feedback with constructive recommendations", + "Actionable issues listed with 'Mark as Resolved' capability", + "Resolution notes recorded for audit trail", + "Flag incorrect feedback — sent to Auditing dashboard", + "Version history with one-click navigation between versions", + ], + "PDF Export", + [ + "Single Proof Report — detailed feedback for one proof", + "Campaign Report — consolidated report for all proofs", + "Cover page with Barclays branding, campaign name, date", + "Proof preview with metadata (name, version, channel)", + "Lead Agent summary with overall status", + "Per-agent sections: RAG status, full feedback, issues list", + "Professional formatting ready for stakeholder review", + ]) + + +def make_knowledge_base_slide(prs): + """Slide 20: Knowledge Base management.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "Knowledge Base Management", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + # Pipeline steps + pipe_steps = [ + ("Upload\nDocuments", "PDF or Markdown\nbrand/legal/channel\nguidelines"), + ("Parse &\nConvert", "AI converts uploads\nto structured\nmarkdown"), + ("Generate\nSpec", "AI synthesises all\ndocuments into\nunified specification"), + ("Version\nControl", "New spec version\ncreated with diff\ncomparison"), + ("Activate", "Admin selects\nwhich version\nagents use"), + ] + + step_w = Inches(2.0) + step_h = Inches(1.2) + gap = Inches(0.35) + total_w = len(pipe_steps) * step_w + (len(pipe_steps) - 1) * gap + start_x = (SLIDE_WIDTH - total_w) / 2 + step_y = Inches(1.5) + + pipe_colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL, ACTIVE_BLUE, RAG_GREEN] + + for idx, (title, desc) in enumerate(pipe_steps): + x = start_x + idx * (step_w + gap) + add_rounded_rect(slide, x, step_y, step_w, step_h, + pipe_colors[idx], title, font_size=13, font_color=WHITE, bold=True) + add_textbox(slide, x, step_y + step_h + Inches(0.1), step_w, Inches(0.9), + desc, font_size=11, color=GREY_700, alignment=PP_ALIGN.CENTER, + line_spacing=1.4) + if idx < len(pipe_steps) - 1: + ax = x + step_w + ay = step_y + step_h / 2 + add_connector_line(slide, ax, ay, ax + gap, ay, GREY_300, 2) + + # Knowledge bases list + add_textbox(slide, MARGIN_LEFT, Inches(4.3), CONTENT_WIDTH, Inches(0.4), + "Five Knowledge Bases", font_size=16, color=TEAL, bold=True) + + kbs = [ + ("Legal", "FCA regulations, ASA/CAP code, financial promotion rules"), + ("Brand — Barclays", "15+ guideline documents covering logo, colour, typography, design"), + ("Brand — Barclaycard", "Core principles, digital guidelines, email and social specs"), + ("Channel Best Practices", "LinkedIn, Reddit, platform optimisation, creative inspiration"), + ("Channel Tech Specs", "Social templates, dimension specs, platform-specific requirements"), + ] + + kb_w = Inches(2.2) + kb_h = Inches(1.8) + kb_gap = Inches(0.25) + total_kb_w = len(kbs) * kb_w + (len(kbs) - 1) * kb_gap + kb_start_x = (SLIDE_WIDTH - total_kb_w) / 2 + kb_y = Inches(4.8) + + kb_colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, ELECTRIC_VIOLET, TEAL, CYAN_BRAND] + + for idx, (name, desc) in enumerate(kbs): + x = kb_start_x + idx * (kb_w + kb_gap) + card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, + x, kb_y, kb_w, kb_h) + card.fill.solid() + card.fill.fore_color.rgb = DARK_NAVY + card.line.fill.background() + card.adjustments[0] = 0.06 + + # Accent + accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, + x + Inches(0.2), kb_y + Inches(0.15), + Inches(0.6), Pt(3)) + accent.fill.solid() + accent.fill.fore_color.rgb = kb_colors[idx] + accent.line.fill.background() + + add_textbox(slide, x + Inches(0.15), kb_y + Inches(0.35), + kb_w - Inches(0.3), Inches(0.35), + name, font_size=12, color=LIME, bold=True, + alignment=PP_ALIGN.CENTER) + add_textbox(slide, x + Inches(0.15), kb_y + Inches(0.75), + kb_w - Inches(0.3), Inches(0.9), + desc, font_size=10, color=GREY_300, + alignment=PP_ALIGN.CENTER, line_spacing=1.4) + + +def make_admin_three_col_slide(prs): + """Slide 21: Analytics, Auditing & Settings (3-column).""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "Analytics, Auditing & Settings", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + columns = [ + ("Analytics", ACTIVE_BLUE, [ + "Proofs Uploaded — total count", + "Pass Rate — % of proofs that passed", + "Issues Found — total across agents", + "Time Saved — hours estimated", + "AI Performance Summary — weekly trends", + "Agent Performance Table — per-agent stats", + ]), + ("Auditing", ELECTRIC_VIOLET, [ + "Flags Tab — user-reported incorrect feedback", + "Resolutions Tab — user-resolved issues", + "Errors Tab — analysis failures", + "Links to specific proof and version", + "Full audit trail with timestamps", + "Agency-filterable for oversight admins", + ]), + ("Settings", TEAL, [ + "Manage Channels — add/remove options", + "Sub-Channels — dependent on parent", + "Proof Types — dependent on sub-channel", + "Changes propagate immediately", + "Admin-only access for editing", + "Oversight admins get read-only view", + ]), + ] + + col_w = Inches(3.7) + col_gap = Inches(0.3) + total_col_w = len(columns) * col_w + (len(columns) - 1) * col_gap + start_x = (SLIDE_WIDTH - total_col_w) / 2 + + for idx, (title, color, items) in enumerate(columns): + x = start_x + idx * (col_w + col_gap) + y = Inches(1.4) + + # Column header card + add_rounded_rect(slide, x, y, col_w, Inches(0.5), + color, title, font_size=15, font_color=WHITE, bold=True) + + # Bullet list + add_bullet_list(slide, x + Inches(0.1), y + Inches(0.7), + col_w - Inches(0.2), Inches(5.0), + items, font_size=13, line_spacing=1.4) + + +def make_user_roles_slide(prs): + """Slide 22: User Roles & Access Control (table-style).""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7), + "User Roles & Access Control", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + # Table data + headers = ["Role", "Write", "Analytics", "Auditing", "Knowledge\nBase", "Settings", "User\nMgmt", "Agency\nFilter"] + rows_data = [ + ["Super Admin", "\u2713", "\u2713", "\u2713", "\u2713", "Full", "\u2713", "\u2713"], + ["Oversight Admin", "\u2717", "\u2713", "\u2713", "\u2717", "Read", "\u2717", "\u2713"], + ["Agency Admin", "\u2713", "\u2713", "\u2717", "\u2717", "Full", "\u2717", "\u2717"], + ["Basic User", "\u2713", "\u2717", "\u2717", "\u2717", "\u2717", "\u2717", "\u2717"], + ] + + # Dimensions + col_widths = [Inches(1.8), Inches(0.9), Inches(1.2), Inches(1.2), Inches(1.2), + Inches(1.0), Inches(1.0), Inches(1.1)] + row_h = Inches(0.6) + header_h = Inches(0.7) + table_start_x = (SLIDE_WIDTH - sum(w for w in col_widths)) / 2 + table_start_y = Inches(1.5) + + # Draw header row + x = table_start_x + for i, header in enumerate(headers): + cell = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, table_start_y, + col_widths[i], header_h) + cell.fill.solid() + cell.fill.fore_color.rgb = DARK_NAVY + cell.line.color.rgb = DARK_NAVY + cell.line.width = Pt(0.5) + + tf = cell.text_frame + tf.word_wrap = True + tf.paragraphs[0].alignment = PP_ALIGN.CENTER + p = tf.paragraphs[0] + p.text = header + p.font.size = Pt(11) + p.font.color.rgb = LIME + p.font.bold = True + p.font.name = FONT_NAME + tf.margin_top = Pt(6) + tf.margin_bottom = Pt(6) + x += col_widths[i] + + # Draw data rows + for row_idx, row in enumerate(rows_data): + x = table_start_x + y = table_start_y + header_h + row_idx * row_h + bg = WHITE if row_idx % 2 == 0 else GREY_100 + + for col_idx, val in enumerate(row): + cell = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, + col_widths[col_idx], row_h) + cell.fill.solid() + cell.fill.fore_color.rgb = bg + cell.line.color.rgb = GREY_300 + cell.line.width = Pt(0.5) + + tf = cell.text_frame + tf.word_wrap = True + p = tf.paragraphs[0] + p.text = val + p.font.size = Pt(12) + p.font.name = FONT_NAME + tf.margin_top = Pt(4) + tf.margin_bottom = Pt(4) + + if col_idx == 0: + p.font.color.rgb = DARK_NAVY + p.font.bold = True + p.alignment = PP_ALIGN.LEFT + tf.margin_left = Pt(8) + else: + p.alignment = PP_ALIGN.CENTER + if val == "\u2713": + p.font.color.rgb = RAG_GREEN + p.font.bold = True + elif val == "\u2717": + p.font.color.rgb = RAG_RED + elif val in ("Full", "Read"): + p.font.color.rgb = ACTIVE_BLUE + p.font.bold = True + else: + p.font.color.rgb = DARK_NAVY + + x += col_widths[col_idx] + + # Auth info below table + auth_y = table_start_y + header_h + len(rows_data) * row_h + Inches(0.6) + add_textbox(slide, MARGIN_LEFT, auth_y, CONTENT_WIDTH, Inches(0.4), + "Authentication & Security", font_size=16, color=TEAL, bold=True) + + add_bullet_list(slide, MARGIN_LEFT, auth_y + Inches(0.4), CONTENT_WIDTH, Inches(2.5), + [ + "Azure AD / O365 SSO integration via MSAL — shared with CopyGenAI application", + "Bearer token authentication on all API requests and WebSocket connections", + "Agency-scoped data access — basic users only see their own agency's campaigns", + "User Management screen (Super Admin only) — assign roles, agencies, view change history", + ], font_size=13) + + +def make_tech_architecture_slide(prs): + """Slide 23: Technical Architecture (layered diagram).""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, WHITE) + + add_textbox(slide, MARGIN_LEFT, Inches(0.3), CONTENT_WIDTH, Inches(0.6), + "Technical Architecture", font_size=28, color=DARK_NAVY, bold=True) + + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(0.85), + Inches(1.5), Pt(3)) + bar.fill.solid() + bar.fill.fore_color.rgb = ACTIVE_BLUE + bar.line.fill.background() + + # Layer definitions + layers = [ + ("Frontend", ACTIVE_BLUE, [ + ("React + TypeScript", Inches(2.5)), + ("Tailwind CSS", Inches(1.8)), + ("MSAL Auth", Inches(1.5)), + ("WebSocket Client", Inches(2.0)), + ("REST Client", Inches(1.6)), + ]), + ("Backend API", ELECTRIC_VIOLET, [ + ("FastAPI (Python)", Inches(2.5)), + ("WebSocket /ws/analyze", Inches(2.3)), + ("REST /api/*", Inches(1.8)), + ("Analysis Service", Inches(2.0)), + ]), + ("AI Layer", TEAL, [ + ("Legal Agent", Inches(1.8)), + ("Brand Agent", Inches(1.8)), + ("Best Practices Agent", Inches(2.3)), + ("Tech Specs Agent", Inches(2.0)), + ("Lead Agent", Inches(1.6)), + ]), + ("Data Layer", DARK_NAVY, [ + ("PostgreSQL", Inches(2.0)), + ("SQLAlchemy Async", Inches(2.2)), + ("Alembic Migrations", Inches(2.2)), + ("File Storage", Inches(1.8)), + ]), + ] + + layer_h = Inches(1.1) + layer_gap = Inches(0.25) + start_y = Inches(1.15) + label_w = Inches(1.8) + content_start_x = MARGIN_LEFT + label_w + Inches(0.2) + # Reserve 2.6" on right for external service boxes + ext_col_w = Inches(2.3) + ext_gap = Inches(0.3) + content_end_x = SLIDE_WIDTH - MARGIN_RIGHT - ext_col_w - ext_gap + + for layer_idx, (layer_name, color, components) in enumerate(layers): + y = start_y + layer_idx * (layer_h + layer_gap) + + # Layer label + add_rounded_rect(slide, MARGIN_LEFT, y, label_w, layer_h, + color, layer_name, font_size=13, font_color=WHITE, bold=True) + + # Components + comp_gap = Inches(0.15) + total_comp_w = sum(w for _, w in components) + (len(components) - 1) * comp_gap + available_w = content_end_x - content_start_x + scale = min(1.0, available_w / total_comp_w) + + cx = content_start_x + for comp_name, comp_w in components: + scaled_w = comp_w * scale + add_rounded_rect(slide, cx, y + Inches(0.15), scaled_w, layer_h - Inches(0.3), + GREY_100, comp_name, font_size=11, font_color=DARK_NAVY, + bold=False, line_color=color) + cx += scaled_w + comp_gap * scale + + # External services on the right (in reserved column) + ext_x = SLIDE_WIDTH - MARGIN_RIGHT - ext_col_w + # Position Azure AD aligned with Backend API row + ext_y_azure = start_y + 1 * (layer_h + layer_gap) + Inches(0.15) + add_rounded_rect(slide, ext_x, ext_y_azure, ext_col_w, layer_h - Inches(0.3), + DARK_NAVY, "Azure AD", font_size=12, font_color=LIME, bold=True) + + # Position Gemini aligned with AI Layer row + ext_y_gemini = start_y + 2 * (layer_h + layer_gap) + Inches(0.15) + add_rounded_rect(slide, ext_x, ext_y_gemini, ext_col_w, layer_h - Inches(0.3), + DARK_NAVY, "Google Gemini\n2.5 Flash", font_size=12, + font_color=LIME, bold=True) + + # Bottom note + note_y = start_y + len(layers) * (layer_h + layer_gap) + Inches(0.1) + add_textbox(slide, MARGIN_LEFT, note_y, CONTENT_WIDTH, Inches(0.8), + "All communication secured via MSAL bearer tokens. " + "Agents run in parallel via asyncio. " + "Database uses async SQLAlchemy + asyncpg for non-blocking I/O.", + font_size=12, color=GREY_700) + + +def make_closing_slide(prs): + """Slide 24: Closing slide with logo.""" + slide = prs.slides.add_slide(prs.slide_layouts[6]) + set_slide_bg(slide, DARK_NAVY) + + # Logo + if LOGO_PATH.exists(): + slide.shapes.add_picture( + str(LOGO_PATH), + Inches(0.8), Inches(1.2), + height=Inches(1.8) + ) + + # Accent bar + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.4), + Inches(2.0), Pt(4)) + bar.fill.solid() + bar.fill.fore_color.rgb = LIME + bar.line.fill.background() + + tf = add_rich_textbox(slide, Inches(0.8), Inches(3.7), Inches(10), Inches(1.0)) + p = tf.paragraphs[0] + run1 = p.add_run() + run1.text = "Intelligent Review. " + run1.font.size = Pt(32) + run1.font.color.rgb = ELECTRIC_VIOLET + run1.font.bold = True + run1.font.name = FONT_NAME + run2 = p.add_run() + run2.text = "Confident Delivery." + run2.font.size = Pt(32) + run2.font.color.rgb = WHITE + run2.font.bold = True + run2.font.name = FONT_NAME + + add_textbox(slide, Inches(0.8), Inches(4.8), Inches(8), Inches(0.5), + "AI-powered proof review for Barclays marketing materials", + font_size=16, color=GREY_300) + + add_textbox(slide, Inches(0.8), Inches(5.6), Inches(6), Inches(0.5), + "Built by OLIVER Agency", font_size=14, color=GREY_700) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + prs = Presentation() + prs.slide_width = SLIDE_WIDTH + prs.slide_height = SLIDE_HEIGHT + + # Slide 1: Title + make_title_slide(prs) + + # Slide 2: Agenda + make_agenda_slide(prs) + + # Slide 3: Section — The Challenge + make_section_divider(prs, 1, "The Challenge", + "Why manual proof review doesn't scale") + + # Slide 4: Problem statement + make_problem_slide(prs) + + # Slide 5: Section — Introducing Mod Comms + make_section_divider(prs, 2, "Introducing Mod Comms", + "AI-powered proof review at a glance") + + # Slide 6: Solution overview + make_solution_slide(prs) + + # Slide 7: Section — Multi-Agent AI System + make_section_divider(prs, 3, "Multi-Agent AI System", + "Four specialist agents working in parallel") + + # Slide 8: Agent architecture diagram + make_agent_architecture_slide(prs) + + # Slide 9: Legal Agent + make_legal_agent_slide(prs) + + # Slide 10: Brand Agent + make_brand_agent_slide(prs) + + # Slide 11: Channel Agents + make_channel_agents_slide(prs) + + # Slide 12: RAG Status + make_rag_status_slide(prs) + + # Slide 13: Section — Campaign Management + make_section_divider(prs, 4, "Campaign Management", + "Organising and tracking marketing proofs") + + # Slide 14: Campaign lifecycle + make_campaign_lifecycle_slide(prs) + + # Slide 15: Section — Real-Time Analysis + make_section_divider(prs, 5, "Real-Time Analysis", + "Live WebSocket-powered proof review") + + # Slide 16: WebSocket analysis + make_websocket_slide(prs) + + # Slide 17: Section — Feedback & Reporting + make_section_divider(prs, 6, "Feedback & Reporting", + "Structured results and PDF export") + + # Slide 18: Feedback reports + make_feedback_reports_slide(prs) + + # Slide 19: Section — Knowledge Base & Admin + make_section_divider(prs, 7, "Knowledge Base & Admin", + "Managing guidelines, analytics, and access control") + + # Slide 20: Knowledge Base + make_knowledge_base_slide(prs) + + # Slide 21: Analytics, Auditing, Settings + make_admin_three_col_slide(prs) + + # Slide 22: User Roles + make_user_roles_slide(prs) + + # Slide 23: Section — Technical Architecture + make_section_divider(prs, 8, "Technical Architecture", + "How it all fits together") + + # Slide 24: Tech Architecture + make_tech_architecture_slide(prs) + + # Slide 25: Closing + make_closing_slide(prs) + + # Save + prs.save(str(OUTPUT_PPTX)) + print(f"Presentation saved to: {OUTPUT_PPTX}") + print(f"Total slides: {len(prs.slides)}") + + +if __name__ == "__main__": + main()