aimodelshare 0.3.7__py3-none-any.whl → 0.3.94__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 +4399 -0
  22. aimodelshare/moral_compass/apps/model_building_app_ca_final.py +3899 -0
  23. aimodelshare/moral_compass/apps/model_building_app_en.py +4167 -0
  24. aimodelshare/moral_compass/apps/model_building_app_en_final.py +3869 -0
  25. aimodelshare/moral_compass/apps/model_building_app_es.py +4351 -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.3.94.dist-info}/METADATA +1 -1
  32. {aimodelshare-0.3.7.dist-info → aimodelshare-0.3.94.dist-info}/RECORD +35 -19
  33. aimodelshare/moral_compass/apps/bias_detective.py +0 -714
  34. {aimodelshare-0.3.7.dist-info → aimodelshare-0.3.94.dist-info}/WHEEL +0 -0
  35. {aimodelshare-0.3.7.dist-info → aimodelshare-0.3.94.dist-info}/licenses/LICENSE +0 -0
  36. {aimodelshare-0.3.7.dist-info → aimodelshare-0.3.94.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1869 @@
1
+ import os
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
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])
19
+
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]]:
37
+ try:
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
50
+ except Exception:
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
+ }
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; }
1198
+
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):
1274
+ try:
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
1292
+ )
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 [])
1302
+ )
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
1319
+ return {
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,
1326
+ }
1327
+ except Exception:
1328
+ return None
1329
+
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"
1417
+ else:
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>"
1459
+ else:
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})"
1465
+ )
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)"
1474
+ )
1475
+ else:
1476
+ rank_line = (
1477
+ f"🔻 Rank: #{old_rank} → <strong>#{new_rank}</strong> "
1478
+ f"({rank_diff} places)"
1479
+ )
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>"
1557
+ )
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>"
1568
+ )
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_ca_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>"
1624
+ )
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),
1771
+ )
1772
+
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
+ )
1852
+
1853
+ return demo
1854
+
1855
+ # --- 10. LAUNCHER ---
1856
+ def launch_fairness_fixer_ca_app(
1857
+ share: bool = False,
1858
+ server_name: str = "0.0.0.0",
1859
+ server_port: int = 8080,
1860
+ theme_primary_hue: str = "indigo",
1861
+ **kwargs
1862
+ ) -> None:
1863
+ app = create_fairness_fixer_ca_app(theme_primary_hue=theme_primary_hue)
1864
+ app.launch(share=share, server_name=server_name,
1865
+ server_port=server_port,
1866
+ **kwargs)
1867
+
1868
+ if __name__ == "__main__":
1869
+ launch_fairness_fixer_ca_app(share=False, debug=True, height=1000)