ProfRick commited on
Commit
7625127
·
verified ·
1 Parent(s): fba4b84

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +218 -469
app.py CHANGED
@@ -1,479 +1,228 @@
1
- import streamlit as st
2
- import streamlit.components.v1 as components
3
- from io import BytesIO
 
 
 
 
 
 
 
 
 
4
  from datetime import datetime
5
 
6
- # --- PDF support (ReportLab) ---
7
- try:
8
- from reportlab.pdfgen import canvas
9
- from reportlab.lib.pagesizes import letter
10
- REPORTLAB_AVAILABLE = True
11
- except Exception:
12
- REPORTLAB_AVAILABLE = False
13
-
14
- st.set_page_config(page_title="Core Concepts Review", page_icon="🧬", layout="centered")
15
-
16
- # ==============================
17
- # Content
18
- # ==============================
19
- SCENARIOS = {
20
- "Blood Pressure (Hypotension)": [
21
- {
22
- "loop_name": "Nervous loop (Baroreflex)",
23
- "sensor_options": ["Chemoreceptor", "Mechanoreceptor", "Thermoreceptor"],
24
- "sensor_correct": "Mechanoreceptor",
25
- "control_options": ["Hypothalamus", "Medulla", "Prefrontal cortex"],
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",
40
- "Cardiac output decreases and vasodilation increases",
41
- "No change"
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)"],
48
- "sensor_correct": "Juxtaglomerular apparatus (kidney)",
49
- "control_options": ["Kidney (renin release)", "Medulla", "Anterior pituitary"],
50
- "control_correct": "Kidney (renin release)",
51
- "effector_options":["Kidney tubules (Na⁺ & water reabsorption)", "Sweat glands", "Airway smooth muscle"],
52
- "effector_correct":"Kidney tubules (Na⁺ & water reabsorption)",
53
- "stage2_desc": "Renin → angiotensin II (hydrophilic) travels **in the blood circulation** to reach distant targets.",
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",
64
- "Sodium excretion increases",
65
- "Urine volume increases"
66
- ],
67
- "outcome_correct": "Sodium & water reabsorption increases",
68
- },
69
- ],
70
-
71
- "Glucose (Post-meal Hyperglycemia)": [
72
- {
73
- "loop_name": "Insulin loop",
74
- "sensor_options": ["Pancreatic beta cells", "Pancreatic alpha cells", "Chemoreceptors (carotid)"],
75
- "sensor_correct": "Pancreatic beta cells",
76
- "control_options": ["Pancreas (islets of Langerhans)", "Hypothalamus", "Adrenal cortex"],
77
- "control_correct": "Pancreas (islets of Langerhans)",
78
- "effector_options":["Skeletal muscle & adipose", "Heart and Blood Vessels", "Kidney tubules"],
79
- "effector_correct":"Skeletal muscle & adipose",
80
- "stage2_desc": "A peptide messenger (hydrophilic) enters the bloodstream and circulates to distant tissues.",
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",
91
- "Hepatic glucose output increases",
92
- "Lipolysis 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)"],
99
- "sensor_correct": "Kidney (proximal tubule)",
100
- "control_options": ["Kidney (local tubular control)", "Pancreas", "Medulla"],
101
- "control_correct": "Kidney (local tubular control)",
102
- "effector_options":[
103
- "Glucose excretion into the urine increases",
104
- "Renal glucose reabsorption increases",
105
- "Insulin secretion increases"
106
- ],
107
- "effector_correct":"Glucose excretion into the urine increases",
108
- "stage2_desc": "Short-range paracrine signals in the tubule adjust transport locally (neighbor-to-neighbor; not via blood).",
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",
119
- "Renal glucose reabsorption increases",
120
- "Insulin secretion increases"
121
- ],
122
- "outcome_correct": "Glucose excretion into the urine increases",
123
- },
124
- ],
125
-
126
- "Temperature (Hypothermia)": [
127
- {
128
- "loop_name": "Shivering loop (somatic motor)",
129
- "sensor_options": ["Thermoreceptor", "Mechanoreceptor", "Chemoreceptor"],
130
- "sensor_correct": "Thermoreceptor",
131
- "control_options": ["Hypothalamus", "Medulla", "Prefrontal cortex"],
132
- "control_correct": "Hypothalamus",
133
- "effector_options":["Skeletal Muscle", "Pancreas", "Cutaneous blood vessels"],
134
- "effector_correct":"Skeletal Muscle",
135
- "stage2_desc": "Afferent neurons relay to the hypothalamus via neurotransmitters (hydrophilic) across synapses.",
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",
146
- "Muscle relaxation",
147
- "Cutaneous vasodilation"
148
- ],
149
- "outcome_correct": "Muscle contraction",
150
- },
151
- {
152
- "loop_name": "Skin vessel loop (vasoconstriction/vasodilation)",
153
- "sensor_options": ["Thermoreceptor", "Mechanoreceptor", "Chemoreceptor"],
154
- "sensor_correct": "Thermoreceptor",
155
- "control_options": ["Hypothalamus", "Medulla", "Anterior pituitary"],
156
- "control_correct": "Hypothalamus",
157
- "effector_options":["Cutaneous blood vessels", "Heart and Blood Vessels", "Skeletal Muscle"],
158
- "effector_correct":"Cutaneous blood vessels",
159
- "stage2_desc": "Afferent neurons signal the controller via neurotransmitters (hydrophilic) across synapses.",
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",
170
- "Cutaneous vasodilation increases",
171
- "Sweating increases"
172
- ],
173
- "outcome_correct": "Cutaneous vasoconstriction increases",
174
- },
175
- ],
176
- }
177
-
178
- # ==============================
179
- # Session state & helpers
180
- # ==============================
181
  def init_state():
