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