Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
-
st.caption("Loaded: app.py (root)") # or "Loaded: src/streamlit_app.py"
|
| 3 |
from io import BytesIO
|
| 4 |
from datetime import datetime
|
| 5 |
|
|
@@ -114,7 +113,7 @@ SCENARIOS = {
|
|
| 114 |
"control_options": ["Kidney (local tubular control)", "Pancreas", "Medulla"],
|
| 115 |
"control_correct": "Kidney (local tubular control)",
|
| 116 |
"effector_options":[
|
| 117 |
-
"Glucose excretion into the urine increases", #
|
| 118 |
"Renal glucose reabsorption increases",
|
| 119 |
"Insulin secretion increases"
|
| 120 |
],
|
|
@@ -217,10 +216,35 @@ def init_state():
|
|
| 217 |
def loop():
|
| 218 |
return SCENARIOS[st.session_state.scenario][st.session_state.loop_idx]
|
| 219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
def reset_loop():
|
| 221 |
st.session_state.assign = {"sensor": None, "control": None, "effector": None}
|
| 222 |
st.session_state.stage = 1
|
| 223 |
st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
def next_loop_or_finish():
|
| 226 |
st.session_state.progress[st.session_state.scenario][st.session_state.loop_idx] = True
|
|
@@ -230,61 +254,107 @@ def next_loop_or_finish():
|
|
| 230 |
st.session_state.msgs["s1"] = "Great—now build the second loop for this variable."
|
| 231 |
else:
|
| 232 |
st.session_state.msgs["s1"] = "Scenario complete! You can generate a certificate below."
|
|
|
|
| 233 |
|
| 234 |
def all_loops_complete_for_current_scenario() -> bool:
|
| 235 |
return all(st.session_state.progress[st.session_state.scenario])
|
| 236 |
|
| 237 |
-
def set_scenario(name):
|
| 238 |
-
st.session_state.scenario = name
|
| 239 |
-
st.session_state.loop_idx = 0
|
| 240 |
-
reset_loop()
|
| 241 |
-
|
| 242 |
# ==============================
|
| 243 |
-
# Diagram (
|
| 244 |
# ==============================
|
| 245 |
def diagram_svg(sensor_txt, control_txt, effector_txt):
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
#
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
svg = f"""
|
| 270 |
<div style="position:relative;width:100%;background:#f6f5ff;border:1px solid #e3e3f8;border-radius:16px;overflow:hidden;">
|
| 271 |
-
<svg viewBox="0 0 1000 600" style="width:100%;height:auto;display:block">
|
| 272 |
<defs>
|
| 273 |
-
<marker id="arrow" markerWidth="
|
| 274 |
-
<path d="M0,0 L0,
|
| 275 |
</marker>
|
| 276 |
</defs>
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
<path d="{
|
| 280 |
-
|
| 281 |
-
<
|
| 282 |
-
|
| 283 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
<rect x="{sx}" y="{sy}" width="{sw}" height="{sh}" rx="10" fill="#e9ecff" stroke="#6b57e5"/>
|
| 285 |
<text x="{sx+sw/2}" y="{sy+sh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{sensor_txt}</text>
|
|
|
|
| 286 |
<rect x="{cx}" y="{cy}" width="{cw}" height="{ch}" rx="10" fill="#e9ecff" stroke="#6b57e5"/>
|
| 287 |
<text x="{cx+cw/2}" y="{cy+ch/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{control_txt}</text>
|
|
|
|
| 288 |
<rect x="{ex}" y="{ey}" width="{ew}" height="{eh}" rx="10" fill="#e9ecff" stroke="#6b57e5"/>
|
| 289 |
<text x="{ex+ew/2}" y="{ey+eh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{effector_txt}</text>
|
| 290 |
</svg>
|
|
@@ -341,17 +411,20 @@ def init_and_render():
|
|
| 341 |
|
| 342 |
st.title("🧩 Interdependence Concept Game")
|
| 343 |
|
| 344 |
-
# Scenario
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
with
|
|
|
|
|
|
|
| 353 |
if st.button("Reset Loop"):
|
| 354 |
reset_loop()
|
|
|
|
| 355 |
|
| 356 |
# Stage 1
|
| 357 |
current_loop = loop()
|
|
@@ -397,7 +470,7 @@ def init_and_render():
|
|
| 397 |
|
| 398 |
# Stage 2 — Sensor → Control
|
| 399 |
if st.session_state.stage >= 2:
|
| 400 |
-
st.subheader("Stage 2 ·
|
| 401 |
st.markdown(current_loop["stage2_desc"])
|
| 402 |
sig = st.radio("Signaling", ["Autocrine","Paracrine","Endocrine"], horizontal=True, key="st2_sig")
|
| 403 |
rec = st.radio("Receptor", ["Cell membrane","Inside the cell"], horizontal=True, key="st2_rec")
|
|
@@ -415,9 +488,9 @@ def init_and_render():
|
|
| 415 |
st.session_state.msgs["s2"] = msg
|
| 416 |
st.info(st.session_state.msgs["s2"])
|
| 417 |
|
| 418 |
-
# Stage 3 — Control → Effector
|
| 419 |
if st.session_state.stage >= 3:
|
| 420 |
-
st.subheader("Stage 3 ·
|
| 421 |
st.markdown(current_loop["stage3_desc"])
|
| 422 |
sig3 = st.radio("Signaling", ["Autocrine","Paracrine","Endocrine"], horizontal=True, key="st3_sig")
|
| 423 |
rec3 = st.radio("Receptor", ["Cell membrane","Inside the cell"], horizontal=True, key="st3_rec")
|
|
@@ -435,7 +508,7 @@ def init_and_render():
|
|
| 435 |
st.session_state.msgs["s3"] = msg
|
| 436 |
st.info(st.session_state.msgs["s3"])
|
| 437 |
|
| 438 |
-
# Stage 4 — Outcome
|
| 439 |
if st.session_state.stage >= 4:
|
| 440 |
st.subheader("Stage 4 · Outcome")
|
| 441 |
st.markdown(f"**{current_loop['outcome_question']}**")
|
|
|
|
| 1 |
import streamlit as st
|
|
|
|
| 2 |
from io import BytesIO
|
| 3 |
from datetime import datetime
|
| 4 |
|
|
|
|
| 113 |
"control_options": ["Kidney (local tubular control)", "Pancreas", "Medulla"],
|
| 114 |
"control_correct": "Kidney (local tubular control)",
|
| 115 |
"effector_options":[
|
| 116 |
+
"Glucose excretion into the urine increases", # (we use this phrasing per your request)
|
| 117 |
"Renal glucose reabsorption increases",
|
| 118 |
"Insulin secretion increases"
|
| 119 |
],
|
|
|
|
| 216 |
def loop():
|
| 217 |
return SCENARIOS[st.session_state.scenario][st.session_state.loop_idx]
|
| 218 |
|
| 219 |
+
# Keys used by widgets that need clearing on reset / loop change
|
| 220 |
+
WIDGET_KEYS = [
|
| 221 |
+
"s1_sensor", "s1_control", "s1_effector",
|
| 222 |
+
"st2_sig", "st2_rec", "st2_grad",
|
| 223 |
+
"st3_sig", "st3_rec", "st3_grad",
|
| 224 |
+
"st4_ans", "scenario_select"
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
def clear_widget_keys():
|
| 228 |
+
for k in WIDGET_KEYS:
|
| 229 |
+
if k in st.session_state:
|
| 230 |
+
del st.session_state[k]
|
| 231 |
+
|
| 232 |
+
def safe_rerun():
|
| 233 |
+
try:
|
| 234 |
+
st.rerun()
|
| 235 |
+
except Exception:
|
| 236 |
+
st.experimental_rerun()
|
| 237 |
+
|
| 238 |
def reset_loop():
|
| 239 |
st.session_state.assign = {"sensor": None, "control": None, "effector": None}
|
| 240 |
st.session_state.stage = 1
|
| 241 |
st.session_state.msgs = {"s1":"", "s2":"", "s3":"", "s4":""}
|
| 242 |
+
clear_widget_keys()
|
| 243 |
+
|
| 244 |
+
def set_scenario(name):
|
| 245 |
+
st.session_state.scenario = name
|
| 246 |
+
st.session_state.loop_idx = 0
|
| 247 |
+
reset_loop()
|
| 248 |
|
| 249 |
def next_loop_or_finish():
|
| 250 |
st.session_state.progress[st.session_state.scenario][st.session_state.loop_idx] = True
|
|
|
|
| 254 |
st.session_state.msgs["s1"] = "Great—now build the second loop for this variable."
|
| 255 |
else:
|
| 256 |
st.session_state.msgs["s1"] = "Scenario complete! You can generate a certificate below."
|
| 257 |
+
safe_rerun() # immediately show the next loop or the certificate UI
|
| 258 |
|
| 259 |
def all_loops_complete_for_current_scenario() -> bool:
|
| 260 |
return all(st.session_state.progress[st.session_state.scenario])
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
# ==============================
|
| 263 |
+
# Diagram (FOUR arrows)
|
| 264 |
# ==============================
|
| 265 |
def diagram_svg(sensor_txt, control_txt, effector_txt):
|
| 266 |
+
# Box geometry
|
| 267 |
+
sx, sy, sw, sh = 90, 260, 240, 56 # Sensor (left)
|
| 268 |
+
cx, cy, cw, ch = 410, 60, 220, 56 # Control (top)
|
| 269 |
+
ex, ey, ew, eh = 740, 320, 260, 56 # Effector (right)
|
| 270 |
+
|
| 271 |
+
# Anchors
|
| 272 |
+
sL = (sx, sy + sh/2) # sensor LEFT-mid
|
| 273 |
+
sR = (sx + sw, sy + sh/2) # sensor RIGHT-mid
|
| 274 |
+
cL = (cx, cy + ch/2) # control LEFT-mid
|
| 275 |
+
cR = (cx + cw, cy + ch/2) # control RIGHT-mid
|
| 276 |
+
cB = (cx + cw/2, cy + ch) # control BOTTOM-mid
|
| 277 |
+
eL = (ex, ey + eh/2) # effector LEFT-mid
|
| 278 |
+
eB = (ex + ew/2, ey + eh) # effector BOTTOM-mid
|
| 279 |
+
|
| 280 |
+
# Baseline
|
| 281 |
+
base_x, base_y, base_w, base_h = 330, 508, 360, 26
|
| 282 |
+
base_mid_x = base_x + base_w/2
|
| 283 |
+
base_imb_anchor = (base_x + 20, base_y + base_h/2) # arrow #1 start
|
| 284 |
+
base_bal_anchor = (base_x + base_w - 20, base_y + base_h/2) # arrow #4 end
|
| 285 |
+
|
| 286 |
+
# Arrow paths (cubic Béziers)
|
| 287 |
+
# (1) Imbalance -> Sensor (to left side)
|
| 288 |
+
imbs_c1 = (base_imb_anchor[0] + 60, base_imb_anchor[1] - 40)
|
| 289 |
+
imbs_c2 = (sL[0] - 80, sL[1] + 20)
|
| 290 |
+
path_imb_to_s = (
|
| 291 |
+
f"M {base_imb_anchor[0]} {base_imb_anchor[1]} "
|
| 292 |
+
f"C {imbs_c1[0]} {imbs_c1[1]}, {imbs_c2[0]} {imbs_c2[1]}, {sL[0]} {sL[1]}"
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
# (2) Sensor -> Control (sensor RIGHT to control BOTTOM)
|
| 296 |
+
sc_c1 = (sR[0] + 70, sR[1] - 100)
|
| 297 |
+
sc_c2 = (cB[0] - 70, cB[1] - 100)
|
| 298 |
+
path_s_to_c = (
|
| 299 |
+
f"M {sR[0]} {sR[1]} "
|
| 300 |
+
f"C {sc_c1[0]} {sc_c1[1]}, {sc_c2[0]} {sc_c2[1]}, {cB[0]} {cB[1]}"
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
# (3) Control -> Effector (control RIGHT to effector LEFT)
|
| 304 |
+
ce_c1 = (cR[0] + 80, cR[1] + 40)
|
| 305 |
+
ce_c2 = (eL[0] - 80, eL[1] - 40)
|
| 306 |
+
path_c_to_e = (
|
| 307 |
+
f"M {cR[0]} {cR[1]} "
|
| 308 |
+
f"C {ce_c1[0]} {ce_c1[1]}, {ce_c2[0]} {ce_c2[1]}, {eL[0]} {eL[1]}"
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# (4) Effector -> Balance (effector BOTTOM to baseline RIGHT)
|
| 312 |
+
eb_c1 = (eB[0] - 120, eB[1] + 20)
|
| 313 |
+
eb_c2 = (base_bal_anchor[0] - 80, base_bal_anchor[1] - 20)
|
| 314 |
+
path_e_to_bal = (
|
| 315 |
+
f"M {eB[0]} {eB[1]} "
|
| 316 |
+
f"C {eb_c1[0]} {eb_c1[1]}, {eb_c2[0]} {eb_c2[1]}, "
|
| 317 |
+
f"{base_bal_anchor[0]} {base_bal_anchor[1]}"
|
| 318 |
+
)
|
| 319 |
|
| 320 |
svg = f"""
|
| 321 |
<div style="position:relative;width:100%;background:#f6f5ff;border:1px solid #e3e3f8;border-radius:16px;overflow:hidden;">
|
| 322 |
+
<svg viewBox="0 0 1000 600" style="width:100%;height:auto;display:block" preserveAspectRatio="xMidYMid meet">
|
| 323 |
<defs>
|
| 324 |
+
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto" markerUnits="strokeWidth">
|
| 325 |
+
<path d="M0,0 L0,8 L8,4 z" fill="#4b2bb3"/>
|
| 326 |
</marker>
|
| 327 |
</defs>
|
| 328 |
+
|
| 329 |
+
<!-- Four arrows -->
|
| 330 |
+
<path d="{path_imb_to_s}" fill="none" stroke="#4b2bb3" stroke-width="5"
|
| 331 |
+
stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow)"/>
|
| 332 |
+
<path d="{path_s_to_c}" fill="none" stroke="#4b2bb3" stroke-width="5"
|
| 333 |
+
stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow)"/>
|
| 334 |
+
<path d="{path_c_to_e}" fill="none" stroke="#4b2bb3" stroke-width="5"
|
| 335 |
+
stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow)"/>
|
| 336 |
+
<path d="{path_e_to_bal}" fill="none" stroke="#4b2bb3" stroke-width="5"
|
| 337 |
+
stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow)"/>
|
| 338 |
+
|
| 339 |
+
<!-- Baseline -->
|
| 340 |
+
<rect x="{base_x}" y="{base_y}" width="{base_w}" height="{base_h}" rx="6"
|
| 341 |
+
fill="none" stroke="#4b2bb3" stroke-width="5"/>
|
| 342 |
+
<polygon points="{base_mid_x - 20},{base_y + base_h + 2}
|
| 343 |
+
{base_mid_x + 20},{base_y + base_h + 2}
|
| 344 |
+
{base_mid_x},{base_y + base_h + 44}"
|
| 345 |
+
fill="#4b2bb3"/>
|
| 346 |
+
<text x="{base_x + 70}" y="{base_y + base_h - 8}" font-size="16"
|
| 347 |
+
text-anchor="middle" fill="#4b2bb3">Imbalance</text>
|
| 348 |
+
<text x="{base_x + base_w - 70}" y="{base_y + base_h - 8}" font-size="16"
|
| 349 |
+
text-anchor="middle" fill="#4b2bb3">Balance</text>
|
| 350 |
+
|
| 351 |
+
<!-- Boxes -->
|
| 352 |
<rect x="{sx}" y="{sy}" width="{sw}" height="{sh}" rx="10" fill="#e9ecff" stroke="#6b57e5"/>
|
| 353 |
<text x="{sx+sw/2}" y="{sy+sh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{sensor_txt}</text>
|
| 354 |
+
|
| 355 |
<rect x="{cx}" y="{cy}" width="{cw}" height="{ch}" rx="10" fill="#e9ecff" stroke="#6b57e5"/>
|
| 356 |
<text x="{cx+cw/2}" y="{cy+ch/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{control_txt}</text>
|
| 357 |
+
|
| 358 |
<rect x="{ex}" y="{ey}" width="{ew}" height="{eh}" rx="10" fill="#e9ecff" stroke="#6b57e5"/>
|
| 359 |
<text x="{ex+ew/2}" y="{ey+eh/2+6}" font-size="16" text-anchor="middle" fill="#1f1d2e">{effector_txt}</text>
|
| 360 |
</svg>
|
|
|
|
| 411 |
|
| 412 |
st.title("🧩 Interdependence Concept Game")
|
| 413 |
|
| 414 |
+
# Scenario select with on_change that truly switches
|
| 415 |
+
scenario_list = list(SCENARIOS.keys())
|
| 416 |
+
def on_change_scenario():
|
| 417 |
+
set_scenario(st.session_state["scenario_select"])
|
| 418 |
+
safe_rerun()
|
| 419 |
+
|
| 420 |
+
idx = scenario_list.index(st.session_state.scenario)
|
| 421 |
+
top_col1, top_col2 = st.columns([2,1])
|
| 422 |
+
with top_col1:
|
| 423 |
+
st.selectbox("Scenario", scenario_list, index=idx, key="scenario_select", on_change=on_change_scenario)
|
| 424 |
+
with top_col2:
|
| 425 |
if st.button("Reset Loop"):
|
| 426 |
reset_loop()
|
| 427 |
+
safe_rerun()
|
| 428 |
|
| 429 |
# Stage 1
|
| 430 |
current_loop = loop()
|
|
|
|
| 470 |
|
| 471 |
# Stage 2 — Sensor → Control
|
| 472 |
if st.session_state.stage >= 2:
|
| 473 |
+
st.subheader("Stage 2 · Sensor → Control Center")
|
| 474 |
st.markdown(current_loop["stage2_desc"])
|
| 475 |
sig = st.radio("Signaling", ["Autocrine","Paracrine","Endocrine"], horizontal=True, key="st2_sig")
|
| 476 |
rec = st.radio("Receptor", ["Cell membrane","Inside the cell"], horizontal=True, key="st2_rec")
|
|
|
|
| 488 |
st.session_state.msgs["s2"] = msg
|
| 489 |
st.info(st.session_state.msgs["s2"])
|
| 490 |
|
| 491 |
+
# Stage 3 — Control → Effector (title without "Zoom")
|
| 492 |
if st.session_state.stage >= 3:
|
| 493 |
+
st.subheader("Stage 3 · Control Center → Effectors")
|
| 494 |
st.markdown(current_loop["stage3_desc"])
|
| 495 |
sig3 = st.radio("Signaling", ["Autocrine","Paracrine","Endocrine"], horizontal=True, key="st3_sig")
|
| 496 |
rec3 = st.radio("Receptor", ["Cell membrane","Inside the cell"], horizontal=True, key="st3_rec")
|
|
|
|
| 508 |
st.session_state.msgs["s3"] = msg
|
| 509 |
st.info(st.session_state.msgs["s3"])
|
| 510 |
|
| 511 |
+
# Stage 4 — Outcome (title already fine)
|
| 512 |
if st.session_state.stage >= 4:
|
| 513 |
st.subheader("Stage 4 · Outcome")
|
| 514 |
st.markdown(f"**{current_loop['outcome_question']}**")
|