182
- if "scenario" not in st.session_state:
183
- st.session_state.scenario = list(SCENARIOS.keys())[0]
184
  if "loop_idx" not in st.session_state:
185
- st.session_state.loop_idx = 0
186
- if "assign" not in st.session_state:
187
- st.session_state.assign = {"sensor": None, "control": None, "effector": None}
188
- # per-loop highest unlocked stage: token -> 1..4
189
- if "unlock" not in st.session_state:
190
- st.session_state.unlock = {}
191
- if "msgs" not in st.session_state:
192
- st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""}
193
- if "progress" not in st.session_state:
194
- st.session_state.progress = {sc: [False]*len(SCENARIOS[sc]) for sc in SCENARIOS}
195
- if "nonce" not in st.session_state:
196
- st.session_state.nonce = 0
197
-
198
- def purge_widget_state():
199
- """Remove old widget values so prior-loop widgets can't linger."""
200
- prefixes = ("s1_", "st2_", "st3_", "st4_", "chk1_", "chk2_", "chk3_", "finish_", "cert_")
201
- for k in list(st.session_state.keys()):
202
- if any(k.startswith(p) for p in prefixes):
203
- st.session_state.pop(k, None)
204
-
205
- def loop_token() -> str:
206
- return f"{st.session_state.scenario}|{st.session_state.loop_idx}"
207
-
208
- def key_suffix() -> str:
209
- # unique across loops and resets
210
- return f"{st.session_state.scenario}_{st.session_state.loop_idx}_{st.session_state.nonce}"
211
-
212
- def current_loop():
213
- return SCENARIOS[st.session_state.scenario][st.session_state.loop_idx]
214
-
215
- def safe_rerun():
216
- try:
217
- st.rerun()
218
- except Exception:
219
- st.experimental_rerun()
220
-
221
- def reset_loop():
222
- # clear all loop-specific state
223
- purge_widget_state()
224
- st.session_state.assign = {"sensor": None, "control": None, "effector": None}
225
- st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""}
226
- st.session_state.unlock = {} # ensures next loop starts at Stage 1 only
227
- st.session_state.nonce += 1 # force new widget keys
228
-
229
- def set_scenario(name):
230
- st.session_state.scenario = name
231
- st.session_state.loop_idx = 0
232
- reset_loop()
233
-
234
- def next_loop_or_finish():
235
- st.session_state.progress[st.session_state.scenario][st.session_state.loop_idx] = True
236
- if st.session_state.loop_idx + 1 < len(SCENARIOS[st.session_state.scenario]):
237
- st.session_state.loop_idx += 1
238
- reset_loop()
239
- st.session_state.msgs["s1"] = "Great—now build the second loop for this variable."
240
  else:
