aimodelshare 0.3.7__py3-none-any.whl → 0.4.71__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. aimodelshare/moral_compass/__init__.py +51 -2
  2. aimodelshare/moral_compass/api_client.py +92 -4
  3. aimodelshare/moral_compass/apps/__init__.py +36 -16
  4. aimodelshare/moral_compass/apps/ai_consequences.py +98 -88
  5. aimodelshare/moral_compass/apps/bias_detective_ca.py +2722 -0
  6. aimodelshare/moral_compass/apps/bias_detective_en.py +2722 -0
  7. aimodelshare/moral_compass/apps/bias_detective_part1.py +2722 -0
  8. aimodelshare/moral_compass/apps/bias_detective_part2.py +2465 -0
  9. aimodelshare/moral_compass/apps/bias_detective_part_es.py +2722 -0
  10. aimodelshare/moral_compass/apps/ethical_revelation.py +237 -147
  11. aimodelshare/moral_compass/apps/fairness_fixer.py +1839 -859
  12. aimodelshare/moral_compass/apps/fairness_fixer_ca.py +1869 -0
  13. aimodelshare/moral_compass/apps/fairness_fixer_en.py +1869 -0
  14. aimodelshare/moral_compass/apps/fairness_fixer_es.py +1869 -0
  15. aimodelshare/moral_compass/apps/judge.py +130 -143
  16. aimodelshare/moral_compass/apps/justice_equity_upgrade.py +793 -831
  17. aimodelshare/moral_compass/apps/justice_equity_upgrade_ca.py +815 -0
  18. aimodelshare/moral_compass/apps/justice_equity_upgrade_en.py +815 -0
  19. aimodelshare/moral_compass/apps/justice_equity_upgrade_es.py +815 -0
  20. aimodelshare/moral_compass/apps/mc_integration_helpers.py +227 -745
  21. aimodelshare/moral_compass/apps/model_building_app_ca.py +4544 -0
  22. aimodelshare/moral_compass/apps/model_building_app_ca_final.py +3899 -0
  23. aimodelshare/moral_compass/apps/model_building_app_en.py +4290 -0
  24. aimodelshare/moral_compass/apps/model_building_app_en_final.py +3869 -0
  25. aimodelshare/moral_compass/apps/model_building_app_es.py +4362 -0
  26. aimodelshare/moral_compass/apps/model_building_app_es_final.py +3899 -0
  27. aimodelshare/moral_compass/apps/model_building_game.py +4211 -935
  28. aimodelshare/moral_compass/apps/moral_compass_challenge.py +195 -95
  29. aimodelshare/moral_compass/apps/what_is_ai.py +126 -117
  30. aimodelshare/moral_compass/challenge.py +98 -17
  31. {aimodelshare-0.3.7.dist-info → aimodelshare-0.4.71.dist-info}/METADATA +1 -1
  32. {aimodelshare-0.3.7.dist-info → aimodelshare-0.4.71.dist-info}/RECORD +35 -19
  33. aimodelshare/moral_compass/apps/bias_detective.py +0 -714
  34. {aimodelshare-0.3.7.dist-info → aimodelshare-0.4.71.dist-info}/WHEEL +0 -0
  35. {aimodelshare-0.3.7.dist-info → aimodelshare-0.4.71.dist-info}/licenses/LICENSE +0 -0
  36. {aimodelshare-0.3.7.dist-info → aimodelshare-0.4.71.dist-info}/top_level.txt +0 -0
@@ -1,889 +1,1869 @@
1
- """
2
- Activity 8: Fairness Fixer - Gradio application for the Justice & Equity Challenge.
3
-
4
- This app teaches:
5
- 1. How to remove biased features (direct demographics)
6
- 2. Identifying and removing proxy variables
7
- 3. Developing representative and continuously improving data strategies
8
- 4. Building ethical roadmaps for responsible AI
9
-
10
- Structure:
11
- - Factory function `create_fairness_fixer_app()` returns a Gradio Blocks object
12
- - Convenience wrapper `launch_fairness_fixer_app()` launches it inline (for notebooks)
13
-
14
- Moral Compass Integration:
15
- - Uses ChallengeManager for progress tracking (tasks D-E)
16
- - Task D: Feature removal and proxy identification
17
- - Task E: Representative data and improvement planning
18
- - Debounced sync with Force Sync option
19
- """
20
- import contextlib
21
1
  import os
22
- import logging
23
-
24
- # Import moral compass integration helpers
25
- from .mc_integration_helpers import (
26
- get_challenge_manager,
27
- sync_user_moral_state,
28
- sync_team_state,
29
- build_moral_leaderboard_html,
30
- get_moral_compass_widget_html,
31
- )
32
-
33
- logger = logging.getLogger("aimodelshare.moral_compass.apps.fairness_fixer")
34
-
35
-
36
- def _get_initial_metrics():
37
- """Get initial fairness metrics before fixes."""
38
- return {
39
- "accuracy": 67.2,
40
- "false_positive_rate": {
41
- "African-American": 44.9,
42
- "Caucasian": 23.5,
43
- "Overall": 34.2
44
- },
45
- "false_negative_rate": {
46
- "African-American": 28.0,
47
- "Caucasian": 47.7,
48
- "Overall": 37.9
49
- }
50
- }
51
-
52
-
53
- def _get_post_demographic_removal_metrics():
54
- """Get metrics after removing direct demographics."""
55
- return {
56
- "accuracy": 64.8, # Slight accuracy drop
57
- "false_positive_rate": {
58
- "African-American": 38.2,
59
- "Caucasian": 26.1,
60
- "Overall": 32.2
61
- },
62
- "false_negative_rate": {
63
- "African-American": 34.5,
64
- "Caucasian": 43.2,
65
- "Overall": 38.9
66
- }
67
- }
68
-
69
-
70
- def _get_post_proxy_removal_metrics():
71
- """Get metrics after removing proxy variables."""
72
- return {
73
- "accuracy": 62.1, # Further accuracy drop, but more fair
74
- "false_positive_rate": {
75
- "African-American": 31.8,
76
- "Caucasian": 28.4,
77
- "Overall": 30.1
78
- },
79
- "false_negative_rate": {
80
- "African-American": 39.2,
81
- "Caucasian": 41.6,
82
- "Overall": 40.4
83
- }
84
- }
2
+ import sys
3
+ import subprocess
4
+ import time
5
+ from typing import Tuple, Optional, List
6
+
7
+ # --- 1. CONFIGURATION ---
8
+ DEFAULT_API_URL = "https://b22q73wp50.execute-api.us-east-1.amazonaws.com/dev"
9
+ ORIGINAL_PLAYGROUND_URL = "https://cf3wdpkg0d.execute-api.us-east-1.amazonaws.com/prod/m"
10
+ TABLE_ID = "m-mc"
11
+ TOTAL_COURSE_TASKS = 20 # Combined count across apps
12
+ LOCAL_TEST_SESSION_ID = None
85
13
 
14
+ # --- 2. SETUP & DEPENDENCIES ---
15
+ def install_dependencies():
16
+ packages = ["gradio>=5.0.0", "aimodelshare", "pandas"]
17
+ for package in packages:
18
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package])
86
19
 
87
- def _get_user_stats():
88
- """Get user statistics."""
20
+ try:
21
+ import gradio as gr
22
+ import pandas as pd
23
+ from aimodelshare.playground import Competition
24
+ from aimodelshare.moral_compass import MoralcompassApiClient
25
+ from aimodelshare.aws import get_token_from_session, _get_username_from_token
26
+ except ImportError:
27
+ print("📦 Installing dependencies...")
28
+ install_dependencies()
29
+ import gradio as gr
30
+ import pandas as pd
31
+ from aimodelshare.playground import Competition
32
+ from aimodelshare.moral_compass import MoralcompassApiClient
33
+ from aimodelshare.aws import get_token_from_session, _get_username_from_token
34
+
35
+ # --- 3. AUTH & HISTORY HELPERS ---
36
+ def _try_session_based_auth(request: "gr.Request") -> Tuple[bool, Optional[str], Optional[str]]:
89
37
  try:
90
- username = os.environ.get("username")
91
- team_name = os.environ.get("TEAM_NAME", "Unknown Team")
92
-
93
- return {
94
- "username": username or "Guest",
95
- "team_name": team_name,
96
- "is_signed_in": bool(username)
97
- }
38
+ session_id = request.query_params.get("sessionid") if request else None
39
+ if not session_id and LOCAL_TEST_SESSION_ID:
40
+ session_id = LOCAL_TEST_SESSION_ID
41
+ if not session_id:
42
+ return False, None, None
43
+ token = get_token_from_session(session_id)
44
+ if not token:
45
+ return False, None, None
46
+ username = _get_username_from_token(token)
47
+ if not username:
48
+ return False, None, None
49
+ return True, username, token
98
50
  except Exception:
