Spaces:
Sleeping
Sleeping
Commit
·
ac24f4d
1
Parent(s):
9d5ec61
feat(ui): Sprint 2 UI — save/hide, filters, deadline sort, CSV, badges
Browse files- 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
final_results = _ministry_filter(open_filtered) if ministry_focus else open_filtered
|
| 282 |
|
| 283 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|