241
- reset_loop()
242
- st.session_state.msgs["s1"] = "Scenario complete! You can generate a certificate below."
243
- safe_rerun()
244
-
245
- def all_loops_complete_for_current_scenario() -> bool:
246
- return all(st.session_state.progress[st.session_state.scenario])
247
-
248
- # ==============================
249
- # Diagram (arrows removed)
250
- # ==============================
251
- def diagram_html(sensor_txt, control_txt, effector_txt):
252
- sx, sy, sw, sh = 110, 260, 240, 56 # Sensor (left)
253
- cx, cy, cw, ch = 420, 70, 220, 56 # Control (top)
254
- ex, ey, ew, eh = 740, 320, 260, 56 # Effector (right)
255
-
256
- base_x, base_y, base_w, base_h = 330, 520, 360, 26
257
- base_mid_x = base_x + base_w/2
258
-
259
- html = f"""
260
- <div style="position:relative;width:100%;background:#f6f5ff;border:1px solid #e3e3f8;border-radius:16px;overflow:hidden;">
261
- <svg viewBox="0 0 1000 620" style="width:100%;height:auto;display:block" preserveAspectRatio="xMidYMid meet">
262
- <rect x="{base_x}" y="{base_y}" width="{base_w}" height="{base_h}" rx="8"
263
- fill="none" stroke="#4b2bb3" stroke-width="5"/>
264
- <polygon points="{base_mid_x - 20},{base_y + base_h + 2}
265
- {base_mid_x + 20},{base_y + base_h + 2}
266
- {base_mid_x},{base_y + base_h + 44}"
267
- fill="#4b2bb3"/>
268
- <text x="{base_x + 70}" y="{base_y + base_h - 8}" font-size="16"
269
- text-anchor="middle" fill="#4b2bb3">Imbalance</text>
270
- <text x="{base_x + base_w - 70}" y="{base_y + base_h - 8}" font-size="16"
271
- text-anchor="middle" fill="#4b2bb3">Balance</text>
272
-
273
- <rect x="{sx}" y="{sy}" width="{sw}" height="{sh}" rx="12" fill="#e9ecff" stroke="#6b57e5"/>
274
- <text x="{sx+sw/2}" y="{sy+sh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{sensor_txt}</text>
275
-
276
- <rect x="{cx}" y="{cy}" width="{cw}" height="{ch}" rx="12" fill="#e9ecff" stroke="#6b57e5"/>
277
- <text x="{cx+cw/2}" y="{cy+ch/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{control_txt}</text>
278
-
279
- <rect x="{ex}" y="{ey}" width="{ew}" height="{eh}" rx="12" fill="#e9ecff" stroke="#6b57e5"/>
280
- <text x="{ex+ew/2}" y="{ey+eh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{effector_txt}</text>
281
- </svg>
282
- </div>
283
- """
284
- return html
285
-
286
- # ==============================
287
- # Certificate
288
- # ==============================
289
- def generate_certificate_pdf(student_name: str, scenario_name: str, loops_done: list[str]) -> bytes:
290
- buf = BytesIO()
291
- c = canvas.Canvas(buf, pagesize=letter)
292
- W, H = letter
293
-
294
- c.setFont("Helvetica-Bold", 28)
295
- c.drawCentredString(W/2, H - 100, "Certificate of Completion")
296
-
297
- c.setFont("Helvetica", 13)
298
- c.drawCentredString(W/2, H - 140, "This certifies that")
299
- c.setFont("Helvetica-Bold", 20)
300
- c.drawCentredString(W/2, H - 170, student_name if student_name.strip() else "Student")
301
- c.setFont("Helvetica", 13)
302
- c.drawCentredString(W/2, H - 200, "has successfully completed the scenario")
303
- c.setFont("Helvetica-Bold", 16)
304
- c.drawCentredString(W/2, H - 225, scenario_name)
305
 