99
- return {
100
- "username": "Guest",
101
- "team_name": "Unknown Team",
102
- "is_signed_in": False
103
- }
51
+ return False, None, None
52
+
53
+ def fetch_user_history(username, token):
54
+ default_acc = 0.0
55
+ default_team = "Team-Unassigned"
56
+ try:
57
+ playground = Competition(ORIGINAL_PLAYGROUND_URL)
58
+ df = playground.get_leaderboard(token=token)
59
+ if df is None or df.empty:
60
+ return default_acc, default_team
61
+ if "username" in df.columns and "accuracy" in df.columns:
62
+ user_rows = df[df["username"] == username]
63
+ if not user_rows.empty:
64
+ best_acc = user_rows["accuracy"].max()
65
+ if "timestamp" in user_rows.columns and "Team" in user_rows.columns:
66
+ try:
67
+ user_rows = user_rows.copy()
68
+ user_rows["timestamp"] = pd.to_datetime(
69
+ user_rows["timestamp"], errors="coerce"
70
+ )
71
+ user_rows = user_rows.sort_values("timestamp", ascending=False)
72
+ found_team = user_rows.iloc[0]["Team"]
73
+ if pd.notna(found_team) and str(found_team).strip():
74
+ default_team = str(found_team).strip()
75
+ except Exception:
76
+ pass
77
+ return float(best_acc), default_team
78
+ except Exception:
79
+ pass
80
+ return default_acc, default_team
81
+
82
+ # --- 4. MODULE DEFINITIONS (FAIRNESS FIXER) ---
83
+ MODULES = [
84
+ # --- MODULE 0: THE PROMOTION ---
85
+ {
86
+ "id": 0,
87
+ "title": "Module 0: The Fairness Engineer's Workbench",
88
+ "html": """
89
+ <div class="scenario-box">
90
+ <div class="slide-body">
91
+
92
+ <div style="display:flex; justify-content:center; margin-bottom:18px;">
93
+ <div style="
94
+ display:inline-flex;
95
+ align-items:center;
96
+ gap:10px;
97
+ padding:10px 18px;
98
+ border-radius:999px;
99
+ background:rgba(16, 185, 129, 0.1);
100
+ border:1px solid #10b981;
101
+ font-size:0.95rem;
102
+ text-transform:uppercase;
103
+ letter-spacing:0.08em;
104
+ font-weight:700;
105
+ color:#065f46;">
106
+ <span style="font-size:1.1rem;">🎓</span>
107
+ <span>PROMOTION: FAIRNESS ENGINEER</span>
108
+ </div>
109
+ </div>
110
+
111
+ <h2 class="slide-title" style="text-align:center;">🔧 Final Phase: The Fix</h2>
112
+
113
+ <p style="font-size:1.05rem; max-width:800px; margin:0 auto 20px auto; text-align:center;">
114
+ <strong>Welcome back.</strong> You successfully exposed the bias in the COMPAS model and blocked its deployment. Good work.
115
+ </p>
116
+
117
+ <p style="font-size:1.05rem; max-width:800px; margin:0 auto 24px auto; text-align:center;">
118
+ But the court is still waiting for a tool to help manage the backlog. Your new mission is to take that broken model and <strong>fix it</strong> so it is safe to use.
119
+ </p>
120
+
121
+ <div class="ai-risk-container" style="border-left:4px solid var(--color-accent);">
122
+ <h4 style="margin-top:0; font-size:1.15rem;">The Challenge: "Sticky Bias"</h4>
123
+ <p style="font-size:1.0rem; margin-bottom:0;">
124
+ You can't just delete the "Race" column and walk away. Bias hides in <strong>Proxy Variables</strong>—data like <em>ZIP Code</em> or <em>Income</em>
125
+ that correlate with race. If you delete the label but keep the proxies, the model learns the bias anyway.
126
+ </p>
127
+ </div>
128
+
129
+ <div class="ai-risk-container" style="margin-top:16px;">
130
+ <h4 style="margin-top:0; font-size:1.15rem; text-align:center;">📋 Engineering Work Order</h4>
131
+ <p style="text-align:center; margin-bottom:12px; font-size:0.95rem; color:var(--body-text-color-subdued);">
132
+ You must complete these three protocols to certify the model for release:
133
+ </p>
134
+
135
+ <div style="display:grid; gap:10px; margin-top:12px;">
136
+
137
+ <div style="display:flex; align-items:center; gap:12px; padding:10px; background:var(--background-fill-secondary); border-radius:8px; opacity:0.7;">
138
+ <div style="font-size:1.4rem;">✂️</div>
139
+ <div>
140
+ <div style="font-weight:700;">Protocol 1: Sanitize Inputs</div>
141
+ <div style="font-size:0.9rem;">Remove protected classes and hunt down hidden proxies.</div>
142
+ </div>
143
+ <div style="margin-left:auto; font-weight:700; font-size:0.8rem; text-transform:uppercase; color:var(--body-text-color-subdued);">Pending</div>
144
+ </div>
145
+
146
+ <div style="display:flex; align-items:center; gap:12px; padding:10px; background:var(--background-fill-secondary); border-radius:8px; opacity:0.7;">
147
+ <div style="font-size:1.4rem;">🔗</div>
148
+ <div>
149
+ <div style="font-weight:700;">Protocol 2: Cause Versus Correlation</div>
150
+ <div style="font-size:0.9rem;">Filter data for actual behavior, not just correlation.</div>
151
+ </div>
152
+ <div style="margin-left:auto; font-weight:700; font-size:0.8rem; text-transform:uppercase; color:var(--body-text-color-subdued);">Locked</div>
153
+ </div>
154
+
155
+ <div style="display:flex; align-items:center; gap:12px; padding:10px; background:var(--background-fill-secondary); border-radius:8px; opacity:0.7;">
156
+ <div style="font-size:1.4rem;">⚖️</div>
157
+ <div>
158
+ <div style="font-weight:700;">Protocol 3: Representation & Sampling</div>
159
+ <div style="font-size:0.9rem;">Balance the data to match the local population.</div>
160
+ </div>
161
+ <div style="margin-left:auto; font-weight:700; font-size:0.8rem; text-transform:uppercase; color:var(--body-text-color-subdued);">Locked</div>
162
+ </div>
163
+
164
+ </div>
165
+ </div>
166
+
167
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
168
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
169
+ 🚀 READY TO START THE FIX?
170
+ </p>
171
+ <p style="font-size:1.05rem; margin:0;">
172
+ Click <strong>Next</strong> to start fixing the model.
173
+ </p>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ """,
178
+ },
179
+ # --- MODULE 1: SANITIZE INPUTS (Protected Classes) ---
180
+ {
181
+ "id": 1,
182
+ "title": "Protocol 1: Sanitize Inputs",
183
+ "html": """
184
+ <div class="scenario-box">
185
+ <div class="slide-body">
186
+
187
+ <div style="display:flex; align-items:center; gap:14px; padding:12px 16px; background:rgba(59,130,246,0.08); border:2px solid var(--color-accent); border-radius:12px; margin-bottom:20px;">
188
+ <div style="font-size:1.8rem; background:white; width:50px; height:50px; display:flex; align-items:center; justify-content:center; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,0.05);">✂️</div>
189
+ <div style="flex-grow:1;">
190
+ <div style="font-weight:800; font-size:1.05rem; color:var(--color-accent); letter-spacing:0.05em;">PROTOCOL 1: SANITIZE INPUTS</div>
191
+ <div style="font-size:0.9rem; color:var(--body-text-color);">Mission: Remove protected classes & hidden proxies.</div>
192
+ </div>
193
+ <div style="text-align:right;">
194
+ <div style="font-weight:800; font-size:0.85rem; color:var(--color-accent);">STEP 1 OF 2</div>
195
+ <div style="height:4px; width:60px; background:#bfdbfe; border-radius:2px; margin-top:4px;">
196
+ <div style="height:100%; width:50%; background:var(--color-accent); border-radius:2px;"></div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <p style="font-size:1.05rem; text-align:center; max-width:800px; margin:0 auto 16px auto;">
202
+ <strong>Fairness Through Blindness.</strong>
203
+ Legally and ethically, we cannot use <strong>Protected Classes</strong> (features you are born with, like race or age) to calculate someone's risk score.
204
+ </p>
205
+
206
+ <div class="ai-risk-container">
207
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
208
+ <h4 style="margin:0;">📂 Dataset Column Inspector</h4>
209
+ <div style="font-size:0.8rem; font-weight:700; color:#ef4444;">⚠ CONTAINS ILLEGAL FEATURES</div>
210
+ </div>
211
+
212
+ <p style="font-size:0.95rem; margin-bottom:12px;">
213
+ Review the raw headers below. Identify the columns that violate fairness laws.
214
+ </p>
215
+
216
+ <div style="display:flex; gap:8px; flex-wrap:wrap; background:rgba(0,0,0,0.05); padding:12px; border-radius:8px; border:1px solid var(--border-color-primary);">
217
+
218
+ <div style="padding:6px 12px; background:#fee2e2; border:1px solid #ef4444; border-radius:6px; font-weight:700; color:#b91c1c;">
219
+ ⚠️ Race
220
+ </div>
221
+ <div style="padding:6px 12px; background:#fee2e2; border:1px solid #ef4444; border-radius:6px; font-weight:700; color:#b91c1c;">
222
+ ⚠️ Gender
223
+ </div>
224
+ <div style="padding:6px 12px; background:#fee2e2; border:1px solid #ef4444; border-radius:6px; font-weight:700; color:#b91c1c;">
225
+ ⚠️ Age
226
+ </div>
227
+
228
+ <div style="padding:6px 12px; background:white; border:1px solid var(--border-color-primary); border-radius:6px;">Prior Convictions</div>
229
+ <div style="padding:6px 12px; background:white; border:1px solid var(--border-color-primary); border-radius:6px;">Employment Status</div>
230
+ <div style="padding:6px 12px; background:white; border:1px solid var(--border-color-primary); border-radius:6px;">Zip Code</div>
231
+ </div>
232
+ </div>
233
+
234
+
235
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
236
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
237
+ 🚀 ACTION REQUIRED: DELETE PROTECTED INPUT DATA
238
+ </p>
239
+ <p style="font-size:1.05rem; margin:0;">
240
+ Use the Command Panel below to execute deletion.
241
+ Then click <strong>Next</strong> to continue fixing the model.
242
+ </p>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ """,
247
+ },
248
+ # --- MODULE 2: SANITIZE INPUTS (Proxy Variables) ---
249
+ {
250
+ "id": 2,
251
+ "title": "Protocol 1: Hunting Proxies",
252
+ "html": """
253
+ <div class="scenario-box">
254
+ <div class="slide-body">
255
+
256
+ <div style="display:flex; align-items:center; gap:14px; padding:12px 16px; background:rgba(59,130,246,0.08); border:2px solid var(--color-accent); border-radius:12px; margin-bottom:20px;">
257
+ <div style="font-size:1.8rem; background:white; width:50px; height:50px; display:flex; align-items:center; justify-content:center; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,0.05);">✂️</div>
258
+ <div style="flex-grow:1;">
259
+ <div style="font-weight:800; font-size:1.05rem; color:var(--color-accent); letter-spacing:0.05em;">PROTOCOL 1: SANITIZE INPUTS</div>
260
+ <div style="font-size:0.9rem; color:var(--body-text-color);">Mission: Remove protected classes & hidden proxies.</div>
261
+ </div>
262
+ <div style="text-align:right;">
263
+ <div style="font-weight:800; font-size:0.85rem; color:var(--color-accent);">STEP 2 OF 2</div>
264
+ <div style="height:4px; width:60px; background:#bfdbfe; border-radius:2px; margin-top:4px;">
265
+ <div style="height:100%; width:100%; background:var(--color-accent); border-radius:2px;"></div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+
270
+ <p style="font-size:1.05rem; text-align:center; max-width:800px; margin:0 auto 16px auto;">
271
+ <strong>The "Sticky Bias" Problem.</strong>
272
+ You removed Race and Gender. Great. But bias often hides in <strong>Proxy Variables</strong>—neutral data points that act as a secret substitute for race.
273
+ </p>
274
+
275
+ <div class="hint-box" style="border-left:4px solid #f97316;">
276
+ <div style="font-weight:700;">Why "Zip Code" is a Proxy</div>
277
+
278
+ <p style="margin:6px 0 0 0;">
279
+ Historically, many cities were segregated by law or class. Even today, <strong>Zip Code</strong> often correlates strongly with background.
280
+ </p>
281
+ <p style="margin-top:8px; font-weight:600; color:#c2410c;">
282
+ 🚨 The Risk: If you give the AI location data, it can "guess" a person's race with high accuracy, re-learning the exact bias you just tried to delete.
283
+ </p>
284
+ </div>
285
+
286
+ <div class="ai-risk-container" style="margin-top:16px;">
287
+ <div style="display:flex; justify-content:space-between; align-items:center;">
288
+ <h4 style="margin:0;">📂 Dataset Column Inspector</h4>
289
+ <div style="font-size:0.8rem; font-weight:700; color:#f97316;">⚠️ 1 PROXY DETECTED</div>
290
+ </div>
291
+
292
+ <div style="display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; padding:12px; background:rgba(0,0,0,0.05); border-radius:8px;">
293
+ <div style="padding:6px 12px; background:#e5e7eb; color:#9ca3af; text-decoration:line-through; border-radius:6px;">Race</div>
294
+ <div style="padding:6px 12px; background:#e5e7eb; color:#9ca3af; text-decoration:line-through; border-radius:6px;">Gender</div>
295
+
296
+ <div style="padding:6px 12px; background:#ffedd5; border:1px solid #f97316; border-radius:6px; font-weight:700; color:#9a3412;">
297
+ ⚠️ Zip Code
298
+ </div>
299
+
300
+ <div style="padding:6px 12px; background:white; border:1px solid var(--border-color-primary); border-radius:6px;">Prior Convictions</div>
301
+ <div style="padding:6px 12px; background:white; border:1px solid var(--border-color-primary); border-radius:6px;">Employment</div>
302
+ </div>
303
+ </div>
304
+
305
+
306
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
307
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
308
+ 🚀 ACTION REQUIRED: DELETE PROXY INPUT DATA
309
+ </p>
310
+ <p style="font-size:1.05rem; margin:0;">
311
+ Select the Proxy Variable below to scrub it.
312
+ Then click <strong>Next</strong> to continue fixing the model.
313
+ </p>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ """,
318
+ },
319
+ # --- MODULE 3: THE ACCURACY CRASH (The Pivot) ---
320
+ {
321
+ "id": 3,
322
+ "title": "System Alert: Model Verification",
323
+ "html": """
324
+ <div class="scenario-box">
325
+ <div class="slide-body">
326
+
327
+ <div style="display:flex; align-items:center; gap:14px; padding:12px 16px; background:rgba(59,130,246,0.08); border:2px solid var(--color-accent); border-radius:12px; margin-bottom:20px;">
328
+ <div style="font-size:1.8rem; background:white; width:50px; height:50px; display:flex; align-items:center; justify-content:center; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,0.05);">✂️</div>
329
+ <div style="flex-grow:1;">
330
+ <div style="font-weight:800; font-size:1.05rem; color:var(--color-accent); letter-spacing:0.05em;">PROTOCOL 1: SANITIZE INPUTS</div>
331
+ <div style="font-size:0.9rem; color:var(--body-text-color);">Phase: Verification & Model Retraining</div>
332
+ </div>
333
+ <div style="text-align:right;">
334
+ <div style="font-weight:800; font-size:0.85rem; color:var(--color-accent);">STEP 3 OF 3</div>
335
+ <div style="height:4px; width:60px; background:#bfdbfe; border-radius:2px; margin-top:4px;">
336
+ <div style="height:100%; width:100%; background:var(--color-accent); border-radius:2px;"></div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ <h2 class="slide-title" style="text-align:center; font-size:1.4rem;">🤖 The Verification Run</h2>
342
+
343
+ <p style="font-size:1.05rem; text-align:center; max-width:800px; margin:0 auto 16px auto;">
344
+ You have successfully deleted <strong>Race, Gender, Age, and Zip Code</strong>.
345
+ The dataset is "sanitized" (stripped of all demographic labels). Now we run the simulation to see if the model still works.
346
+ </p>
347
+
348
+ <details style="border:none; margin-top:20px;">
349
+ <summary style="
350
+ background:var(--color-accent);
351
+ color:white;
352
+ padding:16px 24px;
353
+ border-radius:12px;
354
+ font-weight:800;
355
+ font-size:1.1rem;
356
+ text-align:center;
357
+ cursor:pointer;
358
+ list-style:none;
359
+ box-shadow:0 4px 12px rgba(59,130,246,0.3);
360
+ transition:transform 0.1s ease;">
361
+ ▶️ CLICK TO FIX MODEL USING REPAIRED DATASET
362
+ </summary>
363
+
364
+ <div style="margin-top:24px; animation: fadeIn 0.6s ease-in-out;">
365
+
366
+ <div class="ai-risk-container" style="display:grid; grid-template-columns:1fr 1fr; gap:20px; background:rgba(0,0,0,0.02);">
367
+
368
+ <div style="text-align:center; padding:10px; border-right:1px solid var(--border-color-primary);">
369
+ <div style="font-size:2.2rem; font-weight:800; color:#ef4444;">📉 78%</div>
370
+ <div style="font-weight:bold; font-size:0.9rem; text-transform:uppercase; color:var(--body-text-color-subdued); margin-bottom:6px;">Accuracy (CRASHED)</div>
371
+ <div style="font-size:0.9rem; line-height:1.4;">
372
+ <strong>Diagnosis:</strong> The model lost its "shortcuts" (like Zip Code). It is confused and struggling to predict risk accurately.
373
+ </div>
374
+ </div>
375
+
376
+ <div style="text-align:center; padding:10px;">
377
+ <div style="font-size:2.2rem; font-weight:800; color:#f59e0b;">🧩 MISSING</div>
378
+ <div style="font-weight:bold; font-size:0.9rem; text-transform:uppercase; color:var(--body-text-color-subdued); margin-bottom:6px;">Meaningful Data</div>
379
+ <div style="font-size:0.9rem; line-height:1.4;">
380
+ <strong>Diagnosis:</strong> We cleaned the bad data, but we didn't replace it with <strong>Meaningful Data</strong>. The model needs better signals to learn from.
381
+ </div>
382
+ </div>
383
+ </div>
384
+
385
+ <div class="hint-box" style="margin-top:20px; border-left:4px solid var(--color-accent);">
386
+ <div style="font-weight:700; font-size:1.05rem;">💡 The Engineering Pivot</div>
387
+ <p style="margin:6px 0 0 0;">
388
+ A model that knows <em>nothing</em> is fair, but useless.
389
+ To fix the accuracy safely, we need to stop deleting and start <strong>finding valid patterns</strong>: meaningful data that explains <em>why</em> crime happens.
390
+ </p>
391
+ </div>
392
+
393
+
394
+ </details>
395
+
396
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
397
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
398
+ 🚀 ACTION REQUIRED: Find Meaningful Data
399
+ </p>
400
+ <p style="font-size:1.05rem; margin:0;">
401
+ Answer the below question to receive Moral Compass Points.
402
+ Then click <strong>Next</strong> to continue fixing the model.
403
+ </p>
404
+ </div>
405
+ </div>
406
+ </div>
407
+ <style>
408
+ @keyframes fadeIn {
409
+ from { opacity: 0; transform: translateY(-10px); }
410
+ to { opacity: 1; transform: translateY(0); }
411
+ }
412
+ /* Hide default arrow */
413
+ details > summary { list-style: none; }
414
+ details > summary::-webkit-details-marker { display: none; }
415
+ </style>
416
+ """,
417
+ },
418
+ # --- MODULE 4: CAUSAL VALIDITY (Big Foot) ---
419
+ {
420
+ "id": 4,
421
+ "title": "Protocol 2: Causal Validity",
422
+ "html": """
423
+ <div class="scenario-box">
424
+ <div class="slide-body">
425
+
426
+ <div style="display:flex; align-items:center; gap:14px; padding:12px 16px; background:rgba(16, 185, 129, 0.08); border:2px solid #10b981; border-radius:12px; margin-bottom:20px;">
427
+ <div style="font-size:1.8rem; background:white; width:50px; height:50px; display:flex; align-items:center; justify-content:center; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,0.05);">🔗</div>
428
+ <div style="flex-grow:1;">
429
+ <div style="font-weight:800; font-size:1.05rem; color:#065f46; letter-spacing:0.05em;">
430
+ PROTOCOL 2: CAUSE VS. CORRELATION
431
+ </div>
432
+ <div style="font-size:0.9rem; color:var(--body-text-color);">
433
+ Mission: Learn how to tell when a pattern <strong>actually causes</strong> an outcome — and when it’s just a coincidence.
434
+ </div>
435
+ </div>
436
+ <div style="text-align:right;">
437
+ <div style="font-weight:800; font-size:0.85rem; color:#059669;">STEP 1 OF 2</div>
438
+ <div style="height:4px; width:60px; background:#a7f3d0; border-radius:2px; margin-top:4px;">
439
+ <div style="height:100%; width:50%; background:#10b981; border-radius:2px;"></div>
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ <h2 class="slide-title" style="text-align:center; font-size:1.4rem;">
445
+ 🧠 The “Big Foot” Trap: When Correlation Tricks You
446
+ </h2>
447
+
448
+ <p style="font-size:1.05rem; text-align:center; max-width:800px; margin:0 auto 16px auto;">
449
+ To improve a model, we often add more data.
450
+ <br>
451
+ But here’s the problem: the model finds <strong>Correlations</strong> (a relationship between two data variables) and wrongly assumes one <strong>Causes</strong> the other.
452
+ <br>
453
+ Consider this real statistical pattern:
454
+ </p>
455
+
456
+ <div class="ai-risk-container" style="text-align:center; padding:20px; border:2px solid #ef4444; background:#fef2f2;">
457
+ <div style="font-size:3rem; margin-bottom:10px;">🦶 📈 📖</div>
458
+ <h3 style="margin:0; color:#b91c1c;">
459
+ The Data: “People with bigger feet have higher reading scores.”
460
+ </h3>
461
+ <p style="font-size:1.0rem; margin-top:8px;">
462
+ On average, people with <strong>large feet</strong> score much higher on reading tests than people with <strong>small feet</strong>.
463
+ </p>
464
+ </div>
465
+
466
+ <details style="border:none; margin-top:16px;">
467
+ <summary style="
468
+ background:var(--color-accent);
469
+ color:white;
470
+ padding:12px 20px;
471
+ border-radius:8px;
472
+ font-weight:700;
473
+ text-align:center;
474
+ cursor:pointer;
475
+ list-style:none;
476
+ width:fit-content;
477
+ margin:0 auto;
478
+ box-shadow:0 4px 6px rgba(0,0,0,0.1);">
479
+ 🤔 Why does this happen? (Click to reveal)
480
+ </summary>
481
+
482
+ <div style="margin-top:20px; animation: fadeIn 0.5s ease-in-out;">
483
+ <div class="hint-box" style="border-left:4px solid #16a34a;">
484
+ <div style="font-weight:800; font-size:1.1rem; color:#166534;">
485
+ The Hidden Third Variable: AGE
486
+ </div>
487
+ <p style="margin-top:8px;">
488
+ Do bigger feet <em>cause</em> people to read better? <strong>No.</strong>
489
+ <br>
490
+ Children have smaller feet and are still learning to read.
491
+ <br>
492
+ Adults have bigger feet and have had many more years of reading practice.
493
+ </p>
494
+ <p style="margin-bottom:0;">
495
+ <strong>The Key Idea:</strong> Age causes <em>both</em> foot size and reading ability.
496
+ <br>
497
+ Shoe size is a <em>correlated signal</em>, not a cause.
498
+ </p>
499
+ </div>
500
+
501
+ <p style="font-size:1.05rem; text-align:center; margin-top:20px;">
502
+ <strong>Why this matters:</strong>
503
+ <br>
504
+ In many real-world datasets, some variables look predictive only because they are linked to deeper causes.
505
+ <br>
506
+ Good models focus on <strong>what actually causes outcomes</strong>, not just what happens to move together.
507
+ </p>
508
+ </div>
509
+ </details>
510
+
511
+
512
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
513
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
514
+ 🚀 ACTION REQUIRED: Can you spot the next “Big Foot” trap in the data below?
515
+ </p>
516
+ <p style="font-size:1.05rem; margin:0;">
517
+ Answer this question to boost your Moral Compass score.
518
+ Then click <strong>Next</strong> to continue fixing the model.
519
+ </p>
520
+ </div>
521
+ </div>
522
+ </div>
523
+ <style>
524
+ @keyframes fadeIn {
525
+ from { opacity: 0; transform: translateY(-5px); }
526
+ to { opacity: 1; transform: translateY(0); }
527
+ }
528
+ details > summary { list-style: none; }
529
+ details > summary::-webkit-details-marker { display: none; }
530
+ </style>
531
+ """,
532
+ },
533
+ # --- MODULE 5: APPLYING RESEARCH ---
534
+ # --- MODULE 5: APPLYING RESEARCH (Stylized Candidates) ---
535
+ {
536
+ "id": 5,
537
+ "title": "Protocol 2: Cause vs. Correlation",
538
+ "html": """
539
+ <div class="scenario-box">
540
+ <div class="slide-body">
541
+
542
+ <div style="display:flex; align-items:center; gap:14px; padding:12px 16px; background:rgba(16, 185, 129, 0.08); border:2px solid #10b981; border-radius:12px; margin-bottom:20px;">
543
+ <div style="font-size:1.8rem; background:white; width:50px; height:50px; display:flex; align-items:center; justify-content:center; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,0.05);">🔗</div>
544
+ <div style="flex-grow:1;">
545
+ <div style="font-weight:800; font-size:1.05rem; color:#065f46; letter-spacing:0.05em;">
546
+ PROTOCOL 2: CAUSE VS. CORRELATION
547
+ </div>
548
+ <div style="font-size:0.9rem; color:var(--body-text-color);">
549
+ Mission: Remove variables that <strong>correlate</strong> with outcomes but do not <strong>cause</strong> them.
550
+ </div>
551
+ </div>
552
+ <div style="text-align:right;">
553
+ <div style="font-weight:800; font-size:0.85rem; color:#059669;">STEP 2 OF 2</div>
554
+ <div style="height:4px; width:60px; background:#a7f3d0; border-radius:2px; margin-top:4px;">
555
+ <div style="height:100%; width:100%; background:#10b981; border-radius:2px;"></div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+
560
+ <h2 class="slide-title" style="text-align:center; font-size:1.4rem;">
561
+ 🔬 Research Check: Choosing Fair Features
562
+ </h2>
563
+
564
+ <p style="font-size:1.05rem; text-align:center; max-width:800px; margin:0 auto 16px auto;">
565
+ You are ready to continue to build a more just version of the model. Here are four variables to consider.
566
+ <br>
567
+ Use the rule below to discover which variables represent <strong>actual causes</strong> of behavior — and which are just circumstantial correlations.
568
+ </p>
569
+
570
+ <div class="hint-box" style="border-left:4px solid var(--color-accent); background:white; border:1px solid var(--border-color-primary);">
571
+ <div style="display:flex; align-items:center; gap:10px; margin-bottom:8px;">
572
+ <div style="font-size:1.2rem;">📋</div>
573
+ <div style="font-weight:800; color:var(--color-accent); text-transform:uppercase; letter-spacing:0.05em;">
574
+ The Engineering Rule
575
+ </div>
576
+ </div>
577
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
578
+ <div style="padding:10px; background:#fef2f2; border-radius:6px; border:1px solid #fee2e2;">
579
+ <div style="font-weight:700; color:#b91c1c; font-size:0.9rem; margin-bottom:4px;">
580
+ 🚫 REJECT: BACKGROUND
581
+ </div>
582
+ <div style="font-size:0.85rem; line-height:1.4; color:#7f1d1d;">
583
+ Variables describing a person's situation or environment (e.g., wealth, neighborhood).
584
+ <br><strong>These correlate with crime but do not cause it.</strong>
585
+ </div>
586
+ </div>
587
+ <div style="padding:10px; background:#f0fdf4; border-radius:6px; border:1px solid #dcfce7;">
588
+ <div style="font-weight:700; color:#15803d; font-size:0.9rem; margin-bottom:4px;">
589
+ ✅ KEEP: CONDUCT
590
+ </div>
591
+ <div style="font-size:0.85rem; line-height:1.4; color:#14532d;">
592
+ Variables describing documented actions taken by the person (e.g., missed court dates).
593
+ <br><strong>These reflect actual behavior.</strong>
594
+ </div>
595
+ </div>
596
+ </div>
597
+ </div>
598
+
599
+ <div class="ai-risk-container" style="margin-top:20px; background:#f8fafc; border:1px solid #e2e8f0;">
600
+ <h4 style="margin:0 0 12px 0; color:#334155; text-align:center; font-size:1.1rem;">📂 Input Data Candidates</h4>
601
+
602
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
603
+
604
+ <div style="background:white; border:1px solid #cbd5e1; border-left:4px solid #cbd5e1; border-radius:6px; padding:12px; box-shadow:0 2px 4px rgba(0,0,0,0.03);">
605
+ <div style="font-weight:700; font-size:1rem; color:#1e293b; margin-bottom:6px;">Employment Status</div>
606
+ <div style="font-size:0.85rem; background:#f1f5f9; padding:4px 8px; border-radius:4px; color:#475569; display:inline-block;">
607
+ Category: <strong>Background Condition</strong>
608
+ </div>
609
+ </div>
610
+
611
+ <div style="background:white; border:1px solid #cbd5e1; border-left:4px solid #cbd5e1; border-radius:6px; padding:12px; box-shadow:0 2px 4px rgba(0,0,0,0.03);">
612
+ <div style="font-weight:700; font-size:1rem; color:#1e293b; margin-bottom:6px;">Prior Convictions</div>
613
+ <div style="font-size:0.85rem; background:#dcfce7; padding:4px 8px; border-radius:4px; color:#166534; display:inline-block;">
614
+ Category: <strong>Conduct History</strong>
615
+ </div>
616
+ </div>
617
+
618
+ <div style="background:white; border:1px solid #cbd5e1; border-left:4px solid #cbd5e1; border-radius:6px; padding:12px; box-shadow:0 2px 4px rgba(0,0,0,0.03);">
619
+ <div style="font-weight:700; font-size:1rem; color:#1e293b; margin-bottom:6px;">Neighborhood Score</div>
620
+ <div style="font-size:0.85rem; background:#f1f5f9; padding:4px 8px; border-radius:4px; color:#475569; display:inline-block;">
621
+ Category: <strong>Environment</strong>
622
+ </div>
623
+ </div>
624
+
625
+ <div style="background:white; border:1px solid #cbd5e1; border-left:4px solid #cbd5e1; border-radius:6px; padding:12px; box-shadow:0 2px 4px rgba(0,0,0,0.03);">
626
+ <div style="font-weight:700; font-size:1rem; color:#1e293b; margin-bottom:6px;">Failure to Appear</div>
627
+ <div style="font-size:0.85rem; background:#dcfce7; padding:4px 8px; border-radius:4px; color:#166534; display:inline-block;">
628
+ Category: <strong>Conduct History</strong>
629
+ </div>
630
+ </div>
631
+
632
+ </div>
633
+ </div>
634
+
635
+ <div class="hint-box" style="margin-top:20px; border-left:4px solid #8b5cf6; background:linear-gradient(to right, #f5f3ff, white);">
636
+ <div style="font-weight:700; color:#6d28d9; font-size:1.05rem;">💡 Why this matters for Fairness</div>
637
+ <p style="margin:8px 0 0 0; font-size:0.95rem; line-height:1.5;">
638
+ When an AI judges people based on <strong>Correlations</strong> (like neighborhood or poverty), it punishes them for their <strong>circumstances</strong>—things they often cannot control.
639
+ <br><br>
640
+ When an AI judges based on <strong>Causes</strong> (like Conduct), it holds them accountable for their <strong>actions</strong>.
641
+ <br>
642
+ <strong>True Fairness = Being judged on your choices, not your background.</strong>
643
+ </p>
644
+ </div>
645
+
646
+
647
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
648
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
649
+ 🚀 ACTION REQUIRED:
650
+ </p>
651
+ <p style="font-size:1.05rem; margin:0;">
652
+ Select the variables that represent true <strong>Conduct</strong> to build the fair model..
653
+ Then click <strong>Next</strong> to continue fixing the model.
654
+ </p>
655
+ </div>
656
+ </div>
657
+ </div>
658
+ """,
659
+ },
660
+ # --- MODULE 6: REPRESENTATION (Intro) ---
661
+ {
662
+ "id": 6,
663
+ "title": "Protocol 3: Representation Matters",
664
+ "html": """
665
+ <div class="scenario-box">
666
+ <div class="slide-body">
667
+
668
+ <div style="display:flex; align-items:center; gap:14px; padding:12px 16px; background:rgba(139, 92, 246, 0.08); border:2px solid #8b5cf6; border-radius:12px; margin-bottom:20px;">
669
+ <div style="font-size:1.8rem; background:white; width:50px; height:50px; display:flex; align-items:center; justify-content:center; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,0.05);">🌍</div>
670
+ <div style="flex-grow:1;">
671
+ <div style="font-weight:800; font-size:1.05rem; color:#7c3aed; letter-spacing:0.05em;">
672
+ PROTOCOL 3: REPRESENTATION
673
+ </div>
674
+ <div style="font-size:0.9rem; color:var(--body-text-color);">
675
+ Mission: Make sure the training data matches the place where the model will be used.
676
+ </div>
677
+ </div>
678
+ <div style="text-align:right;">
679
+ <div style="font-weight:800; font-size:0.85rem; color:#7c3aed;">STEP 1 OF 2</div>
680
+ <div style="height:4px; width:60px; background:#ddd6fe; border-radius:2px; margin-top:4px;">
681
+ <div style="height:100%; width:50%; background:#8b5cf6; border-radius:2px;"></div>
682
+ </div>
683
+ </div>
684
+ </div>
685
+
686
+ <h2 class="slide-title" style="text-align:center; font-size:1.4rem;">
687
+ 🗺️ The “Wrong Map” Problem
688
+ </h2>
689
+
690
+ <p style="font-size:1.05rem; text-align:center; max-width:820px; margin:0 auto 15px auto;">
691
+ We fixed the <strong>variables</strong> (the columns). Now we must check the <strong>environment</strong> (the rows).
692
+ </p>
693
+
694
+ <div style="background:linear-gradient(to right, #f8fafc, #f1f5f9); border:2px dashed #94a3b8; border-radius:12px; padding:20px; text-align:center; margin-bottom:25px;">
695
+ <div style="font-weight:700; color:#64748b; font-size:0.9rem; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;">THE SCENARIO</div>
696
+ <p style="font-size:1.15rem; font-weight:600; color:#334155; margin:0; line-height:1.5;">
697
+ This dataset was built using historical data from <span style="color:#ef4444;">Broward County, Florida (USA)</span>.
698
+ <br><br>
699
+ Imagine taking this Florida model and forcing it to judge people in a completely different justice system—like <span style="color:#3b82f6;">Barcelona</span> (or your own hometown).
700
+ </p>
701
+ </div>
702
+
703
+ <div class="ai-risk-container" style="display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:20px;">
704
+
705
+ <div class="hint-box" style="margin:0; border-left:4px solid #ef4444; background:#fef2f2;">
706
+ <div style="font-weight:800; color:#b91c1c; margin-bottom:6px;">
707
+ 🇺🇸 THE SOURCE: FLORIDA
708
+ </div>
709
+ <div style="font-size:0.85rem; font-weight:700; color:#7f1d1d;">
710
+ Training Context: US Justice System
711
+ </div>
712
+ <ul style="font-size:0.85rem; margin-top:8px; padding-left:16px; line-height:1.4;">
713
+ <li><strong>Demographic categories:</strong> Defined using US-specific labels and groupings.</li>
714
+ <li><strong>Crime & law:</strong> Different laws and justice processes (for example, bail and pretrial rules).</li>
715
+ <li><strong>Geography:</strong> Car-centric cities and suburban sprawl.</li>
716
+ </ul>
717
+ </div>
718
+
719
+ <div class="hint-box" style="margin:0; border-left:4px solid #3b82f6; background:#eff6ff;">
720
+ <div style="font-weight:800; color:#1e40af; margin-bottom:6px;">
721
+ 📍 THE TARGET: BARCELONA
722
+ </div>
723
+ <div style="font-size:0.85rem; font-weight:700; color:#1e3a8a;">
724
+ Deployment Context: EU Justice System
725
+ </div>
726
+ <ul style="font-size:0.85rem; margin-top:8px; padding-left:16px; line-height:1.4;">
727
+ <li><strong>Demographic categories:</strong> Defined differently than in US datasets.</li>
728
+ <li><strong>Crime & law:</strong> Different legal rules, policing practices, and common offense types.</li>
729
+ <li><strong>Geography:</strong> Dense, walkable urban environment.</li>
730
+ </ul>
731
+ </div>
732
+ </div>
733
+
734
+ <div class="hint-box" style="border-left:4px solid #8b5cf6;">
735
+ <div style="font-weight:700; color:#6d28d9;">
736
+ Why this fails
737
+ </div>
738
+ <p style="margin-top:6px;">
739
+ The model learned patterns from Florida.
740
+ <br>
741
+ When the real-world environment is different, the model can make <strong>more errors</strong> — and those errors can be <strong>uneven across groups</strong>.
742
+ <br>
743
+ On AI engineering teams, this is called a <strong>dataset (or domain) shift</strong>.
744
+ <br>
745
+ It’s like trying to find La Sagrada Família using a map of Miami.
746
+ </p>
747
+ </div>
748
+
749
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
750
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
751
+ 🚀 ACTION REQUIRED:
752
+ </p>
753
+ <p style="font-size:1.05rem; margin:0;">
754
+ Answer the below question to boost your Moral Compass leaderboard score.
755
+ Then click <strong>Next</strong> to continue fixing the data representation problem.
756
+ </p>
757
+ </div>
758
+ </div>
759
+ </div>
760
+ """,
761
+ },
762
+ # --- MODULE 7: THE DATA SWAP ---
763
+ {
764
+ "id": 7,
765
+ "title": "Protocol 3: Fixing the Representation",
766
+ "html": """
767
+ <div class="scenario-box">
768
+ <div class="slide-body">
769
+
770
+ <div style="display:flex; align-items:center; gap:14px; padding:12px 16px; background:rgba(139, 92, 246, 0.08); border:2px solid #8b5cf6; border-radius:12px; margin-bottom:20px;">
771
+ <div style="font-size:1.8rem; background:white; width:50px; height:50px; display:flex; align-items:center; justify-content:center; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,0.05);">🌍</div>
772
+ <div style="flex-grow:1;">
773
+ <div style="font-weight:800; font-size:1.05rem; color:#7c3aed; letter-spacing:0.05em;">PROTOCOL 3: REPRESENTATION</div>
774
+ <div style="font-size:0.9rem; color:var(--body-text-color);">Mission: Replace "Shortcut Data" with "Local Data."</div>
775
+ </div>
776
+ <div style="text-align:right;">
777
+ <div style="font-weight:800; font-size:0.85rem; color:#7c3aed;">STEP 2 OF 2</div>
778
+ <div style="height:4px; width:60px; background:#ddd6fe; border-radius:2px; margin-top:4px;">
779
+ <div style="height:100%; width:100%; background:#8b5cf6; border-radius:2px;"></div>
780
+ </div>
781
+ </div>
782
+ </div>
783
+
784
+ <h2 class="slide-title" style="text-align:center; font-size:1.4rem;">🔄 The Data Swap</h2>
785
+
786
+ <p style="font-size:1.05rem; text-align:center; max-width:800px; margin:0 auto 16px auto;">
787
+ We cannot use the Florida dataset. It is <strong>"Shortcut Data"</strong>—chosen just because it was easy to find.
788
+ <br>
789
+ To build a fair model for <strong>Any Location</strong> (whether it's Barcelona, Berlin, or Boston), we must reject the easy path.
790
+ <br>
791
+ We must collect <strong>Local Data</strong> that reflects the actual reality of that place.
792
+ </p>
793
+
794
+ <div class="ai-risk-container" style="text-align:center; border:2px solid #ef4444; background:#fef2f2; padding:16px; margin-bottom:20px;">
795
+ <div style="font-weight:800; color:#b91c1c; font-size:1.1rem; margin-bottom:8px;">⚠️ CURRENT DATASET: FLORIDA (INVALID)</div>
796
+
797
+ <p style="font-size:0.9rem; margin:0;">
798
+ Dataset does not match local context where model will be used.
799
+ </p>
800
+ </div>
801
+
802
+ <details style="border:none; margin-top:20px;">
803
+ <summary style="
804
+ background:#7c3aed;
805
+ color:white;
806
+ padding:16px 24px;
807
+ border-radius:12px;
808
+ font-weight:800;
809
+ font-size:1.1rem;
810
+ text-align:center;
811
+ cursor:pointer;
812
+ list-style:none;
813
+ box-shadow:0 4px 12px rgba(124, 58, 237, 0.3);
814
+ transition:transform 0.1s ease;">
815
+ 🔄 CLICK TO IMPORT LOCAL BARCELONA DATA
816
+ </summary>
817
+
818
+ <div style="margin-top:24px; animation: fadeIn 0.6s ease-in-out;">
819
+
820
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:16px;">
821
+ <div style="padding:12px; border:1px solid #22c55e; background:#f0fdf4; border-radius:8px; text-align:center;">
822
+ <div style="font-size:2rem;">📍</div>
823
+ <div style="font-weight:700; color:#15803d; font-size:0.9rem;">GEOGRAPHY MATCHED</div>
824
+ <div style="font-size:0.8rem;">Data source: Catalonia Justice Dept</div>
825
+ </div>
826
+ <div style="padding:12px; border:1px solid #22c55e; background:#f0fdf4; border-radius:8px; text-align:center;">
827
+ <div style="font-size:2rem;">⚖️</div>
828
+ <div style="font-weight:700; color:#15803d; font-size:0.9rem;">LAWS SYNCED</div>
829
+ <div style="font-size:0.8rem;">Removed US-specific offenses</div>
830
+ </div>
831
+ </div>
832
+
833
+ <div class="hint-box" style="border-left:4px solid #22c55e;">
834
+ <div style="font-weight:700; color:#15803d;">System Update Complete</div>
835
+ <p style="margin-top:6px;">
836
+ The model is now learning from the people it will actually affect. Accuracy is now meaningful because it reflects local truth.
837
+ </p>
838
+ </div>
839
+
840
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
841
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
842
+ 🚀 ACTION REQUIRED:
843
+ </p>
844
+ <p style="font-size:1.05rem; margin:0;">
845
+ Answer the below question to boost your Moral Compass score.
846
+ Then click <strong>Next</strong> to review and certify that the model is fixed!
847
+ </p>
848
+ </div>
849
+ </div>
850
+ </div>
851
+ <style>
852
+ @keyframes fadeIn {
853
+ from { opacity: 0; transform: translateY(-10px); }
854
+ to { opacity: 1; transform: translateY(0); }
855
+ }
856
+ /* Hide default arrow */
857
+ details > summary { list-style: none; }
858
+ details > summary::-webkit-details-marker { display: none; }
859
+ </style>
860
+ """,
861
+ },
862
+ # --- MODULE 8: FINAL REPORT (Before & After) ---
863
+ {
864
+ "id": 8,
865
+ "title": "Final Fairness Report",
866
+ "html": """
867
+ <div class="scenario-box">
868
+ <div class="slide-body">
869
+
870
+ <div style="display:flex; align-items:center; gap:14px; padding:12px 16px; background:rgba(34, 197, 94, 0.08); border:2px solid #22c55e; border-radius:12px; margin-bottom:20px;">
871
+ <div style="font-size:1.8rem; background:white; width:50px; height:50px; display:flex; align-items:center; justify-content:center; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,0.05);">🏁</div>
872
+ <div style="flex-grow:1;">
873
+ <div style="font-weight:800; font-size:1.05rem; color:#15803d; letter-spacing:0.05em;">AUDIT COMPLETE</div>
874
+ <div style="font-size:0.9rem; color:var(--body-text-color);">System Status: READY FOR CERTIFICATION.</div>
875
+ </div>
876
+ </div>
877
+
878
+ <h2 class="slide-title" style="text-align:center; font-size:1.4rem;">📊 The "Before & After" Report</h2>
879
+
880
+ <p style="font-size:1.05rem; text-align:center; max-width:800px; margin:0 auto 16px auto;">
881
+ You have successfully scrubbed the data, filtered for causality, and localized the context.
882
+ <br>Let's compare your new model to the original model to review what has changed.
883
+ </p>
884
+
885
+ <div class="ai-risk-container" style="display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:20px;">
886
+
887
+ <div style="opacity:0.6; filter:grayscale(100%);">
888
+ <div style="font-weight:800; color:#ef4444; margin-bottom:8px; text-transform:uppercase;">🚫 The Original Model</div>
889
+
890
+ <div style="padding:10px; border-bottom:1px solid #e5e7eb;">
891
+ <div style="font-size:0.8rem; font-weight:700;">INPUTS</div>
892
+ <div style="color:#b91c1c;">Race, Gender, Zip Code</div>
893
+ </div>
894
+ <div style="padding:10px; border-bottom:1px solid #e5e7eb;">
895
+ <div style="font-size:0.8rem; font-weight:700;">LOGIC</div>
896
+ <div style="color:#b91c1c;">Status & Stereotypes</div>
897
+ </div>
898
+ <div style="padding:10px; border-bottom:1px solid #e5e7eb;">
899
+ <div style="font-size:0.8rem; font-weight:700;">CONTEXT</div>
900
+ <div style="color:#b91c1c;">Florida (Wrong Map)</div>
901
+ </div>
902
+ <div style="padding:10px; background:#fee2e2; margin-top:10px; border-radius:6px; color:#b91c1c; font-weight:700; text-align:center;">
903
+ BIAS RISK: CRITICAL
904
+ </div>
905
+ </div>
906
+
907
+ <div style="transform:scale(1.02); box-shadow:0 4px 12px rgba(0,0,0,0.1); border:2px solid #22c55e; border-radius:8px; overflow:hidden;">
908
+ <div style="background:#22c55e; color:white; padding:6px; font-weight:800; text-align:center; text-transform:uppercase;">✅ Your Engineered Model</div>
909
+
910
+ <div style="padding:10px; border-bottom:1px solid #f0fdf4; background:white;">
911
+ <div style="font-size:0.8rem; font-weight:700; color:#15803d;">INPUTS</div>
912
+ <div>Behavior Only</div>
913
+ </div>
914
+ <div style="padding:10px; border-bottom:1px solid #f0fdf4; background:white;">
915
+ <div style="font-size:0.8rem; font-weight:700; color:#15803d;">LOGIC</div>
916
+ <div>Causal Conduct</div>
917
+ </div>
918
+ <div style="padding:10px; border-bottom:1px solid #f0fdf4; background:white;">
919
+ <div style="font-size:0.8rem; font-weight:700; color:#15803d;">CONTEXT</div>
920
+ <div>Barcelona (Local)</div>
921
+ </div>
922
+ <div style="padding:10px; background:#dcfce7; margin-top:0; color:#15803d; font-weight:700; text-align:center;">
923
+ BIAS RISK: MINIMIZED
924
+ </div>
925
+ </div>
926
+ </div>
927
+
928
+ <div class="hint-box" style="border-left:4px solid #f59e0b;">
929
+ <div style="font-weight:700; color:#b45309;">🚧 A Note on "Perfection"</div>
930
+ <p style="margin-top:6px;">
931
+ Is this model perfect? <strong>No.</strong>
932
+ <br>Real-world data (like arrests) can still have hidden biases from human history.
933
+ But you have moved from a system that <em>amplifies</em> prejudice to one that <em>measures fairness</em> using Conduct and Local Context.
934
+ </p>
935
+ </div>
936
+
937
+ <div style="text-align:center; margin-top:35px; padding:20px; background:linear-gradient(to right, rgba(99,102,241,0.1), rgba(16,185,129,0.1)); border-radius:12px; border:2px solid var(--color-accent);">
938
+ <p style="font-size:1.15rem; font-weight:800; color:var(--color-accent); margin-bottom:5px;">
939
+ 🚀 ALMOST FINISHED!
940
+ </p>
941
+ <p style="font-size:1.05rem; margin:0;">
942
+ Answer the below question to boost your Moral Compass Score.
943
+ <br>
944
+ Click <strong>Next</strong> complete your final model approvals to certify the model.
945
+ </p>
946
+ </div>
947
+ </div>
948
+ </div>
949
+ """,
950
+ },
951
+ # --- MODULE 9: CERTIFICATION ---
952
+ # --- MODULE 9: TRANSITION TO CERTIFICATION ---
953
+ {
954
+ "id": 9,
955
+ "title": "Protocol Complete: Ethics Secured",
956
+ "html": """
957
+ <div class="scenario-box">
958
+ <div class="slide-body">
959
+
960
+ <div style="text-align:center; margin-bottom:25px;">
961
+ <h2 class="slide-title" style="margin-bottom:10px; color:#15803d;">🚀 ETHICAL ARCHITECTURE VERIFIED</h2>
962
+ <p style="font-size:1.1rem; max-width:700px; margin:0 auto; color:#334155;">
963
+ You have successfully refactored the AI. It no longer relies on <strong>hidden proxies and unfair shortcuts</strong>—it is now a transparent tool built on fair principles.
964
+ </p>
965
+ </div>
966
+ <div class="ai-risk-container" style="background:#f0fdf4; border:2px solid #22c55e; padding:25px; border-radius:12px; box-shadow:0 4px 20px rgba(34, 197, 94, 0.15);">
967
+
968
+ <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:2px solid #bbf7d0; padding-bottom:15px; margin-bottom:20px;">
969
+ <div style="font-weight:900; font-size:1.3rem; color:#15803d; letter-spacing:0.05em;">SYSTEM DIAGNOSTIC</div>
970
+ <div style="background:#22c55e; color:white; font-weight:800; padding:6px 12px; border-radius:6px;">SAFETY: 100%</div>
971
+ </div>
972
+
973
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:20px;">
974
+ <div style="display:flex; align-items:center; gap:12px;">
975
+ <div style="font-size:1.5rem; color:#16a34a;">✅</div>
976
+ <div>
977
+ <div style="font-weight:800; color:#14532d;">INPUTS</div>
978
+ <div style="font-size:0.9rem; color:#166534;">Sanitized</div>
979
+ </div>
980
+ </div>
981
+ <div style="display:flex; align-items:center; gap:12px;">
982
+ <div style="font-size:1.5rem; color:#16a34a;">✅</div>
983
+ <div>
984
+ <div style="font-weight:800; color:#14532d;">LOGIC</div>
985
+ <div style="font-size:0.9rem; color:#166534;">Causal</div>
986
+ </div>
987
+ </div>
988
+ <div style="display:flex; align-items:center; gap:12px;">
989
+ <div style="font-size:1.5rem; color:#16a34a;">✅</div>
990
+ <div>
991
+ <div style="font-weight:800; color:#14532d;">CONTEXT</div>
992
+ <div style="font-size:0.9rem; color:#166534;">Localized</div>
993
+ </div>
994
+ </div>
995
+ <div style="display:flex; align-items:center; gap:12px;">
996
+ <div style="font-size:1.5rem; color:#16a34a;">✅</div>
997
+ <div>
998
+ <div style="font-weight:800; color:#14532d;">STATUS</div>
999
+ <div style="font-size:0.9rem; color:#166534;">Ethical</div>
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+ </div>
1004
+
1005
+ <div style="margin-top:30px; padding:20px; background:linear-gradient(to right, #fffbeb, #fff); border:2px solid #fcd34d; border-radius:12px;">
1006
+ <div style="display:flex; gap:15px;">
1007
+ <div style="font-size:2.5rem;">🎓</div>
1008
+ <div>
1009
+ <h3 style="margin:0; color:#92400e;">Next Objective: Certification & Performance</h3>
1010
+ <p style="font-size:1.05rem; line-height:1.5; color:#78350f; margin-top:8px;">
1011
+ Now that you have made your model <strong>ethical</strong>, you can continue to improve your model’s <strong>accuracy</strong> in the final activity below.
1012
+ <br><br>
1013
+ But before you optimize for power, you must secure your credentials.
1014
+ </p>
1015
+ </div>
1016
+ </div>
1017
+ </div>
1018
+
1019
+ <div style="text-align:center; margin-top:25px;">
1020
+ <p style="font-size:1.1rem; font-weight:600; color:#475569; margin-bottom:15px;">
1021
+ ⬇️ <strong>Immediate Next Step</strong> ⬇️
1022
+ </p>
1023
+
1024
+ <div style="display:inline-block; padding:15px 30px; background:linear-gradient(to right, #f59e0b, #d97706); border-radius:50px; color:white; font-weight:800; font-size:1.1rem; box-shadow:0 4px 15px rgba(245, 158, 11, 0.4);">
1025
+ Claim your official "Ethics at Play" Certificate in the next activity.
1026
+ </div>
1027
+ </div>
1028
+
1029
+ </div>
1030
+ </div>
1031
+ """,
1032
+ },
1033
+ ]
1034
+
1035
+ # --- 5. INTERACTIVE CONTENT CONFIGURATION (APP 2) ---
1036
+ QUIZ_CONFIG = {
1037
+ 1: {
1038
+ "t": "t11",
1039
+ "q": "Action: Select the variables that must be deleted immediately because they are Protected Classes.",
1040
+ "o": [
1041
+ "A) Zip Code & Neighborhood",
1042
+ "B) Race, Gender, Age",
1043
+ "C) Prior Convictions",
1044
+ ],
1045
+ "a": "B) Race, Gender, Age",
1046
+ "success": "Task Complete. Columns dropped. The model is now blinded to explicit demographics.",
1047
+ },
1048
+ 2: {
1049
+ "t": "t12",
1050
+ "q": "Why must we also remove 'Zip Code' if we already removed 'Race'?",
1051
+ "o": [
1052
+ "A) Because Zip Codes take up too much memory.",
1053
+ "B) It is a Proxy Variable that re-introduces racial bias due to historical segregation.",
1054
+ "C) Zip Codes are not accurate.",
1055
+ ],
1056
+ "a": "B) It is a Proxy Variable that re-introduces racial bias due to historical segregation.",
1057
+ "success": "Proxy Identified. Location data removed to prevent 'Redlining' bias.",
1058
+ },
1059
+ 3: {
1060
+ "t": "t13",
1061
+ "q": "After removing Race and Zip Code, the model is fair but accuracy dropped. Why?",
1062
+ "o": [
1063
+ "A) The model is broken.",
1064
+ "B) A model that knows nothing is fair but useless. We need better data, not just less data.",
1065
+ "C) We should put the Race column back.",
1066
+ ],
1067
+ "a": "B) A model that knows nothing is fair but useless. We need better data, not just less data.",
1068
+ "success": "Pivot Confirmed. We must move from 'Deleting' to 'Selecting' better features.",
1069
+ },
1070
+ 4: {
1071
+ "t": "t14",
1072
+ "q": "Based on the “Big Foot” example, why can it be misleading to let an AI rely on variables like shoe size?",
1073
+ "o": [
1074
+ "A) Because they are physically hard to measure.",
1075
+ "B) Because they often only correlate with outcomes and are caused by a hidden third factor, rather than causing the outcome themselves."
1076
+ ],
1077
+ "a": "B) Because they often only correlate with outcomes and are caused by a hidden third factor, rather than causing the outcome themselves.",
1078
+ "success": "Filter Calibrated. You are now checking whether a pattern is caused by a hidden third variable — not confusing correlation for causation."
1079
+ },
1080
+
1081
+ 5: {
1082
+ "t": "t15",
1083
+ "q": "Which of these remaining features is a Valid Causal Predictor of criminal conduct?",
1084
+ "o": [
1085
+ "A) Employment (Background Condition)",
1086
+ "B) Marital Status (Lifestyle)",
1087
+ "C) Failure to Appear in Court (Conduct)",
1088
+ ],
1089
+ "a": "C) Failure to Appear in Court (Conduct)",
1090
+ "success": "Feature Selected. 'Failure to Appear' reflects a specific action relevant to flight risk.",
1091
+ },
1092
+ 6: {
1093
+ "t": "t16",
1094
+ "q": "Why can a model trained in Florida make unreliable predictions when used in Barcelona?",
1095
+ "o": [
1096
+ "A) Because the software is in English and needs to be translated.",
1097
+ "B) Context mismatch: the model learned patterns tied to US laws, systems, and environments that don’t match Barcelona’s reality.",
1098
+ "C) Because the number of people in Barcelona is different from the training dataset size."
1099
+ ],
1100
+ "a": "B) Context mismatch: the model learned patterns tied to US laws, systems, and environments that don’t match Barcelona’s reality.",
1101
+ "success": "Correct! This is a dataset (or domain) shift. When training data doesn’t match where a model is used, predictions become less accurate and can fail unevenly across groups."
1102
+ },
1103
+
1104
+ 7: {
1105
+ "t": "t17",
1106
+ "q": "You just rejected a massive, free dataset (Florida) for a smaller, harder-to-get one (Barcelona). Why was this the right engineering choice?",
1107
+ "o": [
1108
+ "A) It wasn't. More data is always better, regardless of where it comes from.",
1109
+ "B) Because 'Relevance' is more important than 'Volume.' A small, accurate map is better than a huge, wrong map.",
1110
+ "C) Because the Florida dataset was too expensive.",
1111
+ ],
1112
+ "a": "B) Because 'Relevance' is more important than 'Volume.' A small, accurate map is better than a huge, wrong map.",
1113
+ "success": "Workshop Complete! You have successfully audited, filtered, and localized the AI model.",
1114
+ },
1115
+ 8: {
1116
+ "t": "t18",
1117
+ "q": "You have fixed the Inputs, the Logic, and the Context. Is your new model now 100% perfectly fair?",
1118
+ "o": [
1119
+ "A) Yes. Math is objective, so if the data is clean, the model is perfect.",
1120
+ "B) No. It is safer because we prioritized 'Conduct' over 'Status' and 'Local Reality' over 'Easy Data,' but we must always remain vigilant.",
1121
+ ],
1122
+ "a": "B) No. It is safer because we prioritized 'Conduct' over 'Status' and 'Local Reality' over 'Easy Data,' but we must always remain vigilant.",
1123
+ "success": "Great work. Next you can officially review this model for use.",
1124
+ },
1125
+ 9: {
1126
+ "t": "t19",
1127
+ "q": "You have sanitized inputs, filtered for causality, and reweighted for representation. Are you ready to approve this repaired AI system?",
1128
+ "o": [
1129
+ "A) Yes, The model is now safe and I authorize the use of this AI system.",
1130
+ "B) No, wait for a perfect model.",
1131
+ ],
1132
+ "a": "A) Yes, The model is now safe and I authorize the use of this repaired AI system.",
1133
+ "success": "Mission Accomplished. You have engineered a safer, fairer system.",
1134
+ },
1135
+ }
1136
+
1137
+ # --- 6. CSS (Shared with App 1 for consistency) ---
1138
+ css = """
1139
+ /* Layout + containers */
1140
+ .summary-box {
1141
+ background: var(--block-background-fill);
1142
+ padding: 20px;
1143
+ border-radius: 12px;
1144
+ border: 1px solid var(--border-color-primary);
1145
+ margin-bottom: 20px;
1146
+ box-shadow: 0 4px 12px rgba(0,0,0,0.06);
1147
+ }
1148
+ .summary-box-inner { display: flex; align-items: center; justify-content: space-between; gap: 30px; }
1149
+ .summary-metrics { display: flex; gap: 30px; align-items: center; }
1150
+ .summary-progress { width: 560px; max-width: 100%; }
1151
+
1152
+ /* Scenario cards */
1153
+ .scenario-box {
1154
+ padding: 24px;
1155
+ border-radius: 14px;
1156
+ background: var(--block-background-fill);
1157
+ border: 1px solid var(--border-color-primary);
1158
+ margin-bottom: 22px;
1159
+ box-shadow: 0 6px 18px rgba(0,0,0,0.08);
1160
+ }
1161
+ .slide-title { margin-top: 0; font-size: 1.9rem; font-weight: 800; }
1162
+ .slide-body { font-size: 1.12rem; line-height: 1.65; }
1163
+
1164
+ /* Hint boxes */
1165
+ .hint-box {
1166
+ padding: 12px;
1167
+ border-radius: 10px;
1168
+ background: var(--background-fill-secondary);
1169
+ border: 1px solid var(--border-color-primary);
1170
+ margin-top: 10px;
1171
+ font-size: 0.98rem;
1172
+ }
104
1173
 
1174
+ /* Success / profile card */
1175
+ .profile-card.success-card {
1176
+ padding: 20px;
1177
+ border-radius: 14px;
1178
+ border-left: 6px solid #22c55e;
1179
+ background: linear-gradient(135deg, rgba(34,197,94,0.08), var(--block-background-fill));
1180
+ margin-top: 16px;
1181
+ box-shadow: 0 4px 18px rgba(0,0,0,0.08);
1182
+ font-size: 1.04rem;
1183
+ line-height: 1.55;
1184
+ }
1185
+ .profile-card.first-score {
1186
+ border-left-color: #facc15;
1187
+ background: linear-gradient(135deg, rgba(250,204,21,0.18), var(--block-background-fill));
1188
+ }
1189
+ .success-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 8px; }
1190
+ .success-title { font-size: 1.26rem; font-weight: 900; color: #16a34a; }
1191
+ .success-summary { font-size: 1.06rem; color: var(--body-text-color-subdued); margin-top: 4px; }
1192
+ .success-delta { font-size: 1.5rem; font-weight: 800; color: #16a34a; }
1193
+ .success-metrics { margin-top: 10px; padding: 10px 12px; border-radius: 10px; background: var(--background-fill-secondary); font-size: 1.06rem; }
1194
+ .success-metric-line { margin-bottom: 4px; }
1195
+ .success-body { margin-top: 10px; font-size: 1.06rem; }
1196
+ .success-body-text { margin: 0 0 6px 0; }
1197
+ .success-cta { margin: 4px 0 0 0; font-weight: 700; font-size: 1.06rem; }
105
1198
 
106
- def create_fairness_fixer_app(theme_primary_hue: str = "indigo") -> "gr.Blocks":
107
- """Create the Fairness Fixer Gradio Blocks app (not launched yet)."""
1199
+ /* Numbers + labels */
1200
+ .score-text-primary { font-size: 2.05rem; font-weight: 900; color: var(--color-accent); }
1201
+ .score-text-team { font-size: 2.05rem; font-weight: 900; color: #60a5fa; }
1202
+ .score-text-global { font-size: 2.05rem; font-weight: 900; }
1203
+ .label-text { font-size: 0.82rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #6b7280; }
1204
+
1205
+ /* Progress bar */
1206
+ .progress-bar-bg { width: 100%; height: 10px; background: #e5e7eb; border-radius: 6px; overflow: hidden; margin-top: 8px; }
1207
+ .progress-bar-fill { height: 100%; background: var(--color-accent); transition: width 280ms ease; }
1208
+
1209
+ /* Leaderboard tabs + tables */
1210
+ .leaderboard-card input[type="radio"] { display: none; }
1211
+ .lb-tab-label {
1212
+ display: inline-block; padding: 8px 16px; margin-right: 8px; border-radius: 20px;
1213
+ cursor: pointer; border: 1px solid var(--border-color-primary); font-weight: 700; font-size: 0.94rem;
1214
+ }
1215
+ #lb-tab-team:checked + label, #lb-tab-user:checked + label {
1216
+ background: var(--color-accent); color: white; border-color: var(--color-accent);
1217
+ box-shadow: 0 3px 8px rgba(99,102,241,0.25);
1218
+ }
1219
+ .lb-panel { display: none; margin-top: 10px; }
1220
+ #lb-tab-team:checked ~ .lb-tab-panels .panel-team { display: block; }
1221
+ #lb-tab-user:checked ~ .lb-tab-panels .panel-user { display: block; }
1222
+ .table-container { height: 320px; overflow-y: auto; border: 1px solid var(--border-color-primary); border-radius: 10px; }
1223
+ .leaderboard-table { width: 100%; border-collapse: collapse; }
1224
+ .leaderboard-table th {
1225
+ position: sticky; top: 0; background: var(--background-fill-secondary);
1226
+ padding: 10px; text-align: left; border-bottom: 2px solid var(--border-color-primary);
1227
+ font-weight: 800;
1228
+ }
1229
+ .leaderboard-table td { padding: 10px; border-bottom: 1px solid var(--border-color-primary); }
1230
+ .row-highlight-me, .row-highlight-team { background: rgba(96,165,250,0.18); font-weight: 700; }
1231
+
1232
+ /* Containers */
1233
+ .ai-risk-container { margin-top: 16px; padding: 16px; background: var(--body-background-fill); border-radius: 10px; border: 1px solid var(--border-color-primary); }
1234
+
1235
+ /* Interactive blocks (text size tuned for 17–20 age group) */
1236
+ .interactive-block { font-size: 1.06rem; }
1237
+ .interactive-block .hint-box { font-size: 1.02rem; }
1238
+ .interactive-text { font-size: 1.06rem; }
1239
+
1240
+ /* Radio sizes */
1241
+ .scenario-radio-large label { font-size: 1.06rem; }
1242
+ .quiz-radio-large label { font-size: 1.06rem; }
1243
+
1244
+ /* Small utility */
1245
+ .divider-vertical { width: 1px; height: 48px; background: var(--border-color-primary); opacity: 0.6; }
1246
+
1247
+ /* Navigation loading overlay */
1248
+ #nav-loading-overlay {
1249
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
1250
+ background: color-mix(in srgb, var(--body-background-fill) 95%, transparent);
1251
+ z-index: 9999; display: none; flex-direction: column; align-items: center;
1252
+ justify-content: center; opacity: 0; transition: opacity 0.3s ease;
1253
+ }
1254
+ .nav-spinner {
1255
+ width: 50px; height: 50px; border: 5px solid var(--border-color-primary);
1256
+ border-top: 5px solid var(--color-accent); border-radius: 50%;
1257
+ animation: nav-spin 1s linear infinite; margin-bottom: 20px;
1258
+ }
1259
+ @keyframes nav-spin {
1260
+ 0% { transform: rotate(0deg); }
1261
+ 100% { transform: rotate(360deg); }
1262
+ }
1263
+ #nav-loading-text {
1264
+ font-size: 1.3rem; font-weight: 600; color: var(--color-accent);
1265
+ }
1266
+ @media (prefers-color-scheme: dark) {
1267
+ #nav-loading-overlay { background: rgba(15, 23, 42, 0.9); }
1268
+ .nav-spinner { border-color: rgba(148, 163, 184, 0.4); border-top-color: var(--color-accent); }
1269
+ }
1270
+ """
1271
+
1272
+ # --- 7. LEADERBOARD & API LOGIC (Reused) ---
1273
+ def get_leaderboard_data(client, username, team_name, local_task_list=None, override_score=None):
108
1274
  try:
