Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| from io import BytesIO | |
| from datetime import datetime | |
| # --- PDF support (ReportLab) --- | |
| try: | |
| from reportlab.pdfgen import canvas | |
| from reportlab.lib.pagesizes import letter | |
| REPORTLAB_AVAILABLE = True | |
| except Exception: | |
| REPORTLAB_AVAILABLE = False | |
| st.set_page_config(page_title="Core Concepts Review", page_icon="🧬", layout="centered") | |
| # ============================== | |
| # Content | |
| # ============================== | |
| SCENARIOS = { | |
| "Blood Pressure (Hypotension)": [ | |
| { | |
| "loop_name": "Nervous loop (Baroreflex)", | |
| "sensor_options": ["Chemoreceptor", "Mechanoreceptor", "Thermoreceptor"], | |
| "sensor_correct": "Mechanoreceptor", | |
| "control_options": ["Hypothalamus", "Medulla", "Prefrontal cortex"], | |
| "control_correct": "Medulla", | |
| "effector_options":["Pancreas", "Skeletal Muscle", "Heart and Blood Vessels"], | |
| "effector_correct":"Heart and Blood Vessels", | |
| "stage2_desc": "Afferent neurons release neurotransmitters (hydrophilic) across a synapse to the control center neuron.", | |
| "stage2_sig": "Paracrine", | |
| "stage2_rec": "Cell membrane", | |
| "stage2_grad": "Concentration", | |
| "stage3_desc": "Controller neurons signal effectors via neurotransmitters (hydrophilic) across synapses.", | |
| "stage3_sig": "Paracrine", | |
| "stage3_rec": "Cell membrane", | |
| "stage3_grad": "Concentration", | |
| "outcome_question": "What will be the following response?", | |
| "outcome_options": [ | |
| "Cardiac output increases and vasoconstriction increases", | |
| "Cardiac output decreases and vasodilation increases", | |
| "No change" | |
| ], | |
| "outcome_correct": "Cardiac output increases and vasoconstriction increases", | |
| }, | |
| { | |
| "loop_name": "Renal loop (RAAS)", | |
| "sensor_options": ["Juxtaglomerular apparatus (kidney)", "Baroreceptors (carotid sinus)", "Osmoreceptors (hypothalamus)"], | |
| "sensor_correct": "Juxtaglomerular apparatus (kidney)", | |
| "control_options": ["Kidney (renin release)", "Medulla", "Anterior pituitary"], | |
| "control_correct": "Kidney (renin release)", | |
| "effector_options":["Kidney tubules (Na⁺ & water reabsorption)", "Sweat glands", "Airway smooth muscle"], | |
| "effector_correct":"Kidney tubules (Na⁺ & water reabsorption)", | |
| "stage2_desc": "Renin → angiotensin II (hydrophilic) travels **in the blood circulation** to reach distant targets.", | |
| "stage2_sig": "Endocrine", | |
| "stage2_rec": "Cell membrane", | |
| "stage2_grad": "Pressure", | |
| "stage3_desc": "Aldosterone (lipophilic steroid) travels **in the blood circulation** to tubular cells.", | |
| "stage3_sig": "Endocrine", | |
| "stage3_rec": "Inside the cell", | |
| "stage3_grad": "Pressure", | |
| "outcome_question": "What will be the following response?", | |
| "outcome_options": [ | |
| "Sodium & water reabsorption increases", | |
| "Sodium excretion increases", | |
| "Urine volume increases" | |
| ], | |
| "outcome_correct": "Sodium & water reabsorption increases", | |
| }, | |
| ], | |
| "Glucose (Post-meal Hyperglycemia)": [ | |
| { | |
| "loop_name": "Insulin loop", | |
| "sensor_options": ["Pancreatic beta cells", "Pancreatic alpha cells", "Chemoreceptors (carotid)"], | |
| "sensor_correct": "Pancreatic beta cells", | |
| "control_options": ["Pancreas (islets of Langerhans)", "Hypothalamus", "Adrenal cortex"], | |
| "control_correct": "Pancreas (islets of Langerhans)", | |
| "effector_options":["Skeletal muscle & adipose", "Heart and Blood Vessels", "Kidney tubules"], | |
| "effector_correct":"Skeletal muscle & adipose", | |
| "stage2_desc": "A peptide messenger (hydrophilic) enters the bloodstream and circulates to distant tissues.", | |
| "stage2_sig": "Endocrine", | |
| "stage2_rec": "Cell membrane", | |
| "stage2_grad": "Pressure", | |
| "stage3_desc": "The messenger reaches distant tissues via circulation to increase glucose uptake.", | |
| "stage3_sig": "Endocrine", | |
| "stage3_rec": "Cell membrane", | |
| "stage3_grad": "Pressure", | |
| "outcome_question": "What will be the following response?", | |
| "outcome_options": [ | |
| "Cellular glucose uptake increases", | |
| "Hepatic glucose output increases", | |
| "Lipolysis increases" | |
| ], | |
| "outcome_correct": "Cellular glucose uptake increases", | |
| }, | |
| { | |
| "loop_name": "Kidney excretion loop (overflow)", | |
| "sensor_options": ["Kidney (proximal tubule)", "Pancreatic beta cells", "Baroreceptors (carotid)"], | |
| "sensor_correct": "Kidney (proximal tubule)", | |
| "control_options": ["Kidney (local tubular control)", "Pancreas", "Medulla"], | |
| "control_correct": "Kidney (local tubular control)", | |
| "effector_options":[ | |
| "Glucose excretion into the urine increases", | |
| "Renal glucose reabsorption increases", | |
| "Insulin secretion increases" | |
| ], | |
| "effector_correct":"Glucose excretion into the urine increases", | |
| "stage2_desc": "Short-range paracrine signals in the tubule adjust transport locally (neighbor-to-neighbor; not via blood).", | |
| "stage2_sig": "Paracrine", | |
| "stage2_rec": "Cell membrane", | |
| "stage2_grad": "Concentration", | |
| "stage3_desc": "Local control continues at transporters within the same tissue.", | |
| "stage3_sig": "Paracrine", | |
| "stage3_rec": "Cell membrane", | |
| "stage3_grad": "Concentration", | |
| "outcome_question": "What will be the following response?", | |
| "outcome_options": [ | |
| "Glucose excretion into the urine increases", | |
| "Renal glucose reabsorption increases", | |
| "Insulin secretion increases" | |
| ], | |
| "outcome_correct": "Glucose excretion into the urine increases", | |
| }, | |
| ], | |
| "Temperature (Hypothermia)": [ | |
| { | |
| "loop_name": "Shivering loop (somatic motor)", | |
| "sensor_options": ["Thermoreceptor", "Mechanoreceptor", "Chemoreceptor"], | |
| "sensor_correct": "Thermoreceptor", | |
| "control_options": ["Hypothalamus", "Medulla", "Prefrontal cortex"], | |
| "control_correct": "Hypothalamus", | |
| "effector_options":["Skeletal Muscle", "Pancreas", "Cutaneous blood vessels"], | |
| "effector_correct":"Skeletal Muscle", | |
| "stage2_desc": "Afferent neurons relay to the hypothalamus via neurotransmitters (hydrophilic) across synapses.", | |
| "stage2_sig": "Paracrine", | |
| "stage2_rec": "Cell membrane", | |
| "stage2_grad": "Concentration", | |
| "stage3_desc": "Motor pathways direct skeletal muscle via neurotransmitters across synapses.", | |
| "stage3_sig": "Paracrine", | |
| "stage3_rec": "Cell membrane", | |
| "stage3_grad": "Concentration", | |
| "outcome_question": "What will be the following response?", | |
| "outcome_options": [ | |
| "Muscle contraction", | |
| "Muscle relaxation", | |
| "Cutaneous vasodilation" | |
| ], | |
| "outcome_correct": "Muscle contraction", | |
| }, | |
| { | |
| "loop_name": "Skin vessel loop (vasoconstriction/vasodilation)", | |
| "sensor_options": ["Thermoreceptor", "Mechanoreceptor", "Chemoreceptor"], | |
| "sensor_correct": "Thermoreceptor", | |
| "control_options": ["Hypothalamus", "Medulla", "Anterior pituitary"], | |
| "control_correct": "Hypothalamus", | |
| "effector_options":["Cutaneous blood vessels", "Heart and Blood Vessels", "Skeletal Muscle"], | |
| "effector_correct":"Cutaneous blood vessels", | |
| "stage2_desc": "Afferent neurons signal the controller via neurotransmitters (hydrophilic) across synapses.", | |
| "stage2_sig": "Paracrine", | |
| "stage2_rec": "Cell membrane", | |
| "stage2_grad": "Concentration", | |
| "stage3_desc": "Autonomic outputs alter peripheral vessel tone via neurotransmitters (synaptic).", | |
| "stage3_sig": "Paracrine", | |
| "stage3_rec": "Cell membrane", | |
| "stage3_grad": "Concentration", | |
| "outcome_question": "What will be the following response?", | |
| "outcome_options": [ | |
| "Cutaneous vasoconstriction increases", | |
| "Cutaneous vasodilation increases", | |
| "Sweating increases" | |
| ], | |
| "outcome_correct": "Cutaneous vasoconstriction increases", | |
| }, | |
| ], | |
| } | |
| # ============================== | |
| # Session state & helpers | |
| # ============================== | |
| def init_state(): | |
| if "scenario" not in st.session_state: | |
| st.session_state.scenario = list(SCENARIOS.keys())[0] | |
| if "loop_idx" not in st.session_state: | |
| st.session_state.loop_idx = 0 | |
| if "assign" not in st.session_state: | |
| st.session_state.assign = {"sensor": None, "control": None, "effector": None} | |
| # per-loop highest unlocked stage: token -> 1..4 | |
| if "unlock" not in st.session_state: | |
| st.session_state.unlock = {} | |
| if "msgs" not in st.session_state: | |
| st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""} | |
| if "progress" not in st.session_state: | |
| st.session_state.progress = {sc: [False]*len(SCENARIOS[sc]) for sc in SCENARIOS} | |
| if "nonce" not in st.session_state: | |
| st.session_state.nonce = 0 | |
| def purge_widget_state(): | |
| """Remove old widget values so prior-loop widgets can't linger.""" | |
| prefixes = ("s1_", "st2_", "st3_", "st4_", "chk1_", "chk2_", "chk3_", "finish_", "cert_") | |
| for k in list(st.session_state.keys()): | |
| if any(k.startswith(p) for p in prefixes): | |
| st.session_state.pop(k, None) | |
| def loop_token() -> str: | |
| return f"{st.session_state.scenario}|{st.session_state.loop_idx}" | |
| def key_suffix() -> str: | |
| # unique across loops and resets | |
| return f"{st.session_state.scenario}_{st.session_state.loop_idx}_{st.session_state.nonce}" | |
| def current_loop(): | |
| return SCENARIOS[st.session_state.scenario][st.session_state.loop_idx] | |
| def safe_rerun(): | |
| try: | |
| st.rerun() | |
| except Exception: | |
| st.experimental_rerun() | |
| def reset_loop(): | |
| # clear all loop-specific state | |
| purge_widget_state() | |
| st.session_state.assign = {"sensor": None, "control": None, "effector": None} | |
| st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""} | |
| st.session_state.unlock = {} # ensures next loop starts at Stage 1 only | |
| st.session_state.nonce += 1 # force new widget keys | |
| def set_scenario(name): | |
| st.session_state.scenario = name | |
| st.session_state.loop_idx = 0 | |
| reset_loop() | |
| def next_loop_or_finish(): | |
| st.session_state.progress[st.session_state.scenario][st.session_state.loop_idx] = True | |
| if st.session_state.loop_idx + 1 < len(SCENARIOS[st.session_state.scenario]): | |
| st.session_state.loop_idx += 1 | |
| reset_loop() | |
| st.session_state.msgs["s1"] = "Great—now build the second loop for this variable." | |
| else: | |
| reset_loop() | |
| st.session_state.msgs["s1"] = "Scenario complete! You can generate a certificate below." | |
| safe_rerun() | |
| def all_loops_complete_for_current_scenario() -> bool: | |
| return all(st.session_state.progress[st.session_state.scenario]) | |
| # ============================== | |
| # Diagram (arrows removed) | |
| # ============================== | |
| def diagram_html(sensor_txt, control_txt, effector_txt): | |
| sx, sy, sw, sh = 110, 260, 240, 56 # Sensor (left) | |
| cx, cy, cw, ch = 420, 70, 220, 56 # Control (top) | |
| ex, ey, ew, eh = 740, 320, 260, 56 # Effector (right) | |
| base_x, base_y, base_w, base_h = 330, 520, 360, 26 | |
| base_mid_x = base_x + base_w/2 | |
| html = f""" | |
| <div style="position:relative;width:100%;background:#f6f5ff;border:1px solid #e3e3f8;border-radius:16px;overflow:hidden;"> | |
| <svg viewBox="0 0 1000 620" style="width:100%;height:auto;display:block" preserveAspectRatio="xMidYMid meet"> | |
| <rect x="{base_x}" y="{base_y}" width="{base_w}" height="{base_h}" rx="8" | |
| fill="none" stroke="#4b2bb3" stroke-width="5"/> | |
| <polygon points="{base_mid_x - 20},{base_y + base_h + 2} | |
| {base_mid_x + 20},{base_y + base_h + 2} | |
| {base_mid_x},{base_y + base_h + 44}" | |
| fill="#4b2bb3"/> | |
| <text x="{base_x + 70}" y="{base_y + base_h - 8}" font-size="16" | |
| text-anchor="middle" fill="#4b2bb3">Imbalance</text> | |
| <text x="{base_x + base_w - 70}" y="{base_y + base_h - 8}" font-size="16" | |
| text-anchor="middle" fill="#4b2bb3">Balance</text> | |
| <rect x="{sx}" y="{sy}" width="{sw}" height="{sh}" rx="12" fill="#e9ecff" stroke="#6b57e5"/> | |
| <text x="{sx+sw/2}" y="{sy+sh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{sensor_txt}</text> | |
| <rect x="{cx}" y="{cy}" width="{cw}" height="{ch}" rx="12" fill="#e9ecff" stroke="#6b57e5"/> | |
| <text x="{cx+cw/2}" y="{cy+ch/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{control_txt}</text> | |
| <rect x="{ex}" y="{ey}" width="{ew}" height="{eh}" rx="12" fill="#e9ecff" stroke="#6b57e5"/> | |
| <text x="{ex+ew/2}" y="{ey+eh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{effector_txt}</text> | |
| </svg> | |
| </div> | |
| """ | |
| return html | |
| # ============================== | |
| # Certificate | |
| # ============================== | |
| def generate_certificate_pdf(student_name: str, scenario_name: str, loops_done: list[str]) -> bytes: | |
| buf = BytesIO() | |
| c = canvas.Canvas(buf, pagesize=letter) | |
| W, H = letter | |
| c.setFont("Helvetica-Bold", 28) | |
| c.drawCentredString(W/2, H - 100, "Certificate of Completion") | |
| c.setFont("Helvetica", 13) | |
| c.drawCentredString(W/2, H - 140, "This certifies that") | |
| c.setFont("Helvetica-Bold", 20) | |
| c.drawCentredString(W/2, H - 170, student_name if student_name.strip() else "Student") | |
| c.setFont("Helvetica", 13) | |
| c.drawCentredString(W/2, H - 200, "has successfully completed the scenario") | |
| c.setFont("Helvetica-Bold", 16) | |
| c.drawCentredString(W/2, H - 225, scenario_name) | |
| c.setFont("Helvetica", 12) | |
| y = H - 270 | |
| c.drawCentredString(W/2, y, "Completed loops:") | |
| y -= 18 | |
| for lp in loops_done: | |
| c.drawCentredString(W/2, y, f"• {lp}") | |
| y -= 16 | |
| c.setFont("Helvetica-Oblique", 11) | |
| c.drawCentredString(W/2, 80, f"Issued on {datetime.now().strftime('%Y-%m-%d %H:%M')}") | |
| c.showPage(); c.save() | |
| buf.seek(0) | |
| return buf.read() | |
| # ============================== | |
| # UI | |
| # ============================== | |
| def init_and_render(): | |
| init_state() | |
| st.title("Core Concepts Review") | |
| scenario_list = list(SCENARIOS.keys()) | |
| idx = scenario_list.index(st.session_state.scenario) | |
| def on_change_scenario(): | |
| st.session_state.scenario = st.session_state["scenario_select"] | |
| st.session_state.loop_idx = 0 | |
| reset_loop() | |
| safe_rerun() | |
| top_col1, top_col2 = st.columns([2,1]) | |
| with top_col1: | |
| st.selectbox("Scenario", scenario_list, index=idx, key="scenario_select", on_change=on_change_scenario) | |
| with top_col2: | |
| if st.button("Reset Loop"): | |
| reset_loop() | |
| safe_rerun() | |
| cloop = current_loop() | |
| token = loop_token() | |
| ksfx = key_suffix() | |
| # ensure unlock entry for this loop | |
| if token not in st.session_state.unlock: | |
| st.session_state.unlock[token] = 1 | |
| # ---------- Diagram ---------- | |
| st.subheader(f"Stage 1 · Build the negative feedback loop — **{cloop['loop_name']}**") | |
| labels = st.session_state.assign | |
| components.html( | |
| diagram_html( | |
| labels['sensor'] or "Sensor", | |
| labels['control'] or "Control Center", | |
| labels['effector'] or "Effector(s)" | |
| ), | |
| height=660, scrolling=False | |
| ) | |
| st.markdown("---") | |
| # ---------- Placeholders for cumulative rendering ---------- | |
| ph1 = st.container() | |
| ph2 = st.container() | |
| ph3 = st.container() | |
| ph4 = st.container() | |
| # ---------------- Stage 1 (always visible) ---------------- | |
| with ph1: | |
| st.write("**Sensor options**") | |
| sel_s = st.radio("Sensor", cloop["sensor_options"], index=None, horizontal=True, key=f"s1_sensor_{ksfx}") | |
| if sel_s is not None: st.session_state.assign["sensor"] = sel_s | |
| st.write("**Control center options**") | |
| sel_c = st.radio("Control center", cloop["control_options"], index=None, horizontal=True, key=f"s1_control_{ksfx}") | |
| if sel_c is not None: st.session_state.assign["control"] = sel_c | |
| st.write("**Effector options**") | |
| sel_e = st.radio("Effector(s)", cloop["effector_options"], index=None, horizontal=True, key=f"s1_effector_{ksfx}") | |
| if sel_e is not None: st.session_state.assign["effector"] = sel_e | |
| if st.button("Check Stage 1", key=f"chk1_{ksfx}"): | |
| a = st.session_state.assign | |
| if not all([a["sensor"], a["control"], a["effector"]]): | |
| st.session_state.msgs["s1"] = "Please complete all answers." | |
| else: | |
| ok = ( | |
| a["sensor"] == cloop["sensor_correct"] and | |
| a["control"] == cloop["control_correct"] and | |
| a["effector"]== cloop["effector_correct"] | |
| ) | |
| if ok: | |
| st.session_state.msgs["s1"] = "Great! Proceed to Stage 2." | |
| st.session_state.unlock[token] = max(st.session_state.unlock.get(token,1), 2) | |
| safe_rerun() | |
| else: | |
| st.session_state.msgs["s1"] = "Please recheck your answers." | |
| st.info(st.session_state.msgs["s1"]) | |
| # helper | |
| def visible(stage_no: int) -> bool: | |
| return st.session_state.unlock.get(token, 1) >= stage_no | |
| # ---------------- Stage 2 ---------------- | |
| if visible(2): | |
| with ph2: | |
| st.subheader("Stage 2 · Sensor → Control Center") | |
| st.markdown(cloop["stage2_desc"]) | |
| sig = st.radio("Signaling", ["Autocrine","Paracrine","Endocrine"], index=None, horizontal=True, key=f"st2_sig_{ksfx}") | |
| rec = st.radio("Receptor", ["Cell membrane","Inside the cell"], index=None, horizontal=True, key=f"st2_rec_{ksfx}") | |
| grd = st.radio("Gradient (primary transport)", ["Concentration","Electrochemical","Pressure"], index=None, horizontal=True, key=f"st2_grad_{ksfx}") | |
| if st.button("Check Stage 2", key=f"chk2_{ksfx}"): | |
| if sig==cloop["stage2_sig"] and rec==cloop["stage2_rec"] and grd==cloop["stage2_grad"]: | |
| st.session_state.msgs["s2"] = "Correct! Continue to Stage 3." | |
| st.session_state.unlock[token] = max(st.session_state.unlock.get(token,2), 3) | |
| safe_rerun() | |
| else: | |
| st.session_state.msgs["s2"] = "Please recheck your answers." | |
| st.info(st.session_state.msgs["s2"]) | |
| # ---------------- Stage 3 ---------------- | |
| if visible(3): | |
| with ph3: | |
| st.subheader("Stage 3 · Control Center → Effectors") | |
| st.markdown(cloop["stage3_desc"]) | |
| sig3 = st.radio("Signaling", ["Autocrine","Paracrine","Endocrine"], index=None, horizontal=True, key=f"st3_sig_{ksfx}") | |
| rec3 = st.radio("Receptor", ["Cell membrane","Inside the cell"], index=None, horizontal=True, key=f"st3_rec_{ksfx}") | |
| grd3 = st.radio("Gradient (primary transport)", ["Concentration","Electrochemical","Pressure"], index=None, horizontal=True, key=f"st3_grad_{ksfx}") | |
| if st.button("Check Stage 3", key=f"chk3_{ksfx}"): | |
| if sig3==cloop["stage3_sig"] and rec3==cloop["stage3_rec"] and grd3==cloop["stage3_grad"]: | |
| st.session_state.msgs["s3"] = "Nice! Final question…" | |
| st.session_state.unlock[token] = max(st.session_state.unlock.get(token,3), 4) | |
| safe_rerun() | |
| else: | |
| st.session_state.msgs["s3"] = "Please recheck your answers." | |
| st.info(st.session_state.msgs["s3"]) | |
| # ---------------- Stage 4 ---------------- | |
| if visible(4): | |
| with ph4: | |
| st.subheader("Stage 4 · Outcome") | |
| st.markdown(f"**{cloop['outcome_question']}**") | |
| ans = st.radio("Choose one:", cloop["outcome_options"], index=None, key=f"st4_ans_{ksfx}") | |
| if st.button("Finish Loop", key=f"finish_{ksfx}"): | |
| if ans == cloop["outcome_correct"]: | |
| st.session_state.msgs["s4"] = "✅ Correct." | |
| next_loop_or_finish() | |
| else: | |
| st.session_state.msgs["s4"] = "Please recheck your answers." | |
| st.info(st.session_state.msgs["s4"]) | |
| # ---------- CERTIFICATE (bottom only) ---------- | |
| st.markdown("---") | |
| if all_loops_complete_for_current_scenario() and REPORTLAB_AVAILABLE: | |
| st.success("Scenario complete. Generate your PDF certificate below.") | |
| student_name = st.text_input("Student name for certificate", "") | |
| loops_list = [lp["loop_name"] for lp in SCENARIOS[st.session_state.scenario]] | |
| if st.button("Generate PDF Certificate", key=f"cert_{ksfx}"): | |
| pdf_bytes = generate_certificate_pdf(student_name, st.session_state.scenario, loops_list) | |
| st.download_button( | |
| "Download certificate", | |
| data=pdf_bytes, | |
| file_name=f"certificate_{st.session_state.scenario.replace(' ','_')}.pdf", | |
| mime="application/pdf" | |
| ) | |
| elif all_loops_complete_for_current_scenario() and not REPORTLAB_AVAILABLE: | |
| st.warning("Certificates are disabled (reportlab not available). Add 'reportlab' to requirements.txt.") | |
| st.caption("Tip: Use 'Reset Loop' to try again or switch scenarios above.") | |
| # Run | |
| init_and_render() | |