306
  c.setFont("Helvetica", 12)
307
- y = H - 270
308
- c.drawCentredString(W/2, y, "Completed loops:")
309
  y -= 18
310
- for lp in loops_done:
311
- c.drawCentredString(W/2, y, f"• {lp}")
312
- y -= 16
313
-
314
- c.setFont("Helvetica-Oblique", 11)
315
- c.drawCentredString(W/2, 80, f"Issued on {datetime.now().strftime('%Y-%m-%d %H:%M')}")
316
-
317
- c.showPage(); c.save()
318
- buf.seek(0)
319
- return buf.read()
320
-
321
- # ==============================
322
- # UI
323
- # ==============================
324
- def init_and_render():
325
- init_state()
326
-
327
- st.title("Core Concepts Review")
328
-
329
- scenario_list = list(SCENARIOS.keys())
330
- idx = scenario_list.index(st.session_state.scenario)
331
-
332
- def on_change_scenario():
333
- st.session_state.scenario = st.session_state["scenario_select"]
334
- st.session_state.loop_idx = 0
335
- reset_loop()
336
- safe_rerun()
337
-
338
- top_col1, top_col2 = st.columns([2,1])
339
- with top_col1:
340
- st.selectbox("Scenario", scenario_list, index=idx, key="scenario_select", on_change=on_change_scenario)
341
- with top_col2:
342
- if st.button("Reset Loop"):
343
- reset_loop()
344
- safe_rerun()
345
-
346
- cloop = current_loop()
347
- token = loop_token()
348
- ksfx = key_suffix()
349
-
350
- # ensure unlock entry for this loop
351
- if token not in st.session_state.unlock:
352
- st.session_state.unlock[token] = 1
353
-
354
- # ---------- Diagram ----------
355
- st.subheader(f"Stage 1 · Build the negative feedback loop — **{cloop['loop_name']}**")
356
- labels = st.session_state.assign
357
- components.html(
358
- diagram_html(
359
- labels['sensor'] or "Sensor",
360
- labels['control'] or "Control Center",
361
- labels['effector'] or "Effector(s)"
362
- ),
363
- height=660, scrolling=False
 
 
 
 
364
  )
365
- st.markdown("---")
366
-
367
- # ---------- Placeholders for cumulative rendering ----------
368
- ph1 = st.container()
369
- ph2 = st.container()
370
- ph3 = st.container()
371
- ph4 = st.container()
372
-
373
- # ---------------- Stage 1 (always visible) ----------------
374
- with ph1:
375
- st.write("**Sensor options**")
376
- sel_s = st.radio("Sensor", cloop["sensor_options"], index=None, horizontal=True, key=f"s1_sensor_{ksfx}")
377
- if sel_s is not None: st.session_state.assign["sensor"] = sel_s
378
-
379
- st.write("**Control center options**")
380
- sel_c = st.radio("Control center", cloop["control_options"], index=None, horizontal=True, key=f"s1_control_{ksfx}")
381
- if sel_c is not None: st.session_state.assign["control"] = sel_c
382
-
383
- st.write("**Effector options**")
384
- sel_e = st.radio("Effector(s)", cloop["effector_options"], index=None, horizontal=True, key=f"s1_effector_{ksfx}")
385
- if sel_e is not None: st.session_state.assign["effector"] = sel_e
386
-
387
- if st.button("Check Stage 1", key=f"chk1_{ksfx}"):
388
- a = st.session_state.assign
389
- if not all([a["sensor"], a["control"], a["effector"]]):
390
- st.session_state.msgs["s1"] = "Please complete all answers."
391
  else:
