| | """ |
| | EBP Research Tool for Student Nurses |
| | Streamlit app — deployable to Hugging Face Spaces (free CPU tier). |
| | """ |
| |
|
| | import streamlit as st |
| |
|
| | from search.pubmed import search_pubmed, fetch_abstract, fetch_summaries |
| | from search.clinical_trials import search_trials |
| | from summarizer import summarize, infer_evidence_level |
| | from utils.citations import format_apa7, format_ama |
| |
|
| | |
| | |
| | |
| | st.set_page_config( |
| | page_title="EBP Research Tool — Student Nurses", |
| | page_icon="🩺", |
| | layout="wide", |
| | initial_sidebar_state="expanded", |
| | ) |
| |
|
| | |
| | |
| | |
| | st.markdown( |
| | """ |
| | <style> |
| | /* Card container */ |
| | .ebp-card { |
| | border: 1px solid #d9e2ec; |
| | border-radius: 10px; |
| | padding: 1rem 1.2rem; |
| | margin-bottom: 1rem; |
| | background: #ffffff; |
| | } |
| | /* Source badges */ |
| | .badge-pubmed { background:#e8f4fd; color:#1558b0; padding:2px 8px; |
| | border-radius:4px; font-size:0.72em; font-weight:600; } |
| | .badge-trials { background:#fff3e0; color:#e65100; padding:2px 8px; |
| | border-radius:4px; font-size:0.72em; font-weight:600; } |
| | /* Evidence level colours */ |
| | .ev-I { color:#1b7c1b; font-weight:700; } |
| | .ev-II { color:#7c6b00; font-weight:700; } |
| | .ev-III { color:#b04e00; font-weight:700; } |
| | .ev-IV { color:#b04e00; font-weight:700; } |
| | .ev-V { color:#005f8c; font-weight:700; } |
| | .ev-VI { color:#005f8c; font-weight:700; } |
| | .ev-VII { color:#333; font-weight:700; } |
| | /* Slightly tighter headings */ |
| | h1 { margin-bottom: 0 !important; } |
| | </style> |
| | """, |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| | |
| | |
| | |
| | _DEFAULTS = { |
| | "saved_articles": [], |
| | "search_results": [], |
| | "search_query": "", |
| | "abstracts": {}, |
| | "summaries": {}, |
| | } |
| | for _k, _v in _DEFAULTS.items(): |
| | if _k not in st.session_state: |
| | st.session_state[_k] = _v |
| |
|
| |
|
| | |
| | |
| | |
| | def render_card(article: dict, card_idx: int, tab_key: str) -> None: |
| | pmid = article.get("pmid", str(card_idx)) |
| | title = article.get("title", "No title") |
| | authors = article.get("authors", []) |
| | journal = article.get("journal", "") |
| | year = article.get("year", "") |
| | url = article.get("url", "") |
| | source = article.get("source", "PubMed") |
| |
|
| | badge_cls = "badge-pubmed" if source == "PubMed" else "badge-trials" |
| | badge_lbl = "PubMed" if source == "PubMed" else "ClinicalTrials" |
| |
|
| | |
| | cached_abstract = st.session_state.abstracts.get(pmid, "") |
| | ev_code, ev_emoji, ev_desc = infer_evidence_level(title, cached_abstract) |
| |
|
| | with st.container(): |
| | st.markdown( |
| | f'<span class="{badge_cls}">{badge_lbl}</span> ' |
| | f'<span style="font-size:0.8em; color:#555;">{ev_emoji} {ev_code} — {ev_desc}</span>', |
| | unsafe_allow_html=True, |
| | ) |
| | st.markdown(f"**{title}**") |
| | meta_parts = [] |
| | if authors: |
| | meta_parts.append(", ".join(authors[:3]) + (" …" if len(authors) > 3 else "")) |
| | if journal: |
| | meta_parts.append(f"*{journal}*") |
| | if year: |
| | meta_parts.append(year) |
| | if meta_parts: |
| | st.caption(" · ".join(meta_parts)) |
| |
|
| | |
| | col_a, col_b, col_c, col_d, _ = st.columns([1.5, 1.5, 1.5, 1.5, 4]) |
| |
|
| | with col_a: |
| | load_key = f"load_{tab_key}_{card_idx}" |
| | if st.button("📄 Abstract", key=load_key, use_container_width=True): |
| | if pmid not in st.session_state.abstracts or not st.session_state.abstracts[pmid]: |
| | with st.spinner("Loading…"): |
| | ab = article.get("abstract") or fetch_abstract(pmid) |
| | st.session_state.abstracts[pmid] = ab |
| | |
| | if pmid in st.session_state.summaries: |
| | del st.session_state.summaries[pmid] |
| |
|
| | with col_b: |
| | sum_key = f"sum_{tab_key}_{card_idx}" |
| | if st.button("🧠 Summarise", key=sum_key, use_container_width=True): |
| | if pmid not in st.session_state.abstracts or not st.session_state.abstracts[pmid]: |
| | with st.spinner("Loading abstract…"): |
| | ab = article.get("abstract") or fetch_abstract(pmid) |
| | st.session_state.abstracts[pmid] = ab |
| | with st.spinner("Summarising…"): |
| | st.session_state.summaries[pmid] = summarize( |
| | st.session_state.abstracts[pmid], title |
| | ) |
| |
|
| | with col_c: |
| | save_key = f"save_{tab_key}_{card_idx}" |
| | already_saved = any(a.get("pmid") == pmid for a in st.session_state.saved_articles) |
| | if already_saved: |
| | st.button("✅ Saved", key=save_key, disabled=True, use_container_width=True) |
| | else: |
| | if st.button("💾 Save", key=save_key, use_container_width=True): |
| | st.session_state.saved_articles.append(article) |
| | st.rerun() |
| |
|
| | with col_d: |
| | if url: |
| | st.link_button("🔗 Full Article", url, use_container_width=True) |
| |
|
| | |
| | if pmid in st.session_state.abstracts and st.session_state.abstracts[pmid]: |
| | if pmid not in st.session_state.summaries: |
| | with st.expander("Abstract", expanded=True): |
| | raw = st.session_state.abstracts[pmid] |
| | |
| | st.markdown(raw) |
| |
|
| | |
| | if pmid in st.session_state.summaries: |
| | s = st.session_state.summaries[pmid] |
| | ev_c, ev_e, ev_d = s["evidence_level"] |
| | with st.expander("Nursing Summary Card", expanded=True): |
| | cols2 = st.columns(2) |
| | with cols2[0]: |
| | st.markdown("**Overview / Background**") |
| | st.write(s["overview"] or "—") |
| | if s.get("methods"): |
| | st.markdown("**Methods**") |
| | st.write(s["methods"]) |
| | with cols2[1]: |
| | st.markdown("**Key Findings**") |
| | st.write(s["key_findings"] or "—") |
| | st.markdown("**Nursing Implications**") |
| | st.info(s["nursing_implications"] or "—") |
| |
|
| | st.markdown(f"**Evidence Level:** {ev_e} {ev_c} — {ev_d}") |
| |
|
| | |
| | st.markdown("**APA 7th Citation**") |
| | st.code(format_apa7(article), language=None) |
| |
|
| | st.divider() |
| |
|
| |
|
| | |
| | |
| | |
| | with st.sidebar: |
| | st.markdown("## 🔬 Search Filters") |
| | st.divider() |
| |
|
| | databases = st.multiselect( |
| | "Databases", |
| | ["PubMed", "ClinicalTrials.gov"], |
| | default=["PubMed"], |
| | ) |
| |
|
| | date_range = st.radio( |
| | "Publication date", |
| | ["Last 1 year", "Last 2 years", "Last 5 years", "Last 10 years", "All time"], |
| | index=2, |
| | ) |
| |
|
| | study_types = st.multiselect( |
| | "Study design", |
| | ["Any", "Systematic Review", "Meta-Analysis", "RCT", "Cohort Study", "Case Study"], |
| | default=["Any"], |
| | ) |
| |
|
| | nursing_focus = st.toggle("Nursing-focused results only", value=True) |
| | max_results = st.slider("Results per database", min_value=5, max_value=30, value=15) |
| |
|
| | st.divider() |
| | st.markdown( |
| | """ |
| | **Evidence Level Guide** |
| | 🟢 Level I — Systematic Review / Meta-Analysis |
| | 🟡 Level II — RCT |
| | 🟠 Level III–IV — Quasi / Cohort |
| | 🔵 Level V–VI — Qualitative |
| | ⚫ Level VII — Expert Opinion |
| | ⚪ Unclassified |
| | """ |
| | ) |
| |
|
| |
|
| | |
| | |
| | |
| | st.title("🩺 EBP Research Tool") |
| | st.caption( |
| | "Evidence-Based Practice search for student nurses · " |
| | "PubMed (30 M+ articles) · ClinicalTrials.gov · Free & open" |
| | ) |
| |
|
| | |
| | QUICK_TOPICS = [ |
| | ("Wound Care", "wound care nursing interventions"), |
| | ("Pain Management", "pain management nursing practice"), |
| | ("Fall Prevention", "fall prevention hospital nursing"), |
| | ("Med Safety", "medication safety nursing errors"), |
| | ("Infection Control", "hand hygiene infection control nursing"), |
| | ("Pt Education", "patient education nursing outcomes"), |
| | ("Mental Health", "mental health nursing interventions"), |
| | ("Paediatric Care", "paediatric nursing care outcomes"), |
| | ] |
| |
|
| | qt_cols = st.columns(4) |
| | for i, (label, query) in enumerate(QUICK_TOPICS): |
| | if qt_cols[i % 4].button(label, use_container_width=True, key=f"qt_{i}"): |
| | st.session_state.search_query = query |
| | st.rerun() |
| |
|
| | st.divider() |
| |
|
| | |
| | |
| | |
| | n_saved = len(st.session_state.saved_articles) |
| | tab_search, tab_library, tab_cite = st.tabs( |
| | ["🔍 Search", f"📚 My Library ({n_saved})", "📝 Citation Builder"] |
| | ) |
| |
|
| | |
| | with tab_search: |
| | col_q, col_btn = st.columns([5, 1]) |
| | with col_q: |
| | query = st.text_input( |
| | "search_box", |
| | value=st.session_state.search_query, |
| | placeholder="e.g. pressure injury prevention nursing ICU", |
| | label_visibility="collapsed", |
| | ) |
| | with col_btn: |
| | do_search = st.button("Search", type="primary", use_container_width=True) |
| |
|
| | if do_search and query.strip(): |
| | st.session_state.search_query = query.strip() |
| | with st.spinner(f"Searching {', '.join(databases)}…"): |
| | results: list[dict] = [] |
| | if "PubMed" in databases: |
| | results += search_pubmed( |
| | query, date_range, study_types, nursing_focus, max_results |
| | ) |
| | if "ClinicalTrials.gov" in databases: |
| | results += search_trials(query, max(5, max_results // 2)) |
| | st.session_state.search_results = results |
| |
|
| | results = st.session_state.search_results |
| | if results: |
| | st.success(f"**{len(results)}** results") |
| | for idx, art in enumerate(results): |
| | render_card(art, idx, "search") |
| | elif st.session_state.search_query: |
| | st.warning("No results found — try broader keywords or adjust filters.") |
| | else: |
| | st.info("Enter a topic above or choose a Quick Topic to begin.") |
| |
|
| | |
| | with tab_library: |
| | if not st.session_state.saved_articles: |
| | st.info("Your library is empty. Search and click 💾 Save to add articles here.") |
| | else: |
| | st.write(f"**{n_saved} saved article{'s' if n_saved != 1 else ''}**") |
| |
|
| | |
| | if st.button("📋 Copy All Citations (APA 7th)"): |
| | all_cites = "\n\n".join(format_apa7(a) for a in st.session_state.saved_articles) |
| | st.code(all_cites, language=None) |
| |
|
| | if st.button("🗑️ Clear Library", type="secondary"): |
| | st.session_state.saved_articles = [] |
| | st.rerun() |
| |
|
| | st.divider() |
| | for idx, art in enumerate(st.session_state.saved_articles): |
| | render_card(art, idx, "library") |
| |
|
| | |
| | with tab_cite: |
| | st.subheader("Citation Builder") |
| | st.write( |
| | "Enter a PubMed ID to instantly generate a formatted reference " |
| | "ready to paste into your assignment." |
| | ) |
| |
|
| | col1, col2, col3 = st.columns([2, 2, 1]) |
| | with col1: |
| | pmid_input = st.text_input("PubMed ID (PMID)", placeholder="e.g. 33721172") |
| | with col2: |
| | cite_style = st.selectbox("Citation style", ["APA 7th Edition", "AMA"]) |
| | with col3: |
| | st.write("") |
| | st.write("") |
| | gen_cite = st.button("Generate", type="primary", use_container_width=True) |
| |
|
| | if gen_cite and pmid_input.strip(): |
| | with st.spinner("Fetching article metadata…"): |
| | arts = fetch_summaries([pmid_input.strip()]) |
| | if arts: |
| | art = arts[0] |
| | citation = format_apa7(art) if cite_style == "APA 7th Edition" else format_ama(art) |
| | st.success("Citation ready — copy the text below:") |
| | st.code(citation, language=None) |
| | st.markdown( |
| | f"**Title:** {art['title']} \n" |
| | f"**Journal:** {art.get('journal','')} · **Year:** {art.get('year','')} \n" |
| | f"[View on PubMed]({art.get('url','')})" |
| | ) |
| | else: |
| | st.error("Article not found. Check the PMID and try again.") |
| |
|
| | st.divider() |
| | st.markdown( |
| | """ |
| | **Formatting guide — APA 7th (nursing standard)** |
| | |
| | > Author, F. M., & Author, F. M. (Year). Title of article. *Journal Name*, *Volume*(Issue), Pages. https://doi.org/xxxxx |
| | |
| | **Tips for nursing assignments:** |
| | - Always verify DOI links are active before submitting |
| | - For articles with no DOI, include the journal homepage URL |
| | - Use the exact journal abbreviation from the article, not your own |
| | """ |
| | ) |
| |
|
| | |
| | |
| | |
| | st.divider() |
| | st.caption( |
| | "Data sourced from PubMed / NCBI (NIH) and ClinicalTrials.gov — " |
| | "both public, freely available databases. " |
| | "Always verify information against original sources before clinical application." |
| | ) |
| |
|