"""
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("""
""", 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'{difficulty}'
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'- โฌค {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'
'
f'{icon} {case["title"]} '
f'{diff_badge(case["difficulty"])}
'
f'๐ท๏ธ {case["category"]} ยท '
f'๐ค {case["patient"]["name"]}, {case["patient"]["age"]}yo ยท '
f'๐ {", ".join(case["tags"][:4])}'
f'
',
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''
f'
{icon} {case["title"]}
'
f'{diff_badge(case["difficulty"])} '
f'{case["category"]}'
f'',
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'๐ {case["presentation"]}
',
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'{key}
'
f'{vitals[key]}
',
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'โ๏ธ {case["medical_diagnosis"]}
',
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''
f'{i}. {priority}
',
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'๐ฅ Active Case: {case["title"]} ยท '
f'๐ค {case["patient"]["name"]}
',
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''
f'Supporting Evidence: {nd["evidence"]}
'
f'Goal: {nd["goal"]}'
f'
',
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'๐ฅ Active Case: {case["title"]}
',
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'โ
Correct! {q["rationale"]}
',
unsafe_allow_html=True
)
else:
st.markdown(
f'โ Incorrect. '
f'Correct answer: {correct_opt}
{q["rationale"]}
',
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''
f'Score: {score} / {len(questions)} ({pct}%)
',
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'๐ฅ {case["patient"]["name"]} ยท '
f'{case["patient"]["age"]} y/o ยท {case["medical_diagnosis"]}
',
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'{nd["diagnosis"]}
',
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."
)