392
- ok = (
393
- a["sensor"] == cloop["sensor_correct"] and
394
- a["control"] == cloop["control_correct"] and
395
- a["effector"]== cloop["effector_correct"]
396
- )
397
- if ok:
398
- st.session_state.msgs["s1"] = "Great! Proceed to Stage 2."
399
- st.session_state.unlock[token] = max(st.session_state.unlock.get(token,1), 2)
400
- safe_rerun()
401
- else:
402
- st.session_state.msgs["s1"] = "Please recheck your answers."
403
- st.info(st.session_state.msgs["s1"])
404
-
405
- # helper
406
- def visible(stage_no: int) -> bool:
407
- return st.session_state.unlock.get(token, 1) >= stage_no
408
-
409
- # ---------------- Stage 2 ----------------
410
- if visible(2):
411
- with ph2:
412
- st.subheader("Stage 2 · Sensor → Control Center")
413
- st.markdown(cloop["stage2_desc"])
414
- sig = st.radio("Signaling", ["Autocrine","Paracrine","Endocrine"], index=None, horizontal=True, key=f"st2_sig_{ksfx}")
415
- rec = st.radio("Receptor", ["Cell membrane","Inside the cell"], index=None, horizontal=True, key=f"st2_rec_{ksfx}")
416
- grd = st.radio("Gradient (primary transport)", ["Concentration","Electrochemical","Pressure"], index=None, horizontal=True, key=f"st2_grad_{ksfx}")
417
-
418
- if st.button("Check Stage 2", key=f"chk2_{ksfx}"):
419
- if sig==cloop["stage2_sig"] and rec==cloop["stage2_rec"] and grd==cloop["stage2_grad"]:
420
- st.session_state.msgs["s2"] = "Correct! Continue to Stage 3."
421
- st.session_state.unlock[token] = max(st.session_state.unlock.get(token,2), 3)
422
- safe_rerun()
423
- else:
424
- st.session_state.msgs["s2"] = "Please recheck your answers."
425
- st.info(st.session_state.msgs["s2"])
426
-
427
- # ---------------- Stage 3 ----------------
428
- if visible(3):
429
- with ph3:
430
- st.subheader("Stage 3 · Control Center → Effectors")
431
- st.markdown(cloop["stage3_desc"])
432
- sig3 = st.radio("Signaling", ["Autocrine","Paracrine","Endocrine"], index=None, horizontal=True, key=f"st3_sig_{ksfx}")
433
- rec3 = st.radio("Receptor", ["Cell membrane","Inside the cell"], index=None, horizontal=True, key=f"st3_rec_{ksfx}")
434
- grd3 = st.radio("Gradient (primary transport)", ["Concentration","Electrochemical","Pressure"], index=None, horizontal=True, key=f"st3_grad_{ksfx}")
435
-
436
- if st.button("Check Stage 3", key=f"chk3_{ksfx}"):
437
- if sig3==cloop["stage3_sig"] and rec3==cloop["stage3_rec"] and grd3==cloop["stage3_grad"]:
438
- st.session_state.msgs["s3"] = "Nice! Final question…"
439
- st.session_state.unlock[token] = max(st.session_state.unlock.get(token,3), 4)
440
- safe_rerun()
441
- else:
442
- st.session_state.msgs["s3"] = "Please recheck your answers."
443
- st.info(st.session_state.msgs["s3"])
444
-
445
- # ---------------- Stage 4 ----------------
446
- if visible(4):
447
- with ph4:
448
- st.subheader("Stage 4 · Outcome")
449
- st.markdown(f"**{cloop['outcome_question']}**")
450
- ans = st.radio("Choose one:", cloop["outcome_options"], index=None, key=f"st4_ans_{ksfx}")
451
- if st.button("Finish Loop", key=f"finish_{ksfx}"):
452
- if ans == cloop["outcome_correct"]:
453
- st.session_state.msgs["s4"] = "✅ Correct."
454
- next_loop_or_finish()
455
- else:
456
- st.session_state.msgs["s4"] = "Please recheck your answers."
457
- st.info(st.session_state.msgs["s4"])
458
-
459
- # ---------- CERTIFICATE (bottom only) ----------
460
- st.markdown("---")
461
- if all_loops_complete_for_current_scenario() and REPORTLAB_AVAILABLE:
462
- st.success("Scenario complete. Generate your PDF certificate below.")
463
- student_name = st.text_input("Student name for certificate", "")
464
- loops_list = [lp["loop_name"] for lp in SCENARIOS[st.session_state.scenario]]
465
- if st.button("Generate PDF Certificate", key=f"cert_{ksfx}"):
466
- pdf_bytes = generate_certificate_pdf(student_name, st.session_state.scenario, loops_list)
467
- st.download_button(
468
- "Download certificate",
469
- data=pdf_bytes,
470
- file_name=f"certificate_{st.session_state.scenario.replace(' ','_')}.pdf",
471
- mime="application/pdf"
472
- )
473
- elif all_loops_complete_for_current_scenario() and not REPORTLAB_AVAILABLE:
474
- st.warning("Certificates are disabled (reportlab not available). Add 'reportlab' to requirements.txt.")
475
-
476
- st.caption("Tip: Use 'Reset Loop' to try again or switch scenarios above.")
477
-
478
- # Run
479
- init_and_render()
 
