Spaces:
Running
Running
File size: 17,962 Bytes
22123a0 99589b3 598f5cb 34f03c5 e7b7547 1aab675 67b91ae cd6fa25 67b91ae b363844 67b91ae b363844 67b91ae b363844 67b91ae b363844 67b91ae b363844 67b91ae b363844 cd6fa25 f5ccd57 a307b42 f087f9c 9344898 6275591 4f1f48e 9344898 f5ccd57 9344898 6275591 9344898 70409b4 a307b42 b363844 a307b42 6275591 b363844 9344898 b363844 6275591 9344898 b363844 9344898 6275591 b363844 6275591 9344898 b363844 9344898 b363844 70409b4 9344898 b363844 9344898 f5ccd57 b363844 bdfae6e b7a235a 9344898 bdfae6e f5ccd57 a307b42 b4c5e1d 99dfef1 22123a0 1aab675 22123a0 1aab675 22123a0 1aab675 b363844 22123a0 1aab675 22123a0 34f03c5 22123a0 34f03c5 22123a0 34f03c5 22123a0 34f03c5 598f5cb 22123a0 598f5cb 22123a0 598f5cb 22123a0 370fc0f 22123a0 99589b3 370fc0f 99589b3 22123a0 598f5cb b363844 99589b3 370fc0f 6c0ec0d b363844 6c0ec0d b363844 6c0ec0d 598f5cb 6c0ec0d b363844 6c0ec0d b363844 6c0ec0d b363844 6c0ec0d b363844 598f5cb b363844 598f5cb 99589b3 598f5cb 22123a0 598f5cb 99589b3 598f5cb 22123a0 99589b3 b363844 99589b3 b363844 598f5cb 99589b3 370fc0f b363844 370fc0f 598f5cb 22123a0 598f5cb 22123a0 598f5cb 22123a0 99589b3 598f5cb b363844 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 |
# app/ui_streamlit.py
# Ensure project root is on sys.path when Streamlit runs this as a script
import sys, pathlib
ROOT = pathlib.Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import os, json
from pathlib import Path
from app.main import get_env, ensure_index_exists
from app.search import search
import streamlit as st
st.markdown("""
<style>
/* --- Global safety net: make default text dark --- */
html, body, [class^="css"], [class*=" css"] {
color: #0f172a !important; /* slate-900 */
}
/* --- Streamlit selectbox/multiselect (BaseWeb rendering) --- */
div[data-baseweb="select"] * { color: #0f172a !important; }
div[data-baseweb="select"] { background: #ffffff !important; border-color: #cbd5e1 !important; }
/* placeholder inside the closed select */
div[data-baseweb="select"] div[aria-hidden="true"] { color: #64748b !important; }
/* open dropdown menu (BaseWeb popover) */
div[data-baseweb="popover"] [role="listbox"], div[data-baseweb="menu"] { background: #ffffff !important; }
div[data-baseweb="popover"] [role="option"], div[data-baseweb="menu"] li { color: #0f172a !important; background: #ffffff !important; }
/* --- Alternative rendering (ARIA hooks) in newer Streamlit builds --- */
div[role="button"][aria-haspopup="listbox"] * { color: #0f172a !important; }
ul[role="listbox"] li, div[role="option"] { color: #0f172a !important; background: #ffffff !important; }
/* --- Streamlit component wrappers --- */
.stSelectbox, .stMultiSelect { color: #0f172a !important; }
.stSelectbox div, .stMultiSelect div { color: #0f172a !important; }
/* --- Hard reset in case a global rule set all <span> to white --- */
span, li { color: inherit !important; }
</style>
""", unsafe_allow_html=True)
# ββ Streamlit config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.set_page_config(page_title="Grants Discovery App By Lupo", page_icon="π§", layout="wide")
# ββ Theme & CSS (BLACK + ORANGE, dark selects) ββββββββββββββββββββββββββββββββ
st.markdown("""
<style>
/* App base */
.stApp { background-color: #000000; color: #f8fafc; }
/* Text defaults */
html, body, [class*="css"], h1, h2, h3, h4, h5, h6, p, span, div { color: #f8fafc !important; }
/* Accents */
a, .stRadio > label, .stSlider label { color: #f97316 !important; }
/* Buttons */
.stButton>button { background:#f97316; color:#fff; border:none; border-radius:8px; padding:0.5rem 0.9rem; font-weight:600; }
.stButton>button:hover { filter:brightness(1.1); }
/* Text input */
.stTextInput input { background:#111827 !important; color:#f8fafc !important; border:1px solid #334155 !important; }
/* Closed control (select/multiselect) */
.stSelectbox div[data-baseweb="select"], .stMultiSelect div[data-baseweb="select"],
.stSelectbox div[role="combobox"], .stMultiSelect div[role="combobox"] {
background-color:#1e293b !important; color:#f8fafc !important; border:1px solid #334155 !important; border-radius:8px !important;
}
/* Text & icons inside control */
.stSelectbox div[data-baseweb="select"] div, .stMultiSelect div[data-baseweb="select"] div,
.stSelectbox div[data-baseweb="select"] input, .stMultiSelect div[data-baseweb="select"] input,
.stSelectbox svg, .stMultiSelect svg { color:#f8fafc !important; fill:#f8fafc !important; }
/* Placeholder */
.stSelectbox div[data-baseweb="select"] input::placeholder, .stMultiSelect div[data-baseweb="select"] input::placeholder { color:#94a3b8 !important; }
/* Selected chips (multiselect) */
.stMultiSelect [data-baseweb="tag"] { background-color:#334155 !important; color:#e2e8f0 !important; border-radius:999px !important; }
/* Open dropdown menu */
div[data-baseweb="menu"] { background-color:#0b1220 !important; color:#f8fafc !important; border:1px solid #334155 !important; border-radius:10px !important; }
div[data-baseweb="menu"] [role="option"] { background:transparent !important; color:#f8fafc !important; }
div[data-baseweb="menu"] [role="option"]:hover { background:#1f2937 !important; }
div[data-baseweb="menu"] [role="option"][aria-selected="true"] { background:#334155 !important; color:#f8fafc !important; }
/* Result cards */
.result-card { border:1px solid #1e293b; background:#1e293b; border-radius:14px; padding:16px; margin:10px 0; box-shadow:0 1px 2px rgba(0,0,0,0.2); }
.result-meta { font-size:13px; color:#94a3b8; margin-top:6px; }
span.chip { display:inline-block; padding:3px 8px; border-radius:999px; background:#334155; margin-right:6px; font-size:12px; color:#e2e8f0; }
/* Compact hero (single, 240px) */
.hero { height: 240px; border-radius: 16px; margin: 6px 0 16px;
background: linear-gradient(rgba(0,0,0,.45), rgba(0,0,0,.45)),
url('https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=1280&q=80') center/cover no-repeat; }
.hero-text { height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; color:#fff; }
.hero-text h1 { margin:0; font-size:28px; font-weight:700; color:#f97316; }
.hero-text p { margin:6px 0 0; font-size:15px; color:#fcd34d; }
/* ===== FORCE DARK SELECT / MULTISELECT ===== */
[data-testid="stSelectbox"] div[role="combobox"], [data-testid="stMultiSelect"] div[role="combobox"],
div[role="combobox"][aria-haspopup="listbox"] { background-color:#1e293b !important; color:#f8fafc !important; border:1px solid #334155 !important; border-radius:8px !important; }
[data-testid="stSelectbox"] div[role="combobox"] input, [data-testid="stMultiSelect"] div[role="combobox"] input,
div[role="combobox"] input { color:#f8fafc !important; }
div[role="combobox"] input::placeholder { color:#94a3b8 !important; }
div[role="combobox"] svg { color:#f8fafc !important; fill:#f8fafc !important; }
[data-testid="stMultiSelect"] [data-baseweb="tag"], [data-testid="stMultiSelect"] [aria-label="remove"] { background-color:#334155 !important; color:#e2e8f0 !important; border-radius:999px !important; }
div[role="listbox"], ul[role="listbox"], div[data-baseweb="menu"] { background-color:#0b1220 !important; color:#f8fafc !important; border:1px solid #334155 !important; border-radius:10px !important; }
[role="listbox"] [role="option"], div[data-baseweb="menu"] [role="option"] { background:transparent !important; color:#f8fafc !important; }
[role="listbox"] [role="option"]:hover, div[data-baseweb="menu"] [role="option"]:hover { background:#1f2937 !important; }
[role="listbox"] [role="option"][aria-selected="true"], div[data-baseweb="menu"] [role="option"][aria-selected="true"] { background:#334155 !important; color:#f8fafc !important; }
</style>
""", unsafe_allow_html=True)
# ββ Hero block (single) βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("""
<div class="hero">
<div class="hero-text">
<h1>Grants Discovery Live RAG by Lupo</h1>
<p>Find capacity-building grants fast.</p>
</div>
</div>
""", unsafe_allow_html=True)
# ββ Hide developer diagnostics by default βββββββββββββββββββββββββββββββββββββ
SHOW_DEV = os.environ.get("SHOW_DEV") == "1"
# ββ Environment + index βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_env = get_env()
ensure_index_exists(_env)
# ---------- helpers ----------
def _dedup_records(rows):
seen, out = set(), []
for r in rows or []:
k = r.get("id") or r.get("url") or r.get("title")
if not k or k in seen:
continue
seen.add(k)
out.append(r)
return out
def _norm_list(v):
if v is None:
return []
if isinstance(v, str):
parts = [p.strip() for p in v.replace(";", ",").split(",")]
return [p.lower() for p in parts if p]
if isinstance(v, (list, tuple, set)):
return [str(x).lower() for x in v]
return []
def _matches_filters(rec, geo_sel, cat_sel):
rec_geo = _norm_list(rec.get("geo") or rec.get("region") or rec.get("state"))
rec_cat = _norm_list(rec.get("categories") or rec.get("cats") or rec.get("category"))
g_ok = (not geo_sel) or (set([g.lower() for g in geo_sel]) & set(rec_geo))
c_ok = (not cat_sel) or (set([c.lower() for c in cat_sel]) & set(rec_cat))
return g_ok and c_ok
def _ministry_filter(rows):
if not rows:
return rows
banned_terms = [
"broad agency announcement", "baa", "research", "r&d", "prototype",
"laboratory", "university", "sbir", "sttr",
"darpa", "office of naval research", "onr", "naval", "air force", "army",
"w911", "n00014", "fa-", "afrl", "arpa"
]
preferred_agencies = {
"FTA", "HHS", "ACL", "USDA", "USDA-FNS", "USDA-RD", "DOL", "DOJ", "OJP", "OVW",
"EDA", "HRSA", "SAMHSA", "CFPB", "HUD"
}
required_any_terms = [
"vehicle", "van", "bus", "paratransit", "mobility",
"congregate meals", "home-delivered meals", "senior nutrition",
"food pantry", "food bank", "hunger relief", "refrigeration", "freezer",
"community", "faith", "church", "ministry", "nonprofit",
"reentry", "workforce", "case management", "technical assistance"
]
def txt(r):
return " ".join([
str(r.get("title","")),
str(r.get("synopsis") or r.get("summary") or ""),
str(r.get("agency") or ""),
]).lower()
kept = []
for r in rows:
t = txt(r)
if any(b in t for b in banned_terms):
continue
agency = (r.get("agency") or "").upper()
cats = [c.lower() for c in (r.get("categories") or [])]
is_preferred_agency = any(agency.startswith(a) for a in preferred_agencies)
has_ministry_cue = any(term in t for term in required_any_terms) or any(
c in {"transportation","vehicle","elderly","disabled","food","community","justice","reentry","workforce"} for c in cats
)
if is_preferred_agency or has_ministry_cue:
kept.append(r)
return kept
# ---------- end helpers ----------
# ---------- optional diagnostics ----------
with st.expander("Diagnostics (optional)", expanded=False):
idx = Path(_env["INDEX_DIR"])
st.write("INDEX_DIR:", str(idx))
st.write("faiss.index exists:", (idx / "faiss.index").exists())
st.write("meta.json exists:", (idx / "meta.json").exists())
if (idx / "meta.json").exists():
try:
meta = json.loads((idx / "meta.json").read_text())
st.write("meta.json count:", len(meta))
st.write("meta head:", [{"id": m.get("id"), "title": m.get("title")} for m in meta[:2]])
except Exception as e:
st.error(f"Failed to read meta.json: {e!r}")
try:
demo = search("transportation", _env, top_k=3, filters={})
st.write("sample search('transportation') results:", len(demo))
if demo:
st.write(demo[:3])
except Exception as e:
st.error(f"search() raised: {e!r}")
# ---------- end diagnostics ----------
st.title("Grants Discovery RAG (Capacity Building)")
preset = st.radio(
"Quick topic:",
["General", "Elderly", "Prison Ministry", "Evangelism", "Vehicles/Transport", "FTA 5310"],
horizontal=True
)
default_q = {
"General": "capacity building",
"Elderly": "capacity building for seniors and aging services",
"Prison Ministry": "capacity building for reentry and prison ministry",
"Evangelism": "capacity building for faith and community outreach",
"Vehicles/Transport": "capacity building transportation vehicles vans buses mobility",
"FTA 5310": "5310 Enhanced Mobility Seniors Individuals with Disabilities",
}.get(preset, "capacity building")
# --- controls ---
q = st.text_input("Search query", value=default_q)
geo = st.multiselect("Geo filter (optional)", options=["US", "MD", "PA"], default=[])
categories = st.multiselect(
"Category filter (optional)",
options=[
"capacity_building","elderly","prison_ministry","evangelism",
"transportation","vehicle",
"justice","reentry","victim_services","youth","women","food","workforce"
],
default=[]
)
top_k = st.slider("Results", 5, 50, 15)
sort_by = st.selectbox("Sort by", ["Relevance", "Deadline (soonest first)"], index=0)
only_open = st.checkbox("Only show opportunities with a future deadline", value=True)
ministry_focus = st.checkbox("Ministry Focus (hide research/defense/academic BAAs)", value=True)
# Build backend filters (if the search() supports them)
backend_filters = {}
if geo: backend_filters["geo"] = geo
if categories: backend_filters["categories"] = categories
col1, col2 = st.columns([1, 1])
with col1:
if st.button("Search"):
try:
raw = search(q, _env, top_k=top_k, filters=backend_filters)
dedup = _dedup_records(raw)
# 1) Geo/Category client-side filter (fallback if backend ignores)
if geo or categories:
base_filtered = [r for r in dedup if _matches_filters(r, geo, categories)]
else:
base_filtered = dedup
# 2) Only-open filter
from datetime import date, datetime
def _to_date_safe(val):
if not val: return None
try: return datetime.fromisoformat(str(val)).date()
except Exception: return None
open_filtered = base_filtered
if only_open:
open_filtered = [r for r in base_filtered
if (_to_date_safe(r.get("deadline")) or date.max) >= date.today()]
# 3) Ministry filter
final_results = _ministry_filter(open_filtered) if ministry_focus else open_filtered
# --- Step 1: clear any previous βshow hiddenβ choice when filter is off
if not ministry_focus and st.session_state.get("show_hidden"):
st.session_state.pop("show_hidden", None)
# --- Step 2: count how many items the ministry filter hid
hidden_due_to_ministry = 0
if ministry_focus:
hidden_due_to_ministry = len(open_filtered) - len(final_results)
# Reset toggle on a new search run
st.session_state.pop("show_hidden", None)
# Save fully-filtered results
st.session_state["results"] = final_results
st.session_state["last_query"] = q
st.session_state["last_filters"] = {
"geo": geo, "categories": categories,
"only_open": only_open, "ministry_focus": ministry_focus,
}
# Honest breakdown message (now includes hidden count)
st.success(
f"Found {len(dedup)} total β’ After geo/cat: {len(base_filtered)} β’ "
f"Open-only: {len(open_filtered)} β’ Displaying: {len(final_results)}"
+ (f" β’ Hidden by ministry filter: {hidden_due_to_ministry}" if ministry_focus else "")
)
# Checkbox to reveal hidden items without re-searching
if ministry_focus and hidden_due_to_ministry > 0:
if st.checkbox(f"Show hidden items ({hidden_due_to_ministry})", value=False, key="show_hidden"):
st.session_state["results"] = open_filtered
except Exception as e:
st.error(str(e))
with col2:
if st.button("Export Results to CSV"):
results_for_export = st.session_state.get("results", [])
if not results_for_export:
st.warning("No results to export. Run a search first.")
else:
os.makedirs(_env["EXPORT_DIR"], exist_ok=True)
out_path = os.path.join(_env["EXPORT_DIR"], "results.csv")
import pandas as pd
pd.DataFrame(results_for_export).to_csv(out_path, index=False)
st.success(f"Exported to {out_path}")
st.markdown("---")
# ---- Sorting/filter helpers ----
from datetime import date, datetime
def _to_date(d):
if not d: return None
try: return datetime.fromisoformat(str(d)).date()
except Exception: return None
# ---- Render results ----
results = st.session_state.get("results", [])
# Apply sort if selected
if sort_by.startswith("Deadline") and results:
results.sort(
key=lambda r: (
_to_date(r.get("deadline")) is None,
_to_date(r.get("deadline")) or date.max,
)
)
# Did the user run a search?
ran_search = bool(st.session_state.get("last_query"))
if results:
st.caption(f"Results: {len(results)}")
for r in results:
title = r.get("title", "(no title)")
url = r.get("url", "")
cats = r.get("categories") or r.get("cats") or []
geo_tags = r.get("geo") or []
st.markdown(f"### {title}")
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}")
if url and not url.startswith("http"):
st.caption("Note: This item may display an ID or number instead of a full link. Open on Grants.gov if needed.")
st.write(f"[Open Link]({url}) \nScore: {r.get('score', 0):.3f}")
posted = r.get("posted_date") or ""
deadline = r.get("deadline") or ""
st.caption(f"Posted: {posted} β’ Deadline: {deadline}")
st.markdown("---")
else:
if ran_search:
st.info("No active grants match these filters right now. Weβll notify you when the next cycle opens.")
else:
st.info("Enter a query and click Search.")
|