Lincoln Gombedza commited on
Commit
a08a7a4
·
1 Parent(s): f299c29

Fix: use streamlit_app.py as entry point for HF Spaces

Browse files
Files changed (2) hide show
  1. README.md +1 -1
  2. streamlit_app.py +373 -0
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: blue
5
  colorTo: green
6
  sdk: streamlit
7
  sdk_version: 1.32.0
8
- app_file: app.py
9
  pinned: false
10
  license: mit
11
  ---
 
5
  colorTo: green
6
  sdk: streamlit
7
  sdk_version: 1.32.0
8
+ app_file: streamlit_app.py
9
  pinned: false
10
  license: mit
11
  ---
streamlit_app.py ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EBP Research Tool for Student Nurses
3
+ Streamlit app — deployable to Hugging Face Spaces (free CPU tier).
4
+ """
5
+
6
+ import streamlit as st
7
+
8
+ from search.pubmed import search_pubmed, fetch_abstract, fetch_summaries
9
+ from search.clinical_trials import search_trials
10
+ from summarizer import summarize, infer_evidence_level
11
+ from utils.citations import format_apa7, format_ama
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Page config (must be first Streamlit call)
15
+ # ---------------------------------------------------------------------------
16
+ st.set_page_config(
17
+ page_title="EBP Research Tool — Student Nurses",
18
+ page_icon="🩺",
19
+ layout="wide",
20
+ initial_sidebar_state="expanded",
21
+ )
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Minimal custom CSS
25
+ # ---------------------------------------------------------------------------
26
+ st.markdown(
27
+ """
28
+ <style>
29
+ /* Card container */
30
+ .ebp-card {
31
+ border: 1px solid #d9e2ec;
32
+ border-radius: 10px;
33
+ padding: 1rem 1.2rem;
34
+ margin-bottom: 1rem;
35
+ background: #ffffff;
36
+ }
37
+ /* Source badges */
38
+ .badge-pubmed { background:#e8f4fd; color:#1558b0; padding:2px 8px;
39
+ border-radius:4px; font-size:0.72em; font-weight:600; }
40
+ .badge-trials { background:#fff3e0; color:#e65100; padding:2px 8px;
41
+ border-radius:4px; font-size:0.72em; font-weight:600; }
42
+ /* Evidence level colours */
43
+ .ev-I { color:#1b7c1b; font-weight:700; }
44
+ .ev-II { color:#7c6b00; font-weight:700; }
45
+ .ev-III { color:#b04e00; font-weight:700; }
46
+ .ev-IV { color:#b04e00; font-weight:700; }
47
+ .ev-V { color:#005f8c; font-weight:700; }
48
+ .ev-VI { color:#005f8c; font-weight:700; }
49
+ .ev-VII { color:#333; font-weight:700; }
50
+ /* Slightly tighter headings */
51
+ h1 { margin-bottom: 0 !important; }
52
+ </style>
53
+ """,
54
+ unsafe_allow_html=True,
55
+ )
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Session state initialisation
59
+ # ---------------------------------------------------------------------------
60
+ _DEFAULTS = {
61
+ "saved_articles": [],
62
+ "search_results": [],
63
+ "search_query": "",
64
+ "abstracts": {}, # pmid → abstract text
65
+ "summaries": {}, # pmid → summary dict
66
+ }
67
+ for _k, _v in _DEFAULTS.items():
68
+ if _k not in st.session_state:
69
+ st.session_state[_k] = _v
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Helper: render one article card
74
+ # ---------------------------------------------------------------------------
75
+ def render_card(article: dict, card_idx: int, tab_key: str) -> None:
76
+ pmid = article.get("pmid", str(card_idx))
77
+ title = article.get("title", "No title")
78
+ authors = article.get("authors", [])
79
+ journal = article.get("journal", "")
80
+ year = article.get("year", "")
81
+ url = article.get("url", "")
82
+ source = article.get("source", "PubMed")
83
+
84
+ badge_cls = "badge-pubmed" if source == "PubMed" else "badge-trials"
85
+ badge_lbl = "PubMed" if source == "PubMed" else "ClinicalTrials"
86
+
87
+ # Infer evidence level from whatever we already have
88
+ cached_abstract = st.session_state.abstracts.get(pmid, "")
89
+ ev_code, ev_emoji, ev_desc = infer_evidence_level(title, cached_abstract)
90
+
91
+ with st.container():
92
+ st.markdown(
93
+ f'<span class="{badge_cls}">{badge_lbl}</span> '
94
+ f'<span style="font-size:0.8em; color:#555;">{ev_emoji} {ev_code} — {ev_desc}</span>',
95
+ unsafe_allow_html=True,
96
+ )
97
+ st.markdown(f"**{title}**")
98
+ meta_parts = []
99
+ if authors:
100
+ meta_parts.append(", ".join(authors[:3]) + (" …" if len(authors) > 3 else ""))
101
+ if journal:
102
+ meta_parts.append(f"*{journal}*")
103
+ if year:
104
+ meta_parts.append(year)
105
+ if meta_parts:
106
+ st.caption(" · ".join(meta_parts))
107
+
108
+ # Action row
109
+ col_a, col_b, col_c, col_d, _ = st.columns([1.5, 1.5, 1.5, 1.5, 4])
110
+
111
+ with col_a:
112
+ load_key = f"load_{tab_key}_{card_idx}"
113
+ if st.button("📄 Abstract", key=load_key, use_container_width=True):
114
+ if pmid not in st.session_state.abstracts or not st.session_state.abstracts[pmid]:
115
+ with st.spinner("Loading…"):
116
+ ab = article.get("abstract") or fetch_abstract(pmid)
117
+ st.session_state.abstracts[pmid] = ab
118
+ # toggle — if summary shown, clear it so only abstract shows
119
+ if pmid in st.session_state.summaries:
120
+ del st.session_state.summaries[pmid]
121
+
122
+ with col_b:
123
+ sum_key = f"sum_{tab_key}_{card_idx}"
124
+ if st.button("🧠 Summarise", key=sum_key, use_container_width=True):
125
+ if pmid not in st.session_state.abstracts or not st.session_state.abstracts[pmid]:
126
+ with st.spinner("Loading abstract…"):
127
+ ab = article.get("abstract") or fetch_abstract(pmid)
128
+ st.session_state.abstracts[pmid] = ab
129
+ with st.spinner("Summarising…"):
130
+ st.session_state.summaries[pmid] = summarize(
131
+ st.session_state.abstracts[pmid], title
132
+ )
133
+
134
+ with col_c:
135
+ save_key = f"save_{tab_key}_{card_idx}"
136
+ already_saved = any(a.get("pmid") == pmid for a in st.session_state.saved_articles)
137
+ if already_saved:
138
+ st.button("✅ Saved", key=save_key, disabled=True, use_container_width=True)
139
+ else:
140
+ if st.button("💾 Save", key=save_key, use_container_width=True):
141
+ st.session_state.saved_articles.append(article)
142
+ st.rerun()
143
+
144
+ with col_d:
145
+ if url:
146
+ st.link_button("🔗 Full Article", url, use_container_width=True)
147
+
148
+ # Abstract pane
149
+ if pmid in st.session_state.abstracts and st.session_state.abstracts[pmid]:
150
+ if pmid not in st.session_state.summaries:
151
+ with st.expander("Abstract", expanded=True):
152
+ raw = st.session_state.abstracts[pmid]
153
+ # Render **LABEL:** markdown nicely
154
+ st.markdown(raw)
155
+
156
+ # Summary / nursing card pane
157
+ if pmid in st.session_state.summaries:
158
+ s = st.session_state.summaries[pmid]
159
+ ev_c, ev_e, ev_d = s["evidence_level"]
160
+ with st.expander("Nursing Summary Card", expanded=True):
161
+ cols2 = st.columns(2)
162
+ with cols2[0]:
163
+ st.markdown("**Overview / Background**")
164
+ st.write(s["overview"] or "—")
165
+ if s.get("methods"):
166
+ st.markdown("**Methods**")
167
+ st.write(s["methods"])
168
+ with cols2[1]:
169
+ st.markdown("**Key Findings**")
170
+ st.write(s["key_findings"] or "—")
171
+ st.markdown("**Nursing Implications**")
172
+ st.info(s["nursing_implications"] or "—")
173
+
174
+ st.markdown(f"**Evidence Level:** {ev_e} {ev_c} — {ev_d}")
175
+
176
+ # APA citation
177
+ st.markdown("**APA 7th Citation**")
178
+ st.code(format_apa7(article), language=None)
179
+
180
+ st.divider()
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Sidebar — filters
185
+ # ---------------------------------------------------------------------------
186
+ with st.sidebar:
187
+ st.markdown("## 🔬 Search Filters")
188
+ st.divider()
189
+
190
+ databases = st.multiselect(
191
+ "Databases",
192
+ ["PubMed", "ClinicalTrials.gov"],
193
+ default=["PubMed"],
194
+ )
195
+
196
+ date_range = st.radio(
197
+ "Publication date",
198
+ ["Last 1 year", "Last 2 years", "Last 5 years", "Last 10 years", "All time"],
199
+ index=2,
200
+ )
201
+
202
+ study_types = st.multiselect(
203
+ "Study design",
204
+ ["Any", "Systematic Review", "Meta-Analysis", "RCT", "Cohort Study", "Case Study"],
205
+ default=["Any"],
206
+ )
207
+
208
+ nursing_focus = st.toggle("Nursing-focused results only", value=True)
209
+ max_results = st.slider("Results per database", min_value=5, max_value=30, value=15)
210
+
211
+ st.divider()
212
+ st.markdown(
213
+ """
214
+ **Evidence Level Guide**
215
+ 🟢 Level I — Systematic Review / Meta-Analysis
216
+ 🟡 Level II — RCT
217
+ 🟠 Level III–IV — Quasi / Cohort
218
+ 🔵 Level V–VI — Qualitative
219
+ ⚫ Level VII — Expert Opinion
220
+ ⚪ Unclassified
221
+ """
222
+ )
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # Header
227
+ # ---------------------------------------------------------------------------
228
+ st.title("🩺 EBP Research Tool")
229
+ st.caption(
230
+ "Evidence-Based Practice search for student nurses · "
231
+ "PubMed (30 M+ articles) · ClinicalTrials.gov · Free & open"
232
+ )
233
+
234
+ # Quick-topic shortcuts
235
+ QUICK_TOPICS = [
236
+ ("Wound Care", "wound care nursing interventions"),
237
+ ("Pain Management", "pain management nursing practice"),
238
+ ("Fall Prevention", "fall prevention hospital nursing"),
239
+ ("Med Safety", "medication safety nursing errors"),
240
+ ("Infection Control", "hand hygiene infection control nursing"),
241
+ ("Pt Education", "patient education nursing outcomes"),
242
+ ("Mental Health", "mental health nursing interventions"),
243
+ ("Paediatric Care", "paediatric nursing care outcomes"),
244
+ ]
245
+
246
+ qt_cols = st.columns(4)
247
+ for i, (label, query) in enumerate(QUICK_TOPICS):
248
+ if qt_cols[i % 4].button(label, use_container_width=True, key=f"qt_{i}"):
249
+ st.session_state.search_query = query
250
+ st.rerun()
251
+
252
+ st.divider()
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # Tabs
256
+ # ---------------------------------------------------------------------------
257
+ n_saved = len(st.session_state.saved_articles)
258
+ tab_search, tab_library, tab_cite = st.tabs(
259
+ ["🔍 Search", f"📚 My Library ({n_saved})", "📝 Citation Builder"]
260
+ )
261
+
262
+ # ============================= SEARCH TAB ==================================
263
+ with tab_search:
264
+ col_q, col_btn = st.columns([5, 1])
265
+ with col_q:
266
+ query = st.text_input(
267
+ "search_box",
268
+ value=st.session_state.search_query,
269
+ placeholder="e.g. pressure injury prevention nursing ICU",
270
+ label_visibility="collapsed",
271
+ )
272
+ with col_btn:
273
+ do_search = st.button("Search", type="primary", use_container_width=True)
274
+
275
+ if do_search and query.strip():
276
+ st.session_state.search_query = query.strip()
277
+ with st.spinner(f"Searching {', '.join(databases)}…"):
278
+ results: list[dict] = []
279
+ if "PubMed" in databases:
280
+ results += search_pubmed(
281
+ query, date_range, study_types, nursing_focus, max_results
282
+ )
283
+ if "ClinicalTrials.gov" in databases:
284
+ results += search_trials(query, max(5, max_results // 2))
285
+ st.session_state.search_results = results
286
+
287
+ results = st.session_state.search_results
288
+ if results:
289
+ st.success(f"**{len(results)}** results")
290
+ for idx, art in enumerate(results):
291
+ render_card(art, idx, "search")
292
+ elif st.session_state.search_query:
293
+ st.warning("No results found — try broader keywords or adjust filters.")
294
+ else:
295
+ st.info("Enter a topic above or choose a Quick Topic to begin.")
296
+
297
+ # ============================= LIBRARY TAB =================================
298
+ with tab_library:
299
+ if not st.session_state.saved_articles:
300
+ st.info("Your library is empty. Search and click 💾 Save to add articles here.")
301
+ else:
302
+ st.write(f"**{n_saved} saved article{'s' if n_saved != 1 else ''}**")
303
+
304
+ # Export all citations
305
+ if st.button("📋 Copy All Citations (APA 7th)"):
306
+ all_cites = "\n\n".join(format_apa7(a) for a in st.session_state.saved_articles)
307
+ st.code(all_cites, language=None)
308
+
309
+ if st.button("🗑️ Clear Library", type="secondary"):
310
+ st.session_state.saved_articles = []
311
+ st.rerun()
312
+
313
+ st.divider()
314
+ for idx, art in enumerate(st.session_state.saved_articles):
315
+ render_card(art, idx, "library")
316
+
317
+ # ============================= CITE BUILDER TAB ============================
318
+ with tab_cite:
319
+ st.subheader("Citation Builder")
320
+ st.write(
321
+ "Enter a PubMed ID to instantly generate a formatted reference "
322
+ "ready to paste into your assignment."
323
+ )
324
+
325
+ col1, col2, col3 = st.columns([2, 2, 1])
326
+ with col1:
327
+ pmid_input = st.text_input("PubMed ID (PMID)", placeholder="e.g. 33721172")
328
+ with col2:
329
+ cite_style = st.selectbox("Citation style", ["APA 7th Edition", "AMA"])
330
+ with col3:
331
+ st.write("") # vertical align
332
+ st.write("")
333
+ gen_cite = st.button("Generate", type="primary", use_container_width=True)
334
+
335
+ if gen_cite and pmid_input.strip():
336
+ with st.spinner("Fetching article metadata…"):
337
+ arts = fetch_summaries([pmid_input.strip()])
338
+ if arts:
339
+ art = arts[0]
340
+ citation = format_apa7(art) if cite_style == "APA 7th Edition" else format_ama(art)
341
+ st.success("Citation ready — copy the text below:")
342
+ st.code(citation, language=None)
343
+ st.markdown(
344
+ f"**Title:** {art['title']} \n"
345
+ f"**Journal:** {art.get('journal','')} · **Year:** {art.get('year','')} \n"
346
+ f"[View on PubMed]({art.get('url','')})"
347
+ )
348
+ else:
349
+ st.error("Article not found. Check the PMID and try again.")
350
+
351
+ st.divider()
352
+ st.markdown(
353
+ """
354
+ **Formatting guide — APA 7th (nursing standard)**
355
+
356
+ > Author, F. M., & Author, F. M. (Year). Title of article. *Journal Name*, *Volume*(Issue), Pages. https://doi.org/xxxxx
357
+
358
+ **Tips for nursing assignments:**
359
+ - Always verify DOI links are active before submitting
360
+ - For articles with no DOI, include the journal homepage URL
361
+ - Use the exact journal abbreviation from the article, not your own
362
+ """
363
+ )
364
+
365
+ # ---------------------------------------------------------------------------
366
+ # Footer
367
+ # ---------------------------------------------------------------------------
368
+ st.divider()
369
+ st.caption(
370
+ "Data sourced from PubMed / NCBI (NIH) and ClinicalTrials.gov — "
371
+ "both public, freely available databases. "
372
+ "Always verify information against original sources before clinical application."
373
+ )