1
+ # app.py Core Concepts Review (Streamlit)
2
+ # Features:
3
+ # - Title updated to "Core Concepts Review"
4
+ # - Stage-by-stage progression (1→2→3→4) with the diagram visible throughout
5
+ # - Prior completed stages remain visible within the same loop
6
+ # - On completing stage 4, a NEW LOOP begins: only the diagram + Stage 1 are shown; previous loop's stages are hidden
7
+ # - Proof-of-completion PDF button appears AT THE BOTTOM only after finishing TWO full loops of a scenario
8
+ # - Unique keys for all buttons/inputs to avoid StreamlitDuplicateElementId
9
+ # - Clean state management via st.session_state
10
+ # - Minimal, self-contained validation logic (replace with your own as needed)
11
+
12
+ import io
13
  from datetime import datetime
14
 
15
+ import streamlit as st
16
+ from reportlab.lib.pagesizes import letter
17
+ from reportlab.pdfgen import canvas
18
+
19
+ st.set_page_config(page_title="Core Concepts Review", page_icon="📘", layout="wide")
20
+
21
+ ############################
22
+ # ---------- DATA ----------
23
+ ############################
24
+ # Define your scenarios. Each scenario has: title, diagram (could be an image URL or placeholder), and 4 stages.
25
+ # For demo purposes, we use placeholders and super-simple checkers.
26
+
27
+ SCENARIOS = [
28
+ {
29
+ "id": "interdependence_sepsis",
30
+ "title": "Interdependence of Body Systems — Sepsis",
31
+ "diagram": "Sepsis flow diagram (placeholder)", # replace with st.image(...) source if desired
32
+ "stages": [
33
+ {"name": "Stage 1: Identify Trigger", "prompt": "What common trigger initiates sepsis?",
34
+ "answer": "infection"},
35
+ {"name": "Stage 2: Cascade", "prompt": "Name one key mediator elevated early in sepsis.",
36
+ "answer": "tnf"},
37
+ {"name": "Stage 3: System Impact", "prompt": "Which organ system often shows early dysfunction?",
38
+ "answer": "cardiovascular"},
39
+ {"name": "Stage 4: Feedback", "prompt": "Is the dysregulation in sepsis a failure of negative or positive feedback?",
40
+ "answer": "negative"},
41
+ ],
42
+ },
43
+ {
44
+ "id": "flow_gradients",
45
+ "title": "Flow Down Gradients — Pressure/Concentration/Electrical",
46
+ "diagram": "Flow gradients diagram (placeholder)",
47
+ "stages": [
48
+ {"name": "Stage 1: Direction", "prompt": "Fluids move from _____ to _____ pressure.",
49
+ "answer": "high to low"},
50
+ {"name": "Stage 2: Diffusion", "prompt": "Particles diffuse from _____ concentration to _____ concentration.",
51
+ "answer": "high to low"},
52
+ {"name": "Stage 3: Membrane Potential", "prompt": "Resting neurons are typically around how many mV (approx)?",
53
+ "answer": "-70"},
54
+ {"name": "Stage 4: Electrochemical", "prompt": "Ions move according to the _________ gradient.",
55
+ "answer": "electrochemical"},
56
+ ],
57
+ },
58
+ ]
59
+
60
+ REQUIRED_LOOPS_FOR_PDF = 2 # show the PDF button only after 2 full loops
61
+
62
+ ############################
63
+ # ------- UTILITIES --------
64
+ ############################
65
+
66
+ def _key(*parts):
67
+ """Create a unique key string from parts to avoid duplicate element IDs."""
68
+ return "::".join(str(p) for p in parts)
69
+
70
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def init_state():
72
+ if "scenario_idx" not in st.session_state:
73
+ st.session_state.scenario_idx = 0 # which scenario is active
74
  if "loop_idx" not in st.session_state:
