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"""
""" 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()