diff --git "a/app.py" "b/app.py" deleted file mode 100644--- "a/app.py" +++ /dev/null @@ -1,3394 +0,0 @@ -# ───────────────────────────────────────────── -# RegMap — AI Regulation Compliance Platform -# Copyright (c) 2026 RegMap. All rights reserved. -# License: CC BY-NC 4.0 -# https://creativecommons.org/licenses/by-nc/4.0/ -# Commercial use prohibited without prior written consent. -# ───────────────────────────────────────────── - -import streamlit as st -import io -from datetime import datetime -from phase2_rkb import ( - QUALIFICATION_QUESTIONS, OBLIGATIONS, OVERLAP_ANALYSIS, GAP_ANALYSIS, - REGULATION_URLS, OTHER_REG_ONE_LINERS, DIFC_CONTROLLER_NOTE, DISCLAIMER, -) - -# ── EU AI Act full exemptions (single source of truth) ── -EU_FULL_EXEMPTIONS = [ - "The AI system is used exclusively for military, defence, or national security purposes", - "The AI system is used solely for scientific research and development and has not yet been placed on the market or put into service", - "The AI system is used for purely personal, non-professional purposes by a natural person", - "The AI system is operated by a third-country public authority under international law enforcement or judicial cooperation agreements", -] - -# ── Startup integrity check ── -# Validates RKB data consistency at app start. If issues found, logs warning. -def _validate_rkb(): - AI_REGS = [ - "EU AI Act (Regulation 2024/1689)", "EU AI Act — GPAI Framework (Chapter V)", - "Colorado AI Act (SB 24-205)", "Texas TRAIGA (HB 149)", - "Utah AI Policy Act (SB 149)", "California CCPA / ADMT Regulations", - "Illinois HB 3773 (AI in Employment)", "DIFC Regulation 10 (AI Processing)", - ] - GAP_REGS = [r for r in AI_REGS if "GPAI" not in r] - issues = [] - for r in AI_REGS: - if r not in OBLIGATIONS: issues.append(f"AI reg not in OBLIGATIONS: {r}") - if r not in QUALIFICATION_QUESTIONS: issues.append(f"AI reg not in QUAL_QUESTIONS: {r}") - if r not in REGULATION_URLS: issues.append(f"AI reg not in URLS: {r}") - for src in GAP_REGS: - if src not in GAP_ANALYSIS: issues.append(f"GAP missing source: {src}") - else: - for tgt in GAP_REGS: - if tgt != src and tgt not in GAP_ANALYSIS[src]: - issues.append(f"GAP missing: {src} → {tgt}") - all_known = set(OBLIGATIONS.keys()) | set(OTHER_REG_ONE_LINERS.keys()) - for ov in OVERLAP_ANALYSIS: - for r in ov["regulations"]: - if r not in all_known: issues.append(f"OVERLAP refs unknown: {r}") - return issues - -_rkb_issues = _validate_rkb() -if _rkb_issues: - import logging - logging.warning(f"RegMap RKB integrity: {len(_rkb_issues)} issues found") - for i in _rkb_issues: - logging.warning(f" RKB: {i}") - -# ───────────────────────────────────────────── -# REGMAP — AI System ID Card -# ───────────────────────────────────────────── - -st.set_page_config( - page_title="RegMap — AI System ID Card", - page_icon="◈", - layout="centered", -) - -# ── Visual Identity ── -st.markdown(""" - -""", unsafe_allow_html=True) - - -# ── Reference Data ── - -INDUSTRY_SECTORS = [ - "Agriculture & Food", "Automotive & Transportation", - "Banking & Financial Services", "Construction & Real Estate", - "Consulting & Professional Services", "Defence & Security", - "Education & Training", "Energy & Utilities", - "Entertainment & Media", "Environmental Services", - "Government & Public Administration", "Healthcare & Life Sciences", - "Hospitality & Tourism", "Human Resources & Recruitment", - "Insurance", "Legal Services", "Logistics & Supply Chain", - "Manufacturing", "Mining & Natural Resources", "Non-Profit & NGO", - "Pharmaceuticals", "Retail & E-commerce", "Telecommunications", - "Technology & Software", "Other", -] - -US_STATES = [ - "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", - "Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", - "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", - "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", - "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", - "New Hampshire", "New Jersey", "New Mexico", "New York", - "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", - "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", - "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", - "West Virginia", "Wisconsin", "Wyoming", "District of Columbia", -] - -# States with specific AI regulation (for downstream analysis) -US_STATES_WITH_AI_REGULATION = [ - "California", "Colorado", "Illinois", "New York", "Texas", "Utah", -] - -# State-specific privacy laws (19 states with comprehensive privacy laws) -US_STATE_PRIVACY_LAWS = { - "California": "California CCPA/CPRA (Privacy)", - "Virginia": "Virginia VCDPA", - "Colorado": "Colorado CPA (Privacy)", - "Connecticut": "Connecticut CTDPA", - "Utah": "Utah UCPA (Privacy)", - "Iowa": "Iowa ICDPA", - "Indiana": "Indiana INCDPA", - "Tennessee": "Tennessee TIPA", - "Texas": "Texas TDPSA (Privacy)", - "Montana": "Montana MCDPA", - "Oregon": "Oregon OCPA", - "Delaware": "Delaware DPGA", - "New Hampshire": "New Hampshire Privacy Act", - "New Jersey": "New Jersey DPA", - "Nebraska": "Nebraska DPA", - "Maryland": "Maryland MODPA", - "Minnesota": "Minnesota CDPA", - "Kentucky": "Kentucky KCDPA", - "Rhode Island": "Rhode Island DPA", -} - -EU_COUNTRIES = [ - "Austria", "Belgium", "Bulgaria", "Croatia", "Cyprus", "Czechia", - "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", - "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", - "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", - "Slovenia", "Spain", "Sweden", - "Iceland (EEA)", "Liechtenstein (EEA)", "Norway (EEA)", -] - -UAE_EMIRATES = [ - "Dubai", "Abu Dhabi", "Sharjah", "Ajman", - "Fujairah", "Ras Al Khaimah", "Umm Al Quwain", -] - -UAE_FREE_ZONES = { - "Dubai": ["DIFC", "DMCC", "JAFZA", "Dubai Internet City", - "Dubai Media City", "Dubai Silicon Oasis", "Dubai Healthcare City", - "Dubai Design District (d3)", "Dubai South", "Dubai Knowledge Park", - "DAFZA", "Dubai World Trade Centre", "Dubai Science Park", - "Dubai Textile City", "DUCAMZ", "Dubai Maritime City", - "Meydan Free Zone", "IFZA"], - "Abu Dhabi": ["ADGM", "KIZAD", "Masdar City", "ADAFZ", - "Khalifa Port Free Trade Zone", "Twofour54"], - "Sharjah": ["Sharjah Media City (Shams)", "SAIF Zone", - "Sharjah Publishing City", "Hamriyah Free Zone"], - "Ras Al Khaimah": ["RAKEZ", "RAK Maritime City", "RAK Media City"], - "Ajman": ["Ajman Free Zone"], - "Fujairah": ["Fujairah Free Zone", "Fujairah Creative City"], - "Umm Al Quwain": ["UAQ Free Trade Zone"], -} - -ROLES = [ - "Provider — You develop or commission the AI system", - "Deployer — You use an AI system in your operations (you did not build it)", - "Authorised Representative (EU) — You act on behalf of a non-EU provider to fulfill EU AI Act obligations", - "Importer (EU) — You place on the EU market an AI system from a non-EU provider", - "Distributor (EU) — You make an AI system available on the EU market (neither provider nor importer)", -] - -AI_TYPES = ["Machine Learning (ML)", "Generative AI (GenAI)", "Agentic AI", "Rule-based"] - -DATA_TYPE_OPTIONS = [ - "Personal data (e.g. name, email, ID)", - "Pseudonymised data (e.g. hashed identifiers, tokenised records)", - "Sensitive/special category data (e.g. health, race, religion, political opinions, sexual orientation)", - "Biometric data (e.g. fingerprints, facial recognition, voice)", - "Children's data (<18)", - "Synthetic data", - "Copyrighted content (text, images, audio, video protected by intellectual property)", - "No personal data", -] - -TRAINING_SOURCES = ["Public web", "Licensed datasets", "Proprietary data", "User-generated content", "None of the above"] -INVOLVEMENT_LEVELS = ["Fully automated", "Human-assisted", "Human decides"] - -ALL_COUNTRIES = [ - "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Argentina", - "Armenia", "Australia", "Austria", "Azerbaijan", "Bahrain", "Bangladesh", - "Belarus", "Belgium", "Bolivia", "Bosnia and Herzegovina", "Brazil", - "Brunei", "Bulgaria", "Cambodia", "Cameroon", "Canada", "Chile", "China", - "Colombia", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czechia", - "Denmark", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", - "Estonia", "Ethiopia", "Finland", "France", "Georgia", "Germany", - "Ghana", "Greece", "Guatemala", "Honduras", "Hong Kong", "Hungary", - "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", - "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kuwait", - "Latvia", "Lebanon", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", - "Malaysia", "Malta", "Mexico", "Moldova", "Monaco", "Mongolia", - "Montenegro", "Morocco", "Mozambique", "Myanmar", "Nepal", - "Netherlands", "New Zealand", "Nicaragua", "Nigeria", "North Korea", - "North Macedonia", "Norway", "Oman", "Pakistan", "Panama", - "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", - "Romania", "Russia", "Rwanda", "Saudi Arabia", "Senegal", "Serbia", - "Singapore", "Slovakia", "Slovenia", "Somalia", "South Africa", - "South Korea", "Spain", "Sri Lanka", "Sudan", "Sweden", "Switzerland", - "Syria", "Taiwan", "Tanzania", "Thailand", "Tunisia", "Turkey", - "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", - "United States", "Uruguay", "Uzbekistan", "Venezuela", "Vietnam", - "Yemen", "Zambia", "Zimbabwe", -] - - -# ── Session State ── - -TOTAL_SCREENS = 10 - -if "screen" not in st.session_state: - st.session_state.screen = 1 -if "data" not in st.session_state: - st.session_state.data = {} -if "detected_regs" not in st.session_state: - st.session_state.detected_regs = [] -if "qualification_answers" not in st.session_state: - st.session_state.qualification_answers = {} - - -def go_next(): - if st.session_state.screen < TOTAL_SCREENS + 1: - st.session_state.screen += 1 - -def go_back(): - if st.session_state.screen > 1: - st.session_state.screen -= 1 - -def go_to(n): - st.session_state.screen = n - - -def render_progress_tracker(current_screen): - """Render a horizontal 4-step progress tracker with time estimates.""" - steps = [ - {"num": 1, "label": "ID Card", "sub": "~3 min", "screens": range(1, 8)}, - {"num": 2, "label": "Regulatory Map", "sub": "~1 min", "screens": [8]}, - {"num": 3, "label": "Deep Dive", "sub": "~3 min", "screens": [9, 10]}, - {"num": 4, "label": "Checklist & Export", "sub": "~1 min", "screens": [11]}, - ] - - # Determine active step - active_step = 1 - for s in steps: - if current_screen in s["screens"]: - active_step = s["num"] - break - - # Fill percentage: 0%, 33%, 66%, 100% - fill_pct = int((active_step - 1) / (len(steps) - 1) * 100) - - dots_html = "" - for s in steps: - if s["num"] < active_step: - state = "done" - elif s["num"] == active_step: - state = "current" - else: - state = "future" - dots_html += f'''
-
{s["num"]}
-
{s["label"]}
-
{s["sub"]}
-
''' - - tracker = f'''
-
-
- {dots_html} -
-
''' - - st.markdown(tracker, unsafe_allow_html=True) - - -def generate_full_pdf(data, detected_regs, qualification_answers, all_obligations, applicable_overlaps, disclaimer_text, map_only=False, gap_items=None): - """Generate a comprehensive PDF report covering ID Card, Regulatory Map, Requirements, Synergies, Gaps, and Checklist.""" - import re - from reportlab.lib.pagesizes import A4 - from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle - from reportlab.lib.colors import HexColor - from reportlab.lib.units import mm - from reportlab.lib.enums import TA_CENTER, TA_RIGHT - from reportlab.platypus import ( - SimpleDocTemplate, Paragraph, Spacer, HRFlowable, PageBreak, KeepTogether, - ) - - buf = io.BytesIO() - W, H = A4 - system_name = data.get("name", "AI System") - - def clean(text): - """Strip emojis and special characters that reportlab can't render.""" - t = str(text) - t = re.sub(r'[^\u0000-\uFFFF]', '', t) - t = re.sub(r'[\uFE0E\uFE0F\u2600-\u27BF\u2B50-\u2B55\u23E9-\u23FA\u200D]', '', t) - return t.strip() - - teal = HexColor("#0D9488") - dark = HexColor("#0F2B46") - grey = HexColor("#64748B") - light_grey = HexColor("#94A3B8") - - def header_footer(canvas, doc): - canvas.saveState() - canvas.setStrokeColor(teal) - canvas.setLineWidth(0.5) - canvas.line(20*mm, H - 14*mm, W - 20*mm, H - 14*mm) - canvas.setFont("Helvetica-Bold", 7) - canvas.setFillColor(teal) - canvas.drawString(20*mm, H - 12.5*mm, "RegMap") - canvas.setFont("Helvetica", 7) - canvas.setFillColor(light_grey) - canvas.drawRightString(W - 20*mm, H - 12.5*mm, f"{clean(system_name)} AI System") - canvas.setStrokeColor(light_grey) - canvas.setLineWidth(0.3) - canvas.line(20*mm, 14*mm, W - 20*mm, 14*mm) - canvas.setFont("Helvetica", 6) - canvas.setFillColor(light_grey) - canvas.drawString(20*mm, 10*mm, "RegMap by Ines Bedar — License: CC BY-NC 4.0") - canvas.drawRightString(W - 20*mm, 10*mm, f"Page {doc.page}") - canvas.restoreState() - - doc = SimpleDocTemplate( - buf, pagesize=A4, - leftMargin=20*mm, rightMargin=20*mm, - topMargin=22*mm, bottomMargin=22*mm, - ) - - styles = getSampleStyleSheet() - s_title = ParagraphStyle("rm_title", parent=styles["Title"], fontSize=20, textColor=dark, spaceAfter=2, leading=24) - s_subtitle = ParagraphStyle("rm_sub", parent=styles["Normal"], fontSize=10, textColor=grey, spaceAfter=4, leading=14) - s_meta = ParagraphStyle("rm_meta", parent=styles["Normal"], fontSize=8, textColor=light_grey, spaceAfter=2) - s_h1 = ParagraphStyle("rm_h1", parent=styles["Heading1"], fontSize=14, textColor=dark, spaceBefore=16, spaceAfter=6, leading=18) - s_h2 = ParagraphStyle("rm_h2", parent=styles["Heading2"], fontSize=11, textColor=teal, spaceBefore=10, spaceAfter=4, leading=14) - s_body = ParagraphStyle("rm_body", parent=styles["Normal"], fontSize=9, textColor=dark, spaceAfter=2, leading=12.5) - s_item = ParagraphStyle("rm_item", parent=styles["Normal"], fontSize=9, textColor=dark, leftIndent=6, spaceAfter=2.5, leading=12.5) - s_source = ParagraphStyle("rm_src", parent=styles["Normal"], fontSize=7, textColor=grey, leftIndent=6, spaceAfter=4, leading=10) - s_overlap_title = ParagraphStyle("rm_ovt", parent=styles["Normal"], fontSize=9, textColor=dark, leftIndent=6, spaceAfter=1, leading=12.5) - s_overlap_body = ParagraphStyle("rm_ovb", parent=styles["Normal"], fontSize=8, textColor=grey, leftIndent=6, spaceAfter=6, leading=11) - s_disclaimer = ParagraphStyle("rm_disc", parent=styles["Normal"], fontSize=7, textColor=light_grey, spaceBefore=12, leading=10) - s_center = ParagraphStyle("rm_center", parent=styles["Normal"], fontSize=9, textColor=grey, alignment=TA_CENTER, spaceAfter=8) - - story = [] - - # ════════════════════════════════════════ - # COVER - # ══════════════════════════��═════════════ - story.append(Spacer(1, 10)) - story.append(Paragraph("RegMap", s_title)) - story.append(Paragraph("AI Regulatory Compliance Report", s_subtitle)) - story.append(HRFlowable(width="100%", thickness=1.5, color=teal, spaceAfter=8)) - - gen_date = datetime.now().strftime("%d %B %Y") - story.append(Paragraph(f"System: {clean(system_name)} AI System", s_meta)) - story.append(Paragraph(f"Generated: {gen_date}", s_meta)) - story.append(Paragraph(f"Author: Ines Bedar", s_meta)) - story.append(Spacer(1, 6)) - - if gap_items is None: - gap_items = [] - total_obl = sum(len(o["obligations"]) for o in all_obligations) if isinstance(all_obligations, list) else 0 - - # ════════════════════════════════════════ - # 1. AI SYSTEM ID CARD - # ════════════════════════════════════════ - story.append(Paragraph("1. AI System ID Card", s_h1)) - story.append(HRFlowable(width="100%", thickness=0.5, color=light_grey, spaceAfter=6)) - - # Build smart Markets string: US (states), UAE (free zones) - countries = data.get("operating_countries", []) - us_states = data.get("us_states", []) - uae_fz = data.get("uae_free_zones", []) - market_parts = [] - for c in countries: - if c == "United States" and us_states: - market_parts.append(f"United States ({', '.join(us_states)})") - elif c == "United Arab Emirates" and uae_fz: - market_parts.append(f"United Arab Emirates ({', '.join(uae_fz)})") - else: - market_parts.append(c) - markets_str = ", ".join(market_parts) if market_parts else "—" - - fields = [ - ("System Name", f"{data.get('name', '—')} AI System"), - ("Description", data.get("description", "—")), - ("Lifecycle Stage", data.get("lifecycle", "—")), - ("Sector(s)", ", ".join(data.get("sector", ["—"]))), - ("Organisation Type", data.get("org_type", "—")), - ("Public Services", ", ".join(ps) if (ps := data.get("provides_public_services", [])) and "None of the above" not in ps else "None"), - ("Organisation Size", data.get("company_size", "—")), - ("EU SME Status", "Yes" if data.get("is_sme", False) else "No"), - ("Headquarters", data.get("company_base", "—")), - ("Markets", markets_str), - ("Role(s)", ", ".join(data.get("roles", ["—"]))), - ("AI Type", ", ".join(data.get("ai_type", ["—"])) if isinstance(data.get("ai_type"), list) else data.get("ai_type", "—")), - ("AI Capabilities", ", ".join(data.get("capabilities", ["—"]))), - ("Data Types", ", ".join(data.get("data_types", ["—"]))), - ] - - for label, value in fields: - story.append(Paragraph(f"{label}: {clean(value)}", s_body)) - - # ════════════════════════════════════════ - # 2. REGULATORY MAP - # ════════════════════════════════════════ - story.append(PageBreak()) - story.append(Paragraph("2. Regulatory Map", s_h1)) - story.append(HRFlowable(width="100%", thickness=0.5, color=light_grey, spaceAfter=6)) - story.append(Paragraph("Regulations detected based on geographic scope, sector, data types, and AI capabilities.", s_center)) - - # Merge GPAI into EU AI Act for display - gpai_name = "EU AI Act — GPAI Framework (Chapter V)" - has_gpai_pdf = any(n == gpai_name for _, n, _, _ in detected_regs) - pdf_display_regs = [] - for tag, name, cat, reason in detected_regs: - if name == gpai_name: - continue - if name == "EU AI Act (Regulation 2024/1689)" and has_gpai_pdf: - pdf_display_regs.append((tag, name, cat, reason + " — incl. GPAI provisions (Chapter V)")) - else: - pdf_display_regs.append((tag, name, cat, reason)) - - ai_regs = [(t, n, r) for t, n, c, r in pdf_display_regs if c == "ai"] - other_regs = [(t, n, r) for t, n, c, r in pdf_display_regs if c == "other"] - - if ai_regs: - story.append(Paragraph("AI-Specific Regulations", s_h2)) - for tag, name, reason in ai_regs: - url = REGULATION_URLS.get(name, "") - link = f' [Official text]' if url else "" - story.append(Paragraph(f"[{tag}] {clean(name)}{link}", s_item)) - story.append(Paragraph(f"{clean(reason)}", s_source)) - - if other_regs: - story.append(Paragraph("Other Applicable Regulations", s_h2)) - for tag, name, reason in other_regs: - url = REGULATION_URLS.get(name, "") - link = f' [Official text]' if url else "" - story.append(Paragraph(f"[{tag}] {clean(name)}{link}", s_item)) - story.append(Paragraph(f"{clean(reason)}", s_source)) - - # ════════════════════════════════════════ - # 3. REQUIREMENTS PER REGULATION (filtered by qualification) - # ════════════════════════════════════════ - if map_only: - # Skip to disclaimer — map_only PDF only includes ID Card + Reg Map - story.append(Spacer(1, 16)) - story.append(HRFlowable(width="100%", thickness=0.5, color=light_grey, spaceAfter=6)) - story.append(Paragraph(f"Disclaimer: {clean(disclaimer_text)}", s_disclaimer)) - story.append(Spacer(1, 4)) - story.append(Paragraph( - "RegMap by Ines Bedar. Licensed under Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0). " - "Commercial use prohibited without prior written consent. https://creativecommons.org/licenses/by-nc/4.0/", - s_disclaimer, - )) - doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer) - buf.seek(0) - return buf.getvalue() - - story.append(PageBreak()) - story.append(Paragraph("3. Requirements per Regulation", s_h1)) - story.append(HRFlowable(width="100%", thickness=0.5, color=light_grey, spaceAfter=6)) - story.append(Paragraph("Obligations filtered by your qualification answers. Only applicable categories are shown.", s_center)) - - if isinstance(all_obligations, list) and all_obligations: - # Group by regulation - from collections import OrderedDict - pdf_reg_groups = OrderedDict() - for obl in all_obligations: - rn = obl["reg_name"] - if rn not in pdf_reg_groups: - pdf_reg_groups[rn] = [] - pdf_reg_groups[rn].append(obl) - - for reg_name, obl_groups in pdf_reg_groups.items(): - url = REGULATION_URLS.get(reg_name, "") - url_text = f" [Official text]" if url else "" - story.append(Paragraph(f"{clean(reg_name)}{url_text}", s_h2)) - - for obl_group in obl_groups: - story.append(Paragraph(f"{clean(obl_group['category'])}", s_body)) - for o in obl_group["obligations"]: - story.append(Paragraph(f"[ ] {clean(o)}", s_item)) - if obl_group.get("deadline"): - story.append(Paragraph(f"Deadline: {clean(obl_group['deadline'])}", s_source)) - else: - # Fallback: list other detected regulations with one-liners - for tag, reg_name, cat, reason in detected_regs: - if reg_name in OTHER_REG_ONE_LINERS: - story.append(Paragraph(f"{clean(reg_name)}", s_h2)) - story.append(Paragraph(clean(OTHER_REG_ONE_LINERS[reg_name]), s_body)) - - # Also list other detected regs with one-liners (not in OBLIGATIONS) - listed_regs = set(o["reg_name"] for o in all_obligations) if isinstance(all_obligations, list) else set() - for tag, reg_name, cat, reason in detected_regs: - if reg_name not in listed_regs and reg_name in OTHER_REG_ONE_LINERS and reg_name != "EU AI Act — GPAI Framework (Chapter V)": - url = REGULATION_URLS.get(reg_name, "") - url_text = f" [Official text]" if url else "" - story.append(Paragraph(f"{clean(reg_name)}{url_text}", s_h2)) - story.append(Paragraph(clean(OTHER_REG_ONE_LINERS[reg_name]), s_body)) - - # ════════════════════════════════════════ - # 4. COMPLIANCE SYNERGIES & OVERLAPS - # ════════════════════════════════════════ - section_num = 4 - if applicable_overlaps: - story.append(PageBreak()) - story.append(Paragraph(f"{section_num}. Compliance Synergies", s_h1)) - story.append(HRFlowable(width="100%", thickness=0.5, color=light_grey, spaceAfter=6)) - story.append(Paragraph("Where obligations across regulations overlap. Implement each topic once to satisfy all tagged regulations.", s_center)) - - for ov in applicable_overlaps: - active = ov.get("active_regulations", ov["regulations"]) - icon_char = ov.get("icon", "") - reg_labels = ov.get("reg_labels", {}) - story.append(Paragraph(f"{clean(ov['title'])}", s_overlap_title)) - # List participating regs with their specific label - for r in active: - short = r.replace("EU AI Act — GPAI Framework (Chapter V)", "EU AI Act (GPAI)") - label = reg_labels.get(r, "") - if label: - story.append(Paragraph(f"- {clean(short)}: {clean(label)}", s_item)) - else: - story.append(Paragraph(f"- {clean(short)}", s_item)) - story.append(Paragraph(f"Recommendation: {clean(ov['recommendation'])}", s_overlap_body)) - all_short = ", ".join(r.replace("EU AI Act — GPAI Framework (Chapter V)", "EU AI Act (GPAI)") for r in active) - for elem in ov.get("shared_elements", []): - story.append(Paragraph(f"[ ] {clean(elem)} ({clean(all_short)})", s_item)) - story.append(Spacer(1, 6)) - section_num += 1 - - # ════════════════════════════════════════ - # 5. GAP ANALYSIS - # ════════════════════════════════════════ - if gap_items: - story.append(PageBreak()) - story.append(Paragraph(f"{section_num}. Cross-Jurisdiction Gap Analysis", s_h1)) - story.append(HRFlowable(width="100%", thickness=0.5, color=light_grey, spaceAfter=6)) - story.append(Paragraph("When complying with one regulation, estimated coverage and additional requirements for another.", s_center)) - - for src, tgt, gap in gap_items: - cov = gap["coverage"] - story.append(Paragraph(f"{clean(src)} -> {clean(tgt)} ({cov}% already covered)", s_overlap_title)) - story.append(Paragraph("Already covered:", s_body)) - for c in gap.get("covered", []): - story.append(Paragraph(f"+ {clean(c)}", s_item)) - story.append(Paragraph("Gaps to address:", s_body)) - for g in gap.get("gaps", []): - story.append(Paragraph(f"[ ] {clean(g)}", s_item)) - story.append(Spacer(1, 4)) - section_num += 1 - - # ════════════════════════════════════════ - # DISCLAIMER + LICENSE - # ════════════════════════════════════════ - story.append(Spacer(1, 16)) - story.append(HRFlowable(width="100%", thickness=0.5, color=light_grey, spaceAfter=6)) - story.append(Paragraph(f"Disclaimer: {clean(disclaimer_text)}", s_disclaimer)) - story.append(Spacer(1, 4)) - story.append(Paragraph( - "RegMap by Ines Bedar. Licensed under Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0). " - "Commercial use prohibited without prior written consent. https://creativecommons.org/licenses/by-nc/4.0/", - s_disclaimer, - )) - - doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer) - buf.seek(0) - return buf.getvalue() - - -# ── Shared qualification helpers (used by Screen 10, 11, and PDF) ── - -def classify_eu_ai_act(answers, is_provider, is_deployer): - """Return applicable EU AI Act categories and obligation keys.""" - cats = [] - prefix = "q_EU AI Act (Regulation 2024/1689)_" - exceptions = answers.get(prefix + "euaia_exception", []) - full_exemptions = EU_FULL_EXEMPTIONS - if any(ex in exceptions for ex in full_exemptions): - return ["exempt"], [] - prohibited = answers.get(prefix + "euaia_prohibited", []) - if prohibited and "None of the above" not in prohibited: - return ["prohibited"], ["prohibited"] - annex3 = answers.get(prefix + "euaia_annex3", []) - obl_keys = [] - if annex3 and "None of the above" not in annex3: - art6_3 = answers.get(prefix + "euaia_art6_3", "— Select —") - if "Yes" not in art6_3: - cats.append("high_risk") - if is_provider: - obl_keys.append("high_risk_provider") - if is_deployer: - obl_keys.append("high_risk_deployer") - transparency = answers.get(prefix + "euaia_transparency", []) - if transparency and "None of the above" not in transparency: - cats.append("limited_risk") - obl_keys.append("limited_risk") - if not cats: - cats.append("minimal_risk") - obl_keys.append("minimal_risk") - return cats, obl_keys - - -def classify_gpai(answers): - """Return GPAI obligation key.""" - prefix = "q_EU AI Act — GPAI Framework (Chapter V)_" - systemic = answers.get(prefix + "gpai_systemic", "— Select —") - open_source = answers.get(prefix + "gpai_open_source", "— Select —") - if "Yes" in systemic: - return "gpai_systemic" - elif "open-source" in open_source.lower(): - return "gpai_open_source" - return "gpai_standard" - - -def classify_colorado(answers, is_provider, is_deployer, data=None): - """Return Colorado obligation keys or 'exempt'.""" - prefix = "q_Colorado AI Act (SB 24-205)_" - consequential = answers.get(prefix + "co_consequential", []) - exceptions = answers.get(prefix + "co_exception", []) - if not consequential or "None of the above — system does not make consequential decisions" in consequential: - return ["exempt"] - if exceptions and "None of the above" not in exceptions: - if any("approved/regulated by a federal agency" in e.lower() for e in exceptions): - return ["exempt"] - keys = [] - if is_provider: - keys.append("developer") - if is_deployer: - # Auto-detect small deployer from company size (Point 5) - is_small = (data or {}).get("is_small_business", False) - use_as_intended = answers.get(prefix + "co_use_as_intended", "— Select —") - if is_small and "Yes" in use_as_intended: - keys.append("small_deployer_exemption") - else: - keys.append("deployer") - return keys if keys else ["deployer"] - - -def requires_fria(answers, data): - """Determine if FRIA (Art. 27) is required based on sector and org type. - Returns True if the deployer must conduct a FRIA.""" - # 1. Public sector → always required - if data.get("is_public_sector", False): - return True - # 2. Private sector providing public services → required (from ID Card) - public_services = data.get("provides_public_services", []) - if public_services and "None of the above" not in public_services: - return True - # 3. Credit scoring or life/health insurance (Annex III category 5) → required - prefix = "q_EU AI Act (Regulation 2024/1689)_" - annex3 = answers.get(prefix + "euaia_annex3", []) - for a in annex3: - if "credit scoring" in a.lower() or "insurance" in a.lower(): - return True - return False - - -def classify_difc_reg10(answers): - """Return DIFC Reg 10 obligation keys.""" - prefix = "q_DIFC Regulation 10 (AI Processing)_" - keys = [] - autonomous = answers.get(prefix + "difc_autonomous", "— Select —") - if "No" in autonomous: - return ["exempt"] - keys.append("deployer_operator") - commercial = answers.get(prefix + "difc_commercial_high_risk", "— Select —") - if "Yes" in commercial: - keys.append("high_risk") - return keys - - -def classify_texas(answers, data): - """Return Texas TRAIGA obligation keys based on entity type.""" - prefix = "q_Texas TRAIGA (HB 149)_" - entity = answers.get(prefix + "tx_entity_type", "— Select —") - keys = ["all_covered"] # Prohibited practices apply to all - if "state agency" in entity.lower() or "government" in entity.lower(): - keys.append("government_deployer") - if "healthcare" in entity.lower(): - keys.append("healthcare_deployer") - # Also check org_type from ID Card as fallback - if data.get("is_public_sector", False) and "government_deployer" not in keys: - keys.append("government_deployer") - return keys - - -def classify_illinois(answers): - """Return Illinois HB 3773 obligation keys.""" - prefix = "q_Illinois HB 3773 (AI in Employment)_" - employment = answers.get(prefix + "il_employment_ai", []) - if not employment or "None of the above" in employment: - return ["exempt"] - keys = ["employer_hb3773"] - video = answers.get(prefix + "il_video_interview", "— Select —") - if "Yes" in video: - keys.append("employer_aivia") - return keys - - -def classify_california(answers, data): - """Return California CCPA/ADMT obligation keys or 'exempt'.""" - # Government and non-profit entities are exempt from CCPA entirely - if data.get("is_public_sector", False) or data.get("org_type") == "Non-profit / NGO / academic institution": - return ["exempt"] - prefix = "q_California CCPA / ADMT Regulations_" - threshold = answers.get(prefix + "ca_threshold", []) - if not threshold or "None of the above" in threshold: - return ["exempt"] - admt = answers.get(prefix + "ca_admt", "— Select —") - if "No" in admt: - return ["exempt"] - return ["deployer"] - - -def classify_ca_sb53(answers): - """Return CA SB 53 obligation keys or 'not_applicable'.""" - prefix = "q_California SB 53 (Frontier AI Transparency)_" - frontier = answers.get(prefix + "casb53_frontier", "— Select —") - revenue = answers.get(prefix + "casb53_revenue", "— Select —") - if "No" in frontier: - return ["not_applicable"] - if "Yes" not in frontier: - return ["not_applicable"] - # Frontier model confirmed - if "Yes" in revenue: - return ["frontier_developer", "large_frontier_developer"] - return ["frontier_developer"] - - -def classify_ny_raise(answers): - """Return NY RAISE Act obligation keys or 'not_applicable'.""" - prefix = "q_New York RAISE Act (Frontier AI Safety)_" - frontier = answers.get(prefix + "nyraise_frontier", "— Select —") - revenue = answers.get(prefix + "nyraise_revenue", "— Select —") - if "No" in frontier: - return ["not_applicable"] - if "Yes" not in frontier: - return ["not_applicable"] - # Frontier model confirmed — RAISE Act requires >$500M revenue - if "Yes" in revenue: - return ["large_developer"] - return ["not_applicable"] - - -def collect_all_obligations(detected_regs, answers, roles, data=None): - """Collect all applicable obligations, returning structured data. - Returns: list of dicts: {'reg_name': str, 'category': str, 'obligations': [str], 'is_ai': bool} - """ - if data is None: - data = {} - is_provider = any("Provider" in r for r in roles) - is_deployer = any("Deployer" in r for r in roles) - gpai_name = "EU AI Act — GPAI Framework (Chapter V)" - result = [] - - for tag, reg_name, cat, reason in detected_regs: - if reg_name == gpai_name: - continue # Handled under EU AI Act - if reg_name not in OBLIGATIONS: - continue - - reg_oblig = OBLIGATIONS[reg_name] - is_ai = (cat == "ai") - - if reg_name == "EU AI Act (Regulation 2024/1689)": - cats, obl_keys = classify_eu_ai_act(answers, is_provider, is_deployer) - if "exempt" in cats: - continue - for key in obl_keys: - if key in reg_oblig: - result.append({ - "reg_name": reg_name, - "category": reg_oblig[key].get("label", key), - "obligations": reg_oblig[key]["obligations"], - "deadline": reg_oblig[key].get("deadline", ""), - "is_ai": True, - }) - # FRIA — only if conditions met (Point 6) - if is_deployer and "high_risk" in cats and "high_risk_deployer_fria" in reg_oblig: - if requires_fria(answers, data): - fria = reg_oblig["high_risk_deployer_fria"] - result.append({ - "reg_name": reg_name, - "category": fria.get("label", "FRIA"), - "obligations": fria["obligations"], - "deadline": fria.get("deadline", ""), - "is_ai": True, - }) - # GPAI - has_gpai = any(n == gpai_name for _, n, _, _ in detected_regs) - if has_gpai and gpai_name in OBLIGATIONS: - gpai_key = classify_gpai(answers) - gpai_oblig = OBLIGATIONS[gpai_name] - if gpai_key in gpai_oblig: - result.append({ - "reg_name": "EU AI Act (Regulation 2024/1689)", - "category": gpai_oblig[gpai_key].get("label", gpai_key) + " (GPAI)", - "obligations": gpai_oblig[gpai_key]["obligations"], - "deadline": gpai_oblig[gpai_key].get("deadline", ""), - "is_ai": True, - }) - - elif reg_name == "Colorado AI Act (SB 24-205)": - co_keys = classify_colorado(answers, is_provider, is_deployer, data) - if "exempt" in co_keys: - continue - for key in co_keys: - if key in reg_oblig: - result.append({ - "reg_name": reg_name, - "category": reg_oblig[key].get("label", key), - "obligations": reg_oblig[key]["obligations"], - "deadline": reg_oblig[key].get("deadline", ""), - "is_ai": True, - }) - - elif reg_name == "DIFC Regulation 10 (AI Processing)": - difc_keys = classify_difc_reg10(answers) - if "exempt" in difc_keys: - continue - for key in difc_keys: - if key in reg_oblig: - result.append({ - "reg_name": reg_name, - "category": reg_oblig[key].get("label", key), - "obligations": reg_oblig[key]["obligations"], - "deadline": reg_oblig[key].get("deadline", ""), - "is_ai": True, - }) - - elif "key_obligations" in reg_oblig: - # Privacy / other regs with key_obligations - result.append({ - "reg_name": reg_name, - "category": "Key Obligations", - "obligations": reg_oblig["key_obligations"], - "deadline": "", - "is_ai": False, - }) - - elif reg_name == "Texas TRAIGA (HB 149)": - tx_keys = classify_texas(answers, data) - for key in tx_keys: - if key in reg_oblig: - result.append({ - "reg_name": reg_name, - "category": reg_oblig[key].get("label", key), - "obligations": reg_oblig[key]["obligations"], - "deadline": reg_oblig[key].get("deadline", reg_oblig.get("deadline", "")), - "is_ai": True, - }) - - elif reg_name == "Illinois HB 3773 (AI in Employment)": - il_keys = classify_illinois(answers) - if "exempt" in il_keys: - continue - for key in il_keys: - if key in reg_oblig: - result.append({ - "reg_name": reg_name, - "category": reg_oblig[key].get("label", key), - "obligations": reg_oblig[key]["obligations"], - "deadline": reg_oblig[key].get("deadline", ""), - "is_ai": True, - }) - - elif reg_name == "California CCPA / ADMT Regulations": - ca_keys = classify_california(answers, data) - if "exempt" in ca_keys: - continue - for key in ca_keys: - if key in reg_oblig: - result.append({ - "reg_name": reg_name, - "category": reg_oblig[key].get("label", key), - "obligations": reg_oblig[key]["obligations"], - "deadline": reg_oblig[key].get("deadline", reg_oblig.get("deadline", "")), - "is_ai": True, - }) - - elif reg_name == "California SB 53 (Frontier AI Transparency)": - sb53_keys = classify_ca_sb53(answers) - if "not_applicable" in sb53_keys: - continue - for key in sb53_keys: - if key in reg_oblig: - result.append({ - "reg_name": reg_name, - "category": reg_oblig[key].get("label", key), - "obligations": reg_oblig[key]["obligations"], - "deadline": reg_oblig[key].get("deadline", ""), - "is_ai": True, - }) - - elif reg_name == "New York RAISE Act (Frontier AI Safety)": - raise_keys = classify_ny_raise(answers) - if "not_applicable" in raise_keys: - continue - for key in raise_keys: - if key in reg_oblig: - result.append({ - "reg_name": reg_name, - "category": reg_oblig[key].get("label", key), - "obligations": reg_oblig[key]["obligations"], - "deadline": reg_oblig[key].get("deadline", ""), - "is_ai": True, - }) - - else: - # Utah and any other simple AI regs — render all obligation blocks - for key, value in reg_oblig.items(): - if key in ("deadline", "penalty", "scope_note", "threshold_note", "exemptions", "phased_deadlines", "enforcement_note"): - continue - if isinstance(value, dict) and "obligations" in value: - result.append({ - "reg_name": reg_name, - "category": value.get("label", key), - "obligations": value["obligations"], - "deadline": value.get("deadline", ""), - "is_ai": is_ai, - }) - - return result - - -# ── Header ── -st.markdown(""" -
- -

Navigate AI regulation across jurisdictions

-
- 🇪🇺 EU - 🇺🇸 US - 🇦🇪 UAE -
-
-""", unsafe_allow_html=True) - -# ── Progress ── -current = st.session_state.screen -screen_labels = { - 1: "AI System Identity", - 2: "Application Domain", - 3: "Company Location", - 4: "Geographic Scope", - 5: "Role in Value Chain", - 6: "Technology Profile", - 7: "Data Profile", - 8: "Regulatory Map", - 9: "Qualification", - 10: "Requirements Deep Dive", - 11: "Compliance Checklist", -} -label = screen_labels.get(current, "") - -# Show intro/about/disclaimer before progress bar on Screen 1 only -if current == 1: - st.markdown('
From AI system profile to compliance checklist in minutes — RegMap identifies which regulations could apply to your AI system, synergies, and gaps across jurisdictions.
', unsafe_allow_html=True) - - with st.expander("ℹ️ About RegMap"): - st.markdown(""" -**RegMap** is a free, open-source tool that helps identify which regulations and requirements may apply to your AI systems across multiple jurisdictions. - -**Why RegMap?** - -AI regulation is fragmented — countries and regions legislate at different speeds and with varying requirements. An AI system is a complex stack of components, meaning multiple regulations may apply depending on the sector, data processed, people affected, and markets served. A single system can trigger dozens of obligations across multiple legal frameworks. - -RegMap solves this complexity in minutes. Describe your AI system, and the tool will detect applicable regulations across EU, US, and UAE (for now!), qualify your risk level, list specific obligations per regulation and role, identify cross-regulation synergies and jurisdiction gaps, and generate a consolidated compliance checklist exportable as PDF. - -RegMap does not provide legal advice. It is an orientation tool designed to give organisations a structured starting point for their AI compliance journey. - ---- - -**Built by Inès Bedar** - -AI governance and regulatory expert with 7+ years' experience across the French Government (Prime Minister's Services and Data Protection Authority) and multiple industries worldwide. - -📄 License: CC BY-NC 4.0 -""") - - with st.expander("🔬 Methodology"): - st.markdown(""" -RegMap's analysis is powered by a curated Regulatory Knowledge Base (RKB) built exclusively from official legal texts and government sources. - -**Regulations covered:** - -*AI-specific regulations:* EU AI Act (including GPAI/systemic risk provisions), Colorado AI Act, Texas TRAIGA, Utah AI Policy Act, California CCPA/ADMT Regulations, Illinois HB 3773, and DIFC Regulation 10. - -*Cross-cutting regulations:* GDPR, ePrivacy Directive, Copyright Directive, NIS2, Product Liability Directive, UAE Federal PDPL, and 20+ federal and sector-specific laws across US, EU, and UAE. - -**How detection works:** The tool matches your AI system profile (jurisdiction, sector, data types, capabilities, and value chain role) against each regulation's scope provisions. If your system falls within a regulation's geographic and material scope, that regulation is flagged as applicable. - -**How qualification works:** For each detected AI-specific regulation, targeted questions determine your exact risk category, exemptions, and obligation set. For example, EU AI Act qualification determines whether your system is high-risk (Annex III), limited-risk, or exempt. - -**How synergies and gaps work:** When multiple regulations apply, RegMap compares obligations at the requirement level. Shared requirements (e.g., impact assessments required by both EU AI Act and Colorado AI Act) are consolidated into synergies. Coverage percentages in gap analysis represent the proportion of one regulation's requirements already satisfied by compliance with another. - -**No AI involved:** All detection, qualification, and mapping logic is rule-based and deterministic. No language model generates or interprets the analysis. The RKB is manually curated and version-controlled. -""") - - st.markdown('
Beta version — This Space is public by default. We recommend not entering directly identifying data, especially in free text fields.
', unsafe_allow_html=True) - -render_progress_tracker(current) - - -# ═══════════════════════════════════════════════ -# SCREEN 1: AI System Identity -# ═══════════════════════════════════════════════ - -if current == 1: - st.markdown('

AI System ID Card

', unsafe_allow_html=True) - st.markdown('

Basic information about your AI system

', unsafe_allow_html=True) - - name = st.text_input( - "1.1 — AI system name", - value=st.session_state.data.get("name", ""), - placeholder="e.g. ResumeScreener, ChatAssist, FraudDetect", - ) - description = st.text_area( - "1.2 — Brief description", - value=st.session_state.data.get("description", ""), - placeholder="Describe what your AI system does in 1-2 sentences", - height=100, - ) - - LIFECYCLE_STAGES = [ - "Framing / Design — Defining purpose, scope, and intended use. No model trained yet", - "Development — Building, training, or fine-tuning the model. Testing in controlled environments", - "Pre-deployment — System built, undergoing validation, conformity assessment, or pilot before release", - "In production — System is live, placed on the market or put into service", - "Post-market monitoring — System in production, actively monitored for incidents and compliance", - ] - prev_lifecycle = st.session_state.data.get("lifecycle", None) - lc_options = ["— Select —"] + LIFECYCLE_STAGES - lc_default = 0 - if prev_lifecycle: - for i, opt in enumerate(lc_options): - if opt.startswith(prev_lifecycle): - lc_default = i - break - lifecycle = st.selectbox( - "1.3 — Where are you in the AI system lifecycle?", - options=lc_options, - index=lc_default, - ) - - col1, col2 = st.columns([3, 1]) - with col2: - if st.button("Next →", use_container_width=True, disabled=(not name.strip())): - st.session_state.data["name"] = name.strip() - st.session_state.data["description"] = description.strip() - if lifecycle != "— Select —": - st.session_state.data["lifecycle"] = lifecycle.split(" — ")[0] - go_next() - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 2: Application Domain -# ═══════════════════════════════════════════════ - -elif current == 2: - st.markdown('

Application Domain

', unsafe_allow_html=True) - st.markdown('

Industry sectors where your AI system is used

', unsafe_allow_html=True) - - prev_sector = st.session_state.data.get("sector", []) - sector = st.multiselect( - "2.1 — Industry sector(s) (select all that apply)", - INDUSTRY_SECTORS, - default=[s for s in prev_sector if s in INDUSTRY_SECTORS], - ) - - other_sector_text = "" - if "Other" in sector: - other_sector_text = st.text_input( - "2.2 — Please specify your sector", - value=st.session_state.data.get("other_sector_text", ""), - placeholder="e.g. Space industry, Maritime, Blockchain", - key="q_other_sector", - ) - - ORG_TYPES = [ - "Private sector entity", - "Government / public sector entity", - "Non-profit / NGO / academic institution", - ] - prev_org_type = st.session_state.data.get("org_type", None) - org_type_idx = ORG_TYPES.index(prev_org_type) if prev_org_type in ORG_TYPES else None - org_type = st.selectbox( - "2.3 — Organisation type", - ORG_TYPES, - index=org_type_idx, - placeholder="Choose an option", - key="q_org_type", - help="This determines which obligations apply under certain regulations (e.g. Texas TRAIGA disclosure requirements apply primarily to government entities).", - ) - - # ── Public services question (private orgs only) ── - PUBLIC_SERVICES_OPTIONS = [ - "Education", - "Healthcare", - "Social services", - "Housing", - "Administration of justice", - "Public administration", - "None of the above", - ] - provides_public_services = [] - if org_type is not None and org_type != "Government / public sector entity": - prev_ps = st.session_state.data.get("provides_public_services", []) - default_ps = [o for o in prev_ps if o in PUBLIC_SERVICES_OPTIONS] - provides_public_services = st.multiselect( - "2.3b — As a private organisation, does your entity provide any of the following public services?", - PUBLIC_SERVICES_OPTIONS, - default=default_ps, - key="q_public_services", - help="Private entities providing public services must conduct a Fundamental Rights Impact Assessment (FRIA) under the EU AI Act Art. 27 when deploying high-risk AI systems, just like public bodies.", - ) - - COMPANY_SIZES = [ - "Fewer than 50 employees", - "50–249 employees", - "250–999 employees", - "1,000+ employees", - ] - prev_size = st.session_state.data.get("company_size", None) - size_idx = COMPANY_SIZES.index(prev_size) if prev_size in COMPANY_SIZES else None - company_size = st.selectbox( - "2.4 — Organisation size", - COMPANY_SIZES, - index=size_idx, - placeholder="Choose an option", - key="q_company_size", - ) - - can_proceed = len(sector) > 0 and company_size is not None and org_type is not None - if "Other" in sector and not other_sector_text.strip(): - can_proceed = False - - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - if st.button("Next →", use_container_width=True, disabled=(not can_proceed)): - st.session_state.data["sector"] = sector - st.session_state.data["other_sector_text"] = other_sector_text.strip() if "Other" in sector else "" - st.session_state.data["org_type"] = org_type - st.session_state.data["is_public_sector"] = org_type == "Government / public sector entity" - st.session_state.data["provides_public_services"] = provides_public_services - st.session_state.data["company_size"] = company_size - st.session_state.data["is_small_business"] = company_size == "Fewer than 50 employees" - go_next() - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 3: Company Location -# ═══════════════════════════════════════════════ - -elif current == 3: - st.markdown('

Company Location

', unsafe_allow_html=True) - st.markdown('

Legal establishment of your organisation

', unsafe_allow_html=True) - - prev_base = st.session_state.data.get("company_base", None) - base_idx = ALL_COUNTRIES.index(prev_base) if prev_base in ALL_COUNTRIES else None - company_base = st.selectbox( - "3.1 — Country of legal establishment", - ALL_COUNTRIES, - index=base_idx, - placeholder="Choose a country", - key="q_company_base", - ) - - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - if st.button("Next →", use_container_width=True, disabled=(not company_base)): - st.session_state.data["company_base"] = company_base - go_next() - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 4: Geographic Scope -# ═══════════════════════════════════════════════ - -elif current == 4: - st.markdown('

Geographic Scope

', unsafe_allow_html=True) - st.markdown('

Countries where your AI system has a presence

', unsafe_allow_html=True) - - prev_countries = st.session_state.data.get("operating_countries", []) - operating_countries = st.multiselect( - "4.1 — Countries where the AI system is deployed, distributed, or has end users (select all that apply)", - ALL_COUNTRIES, - default=[c for c in prev_countries if c in ALL_COUNTRIES], - key="q_operating_countries", - ) - - EU_COUNTRY_NAMES_CLEAN = [c.replace(" (EEA)", "") for c in EU_COUNTRIES] - selected_eu = [c for c in operating_countries if c in EU_COUNTRY_NAMES_CLEAN or c in ["Iceland", "Liechtenstein", "Norway"]] - has_us = "United States" in operating_countries - has_uae = "United Arab Emirates" in operating_countries - - us_states = [] - if has_us: - prev_states = st.session_state.data.get("us_states", []) - us_states = st.multiselect( - "4.2 — US states (select all that apply)", - US_STATES, - default=[s for s in prev_states if s in US_STATES], - key="q_us_states", - ) - - uae_emirates = [] - if has_uae: - prev_emirates = st.session_state.data.get("uae_emirates", []) - uae_emirates = st.multiselect( - "4.3 — UAE emirates (select all that apply)", - UAE_EMIRATES, - default=[e for e in prev_emirates if e in UAE_EMIRATES], - key="q_uae_emirates", - ) - - # 4.4 — UAE free zones (shown for all emirates) - uae_free_zones = [] - if uae_emirates: - available_fz = [] - for em in uae_emirates: - if em in UAE_FREE_ZONES: - available_fz.extend(UAE_FREE_ZONES[em]) - if available_fz: - prev_fz = st.session_state.data.get("uae_free_zones", []) - uae_free_zones = st.multiselect( - "4.4 — UAE free zones (select all that apply, or leave empty if none)", - available_fz, - default=[f for f in prev_fz if f in available_fz], - key="q_uae_free_zones", - ) - - can_proceed = len(operating_countries) > 0 - if has_us and not us_states: - can_proceed = False - if has_uae and not uae_emirates: - can_proceed = False - - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - if st.button("Next →", use_container_width=True, disabled=(not can_proceed)): - st.session_state.data["operating_countries"] = operating_countries - st.session_state.data["us_states"] = us_states if has_us else [] - st.session_state.data["us_states_with_regulation"] = [s for s in us_states if s in US_STATES_WITH_AI_REGULATION] if has_us else [] - st.session_state.data["eu_countries"] = selected_eu - st.session_state.data["uae_emirates"] = uae_emirates if has_uae else [] - st.session_state.data["uae_free_zones"] = uae_free_zones if has_uae else [] - regions = [] - if has_us: - regions.append("United States") - if selected_eu: - regions.append("Europe") - if has_uae: - regions.append("UAE") - st.session_state.data["regions"] = regions - go_next() - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 5: Role in Value Chain -# ═══════════════════════════════════════════════ - -elif current == 5: - st.markdown('

Role in Value Chain

', unsafe_allow_html=True) - st.markdown('

Your organisation\'s position in the AI value chain

', unsafe_allow_html=True) - - prev_roles = st.session_state.data.get("roles", []) - roles = st.multiselect( - "5.1 — Organisation's role (select all that apply)", - ROLES, - default=[r for r in prev_roles if r in ROLES], - key="q_roles", - ) - - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - if st.button("Next →", use_container_width=True, disabled=(not roles)): - st.session_state.data["roles"] = roles - go_next() - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 6: Technology Profile -# ═══════════════════════════════════════════════ - -elif current == 6: - st.markdown('

Technology Profile

', unsafe_allow_html=True) - st.markdown('

Technical characteristics of your AI system

', unsafe_allow_html=True) - - # 6.1 AI type - prev_type = st.session_state.data.get("ai_type", []) - # Backward compat: if old data stored a single string, convert to list - if isinstance(prev_type, str): - prev_type = [prev_type] if prev_type else [] - ai_type = st.multiselect( - "6.1 — AI system type (select all that apply)", - AI_TYPES, - default=[t for t in prev_type if t in AI_TYPES], - key="q_ai_type", - ) - - # 6.2 Model type - MODEL_TYPES = [ - "Foundation model — general-purpose, trained on broad data (e.g. GPT, Claude, Llama)", - "Fine-tuned model — adapted from a foundation model for specific tasks", - "Task-specific model — built and trained for a single purpose", - ] - prev_model = st.session_state.data.get("model_type", None) - model_idx = MODEL_TYPES.index(prev_model) if prev_model in MODEL_TYPES else None - model_type = st.selectbox("6.2 — Model type", MODEL_TYPES, index=model_idx, placeholder="Choose an option", key="q_model_type") - - # 6.3 Capabilities - CAPABILITY_OPTIONS = [ - "Interacts directly with end users", - "Makes or supports decisions that affect individuals", - "Profiles individuals (builds user profiles based on behaviour, preferences, or characteristics)", - "Generates synthetic content (text, images, audio, video, code)", - "Performs emotion recognition", - "None of the above", - ] - prev_caps = st.session_state.data.get("capabilities", []) - capabilities = st.multiselect( - "6.3 — Capabilities (select all that apply)", - CAPABILITY_OPTIONS, - default=[c for c in prev_caps if c in CAPABILITY_OPTIONS], - key="q_capabilities", - ) - # Enforce None exclusivity - none_opt_cap = "None of the above" - if none_opt_cap in capabilities and len(capabilities) > 1: - if none_opt_cap not in prev_caps: - capabilities = [none_opt_cap] - else: - capabilities = [c for c in capabilities if c != none_opt_cap] - st.session_state.data["capabilities"] = capabilities - st.rerun() - - # 6.4 Human involvement — always shown - prev_involvement = st.session_state.data.get("human_involvement", None) - INVOLVEMENT_WITH_NA = ["N/A — no decisions affecting individuals", "Fully automated", "Human-assisted", "Human decides"] - inv_idx = INVOLVEMENT_WITH_NA.index(prev_involvement) if prev_involvement in INVOLVEMENT_WITH_NA else None - human_involvement = st.selectbox( - "6.4 — Human involvement in decisions", - INVOLVEMENT_WITH_NA, - index=inv_idx, - placeholder="Choose an option", - key="q_involvement", - ) - - # 6.5 Synthetic content types — always shown - SYNTHETIC_TYPES = ["Text", "Images", "Audio", "Video", "Code", "None of the above"] - prev_content = st.session_state.data.get("synthetic_content", []) - synthetic_content = st.multiselect( - "6.5 — Synthetic content types (select all that apply)", - SYNTHETIC_TYPES, - default=[s for s in prev_content if s in SYNTHETIC_TYPES], - key="q_content", - ) - # Enforce None exclusivity - none_opt_sc = "None of the above" - if none_opt_sc in synthetic_content and len(synthetic_content) > 1: - if none_opt_sc not in prev_content: - synthetic_content = [none_opt_sc] - else: - synthetic_content = [s for s in synthetic_content if s != none_opt_sc] - st.session_state.data["synthetic_content"] = synthetic_content - st.rerun() - - can_proceed_6 = len(ai_type) > 0 and model_type is not None and human_involvement is not None - - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - if st.button("Next →", use_container_width=True, disabled=(not can_proceed_6)): - st.session_state.data["ai_type"] = ai_type - st.session_state.data["model_type"] = model_type - st.session_state.data["gpai"] = model_type.startswith("Foundation model") - st.session_state.data["capabilities"] = capabilities - st.session_state.data["direct_interaction"] = "Interacts directly with end users" in capabilities - st.session_state.data["decision_making"] = "Makes or supports decisions that affect individuals" in capabilities - st.session_state.data["profiling"] = "Profiles individuals (builds user profiles based on behaviour, preferences, or characteristics)" in capabilities - is_decision = "Makes or supports decisions that affect individuals" in capabilities - st.session_state.data["human_involvement"] = human_involvement if is_decision and not human_involvement.startswith("N/A") else "N/A" - is_synthetic = "Generates synthetic content (text, images, audio, video, code)" in capabilities - st.session_state.data["synthetic_content"] = synthetic_content if is_synthetic else [] - st.session_state.data["emotion_recognition"] = "Performs emotion recognition" in capabilities - go_next() - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 7: Data Profile -# ═══════════════════════════════════════════════ - -elif current == 7: - st.markdown('

Data Profile

', unsafe_allow_html=True) - st.markdown('

Data processed and training sources

', unsafe_allow_html=True) - - prev_data_types = st.session_state.data.get("data_types", []) - selected_data_types = st.multiselect( - "7.1 — Data types processed (select all that apply)", - DATA_TYPE_OPTIONS, - default=[dt for dt in prev_data_types if dt in DATA_TYPE_OPTIONS], - ) - st.caption("Select all that apply. Most AI systems process a combination of data types (e.g. both personal and non-personal data).") - - st.divider() - - prev_sources = st.session_state.data.get("training_sources", []) - training_sources = st.multiselect( - "7.2 — Training data sources (select all that apply)", - TRAINING_SOURCES, - default=[s for s in prev_sources if s in TRAINING_SOURCES], - ) - # Enforce None exclusivity - none_opt_ts = "None of the above" - if none_opt_ts in training_sources and len(training_sources) > 1: - if none_opt_ts not in prev_sources: - training_sources = [none_opt_ts] - else: - training_sources = [s for s in training_sources if s != none_opt_ts] - st.session_state.data["training_sources"] = training_sources - st.rerun() - - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - if st.button("View Summary →", use_container_width=True, disabled=(not selected_data_types)): - st.session_state.data["data_types"] = selected_data_types - st.session_state.data["training_sources"] = training_sources - go_next() - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 8: Summary -# ═══════════════════════════════════════════════ - -elif current == 8: - d = st.session_state.data - - st.markdown('

AI System ID Card — Summary

', unsafe_allow_html=True) - st.markdown(f'

Review your inputs for {d.get("name", "")} AI System

', unsafe_allow_html=True) - - # Identity - lifecycle_str = d.get("lifecycle", "") - lifecycle_html = f'

Lifecycle: {lifecycle_str}

' if lifecycle_str else "" - st.markdown(f'

AI System Identity

Name: {d.get("name", "—")}

Description: {d.get("description", "—") or "—"}

{lifecycle_html}
', unsafe_allow_html=True) - - # Domain - sectors = d.get("sector", []) - sectors_display = ", ".join(sectors) if sectors else "—" - other_sec = d.get("other_sector_text", "") - if other_sec: - sectors_display = sectors_display.replace("Other", f"Other ({other_sec})") - st.markdown(f'

Application Domain

Sector(s): {sectors_display}

', unsafe_allow_html=True) - - # Geography - geo_lines = f'

Company base: {d.get("company_base", "—")}

' - geo_lines += f'

Organisation type: {d.get("org_type", "—")}

' - ps = d.get("provides_public_services", []) - if ps and "None of the above" not in ps: - geo_lines += f'

Public services provided: {", ".join(ps)}

' - geo_lines += f'

Organisation size: {d.get("company_size", "—")}

' - op_countries = d.get("operating_countries", []) - if op_countries: - geo_lines += f'

Countries: {", ".join(op_countries)}

' - if d.get("us_states"): - regulated = d.get("us_states_with_regulation", []) - non_regulated = [s for s in d["us_states"] if s not in regulated] - geo_lines += f'

US states: {", ".join(d["us_states"])}

' - if regulated: - geo_lines += f'

States with AI regulation: {", ".join(regulated)}

' - if non_regulated: - geo_lines += f'

States without specific AI regulation: {", ".join(non_regulated)}

' - if d.get("eu_countries"): - geo_lines += f'

EU/EEA countries detected: {", ".join(d["eu_countries"])}

' - if d.get("uae_emirates"): - geo_lines += f'

UAE emirates: {", ".join(d["uae_emirates"])}

' - if d.get("uae_free_zones"): - geo_lines += f'

UAE free zones: {", ".join(d["uae_free_zones"])}

' - st.markdown(f'

Geographic Scope

{geo_lines}
', unsafe_allow_html=True) - - # Roles - roles_display = ", ".join(d.get("roles", ["—"])) - st.markdown(f'

Role in Value Chain

{roles_display}

', unsafe_allow_html=True) - - # Technology - model_type_display = d.get("model_type", "—") - caps = d.get("capabilities", []) - caps_display = ", ".join(caps) if caps else "None selected" - synthetic = ", ".join(d.get("synthetic_content", [])) or "None" - involvement = d.get("human_involvement", "N/A") - - ai_type_val = d.get("ai_type", []) - if isinstance(ai_type_val, str): - ai_type_val = [ai_type_val] if ai_type_val else [] - ai_type_display = ", ".join(ai_type_val) if ai_type_val else "—" - tech_lines = f'

AI type: {ai_type_display}

' - tech_lines += f'

Model: {model_type_display}

' - tech_lines += f'

Capabilities: {caps_display}

' - if d.get("decision_making"): - tech_lines += f'

Human involvement in decisions: {involvement}

' - if d.get("synthetic_content"): - tech_lines += f'

Synthetic content types: {synthetic}

' - st.markdown(f'

Technology Profile

{tech_lines}
', unsafe_allow_html=True) - - # Data - data_types_display = ", ".join(d.get("data_types", ["—"])) - sources_display = ", ".join(d.get("training_sources", ["—"])) - st.markdown(f'

Data Profile

Data types: {data_types_display}

Training sources: {sources_display}

', unsafe_allow_html=True) - - # ── Regulation detection ── - # Tuples: (region_tag, regulation_name, category) - # category: "ai" = AI-specific, "other" = related regulation - detected_regs = [] - - # Emoji flags for regions - FLAG_MAP = { - "EU": "🇪🇺", "FR": "🇫🇷", "US": "🇺🇸", "UAE": "🇦🇪", - "DIFC": "🇦🇪", "ADGM": "🇦🇪", - } - - # Helper flags from ID card data - data_types = d.get("data_types", []) - sectors = d.get("sector", []) - roles = d.get("roles", []) - capabilities = d.get("capabilities", []) - - personal_data_types = [ - "Personal data (e.g. name, email, ID)", - "Pseudonymised data (e.g. hashed identifiers, tokenised records)", - "Sensitive/special category data (e.g. health, race, religion, political opinions, sexual orientation)", - "Biometric data (e.g. fingerprints, facial recognition, voice)", - "Children's data (<18)", - ] - has_personal_data = any(dt in data_types for dt in personal_data_types) - has_copyrighted = "Copyrighted content (text, images, audio, video protected by intellectual property)" in data_types - has_biometric = "Biometric data (e.g. fingerprints, facial recognition, voice)" in data_types - has_children_data = "Children's data (<18)" in data_types - has_decision_making = d.get("decision_making", False) - has_direct_interaction = d.get("direct_interaction", False) - is_provider = any("Provider" in r for r in roles) - is_sme = d.get("is_sme", False) - is_employment_sector = "Human Resources & Recruitment" in sectors - is_financial_sector = any(s in sectors for s in ["Banking & Financial Services", "Insurance"]) - is_healthcare_sector = "Healthcare & Life Sciences" in sectors - is_education_sector = "Education & Training" in sectors - is_critical_sector = any(s in sectors for s in [ - "Energy & Utilities", "Healthcare & Life Sciences", "Banking & Financial Services", - "Government & Public Administration", "Telecommunications", "Logistics & Supply Chain", - ]) - is_manufacturing_or_automotive = any(s in sectors for s in ["Manufacturing", "Automotive & Transportation"]) - is_robotics = False - is_iot_connected = False - is_platform = False - - # ── Keyword matching on AI system description + other sector ── - other_sector_text = d.get("other_sector_text", "") or "" - desc_text = ((d.get("description", "") or "") + " " + other_sector_text).lower() - - kw_sector_map = { - "is_healthcare_sector": ["medical", "diagnosis", "diagnostic", "patient", "clinical", "health", "hospital", "pharma", "drug", "therapy", "radiology", "pathology"], - "is_financial_sector": ["credit", "loan", "lending", "insurance", "underwriting", "banking", "trading", "investment", "portfolio", "risk scoring", "fraud detection"], - "is_employment_sector": ["hiring", "recruitment", "recruiting", "candidate", "resume", "cv screening", "job applicant", "employee", "workforce", "hr ", "human resources", "talent acquisition", "performance review", "workplace monitoring", "employee tracking", "employee surveillance", "worker monitoring"], - "is_education_sector": ["student", "school", "university", "education", "learning platform", "grading", "academic", "classroom", "tutor", "e-learning"], - "is_real_estate": ["housing", "tenant", "rental", "real estate", "property", "mortgage", "landlord"], - "is_critical_sector": ["energy", "power grid", "water supply", "telecom", "transport", "infrastructure", "government"], - "is_robotics": ["robot", "robotic", "autonomous", "drone", "unmanned", "self-driving", "autonomous vehicle", "cobot", "exoskeleton", "automated machine", "industrial automation"], - "is_iot_connected": ["iot", "connected device", "embedded ai", "edge ai", "smart device", "wearable", "sensor", "smart home", "smart city"], - "is_platform": ["platform", "marketplace", "content moderation", "recommendation system", "recommender", "feed algorithm", "content ranking", "social media", "online platform", "user-generated content"], - } - - kw_data_map = { - "has_personal_data": ["personal data", "user data", "customer data", "pii", "name", "email", "identity", "user profile"], - "has_biometric": ["facial recognition", "fingerprint", "biometric", "face detection", "voice recognition", "iris", "retina", "surveillance", "cctv", "video monitoring", "camera monitoring", "video analytics"], - "has_children_data": ["children", "child", "minor", "kid", "underage", "under 13", "under 18", "youth", "parental"], - "has_copyrighted": ["copyrighted", "copyright", "licensed content", "intellectual property", "training data", "scraped content", "web scraping"], - "has_decision_making": ["decision", "scoring", "ranking", "classify", "classification", "prediction", "recommend", "eligibility", "approval", "rejection", "screening", "assessment", "evaluate", "monitoring", "surveillance", "tracking"], - "has_direct_interaction": ["chatbot", "chat bot", "virtual assistant", "conversational", "customer service", "customer support", "interact with users", "end user", "consumer facing"], - } - - # Enrich sector flags from description - if desc_text: - for kw in kw_sector_map["is_healthcare_sector"]: - if kw in desc_text: - is_healthcare_sector = True; break - for kw in kw_sector_map["is_financial_sector"]: - if kw in desc_text: - is_financial_sector = True; break - for kw in kw_sector_map["is_employment_sector"]: - if kw in desc_text: - is_employment_sector = True; break - for kw in kw_sector_map["is_education_sector"]: - if kw in desc_text: - is_education_sector = True; break - if any(kw in desc_text for kw in kw_sector_map["is_real_estate"]): - if "Construction & Real Estate" not in sectors: - sectors = sectors + ["Construction & Real Estate"] - for kw in kw_sector_map["is_critical_sector"]: - if kw in desc_text: - is_critical_sector = True; break - for kw in kw_sector_map["is_robotics"]: - if kw in desc_text: - is_robotics = True; break - for kw in kw_sector_map["is_iot_connected"]: - if kw in desc_text: - is_iot_connected = True; break - for kw in kw_sector_map["is_platform"]: - if kw in desc_text: - is_platform = True; break - - # Enrich data flags from description - for kw in kw_data_map["has_personal_data"]: - if kw in desc_text: - has_personal_data = True; break - for kw in kw_data_map["has_biometric"]: - if kw in desc_text: - has_biometric = True; break - for kw in kw_data_map["has_children_data"]: - if kw in desc_text: - has_children_data = True; break - for kw in kw_data_map["has_copyrighted"]: - if kw in desc_text: - has_copyrighted = True; break - for kw in kw_data_map["has_decision_making"]: - if kw in desc_text: - has_decision_making = True; break - for kw in kw_data_map["has_direct_interaction"]: - if kw in desc_text: - has_direct_interaction = True; break - - # Sector-based flags (outside desc_text block) - if is_manufacturing_or_automotive: - is_robotics = True - - # ── EU ── - company_base = d.get("company_base", "") - eu_countries = d.get("eu_countries", []) - EU_BASE_NAMES = [c.replace(" (EEA)", "") for c in EU_COUNTRIES] - has_eu = len(eu_countries) > 0 or company_base in EU_BASE_NAMES - - if has_eu: - # AI-specific - detected_regs.append(("EU", "EU AI Act (Regulation 2024/1689)", "ai", "AI system operates in or targets the EU market")) - if d.get("gpai", False): - detected_regs.append(("EU", "EU AI Act — GPAI Framework (Chapter V)", "ai", "General-purpose AI model with EU presence")) - # Related - if has_personal_data: - detected_regs.append(("EU", "GDPR (Regulation 2016/679)", "other", "Personal data is processed within the EU")) - # France-specific: Loi Informatique et Libertés - if "France" in eu_countries or company_base == "France": - detected_regs.append(("FR", "Loi Informatique et Libertés (Loi n° 78-17)", "other", "Personal data processed in France — national GDPR implementation with CNIL-specific requirements")) - if has_copyrighted: - detected_regs.append(("EU", "Copyright Directive (2019/790)", "other", "Copyrighted content is used (training or output)")) - if is_critical_sector: - detected_regs.append(("EU", "NIS2 Directive (2022/2555)", "other", "Critical infrastructure sector in the EU")) - if is_provider: - detected_regs.append(("EU", "Product Liability Directive (2024/2853)", "other", "You are a provider — strict liability applies to AI products")) - if has_decision_making: - detected_regs.append(("EU", "Equal Treatment Directives", "other", "AI system makes or supports decisions affecting individuals")) - if has_direct_interaction: - detected_regs.append(("EU", "Consumer Rights Directive / GPSR", "other", "AI system interacts directly with consumers")) - detected_regs.append(("EU", "ePrivacy Directive (2002/58/EC)", "other", "Direct interaction may involve electronic communications data")) - if is_healthcare_sector: - detected_regs.append(("EU", "Medical Device Regulation (MDR 2017/745)", "other", "Healthcare sector — AI may qualify as a medical device")) - if is_robotics: - detected_regs.append(("EU", "Machinery Regulation (2023/1230)", "other", "AI integrated in machinery, robotics, or autonomous systems")) - if is_platform: - detected_regs.append(("EU", "Digital Services Act (DSA 2022/2065)", "other", "Online platform with algorithmic content moderation or recommendation")) - if is_iot_connected: - detected_regs.append(("EU", "Radio Equipment Directive (RED 2014/53)", "other", "AI embedded in connected devices or IoT hardware")) - - # ── US ── - has_us = "United States" in d.get("operating_countries", []) - us_regulated = d.get("us_states_with_regulation", []) - - # Federal laws first - if has_us: - detected_regs.append(("US", "FTC Act Section 5 (Unfair/Deceptive Practices)", "other", "AI system operates in the US market")) - if is_employment_sector: - detected_regs.append(("US", "Title VII (Civil Rights Act)", "other", "Employment sector — AI must not discriminate on protected characteristics")) - detected_regs.append(("US", "ADA (Americans with Disabilities Act)", "other", "Employment sector — AI must accommodate disabilities")) - if is_financial_sector: - detected_regs.append(("US", "ECOA (Equal Credit Opportunity Act)", "other", "Financial sector — credit decisions must be non-discriminatory")) - detected_regs.append(("US", "FCRA (Fair Credit Reporting Act)", "other", "Financial sector — AI may qualify as consumer reporting agency")) - if "Construction & Real Estate" in sectors: - detected_regs.append(("US", "Fair Housing Act", "other", "Real estate sector — AI must not discriminate in housing decisions")) - if is_healthcare_sector and has_personal_data: - detected_regs.append(("US", "HIPAA (Health Insurance Portability and Accountability Act)", "other", "Healthcare sector with personal health data")) - if has_children_data: - detected_regs.append(("US", "COPPA (Children's Online Privacy Protection Act)", "other", "System processes children's data (<13)")) - if is_education_sector and has_personal_data: - detected_regs.append(("US", "FERPA (Family Educational Rights and Privacy Act)", "other", "Education sector with student personal data")) - - # State-level laws — privacy (replace generic catch-all with actual state laws) - us_states = d.get("us_states", []) - if has_personal_data and has_us: - for state in us_states: - if state in US_STATE_PRIVACY_LAWS: - law_name = US_STATE_PRIVACY_LAWS[state] - detected_regs.append(("US", law_name, "other", f"Personal data processed — {state} comprehensive privacy law applies")) - # US Copyright Act (federal) - if has_us and has_copyrighted: - detected_regs.append(("US", "US Copyright Act (Title 17)", "other", "Copyrighted content used — fair use (§ 107) assessed case-by-case, no specific AI/TDM exception")) - # State biometric laws - if "Texas" in us_states and has_biometric: - detected_regs.append(("US", "Texas CUBI Act (Biometric)", "other", "Biometric data processed in Texas — consent required for commercial use")) - if "Washington" in us_states and has_biometric: - detected_regs.append(("US", "Washington Biometric Privacy (HB 1493)", "other", "Biometric data enrolled in databases in Washington — notice and consent required")) - # State employment/AI laws - if "New York" in us_states and is_employment_sector: - detected_regs.append(("US", "NYC Local Law 144 (AI in Employment)", "other", "Automated employment decision tools used in NYC — annual bias audit required")) - if "California" in us_states and is_employment_sector: - detected_regs.append(("US", "California FEHA ADS Regulations", "other", "AI used in employment decisions in California — non-discrimination and record-keeping obligations")) - if "Maryland" in us_states and is_employment_sector and has_biometric: - detected_regs.append(("US", "Maryland HB 1202 (Facial Recognition in Employment)", "other", "Facial recognition used in job interviews in Maryland — applicant waiver required")) - if "Illinois" in us_states and is_employment_sector: - detected_regs.append(("US", "Illinois AIVIA (HB 2557)", "other", "AI used to analyse video interviews in Illinois — disclosure and consent required")) - # State other AI laws - if "Nevada" in us_states and is_healthcare_sector: - detected_regs.append(("US", "Nevada AB 406 (AI in Mental Health)", "other", "AI system in mental/behavioural health in Nevada — restrictions on AI diagnosis and treatment")) - if "New Jersey" in us_states and has_direct_interaction: - detected_regs.append(("US", "New Jersey AB 4563 (Bot Disclosure)", "other", "Bot interacting with NJ residents for commercial/political purposes — disclosure required")) - if "Washington" in us_states and has_decision_making: - detected_regs.append(("US", "Washington SB 5827 (Algorithmic Discrimination)", "other", "Automated decision system in Washington — algorithmic discrimination prohibited")) - # AI-specific state laws - if "Colorado" in us_regulated: - detected_regs.append(("US", "Colorado AI Act (SB 24-205)", "ai", "AI system operates in Colorado")) - if "Texas" in us_regulated: - if d.get("is_public_sector", False): - detected_regs.append(("US", "Texas TRAIGA (HB 149)", "ai", "Government entity operating AI in Texas — full disclosure + prohibited practices apply")) - else: - detected_regs.append(("US", "Texas TRAIGA (HB 149)", "ai", "AI system operates in Texas — prohibited practices apply (disclosure obligations apply to government entities only)")) - if "Utah" in us_regulated: - detected_regs.append(("US", "Utah AI Policy Act (SB 149)", "ai", "AI system operates in Utah")) - if "California" in us_regulated: - if d.get("is_public_sector", False) or d.get("org_type") == "Non-profit / NGO / academic institution": - detected_regs.append(("US", "California CCPA / ADMT Regulations", "ai", "AI system operates in California — NOTE: CCPA/ADMT does not apply to government agencies and non-profit organisations")) - else: - detected_regs.append(("US", "California CCPA / ADMT Regulations", "ai", "AI system operates in California")) - if "California" in us_states: - detected_regs.append(("US", "California SB 53 (Frontier AI Transparency)", "ai", "AI system operates in California — frontier AI transparency requirements may apply")) - if "Illinois" in us_regulated: - detected_regs.append(("US", "Illinois HB 3773 (AI in Employment)", "ai", "AI system operates in Illinois")) - if has_biometric: - detected_regs.append(("US", "Illinois BIPA (Biometric Information Privacy Act)", "other", "Biometric data processed in Illinois")) - if "New York" in us_states: - detected_regs.append(("US", "New York RAISE Act (Frontier AI Safety)", "ai", "AI system operates in New York — frontier AI safety requirements may apply")) - - # ── UAE ── - uae_emirates = d.get("uae_emirates", []) - - if uae_emirates: - # Federal laws first - if has_personal_data: - detected_regs.append(("UAE", "UAE Federal PDPL (Decree-Law 45/2021)", "other", "Personal data processed in the UAE")) - if has_copyrighted: - detected_regs.append(("UAE", "Copyright Law (Decree-Law 38/2021) — No TDM exception", "other", "Copyrighted content used — no text and data mining exception in UAE")) - detected_regs.append(("UAE", "Cybercrime Law (Decree-Law 34/2021)", "other", "AI system operates in the UAE")) - detected_regs.append(("UAE", "Civil Transactions Law (Federal Law 5/1985)", "other", "General tort liability applies to AI in the UAE")) - if has_direct_interaction: - detected_regs.append(("UAE", "Consumer Protection (Federal Law 15/2020)", "other", "AI system interacts directly with consumers in the UAE")) - if has_decision_making or is_employment_sector: - detected_regs.append(("UAE", "Anti-Discrimination (Decree-Law 34/2023)", "other", "AI makes decisions or is used in employment in the UAE")) - detected_regs.append(("UAE", "Labour Law (Decree-Law 33/2021)", "other", "AI makes decisions or is used in employment in the UAE")) - - # Free zone level — only if specific free zone selected - uae_fz = d.get("uae_free_zones", []) - if "DIFC" in uae_fz and has_personal_data: - detected_regs.append(("UAE", "DIFC Data Protection Law (Law No. 5 of 2020)", "other", "Personal data processed within DIFC free zone")) - detected_regs.append(("UAE", "DIFC Regulation 10 (AI Processing)", "ai", "AI processes personal data within DIFC free zone")) - if "ADGM" in uae_fz and has_personal_data: - detected_regs.append(("UAE", "ADGM Data Protection Regulations 2021", "other", "Personal data processed within ADGM free zone")) - - # ── Build banner ── - # Merge GPAI into EU AI Act for display (keep separate internally for obligations) - has_gpai = any(n == "EU AI Act — GPAI Framework (Chapter V)" for _, n, _, _ in detected_regs) - display_regs = [] - for tag, name, cat, reason in detected_regs: - if name == "EU AI Act — GPAI Framework (Chapter V)": - continue # Skip — merged into EU AI Act display - if name == "EU AI Act (Regulation 2024/1689)" and has_gpai: - display_regs.append((tag, name, cat, reason + " — incl. GPAI provisions (Chapter V)")) - else: - display_regs.append((tag, name, cat, reason)) - - ai_regs = [(t, n, r) for t, n, c, r in display_regs if c == "ai"] - other_regs = [(t, n, r) for t, n, c, r in display_regs if c == "other"] - - if detected_regs: - banner_content = "" - if ai_regs: - banner_content += '

AI-Specific Regulations

' - banner_content += '' - if other_regs: - label_class = "reg-section-label" if ai_regs else "reg-section-label-first" - banner_content += f'

Related Regulations

' - banner_content += '' - - banner_html = f""" -
-

⚖️ Based on your AI System ID Card, these regulations could apply:

- {banner_content} -

This list is not exhaustive. Other regulations may apply. Always consult qualified legal counsel.

-
-""" - st.markdown(banner_html, unsafe_allow_html=True) - else: - st.info("No specific AI regulations detected based on your inputs. This may change as more jurisdictions are added.") - - # Navigation - st.session_state.detected_regs = detected_regs - if ai_regs: - st.markdown("""
-

Want to go further? Continue to get qualification questions, detailed obligations per regulation, gap analysis, synergies, and a consolidated compliance checklist.

-
""", unsafe_allow_html=True) - - col1, col2, col3 = st.columns([1, 1, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col2: - if detected_regs: - try: - empty_themes = {} - pdf_bytes = generate_full_pdf(d, detected_regs, {}, empty_themes, [], DISCLAIMER, map_only=True) - safe_name = d.get("name", "AI_System").replace(" ", "_")[:30] - st.download_button( - "Export PDF", - data=pdf_bytes, - file_name=f"RegMap_{safe_name}_map.pdf", - mime="application/pdf", - use_container_width=True, - ) - except Exception: - pass - with col3: - if ai_regs: - if st.button("Next →", use_container_width=True): - go_to(9) - st.rerun() - else: - st.info("No AI-specific regulations detected.") - - -# ═══════════════════════════════════════════════ -# SCREEN 9: Qualification Questions -# ═══════════════════════════════════════════════ - -elif current == 9: - st.markdown('

Qualification

', unsafe_allow_html=True) - st.markdown('

Refine regulation applicability by answering scope questions for each detected AI-specific regulation.

', unsafe_allow_html=True) - - # Helper: enforce "None of the above" mutual exclusion - def enforce_none_exclusive(selected, previous, options): - """If 'None of the above' and other items coexist, keep the most recent intent.""" - none_opts = [o for o in options if o.startswith("None of the above")] - if not none_opts: - return selected - none_opt = none_opts[0] - has_none = none_opt in selected - other_items = [s for s in selected if s != none_opt] - if not has_none or not other_items: - return selected - # Both "None" and other items selected — resolve conflict - prev_had_none = none_opt in previous - if not prev_had_none: - # User just added "None" → keep only "None" - return [none_opt] - else: - # User added something else → remove "None" - return other_items - - detected = st.session_state.get("detected_regs", []) - ai_specific = [(tag, name, cat, reason) for tag, name, cat, reason in detected if cat == "ai"] - - if not ai_specific: - st.warning("No AI-specific regulations detected. Please go back and complete the ID Card.") - else: - answers = st.session_state.qualification_answers - d = st.session_state.data - gpai_name = "EU AI Act — GPAI Framework (Chapter V)" - has_gpai = any(n == gpai_name for _, n, _, _ in ai_specific) - - displayed = 0 - for idx, (tag, reg_name, _, reason) in enumerate(ai_specific): - if reg_name == gpai_name: - continue # Show inside EU AI Act expander - - label = f"{tag} — {reg_name}" - if reg_name == "EU AI Act (Regulation 2024/1689)" and has_gpai: - label += " (incl. GPAI)" - - with st.expander(label, expanded=True): - st.caption(f"Detected because: {reason}") - - if reg_name in QUALIFICATION_QUESTIONS: - questions = QUALIFICATION_QUESTIONS[reg_name]["questions"] - for q in questions: - q_key = f"q_{reg_name}_{q['id']}" - - # ── If full exemption selected, skip ALL remaining EU questions ── - if reg_name == "EU AI Act (Regulation 2024/1689)" and q["id"] != "euaia_exception": - exception_key = f"q_{reg_name}_euaia_exception" - exception_val = answers.get(exception_key, []) - full_exemptions = EU_FULL_EXEMPTIONS - if any(ex in exception_val for ex in full_exemptions): - answers[q_key] = [] if q["type"] == "multi_select" else "— Select —" - continue - - # ── If prohibited practice selected, skip remaining EU questions (not exception/sme/prohibited) ── - if reg_name == "EU AI Act (Regulation 2024/1689)" and q["id"] not in ("euaia_exception", "euaia_sme", "euaia_prohibited"): - prohibited_key = f"q_{reg_name}_euaia_prohibited" - prohibited_val = answers.get(prohibited_key, []) - if prohibited_val and "None of the above" not in prohibited_val: - # User selected a prohibited practice → skip all further questions - answers[q_key] = [] if q["type"] == "multi_select" else "— Select —" - continue - - # Point 4b: Skip Art. 6(3) if Annex III = None - if q["id"] == "euaia_art6_3": - annex3_key = f"q_{reg_name}_euaia_annex3" - annex3_val = answers.get(annex3_key, []) - if not annex3_val or "None of the above" in annex3_val: - answers[q_key] = "— Select —" - continue - - # ── Public services: now collected on ID Card, always skip here ── - if q["id"] == "euaia_public_services": - answers[q_key] = [] - continue - - # ── Point 5: Show Colorado use-as-intended only if <50 employees ── - if q["id"] == "co_use_as_intended": - if not d.get("is_small_business", False): - answers[q_key] = "— Select —" - continue - - # Point 7: DIFC high-risk helper - if q["id"] == "difc_commercial_high_risk": - st.caption("ℹ️ **High-risk processing activities** under DIFC include: (a) new/different technologies, (b) considerable amount of personal data with high risk, (c) systematic profiling with legal effects, (d) material amount of special category data.") - - if q["type"] == "multi_select": - prev_val = answers.get(q_key, []) - selected = st.multiselect( - q["text"], - options=q["options"], - default=prev_val, - key=q_key + "_widget", - ) - cleaned = enforce_none_exclusive(selected, prev_val, q["options"]) - if cleaned != selected: - answers[q_key] = cleaned - st.rerun() - answers[q_key] = cleaned - elif q["type"] == "single_select": - options_with_blank = ["— Select —"] + q["options"] - prev = answers.get(q_key, "— Select —") - default_idx = options_with_blank.index(prev) if prev in options_with_blank else 0 - selected = st.selectbox( - q["text"], - options=options_with_blank, - index=default_idx, - key=q_key + "_widget", - ) - answers[q_key] = selected - else: - st.caption("No qualification questions available for this regulation yet.") - - # Show GPAI questions inside EU AI Act expander - if reg_name == "EU AI Act (Regulation 2024/1689)" and has_gpai: - st.markdown("---") - st.markdown("**GPAI Provisions (Chapter V)**") - if gpai_name in QUALIFICATION_QUESTIONS: - gpai_questions = QUALIFICATION_QUESTIONS[gpai_name]["questions"] - for q in gpai_questions: - q_key = f"q_{gpai_name}_{q['id']}" - if q["type"] == "multi_select": - prev_val = answers.get(q_key, []) - selected = st.multiselect(q["text"], options=q["options"], default=prev_val, key=q_key + "_widget") - cleaned = enforce_none_exclusive(selected, prev_val, q["options"]) - if cleaned != selected: - answers[q_key] = cleaned - st.rerun() - answers[q_key] = cleaned - elif q["type"] == "single_select": - options_with_blank = ["— Select —"] + q["options"] - prev = answers.get(q_key, "— Select —") - default_idx = options_with_blank.index(prev) if prev in options_with_blank else 0 - selected = st.selectbox(q["text"], options=options_with_blank, index=default_idx, key=q_key + "_widget") - answers[q_key] = selected - - displayed += 1 - - st.session_state.qualification_answers = answers - - # Validate all questions answered - all_answered = True - d = st.session_state.data - if ai_specific: - for tag, reg_name, _, reason in ai_specific: - if reg_name == gpai_name: - continue - if reg_name in QUALIFICATION_QUESTIONS: - for q in QUALIFICATION_QUESTIONS[reg_name]["questions"]: - q_key = f"q_{reg_name}_{q['id']}" - - # Skip EU questions after full exemption - if reg_name == "EU AI Act (Regulation 2024/1689)" and q["id"] != "euaia_exception": - exception_key = f"q_{reg_name}_euaia_exception" - exception_val = answers.get(exception_key, []) - full_exemptions = EU_FULL_EXEMPTIONS - if any(ex in exception_val for ex in full_exemptions): - continue - - # Skip EU questions after prohibited practice - if reg_name == "EU AI Act (Regulation 2024/1689)" and q["id"] not in ("euaia_exception", "euaia_sme", "euaia_prohibited"): - prohibited_key = f"q_{reg_name}_euaia_prohibited" - prohibited_val = answers.get(prohibited_key, []) - if prohibited_val and "None of the above" not in prohibited_val: - continue - - # Skip Art 6(3) if Annex III = None - if q["id"] == "euaia_art6_3": - annex3_key = f"q_{reg_name}_euaia_annex3" - annex3_val = answers.get(annex3_key, []) - if not annex3_val or "None of the above" in annex3_val: - continue - - # Skip public services — now collected on ID Card - if q["id"] == "euaia_public_services": - continue - - # Skip use-as-intended if not small deployer - if q["id"] == "co_use_as_intended": - if not d.get("is_small_business", False): - continue - - val = answers.get(q_key) - if q["type"] == "multi_select" and not val: - all_answered = False - elif q["type"] == "single_select" and (not val or val == "— Select —"): - all_answered = False - # Check GPAI questions - if reg_name == "EU AI Act (Regulation 2024/1689)" and has_gpai and gpai_name in QUALIFICATION_QUESTIONS: - for q in QUALIFICATION_QUESTIONS[gpai_name]["questions"]: - q_key = f"q_{gpai_name}_{q['id']}" - val = answers.get(q_key) - if q["type"] == "multi_select" and not val: - all_answered = False - elif q["type"] == "single_select" and (not val or val == "— Select —"): - all_answered = False - - # Navigation - if not all_answered: - st.info("Please answer all qualification questions to continue.") - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - if st.button("Next →", use_container_width=True, disabled=(not all_answered)): - # Derive is_sme from EU AI Act qualification answer — only if not exempt - sme_key = "q_EU AI Act (Regulation 2024/1689)_euaia_sme" - exception_key = "q_EU AI Act (Regulation 2024/1689)_euaia_exception" - sme_answer = st.session_state.qualification_answers.get(sme_key, "") - exception_val = st.session_state.qualification_answers.get(exception_key, []) - full_exemptions = EU_FULL_EXEMPTIONS - is_exempt = any(ex in exception_val for ex in full_exemptions) - st.session_state.data["is_sme"] = "Yes" in sme_answer and not is_exempt - go_to(10) - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 10: Requirements Deep Dive -# ═══════════════════════════════════════════════ - -elif current == 10: - st.markdown('

Requirements Deep Dive

', unsafe_allow_html=True) - st.markdown('

Obligations, synergies, and cross-jurisdiction gap analysis for your AI system.

', unsafe_allow_html=True) - - detected = st.session_state.get("detected_regs", []) - answers = st.session_state.get("qualification_answers", {}) - d = st.session_state.data - roles = d.get("roles", []) - is_provider = any("Provider" in r for r in roles) - is_deployer = any("Deployer" in r for r in roles) - is_sme = d.get("is_sme", False) - company_size = d.get("company_size", "") - - ai_specific = [(tag, name) for tag, name, cat, _ in detected if cat == "ai"] - privacy_regs = [(tag, name) for tag, name, cat, _ in detected if cat == "other" and name in OBLIGATIONS and "key_obligations" in OBLIGATIONS.get(name, {})] - other_detected = [(tag, name) for tag, name, cat, _ in detected if cat == "other" and name in OTHER_REG_ONE_LINERS] - all_reg_names = [name for _, name, _, _ in detected] - - # ─── Helper: regulation link ─── - def reg_link(reg_name): - url = REGULATION_URLS.get(reg_name) - if url: - return f'📄 Official text' - return "" - - # ─── Helper: render obligations as single HTML block ─── - def render_obligations(obligations, label=None): - html = '
' - if label: - html += f'

{label}

' - for o in obligations: - html += f'

☐ {o}

' - html += '
' - st.markdown(html, unsafe_allow_html=True) - - # ─── Qualification helpers ─── - def get_eu_ai_act_categories(answers): - cats = [] - prefix = "q_EU AI Act (Regulation 2024/1689)_" - exceptions = answers.get(prefix + "euaia_exception", []) - full_exemptions = EU_FULL_EXEMPTIONS - if any(ex in exceptions for ex in full_exemptions): - return ["exempt"] - prohibited = answers.get(prefix + "euaia_prohibited", []) - if prohibited and "None of the above" not in prohibited: - cats.append("prohibited") - annex3 = answers.get(prefix + "euaia_annex3", []) - if annex3 and "None of the above" not in annex3: - art6_3 = answers.get(prefix + "euaia_art6_3", "— Select —") - if "Yes" not in art6_3: - cats.append("high_risk") - transparency = answers.get(prefix + "euaia_transparency", []) - if transparency and "None of the above" not in transparency: - cats.append("limited_risk") - if not cats: - cats.append("minimal_risk") - return cats - - def get_gpai_categories(answers): - prefix = "q_EU AI Act — GPAI Framework (Chapter V)_" - systemic = answers.get(prefix + "gpai_systemic", "— Select —") - open_source = answers.get(prefix + "gpai_open_source", "— Select —") - if "Yes" in systemic: - return ["gpai_systemic"] - elif "open-source" in open_source.lower(): - return ["gpai_open_source"] - else: - return ["gpai_standard"] - - def get_colorado_status(answers): - prefix = "q_Colorado AI Act (SB 24-205)_" - consequential = answers.get(prefix + "co_consequential", []) - exceptions = answers.get(prefix + "co_exception", []) - if not consequential or "None of the above — system does not make consequential decisions" in consequential: - return ["exempt"] - if exceptions and "None of the above" not in exceptions: - if any("approved/regulated by a federal agency" in e.lower() for e in exceptions): - return ["exempt"] - roles_out = [] - if is_provider: - roles_out.append("developer") - if is_deployer: - roles_out.append("deployer") - return roles_out if roles_out else ["deployer"] - - # ─── Compute counts for jump nav ─── - gpai_name = "EU AI Act — GPAI Framework (Chapter V)" - has_gpai_s10 = any(n == gpai_name for _, n in ai_specific) - ai_count = len([1 for _, n in ai_specific if n != gpai_name]) - privacy_count = len(privacy_regs) - other_count = len(other_detected) - - # Determine which AI regs are NOT exempt (for gap analysis AND synergies) - non_exempt_ai = [] - for tag, reg_name in ai_specific: - if reg_name == gpai_name: - continue - if reg_name == "EU AI Act (Regulation 2024/1689)": - eu_cats = get_eu_ai_act_categories(answers) - if "exempt" in eu_cats or "prohibited" in eu_cats: - continue - elif reg_name == "Colorado AI Act (SB 24-205)": - if "exempt" in get_colorado_status(answers): - continue - elif reg_name == "Illinois HB 3773 (AI in Employment)": - if "exempt" in classify_illinois(answers): - continue - elif reg_name == "California CCPA / ADMT Regulations": - if "exempt" in classify_california(answers, d): - continue - elif reg_name == "DIFC Regulation 10 (AI Processing)": - if "exempt" in classify_difc_reg10(answers): - continue - elif reg_name == "California SB 53 (Frontier AI Transparency)": - if "not_applicable" in classify_ca_sb53(answers): - continue - elif reg_name == "New York RAISE Act (Frontier AI Safety)": - if "not_applicable" in classify_ny_raise(answers): - continue - non_exempt_ai.append(reg_name) - - # Build list of all active (non-exempt) regulation names for synergy filtering - active_reg_names = list(non_exempt_ai) - # Add privacy/other regs (always active if detected) - for tag, name, cat, _ in detected: - if cat != "ai" and name not in active_reg_names: - active_reg_names.append(name) - # Add GPAI if EU AI Act is active - if "EU AI Act (Regulation 2024/1689)" in non_exempt_ai: - if gpai_name not in active_reg_names: - active_reg_names.append(gpai_name) - - # Topic-based synergy filtering: show synergy if 2+ listed regulations apply - applicable_overlaps = [] - for ov in OVERLAP_ANALYSIS: - matching_regs = [r for r in ov["regulations"] if r in active_reg_names] - if len(matching_regs) >= 2: - applicable_overlaps.append({**ov, "active_regulations": matching_regs}) - synergy_count = len(applicable_overlaps) - - ai_reg_names_for_gap = non_exempt_ai - gap_count = 0 - for src in ai_reg_names_for_gap: - if src in GAP_ANALYSIS: - for tgt in ai_reg_names_for_gap: - if tgt != src and tgt in GAP_ANALYSIS[src]: - gap_count += 1 - - # ─── Jump nav ─── - jump_links = f'⚙️ AI-Specific ({ai_count})' - if privacy_count: - jump_links += f' 🔒 Privacy ({privacy_count})' - if other_count: - jump_links += f' 📋 Other ({other_count})' - if synergy_count: - jump_links += f' 🔗 Synergies ({synergy_count})' - if gap_count: - jump_links += f' 📊 Gap Analysis ({gap_count})' - - st.markdown(f"""
-

On this page

- -
""", unsafe_allow_html=True) - - # ════════════════════════════════════════ - # SECTION A: AI-SPECIFIC OBLIGATIONS - # ════════════════════════════════════════ - - st.markdown(f'
⚙️ AI-Specific Regulations — Obligations {ai_count} regulations
', unsafe_allow_html=True) - - for tag, reg_name in ai_specific: - if reg_name == gpai_name: - continue - - display_label = f"{tag} — {reg_name}" - if reg_name == "EU AI Act (Regulation 2024/1689)" and has_gpai_s10: - display_label += " (incl. GPAI)" - - link_html = reg_link(reg_name) - with st.expander(display_label, expanded=True): - st.markdown(link_html, unsafe_allow_html=True) - - if reg_name not in OBLIGATIONS: - st.caption("Detailed obligations not yet available for this regulation.") - continue - - reg_oblig = OBLIGATIONS[reg_name] - - # ── EU AI Act ── - if reg_name == "EU AI Act (Regulation 2024/1689)": - prefix = "q_EU AI Act (Regulation 2024/1689)_" - cats = get_eu_ai_act_categories(answers) - - if "exempt" in cats: - st.markdown('EXEMPT', unsafe_allow_html=True) - st.write("Based on your answers, your AI system falls under an exception to the EU AI Act. No specific obligations apply under this regulation.") - continue - - if "prohibited" in cats: - st.markdown('PROHIBITED PRACTICE DETECTED', unsafe_allow_html=True) - render_obligations(reg_oblig["prohibited"]["obligations"]) - st.error("⚠️ If your AI system performs a prohibited practice under Art. 5, it must not be placed on the market, put into service, or used in the EU. Existing systems must be withdrawn or recalled. Fines: up to €35M or 7% of global annual turnover.") - if is_sme: - st.info("💡 **SME penalty cap applies.** As an SME, penalties are capped at whichever is **lower** between the percentage and the absolute amount (Art. 99(5)).") - st.info("💡 **Next step:** If you modify your system to remove the prohibited characteristics, re-run this assessment. Your system will likely fall into another EU AI Act category (high-risk, limited-risk, or minimal-risk) with different, manageable compliance obligations.") - continue - - if "high_risk" in cats: - os_option = "The AI system is released under a free and open-source licence with publicly available parameters, including weights" - exceptions_selected = answers.get(prefix + "euaia_exception", []) - if os_option in exceptions_selected: - st.warning("⚠️ **Open-source does not exempt your system.** Under Art. 2(12), the open-source exception does NOT apply to high-risk AI systems (Annex III), prohibited practices (Art. 5), or GPAI models with systemic risk. All obligations apply in full.") - st.markdown('HIGH-RISK AI SYSTEM', unsafe_allow_html=True) - if is_sme: - st.success("💡 **SME provisions apply.** As an SME/start-up, you benefit from: simplified technical documentation (Art. 11), reduced conformity assessment fees (Art. 62), priority access to regulatory sandboxes, and capped penalties (whichever is lower, not higher).") - if is_provider and "high_risk_provider" in reg_oblig: - render_obligations(reg_oblig["high_risk_provider"]["obligations"], reg_oblig["high_risk_provider"]["label"]) - if is_deployer and "high_risk_deployer" in reg_oblig: - render_obligations(reg_oblig["high_risk_deployer"]["obligations"], reg_oblig["high_risk_deployer"]["label"]) - if is_deployer and "high_risk_deployer_fria" in reg_oblig: - if requires_fria(answers, d): - fria = reg_oblig["high_risk_deployer_fria"] - render_obligations(fria["obligations"], fria["label"]) - - if "limited_risk" in cats: - label = "Transparency Obligations (Art. 50)" if "high_risk" in cats else reg_oblig["limited_risk"]["label"] - render_obligations(reg_oblig["limited_risk"]["obligations"], label) - - if "minimal_risk" in cats: - render_obligations(reg_oblig["minimal_risk"]["obligations"], reg_oblig["minimal_risk"]["label"]) - - # GPAI obligations - if has_gpai_s10 and gpai_name in OBLIGATIONS: - st.markdown("---") - st.markdown("**GPAI Provisions (Chapter V)**") - gpai_oblig = OBLIGATIONS[gpai_name] - gpai_cats = get_gpai_categories(answers) - for cat in gpai_cats: - if cat in gpai_oblig: - render_obligations(gpai_oblig[cat]["obligations"], gpai_oblig[cat]["label"]) - - # ── Colorado ── - elif reg_name == "Colorado AI Act (SB 24-205)": - cats = get_colorado_status(answers) - if "exempt" in cats: - st.markdown('NOT APPLICABLE', unsafe_allow_html=True) - st.write("Based on your answers, your system does not make consequential decisions or falls under a federal exemption.") - else: - is_small_deployer = is_deployer and d.get("is_small_business", False) - if is_small_deployer: - st.success("💡 **Small business deployer provisions may apply.** Deployers with fewer than 50 FTE are exempt from risk management programs, impact assessments, and public statements — **only if** you do not train the AI system with your own data and use it only as intended by the developer. You must still provide consumers with the developer's impact assessment and required notices.") - for cat in cats: - if cat in reg_oblig: - render_obligations(reg_oblig[cat]["obligations"], reg_oblig[cat]["label"]) - - # ── DIFC ── - elif reg_name == "DIFC Regulation 10 (AI Processing)": - st.info(f"ℹ️ **Deployer vs. Operator:** {DIFC_CONTROLLER_NOTE}") - if "deployer_operator" in reg_oblig: - render_obligations(reg_oblig["deployer_operator"]["obligations"], reg_oblig["deployer_operator"]["label"]) - difc_hr = answers.get("q_DIFC Regulation 10 (AI Processing)_difc_commercial_high_risk", "— Select —") - if "Yes" in difc_hr and "high_risk" in reg_oblig: - obl_set = reg_oblig["high_risk"] - render_obligations(obl_set["obligations"], obl_set["label"]) - if obl_set.get("note"): - st.caption(obl_set["note"]) - - # ── Texas ── - elif reg_name == "Texas TRAIGA (HB 149)": - tx_keys = classify_texas(answers, d) - for key in tx_keys: - if key in reg_oblig: - render_obligations(reg_oblig[key]["obligations"], reg_oblig[key]["label"]) - if d.get("is_public_sector", False) or "government_deployer" in tx_keys: - st.info("ℹ️ As a government entity, full disclosure obligations apply in addition to prohibited practices.") - else: - st.caption("Prohibited practices apply to all entities. Government disclosure obligations are not applicable to private-sector entities.") - - # ── Illinois ── - elif reg_name == "Illinois HB 3773 (AI in Employment)": - il_keys = classify_illinois(answers) - if "exempt" in il_keys: - st.markdown('NOT APPLICABLE', unsafe_allow_html=True) - st.write("Based on your answers, this AI system is not used for employment-related decisions in Illinois.") - else: - for key in il_keys: - if key in reg_oblig: - render_obligations(reg_oblig[key]["obligations"], reg_oblig[key]["label"]) - if reg_oblig[key].get("scope"): - st.caption(f"Scope: {reg_oblig[key]['scope']}") - if "employer_aivia" not in il_keys: - st.caption("The AI Video Interview Act (AIVIA) does not apply — your system does not analyse video interviews.") - - # ── California ── - elif reg_name == "California CCPA / ADMT Regulations": - ca_keys = classify_california(answers, d) - if "exempt" in ca_keys: - st.markdown('NOT APPLICABLE', unsafe_allow_html=True) - exemptions = reg_oblig.get("exemptions", "") - if d.get("is_public_sector", False) or d.get("org_type") == "Non-profit / NGO / academic institution": - st.write(f"**Exempt:** {exemptions}") - else: - st.write("Based on your answers, your organisation does not meet CCPA thresholds or does not use ADMT for significant decisions.") - else: - for key in ca_keys: - if key in reg_oblig: - render_obligations(reg_oblig[key]["obligations"], reg_oblig[key]["label"]) - # Show phased deadlines - phased = reg_oblig.get("phased_deadlines", {}) - if phased: - st.markdown("**Phased Compliance Deadlines:**") - for milestone, date in phased.items(): - label = milestone.replace("_", " ").title() - st.markdown(f"- **{label}:** {date}") - - # ── California SB 53 (Frontier AI) ── - elif reg_name == "California SB 53 (Frontier AI Transparency)": - sb53_keys = classify_ca_sb53(answers) - if "not_applicable" in sb53_keys: - st.markdown('NOT APPLICABLE', unsafe_allow_html=True) - st.write("Based on your answers, your model does not meet the frontier threshold (>10²⁶ FLOPs) or you do not meet the revenue threshold.") - else: - for key in sb53_keys: - if key in reg_oblig: - render_obligations(reg_oblig[key]["obligations"], reg_oblig[key]["label"]) - if reg_oblig.get("penalty"): - st.caption(f"Penalty: {reg_oblig['penalty']}") - - # ── New York RAISE Act (Frontier AI) ── - elif reg_name == "New York RAISE Act (Frontier AI Safety)": - raise_keys = classify_ny_raise(answers) - if "not_applicable" in raise_keys: - st.markdown('NOT APPLICABLE', unsafe_allow_html=True) - st.write("Based on your answers, your model does not meet frontier thresholds or your revenue is below USD 500M.") - else: - for key in raise_keys: - if key in reg_oblig: - render_obligations(reg_oblig[key]["obligations"], reg_oblig[key]["label"]) - if reg_oblig.get("penalty"): - st.caption(f"Penalty: {reg_oblig['penalty']}") - - # ── All other AI-specific (Utah, etc.) ── - else: - for key, value in reg_oblig.items(): - if key in ("deadline", "penalty", "scope_note", "threshold_note", "exemptions", "phased_deadlines", "enforcement_note"): - continue - if isinstance(value, dict) and "obligations" in value: - render_obligations(value["obligations"], value.get("label", key)) - - # ════════════════════════════════════════ - # SECTION B: PRIVACY OBLIGATIONS - # ════════════════════════════════════════ - - if privacy_regs: - st.markdown(f'
🔒 Privacy & Related — Key AI Obligations {privacy_count} regulations
', unsafe_allow_html=True) - - for tag, reg_name in privacy_regs: - link_html = reg_link(reg_name) - with st.expander(f"{tag} — {reg_name}", expanded=True): - st.markdown(link_html, unsafe_allow_html=True) - reg_oblig = OBLIGATIONS.get(reg_name, {}) - key_obligs = reg_oblig.get("key_obligations", []) - if key_obligs: - render_obligations(key_obligs) - if reg_oblig.get("ai_relevant_note"): - st.caption(reg_oblig['ai_relevant_note']) - - # ════════════════════════════════════════ - # SECTION C: OTHER APPLICABLE REGULATIONS - # ════════════════════════════════════════ - - if other_detected: - st.markdown(f'
📋 Other Applicable Regulations {other_count} regulations
', unsafe_allow_html=True) - - for tag, reg_name in other_detected: - link_html = reg_link(reg_name) - one_liner = OTHER_REG_ONE_LINERS.get(reg_name, "") - with st.expander(f"{tag} — {reg_name}", expanded=True): - st.markdown(link_html, unsafe_allow_html=True) - if one_liner: - st.markdown(f'

{one_liner}

', unsafe_allow_html=True) - - # ════════════════════════════════════════ - # SECTION D: COMPLIANCE SYNERGIES - # ════════════════════════════════════════ - - if applicable_overlaps: - st.markdown(f'
🔗 Compliance Synergies {synergy_count} topics identified
', unsafe_allow_html=True) - st.caption("Where obligations across regulations overlap. Comply once to satisfy multiple requirements.") - - for ov in applicable_overlaps: - active = ov.get("active_regulations", ov["regulations"]) - icon = ov.get("icon", "🔗") - reg_labels = ov.get("reg_labels", {}) - # Build per-regulation label list - regs_html = "" - for r in active: - short = r.replace("EU AI Act — GPAI Framework (Chapter V)", "EU AI Act (GPAI)") - label = reg_labels.get(r, "") - if label: - regs_html += f'

{short}: {label}

' - else: - regs_html += f'

• {short}

' - - st.markdown(f"""
-

{icon} {ov['title']}

-
{regs_html}
-

Recommendation: {ov['recommendation']}

-
""", unsafe_allow_html=True) - - # ════════════════════════════════════════ - # SECTION E: GAP ANALYSIS (non-exempt AI regs only) - # ════════════════════════════════════════ - - if gap_count > 0 and len(ai_reg_names_for_gap) >= 2: - st.markdown(f'
📊 Gap Analysis {gap_count} comparisons
', unsafe_allow_html=True) - st.caption("Estimated coverage when complying with one regulation, then expanding to another.") - - for src in ai_reg_names_for_gap: - if src not in GAP_ANALYSIS: - continue - for tgt in ai_reg_names_for_gap: - if tgt == src or tgt not in GAP_ANALYSIS[src]: - continue - gap = GAP_ANALYSIS[src][tgt] - cov = gap["coverage"] - rem = 100 - cov - covered_html = "".join(f'

+ {c}

' for c in gap["covered"]) - gaps_html = "".join(f'

− {g}

' for g in gap["gaps"]) - filled_w = max(cov, 15) - empty_w = max(rem, 5) - st.markdown(f"""
-

{src} → {tgt}

-
-
{cov}% covered
-
{rem}% gap
-
-

Already covered:

- {covered_html} -

Gaps to address:

- {gaps_html} -
""", unsafe_allow_html=True) - - # ════════════════════════════════════════ - # DISCLAIMER + NAVIGATION - # ════════════════════════════════════════ - - st.markdown(f'
⚠️ Disclaimer: {DISCLAIMER}
', unsafe_allow_html=True) - - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - if st.button("Next →", use_container_width=True): - go_to(11) - st.rerun() - - -# ═══════════════════════════════════════════════ -# SCREEN 11: Compliance Checklist -# Faithful to Deep Dive: obligations, synergies, gaps -# ═══════════════════════════════════════════════ - -elif current == 11: - st.markdown('

Compliance Checklist

', unsafe_allow_html=True) - st.markdown('

Consolidated requirements taking synergies into account. Export the full PDF for gap analysis and detailed breakdown.

', unsafe_allow_html=True) - - detected = st.session_state.get("detected_regs", []) - answers = st.session_state.get("qualification_answers", {}) - d = st.session_state.data - roles = d.get("roles", []) - is_provider = any("Provider" in r for r in roles) - is_deployer = any("Deployer" in r for r in roles) - system_name = d.get("name", "Your AI system") - all_reg_names = [name for _, name, _, _ in detected] - - # ── Collect all applicable obligations ── - all_obligations = collect_all_obligations(detected, answers, roles, d) - - # ── Compute non-exempt AI regs (for synergies and gap analysis) ── - gpai_name = "EU AI Act — GPAI Framework (Chapter V)" - ai_specific = [(tag, name) for tag, name, cat, _ in detected if cat == "ai"] - non_exempt_ai = [] - for tag, reg_name in ai_specific: - if reg_name == gpai_name: - continue - if reg_name == "EU AI Act (Regulation 2024/1689)": - cats, _ = classify_eu_ai_act(answers, is_provider, is_deployer) - if "exempt" in cats or "prohibited" in cats: - continue - elif reg_name == "Colorado AI Act (SB 24-205)": - co_keys = classify_colorado(answers, is_provider, is_deployer, d) - if "exempt" in co_keys: - continue - elif reg_name == "Illinois HB 3773 (AI in Employment)": - il_keys = classify_illinois(answers) - if "exempt" in il_keys: - continue - elif reg_name == "California CCPA / ADMT Regulations": - ca_keys = classify_california(answers, d) - if "exempt" in ca_keys: - continue - elif reg_name == "DIFC Regulation 10 (AI Processing)": - difc_keys = classify_difc_reg10(answers) - if "exempt" in difc_keys: - continue - elif reg_name == "California SB 53 (Frontier AI Transparency)": - if "not_applicable" in classify_ca_sb53(answers): - continue - elif reg_name == "New York RAISE Act (Frontier AI Safety)": - if "not_applicable" in classify_ny_raise(answers): - continue - non_exempt_ai.append(reg_name) - - # Build active (non-exempt) reg list for synergy filtering - active_reg_names = list(non_exempt_ai) - for tag, name, cat, _ in detected: - if cat != "ai" and name not in active_reg_names: - active_reg_names.append(name) - if "EU AI Act (Regulation 2024/1689)" in non_exempt_ai and gpai_name not in active_reg_names: - active_reg_names.append(gpai_name) - - # ── Compute applicable synergies (topic-based) ── - applicable_overlaps = [] - for ov in OVERLAP_ANALYSIS: - matching_regs = [r for r in ov["regulations"] if r in active_reg_names] - if len(matching_regs) >= 2: - applicable_overlaps.append({**ov, "active_regulations": matching_regs}) - - gap_items = [] - for src in non_exempt_ai: - if src in GAP_ANALYSIS: - for tgt in non_exempt_ai: - if tgt != src and tgt in GAP_ANALYSIS[src]: - gap_items.append((src, tgt, GAP_ANALYSIS[src][tgt])) - - # ── Count totals ── - total_reg_obligations = sum(len(o["obligations"]) for o in all_obligations) - gap_total = sum(len(g[2]["gaps"]) for g in gap_items) - - st.markdown(f"""
- {system_name} — {total_reg_obligations} obligations across {len(set(o['reg_name'] for o in all_obligations))} regulations - {f' · {len(applicable_overlaps)} synergies consolidating your compliance effort' if applicable_overlaps else ''} -
""", unsafe_allow_html=True) - - # ════════════════════════════════════════ - # SECTION A: CONSOLIDATED REQUIREMENTS - # Synergy-aware: show shared obligations once with all regs tagged - # ════════════════════════════════════════ - - if applicable_overlaps: - st.markdown(f'
✅ Consolidated Requirements {len(applicable_overlaps)} synergy topics
', unsafe_allow_html=True) - st.caption("Where multiple regulations require the same action, we list it once. Implement each item to satisfy all tagged regulations simultaneously.") - - for ov in applicable_overlaps: - active = ov.get("active_regulations", ov["regulations"]) - icon = ov.get("icon", "🔗") - reg_labels = ov.get("reg_labels", {}) - - # Regulation tags - tags_html = "" - for r in active: - short = r.replace("EU AI Act — GPAI Framework (Chapter V)", "EU AI Act (GPAI)") - label = reg_labels.get(r, "") - if label: - tags_html += f'

{short}: {label}

' - else: - tags_html += f'

• {short}

' - - # Checklist items from shared_elements - checklist_html = "" - all_short = ", ".join(r.replace("EU AI Act — GPAI Framework (Chapter V)", "EU AI Act (GPAI)") for r in active) - for elem in ov.get("shared_elements", []): - checklist_html += f'

☐ {elem} ({all_short})

' - - st.markdown(f"""
-

{icon} {ov['title']}

-
{tags_html}
-

Recommendation: {ov['recommendation']}

- {checklist_html} -
""", unsafe_allow_html=True) - - # ════════════════════════════════════════ - # SECTION B: REGULATION-SPECIFIC OBLIGATIONS - # (what remains after synergies) - # ════════════════════════════════════════ - - # Group by regulation - from collections import OrderedDict - reg_groups = OrderedDict() - for obl in all_obligations: - rn = obl["reg_name"] - if rn not in reg_groups: - reg_groups[rn] = [] - reg_groups[rn].append(obl) - - ai_reg_names = set(o["reg_name"] for o in all_obligations if o["is_ai"]) - privacy_reg_names = set(o["reg_name"] for o in all_obligations if not o["is_ai"]) - - # AI-specific regulations - ai_regs_to_show = [rn for rn in reg_groups if rn in ai_reg_names] - if ai_regs_to_show: - st.markdown(f'
⚙️ AI Obligations by Regulation {len(ai_regs_to_show)} regulations
', unsafe_allow_html=True) - - for reg_name in ai_regs_to_show: - url = REGULATION_URLS.get(reg_name, "") - link_html = f' 📄 Official text' if url else "" - with st.expander(reg_name, expanded=True): - if url: - st.markdown(link_html, unsafe_allow_html=True) - for obl_group in reg_groups[reg_name]: - items_html = f'

{obl_group["category"]}

' - for o in obl_group["obligations"]: - items_html += f'

☐ {o}

' - if obl_group.get("deadline"): - items_html += f'

⏰ Deadline: {obl_group["deadline"]}

' - st.markdown(f'
{items_html}
', unsafe_allow_html=True) - - # Privacy regulations - priv_regs_to_show = [rn for rn in reg_groups if rn in privacy_reg_names] - if priv_regs_to_show: - st.markdown(f'
🔒 Data Protection & Privacy {len(priv_regs_to_show)} regulations
', unsafe_allow_html=True) - - for reg_name in priv_regs_to_show: - url = REGULATION_URLS.get(reg_name, "") - link_html = f' 📄 Official text' if url else "" - with st.expander(reg_name, expanded=True): - if url: - st.markdown(link_html, unsafe_allow_html=True) - for obl_group in reg_groups[reg_name]: - items_html = "" - for o in obl_group["obligations"]: - items_html += f'

☐ {o}

' - st.markdown(f'
{items_html}
', unsafe_allow_html=True) - - # ════════════════════════════════════════ - # SECTION C: OTHER APPLICABLE REGULATIONS - # ════════════════════════════════════════ - - other_detected = [(tag, name) for tag, name, cat, _ in detected if cat == "other" and name in OTHER_REG_ONE_LINERS and name not in reg_groups] - if other_detected: - st.markdown(f'
📋 Other Applicable Regulations {len(other_detected)} regulations
', unsafe_allow_html=True) - for tag, reg_name in other_detected: - one_liner = OTHER_REG_ONE_LINERS.get(reg_name, "") - url = REGULATION_URLS.get(reg_name, "") - link_html = f' 📄' if url else "" - st.markdown(f'

{tag} — {reg_name}{link_html}
{one_liner}

', unsafe_allow_html=True) - - # ── What's Next + CTA ── - st.markdown("""
-

What's next?

-

This checklist is your starting point. The next steps depend on your system's lifecycle stage, risk level, and organisational context: prioritising obligations, building internal processes, preparing documentation, and engaging with authorities where required.

-

Export the full PDF report for detailed synergies, gap analysis, and the complete compliance breakdown.

-

Need help implementing these requirements? Let's talk

-
""", unsafe_allow_html=True) - - # ── Disclaimer + Navigation ── - st.markdown(f'
⚠️ Disclaimer: {DISCLAIMER}
', unsafe_allow_html=True) - - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("← Back", use_container_width=True): - go_back() - st.rerun() - with col3: - try: - pdf_bytes = generate_full_pdf(d, detected, answers, all_obligations, applicable_overlaps, DISCLAIMER, gap_items=gap_items) - safe_name = system_name.replace(" ", "_")[:30] - st.download_button( - "📥 Export Full PDF Report", - data=pdf_bytes, - file_name=f"RegMap_{safe_name}_report.pdf", - mime="application/pdf", - use_container_width=True, - ) - except ImportError: - if st.button("Export PDF", use_container_width=True): - st.toast("PDF generation requires reportlab. Add it to requirements.txt.", icon="⚠️") - except Exception as e: - if st.button("Export PDF", use_container_width=True): - st.error(f"PDF generation failed: {e}")