75
+ st.session_state.loop_idx = 1 # loop counter within current scenario
76
+ if "stage_visible" not in st.session_state:
77
+ # Which stages are visible in the current loop (1..4)
78
+ st.session_state.stage_visible = {1: True, 2: False, 3: False, 4: False}
79
+ if "stage_correct" not in st.session_state:
80
+ # Whether each stage has been correctly completed in the current loop
81
+ st.session_state.stage_correct = {1: False, 2: False, 3: False, 4: False}
82
+ if "loops_completed" not in st.session_state:
83
+ # How many full loops completed (per scenario id)
84
+ st.session_state.loops_completed = {s["id"]: 0 for s in SCENARIOS}
85
+ if "answers" not in st.session_state:
86
+ # Persist students' typed answers for the *current loop only*
87
+ st.session_state.answers = {1: "", 2: "", 3: "", 4: ""}
88
+
89
+
90
+ def reset_for_new_loop():
91
+ st.session_state.loop_idx += 1
92
+ st.session_state.stage_visible = {1: True, 2: False, 3: False, 4: False}
93
+ st.session_state.stage_correct = {1: False, 2: False, 3: False, 4: False}
94
+ st.session_state.answers = {1: "", 2: "", 3: "", 4: ""}
95
+
96
+
97
+ def next_stage(stage_num):
98
+ # Mark stage as visible/correct appropriately
99
+ st.session_state.stage_correct[stage_num] = True
100
+ if stage_num < 4:
101
+ st.session_state.stage_visible[stage_num + 1] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  else:
103
+ # Completed Stage 4 — full loop done
104
+ scenario = SCENARIOS[st.session_state.scenario_idx]
105
+ st.session_state.loops_completed[scenario["id"]] += 1
106
+ reset_for_new_loop()
107
+
108
+
109
+ def validate_answer(user_input: str, expected: str) -> bool:
110
+ if expected is None or expected == "":
111
+ return True
112
+ ui = (user_input or "").strip().lower()
113
+ exp = expected.strip().lower()
114
+ # allow minor flexibility e.g., hyphens/spaces
115
+ ui = ui.replace("—", "-").replace("–", "-")
116
+ exp = exp.replace("—", "-").replace("–", "-")
117
+ return ui == exp
118
+
119
+
120
+ def generate_pdf_bytes(scenario_title: str, loops_done: int):
121
+ """Return a BytesIO of the proof-of-completion PDF."""
122
+ buffer = io.BytesIO()
123
+ c = canvas.Canvas(buffer, pagesize=letter)
124
+ width, height = letter
125
+
126
+ c.setFont("Helvetica-Bold", 18)
127
+ c.drawString(72, height - 72, "Core Concepts Review — Proof of Completion")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  c.setFont("Helvetica", 12)
130
+ y = height - 110
131
+ c.drawString(72, y, f"Scenario: {scenario_title}")
132
  y -= 18
