michaellupo74 commited on
Commit
ac24f4d
·
1 Parent(s): 9d5ec61

feat(ui): Sprint 2 UI — save/hide, filters, deadline sort, CSV, badges

Browse files
Files changed (1) hide show
  1. app/ui_streamlit.py +76 -8
app/ui_streamlit.py CHANGED
@@ -186,6 +186,27 @@ def _ministry_filter(rows):
186
  if is_preferred_agency or has_ministry_cue:
187
  kept.append(r)
188
  return kept
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  # ---------- end helpers ----------
190
 
191
  # ---------- optional diagnostics ----------
@@ -246,10 +267,38 @@ sort_by = st.selectbox("Sort by", ["Relevance", "Deadline (soonest first)"], ind
246
  only_open = st.checkbox("Only show opportunities with a future deadline", value=True)
247
  ministry_focus = st.checkbox("Ministry Focus (hide research/defense/academic BAAs)", value=True)
248
 
 
 
 
 
 
 
 
 
 
 
249
  # Build backend filters (if the search() supports them)
250
  backend_filters = {}
251
  if geo: backend_filters["geo"] = geo
252
  if categories: backend_filters["categories"] = categories
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
  col1, col2 = st.columns([1, 1])
255
 
@@ -277,36 +326,37 @@ with col1:
277
  open_filtered = [r for r in base_filtered
278
  if (_to_date_safe(r.get("deadline")) or date.max) >= date.today()]
279
 
280
- # 3) Ministry filter
 
 
 
 
 
281
  final_results = _ministry_filter(open_filtered) if ministry_focus else open_filtered
282
 
283
- # --- Step 1: clear any previous “show hidden choice when filter is off
284
  if not ministry_focus and st.session_state.get("show_hidden"):
285
  st.session_state.pop("show_hidden", None)
286
 
287
- # --- Step 2: count how many items the ministry filter hid
288
  hidden_due_to_ministry = 0
289
  if ministry_focus:
290
  hidden_due_to_ministry = len(open_filtered) - len(final_results)
291
- # Reset toggle on a new search run
292
  st.session_state.pop("show_hidden", None)
293
 
294
- # Save fully-filtered results
295
  st.session_state["results"] = final_results
296
  st.session_state["last_query"] = q
297
  st.session_state["last_filters"] = {
298
  "geo": geo, "categories": categories,
299
  "only_open": only_open, "ministry_focus": ministry_focus,
 
300
  }
301
 
302
- # Honest breakdown message (now includes hidden count)
303
  st.success(
304
  f"Found {len(dedup)} total • After geo/cat: {len(base_filtered)} • "
305
  f"Open-only: {len(open_filtered)} • Displaying: {len(final_results)}"
306
  + (f" • Hidden by ministry filter: {hidden_due_to_ministry}" if ministry_focus else "")
307
  )
308
 
309
- # Checkbox to reveal hidden items without re-searching
310
  if ministry_focus and hidden_due_to_ministry > 0:
311
  if st.checkbox(f"Show hidden items ({hidden_due_to_ministry})", value=False, key="show_hidden"):
312
  st.session_state["results"] = open_filtered
@@ -338,6 +388,12 @@ def _to_date(d):
338
  # ---- Render results ----
339
  results = st.session_state.get("results", [])
340
 
 
 
 
 
 
 
341
  # Apply sort if selected
342
  if sort_by.startswith("Deadline") and results:
343
  results.sort(
@@ -357,17 +413,29 @@ if results:
357
  url = r.get("url", "")
358
  cats = r.get("categories") or r.get("cats") or []
359
  geo_tags = r.get("geo") or []
 
360
 
361
  st.markdown(f"### {title}")
362
  st.write(f"**Source:** {r.get('source','')} | **Geo:** {', '.join(geo_tags) if isinstance(geo_tags, list) else geo_tags} | **Categories:** {', '.join(cats) if isinstance(cats, list) else cats}")
363
 
 
364
  if url and not url.startswith("http"):
365
  st.caption("Note: This item may display an ID or number instead of a full link. Open on Grants.gov if needed.")
366
  st.write(f"[Open Link]({url}) \nScore: {r.get('score', 0):.3f}")
367
 
 
368
  posted = r.get("posted_date") or ""
369
  deadline = r.get("deadline") or ""
370
- st.caption(f"Posted: {posted} • Deadline: {deadline}")
 
 
 
 
 
 
 
 
 
371
  st.markdown("---")
372
  else:
373
  if ran_search:
 
186
  if is_preferred_agency or has_ministry_cue:
187
  kept.append(r)
188
  return kept
189
+
190
+ def _days_until(iso):
191
+ from datetime import date, datetime
192
+ if not iso:
193
+ return None
194
+ try:
195
+ d = datetime.fromisoformat(str(iso)).date()
196
+ return (d - date.today()).days
197
+ except Exception:
198
+ return None
199
+
200
+ def _deadline_badge(days_left):
201
+ if days_left is None:
202
+ return "🟦 TBD"
203
+ if days_left < 0:
204
+ return "⬛ Closed"
205
+ if days_left <= 14:
206
+ return f"🟥 Due in {days_left}d"
207
+ if days_left <= 30:
208
+ return f"🟨 {days_left}d"
209
+ return f"🟩 {days_left}d"
210
  # ---------- end helpers ----------
211
 
212
  # ---------- optional diagnostics ----------
 
267
  only_open = st.checkbox("Only show opportunities with a future deadline", value=True)
268
  ministry_focus = st.checkbox("Ministry Focus (hide research/defense/academic BAAs)", value=True)
269
 
270
+ # NEW: Sprint 2 view + agency facet
271
+ view = st.selectbox("View", ["All", "Saved", "Hidden"], index=0)
272
+ # pre-load agencies list (from meta.json when present)
273
+ try:
274
+ meta_for_agencies = json.loads(Path(_env["INDEX_DIR"], "meta.json").read_text())
275
+ agency_options = sorted({m.get("agency") for m in meta_for_agencies if m.get("agency")})
276
+ except Exception:
277
+ agency_options = []
278
+ sel_agencies = st.multiselect("Agency filter (optional)", options=agency_options, default=[])
279
+
280
  # Build backend filters (if the search() supports them)
281
  backend_filters = {}
282
  if geo: backend_filters["geo"] = geo
283
  if categories: backend_filters["categories"] = categories
284
+ if sel_agencies: backend_filters["agency"] = sel_agencies
285
+
286
+ # --- Sprint 2 session state for Save/Hide ---
287
+ if "saved_ids" not in st.session_state:
288
+ st.session_state.saved_ids = set()
289
+ if "hidden_ids" not in st.session_state:
290
+ st.session_state.hidden_ids = set()
291
+
292
+ # action helpers
293
+ def _save_item(item_id: str):
294
+ st.session_state.saved_ids.add(item_id)
295
+ st.session_state.hidden_ids.discard(item_id)
296
+ st.experimental_rerun()
297
+
298
+ def _hide_item(item_id: str):
299
+ st.session_state.hidden_ids.add(item_id)
300
+ st.session_state.saved_ids.discard(item_id)
301
+ st.experimental_rerun()
302
 
303
  col1, col2 = st.columns([1, 1])
304
 
 
326
  open_filtered = [r for r in base_filtered
327
  if (_to_date_safe(r.get("deadline")) or date.max) >= date.today()]
328
 
329
+ # 3) Agency filter (client-side, in case backend didn't apply)
330
+ if sel_agencies:
331
+ af = set(sel_agencies)
332
+ open_filtered = [r for r in open_filtered if (r.get("agency") in af)]
333
+
334
+ # 4) Ministry filter
335
  final_results = _ministry_filter(open_filtered) if ministry_focus else open_filtered
336
 
337
+ # Clear/show hidden toggle mgmt
338
  if not ministry_focus and st.session_state.get("show_hidden"):
339
  st.session_state.pop("show_hidden", None)
340
 
 
341
  hidden_due_to_ministry = 0
342
  if ministry_focus:
343
  hidden_due_to_ministry = len(open_filtered) - len(final_results)
 
344
  st.session_state.pop("show_hidden", None)
345
 
 
346
  st.session_state["results"] = final_results
347
  st.session_state["last_query"] = q
348
  st.session_state["last_filters"] = {
349
  "geo": geo, "categories": categories,
350
  "only_open": only_open, "ministry_focus": ministry_focus,
351
+ "agencies": sel_agencies,
352
  }
353
 
 
354
  st.success(
355
  f"Found {len(dedup)} total • After geo/cat: {len(base_filtered)} • "
356
  f"Open-only: {len(open_filtered)} • Displaying: {len(final_results)}"
357
  + (f" • Hidden by ministry filter: {hidden_due_to_ministry}" if ministry_focus else "")
358
  )
359
 
 
360
  if ministry_focus and hidden_due_to_ministry > 0:
361
  if st.checkbox(f"Show hidden items ({hidden_due_to_ministry})", value=False, key="show_hidden"):
362
  st.session_state["results"] = open_filtered
 
388
  # ---- Render results ----
389
  results = st.session_state.get("results", [])
390
 
391
+ # Apply "View" (All/Saved/Hidden)
392
+ if view == "Saved":
393
+ results = [r for r in results if r.get("id") in st.session_state.saved_ids]
394
+ elif view == "Hidden":
395
+ results = [r for r in results if r.get("id") in st.session_state.hidden_ids]
396
+
397
  # Apply sort if selected
398
  if sort_by.startswith("Deadline") and results:
399
  results.sort(
 
413
  url = r.get("url", "")
414
  cats = r.get("categories") or r.get("cats") or []
415
  geo_tags = r.get("geo") or []
416
+ _id = r.get("id") or r.get("url") or title
417
 
418
  st.markdown(f"### {title}")
419
  st.write(f"**Source:** {r.get('source','')} | **Geo:** {', '.join(geo_tags) if isinstance(geo_tags, list) else geo_tags} | **Categories:** {', '.join(cats) if isinstance(cats, list) else cats}")
420
 
421
+ # Link / score
422
  if url and not url.startswith("http"):
423
  st.caption("Note: This item may display an ID or number instead of a full link. Open on Grants.gov if needed.")
424
  st.write(f"[Open Link]({url}) \nScore: {r.get('score', 0):.3f}")
425
 
426
+ # Deadline + badge
427
  posted = r.get("posted_date") or ""
428
  deadline = r.get("deadline") or ""
429
+ days_left = _days_until(deadline)
430
+ st.caption(f"Posted: {posted} • Deadline: {deadline} • {_deadline_badge(days_left)}")
431
+
432
+ # Save / Hide buttons
433
+ c1, c2, _ = st.columns([1,1,6])
434
+ if c1.button(("✅ Saved" if _id in st.session_state.saved_ids else "💾 Save"), key=f"save-{_id}"):
435
+ _save_item(_id)
436
+ if c2.button(("🙈 Hidden" if _id in st.session_state.hidden_ids else "🙈 Hide"), key=f"hide-{_id}"):
437
+ _hide_item(_id)
438
+
439
  st.markdown("---")
440
  else:
441
  if ran_search: