ProfRick's picture
Update app.py
4ca3ada verified
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()