nursing-case-studies / streamlit_app.py
Lincoln Gombedza
Initial commit: Nursing Case Study Builder (Tool #5)
95e4355
"""
Nursing Case Study Builder
Streamlit app β€” Hugging Face Spaces (free CPU tier)
"""
import streamlit as st
from cases.bank import (
get_all_cases, get_categories, get_by_category, get_by_difficulty,
get_by_id, search_cases, get_difficulties,
)
# ---------------------------------------------------------------------------
# Page config
# ---------------------------------------------------------------------------
st.set_page_config(
page_title="Nursing Case Studies β€” Student Nurses",
page_icon="πŸ₯",
layout="wide",
initial_sidebar_state="expanded",
)
# ---------------------------------------------------------------------------
# CSS
# ---------------------------------------------------------------------------
st.markdown("""
<style>
.case-card {
background:#f8fafc; border:1px solid #d0dae8;
border-radius:10px; padding:1.2rem 1.4rem; margin-bottom:1rem;
}
.patient-banner {
background:#e3f2fd; border-left:5px solid #1565c0;
padding:0.8rem 1.2rem; border-radius:4px; margin-bottom:0.8rem;
}
.warning-banner {
background:#fff3e0; border-left:5px solid #e65100;
padding:0.8rem 1.2rem; border-radius:4px; margin-bottom:0.8rem;
}
.critical-banner {
background:#fce4ec; border-left:5px solid #c62828;
padding:0.8rem 1.2rem; border-radius:4px; margin-bottom:0.8rem;
}
.success-banner {
background:#e8f5e9; border-left:5px solid #2e7d32;
padding:0.8rem 1.2rem; border-radius:4px; margin-bottom:0.8rem;
}
.info-banner {
background:#e8eaf6; border-left:5px solid #3949ab;
padding:0.8rem 1.2rem; border-radius:4px; margin-bottom:0.8rem;
}
.badge-beginner { background:#e8f5e9; color:#2e7d32; padding:3px 10px;
border-radius:12px; font-size:0.78em; font-weight:700; }
.badge-intermediate { background:#fff8e1; color:#f57f17; padding:3px 10px;
border-radius:12px; font-size:0.78em; font-weight:700; }
.badge-advanced { background:#fce4ec; color:#c62828; padding:3px 10px;
border-radius:12px; font-size:0.78em; font-weight:700; }
.vital-box {
background:#f5f5f5; border-radius:8px; padding:0.6rem 1rem;
text-align:center; margin-bottom:0.4rem;
}
.nd-card {
background:#fafafa; border:1px solid #e0e0e0;
border-radius:8px; padding:1rem 1.2rem; margin-bottom:0.8rem;
}
</style>
""", unsafe_allow_html=True)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
DIFF_COLOURS = {"Beginner": "#2e7d32", "Intermediate": "#f57f17", "Advanced": "#c62828"}
CAT_ICONS = {
"Cardiovascular": "❀️",
"Respiratory": "🫁",
"Endocrine": "🩸",
"Neurological": "🧠",
"Multi-system / Infectious": "🦠",
"Musculoskeletal / Surgical": "🦴",
"Maternal / Obstetric": "🀰",
"Paediatric": "πŸ‘Ά",
"Mental Health / Toxicology": "πŸ’œ",
"Renal": "πŸ’§",
}
def diff_badge(difficulty: str) -> str:
cls = f"badge-{difficulty.lower()}"
return f'<span class="{cls}">{difficulty}</span>'
def cat_icon(category: str) -> str:
return CAT_ICONS.get(category, "πŸ“‹")
def init_state():
if "selected_case_id" not in st.session_state:
st.session_state.selected_case_id = None
if "adpie_answers" not in st.session_state:
st.session_state.adpie_answers = {}
if "quiz_answers" not in st.session_state:
st.session_state.quiz_answers = {}
if "quiz_submitted" not in st.session_state:
st.session_state.quiz_submitted = False
if "careplan_items" not in st.session_state:
st.session_state.careplan_items = {}
init_state()
# ---------------------------------------------------------------------------
# Sidebar
# ---------------------------------------------------------------------------
with st.sidebar:
st.markdown("## πŸ₯ Case Studies")
st.divider()
all_cases = get_all_cases()
categories = get_categories()
difficulties = get_difficulties()
st.markdown("**Quick Stats**")
st.markdown(f"- πŸ“‹ {len(all_cases)} clinical cases")
st.markdown(f"- 🏷️ {len(categories)} body systems")
diff_counts = {d: len(get_by_difficulty(d)) for d in difficulties}
for d, n in diff_counts.items():
colour = DIFF_COLOURS[d]
st.markdown(f'- <span style="color:{colour}">⬀</span> {d}: {n} cases', unsafe_allow_html=True)
st.divider()
st.markdown("**Cases by System**")
for cat in categories:
icon = cat_icon(cat)
n = len(get_by_category(cat))
st.markdown(f"{icon} **{cat}** β€” {n}")
st.divider()
st.markdown("**ADPIE Framework**")
st.markdown("""
- **A**ssessment
- **D**iagnosis
- **P**lanning
- **I**mplementation
- **E**valuation
""")
st.divider()
st.caption(
"Part of the [Nursing Citizen Development](https://huggingface.co/NurseCitizenDeveloper) suite"
)
# ---------------------------------------------------------------------------
# Header
# ---------------------------------------------------------------------------
st.title("πŸ₯ Nursing Case Studies")
st.caption(
"10 clinical scenarios across major body systems Β· ADPIE Framework Β· "
"Nursing Diagnoses Β· Care Planning Β· For educational purposes only"
)
# ---------------------------------------------------------------------------
# Tabs
# ---------------------------------------------------------------------------
tab_lib, tab_case, tab_adpie, tab_quiz, tab_plan = st.tabs([
"πŸ“š Case Library",
"πŸ” Full Case",
"🧠 ADPIE Reasoning",
"❓ Quiz Questions",
"πŸ“ Care Plan",
])
# ========================= CASE LIBRARY =====================================
with tab_lib:
st.subheader("πŸ“š Clinical Case Library")
st.caption("Browse 10 evidence-based patient scenarios. Click any case to open it.")
col_search, col_diff, col_cat = st.columns([3, 2, 2])
with col_search:
lib_query = st.text_input("πŸ” Search cases", placeholder="e.g. sepsis, chest pain, stroke")
with col_diff:
diff_filter = st.selectbox("Difficulty", ["All"] + difficulties)
with col_cat:
cat_filter = st.selectbox("Body system", ["All"] + categories)
st.divider()
# Filter
if lib_query.strip():
display_cases = search_cases(lib_query)
if not display_cases:
st.info(f"No cases found for '{lib_query}'.")
elif diff_filter != "All" and cat_filter != "All":
display_cases = [c for c in get_by_category(cat_filter)
if c["difficulty"] == diff_filter]
elif diff_filter != "All":
display_cases = get_by_difficulty(diff_filter)
elif cat_filter != "All":
display_cases = get_by_category(cat_filter)
else:
display_cases = all_cases
st.markdown(f"**Showing {len(display_cases)} case(s)**")
st.markdown("")
for case in display_cases:
icon = cat_icon(case["category"])
col_a, col_b = st.columns([5, 1])
with col_a:
st.markdown(
f'<div class="case-card">'
f'<b>{icon} {case["title"]}</b> &nbsp;'
f'{diff_badge(case["difficulty"])}<br/>'
f'<small>🏷️ {case["category"]} &nbsp;·&nbsp; '
f'πŸ‘€ {case["patient"]["name"]}, {case["patient"]["age"]}yo &nbsp;Β·&nbsp; '
f'πŸ”– {", ".join(case["tags"][:4])}</small>'
f'</div>',
unsafe_allow_html=True
)
with col_b:
if st.button("Open β†’", key=f"open_{case['id']}", use_container_width=True):
st.session_state.selected_case_id = case["id"]
st.session_state.quiz_answers = {}
st.session_state.quiz_submitted = False
st.session_state.adpie_answers = {}
st.success(f"βœ… Case loaded: **{case['title']}** β€” navigate to other tabs.")
# ========================= FULL CASE ========================================
with tab_case:
st.subheader("πŸ” Full Case Study")
if not st.session_state.selected_case_id:
st.info("πŸ’‘ Select a case from the **Case Library** tab to begin.")
else:
case = get_by_id(st.session_state.selected_case_id)
if not case:
st.error("Case not found.")
else:
p = case["patient"]
# Title banner
icon = cat_icon(case["category"])
st.markdown(
f'<div class="patient-banner">'
f'<h3 style="margin:0">{icon} {case["title"]}</h3>'
f'<span>{diff_badge(case["difficulty"])}</span> &nbsp;'
f'<small>{case["category"]}</small>'
f'</div>',
unsafe_allow_html=True
)
# Learning objectives
with st.expander("🎯 Learning Objectives", expanded=False):
for lo in case["learning_objectives"]:
st.markdown(f"β€’ {lo}")
st.divider()
# Patient demographics
col1, col2 = st.columns(2)
with col1:
st.markdown("### πŸ‘€ Patient Profile")
st.markdown(f"**Name:** {p['name']}")
st.markdown(f"**Age / Gender:** {p['age']} years Β· {p['gender']}")
st.markdown(f"**Weight / Height:** {p['weight_kg']} kg Β· {p['height_cm']} cm")
st.markdown(f"**Allergies:** {', '.join(p['allergies'])}")
with col2:
st.markdown("### πŸ“‹ Medical History")
st.markdown("**Past Medical History:**")
for h in p["pmhx"]:
st.markdown(f"β€’ {h}")
st.markdown(f"**Medications:** {', '.join(p['medications'])}")
st.markdown(f"**Social:** {p['social']}")
st.markdown(f"**Family History:** {p.get('family_hx', 'Nil relevant')}")
st.divider()
# Presentation
st.markdown("### 🚨 Presentation")
st.markdown(
f'<div class="warning-banner">πŸ“‹ {case["presentation"]}</div>',
unsafe_allow_html=True
)
# Vital signs
st.markdown("### πŸ“Š Vital Signs")
vitals = case["vitals"]
v_keys = list(vitals.keys())
cols = st.columns(min(len(v_keys), 5))
for i, key in enumerate(v_keys):
with cols[i % len(cols)]:
st.markdown(
f'<div class="vital-box"><small>{key}</small><br/>'
f'<b>{vitals[key]}</b></div>',
unsafe_allow_html=True
)
# Physical exam
st.markdown("### 🩺 Physical Examination")
st.markdown(case["physical_exam"])
st.divider()
# Investigations
st.markdown("### πŸ”¬ Investigations")
inv = case["investigations"]
inv_keys = list(inv.keys())
col_a, col_b = st.columns(2)
for i, key in enumerate(inv_keys):
col = col_a if i % 2 == 0 else col_b
with col:
val = inv[key]
flag = "πŸ”΄ " if ("↑↑" in val or "CRITICAL" in val or "severe" in val.lower()) else ""
st.markdown(f"**{key}:** {flag}{val}")
st.divider()
# Medical diagnosis
st.markdown("### πŸ₯ Medical Diagnosis")
st.markdown(
f'<div class="critical-banner">βš•οΈ <b>{case["medical_diagnosis"]}</b></div>',
unsafe_allow_html=True
)
# Nursing priorities
st.markdown("### πŸ”” Nursing Priorities")
for i, priority in enumerate(case["nursing_priorities"], 1):
colour = "#c62828" if i <= 3 else "#e65100" if i <= 6 else "#2e7d32"
st.markdown(
f'<div style="padding:4px 0"><span style="color:{colour};font-weight:700">'
f'{i}.</span> {priority}</div>',
unsafe_allow_html=True
)
# ========================= ADPIE REASONING ==================================
with tab_adpie:
st.subheader("🧠 ADPIE Clinical Reasoning Framework")
st.caption("Work through the case using the ADPIE nursing process. Reveal the model answer when ready.")
if not st.session_state.selected_case_id:
st.info("πŸ’‘ Select a case from the **Case Library** tab first.")
else:
case = get_by_id(st.session_state.selected_case_id)
adpie = case["adpie"]
st.markdown(
f'<div class="patient-banner">πŸ₯ Active Case: <b>{case["title"]}</b> &nbsp;Β·&nbsp; '
f'πŸ‘€ {case["patient"]["name"]}</div>',
unsafe_allow_html=True
)
st.markdown("**Instructions:** Read the case, then write your own answers before revealing the model answers.")
st.divider()
steps = [
("A", "Assessment", "πŸ“‹", "What are the key subjective and objective findings?", "assessment"),
("D", "Nursing Diagnosis", "πŸ”΄", "Identify priority nursing diagnoses (NANDA format: problem r/t aetiology AEB evidence)", "diagnosis"),
("P", "Planning", "🎯", "What are your SMART nursing goals (short and long term)?", "planning"),
("I", "Implementation", "βš™οΈ", "What nursing interventions will you implement and why?", "implementation"),
("E", "Evaluation", "βœ…", "How will you evaluate if goals were met? What actually happened?", "evaluation"),
]
for letter, title, icon, prompt, key in steps:
st.markdown(f"### {icon} {letter} β€” {title}")
st.markdown(f"*{prompt}*")
# Student input
ans_key = f"adpie_{case['id']}_{key}"
student_ans = st.text_area(
f"Your {title}:",
value=st.session_state.adpie_answers.get(ans_key, ""),
height=100,
key=f"adpie_input_{case['id']}_{key}",
placeholder=f"Write your {title.lower()} here...",
label_visibility="collapsed",
)
st.session_state.adpie_answers[ans_key] = student_ans
# Reveal model answer
with st.expander(f"πŸ” Reveal Model Answer β€” {title}"):
st.markdown(adpie[key])
st.markdown("")
# Nursing diagnoses deep-dive
st.divider()
st.markdown("### πŸ”΄ Nursing Diagnoses β€” Detailed")
for nd in case["nursing_diagnoses"]:
with st.expander(f"πŸ“Œ {nd['diagnosis']}", expanded=False):
st.markdown(
f'<div class="nd-card">'
f'<b>Supporting Evidence:</b> {nd["evidence"]}<br/><br/>'
f'<b>Goal:</b> {nd["goal"]}'
f'</div>',
unsafe_allow_html=True
)
st.markdown("**Nursing Interventions:**")
for inv in nd["interventions"]:
st.markdown(f"β€’ {inv}")
# ========================= QUIZ QUESTIONS ===================================
with tab_quiz:
st.subheader("❓ Case-Based Quiz Questions")
st.caption("NCLEX-style questions based on the active case. Select answers and submit for feedback.")
if not st.session_state.selected_case_id:
st.info("πŸ’‘ Select a case from the **Case Library** tab first.")
else:
case = get_by_id(st.session_state.selected_case_id)
st.markdown(
f'<div class="patient-banner">πŸ₯ Active Case: <b>{case["title"]}</b></div>',
unsafe_allow_html=True
)
# Generate case-specific questions dynamically
nd_names = [nd["diagnosis"].split(" related to")[0] for nd in case["nursing_diagnoses"]]
priorities = case["nursing_priorities"]
# Build 5 consistent case-based questions
questions = [
{
"q": f"Based on the case of {case['patient']['name']}, which nursing diagnosis should be prioritised FIRST?",
"options": nd_names + ["Pain management only"],
"answer": 0,
"rationale": (
f"The priority nursing diagnosis is '{nd_names[0]}'. "
f"Using Maslow's hierarchy, physiological and safety needs are addressed first. "
f"The supporting evidence is: {case['nursing_diagnoses'][0]['evidence']}."
),
},
{
"q": f"What is the FIRST priority nursing action for {case['patient']['name']}?",
"options": [
priorities[0] if len(priorities) > 0 else "Call the doctor",
priorities[2] if len(priorities) > 2 else "Reassess vitals",
"Complete full medication history",
"Discharge planning",
],
"answer": 0,
"rationale": (
f"The immediate priority is: '{priorities[0]}'. "
"Nursing priorities are ordered by urgency β€” life-threatening physiological "
"problems must be addressed before less urgent concerns."
),
},
{
"q": f"Which assessment finding in {case['patient']['name']}'s case is MOST concerning?",
"options": list(case["vitals"].items())[:4],
"answer": 0,
"rationale": (
"The most concerning vital sign is the first listed, which is outside normal limits. "
"Always assess using ABCDE β€” Airway, Breathing, Circulation, Disability, Exposure β€” "
"to prioritise life-threatening abnormalities."
),
"is_vitals": True,
},
{
"q": f"The goal for the priority nursing diagnosis in this case is: '{case['nursing_diagnoses'][0]['goal']}'. Which nursing intervention BEST addresses this goal?",
"options": case["nursing_diagnoses"][0]["interventions"][:4],
"answer": 0,
"rationale": (
f"The first listed intervention directly addresses the priority goal. "
f"Rationale: {case['nursing_diagnoses'][0]['interventions'][0]}"
),
},
{
"q": f"In evaluating the outcomes for {case['patient']['name']}, which finding would indicate the PRIORITY nursing goal has been MET?",
"options": [
case["nursing_diagnoses"][0]["goal"],
"Patient is pain-free",
"Family is satisfied with care",
"All documentation is complete",
],
"answer": 0,
"rationale": (
f"The priority goal for this case is: '{case['nursing_diagnoses'][0]['goal']}'. "
"Goals must be patient-centred, measurable, and time-bound (SMART). "
"This outcome directly measures resolution of the priority nursing diagnosis."
),
},
]
if st.button("πŸ”„ Reset Quiz", use_container_width=False):
st.session_state.quiz_answers = {}
st.session_state.quiz_submitted = False
st.rerun()
st.divider()
for i, q in enumerate(questions):
st.markdown(f"**Question {i + 1}:** {q['q']}")
if q.get("is_vitals"):
opts = [f"{k}: {v}" for k, v in q["options"]]
else:
opts = q["options"]
selected = st.radio(
f"Q{i+1}",
options=opts,
key=f"quiz_{case['id']}_q{i}",
label_visibility="collapsed",
)
st.session_state.quiz_answers[i] = selected
if st.session_state.quiz_submitted:
correct_opt = opts[q["answer"]]
if selected == correct_opt:
st.markdown(
f'<div class="success-banner">βœ… <b>Correct!</b> {q["rationale"]}</div>',
unsafe_allow_html=True
)
else:
st.markdown(
f'<div class="critical-banner">❌ <b>Incorrect.</b> '
f'Correct answer: <b>{correct_opt}</b><br/>{q["rationale"]}</div>',
unsafe_allow_html=True
)
st.markdown("")
col_sub, col_score = st.columns([2, 3])
with col_sub:
if st.button("βœ… Submit Answers", type="primary", use_container_width=True):
st.session_state.quiz_submitted = True
st.rerun()
if st.session_state.quiz_submitted:
score = 0
for i, q in enumerate(questions):
if q.get("is_vitals"):
opts = [f"{k}: {v}" for k, v in q["options"]]
else:
opts = q["options"]
if st.session_state.quiz_answers.get(i) == opts[q["answer"]]:
score += 1
pct = round((score / len(questions)) * 100)
colour = "#2e7d32" if pct >= 80 else "#e65100" if pct >= 60 else "#c62828"
with col_score:
st.markdown(
f'<div style="padding:0.6rem 1rem; background:#f5f5f5; border-radius:8px; '
f'font-size:1.1em; font-weight:700; color:{colour};">'
f'Score: {score} / {len(questions)} ({pct}%)</div>',
unsafe_allow_html=True
)
# ========================= CARE PLAN ========================================
with tab_plan:
st.subheader("πŸ“ Nursing Care Plan Builder")
st.caption("Build a structured care plan for the active case using the NANDA-NIC-NOC framework.")
if not st.session_state.selected_case_id:
st.info("πŸ’‘ Select a case from the **Case Library** tab first.")
else:
case = get_by_id(st.session_state.selected_case_id)
st.markdown(
f'<div class="patient-banner">πŸ₯ <b>{case["patient"]["name"]}</b> Β· '
f'{case["patient"]["age"]} y/o Β· {case["medical_diagnosis"]}</div>',
unsafe_allow_html=True
)
st.markdown("Complete the care plan below. Toggle **Show Model Answer** to reveal guidance.")
st.divider()
for idx, nd in enumerate(case["nursing_diagnoses"]):
st.markdown(f"### πŸ“Œ Nursing Diagnosis {idx + 1}")
st.markdown(
f'<div class="nd-card"><b>{nd["diagnosis"]}</b></div>',
unsafe_allow_html=True
)
cp_key = f"cp_{case['id']}_{idx}"
if cp_key not in st.session_state.careplan_items:
st.session_state.careplan_items[cp_key] = {
"diagnosis": "", "goal": "", "interventions": "", "evaluation": ""
}
c1, c2 = st.columns(2)
with c1:
st.markdown("**Your Nursing Diagnosis (NANDA format):**")
nd_input = st.text_area(
"nd",
value=st.session_state.careplan_items[cp_key]["diagnosis"],
height=80,
placeholder="[Problem] related to [Aetiology] as evidenced by [Signs/Symptoms]",
key=f"{cp_key}_nd",
label_visibility="collapsed",
)
st.session_state.careplan_items[cp_key]["diagnosis"] = nd_input
st.markdown("**Your SMART Goal:**")
goal_input = st.text_area(
"goal",
value=st.session_state.careplan_items[cp_key]["goal"],
height=80,
placeholder="Patient will [outcome] by [time] as measured by [indicator]",
key=f"{cp_key}_goal",
label_visibility="collapsed",
)
st.session_state.careplan_items[cp_key]["goal"] = goal_input
with c2:
st.markdown("**Your Nursing Interventions (with rationale):**")
inv_input = st.text_area(
"interventions",
value=st.session_state.careplan_items[cp_key]["interventions"],
height=80,
placeholder="1. Intervention (Rationale)\n2. Intervention (Rationale)\n3. ...",
key=f"{cp_key}_inv",
label_visibility="collapsed",
)
st.session_state.careplan_items[cp_key]["interventions"] = inv_input
st.markdown("**Evaluation Criteria:**")
eval_input = st.text_area(
"evaluation",
value=st.session_state.careplan_items[cp_key]["evaluation"],
height=80,
placeholder="Goal met / partially met / not met because...",
key=f"{cp_key}_eval",
label_visibility="collapsed",
)
st.session_state.careplan_items[cp_key]["evaluation"] = eval_input
# Model answer toggle
with st.expander("πŸ” Show Model Answer"):
st.markdown(f"**Diagnosis:** {nd['diagnosis']}")
st.markdown(f"**Evidence:** {nd['evidence']}")
st.markdown(f"**Goal:** {nd['goal']}")
st.markdown("**Model Interventions:**")
for i in nd["interventions"]:
st.markdown(f"β€’ {i}")
st.divider()
# Print / export summary
if any(
any(v for v in st.session_state.careplan_items.get(f"cp_{case['id']}_{i}", {}).values())
for i in range(len(case["nursing_diagnoses"]))
):
st.markdown("### πŸ“„ Care Plan Summary")
summary_lines = [f"# Care Plan: {case['patient']['name']}\n",
f"**Diagnosis:** {case['medical_diagnosis']}\n"]
for idx, nd in enumerate(case["nursing_diagnoses"]):
cp_key = f"cp_{case['id']}_{idx}"
item = st.session_state.careplan_items.get(cp_key, {})
summary_lines.append(f"\n## Nursing Diagnosis {idx+1}\n")
summary_lines.append(f"**Diagnosis:** {item.get('diagnosis', 'β€”')}\n")
summary_lines.append(f"**Goal:** {item.get('goal', 'β€”')}\n")
summary_lines.append(f"**Interventions:** {item.get('interventions', 'β€”')}\n")
summary_lines.append(f"**Evaluation:** {item.get('evaluation', 'β€”')}\n")
summary_text = "\n".join(summary_lines)
st.download_button(
"⬇️ Download Care Plan (.txt)",
data=summary_text,
file_name=f"care_plan_{case['id']}.txt",
mime="text/plain",
use_container_width=False,
)
# ---------------------------------------------------------------------------
# Footer
# ---------------------------------------------------------------------------
st.divider()
st.caption(
"Cases are fictional composite scenarios for educational purposes only. "
"Always follow your institution's clinical guidelines, evidence-based practice, "
"and clinical supervisor guidance in real patient care."
)