| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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_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", |
| | ] |
| |
|
| | |
| | |
| | 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}") |
| |
|
| | |
| | |
| | |
| |
|
| | st.set_page_config( |
| | page_title="RegMap — AI System ID Card", |
| | page_icon="◈", |
| | layout="centered", |
| | ) |
| |
|
| | |
| | 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) |
| |
|
| |
|
| | |
| |
|
| | 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", |
| | ] |
| |
|
| | |
| | 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", |
| | ] |
| |
|
| |
|
| | |
| |
|
| | 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]}, |
| | ] |
| |
|
| | |
| | active_step = 1 |
| | for s in steps: |
| | if current_screen in s["screens"]: |
| | active_step = s["num"] |
| | break |
| |
|
| | |
| | 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 = [] |
| |
|
| | |
| | |
| | |
| | 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 |
| |
|
| | |
| | |
| | |
| | story.append(Paragraph("1. AI System ID Card", s_h1)) |
| | story.append(HRFlowable(width="100%", thickness=0.5, color=light_grey, spaceAfter=6)) |
| |
|
| | |
| | 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)) |
| |
|
| | |
| | |
| | |
| | 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)) |
| |
|
| | |
| | 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)) |
| |
|
| | |
| | |
| | |
| | if map_only: |
| | |
| | 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: |
| | |
| | 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: |
| | |
| | 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)) |
| |
|
| | |
| | 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)) |
| |
|
| | |
| | |
| | |
| | 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)) |
| | |
| | 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 |
| |
|
| | |
| | |
| | |
| | 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 |
| |
|
| | |
| | |
| | |
| | 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() |
| |
|
| |
|
| | |
| |
|
| | 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: |
| | |
| | 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.""" |
| | |
| | if data.get("is_public_sector", False): |
| | return True |
| | |
| | public_services = data.get("provides_public_services", []) |
| | if public_services and "None of the above" not in public_services: |
| | return True |
| | |
| | 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"] |
| | if "state agency" in entity.lower() or "government" in entity.lower(): |
| | keys.append("government_deployer") |
| | if "healthcare" in entity.lower(): |
| | keys.append("healthcare_deployer") |
| | |
| | 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'.""" |
| | |
| | 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 |
| | 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, |
| | }) |
| | |
| | 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, |
| | }) |
| | |
| | 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: |
| | |
| | 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: |
| | |
| | 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 |
| |
|
| |
|
| | |
| | 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">🇪🇺 EU</span> |
| | <span class="rmh-badge rmh-el">🇺🇸 US</span> |
| | <span class="rmh-badge rmh-el">🇦🇪 UAE</span> |
| | </div> |
| | </div> |
| | """, unsafe_allow_html=True) |
| |
|
| | |
| | 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, "") |
| |
|
| | |
| | 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) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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_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() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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", |
| | ) |
| |
|
| | |
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| | |
| | 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") |
| |
|
| | |
| | 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") |
| |
|
| | |
| | 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", |
| | ) |
| |
|
| | |
| | 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", |
| | ) |
| |
|
| | |
| | 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", |
| | ) |
| | |
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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_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) |
| |
|
| | |
| | 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_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) |
| |
|
| | |
| | |
| | |
| | detected_regs = [] |
| |
|
| | |
| | FLAG_MAP = { |
| | "EU": "🇪🇺", "FR": "🇫🇷", "US": "🇺🇸", "UAE": "🇦🇪", |
| | "DIFC": "🇦🇪", "ADGM": "🇦🇪", |
| | } |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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"], |
| | } |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | if is_manufacturing_or_automotive: |
| | is_robotics = True |
| |
|
| | |
| | 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: |
| | |
| | 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")) |
| | |
| | if has_personal_data: |
| | detected_regs.append(("EU", "GDPR (Regulation 2016/679)", "other", "Personal data is processed within the EU")) |
| | |
| | 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")) |
| |
|
| | |
| | has_us = "United States" in d.get("operating_countries", []) |
| | us_regulated = d.get("us_states_with_regulation", []) |
| |
|
| | |
| | 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")) |
| |
|
| | |
| | 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")) |
| | |
| | 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_emirates = d.get("uae_emirates", []) |
| |
|
| | if uae_emirates: |
| | |
| | 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")) |
| |
|
| | |
| | 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")) |
| |
|
| | |
| | |
| | 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 |
| | 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">⚖️ 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.") |
| |
|
| | |
| | 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.") |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| | |
| | 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 |
| | |
| | prev_had_none = none_opt in previous |
| | if not prev_had_none: |
| | |
| | return [none_opt] |
| | else: |
| | |
| | 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 |
| |
|
| | 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 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 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: |
| | |
| | answers[q_key] = [] if q["type"] == "multi_select" else "— Select —" |
| | continue |
| |
|
| | |
| | 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 |
| |
|
| | |
| | if q["id"] == "euaia_public_services": |
| | answers[q_key] = [] |
| | continue |
| |
|
| | |
| | if q["id"] == "co_use_as_intended": |
| | if not d.get("is_small_business", False): |
| | answers[q_key] = "— Select —" |
| | continue |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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']}" |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | if q["id"] == "euaia_public_services": |
| | continue |
| |
|
| | |
| | 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 |
| | |
| | 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 |
| |
|
| | |
| | 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)): |
| | |
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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] |
| |
|
| | |
| | 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 "" |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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"] |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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: |
| | if gpai_name not in active_reg_names: |
| | active_reg_names.append(gpai_name) |
| |
|
| | |
| | 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_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) |
| |
|
| | |
| | |
| | |
| |
|
| | 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] |
| |
|
| | |
| | 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"]) |
| |
|
| | |
| | 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"]) |
| |
|
| | |
| | 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"]) |
| |
|
| | |
| | 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"]) |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | 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"]) |
| | |
| | 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}") |
| |
|
| | |
| | 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)) |
| |
|
| | |
| | |
| | |
| |
|
| | 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']) |
| |
|
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| | |
| | |
| | |
| |
|
| | 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", {}) |
| | |
| | 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) |
| |
|
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| | |
| | |
| | |
| |
|
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| | |
| |
|
| | 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] |
| |
|
| | |
| | all_obligations = collect_all_obligations(detected, answers, roles, d) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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])) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | 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", {}) |
| |
|
| | |
| | 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_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) |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| | 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_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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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}") |
| |
|