133
+ c.drawString(72, y, f"Loops completed: {loops_done}")
134
+ y -= 18
135
+ c.drawString(72, y, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
136
+
137
+ c.showPage()
138
+ c.save()
139
+ buffer.seek(0)
140
+ return buffer
141
+
142
+ ############################
143
+ # --------- UI -------------
144
+ ############################
145
+ init_state()
146
+
147
+ scenario = SCENARIOS[st.session_state.scenario_idx]
148
+
149
+ st.title("Core Concepts Review")
150
+
151
+ # Scenario selector (optional)
152
+ cols_top = st.columns([3, 1])
153
+ with cols_top[0]:
154
+ st.subheader(f"Scenario: {scenario['title']}")
155
+ with cols_top[1]:
156
+ if st.button("Switch Scenario", key=_key("switch", st.session_state.scenario_idx, st.session_state.loop_idx)):
157
+ # rotate to next scenario and hard reset loop state
158
+ st.session_state.scenario_idx = (st.session_state.scenario_idx + 1) % len(SCENARIOS)
159
+ st.session_state.loop_idx = 1
160
+ st.session_state.stage_visible = {1: True, 2: False, 3: False, 4: False}
161
+ st.session_state.stage_correct = {1: False, 2: False, 3: False, 4: False}
162
+ st.session_state.answers = {1: "", 2: "", 3: "", 4: ""}
163
+ st.experimental_rerun()
164
+
165
+ st.caption(f"Loop #{st.session_state.loop_idx}")
166
+
167
+ # Diagram area — stays visible through all stages of the current loop
168
+ with st.container(border=True):
169
+ st.markdown("**Diagram**")
170
+ # Replace with st.image(scenario['diagram']) if you have an actual file/URL
171
+ st.write(scenario["diagram"]) # placeholder text
172
+
173
+ st.divider()
174
+
175
+ # Stages 1..4; each stage remains visible once unlocked, within the current loop
176
+ for stage_idx in range(1, 5):
177
+ if not st.session_state.stage_visible[stage_idx]:
178
+ continue
179
+
180
+ st.markdown(f"### {scenario['stages'][stage_idx-1]['name']}")
181
+ prompt = scenario['stages'][stage_idx-1]['prompt']
182
+ expected = scenario['stages'][stage_idx-1]['answer']
183
+
184
+ st.write(prompt)
185
+ ukey = _key("input", stage_idx, st.session_state.loop_idx, st.session_state.scenario_idx)
186
+ st.session_state.answers[stage_idx] = st.text_input(
187
+ label="Your answer",
188
+ key=ukey,
189
+ value=st.session_state.answers.get(stage_idx, ""),
190
+ placeholder="Type your response...",
191
  )
192
+
193
+ cols = st.columns([1, 5])
194
+ with cols[0]:
195
+ if st.button("Check", key=_key("check", stage_idx, st.session_state.loop_idx, st.session_state.scenario_idx)):
196
+ ok = validate_answer(st.session_state.answers[stage_idx], expected)
197
+ if ok:
198
+ st.success("Correct! Proceeding to the next stage…")
199
+ next_stage(stage_idx)
200
+ st.experimental_rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  else:
202
+ # No hints; just feedback per your request to remove on-the-nose clues
203
+ st.error("Not quite. Try again.")
204
+
205
+ with cols[1]:
206
+ if st.session_state.stage_correct[stage_idx]:
207
+ st.info("✅ Completed")
208
+
209
+ st.divider()
210
+
211
+ # Bottom section: PDF appears ONLY after the required # of loops is completed for this scenario
212
+ loops_done = st.session_state.loops_completed[scenario["id"]]
213
+ if loops_done >= REQUIRED_LOOPS_FOR_PDF:
214
+ st.markdown("#### Download your Proof-of-Completion (after two full loops)")
215
+ pdf_bytes = generate_pdf_bytes(scenario_title=scenario["title"], loops_done=loops_done)
216
+ st.download_button(
217
+ label="⬇️ Generate & Download PDF",
218
+ file_name=f"Core_Concepts_Review_{scenario['id']}_loops{loops_done}.pdf",
219
+ mime="application/pdf",
220
+ data=pdf_bytes,
221
+ key=_key("pdf", st.session_state.scenario_idx, st.session_state.loop_idx),
222
+ use_container_width=True,
223
+ )
224
+
225
+ # Footer spacing
226
+ st.write("")
227
+ st.write("")
228
+ st.caption("© Your Course • Prepared for student practice • No hints shown on incorrect answers")