ProfRick commited on
Commit
9898268
·
verified ·
1 Parent(s): 40822dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +25 -60
app.py CHANGED
@@ -18,7 +18,6 @@ st.set_page_config(page_title="Interdependence Concept Game", page_icon="🧬",
18
  # ==============================
19
  SCENARIOS = {
20
  "Blood Pressure (Hypotension)": [
21
- # Loop 1: Baroreflex (nervous)
22
  {
23
  "loop_name": "Nervous loop (Baroreflex)",
24
  "sensor_options": ["Chemoreceptor", "Mechanoreceptor", "Thermoreceptor"],
@@ -27,19 +26,14 @@ SCENARIOS = {
27
  "control_correct": "Medulla",
28
  "effector_options":["Pancreas", "Skeletal Muscle", "Heart and Blood Vessels"],
29
  "effector_correct":"Heart and Blood Vessels",
30
- # Stage 2: Sensor -> Control
31
  "stage2_desc": "Afferent neurons release neurotransmitters (hydrophilic) across a synapse to the control center neuron.",
32
  "stage2_sig": "Paracrine",
33
  "stage2_rec": "Cell membrane",
34
  "stage2_grad": "Concentration",
35
- "stage2_feedback_wrong": "For synapses, diffusion across a tiny gap is primary (concentration gradient).",
36
- # Stage 3: Control -> Effector
37
  "stage3_desc": "Controller neurons signal effectors via neurotransmitters (hydrophilic) across synapses.",
38
  "stage3_sig": "Paracrine",
39
  "stage3_rec": "Cell membrane",
40
  "stage3_grad": "Concentration",
41
- "stage3_feedback_wrong": "Same synaptic logic: diffusion across a small gap.",
42
- # Stage 4: Outcome
43
  "outcome_question": "What will be the following response?",
44
  "outcome_options": [
45
  "Cardiac output increases and vasoconstriction increases",
@@ -48,7 +42,6 @@ SCENARIOS = {
48
  ],
49
  "outcome_correct": "Cardiac output increases and vasoconstriction increases",
50
  },
51
- # Loop 2: RAAS (renal)
52
  {
53
  "loop_name": "Renal loop (RAAS)",
54
  "sensor_options": ["Juxtaglomerular apparatus (kidney)", "Baroreceptors (carotid sinus)", "Osmoreceptors (hypothalamus)"],
@@ -61,12 +54,10 @@ SCENARIOS = {
61
  "stage2_sig": "Endocrine",
62
  "stage2_rec": "Cell membrane",
63
  "stage2_grad": "Pressure",
64
- "stage2_feedback_wrong": "Primary long-distance transport in blood is pressure-driven bulk flow; diffusion matters locally.",
65
  "stage3_desc": "Aldosterone (lipophilic steroid) travels **in the blood circulation** to tubular cells.",
66
  "stage3_sig": "Endocrine",
67
  "stage3_rec": "Inside the cell",
68
  "stage3_grad": "Pressure",
69
- "stage3_feedback_wrong": "Again, long-distance delivery in blood is pressure-driven; diffusion is local.",
70
  "outcome_question": "What will be the following response?",
71
  "outcome_options": [
72
  "Sodium & water reabsorption increases",
@@ -78,7 +69,6 @@ SCENARIOS = {
78
  ],
79
 
80
  "Glucose (Post-meal Hyperglycemia)": [
81
- # Loop 1: Insulin
82
  {
83
  "loop_name": "Insulin loop",
84
  "sensor_options": ["Pancreatic beta cells", "Pancreatic alpha cells", "Chemoreceptors (carotid)"],
@@ -91,12 +81,10 @@ SCENARIOS = {
91
  "stage2_sig": "Endocrine",
92
  "stage2_rec": "Cell membrane",
93
  "stage2_grad": "Pressure",
94
- "stage2_feedback_wrong":"For long-distance delivery in blood, the primary transport is pressure-driven bulk flow.",
95
  "stage3_desc": "The messenger reaches distant tissues via circulation to increase glucose uptake.",
96
  "stage3_sig": "Endocrine",
97
  "stage3_rec": "Cell membrane",
98
  "stage3_grad": "Pressure",
99
- "stage3_feedback_wrong":"Primary transport in blood is pressure-driven.",
100
  "outcome_question": "What will be the following response?",
101
  "outcome_options": [
102
  "Cellular glucose uptake increases",
@@ -105,7 +93,6 @@ SCENARIOS = {
105
  ],
106
  "outcome_correct": "Cellular glucose uptake increases",
107
  },
108
- # Loop 2: Kidney overflow/excretion
109
  {
110
  "loop_name": "Kidney excretion loop (overflow)",
111
  "sensor_options": ["Kidney (proximal tubule)", "Pancreatic beta cells", "Baroreceptors (carotid)"],
@@ -122,12 +109,10 @@ SCENARIOS = {
122
  "stage2_sig": "Paracrine",
123
  "stage2_rec": "Cell membrane",
124
  "stage2_grad": "Concentration",
125
- "stage2_feedback_wrong":"Local paracrine movement is diffusion-dominant (concentration gradient).",
126
  "stage3_desc": "Local control continues at transporters within the same tissue.",
127
  "stage3_sig": "Paracrine",
128
  "stage3_rec": "Cell membrane",
129
  "stage3_grad": "Concentration",
130
- "stage3_feedback_wrong":"Local paracrine movement is diffusion-dominant.",
131
  "outcome_question": "What will be the following response?",
132
  "outcome_options": [
133
  "Glucose excretion into the urine increases",
@@ -139,7 +124,6 @@ SCENARIOS = {
139
  ],
140
 
141
  "Temperature (Hypothermia)": [
142
- # Loop 1: Shivering (somatic motor)
143
  {
144
  "loop_name": "Shivering loop (somatic motor)",
145
  "sensor_options": ["Thermoreceptor", "Mechanoreceptor", "Chemoreceptor"],
@@ -152,12 +136,10 @@ SCENARIOS = {
152
  "stage2_sig": "Paracrine",
153
  "stage2_rec": "Cell membrane",
154
  "stage2_grad": "Concentration",
155
- "stage2_feedback_wrong":"Synaptic transmission uses diffusion across a tiny space (concentration gradient).",
156
  "stage3_desc": "Motor pathways direct skeletal muscle via neurotransmitters across synapses.",
157
  "stage3_sig": "Paracrine",
158
  "stage3_rec": "Cell membrane",
159
  "stage3_grad": "Concentration",
160
- "stage3_feedback_wrong":"Synaptic diffusion across a small gap.",
161
  "outcome_question": "What will be the following response?",
162
  "outcome_options": [
163
  "Muscle contraction",
@@ -166,7 +148,6 @@ SCENARIOS = {
166
  ],
167
  "outcome_correct": "Muscle contraction",
168
  },
169
- # Loop 2: Skin vessels
170
  {
171
  "loop_name": "Skin vessel loop (vasoconstriction/vasodilation)",
172
  "sensor_options": ["Thermoreceptor", "Mechanoreceptor", "Chemoreceptor"],
@@ -179,12 +160,10 @@ SCENARIOS = {
179
  "stage2_sig": "Paracrine",
180
  "stage2_rec": "Cell membrane",
181
  "stage2_grad": "Concentration",
182
- "stage2_feedback_wrong":"Synaptic diffusion dominates at short range.",
183
  "stage3_desc": "Autonomic outputs alter peripheral vessel tone via neurotransmitters (synaptic).",
184
  "stage3_sig": "Paracrine",
185
  "stage3_rec": "Cell membrane",
186
  "stage3_grad": "Concentration",
187
- "stage3_feedback_wrong":"Local neurotransmitter diffusion across synapses.",
188
  "outcome_question": "What will be the following response?",
189
  "outcome_options": [
190
  "Cutaneous vasoconstriction increases",
@@ -207,15 +186,15 @@ def init_state():
207
  if "assign" not in st.session_state:
208
  st.session_state.assign = {"sensor": None, "control": None, "effector": None}
209
  if "stage" not in st.session_state:
210
- st.session_state.stage = 1 # 1..4
211
  if "stage_token" not in st.session_state:
212
- st.session_state.stage_token = None # set when Stage 1 is passed
213
  if "msgs" not in st.session_state:
214
  st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""}
215
  if "progress" not in st.session_state:
216
  st.session_state.progress = {sc: [False]*len(SCENARIOS[sc]) for sc in SCENARIOS}
217
  if "nonce" not in st.session_state:
218
- st.session_state.nonce = 0 # forces unique widget keys per loop
219
 
220
  def current_loop():
221
  return SCENARIOS[st.session_state.scenario][st.session_state.loop_idx]
@@ -237,7 +216,7 @@ def reset_loop():
237
  st.session_state.stage = 1
238
  st.session_state.stage_token = None
239
  st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""}
240
- st.session_state.nonce += 1 # ensures every widget key is new
241
 
242
  def set_scenario(name):
243
  st.session_state.scenario = name
@@ -262,19 +241,16 @@ def all_loops_complete_for_current_scenario() -> bool:
262
  # Diagram (arrows removed)
263
  # ==============================
264
  def diagram_html(sensor_txt, control_txt, effector_txt):
265
- # Box geometry
266
  sx, sy, sw, sh = 110, 260, 240, 56 # Sensor (left)
267
  cx, cy, cw, ch = 420, 70, 220, 56 # Control (top)
268
  ex, ey, ew, eh = 740, 320, 260, 56 # Effector (right)
269
 
270
- # Baseline
271
  base_x, base_y, base_w, base_h = 330, 520, 360, 26
272
  base_mid_x = base_x + base_w/2
273
 
274
  html = f"""
275
  <div style="position:relative;width:100%;background:#f6f5ff;border:1px solid #e3e3f8;border-radius:16px;overflow:hidden;">
276
  <svg viewBox="0 0 1000 620" style="width:100%;height:auto;display:block" preserveAspectRatio="xMidYMid meet">
277
- <!-- Baseline -->
278
  <rect x="{base_x}" y="{base_y}" width="{base_w}" height="{base_h}" rx="8"
279
  fill="none" stroke="#4b2bb3" stroke-width="5"/>
280
  <polygon points="{base_mid_x - 20},{base_y + base_h + 2}
@@ -286,7 +262,6 @@ def diagram_html(sensor_txt, control_txt, effector_txt):
286
  <text x="{base_x + base_w - 70}" y="{base_y + base_h - 8}" font-size="16"
287
  text-anchor="middle" fill="#4b2bb3">Balance</text>
288
 
289
- <!-- Boxes (no arrows) -->
290
  <rect x="{sx}" y="{sy}" width="{sw}" height="{sh}" rx="12" fill="#e9ecff" stroke="#6b57e5"/>
291
  <text x="{sx+sw/2}" y="{sy+sh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{sensor_txt}</text>
292
 
@@ -343,7 +318,6 @@ def init_and_render():
343
 
344
  st.title("🧩 Interdependence Concept Game")
345
 
346
- # Scenario select (reruns on change)
347
  scenario_list = list(SCENARIOS.keys())
348
  idx = scenario_list.index(st.session_state.scenario)
349
 
@@ -359,25 +333,24 @@ def init_and_render():
359
  reset_loop()
360
  safe_rerun()
361
 
362
- # Current loop & gating tokens/keys
363
  cloop = current_loop()
364
  token = loop_token()
365
  ksfx = key_suffix()
366
 
367
  st.subheader(f"Stage 1 · Build the negative feedback loop — **{cloop['loop_name']}**")
368
 
369
- # Diagram render (no arrows) — give it a unique key so it refreshes cleanly
370
  labels = st.session_state.assign
371
  html = diagram_html(
372
  labels['sensor'] or "Sensor",
373
  labels['control'] or "Control Center",
374
  labels['effector'] or "Effector(s)"
375
  )
376
- components.html(html, height=660, scrolling=False, key=f"diagram_{ksfx}")
377
 
378
  st.markdown("---")
379
 
380
- # ------- Stage 1 (only this stage shows until passed) -------
381
  st.write("**Sensor options**")
382
  sel_s = st.radio("Sensor", cloop["sensor_options"], index=None, horizontal=True, key=f"s1_sensor_{ksfx}")
383
  if sel_s is not None: st.session_state.assign["sensor"] = sel_s
@@ -392,24 +365,24 @@ def init_and_render():
392
 
393
  if st.button("Check Stage 1", key=f"chk1_{ksfx}"):
394
  a = st.session_state.assign
395
- missing = [k for k,v in a.items() if not v]
396
- if missing:
397
- st.session_state.msgs["s1"] = "Place all three answers."
398
  else:
399
- wrong = []
400
- if a["sensor"] != cloop["sensor_correct"]: wrong.append("Sensor")
401
- if a["control"] != cloop["control_correct"]: wrong.append("Control Center")
402
- if a["effector"]!= cloop["effector_correct"]:wrong.append("Effector(s)")
403
- if wrong:
404
- st.session_state.msgs["s1"] = "Not quite. Check: " + ", ".join(wrong)
405
- else:
406
  st.session_state.msgs["s1"] = "Great! Proceed to Stage 2."
407
  st.session_state.stage = 2
408
- st.session_state.stage_token = token # unlock later stages for THIS loop only
409
  safe_rerun()
 
 
410
  st.info(st.session_state.msgs["s1"])
411
 
412
- # Helper: show only one stage at a time and only for this loop
413
  def show_stage(n: int) -> bool:
414
  return (st.session_state.stage == n) and (st.session_state.stage_token == token if n >= 2 else True)
415
 
@@ -422,16 +395,12 @@ def init_and_render():
422
  grd = st.radio("Gradient (primary transport)", ["Concentration","Electrochemical","Pressure"], index=None, horizontal=True, key=f"st2_grad_{ksfx}")
423
 
424
  if st.button("Check Stage 2", key=f"chk2_{ksfx}"):
425
- ok = (sig==cloop["stage2_sig"] and rec==cloop["stage2_rec"] and grd==cloop["stage2_grad"])
426
- if ok:
427
  st.session_state.msgs["s2"] = "Correct! Continue to Stage 3."
428
  st.session_state.stage = 3
429
  safe_rerun()
430
  else:
431
- msg = "Check again."
432
- if "stage2_feedback_wrong" in cloop:
433
- msg += " " + cloop["stage2_feedback_wrong"]
434
- st.session_state.msgs["s2"] = msg
435
  st.info(st.session_state.msgs["s2"])
436
 
437
  # ------- Stage 3 -------
@@ -443,16 +412,12 @@ def init_and_render():
443
  grd3 = st.radio("Gradient (primary transport)", ["Concentration","Electrochemical","Pressure"], index=None, horizontal=True, key=f"st3_grad_{ksfx}")
444
 
445
  if st.button("Check Stage 3", key=f"chk3_{ksfx}"):
446
- ok = (sig3==cloop["stage3_sig"] and rec3==cloop["stage3_rec"] and grd3==cloop["stage3_grad"])
447
- if ok:
448
  st.session_state.msgs["s3"] = "Nice! Final question…"
449
  st.session_state.stage = 4
450
  safe_rerun()
451
  else:
452
- msg = "Try again."
453
- if "stage3_feedback_wrong" in cloop:
454
- msg += " " + cloop["stage3_feedback_wrong"]
455
- st.session_state.msgs["s3"] = msg
456
  st.info(st.session_state.msgs["s3"])
457
 
458
  # ------- Stage 4 -------
@@ -465,12 +430,12 @@ def init_and_render():
465
  st.session_state.msgs["s4"] = "✅ Correct."
466
  next_loop_or_finish() # resets & advances; only Stage 1 will show next
467
  else:
468
- st.session_state.msgs["s4"] = " Not quite—revisit the loop."
469
  st.info(st.session_state.msgs["s4"])
470
 
471
  st.markdown("---")
472
 
473
- # Certificate (per-scenario after both loops complete)
474
  if all_loops_complete_for_current_scenario() and REPORTLAB_AVAILABLE:
475
  st.success("Scenario complete. Generate your PDF certificate below.")
476
  student_name = st.text_input("Student name for certificate", "")
 
18
  # ==============================
19
  SCENARIOS = {
20
  "Blood Pressure (Hypotension)": [
 
21
  {
22
  "loop_name": "Nervous loop (Baroreflex)",
23
  "sensor_options": ["Chemoreceptor", "Mechanoreceptor", "Thermoreceptor"],
 
26
  "control_correct": "Medulla",
27
  "effector_options":["Pancreas", "Skeletal Muscle", "Heart and Blood Vessels"],
28
  "effector_correct":"Heart and Blood Vessels",
 
29
  "stage2_desc": "Afferent neurons release neurotransmitters (hydrophilic) across a synapse to the control center neuron.",
30
  "stage2_sig": "Paracrine",
31
  "stage2_rec": "Cell membrane",
32
  "stage2_grad": "Concentration",
 
 
33
  "stage3_desc": "Controller neurons signal effectors via neurotransmitters (hydrophilic) across synapses.",
34
  "stage3_sig": "Paracrine",
35
  "stage3_rec": "Cell membrane",
36
  "stage3_grad": "Concentration",
 
 
37
  "outcome_question": "What will be the following response?",
38
  "outcome_options": [
39
  "Cardiac output increases and vasoconstriction increases",
 
42
  ],
43
  "outcome_correct": "Cardiac output increases and vasoconstriction increases",
44
  },
 
45
  {
46
  "loop_name": "Renal loop (RAAS)",
47
  "sensor_options": ["Juxtaglomerular apparatus (kidney)", "Baroreceptors (carotid sinus)", "Osmoreceptors (hypothalamus)"],
 
54
  "stage2_sig": "Endocrine",
55
  "stage2_rec": "Cell membrane",
56
  "stage2_grad": "Pressure",
 
57
  "stage3_desc": "Aldosterone (lipophilic steroid) travels **in the blood circulation** to tubular cells.",
58
  "stage3_sig": "Endocrine",
59
  "stage3_rec": "Inside the cell",
60
  "stage3_grad": "Pressure",
 
61
  "outcome_question": "What will be the following response?",
62
  "outcome_options": [
63
  "Sodium & water reabsorption increases",
 
69
  ],
70
 
71
  "Glucose (Post-meal Hyperglycemia)": [
 
72
  {
73
  "loop_name": "Insulin loop",
74
  "sensor_options": ["Pancreatic beta cells", "Pancreatic alpha cells", "Chemoreceptors (carotid)"],
 
81
  "stage2_sig": "Endocrine",
82
  "stage2_rec": "Cell membrane",
83
  "stage2_grad": "Pressure",
 
84
  "stage3_desc": "The messenger reaches distant tissues via circulation to increase glucose uptake.",
85
  "stage3_sig": "Endocrine",
86
  "stage3_rec": "Cell membrane",
87
  "stage3_grad": "Pressure",
 
88
  "outcome_question": "What will be the following response?",
89
  "outcome_options": [
90
  "Cellular glucose uptake increases",
 
93
  ],
94
  "outcome_correct": "Cellular glucose uptake increases",
95
  },
 
96
  {
97
  "loop_name": "Kidney excretion loop (overflow)",
98
  "sensor_options": ["Kidney (proximal tubule)", "Pancreatic beta cells", "Baroreceptors (carotid)"],
 
109
  "stage2_sig": "Paracrine",
110
  "stage2_rec": "Cell membrane",
111
  "stage2_grad": "Concentration",
 
112
  "stage3_desc": "Local control continues at transporters within the same tissue.",
113
  "stage3_sig": "Paracrine",
114
  "stage3_rec": "Cell membrane",
115
  "stage3_grad": "Concentration",
 
116
  "outcome_question": "What will be the following response?",
117
  "outcome_options": [
118
  "Glucose excretion into the urine increases",
 
124
  ],
125
 
126
  "Temperature (Hypothermia)": [
 
127
  {
128
  "loop_name": "Shivering loop (somatic motor)",
129
  "sensor_options": ["Thermoreceptor", "Mechanoreceptor", "Chemoreceptor"],
 
136
  "stage2_sig": "Paracrine",
137
  "stage2_rec": "Cell membrane",
138
  "stage2_grad": "Concentration",
 
139
  "stage3_desc": "Motor pathways direct skeletal muscle via neurotransmitters across synapses.",
140
  "stage3_sig": "Paracrine",
141
  "stage3_rec": "Cell membrane",
142
  "stage3_grad": "Concentration",
 
143
  "outcome_question": "What will be the following response?",
144
  "outcome_options": [
145
  "Muscle contraction",
 
148
  ],
149
  "outcome_correct": "Muscle contraction",
150
  },
 
151
  {
152
  "loop_name": "Skin vessel loop (vasoconstriction/vasodilation)",
153
  "sensor_options": ["Thermoreceptor", "Mechanoreceptor", "Chemoreceptor"],
 
160
  "stage2_sig": "Paracrine",
161
  "stage2_rec": "Cell membrane",
162
  "stage2_grad": "Concentration",
 
163
  "stage3_desc": "Autonomic outputs alter peripheral vessel tone via neurotransmitters (synaptic).",
164
  "stage3_sig": "Paracrine",
165
  "stage3_rec": "Cell membrane",
166
  "stage3_grad": "Concentration",
 
167
  "outcome_question": "What will be the following response?",
168
  "outcome_options": [
169
  "Cutaneous vasoconstriction increases",
 
186
  if "assign" not in st.session_state:
187
  st.session_state.assign = {"sensor": None, "control": None, "effector": None}
188
  if "stage" not in st.session_state:
189
+ st.session_state.stage = 1
190
  if "stage_token" not in st.session_state:
191
+ st.session_state.stage_token = None
192
  if "msgs" not in st.session_state:
193
  st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""}
194
  if "progress" not in st.session_state:
195
  st.session_state.progress = {sc: [False]*len(SCENARIOS[sc]) for sc in SCENARIOS}
196
  if "nonce" not in st.session_state:
197
+ st.session_state.nonce = 0
198
 
199
  def current_loop():
200
  return SCENARIOS[st.session_state.scenario][st.session_state.loop_idx]
 
216
  st.session_state.stage = 1
217
  st.session_state.stage_token = None
218
  st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""}
219
+ st.session_state.nonce += 1 # ensures fresh widget keys
220
 
221
  def set_scenario(name):
222
  st.session_state.scenario = name
 
241
  # Diagram (arrows removed)
242
  # ==============================
243
  def diagram_html(sensor_txt, control_txt, effector_txt):
 
244
  sx, sy, sw, sh = 110, 260, 240, 56 # Sensor (left)
245
  cx, cy, cw, ch = 420, 70, 220, 56 # Control (top)
246
  ex, ey, ew, eh = 740, 320, 260, 56 # Effector (right)
247
 
 
248
  base_x, base_y, base_w, base_h = 330, 520, 360, 26
249
  base_mid_x = base_x + base_w/2
250
 
251
  html = f"""
252
  <div style="position:relative;width:100%;background:#f6f5ff;border:1px solid #e3e3f8;border-radius:16px;overflow:hidden;">
253
  <svg viewBox="0 0 1000 620" style="width:100%;height:auto;display:block" preserveAspectRatio="xMidYMid meet">
 
254
  <rect x="{base_x}" y="{base_y}" width="{base_w}" height="{base_h}" rx="8"
255
  fill="none" stroke="#4b2bb3" stroke-width="5"/>
256
  <polygon points="{base_mid_x - 20},{base_y + base_h + 2}
 
262
  <text x="{base_x + base_w - 70}" y="{base_y + base_h - 8}" font-size="16"
263
  text-anchor="middle" fill="#4b2bb3">Balance</text>
264
 
 
265
  <rect x="{sx}" y="{sy}" width="{sw}" height="{sh}" rx="12" fill="#e9ecff" stroke="#6b57e5"/>
266
  <text x="{sx+sw/2}" y="{sy+sh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{sensor_txt}</text>
267
 
 
318
 
319
  st.title("🧩 Interdependence Concept Game")
320
 
 
321
  scenario_list = list(SCENARIOS.keys())
322
  idx = scenario_list.index(st.session_state.scenario)
323
 
 
333
  reset_loop()
334
  safe_rerun()
335
 
 
336
  cloop = current_loop()
337
  token = loop_token()
338
  ksfx = key_suffix()
339
 
340
  st.subheader(f"Stage 1 · Build the negative feedback loop — **{cloop['loop_name']}**")
341
 
342
+ # Diagram render (no arrows)
343
  labels = st.session_state.assign
344
  html = diagram_html(
345
  labels['sensor'] or "Sensor",
346
  labels['control'] or "Control Center",
347
  labels['effector'] or "Effector(s)"
348
  )
349
+ components.html(html, height=660, scrolling=False)
350
 
351
  st.markdown("---")
352
 
353
+ # ------- Stage 1 -------
354
  st.write("**Sensor options**")
355
  sel_s = st.radio("Sensor", cloop["sensor_options"], index=None, horizontal=True, key=f"s1_sensor_{ksfx}")
356
  if sel_s is not None: st.session_state.assign["sensor"] = sel_s
 
365
 
366
  if st.button("Check Stage 1", key=f"chk1_{ksfx}"):
367
  a = st.session_state.assign
368
+ if not all([a["sensor"], a["control"], a["effector"]]):
369
+ st.session_state.msgs["s1"] = "Please complete all answers."
 
370
  else:
371
+ ok = (
372
+ a["sensor"] == cloop["sensor_correct"] and
373
+ a["control"] == cloop["control_correct"] and
374
+ a["effector"]== cloop["effector_correct"]
375
+ )
376
+ if ok:
 
377
  st.session_state.msgs["s1"] = "Great! Proceed to Stage 2."
378
  st.session_state.stage = 2
379
+ st.session_state.stage_token = token
380
  safe_rerun()
381
+ else:
382
+ st.session_state.msgs["s1"] = "Please recheck your answers."
383
  st.info(st.session_state.msgs["s1"])
384
 
385
+ # Only show one stage at a time, and only for this loop
386
  def show_stage(n: int) -> bool:
387
  return (st.session_state.stage == n) and (st.session_state.stage_token == token if n >= 2 else True)
388
 
 
395
  grd = st.radio("Gradient (primary transport)", ["Concentration","Electrochemical","Pressure"], index=None, horizontal=True, key=f"st2_grad_{ksfx}")
396
 
397
  if st.button("Check Stage 2", key=f"chk2_{ksfx}"):
398
+ if sig==cloop["stage2_sig"] and rec==cloop["stage2_rec"] and grd==cloop["stage2_grad"]:
 
399
  st.session_state.msgs["s2"] = "Correct! Continue to Stage 3."
400
  st.session_state.stage = 3
401
  safe_rerun()
402
  else:
403
+ st.session_state.msgs["s2"] = "Please recheck your answers."
 
 
 
404
  st.info(st.session_state.msgs["s2"])
405
 
406
  # ------- Stage 3 -------
 
412
  grd3 = st.radio("Gradient (primary transport)", ["Concentration","Electrochemical","Pressure"], index=None, horizontal=True, key=f"st3_grad_{ksfx}")
413
 
414
  if st.button("Check Stage 3", key=f"chk3_{ksfx}"):
415
+ if sig3==cloop["stage3_sig"] and rec3==cloop["stage3_rec"] and grd3==cloop["stage3_grad"]:
 
416
  st.session_state.msgs["s3"] = "Nice! Final question…"
417
  st.session_state.stage = 4
418
  safe_rerun()
419
  else:
420
+ st.session_state.msgs["s3"] = "Please recheck your answers."
 
 
 
421
  st.info(st.session_state.msgs["s3"])
422
 
423
  # ------- Stage 4 -------
 
430
  st.session_state.msgs["s4"] = "✅ Correct."
431
  next_loop_or_finish() # resets & advances; only Stage 1 will show next
432
  else:
433
+ st.session_state.msgs["s4"] = "Please recheck your answers."
434
  st.info(st.session_state.msgs["s4"])
435
 
436
  st.markdown("---")
437
 
438
+ # Certificate
439
  if all_loops_complete_for_current_scenario() and REPORTLAB_AVAILABLE:
440
  st.success("Scenario complete. Generate your PDF certificate below.")
441
  student_name = st.text_input("Student name for certificate", "")