RegMap / app.py
inesbedar's picture
Upload app.py
1bedaea verified
# ─────────────────────────────────────────────
# 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("""
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
html, body, [class*="css"] { font-family: 'DM Sans', sans-serif; }
.stApp { background-color: #F7F8FA; }
/* ── Dark text override for HF Spaces dark mode ──
Excludes .rmh (RegMap Header) elements */
.stApp p:not(.rmh-el), .stApp span:not(.rmh-el), .stApp label,
.stApp h1, .stApp h2, .stApp h3, .stApp h4,
.stApp li, .stApp strong:not(.rmh-el), .stApp em,
[data-testid="stMarkdownContainer"] p:not(.rmh-el),
[data-testid="stMarkdownContainer"] h4,
[data-testid="stMarkdownContainer"] strong:not(.rmh-el),
[data-testid="stMarkdownContainer"] li {
color: #1E293B !important;
}
/* Override dark text for banner elements */
.stApp .reg-banner li,
.stApp .reg-banner-list li,
.stApp .reg-banner p,
.stApp .reg-banner span.reg-reason,
.stApp .reg-banner span.reg-tag,
[data-testid="stMarkdownContainer"] .reg-banner li,
[data-testid="stMarkdownContainer"] .reg-banner-list li {
color: #E2E8F0 !important;
}
.stApp .reg-banner span.reg-reason,
[data-testid="stMarkdownContainer"] .reg-banner span.reg-reason {
color: #94A3B8 !important;
-webkit-text-fill-color: #94A3B8 !important;
}
.stApp .reg-banner span.reg-tag,
[data-testid="stMarkdownContainer"] .reg-banner span.reg-tag {
color: #5EEAD4 !important;
-webkit-text-fill-color: #5EEAD4 !important;
}
.stApp .reg-banner p.reg-banner-disclaimer,
[data-testid="stMarkdownContainer"] .reg-banner p.reg-banner-disclaimer {
color: #94A3B8 !important;
-webkit-text-fill-color: #94A3B8 !important;
}
/* ── Header ── */
.rmh {
background: linear-gradient(135deg, #0F2B46 0%, #143A5C 60%, #0F2B46 100%);
padding: 2rem 2.2rem 1.6rem 2.2rem;
border-radius: 14px;
margin-bottom: 1.8rem;
position: relative;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.04);
}
.rmh::before {
content: '';
position: absolute;
top: -50%; right: -20%;
width: 60%; height: 200%;
background: radial-gradient(ellipse, rgba(13,148,136,0.15) 0%, transparent 70%);
pointer-events: none;
}
.rmh::after {
content: '';
position: absolute;
bottom: 0; left: 0;
width: 100%; height: 1px;
background: linear-gradient(90deg, transparent, rgba(45,212,191,0.3), transparent);
}
.rmh-logo {
font-family: 'Space Grotesk', sans-serif !important;
font-size: 1.7rem !important; font-weight: 700 !important;
letter-spacing: -0.02em !important;
color: #FFFFFF !important;
margin: 0 !important; position: relative; z-index: 1;
line-height: 1.2 !important;
}
.rmh-accent { color: #2DD4BF !important; }
.rmh-tagline {
font-family: 'DM Sans', sans-serif !important;
color: #8BA3BB !important;
font-size: 0.82rem !important;
font-weight: 400 !important;
letter-spacing: 0.03em !important;
margin: 0.35rem 0 0 0 !important;
position: relative; z-index: 1;
}
.rmh-badges {
display: flex; gap: 0.45rem;
margin-top: 0.9rem;
position: relative; z-index: 1;
}
.rmh-badge {
background: rgba(45,212,191,0.08) !important;
border: 1px solid rgba(45,212,191,0.2) !important;
border-radius: 5px !important;
padding: 0.18rem 0.55rem !important;
font-family: 'Space Grotesk', sans-serif !important;
font-size: 0.68rem !important;
font-weight: 600 !important;
color: #5EEAD4 !important;
letter-spacing: 0.06em !important;
text-transform: uppercase !important;
}
/* ── Progress ── */
.progress-label {
color: #64748B !important; font-size: 0.78rem;
font-weight: 500; margin-bottom: 0.2rem;
}
/* ── Screen title ── */
.screen-title {
font-family: 'Space Grotesk', sans-serif;
color: #0F2B46 !important; font-size: 1.15rem;
font-weight: 700; letter-spacing: -0.01em;
margin-bottom: 0.2rem;
}
.screen-subtitle {
color: #64748B !important; font-size: 0.82rem;
font-weight: 400; margin-bottom: 1.5rem;
line-height: 1.4;
}
/* ── Buttons ── */
.stButton > button {
font-family: 'DM Sans', sans-serif;
font-weight: 600; font-size: 0.85rem;
border-radius: 8px; padding: 0.45rem 1.5rem;
color: #FFFFFF !important;
}
/* ── Summary cards ── */
.summary-section {
background: #F8FAFB;
border: 1px solid #E2E8F0;
border-left: 3px solid #0D9488;
border-radius: 8px;
padding: 1rem 1.3rem;
margin-bottom: 0.8rem;
}
.summary-section h4 {
color: #0F2B46 !important;
font-family: 'Space Grotesk', sans-serif;
font-size: 0.88rem; font-weight: 700;
margin: 0 0 0.5rem 0;
}
.summary-section p {
color: #334155 !important; font-size: 0.85rem;
margin: 0.15rem 0; line-height: 1.5;
}
/* ── Widget spacing ── */
.stSelectbox, .stMultiSelect, .stTextInput, .stTextArea { margin-bottom: 0.6rem; }
/* ── Widget labels (belt-and-suspenders) ── */
.stSelectbox label, .stMultiSelect label, .stTextInput label, .stTextArea label,
.stRadio label, .stCheckbox label, .stSlider label {
color: #1E293B !important;
}
.stSelectbox label p, .stMultiSelect label p, .stTextInput label p, .stTextArea label p,
.stRadio label p, .stCheckbox label p, .stSlider label p {
color: #1E293B !important;
}
.stRadio div[role="radiogroup"] label p,
.stRadio div[role="radiogroup"] label span,
.stRadio div[role="radiogroup"] label,
[data-testid="stRadio"] label p,
[data-testid="stRadio"] label span,
[data-testid="stRadio"] label,
[data-testid="stWidgetLabel"] p,
[data-testid="stWidgetLabel"] span {
color: #1E293B !important;
}
.stMarkdown h4, .stMarkdown p:not(.rmh-el), .stMarkdown strong:not(.rmh-el), .stMarkdown em,
.stMarkdown li, .stMarkdown span:not(.rmh-el),
[data-testid="stMarkdownContainer"] span:not(.rmh-el) {
color: #1E293B !important;
}
.stAlert p, .stAlert span,
[data-testid="stAlert"] p,
[data-testid="stAlert"] span {
color: #1E293B !important;
}
[data-testid="stCaptionContainer"] p,
[data-testid="stCaptionContainer"] span {
color: #64748B !important;
}
/* ── Beta disclaimer ── */
.beta-disclaimer {
background: #FEF3C7;
border: 1px solid #F59E0B;
border-radius: 8px;
padding: 0.8rem 1rem;
margin-bottom: 1rem;
color: #92400E !important;
font-size: 0.82rem;
line-height: 1.5;
}
.beta-disclaimer strong { color: #92400E !important; }
.intro-box {
background: linear-gradient(135deg, #0F2B46 0%, #143A5C 100%);
border: 1px solid rgba(45,212,191,0.2);
border-radius: 10px;
padding: 1rem 1.3rem;
margin-bottom: 1rem;
color: #CBD5E1 !important;
font-size: 0.85rem;
line-height: 1.6;
}
.intro-box strong { color: #5EEAD4 !important; }
.intro-box .sys-name,
.intro-box span[class="sys-name"],
.intro-box span.sys-name,
div.intro-box span.sys-name {
color: #FFFFFF !important;
font-weight: 700 !important;
font-size: 1.1rem !important;
display: inline !important;
}
/* ── Regulation banner ── */
.reg-banner {
background: linear-gradient(135deg, #0F2B46 0%, #143A5C 100%);
border: 1px solid rgba(45,212,191,0.2);
border-radius: 12px;
padding: 1.5rem 1.8rem;
margin: 1.5rem 0 1rem 0;
}
.reg-banner-title {
font-family: 'Space Grotesk', sans-serif !important;
font-size: 1rem !important;
font-weight: 700 !important;
color: #FFFFFF !important;
margin: 0 0 0.8rem 0 !important;
}
.reg-banner-list {
list-style: none !important;
padding: 0 !important;
margin: 0 0 1rem 0 !important;
}
.reg-banner-list li {
color: #E2E8F0 !important;
font-size: 0.85rem !important;
padding: 0.35rem 0 !important;
border-bottom: 1px solid rgba(255,255,255,0.06) !important;
}
.reg-banner-list li:last-child { border-bottom: none !important; }
.reg-reason {
color: #94A3B8 !important;
font-size: 0.75rem !important;
font-style: italic !important;
margin-left: 3.2rem !important;
display: inline-block !important;
opacity: 1 !important;
visibility: visible !important;
-webkit-text-fill-color: #94A3B8 !important;
}
.reg-banner .reg-reason,
.reg-banner-list .reg-reason,
.reg-banner-list li .reg-reason {
color: #94A3B8 !important;
-webkit-text-fill-color: #94A3B8 !important;
}
.reg-tag {
display: inline-block !important;
background: rgba(45,212,191,0.15) !important;
color: #5EEAD4 !important;
font-size: 0.65rem !important;
font-weight: 600 !important;
padding: 0.1rem 0.4rem !important;
border-radius: 4px !important;
margin-right: 0.5rem !important;
vertical-align: middle !important;
letter-spacing: 0.03em !important;
}
.reg-banner-cta {
color: #E2E8F0 !important;
font-size: 0.9rem !important;
font-weight: 700 !important;
margin: 0.5rem 0 0 0 !important;
}
.reg-banner-disclaimer {
color: #94A3B8 !important;
font-size: 0.72rem !important;
font-style: italic !important;
margin: 1rem 0 0 0 !important;
padding-top: 0.6rem !important;
border-top: 1px solid rgba(255,255,255,0.08) !important;
}
.reg-section-label {
color: #5EEAD4 !important;
font-size: 0.7rem !important;
font-weight: 700 !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
margin: 0.8rem 0 0.3rem 0 !important;
padding-top: 0.6rem !important;
border-top: 1px solid rgba(255,255,255,0.08) !important;
}
.reg-section-label-first {
color: #5EEAD4 !important;
font-size: 0.7rem !important;
font-weight: 700 !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
margin: 0 0 0.3rem 0 !important;
padding-top: 0 !important;
border-top: none !important;
}
/* ── Phase 2: Obligation cards ── */
.p2-section-header {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.8rem 1rem;
border-radius: 8px;
margin: 2rem 0 1rem 0;
font-size: 1rem;
font-weight: 700;
background: #F8FAFC;
border-left: 4px solid #0D9488;
color: #0F2B46 !important;
}
.p2-section-count {
font-size: 0.75rem;
font-weight: 500;
color: #64748B;
margin-left: auto;
}
.p2-jump-nav {
background: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 10px;
padding: 1rem 1.3rem;
margin-bottom: 1.5rem;
}
.p2-jump-nav-title {
font-size: 0.75rem;
font-weight: 600;
color: #64748B;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.6rem;
}
.p2-jump-nav-links {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.p2-jump-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
background: rgba(13,148,136,0.08);
color: #0D9488;
border: 1px solid rgba(13,148,136,0.2);
}
.p2-reg-card {
background: #F8FAFB;
border: 1px solid #E2E8F0;
border-left: 3px solid #0D9488;
border-radius: 8px;
padding: 1rem 1.3rem;
margin-bottom: 0.7rem;
}
.p2-reg-card h4 {
color: #0F2B46 !important;
font-size: 0.88rem; font-weight: 700;
margin: 0 0 0.4rem 0;
}
.p2-reg-card p, .p2-reg-card li {
color: #334155 !important; font-size: 0.82rem;
margin: 0.15rem 0; line-height: 1.55;
}
.p2-reg-card .p2-status {
display: inline-block;
padding: 0.15rem 0.6rem;
border-radius: 10px;
font-size: 0.72rem;
font-weight: 600;
margin-bottom: 0.4rem;
}
.p2-status-applies { background: #DCFCE7; color: #166534 !important; }
.p2-status-exempt { background: #FEF3C7; color: #92400E !important; }
.p2-status-prohibited { background: #FEE2E2; color: #991B1B !important; }
.p2-synergy-card {
background: rgba(16,185,129,0.04);
border-left: 3px solid #10B981;
border-radius: 0 8px 8px 0;
padding: 0.8rem 1rem;
margin-bottom: 0.6rem;
}
.p2-synergy-card h4 {
color: #0F2B46 !important;
font-size: 0.9rem; font-weight: 600;
margin: 0 0 0.2rem 0;
}
.p2-synergy-card .regs {
font-size: 0.75rem;
color: #059669 !important;
margin: 0 0 0.4rem 0;
}
.p2-synergy-card p {
color: #475569 !important;
font-size: 0.82rem;
line-height: 1.55;
}
.p2-gap-card {
background: #FAFAFA;
border: 1px solid #E2E8F0;
border-radius: 8px;
padding: 0.8rem 1rem;
margin-bottom: 0.6rem;
}
.p2-gap-card h4 {
color: #0F2B46 !important;
font-size: 0.88rem; font-weight: 600;
margin: 0 0 0.5rem 0;
}
.p2-gap-bar {
display: flex;
height: 24px;
border-radius: 6px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.p2-gap-bar-filled {
background: #0D9488;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
font-weight: 600;
color: white;
}
.p2-gap-bar-empty {
background: rgba(220,38,38,0.08);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
font-weight: 600;
color: #DC2626;
}
.p2-gap-covered { font-size: 0.78rem; color: #059669 !important; margin: 0.15rem 0; line-height: 1.5; }
.p2-gap-missing { font-size: 0.78rem; color: #DC2626 !important; margin: 0.15rem 0; line-height: 1.5; }
.p2-gap-label { font-size: 0.78rem; font-weight: 600; color: #64748B !important; margin: 0.4rem 0 0.2rem 0; }
.p2-disclaimer {
background: #FEF3C7;
border: 1px solid #F59E0B;
border-radius: 8px;
padding: 0.8rem 1rem;
margin: 1.5rem 0 1rem 0;
color: #92400E !important;
font-size: 0.78rem;
line-height: 1.5;
}
.p2-disclaimer strong { color: #92400E !important; }
/* ── Progress Tracker ── */
.tracker {
background: rgba(255,255,255,0.04);
border-radius: 12px;
padding: 20px 32px 16px 32px;
margin: 0.3rem 0 1.5rem 0;
}
.tracker-steps {
display: flex;
align-items: flex-start;
position: relative;
}
.tracker-line {
position: absolute;
top: 16px;
left: 16px;
right: 16px;
height: 3px;
background: #1E293B;
z-index: 0;
}
.tracker-line-fill {
height: 3px;
background: #0D9488;
border-radius: 1px;
transition: width 0.4s;
}
.tracker-step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
}
.tracker-dot {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
margin-bottom: 8px;
flex-shrink: 0;
}
.tracker-dot.future {
background: #1E293B;
border: 2px solid #334155;
color: #475569;
}
.tracker-dot.current {
background: #2DD4BF;
border: 2px solid #2DD4BF;
color: #0F2B46;
box-shadow: 0 0 0 4px rgba(45,212,191,0.2);
}
.tracker-dot.done {
background: #0D9488;
border: 2px solid #0D9488;
color: white;
}
.tracker-label {
font-size: 13px;
font-weight: 600;
text-align: center;
line-height: 1.3;
color: #475569;
}
.tracker-label.done { color: #0D9488; }
.tracker-label.current { color: #2DD4BF; }
.tracker-sub {
font-size: 11px;
text-align: center;
color: #334155;
margin-top: 2px;
line-height: 1.2;
}
.tracker-sub.done { color: rgba(13,148,136,0.5); }
.tracker-sub.current { color: rgba(45,212,191,0.5); }
/* ── Hide branding ── */
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
header {visibility: hidden;}
/* ── What's Next CTA box ── */
div.whats-next-box,
.stMarkdown div.whats-next-box {
background: linear-gradient(135deg,#0F2B46 0%,#143A5C 100%) !important;
border: 1px solid rgba(45,212,191,0.2) !important;
border-radius: 10px !important;
padding: 1.2rem 1.5rem !important;
margin: 1.5rem 0 1rem 0 !important;
}
div.whats-next-box p.wn-title,
.stMarkdown div.whats-next-box p.wn-title {
color: #2DD4BF !important;
font-weight: 700 !important;
font-size: 1rem !important;
margin: 0 0 0.5rem 0 !important;
}
div.whats-next-box p.wn-body,
.stMarkdown div.whats-next-box p.wn-body {
color: #E2E8F0 !important;
font-size: 0.85rem !important;
line-height: 1.6 !important;
margin: 0 0 0.8rem 0 !important;
}
div.whats-next-box a.wn-link,
.stMarkdown div.whats-next-box a.wn-link {
color: #2DD4BF !important;
font-weight: 600 !important;
text-decoration: none !important;
}
</style>
""", 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", "Texas", "Utah",
]
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", "Hybrid"]
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"]
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'''<div class="tracker-step">
<div class="tracker-dot {state}">{s["num"]}</div>
<div class="tracker-label {state}">{s["label"]}</div>
<div class="tracker-sub {state}">{s["sub"]}</div>
</div>'''
tracker = f'''<div class="tracker">
<div class="tracker-steps">
<div class="tracker-line"><div class="tracker-line-fill" style="width:{fill_pct}%"></div></div>
{dots_html}
</div>
</div>'''
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"<b>System:</b> {clean(system_name)} AI System", s_meta))
story.append(Paragraph(f"<b>Generated:</b> {gen_date}", s_meta))
story.append(Paragraph(f"<b>Author:</b> 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 Capabilities", ", ".join(data.get("capabilities", ["—"]))),
("Data Types", ", ".join(data.get("data_types", ["—"]))),
]
for label, value in fields:
story.append(Paragraph(f"<b>{label}:</b> {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' <a href="{url}" color="#0D9488">[Official text]</a>' if url else ""
story.append(Paragraph(f"<b>[{tag}] {clean(name)}</b>{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' <a href="{url}" color="#0D9488">[Official text]</a>' if url else ""
story.append(Paragraph(f"<b>[{tag}] {clean(name)}</b>{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"<b>Disclaimer:</b> {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" <font color='#0D9488' size='7'><a href=\"{url}\" color=\"#0D9488\">[Official text]</a></font>" if url else ""
story.append(Paragraph(f"{clean(reg_name)}{url_text}", s_h2))
for obl_group in obl_groups:
story.append(Paragraph(f"<b>{clean(obl_group['category'])}</b>", 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"<b>{clean(reg_name)}</b>", 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" <font color='#0D9488' size='7'><a href=\"{url}\" color=\"#0D9488\">[Official text]</a></font>" if url else ""
story.append(Paragraph(f"<b>{clean(reg_name)}</b>{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"<b>{clean(ov['title'])}</b>", 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"<font color='#475569'>- <b>{clean(short)}</b>: {clean(label)}</font>", s_item))
else:
story.append(Paragraph(f"<font color='#475569'>- {clean(short)}</font>", s_item))
story.append(Paragraph(f"<b>Recommendation:</b> {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)} <font color='#94A3B8' size='7'>({clean(all_short)})</font>", 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"<b>{clean(src)} -> {clean(tgt)}</b> <font color='#64748B'>({cov}% already covered)</font>", s_overlap_title))
story.append(Paragraph("<b>Already covered:</b>", s_body))
for c in gap.get("covered", []):
story.append(Paragraph(f"+ {clean(c)}", s_item))
story.append(Paragraph("<b>Gaps to address:</b>", 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"<b>Disclaimer:</b> {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 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,
})
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("""
<div class="rmh">
<p class="rmh-logo rmh-el">Reg<span class="rmh-accent rmh-el">Map</span></p>
<p class="rmh-tagline rmh-el">Navigate AI regulation across jurisdictions</p>
<div class="rmh-badges">
<span class="rmh-badge rmh-el">&#127466;&#127482; EU</span>
<span class="rmh-badge rmh-el">&#127482;&#127480; US</span>
<span class="rmh-badge rmh-el">&#127462;&#127466; UAE</span>
</div>
</div>
""", 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('<div class="intro-box">From AI system profile to compliance checklist in minutes — RegMap identifies which regulations could apply to your AI system, synergies, and gaps across jurisdictions.</div>', 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 based on a structured Regulatory Knowledge Base built from official legal texts. The tool cross-references your AI system profile (jurisdiction, sector, purpose, data types, capabilities, role) against regulatory scope provisions to detect applicable regulations.
Qualification questions refine the analysis by determining risk level, exemptions, and specific categories. Coverage percentages in gap analysis are estimated based on requirement-level overlap between regulations.
Where multiple regulations require the same compliance activity (e.g. impact assessments, transparency notices, human oversight), RegMap identifies synergies and consolidates them into a single actionable requirement — so you know what to do, not just what each law says.
This tool does not use AI to generate its analysis — all logic is rule-based and deterministic.
""")
st.markdown('<div class="beta-disclaimer"><strong>Beta version</strong> — This Space is public by default. We recommend not entering directly identifying data, especially in free text fields.</div>', unsafe_allow_html=True)
render_progress_tracker(current)
# ═══════════════════════════════════════════════
# SCREEN 1: AI System Identity
# ═══════════════════════════════════════════════
if current == 1:
st.markdown('<p class="screen-title">AI System ID Card</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Basic information about your AI system</p>', 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('<p class="screen-title">Application Domain</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Industry sectors where your AI system is used</p>', 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('<p class="screen-title">Company Location</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Legal establishment of your organisation</p>', 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('<p class="screen-title">Geographic Scope</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Countries where your AI system has a presence</p>', 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('<p class="screen-title">Role in Value Chain</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Your organisation\'s position in the AI value chain</p>', 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('<p class="screen-title">Technology Profile</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Technical characteristics of your AI system</p>', unsafe_allow_html=True)
# 6.1 AI type
prev_type = st.session_state.data.get("ai_type", None)
type_idx = AI_TYPES.index(prev_type) if prev_type in AI_TYPES else None
ai_type = st.selectbox("6.1 — AI system type", AI_TYPES, index=type_idx, placeholder="Choose an option", 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",
]
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",
)
# 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 = ai_type is not None 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('<p class="screen-title">Data Profile</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Data processed and training sources</p>', 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],
)
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('<p class="screen-title">AI System ID Card — Summary</p>', unsafe_allow_html=True)
st.markdown(f'<p class="screen-subtitle">Review your inputs for <strong>{d.get("name", "")} AI System</strong></p>', unsafe_allow_html=True)
# Identity
lifecycle_str = d.get("lifecycle", "")
lifecycle_html = f'<p><strong>Lifecycle:</strong> {lifecycle_str}</p>' if lifecycle_str else ""
st.markdown(f'<div class="summary-section"><h4>AI System Identity</h4><p><strong>Name:</strong> {d.get("name", "—")}</p><p><strong>Description:</strong> {d.get("description", "—") or "—"}</p>{lifecycle_html}</div>', 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'<div class="summary-section"><h4>Application Domain</h4><p><strong>Sector(s):</strong> {sectors_display}</p></div>', unsafe_allow_html=True)
# Geography
geo_lines = f'<p><strong>Company base:</strong> {d.get("company_base", "—")}</p>'
geo_lines += f'<p><strong>Organisation type:</strong> {d.get("org_type", "—")}</p>'
ps = d.get("provides_public_services", [])
if ps and "None of the above" not in ps:
geo_lines += f'<p><strong>Public services provided:</strong> {", ".join(ps)}</p>'
geo_lines += f'<p><strong>Organisation size:</strong> {d.get("company_size", "—")}</p>'
op_countries = d.get("operating_countries", [])
if op_countries:
geo_lines += f'<p><strong>Countries:</strong> {", ".join(op_countries)}</p>'
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'<p><strong>US states:</strong> {", ".join(d["us_states"])}</p>'
if regulated:
geo_lines += f'<p><strong>States with AI regulation:</strong> {", ".join(regulated)}</p>'
if non_regulated:
geo_lines += f'<p><strong>States without specific AI regulation:</strong> {", ".join(non_regulated)}</p>'
if d.get("eu_countries"):
geo_lines += f'<p><strong>EU/EEA countries detected:</strong> {", ".join(d["eu_countries"])}</p>'
if d.get("uae_emirates"):
geo_lines += f'<p><strong>UAE emirates:</strong> {", ".join(d["uae_emirates"])}</p>'
if d.get("uae_free_zones"):
geo_lines += f'<p><strong>UAE free zones:</strong> {", ".join(d["uae_free_zones"])}</p>'
st.markdown(f'<div class="summary-section"><h4>Geographic Scope</h4>{geo_lines}</div>', unsafe_allow_html=True)
# Roles
roles_display = ", ".join(d.get("roles", ["—"]))
st.markdown(f'<div class="summary-section"><h4>Role in Value Chain</h4><p>{roles_display}</p></div>', 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")
tech_lines = f'<p><strong>AI type:</strong> {d.get("ai_type", "—")}</p>'
tech_lines += f'<p><strong>Model:</strong> {model_type_display}</p>'
tech_lines += f'<p><strong>Capabilities:</strong> {caps_display}</p>'
if d.get("decision_making"):
tech_lines += f'<p><strong>Human involvement in decisions:</strong> {involvement}</p>'
if d.get("synthetic_content"):
tech_lines += f'<p><strong>Synthetic content types:</strong> {synthetic}</p>'
st.markdown(f'<div class="summary-section"><h4>Technology Profile</h4>{tech_lines}</div>', unsafe_allow_html=True)
# Data
data_types_display = ", ".join(d.get("data_types", ["—"]))
sources_display = ", ".join(d.get("training_sources", ["—"]))
st.markdown(f'<div class="summary-section"><h4>Data Profile</h4><p><strong>Data types:</strong> {data_types_display}</p><p><strong>Training sources:</strong> {sources_display}</p></div>', 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
if has_personal_data and has_us:
detected_regs.append(("US", "State Data Protection Laws (CCPA, CTDPA, etc.)", "other", "Personal data processed in the US"))
# 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 "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"))
# ── 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 += '<p class="reg-section-label-first rmh-el">AI-Specific Regulations</p>'
banner_content += '<ul class="reg-banner-list">'
for tag, name, reason in ai_regs:
flag = FLAG_MAP.get(tag, "")
url = REGULATION_URLS.get(name, "")
link = f' <a href="{url}" target="_blank" style="color:#5EEAD4;font-size:0.7rem;text-decoration:none;">Official text →</a>' if url else ""
banner_content += f'<li>{flag} <span class="reg-tag rmh-el">{tag}</span> {name}{link}<br><span class="reg-reason rmh-el">{reason}</span></li>'
banner_content += '</ul>'
if other_regs:
label_class = "reg-section-label" if ai_regs else "reg-section-label-first"
banner_content += f'<p class="{label_class} rmh-el">Related Regulations</p>'
banner_content += '<ul class="reg-banner-list">'
for tag, name, reason in other_regs:
flag = FLAG_MAP.get(tag, "")
url = REGULATION_URLS.get(name, "")
link = f' <a href="{url}" target="_blank" style="color:#5EEAD4;font-size:0.7rem;text-decoration:none;">Official text →</a>' if url else ""
banner_content += f'<li>{flag} <span class="reg-tag rmh-el">{tag}</span> {name}{link}<br><span class="reg-reason rmh-el">{reason}</span></li>'
banner_content += '</ul>'
banner_html = f"""
<div class="reg-banner">
<p class="reg-banner-title rmh-el">&#9878;&#65039; Based on your AI System ID Card, these regulations could apply:</p>
{banner_content}
<p class="reg-banner-disclaimer rmh-el">This list is not exhaustive. Other regulations may apply. Always consult qualified legal counsel.</p>
</div>
"""
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("""<div style="background:rgba(13,148,136,0.08);border:1px solid rgba(13,148,136,0.25);border-radius:8px;padding:0.8rem 1rem;margin:1rem 0;">
<p style="margin:0;font-size:0.88rem;color:#CBD5E1;"><strong style="color:#2DD4BF;">Want to go further?</strong> Continue to get qualification questions, detailed obligations per regulation, gap analysis, synergies, and a consolidated compliance checklist.</p>
</div>""", 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('<p class="screen-title">Qualification</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Refine regulation applicability by answering scope questions for each detected AI-specific regulation.</p>', 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('<p class="screen-title">Requirements Deep Dive</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Obligations, synergies, and cross-jurisdiction gap analysis for your AI system.</p>', 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'<a href="{url}" target="_blank" style="color:#0D9488;font-size:0.78rem;">📄 Official text</a>'
return ""
# ─── Helper: render obligations as single HTML block ───
def render_obligations(obligations, label=None):
html = '<div class="p2-reg-card">'
if label:
html += f'<h4>{label}</h4>'
for o in obligations:
html += f'<p>☐ {o}</p>'
html += '</div>'
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
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'<span class="p2-jump-link">⚙️ AI-Specific ({ai_count})</span>'
if privacy_count:
jump_links += f' <span class="p2-jump-link">🔒 Privacy ({privacy_count})</span>'
if other_count:
jump_links += f' <span class="p2-jump-link">📋 Other ({other_count})</span>'
if synergy_count:
jump_links += f' <span class="p2-jump-link">🔗 Synergies ({synergy_count})</span>'
if gap_count:
jump_links += f' <span class="p2-jump-link">📊 Gap Analysis ({gap_count})</span>'
st.markdown(f"""<div class="p2-jump-nav">
<p class="p2-jump-nav-title">On this page</p>
<div class="p2-jump-nav-links">{jump_links}</div>
</div>""", unsafe_allow_html=True)
# ════════════════════════════════════════
# SECTION A: AI-SPECIFIC OBLIGATIONS
# ════════════════════════════════════════
st.markdown(f'<div class="p2-section-header">⚙️ AI-Specific Regulations — Obligations <span class="p2-section-count">{ai_count} regulations</span></div>', 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('<span class="p2-status p2-status-exempt">EXEMPT</span>', 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('<span class="p2-status p2-status-prohibited">PROHIBITED PRACTICE DETECTED</span>', 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('<span class="p2-status p2-status-applies">HIGH-RISK AI SYSTEM</span>', 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('<span class="p2-status p2-status-exempt">NOT APPLICABLE</span>', 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('<span class="p2-status p2-status-exempt">NOT APPLICABLE</span>', 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('<span class="p2-status p2-status-exempt">NOT APPLICABLE</span>', 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}")
# ── 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'<div class="p2-section-header">🔒 Privacy & Related — Key AI Obligations <span class="p2-section-count">{privacy_count} regulations</span></div>', 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'<div class="p2-section-header">📋 Other Applicable Regulations <span class="p2-section-count">{other_count} regulations</span></div>', 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'<div class="p2-reg-card"><p style="margin:0;line-height:1.5;">{one_liner}</p></div>', unsafe_allow_html=True)
# ════════════════════════════════════════
# SECTION D: COMPLIANCE SYNERGIES
# ════════════════════════════════════════
if applicable_overlaps:
st.markdown(f'<div class="p2-section-header">🔗 Compliance Synergies <span class="p2-section-count">{synergy_count} topics identified</span></div>', 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'<p style="margin:0.15rem 0;font-size:0.8rem;line-height:1.4;color:#475569;">• <strong>{short}</strong>: {label}</p>'
else:
regs_html += f'<p style="margin:0.15rem 0;font-size:0.8rem;line-height:1.4;color:#475569;">• {short}</p>'
st.markdown(f"""<div class="p2-synergy-card">
<h4>{icon} {ov['title']}</h4>
<div style="margin:0.4rem 0 0.6rem;">{regs_html}</div>
<p style="font-size:0.82rem;color:#334155;line-height:1.5;margin:0.5rem 0;"><strong>Recommendation:</strong> {ov['recommendation']}</p>
</div>""", 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'<div class="p2-section-header">📊 Gap Analysis <span class="p2-section-count">{gap_count} comparisons</span></div>', 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'<p class="p2-gap-covered">+ {c}</p>' for c in gap["covered"])
gaps_html = "".join(f'<p class="p2-gap-missing">− {g}</p>' for g in gap["gaps"])
filled_w = max(cov, 15)
empty_w = max(rem, 5)
st.markdown(f"""<div class="p2-gap-card">
<h4>{src}{tgt}</h4>
<div class="p2-gap-bar">
<div class="p2-gap-bar-filled" style="width:{filled_w}%">{cov}% covered</div>
<div class="p2-gap-bar-empty" style="width:{empty_w}%">{rem}% gap</div>
</div>
<p class="p2-gap-label">Already covered:</p>
{covered_html}
<p class="p2-gap-label">Gaps to address:</p>
{gaps_html}
</div>""", unsafe_allow_html=True)
# ════════════════════════════════════════
# DISCLAIMER + NAVIGATION
# ════════════════════════════════════════
st.markdown(f'<div class="p2-disclaimer"><strong>⚠️ Disclaimer:</strong> {DISCLAIMER}</div>', 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('<p class="screen-title">Compliance Checklist</p>', unsafe_allow_html=True)
st.markdown('<p class="screen-subtitle">Consolidated requirements taking synergies into account. Export the full PDF for gap analysis and detailed breakdown.</p>', 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
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"""<div class="intro-box">
<span class="sys-name">{system_name}</span> — {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 ''}
</div>""", unsafe_allow_html=True)
# ════════════════════════════════════════
# SECTION A: CONSOLIDATED REQUIREMENTS
# Synergy-aware: show shared obligations once with all regs tagged
# ════════════════════════════════════════
if applicable_overlaps:
st.markdown(f'<div class="p2-section-header">✅ Consolidated Requirements <span class="p2-section-count">{len(applicable_overlaps)} synergy topics</span></div>', 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'<p style="margin:0.1rem 0;font-size:0.78rem;line-height:1.3;color:#475569;">• <strong>{short}</strong>: {label}</p>'
else:
tags_html += f'<p style="margin:0.1rem 0;font-size:0.78rem;line-height:1.3;color:#475569;">• {short}</p>'
# 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'<p style="margin:0.2rem 0;line-height:1.5;">☐ {elem} <span style="font-size:0.7rem;color:#94A3B8;">({all_short})</span></p>'
st.markdown(f"""<div class="p2-synergy-card">
<h4>{icon} {ov['title']}</h4>
<div style="margin:0.3rem 0 0.5rem;">{tags_html}</div>
<p style="font-size:0.82rem;color:#334155;line-height:1.5;margin:0.4rem 0 0.6rem;"><strong>Recommendation:</strong> {ov['recommendation']}</p>
{checklist_html}
</div>""", 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'<div class="p2-section-header">⚙️ AI Obligations by Regulation <span class="p2-section-count">{len(ai_regs_to_show)} regulations</span></div>', unsafe_allow_html=True)
for reg_name in ai_regs_to_show:
url = REGULATION_URLS.get(reg_name, "")
link_html = f' <a href="{url}" target="_blank" style="font-size:0.72rem;color:#0D9488;">📄 Official text</a>' 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'<h4>{obl_group["category"]}</h4>'
for o in obl_group["obligations"]:
items_html += f'<p style="margin:0.2rem 0;line-height:1.5;">☐ {o}</p>'
if obl_group.get("deadline"):
items_html += f'<p style="font-size:0.75rem;color:#DC2626;margin-top:0.3rem;">⏰ Deadline: {obl_group["deadline"]}</p>'
st.markdown(f'<div class="p2-reg-card">{items_html}</div>', 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'<div class="p2-section-header">🔒 Data Protection & Privacy <span class="p2-section-count">{len(priv_regs_to_show)} regulations</span></div>', unsafe_allow_html=True)
for reg_name in priv_regs_to_show:
url = REGULATION_URLS.get(reg_name, "")
link_html = f' <a href="{url}" target="_blank" style="font-size:0.72rem;color:#0D9488;">📄 Official text</a>' 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'<p style="margin:0.2rem 0;line-height:1.5;">☐ {o}</p>'
st.markdown(f'<div class="p2-reg-card">{items_html}</div>', 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'<div class="p2-section-header">📋 Other Applicable Regulations <span class="p2-section-count">{len(other_detected)} regulations</span></div>', 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' <a href="{url}" target="_blank" style="font-size:0.72rem;color:#0D9488;">📄</a>' if url else ""
st.markdown(f'<div class="p2-reg-card"><p style="margin:0;line-height:1.5;"><strong>{tag}{reg_name}</strong>{link_html}<br>{one_liner}</p></div>', unsafe_allow_html=True)
# ── What's Next + CTA ──
st.markdown("""<div class="whats-next-box">
<p class="wn-title">What's next?</p>
<p class="wn-body">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.</p>
<p class="wn-body">Export the full PDF report for detailed synergies, gap analysis, and the complete compliance breakdown.</p>
<p class="wn-body">Need help implementing these requirements? <a class="wn-link" href="https://www.linkedin.com/in/in%C3%A8s-bedar-5a65b013a" target="_blank">Let's talk</a></p>
</div>""", unsafe_allow_html=True)
# ── Disclaimer + Navigation ──
st.markdown(f'<div class="p2-disclaimer"><strong>⚠️ Disclaimer:</strong> {DISCLAIMER}</div>', 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}")