109
- import gradio as gr
110
- gr.close_all(verbose=False)
111
- except ImportError as e:
112
- raise ImportError(
113
- "Gradio is required for the fairness fixer app. Install with `pip install gradio`."
114
- ) from e
115
-
116
- initial_metrics = _get_initial_metrics()
117
-
118
- # Get user stats and initialize challenge manager
119
- user_stats = _get_user_stats()
120
- challenge_manager = None
121
- if user_stats["is_signed_in"]:
122
- challenge_manager = get_challenge_manager(user_stats["username"])
123
-
124
- # Track state
125
- moral_compass_points = {"value": 0}
126
- server_moral_score = {"value": None}
127
- is_synced = {"value": False}
128
- features_removed = []
129
- proxy_fixes_applied = []
130
-
131
- def sync_moral_state(override=False):
132
- """Sync moral state to server (debounced unless override)."""
133
- if not challenge_manager:
134
- return {
135
- 'widget_html': get_moral_compass_widget_html(
136
- local_points=moral_compass_points["value"],
137
- server_score=None,
138
- is_synced=False
139
- ),
140
- 'status': 'Guest mode - sign in to sync'
141
- }
142
-
143
- # Sync to server
144
- sync_result = sync_user_moral_state(
145
- cm=challenge_manager,
146
- moral_points=moral_compass_points["value"],
147
- override=override
1275
+ resp = client.list_users(table_id=TABLE_ID, limit=500)
1276
+ users = resp.get("users", [])
1277
+
1278
+ if override_score is not None:
1279
+ found = False
1280
+ for u in users:
1281
+ if u.get("username") == username:
1282
+ u["moralCompassScore"] = override_score
1283
+ found = True
1284
+ break
1285
+ if not found:
1286
+ users.append(
1287
+ {"username": username, "moralCompassScore": override_score, "teamName": team_name}
1288
+ )
1289
+
1290
+ users_sorted = sorted(
1291
+ users, key=lambda x: float(x.get("moralCompassScore", 0) or 0), reverse=True
148
1292
  )
149
-
150
- # Update state
151
- if sync_result['synced']:
152
- server_moral_score["value"] = sync_result.get('server_score')
153
- is_synced["value"] = True
154
-
155
- # Trigger team sync if user sync succeeded
156
- if user_stats.get("team_name"):
157
- sync_team_state(user_stats["team_name"])
158
-
159
- # Generate widget HTML
160
- widget_html = get_moral_compass_widget_html(
161
- local_points=moral_compass_points["value"],
162
- server_score=server_moral_score["value"],
163
- is_synced=is_synced["value"]
1293
+
1294
+ my_user = next((u for u in users_sorted if u.get("username") == username), None)
1295
+ score = float(my_user.get("moralCompassScore", 0) or 0) if my_user else 0.0
1296
+ rank = users_sorted.index(my_user) + 1 if my_user else 0
1297
+
1298
+ completed_task_ids = (
1299
+ local_task_list
1300
+ if local_task_list is not None
1301
+ else (my_user.get("completedTaskIds", []) if my_user else [])
164
1302
  )
165
-
1303
+
1304
+ team_map = {}
1305
+ for u in users:
1306
+ t = u.get("teamName")
1307
+ s = float(u.get("moralCompassScore", 0) or 0)
1308
+ if t:
1309
+ if t not in team_map:
1310
+ team_map[t] = {"sum": 0, "count": 0}
1311
+ team_map[t]["sum"] += s
1312
+ team_map[t]["count"] += 1
1313
+ teams_sorted = []
1314
+ for t, d in team_map.items():
1315
+ teams_sorted.append({"team": t, "avg": d["sum"] / d["count"]})
1316
+ teams_sorted.sort(key=lambda x: x["avg"], reverse=True)
1317
+ my_team = next((t for t in teams_sorted if t["team"] == team_name), None)
1318
+ team_rank = teams_sorted.index(my_team) + 1 if my_team else 0
166
1319
  return {
167
- 'widget_html': widget_html,
168
- 'status': sync_result['message']
1320
+ "score": score,
1321
+ "rank": rank,
1322
+ "team_rank": team_rank,
1323
+ "all_users": users_sorted,
1324
+ "all_teams": teams_sorted,
1325
+ "completed_task_ids": completed_task_ids,
169
1326
  }
170
-
171
- def remove_demographics():
172
- """Remove direct demographic features and show impact."""
173
- features_removed.extend(["race", "sex", "age"])
174
- moral_compass_points["value"] += 100
175
-
176
- # Update ChallengeManager (Task D: Remove demographics)
177
- if challenge_manager:
178
- challenge_manager.complete_task('D')
179
- challenge_manager.answer_question('D', 'D1', 1)
180
-
181
- before = initial_metrics
182
- after = _get_post_demographic_removal_metrics()
183
-
184
- report = "## Before/After: Removing Direct Demographics\n\n"
185
- report += "### Overall Metrics\n"
186
- report += f"| Metric | Before | After | Change |\n"
187
- report += f"|--------|--------|-------|--------|\n"
188
- report += f"| Accuracy | {before['accuracy']}% | {after['accuracy']}% | {after['accuracy'] - before['accuracy']:.1f}% |\n\n"
189
-
190
- report += "### False Positive Rates (wrongly labeled as high risk)\n"
191
- report += f"| Group | Before | After | Improvement |\n"
192
- report += f"|-------|--------|-------|-------------|\n"
193
- report += f"| African-American | {before['false_positive_rate']['African-American']}% | {after['false_positive_rate']['African-American']}% | {before['false_positive_rate']['African-American'] - after['false_positive_rate']['African-American']:.1f}% |\n"
194
- report += f"| Caucasian | {before['false_positive_rate']['Caucasian']}% | {after['false_positive_rate']['Caucasian']}% | {after['false_positive_rate']['Caucasian'] - before['false_positive_rate']['Caucasian']:.1f}% |\n\n"
195
-
196
- report += "### 📊 Key Findings:\n"
197
- report += "- ✓ False positive rate disparity **reduced from 21.4% to 12.1%**\n"
198
- report += "- ⚠️ Overall accuracy decreased by 2.4%\n"
199
- report += "- ✓ Model is now more equitable across racial groups\n\n"
200
- report += "**Trade-off Note:** We accept a small accuracy decrease to achieve greater fairness.\n\n"
201
- report += "🏆 +100 Moral Compass points for removing biased features!"
202
-
203
- # Trigger sync
204
- sync_result = sync_moral_state()
205
- report += f"\n\n{sync_result['status']}"
206
-
207
- return report
208
-
209
- def check_first_step_question(answer):
210
- """Check the first step question."""
211
- if answer == "Remove demographic attributes like race and gender":
212
- moral_compass_points["value"] += 50
213
- return "✓ Correct! The first step in fairness is removing direct demographic attributes that can cause discriminatory predictions.\n\n🏆 +50 Moral Compass points!"
214
- else:
215
- return "✗ Not quite. Think about which variables directly encode protected characteristics."
216
-
217
- def rank_proxy_variables(neighborhood_rank, income_rank, priors_rank):
218
- """Check proxy variable ranking."""
219
- correct_ranking = {
220
- "Neighborhood ZIP code": "1",
221
- "Prior arrests (count)": "2",
222
- "Income level": "3"
223
- }
224
-
225
- score = 0
226
- feedback = []
227
-
228
- if neighborhood_rank == "1":
229
- score += 1
230
- feedback.append("✓ Correct! Neighborhood is the strongest proxy for race due to residential segregation.")
231
- else:
232
- feedback.append("✗ Neighborhood ZIP code is actually the #1 proxy due to residential segregation.")
233
-
234
- if priors_rank == "2":
235
- score += 1
236
- feedback.append("✓ Correct! Prior arrests reflect historical bias in policing patterns.")
237
- else:
238
- feedback.append("✗ Prior arrests is the #2 proxy, reflecting historical policing bias.")
239
-
240
- if income_rank == "3":
241
- score += 1
242
- feedback.append("✓ Correct! Income correlates with race but is less direct than ZIP code.")
243
- else:
244
- feedback.append("✗ Income level is the #3 proxy, less direct than neighborhood.")
245
-
246
- if score == 3:
247
- moral_compass_points["value"] += 100
248
- feedback.append("\n🎉 Perfect! You understand proxy variables!\n🏆 +100 Moral Compass points!")
249
- elif score >= 2:
250
- moral_compass_points["value"] += 50
251
- feedback.append("\n🏆 +50 Moral Compass points for good understanding!")
252
-
253
- return "\n".join(feedback)
254
-
255
- def remove_proxy_variables():
256
- """Remove proxy variables and reanalyze."""
257
- proxy_fixes_applied.extend(["neighborhood", "prior_arrests_count", "income"])
258
- moral_compass_points["value"] += 100
259
-
260
- # Update ChallengeManager (continuing Task D: Remove proxies)
261
- if challenge_manager:
262
- challenge_manager.answer_question('D', 'D2', 1)
263
-
264
- before = _get_post_demographic_removal_metrics()
265
- after = _get_post_proxy_removal_metrics()
266
-
267
- report = "## Impact of Removing Proxy Variables\n\n"
268
- report += "**Removed:** Neighborhood ZIP code, Prior arrests count, Income level\n\n"
269
- report += "### False Positive Rate Disparity\n"
270
- report += f"| Metric | After Demographics Removed | After Proxies Removed | Improvement |\n"
271
- report += f"|--------|---------------------------|----------------------|-------------|\n"
272
- report += f"| African-American FPR | {before['false_positive_rate']['African-American']}% | {after['false_positive_rate']['African-American']}% | {before['false_positive_rate']['African-American'] - after['false_positive_rate']['African-American']:.1f}% |\n"
273
- report += f"| Caucasian FPR | {before['false_positive_rate']['Caucasian']}% | {after['false_positive_rate']['Caucasian']}% | {after['false_positive_rate']['Caucasian'] - before['false_positive_rate']['Caucasian']:.1f}% |\n"
274
- report += f"| Disparity | {before['false_positive_rate']['African-American'] - before['false_positive_rate']['Caucasian']:.1f}% | {after['false_positive_rate']['African-American'] - after['false_positive_rate']['Caucasian']:.1f}% | {(before['false_positive_rate']['African-American'] - before['false_positive_rate']['Caucasian']) - (after['false_positive_rate']['African-American'] - after['false_positive_rate']['Caucasian']):.1f}% |\n\n"
275
-
276
- report += "### 📊 Key Findings:\n"
277
- report += "- ✓ Disparity further reduced from 12.1% to **3.4%** - nearly equalized!\n"
278
- report += "- ⚠️ Accuracy decreased to 62.1% (from 64.8%)\n"
279
- report += "- ✓ **Model now treats groups much more fairly**\n\n"
280
- report += "**Important:** Removing proxies prevents indirect discrimination through correlated variables.\n\n"
281
- report += "🏆 +100 Moral Compass points for eliminating proxy bias!"
282
-
283
- # Trigger sync
284
- sync_result = sync_moral_state()
285
- report += f"\n\n{sync_result['status']}"
286
-
287
- return report
288
-
289
- def check_proxy_question(answer):
290
- """Check proxy identification question."""
291
- if answer == "ZIP code in a city with segregated neighborhoods":
292
- moral_compass_points["value"] += 50
293
- return "✓ Correct! ZIP codes can serve as a strong proxy for race in segregated areas, reintroducing bias even after removing race explicitly.\n\n🏆 +50 Moral Compass points!"
294
- else:
295
- return "✗ Not quite. Think about which variable most closely correlates with protected attributes in real-world settings."
296
-
297
- def generate_data_guidelines():
298
- """Generate representative data guidelines."""
299
- moral_compass_points["value"] += 75
300
-
301
- # Update ChallengeManager (Task E: Representative data)
302
- if challenge_manager:
303
- challenge_manager.complete_task('E')
304
- challenge_manager.answer_question('E', 'E1', 1)
305
-
306
- guidelines = """
307
- ## 📋 Representative Data Guidelines
308
-
309
- Based on expert consensus between data scientists, judges, and community members:
310
-
311
- ### 1. Population Matching
312
- - ✓ Training data must reflect the demographics of the population where the model will be deployed
313
- - ✓ Include all subgroups that will be affected by model predictions
314
- - ✗ Avoid over-sampling from convenient but unrepresentative sources
315
-
316
- ### 2. Geographic Balance
317
- - ✓ Collect data from multiple jurisdictions (urban, suburban, rural)
318
- - ✓ Ensure regional representation matches deployment scope
319
- - ✗ Don't rely solely on data from a single city or region
320
-
321
- ### 3. Temporal Relevance
322
- - ✓ Use recent data that reflects current social conditions
323
- - ✓ Regularly update training data as society evolves
324
- - ✗ Avoid relying on outdated data from different legal/social contexts
325
-
326
- ### 4. Outcome Diversity
327
- - ✓ Include both positive and negative outcomes for all groups
328
- - ✓ Balance historical bias in outcome labels when possible
329
- - ✗ Don't perpetuate historical discrimination patterns
330
-
331
- ### 5. Community Input
332
- - ✓ Consult with affected communities about data collection
333
- - ✓ Incorporate local knowledge about relevant features
334
- - ✗ Don't make assumptions without stakeholder input
335
-
336
- 🏆 +75 Moral Compass points for developing data guidelines!
337
- """
338
- # Trigger sync
339
- sync_result = sync_moral_state()
340
- guidelines += f"\n\n{sync_result['status']}"
341
-
342
- return guidelines
343
-
344
- def check_representative_data_question(answer):
345
- """Check representative data question."""
346
- if answer == "Training data that mirrors the demographics and conditions of the target population":
347
- moral_compass_points["value"] += 25
348
- return "✓ Correct! Representative data accurately reflects the population where the model will be used.\n\n🏆 +25 Moral Compass points!"
349
- else:
350
- return "✗ Not quite. Representative data should match the target deployment population."
1327
+ except Exception:
1328
+ return None
351
1329
 
352
- def check_geographic_question(answer):
353
- """Check geographic mismatch question."""
354
- if answer == "No - the model may not generalize well to different geographic contexts":
355
- moral_compass_points["value"] += 25
356
- return "✓ Correct! Models trained in one region may not work fairly or accurately in areas with different demographics and social conditions.\n\n🏆 +25 Moral Compass points!"
357
- else:
358
- return "✗ Not quite. Consider how regional differences might affect model performance and fairness."
359
-
360
- def build_improvement_plan(audit_order, doc_order, stakeholder_order):
361
- """Check if roadmap steps are correctly ordered."""
362
- score = 0
363
- feedback = []
364
-
365
- if audit_order == "1":
366
- score += 1
367
- feedback.append("✓ Correct! Regular auditing should be the first step.")
368
- else:
369
- feedback.append("✗ Regular auditing should be step 1 to catch issues early.")
370
-
371
- if doc_order == "2":
372
- score += 1
373
- feedback.append("✓ Correct! Documentation enables transparency and accountability.")
374
- else:
375
- feedback.append(" Documentation should be step 2 to ensure transparency.")
376
-
377
- if stakeholder_order == "3":
378
- score += 1
379
- feedback.append("✓ Correct! Ongoing stakeholder engagement is the final continuous process.")
380
- else:
381
- feedback.append("✗ Stakeholder engagement should be step 3 for continuous improvement.")
382
-
383
- if score == 3:
384
- moral_compass_points["value"] += 100
385
- plan = """
386
- ## 🗺️ Continuous Improvement Plan
387
-
388
- ### Phase 1: Regular Auditing
389
- - ✓ Quarterly fairness metric reviews
390
- - ✓ Automated bias detection systems
391
- - Disparate impact analysis across all protected groups
392
-
393
- ### Phase 2: Transparent Documentation
394
- - ✓ Public model cards with fairness metrics
395
- - ✓ Dataset composition reports
396
- - ✓ Decision-making process documentation
397
-
398
- ### Phase 3: Stakeholder Engagement
399
- - ✓ Community advisory board meetings
400
- - ✓ Feedback mechanisms for affected individuals
401
- - ✓ Regular consultations with advocacy groups
402
-
403
- 🎉 Perfect roadmap! 🏆 +100 Moral Compass points!
404
- """
405
- feedback.append(plan)
406
- elif score >= 2:
407
- moral_compass_points["value"] += 50
408
- feedback.append("\n🏆 +50 Moral Compass points for good understanding!")
409
-
410
- return "\n".join(feedback)
411
-
412
- def check_model_card_question(answer):
413
- """Check model card question."""
414
- if answer == "Fairness metrics across demographic groups":
415
- moral_compass_points["value"] += 50
416
- return "✓ Correct! Judges need to understand how fairly the model treats different groups to make informed decisions about its use.\n\n🏆 +50 Moral Compass points!"
417
- else:
418
- return "✗ Not quite. Think about what information would help a judge evaluate whether the model should be used in court."
419
-
420
- def generate_fairness_summary():
421
- """Generate final fairness fix summary."""
422
- report = "# 🔧 Fairness Fix Summary\n\n"
423
- report += f"**Moral Compass Score:** {moral_compass_points['value']} points\n\n"
424
-
425
- report += "## Features Removed:\n"
426
- if features_removed:
427
- for feature in set(features_removed):
428
- report += f"- ✓ {feature}\n"
1330
+ def ensure_table_and_get_data(username, token, team_name, task_list_state=None):
1331
+ if not username or not token:
1332
+ return None, username
1333
+ os.environ["MORAL_COMPASS_API_BASE_URL"] = DEFAULT_API_URL
1334
+ client = MoralcompassApiClient(api_base_url=DEFAULT_API_URL, auth_token=token)
1335
+ try:
1336
+ client.get_table(TABLE_ID)
1337
+ except Exception:
1338
+ try:
1339
+ client.create_table(
1340
+ table_id=TABLE_ID,
1341
+ display_name="LMS",
1342
+ playground_url="https://example.com",
1343
+ )
1344
+ except Exception:
1345
+ pass
1346
+ return get_leaderboard_data(client, username, team_name, task_list_state), username
1347
+
1348
+ def trigger_api_update(
1349
+ username, token, team_name, module_id, user_real_accuracy, task_list_state, append_task_id=None
1350
+ ):
1351
+ if not username or not token:
1352
+ return None, None, username, task_list_state
1353
+ os.environ["MORAL_COMPASS_API_BASE_URL"] = DEFAULT_API_URL
1354
+ client = MoralcompassApiClient(api_base_url=DEFAULT_API_URL, auth_token=token)
1355
+
1356
+ acc = float(user_real_accuracy) if user_real_accuracy is not None else 0.0
1357
+
1358
+ old_task_list = list(task_list_state) if task_list_state else []
1359
+ new_task_list = list(old_task_list)
1360
+ if append_task_id and append_task_id not in new_task_list:
1361
+ new_task_list.append(append_task_id)
1362
+ try:
1363
+ new_task_list.sort(
1364
+ key=lambda x: int(x[1:]) if x.startswith("t") and x[1:].isdigit() else 0
1365
+ )
1366
+ except Exception:
1367
+ pass
1368
+
1369
+ tasks_completed = len(new_task_list)
1370
+ client.update_moral_compass(
1371
+ table_id=TABLE_ID,
1372
+ username=username,
1373
+ team_name=team_name,
1374
+ metrics={"accuracy": acc},
1375
+ tasks_completed=tasks_completed,
1376
+ total_tasks=TOTAL_COURSE_TASKS,
1377
+ primary_metric="accuracy",
1378
+ completed_task_ids=new_task_list,
1379
+ )
1380
+
1381
+ old_score_calc = acc * (len(old_task_list) / TOTAL_COURSE_TASKS)
1382
+ new_score_calc = acc * (len(new_task_list) / TOTAL_COURSE_TASKS)
1383
+
1384
+ prev_data = get_leaderboard_data(
1385
+ client, username, team_name, old_task_list, override_score=old_score_calc
1386
+ )
1387
+ lb_data = get_leaderboard_data(
1388
+ client, username, team_name, new_task_list, override_score=new_score_calc
1389
+ )
1390
+
1391
+ return prev_data, lb_data, username, new_task_list
1392
+
1393
+ # --- 8. SUCCESS MESSAGE / DASHBOARD RENDERING ---
1394
+ def generate_success_message(prev, curr, specific_text):
1395
+ old_score = float(prev.get("score", 0) or 0) if prev else 0.0
1396
+ new_score = float(curr.get("score", 0) or 0)
1397
+ diff_score = new_score - old_score
1398
+
1399
+ old_rank = prev.get("rank", "–") if prev else ""
1400
+ new_rank = curr.get("rank", "–")
1401
+
1402
+ ranks_are_int = isinstance(old_rank, int) and isinstance(new_rank, int)
1403
+ rank_diff = old_rank - new_rank if ranks_are_int else 0
1404
+
1405
+ if old_score == 0 and new_score > 0:
1406
+ style_key = "first"
1407
+ else:
1408
+ if ranks_are_int:
1409
+ if rank_diff >= 3:
1410
+ style_key = "major"
1411
+ elif rank_diff > 0:
1412
+ style_key = "climb"
1413
+ elif diff_score > 0 and new_rank == old_rank:
1414
+ style_key = "solid"
1415
+ else:
1416
+ style_key = "tight"
429
1417
  else:
430
- report += "- No features removed yet\n"
431
-
432
- report += "\n## Proxy Fixes Applied:\n"
433
- if proxy_fixes_applied:
434
- for proxy in set(proxy_fixes_applied):
435
- report += f"- ✓ {proxy}\n"
1418
+ style_key = "solid" if diff_score > 0 else "tight"
1419
+
1420
+ card_class = "profile-card success-card"
1421
+
1422
+ if style_key == "first":
1423
+ card_class += " first-score"
1424
+ header_emoji = "🎉"
1425
+ header_title = "You're Officially on the Board!"
1426
+ summary_line = (
1427
+ "You just earned your first Moral Compass Score — you're now part of the global rankings."
1428
+ )
1429
+ cta_line = "Scroll down to take your next step and start climbing."
1430
+ elif style_key == "major":
1431
+ header_emoji = "🔥"
1432
+ header_title = "Major Moral Compass Boost!"
1433
+ summary_line = (
1434
+ "Your decision made a big impact — you just moved ahead of other participants."
1435
+ )
1436
+ cta_line = "Scroll down to take on your next challenge and keep the boost going."
1437
+ elif style_key == "climb":
1438
+ header_emoji = "🚀"
1439
+ header_title = "You're Climbing the Leaderboard"
1440
+ summary_line = "Nice work — you edged out a few other participants."
1441
+ cta_line = "Scroll down to continue your investigation and push even higher."
1442
+ elif style_key == "tight":
1443
+ header_emoji = "📊"
1444
+ header_title = "The Leaderboard Is Shifting"
1445
+ summary_line = (
1446
+ "Other teams are moving too. You'll need a few more strong decisions to stand out."
1447
+ )
1448
+ cta_line = "Take on the next question to strengthen your position."
1449
+ else:
1450
+ header_emoji = "✅"
1451
+ header_title = "Progress Logged"
1452
+ summary_line = "Your ethical insight increased your Moral Compass Score."
1453
+ cta_line = "Try the next scenario to break into the next tier."
1454
+
1455
+ if style_key == "first":
1456
+ score_line = f"🧭 Score: <strong>{new_score:.3f}</strong>"
1457
+ if ranks_are_int:
1458
+ rank_line = f"🏅 Initial Rank: <strong>#{new_rank}</strong>"
436
1459
  else:
437
- report += "- No proxy fixes applied yet\n"
438
-
439
- report += "\n## Fairness Metric Changes:\n"
440
- initial = _get_initial_metrics()
441
- final = _get_post_proxy_removal_metrics()
442
-
443
- report += f"- False Positive Rate Disparity: {initial['false_positive_rate']['African-American'] - initial['false_positive_rate']['Caucasian']:.1f}% → {final['false_positive_rate']['African-American'] - final['false_positive_rate']['Caucasian']:.1f}%\n"
444
- report += f"- Overall Accuracy: {initial['accuracy']:.1f}% → {final['accuracy']:.1f}%\n"
445
- report += f"- **Fairness Improvement:** {((initial['false_positive_rate']['African-American'] - initial['false_positive_rate']['Caucasian']) - (final['false_positive_rate']['African-American'] - final['false_positive_rate']['Caucasian'])):.1f}% reduction in disparity\n"
446
-
447
- report += "\n## Ongoing Improvement Plan:\n"
448
- report += "- ✓ Regular fairness audits (quarterly)\n"
449
- report += "- ✓ Transparent model documentation\n"
450
- report += "- ✓ Stakeholder engagement process\n\n"
451
-
452
- report += "**Status:** Ready to proceed to Activity 9 - Justice & Equity Upgrade\n"
453
-
454
- return report
455
-
456
- # Create the Gradio app
457
- with gr.Blocks(
458
- title="Activity 8: Fairness Fixer",
459
- theme=gr.themes.Soft(primary_hue=theme_primary_hue)
460
- ) as app:
461
- gr.Markdown("# 🔧 Activity 8: Fairness Fixer")
462
- gr.Markdown(
463
- """
464
- **Objective:** Apply hands-on fairness fixes: remove biased features, eliminate proxy variables,
465
- and develop a representative and continuously improving data strategy.
466
-
467
- **Your Role:** You're now a **Fairness Engineer**.
468
-
469
- **Progress:** Activity 8 of 10 — Fix the Model
470
-
471
- **Estimated Time:** 12–15 minutes
472
- """
1460
+ rank_line = f"🏅 Initial Rank: <strong>#{new_rank}</strong>"
1461
+ else:
1462
+ score_line = (
1463
+ f"🧭 Score: {old_score:.3f} → <strong>{new_score:.3f}</strong> "
1464
+ f"(+{diff_score:.3f})"
473
1465
  )
474
-
475
- # Moral Compass widget with Force Sync
476
- with gr.Row():
477
- with gr.Column(scale=3):
478
- moral_compass_display = gr.HTML(
479
- get_moral_compass_widget_html(
480
- local_points=0,
481
- server_score=None,
482
- is_synced=False
483
- )
1466
+
1467
+ if ranks_are_int:
1468
+ if old_rank == new_rank:
1469
+ rank_line = f"📊 Rank: <strong>#{new_rank}</strong> (holding steady)"
1470
+ elif rank_diff > 0:
1471
+ rank_line = (
1472
+ f"📈 Rank: #{old_rank} → <strong>#{new_rank}</strong> "
1473
+ f"(+{rank_diff} places)"
484
1474
  )
485
- with gr.Column(scale=1):
486
- force_sync_btn = gr.Button("Force Sync", variant="secondary", size="sm")
487
- sync_status = gr.Markdown("")
488
-
489
- # Force Sync handler
490
- def handle_force_sync():
491
- sync_result = sync_moral_state(override=True)
492
- return sync_result['widget_html'], sync_result['status']
493
-
494
- force_sync_btn.click(
495
- fn=handle_force_sync,
496
- outputs=[moral_compass_display, sync_status]
497
- )
498
-
499
- # Section 8.2: Remove Direct Demographics
500
- with gr.Tab("8.2 Remove Demographics"):
501
- gr.Markdown(
502
- """
503
- ## Remove Direct Demographics
504
-
505
- **Problem:** Direct demographic attributes (race, sex, age) create unfair predictions
506
- because they encode protected characteristics.
507
-
508
- **Solution:** Remove these features from the model entirely.
509
- """
510
- )
511
-
512
- remove_demo_btn = gr.Button("Remove Demographics & Reanalyze", variant="primary")
513
- demo_removal_output = gr.Markdown("")
514
-
515
- remove_demo_btn.click(
516
- fn=remove_demographics,
517
- outputs=demo_removal_output
518
- ).then(
519
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
520
- outputs=moral_compass_display
521
- )
522
-
523
- gr.Markdown("### Check-In Question")
524
- first_step_question = gr.Radio(
525
- choices=[
526
- "Increase model accuracy at all costs",
527
- "Remove demographic attributes like race and gender",
528
- "Collect more data from the majority group",
529
- "Use all available features without restriction"
530
- ],
531
- label="What should be the first step in making an AI model fairer?",
532
- value=None
533
- )
534
- first_step_btn = gr.Button("Check Answer")
535
- first_step_feedback = gr.Markdown("")
536
-
537
- first_step_btn.click(
538
- fn=check_first_step_question,
539
- inputs=first_step_question,
540
- outputs=first_step_feedback
541
- ).then(
542
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
543
- outputs=moral_compass_display
544
- )
545
-
546
- # Section 8.3: Identify & Remove Proxies
547
- with gr.Tab("8.3 Remove Proxies"):
548
- gr.Markdown(
549
- """
550
- ## Identify & Remove Proxy Variables
551
-
552
- **Problem:** Even after removing race, other variables can serve as **proxies** that
553
- replicate bias. A proxy is a variable that strongly correlates with a protected attribute.
554
-
555
- ### 📚 Fairness Metrics: Statistical Parity vs Equal Opportunity
556
-
557
- <details>
558
- <summary><b>Click to expand: Fairness Metric Comparison</b></summary>
559
-
560
- **Scenario:** Pretrial risk model predicting recidivism
561
-
562
- | Metric | Definition | Example Calculation | Limitation |
563
- |--------|-----------|---------------------|------------|
564
- | **Statistical Parity** | Equal positive prediction rates across groups | P(Predicted High Risk \\| African-American) = P(Predicted High Risk \\| Caucasian) | May require unequal treatment of equally risky individuals |
565
- | **Equal Opportunity** | Equal true positive rates across groups | P(Predicted High Risk \\| Actually Risky, African-American) = P(Predicted High Risk \\| Actually Risky, Caucasian) | Ignores false positive disparity |
566
- | **Equalized Odds** | Both TPR and FPR equal across groups | Both conditions above must hold | Difficult to achieve simultaneously |
567
-
568
- **Numeric Example:**
569
-
570
- **Group A (1,000 people, 200 actually risky):**
571
- - Predicted High Risk: 250 people
572
- - True Positives: 150 / 200 = **75% TPR**
573
- - False Positives: 100 / 800 = 12.5% FPR
574
-
575
- **Group B (1,000 people, 200 actually risky):**
576
- - Predicted High Risk: 400 people
577
- - True Positives: 180 / 200 = **90% TPR** ⚠️ (disparity!)
578
- - False Positives: 220 / 800 = **27.5% FPR** ⚠️ (disparity!)
579
-
580
- **Analysis:** Even though both groups have same base rate (20% risky), Group B is
581
- over-predicted, leading to higher both TPR and FPR. Equal Opportunity would require
582
- equalizing TPR; Equalized Odds would require equalizing both.
583
-
584
- </details>
585
-
586
- ---
587
-
588
- ### Proxy Identification Mini-Game
589
-
590
- Rank these variables by how likely they are to serve as proxies for race (1 = strongest proxy):
591
- """
592
- )
593
-
594
- neighborhood_rank = gr.Radio(
595
- choices=["1", "2", "3"],
596
- label="Neighborhood ZIP code:",
597
- value=None
598
- )
599
- priors_rank = gr.Radio(
600
- choices=["1", "2", "3"],
601
- label="Prior arrests (count):",
602
- value=None
603
- )
604
- income_rank = gr.Radio(
605
- choices=["1", "2", "3"],
606
- label="Income level:",
607
- value=None
608
- )
609
-
610
- rank_btn = gr.Button("Check My Ranking", variant="primary")
611
- rank_feedback = gr.Markdown("")
612
-
613
- rank_btn.click(
614
- fn=rank_proxy_variables,
615
- inputs=[neighborhood_rank, income_rank, priors_rank],
616
- outputs=rank_feedback
617
- ).then(
618
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
619
- outputs=moral_compass_display
620
- )
621
-
622
- gr.Markdown("### Remove Proxy Variables")
623
- remove_proxy_btn = gr.Button("Remove Proxy Variables & Reanalyze", variant="primary")
624
- proxy_removal_output = gr.Markdown("")
625
-
626
- remove_proxy_btn.click(
627
- fn=remove_proxy_variables,
628
- outputs=proxy_removal_output
629
- ).then(
630
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
631
- outputs=moral_compass_display
632
- )
633
-
634
- gr.Markdown("### Check-In Question")
635
- proxy_question = gr.Radio(
636
- choices=[
637
- "Email address format",
638
- "ZIP code in a city with segregated neighborhoods",
639
- "Browser type",
640
- "Time of day of arrest"
641
- ],
642
- label="Which feature is most likely to be a proxy for race?",
643
- value=None
644
- )
645
- proxy_check_btn = gr.Button("Check Answer")
646
- proxy_feedback = gr.Markdown("")
647
-
648
- proxy_check_btn.click(
649
- fn=check_proxy_question,
650
- inputs=proxy_question,
651
- outputs=proxy_feedback
652
- ).then(
653
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
654
- outputs=moral_compass_display
655
- )
656
-
657
- # Section 8.4: Representative Data
658
- with gr.Tab("8.4 Representative Data"):
659
- gr.Markdown(
660
- """
661
- ## Representative Data Strategy
662
-
663
- **Problem:** Models trained on unrepresentative data may not work fairly for all groups.
664
-
665
- **Solution:** Ensure training data matches the intended population.
666
-
667
- ### Expert Chat: Building Representative Datasets
668
-
669
- Below is a conversation between experts discussing data collection best practices:
670
- """
671
- )
672
-
673
- gr.Markdown(
674
- """
675
- ---
676
-
677
- **👨‍💻 Data Scientist:** "We have 10,000 records from City A. Can we deploy this model nationwide?"
678
-
679
- **👩‍⚖️ Judge:** "City A has unique demographics. What about rural areas? Different regions?"
680
-
681
- **👥 Community Member:** "Our neighborhood isn't represented. The model may not understand our context."
682
-
683
- **👨‍💻 Data Scientist:** "You're right. We need geographic, demographic, and temporal balance. Let's create guidelines."
684
-
685
- ---
686
- """
687
- )
688
-
689
- guidelines_btn = gr.Button("Generate Representative Data Guidelines", variant="primary")
690
- guidelines_output = gr.Markdown("")
691
-
692
- guidelines_btn.click(
693
- fn=generate_data_guidelines,
694
- outputs=guidelines_output
695
- ).then(
696
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
697
- outputs=moral_compass_display
698
- )
699
-
700
- gr.Markdown("### Check-In Questions")
701
-
702
- rep_data_question = gr.Radio(
703
- choices=[
704
- "Data from any available source",
705
- "Training data that mirrors the demographics and conditions of the target population",
706
- "The largest dataset available",
707
- "Data collected only from cooperative participants"
708
- ],
709
- label="What is representative data?",
710
- value=None
711
- )
712
- rep_data_btn = gr.Button("Check Answer")
713
- rep_data_feedback = gr.Markdown("")
714
-
715
- rep_data_btn.click(
716
- fn=check_representative_data_question,
717
- inputs=rep_data_question,
718
- outputs=rep_data_feedback
719
- ).then(
720
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
721
- outputs=moral_compass_display
722
- )
723
-
724
- geo_question = gr.Radio(
725
- choices=[
726
- "Yes - data is data regardless of source",
727
- "No - the model may not generalize well to different geographic contexts",
728
- "Yes - as long as the dataset is large enough",
729
- "It doesn't matter where data comes from"
730
- ],
731
- label="A model trained on urban data from California - can it be fairly deployed in rural Texas?",
732
- value=None
733
- )
734
- geo_btn = gr.Button("Check Answer")
735
- geo_feedback = gr.Markdown("")
736
-
737
- geo_btn.click(
738
- fn=check_geographic_question,
739
- inputs=geo_question,
740
- outputs=geo_feedback
741
- ).then(
742
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
743
- outputs=moral_compass_display
744
- )
745
-
746
- # Section 8.5: Continuous Improvement Plan
747
- with gr.Tab("8.5 Improvement Plan"):
748
- gr.Markdown(
749
- """
750
- ## Continuous Improvement Plan
751
-
752
- **Goal:** Create an ongoing process for auditing, documentation, and stakeholder engagement.
753
-
754
- ### Ethical Roadmap Builder
755
-
756
- Put these steps in the correct order (1 = first, 3 = last) for a responsible model lifecycle:
757
- """
758
- )
759
-
760
- audit_order = gr.Radio(
761
- choices=["1", "2", "3"],
762
- label="Regular fairness auditing:",
763
- value=None
764
- )
765
- doc_order = gr.Radio(
766
- choices=["1", "2", "3"],
767
- label="Transparent documentation (model cards):",
768
- value=None
769
- )
770
- stakeholder_order = gr.Radio(
771
- choices=["1", "2", "3"],
772
- label="Ongoing stakeholder engagement:",
773
- value=None
774
- )
775
-
776
- roadmap_btn = gr.Button("Build Improvement Plan", variant="primary")
777
- roadmap_output = gr.Markdown("")
778
-
779
- roadmap_btn.click(
780
- fn=build_improvement_plan,
781
- inputs=[audit_order, doc_order, stakeholder_order],
782
- outputs=roadmap_output
783
- ).then(
784
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
785
- outputs=moral_compass_display
786
- )
787
-
788
- gr.Markdown("### Check-In Question")
789
- model_card_question = gr.Radio(
790
- choices=[
791
- "The model's source code",
792
- "Fairness metrics across demographic groups",
793
- "The names of data scientists who built it",
794
- "Hardware requirements"
795
- ],
796
- label="What is the most critical information for a judge's model card?",
797
- value=None
798
- )
799
- model_card_btn = gr.Button("Check Answer")
800
- model_card_feedback = gr.Markdown("")
801
-
802
- model_card_btn.click(
803
- fn=check_model_card_question,
804
- inputs=model_card_question,
805
- outputs=model_card_feedback
806
- ).then(
807
- fn=lambda: f"## 🧭 Moral Compass Score: {moral_compass_points['value']} points",
808
- outputs=moral_compass_display
809
- )
810
-
811
- # Section 8.6: Completion
812
- # Ethics Leaderboard Tab
813
- with gr.Tab("Ethics Leaderboard"):
814
- gr.Markdown(
815
- """
816
- ## 🏆 Ethics Leaderboard
817
-
818
- This leaderboard shows **combined ethical engagement + performance scores**.
819
-
820
- **What's measured:**
821
- - Moral compass points (fairness engineering skills)
822
- - Model accuracy (technical performance)
823
- - Combined score = accuracy × normalized_moral_points
824
-
825
- **Your progress:**
826
- Track how you compare to others who have also tackled bias and fairness challenges!
827
- """
828
- )
829
-
830
- leaderboard_display = gr.HTML("")
831
- refresh_leaderboard_btn = gr.Button("Refresh Leaderboard", variant="secondary")
832
-
833
- def load_leaderboard():
834
- return build_moral_leaderboard_html(
835
- highlight_username=user_stats.get("username"),
836
- include_teams=True
1475
+ else:
1476
+ rank_line = (
1477
+ f"🔻 Rank: #{old_rank} → <strong>#{new_rank}</strong> "
1478
+ f"({rank_diff} places)"
837
1479
  )
838
-
839
- refresh_leaderboard_btn.click(
840
- fn=load_leaderboard,
841
- outputs=leaderboard_display
1480
+ else:
1481
+ rank_line = f"📊 Rank: <strong>#{new_rank}</strong>"
1482
+
1483
+ return f"""
1484
+ <div class="{card_class}">
1485
+ <div class="success-header">
1486
+ <div>
1487
+ <div class="success-title">{header_emoji} {header_title}</div>
1488
+ <div class="success-summary">{summary_line}</div>
1489
+ </div>
1490
+ <div class="success-delta">
1491
+ +{diff_score:.3f}
1492
+ </div>
1493
+ </div>
1494
+
1495
+ <div class="success-metrics">
1496
+ <div class="success-metric-line">{score_line}</div>
1497
+ <div class="success-metric-line">{rank_line}</div>
1498
+ </div>
1499
+
1500
+ <div class="success-body">
1501
+ <p class="success-body-text">{specific_text}</p>
1502
+ <p class="success-cta">{cta_line}</p>
1503
+ </div>
1504
+ </div>
1505
+ """
1506
+
1507
+ def render_top_dashboard(data, module_id):
1508
+ display_score = 0.0
1509
+ count_completed = 0
1510
+ rank_display = "–"
1511
+ team_rank_display = "–"
1512
+ if data:
1513
+ display_score = float(data.get("score", 0.0))
1514
+ rank_display = f"#{data.get('rank', '–')}"
1515
+ team_rank_display = f"#{data.get('team_rank', '–')}"
1516
+ count_completed = len(data.get("completed_task_ids", []) or [])
1517
+ progress_pct = min(100, int((count_completed / TOTAL_COURSE_TASKS) * 100))
1518
+ return f"""
1519
+ <div class="summary-box">
1520
+ <div class="summary-box-inner">
1521
+ <div class="summary-metrics">
1522
+ <div style="text-align:center;">
1523
+ <div class="label-text">Moral Compass Score</div>
1524
+ <div class="score-text-primary">🧭 {display_score:.3f}</div>
1525
+ </div>
1526
+ <div class="divider-vertical"></div>
1527
+ <div style="text-align:center;">
1528
+ <div class="label-text">Team Rank</div>
1529
+ <div class="score-text-team">{team_rank_display}</div>
1530
+ </div>
1531
+ <div class="divider-vertical"></div>
1532
+ <div style="text-align:center;">
1533
+ <div class="label-text">Global Rank</div>
1534
+ <div class="score-text-global">{rank_display}</div>
1535
+ </div>
1536
+ </div>
1537
+ <div class="summary-progress">
1538
+ <div class="progress-label">Mission Progress: {progress_pct}%</div>
1539
+ <div class="progress-bar-bg">
1540
+ <div class="progress-bar-fill" style="width:{progress_pct}%;"></div>
1541
+ </div>
1542
+ </div>
1543
+ </div>
1544
+ </div>
1545
+ """
1546
+
1547
+ def render_leaderboard_card(data, username, team_name):
1548
+ team_rows = ""
1549
+ user_rows = ""
1550
+ if data and data.get("all_teams"):
1551
+ for i, t in enumerate(data["all_teams"]):
1552
+ cls = "row-highlight-team" if t["team"] == team_name else "row-normal"
1553
+ team_rows += (
1554
+ f"<tr class='{cls}'><td style='padding:8px;text-align:center;'>{i+1}</td>"
1555
+ f"<td style='padding:8px;'>{t['team']}</td>"
1556
+ f"<td style='padding:8px;text-align:right;'>{t['avg']:.3f}</td></tr>"
842
1557
  )
843
-
844
- # Load initially
845
- app.load(fn=load_leaderboard, outputs=leaderboard_display)
846
-
847
- with gr.Tab("8.6 Fix Summary"):
848
- gr.Markdown(
849
- """
850
- ## Generate Your Fairness Fix Summary
851
-
852
- Review all the improvements you've made to the model.
853
- """
1558
+ if data and data.get("all_users"):
1559
+ for i, u in enumerate(data["all_users"]):
1560
+ cls = "row-highlight-me" if u.get("username") == username else "row-normal"
1561
+ sc = float(u.get("moralCompassScore", 0))
1562
+ if u.get("username") == username and data.get("score") != sc:
1563
+ sc = data.get("score")
1564
+ user_rows += (
1565
+ f"<tr class='{cls}'><td style='padding:8px;text-align:center;'>{i+1}</td>"
1566
+ f"<td style='padding:8px;'>{u.get('username','')}</td>"
1567
+ f"<td style='padding:8px;text-align:right;'>{sc:.3f}</td></tr>"
854
1568
  )
855
-
856
- summary_btn = gr.Button("Generate Fix Summary", variant="primary")
857
- summary_output = gr.Markdown("")
858
-
859
- summary_btn.click(
860
- fn=generate_fairness_summary,
861
- outputs=summary_output
1569
+ return f"""
1570
+ <div class="scenario-box leaderboard-card">
1571
+ <h3 class="slide-title" style="margin-bottom:10px;">📊 Live Standings</h3>
1572
+ <div class="lb-tabs">
1573
+ <input type="radio" id="lb-tab-team" name="lb-tabs" checked>
1574
+ <label for="lb-tab-team" class="lb-tab-label">🏆 Team</label>
1575
+ <input type="radio" id="lb-tab-user" name="lb-tabs">
1576
+ <label for="lb-tab-user" class="lb-tab-label">👤 Individual</label>
1577
+ <div class="lb-tab-panels">
1578
+ <div class="lb-panel panel-team">
1579
+ <div class='table-container'>
1580
+ <table class='leaderboard-table'>
1581
+ <thead>
1582
+ <tr><th>Rank</th><th>Team</th><th style='text-align:right;'>Avg 🧭</th></tr>
1583
+ </thead>
1584
+ <tbody>{team_rows}</tbody>
1585
+ </table>
1586
+ </div>
1587
+ </div>
1588
+ <div class="lb-panel panel-user">
1589
+ <div class='table-container'>
1590
+ <table class='leaderboard-table'>
1591
+ <thead>
1592
+ <tr><th>Rank</th><th>Agent</th><th style='text-align:right;'>Score 🧭</th></tr>
1593
+ </thead>
1594
+ <tbody>{user_rows}</tbody>
1595
+ </table>
1596
+ </div>
1597
+ </div>
1598
+ </div>
1599
+ </div>
1600
+ </div>
1601
+ """
1602
+
1603
+ # --- 9. APP FACTORY (FAIRNESS FIXER) ---
1604
+ def create_fairness_fixer_app(theme_primary_hue: str = "indigo"):
1605
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue=theme_primary_hue), css=css) as demo:
1606
+ # States
1607
+ username_state = gr.State(value=None)
1608
+ token_state = gr.State(value=None)
1609
+ team_state = gr.State(value=None)
1610
+ accuracy_state = gr.State(value=0.0)
1611
+ task_list_state = gr.State(value=[])
1612
+
1613
+ # --- TOP ANCHOR & LOADING OVERLAY ---
1614
+ gr.HTML("<div id='app_top_anchor' style='height:0;'></div>")
1615
+ gr.HTML("<div id='nav-loading-overlay'><div class='nav-spinner'></div><span id='nav-loading-text'>Loading...</span></div>")
1616
+
1617
+ # --- LOADING VIEW ---
1618
+ with gr.Column(visible=True, elem_id="app-loader") as loader_col:
1619
+ gr.HTML(
1620
+ "<div style='text-align:center; padding:100px;'>"
1621
+ "<h2>🕵️‍♀️ Authenticating...</h2>"
1622
+ "<p>Syncing Fairness Engineer Profile...</p>"
1623
+ "</div>"
862
1624
  )
863
-
864
- gr.Markdown(
865
- """
866
- ---
867
-
868
- ### 🎉 Activity 8 Complete!
869
-
870
- **Next Step:** Proceed to **Activity 9: Justice & Equity Upgrade** to elevate your fairness improvements.
871
- """
1625
+
1626
+ # --- MAIN APP VIEW ---
1627
+ with gr.Column(visible=False) as main_app_col:
1628
+ # Top summary dashboard
1629
+ out_top = gr.HTML()
1630
+
1631
+ # Dynamic modules container
1632
+ module_ui_elements = {}
1633
+ quiz_wiring_queue = []
1634
+
1635
+ # --- DYNAMIC MODULE GENERATION ---
1636
+ for i, mod in enumerate(MODULES):
1637
+ with gr.Column(
1638
+ elem_id=f"module-{i}",
1639
+ elem_classes=["module-container"],
1640
+ visible=(i == 0),
1641
+ ) as mod_col:
1642
+ # Core slide HTML
1643
+ gr.HTML(mod["html"])
1644
+
1645
+ # --- QUIZ CONTENT ---
1646
+ if i in QUIZ_CONFIG:
1647
+ q_data = QUIZ_CONFIG[i]
1648
+ gr.Markdown(f"### 🧠 {q_data['q']}")
1649
+ radio = gr.Radio(
1650
+ choices=q_data["o"],
1651
+ label="Select Action:",
1652
+ elem_classes=["quiz-radio-large"],
1653
+ )
1654
+ feedback = gr.HTML("")
1655
+ quiz_wiring_queue.append((i, radio, feedback))
1656
+
1657
+ # --- NAVIGATION BUTTONS ---
1658
+ with gr.Row():
1659
+ btn_prev = gr.Button("⬅️ Previous", visible=(i > 0))
1660
+ next_label = (
1661
+ "Next ▶️"
1662
+ if i < len(MODULES) - 1
1663
+ else "🎉 Model Authorized! Scroll Down to Receive your official 'Ethics at Play' Certificate!"
1664
+ )
1665
+ btn_next = gr.Button(next_label, variant="primary")
1666
+
1667
+ module_ui_elements[i] = (mod_col, btn_prev, btn_next)
1668
+
1669
+ # Leaderboard card
1670
+ leaderboard_html = gr.HTML()
1671
+
1672
+ # --- WIRING: QUIZ LOGIC ---
1673
+ for mod_id, radio_comp, feedback_comp in quiz_wiring_queue:
1674
+ def quiz_logic_wrapper(user, tok, team, acc_val, task_list, ans, mid=mod_id):
1675
+ cfg = QUIZ_CONFIG[mid]
1676
+ if ans == cfg["a"]:
1677
+ prev, curr, _, new_tasks = trigger_api_update(
1678
+ user, tok, team, mid, acc_val, task_list, cfg["t"]
1679
+ )
1680
+ msg = generate_success_message(prev, curr, cfg["success"])
1681
+ return (
1682
+ render_top_dashboard(curr, mid),
1683
+ render_leaderboard_card(curr, user, team),
1684
+ msg,
1685
+ new_tasks,
1686
+ )
1687
+ else:
1688
+ return (
1689
+ gr.update(),
1690
+ gr.update(),
1691
+ "<div class='hint-box' style='border-color:red;'>❌ Incorrect. Try again.</div>",
1692
+ task_list,
1693
+ )
1694
+
1695
+ radio_comp.change(
1696
+ fn=quiz_logic_wrapper,
1697
+ inputs=[username_state, token_state, team_state, accuracy_state, task_list_state, radio_comp],
1698
+ outputs=[out_top, leaderboard_html, feedback_comp, task_list_state],
1699
+ )
1700
+
1701
+ # --- GLOBAL LOAD HANDLER ---
1702
+ def handle_load(req: gr.Request):
1703
+ success, user, token = _try_session_based_auth(req)
1704
+ team = "Team-Unassigned"
1705
+ acc = 0.0
1706
+ fetched_tasks: List[str] = []
1707
+
1708
+ if success and user and token:
1709
+ acc, fetched_team = fetch_user_history(user, token)
1710
+ os.environ["MORAL_COMPASS_API_BASE_URL"] = DEFAULT_API_URL
1711
+ client = MoralcompassApiClient(api_base_url=DEFAULT_API_URL, auth_token=token)
1712
+
1713
+ def get_or_assign_team(client_obj, username_val):
1714
+ try:
1715
+ user_data = client_obj.get_user(table_id=TABLE_ID, username=username_val)
1716
+ except Exception:
1717
+ user_data = None
1718
+ if user_data and isinstance(user_data, dict):
1719
+ if user_data.get("teamName"):
1720
+ return user_data["teamName"]
1721
+ return "team-a"
1722
+
1723
+ exist_team = get_or_assign_team(client, user)
1724
+ if fetched_team != "Team-Unassigned":
1725
+ team = fetched_team
1726
+ elif exist_team != "team-a":
1727
+ team = exist_team
1728
+ else:
1729
+ team = "team-a"
1730
+
1731
+ try:
1732
+ user_stats = client.get_user(table_id=TABLE_ID, username=user)
1733
+ except Exception:
1734
+ user_stats = None
1735
+
1736
+ if user_stats:
1737
+ if isinstance(user_stats, dict):
1738
+ fetched_tasks = user_stats.get("completedTaskIds") or []
1739
+ else:
1740
+ fetched_tasks = getattr(user_stats, "completed_task_ids", []) or []
1741
+
1742
+ try:
1743
+ client.update_moral_compass(
1744
+ table_id=TABLE_ID,
1745
+ username=user,
1746
+ team_name=team,
1747
+ metrics={"accuracy": acc},
1748
+ tasks_completed=len(fetched_tasks),
1749
+ total_tasks=TOTAL_COURSE_TASKS,
1750
+ primary_metric="accuracy",
1751
+ completed_task_ids=fetched_tasks,
1752
+ )
1753
+ time.sleep(1.0)
1754
+ except Exception:
1755
+ pass
1756
+
1757
+ data, _ = ensure_table_and_get_data(user, token, team, fetched_tasks)
1758
+ return (
1759
+ user, token, team, False,
1760
+ render_top_dashboard(data, 0),
1761
+ render_leaderboard_card(data, user, team),
1762
+ acc, fetched_tasks,
1763
+ gr.update(visible=False), gr.update(visible=True),
1764
+ )
1765
+
1766
+ return (
1767
+ None, None, None, False,
1768
+ "<div class='hint-box'>⚠️ Auth Failed. Please launch from the course link.</div>",
1769
+ "", 0.0, [],
1770
+ gr.update(visible=False), gr.update(visible=True),
872
1771
  )
873
1772
 
874
- return app
1773
+ demo.load(
1774
+ handle_load, None,
1775
+ [username_state, token_state, team_state, gr.State(False), out_top, leaderboard_html, accuracy_state, task_list_state, loader_col, main_app_col],
1776
+ )
1777
+
1778
+ # --- JAVASCRIPT NAVIGATION ---
1779
+ def nav_js(target_id: str, message: str) -> str:
1780
+ return f"""
1781
+ ()=>{{
1782
+ try {{
1783
+ const overlay = document.getElementById('nav-loading-overlay');
1784
+ const messageEl = document.getElementById('nav-loading-text');
1785
+ if(overlay && messageEl) {{
1786
+ messageEl.textContent = '{message}';
1787
+ overlay.style.display = 'flex';
1788
+ setTimeout(() => {{ overlay.style.opacity = '1'; }}, 10);
1789
+ }}
1790
+ const startTime = Date.now();
1791
+ setTimeout(() => {{
1792
+ const anchor = document.getElementById('app_top_anchor');
1793
+ if(anchor) anchor.scrollIntoView({{behavior:'smooth', block:'start'}});
1794
+ }}, 40);
1795
+ const targetId = '{target_id}';
1796
+ const pollInterval = setInterval(() => {{
1797
+ const elapsed = Date.now() - startTime;
1798
+ const target = document.getElementById(targetId);
1799
+ const isVisible = target && target.offsetParent !== null &&
1800
+ window.getComputedStyle(target).display !== 'none';
1801
+ if((isVisible && elapsed >= 1200) || elapsed > 7000) {{
1802
+ clearInterval(pollInterval);
1803
+ if(overlay) {{
1804
+ overlay.style.opacity = '0';
1805
+ setTimeout(() => {{ overlay.style.display = 'none'; }}, 300);
1806
+ }}
1807
+ }}
1808
+ }}, 90);
1809
+ }} catch(e) {{ console.warn('nav-js error', e); }}
1810
+ }}
1811
+ """
1812
+
1813
+ # --- NAV BUTTON WIRING ---
1814
+ for i in range(len(MODULES)):
1815
+ curr_col, prev_btn, next_btn = module_ui_elements[i]
1816
+ if i > 0:
1817
+ prev_col = module_ui_elements[i - 1][0]
1818
+ prev_target_id = f"module-{i-1}"
1819
+ def make_prev_handler(p_col, c_col):
1820
+ def navigate_prev():
1821
+ yield gr.update(visible=False), gr.update(visible=False)
1822
+ yield gr.update(visible=True), gr.update(visible=False)
1823
+ return navigate_prev
1824
+ prev_btn.click(
1825
+ fn=make_prev_handler(prev_col, curr_col),
1826
+ outputs=[prev_col, curr_col],
1827
+ js=nav_js(prev_target_id, "Loading..."),
1828
+ )
1829
+
1830
+ if i < len(MODULES) - 1:
1831
+ next_col = module_ui_elements[i + 1][0]
1832
+ next_target_id = f"module-{i+1}"
1833
+ def make_next_handler(c_col, n_col, next_idx):
1834
+ def wrapper_next(user, tok, team, tasks):
1835
+ data, _ = ensure_table_and_get_data(user, tok, team, tasks)
1836
+ return render_top_dashboard(data, next_idx)
1837
+ return wrapper_next
1838
+ def make_nav_generator(c_col, n_col):
1839
+ def navigate_next():
1840
+ yield gr.update(visible=False), gr.update(visible=False)
1841
+ yield gr.update(visible=False), gr.update(visible=True)
1842
+ return navigate_next
1843
+ next_btn.click(
1844
+ fn=make_next_handler(curr_col, next_col, i + 1),
1845
+ inputs=[username_state, token_state, team_state, task_list_state],
1846
+ outputs=[out_top],
1847
+ js=nav_js(next_target_id, "Loading..."),
1848
+ ).then(
1849
+ fn=make_nav_generator(curr_col, next_col),
1850
+ outputs=[curr_col, next_col],
1851
+ )
875
1852
 
1853
+ return demo
876
1854
 
1855
+ # --- 10. LAUNCHER ---
877
1856
  def launch_fairness_fixer_app(
878
1857
  share: bool = False,
879
- server_name: str = None,
880
- server_port: int = None,
881
- theme_primary_hue: str = "indigo"
1858
+ server_name: str = "0.0.0.0",
1859
+ server_port: int = 8080,
1860
+ theme_primary_hue: str = "indigo",
1861
+ **kwargs
882
1862
  ) -> None:
883
- """Convenience wrapper to create and launch the fairness fixer app inline."""
884
1863
  app = create_fairness_fixer_app(theme_primary_hue=theme_primary_hue)
885
- # Use provided values or fall back to PORT env var and 0.0.0.0
1864
+ app.launch(share=share, server_name=server_name,
1865
+ server_port=server_port,
1866
+ **kwargs)
886
1867
 
887
- if server_port is None:
888
- server_port = int(os.environ.get("PORT", 8080))
889
- app.launch(share=share, sserver_port=server_port)
1868
+ if __name__ == "__main__":
1869
+ launch_fairness_fixer_app(share=False, debug=True, height=1000)