Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Upload streamlit_app.py with huggingface_hub
Browse files- streamlit_app.py +174 -2
streamlit_app.py
CHANGED
|
@@ -566,7 +566,7 @@ if not st.session_state.get("_hf_downloaded"):
|
|
| 566 |
# ββ Data loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 567 |
from utils.data_loader import (
|
| 568 |
load_bills, load_summaries, load_suggested_questions,
|
| 569 |
-
load_reports, load_newsletters, get_last_updated,
|
| 570 |
get_summary, get_suggested_questions, get_report,
|
| 571 |
CHANGES_DIR,
|
| 572 |
)
|
|
@@ -575,6 +575,7 @@ df = load_bills()
|
|
| 575 |
summaries = load_summaries()
|
| 576 |
questions_cache = load_suggested_questions()
|
| 577 |
reports_cache = load_reports()
|
|
|
|
| 578 |
|
| 579 |
|
| 580 |
if df.empty:
|
|
@@ -751,7 +752,7 @@ with st.sidebar:
|
|
| 751 |
st.button("β Clear all filters", on_click=_clear_filters, width="stretch")
|
| 752 |
|
| 753 |
# ββ Tabs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 754 |
-
tab_bills, tab_insights, tab_ai, tab_updates = st.tabs(["Bills", "Insights", "AI Tools", "Newsletter"])
|
| 755 |
|
| 756 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 757 |
# TAB 1 β BILLS
|
|
@@ -883,6 +884,177 @@ with tab_bills:
|
|
| 883 |
st.download_button("β¬ CSV", tbl.to_csv(index=False), "bills.csv", "text/csv")
|
| 884 |
|
| 885 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 886 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 887 |
# TAB 2 β INSIGHTS (Charts)
|
| 888 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 566 |
# ββ Data loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 567 |
from utils.data_loader import (
|
| 568 |
load_bills, load_summaries, load_suggested_questions,
|
| 569 |
+
load_reports, load_newsletters, load_calendar, get_last_updated,
|
| 570 |
get_summary, get_suggested_questions, get_report,
|
| 571 |
CHANGES_DIR,
|
| 572 |
)
|
|
|
|
| 575 |
summaries = load_summaries()
|
| 576 |
questions_cache = load_suggested_questions()
|
| 577 |
reports_cache = load_reports()
|
| 578 |
+
calendar_events = load_calendar()
|
| 579 |
|
| 580 |
|
| 581 |
if df.empty:
|
|
|
|
| 752 |
st.button("β Clear all filters", on_click=_clear_filters, width="stretch")
|
| 753 |
|
| 754 |
# ββ Tabs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 755 |
+
tab_bills, tab_calendar, tab_insights, tab_ai, tab_updates = st.tabs(["Bills", "Calendar", "Insights", "AI Tools", "Newsletter"])
|
| 756 |
|
| 757 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 758 |
# TAB 1 β BILLS
|
|
|
|
| 884 |
st.download_button("β¬ CSV", tbl.to_csv(index=False), "bills.csv", "text/csv")
|
| 885 |
|
| 886 |
|
| 887 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 888 |
+
# TAB β CALENDAR (Bills to Watch)
|
| 889 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 890 |
+
with tab_calendar:
|
| 891 |
+
import calendar as _pycal
|
| 892 |
+
from collections import defaultdict
|
| 893 |
+
|
| 894 |
+
_cal_colors = {
|
| 895 |
+
"hearing_scheduled": "#E8A317",
|
| 896 |
+
"committee_referral": "#5B8DEF",
|
| 897 |
+
"committee_passed": "#50C878",
|
| 898 |
+
"floor_vote": "#CF4520",
|
| 899 |
+
"sent_to_governor": "#8B6914",
|
| 900 |
+
}
|
| 901 |
+
_cal_dots = {
|
| 902 |
+
"hearing_scheduled": "●",
|
| 903 |
+
"committee_referral": "●",
|
| 904 |
+
"committee_passed": "●",
|
| 905 |
+
"floor_vote": "●",
|
| 906 |
+
"sent_to_governor": "●",
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
if calendar_events:
|
| 910 |
+
# Filter by milestone type
|
| 911 |
+
all_types = sorted(set(e.get("event_label", e.get("event_type", "")) for e in calendar_events))
|
| 912 |
+
selected_types = st.multiselect(
|
| 913 |
+
"Filter by milestone type", all_types, default=all_types, key="cal_type_filter"
|
| 914 |
+
)
|
| 915 |
+
cal_filtered = [e for e in calendar_events
|
| 916 |
+
if e.get("event_label", e.get("event_type", "")) in selected_types]
|
| 917 |
+
|
| 918 |
+
if cal_filtered:
|
| 919 |
+
# Determine which month to show (most recent event)
|
| 920 |
+
latest_date = cal_filtered[0].get("event_date", "")
|
| 921 |
+
if latest_date:
|
| 922 |
+
cal_year = int(latest_date[:4])
|
| 923 |
+
cal_month = int(latest_date[5:7])
|
| 924 |
+
else:
|
| 925 |
+
cal_year = datetime.now().year
|
| 926 |
+
cal_month = datetime.now().month
|
| 927 |
+
|
| 928 |
+
# Month navigation
|
| 929 |
+
if "cal_month_offset" not in st.session_state:
|
| 930 |
+
st.session_state.cal_month_offset = 0
|
| 931 |
+
|
| 932 |
+
prev_col, title_col, next_col = st.columns([1, 5, 1])
|
| 933 |
+
with prev_col:
|
| 934 |
+
if st.button("β Prev", key="cal_prev", use_container_width=True):
|
| 935 |
+
st.session_state.cal_month_offset -= 1
|
| 936 |
+
st.rerun()
|
| 937 |
+
with next_col:
|
| 938 |
+
if st.button("Next β", key="cal_next", use_container_width=True):
|
| 939 |
+
st.session_state.cal_month_offset += 1
|
| 940 |
+
st.rerun()
|
| 941 |
+
|
| 942 |
+
# Apply offset
|
| 943 |
+
display_month = cal_month + st.session_state.cal_month_offset
|
| 944 |
+
display_year = cal_year
|
| 945 |
+
while display_month < 1:
|
| 946 |
+
display_month += 12
|
| 947 |
+
display_year -= 1
|
| 948 |
+
while display_month > 12:
|
| 949 |
+
display_month -= 12
|
| 950 |
+
display_year += 1
|
| 951 |
+
|
| 952 |
+
with title_col:
|
| 953 |
+
st.markdown(
|
| 954 |
+
f"<h3 style='text-align:center;margin:0;padding:0.3rem 0;'>"
|
| 955 |
+
f"{_pycal.month_name[display_month]} {display_year}</h3>",
|
| 956 |
+
unsafe_allow_html=True,
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
# Group events by date
|
| 960 |
+
events_by_date = defaultdict(list)
|
| 961 |
+
for e in cal_filtered:
|
| 962 |
+
events_by_date[e.get("event_date", "")].append(e)
|
| 963 |
+
|
| 964 |
+
# Build HTML calendar grid
|
| 965 |
+
cal_obj = _pycal.Calendar(firstweekday=6) # Sunday first
|
| 966 |
+
month_days = cal_obj.monthdayscalendar(display_year, display_month)
|
| 967 |
+
|
| 968 |
+
cal_html = '<table style="width:100%;border-collapse:collapse;font-family:Inter,sans-serif;table-layout:fixed;">'
|
| 969 |
+
# Header row
|
| 970 |
+
cal_html += '<tr>'
|
| 971 |
+
for day_name in ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]:
|
| 972 |
+
cal_html += f'<th style="padding:8px 4px;text-align:center;font-size:0.8rem;color:#64748B;border-bottom:2px solid #E2E8F0;">{day_name}</th>'
|
| 973 |
+
cal_html += '</tr>'
|
| 974 |
+
|
| 975 |
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
| 976 |
+
|
| 977 |
+
for week in month_days:
|
| 978 |
+
cal_html += '<tr>'
|
| 979 |
+
for day in week:
|
| 980 |
+
if day == 0:
|
| 981 |
+
cal_html += '<td style="padding:4px;border:1px solid #F1F5F9;min-height:80px;height:80px;vertical-align:top;background:#FAFAFA;"></td>'
|
| 982 |
+
else:
|
| 983 |
+
date_str = f"{display_year}-{display_month:02d}-{day:02d}"
|
| 984 |
+
is_today = date_str == today_str
|
| 985 |
+
day_events = events_by_date.get(date_str, [])
|
| 986 |
+
|
| 987 |
+
bg = "#FFF9E6" if day_events else "#FFFFFF"
|
| 988 |
+
border = "2px solid #CFB991" if is_today else "1px solid #F1F5F9"
|
| 989 |
+
day_num_style = "font-weight:700;color:#CFB991;" if is_today else "color:#0F172A;"
|
| 990 |
+
|
| 991 |
+
cal_html += f'<td style="padding:4px 6px;border:{border};min-height:80px;height:80px;vertical-align:top;background:{bg};">'
|
| 992 |
+
cal_html += f'<div style="font-size:0.75rem;{day_num_style}">{day}</div>'
|
| 993 |
+
|
| 994 |
+
for ev in day_events[:3]:
|
| 995 |
+
color = _cal_colors.get(ev.get("event_type", ""), "#CFB991")
|
| 996 |
+
label = f"{ev.get('state', '')} {ev.get('bill_number', '')}"
|
| 997 |
+
url = ev.get("bill_url", "")
|
| 998 |
+
milestone = ev.get("event_label", "")
|
| 999 |
+
if url:
|
| 1000 |
+
cal_html += (
|
| 1001 |
+
f'<a href="{url}" target="_blank" style="display:block;font-size:0.65rem;'
|
| 1002 |
+
f'padding:1px 3px;margin:1px 0;border-radius:3px;background:{color};'
|
| 1003 |
+
f'color:#fff;text-decoration:none;overflow:hidden;white-space:nowrap;'
|
| 1004 |
+
f'text-overflow:ellipsis;" title="{milestone}: {ev.get("event_description", "")}">'
|
| 1005 |
+
f'{label}</a>'
|
| 1006 |
+
)
|
| 1007 |
+
else:
|
| 1008 |
+
cal_html += (
|
| 1009 |
+
f'<div style="font-size:0.65rem;padding:1px 3px;margin:1px 0;'
|
| 1010 |
+
f'border-radius:3px;background:{color};color:#fff;overflow:hidden;'
|
| 1011 |
+
f'white-space:nowrap;text-overflow:ellipsis;" '
|
| 1012 |
+
f'title="{milestone}: {ev.get("event_description", "")}">'
|
| 1013 |
+
f'{label}</div>'
|
| 1014 |
+
)
|
| 1015 |
+
if len(day_events) > 3:
|
| 1016 |
+
cal_html += f'<div style="font-size:0.6rem;color:#64748B;">+{len(day_events) - 3} more</div>'
|
| 1017 |
+
|
| 1018 |
+
cal_html += '</td>'
|
| 1019 |
+
cal_html += '</tr>'
|
| 1020 |
+
|
| 1021 |
+
cal_html += '</table>'
|
| 1022 |
+
|
| 1023 |
+
# Legend
|
| 1024 |
+
legend = '<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-top:0.75rem;margin-bottom:0.5rem;">'
|
| 1025 |
+
for etype, color in _cal_colors.items():
|
| 1026 |
+
label = etype.replace("_", " ").title()
|
| 1027 |
+
legend += (
|
| 1028 |
+
f'<span style="font-size:0.75rem;color:#64748B;">'
|
| 1029 |
+
f'<span style="display:inline-block;width:10px;height:10px;border-radius:2px;'
|
| 1030 |
+
f'background:{color};margin-right:4px;vertical-align:middle;"></span>{label}</span>'
|
| 1031 |
+
)
|
| 1032 |
+
legend += '</div>'
|
| 1033 |
+
|
| 1034 |
+
st.markdown(cal_html + legend, unsafe_allow_html=True)
|
| 1035 |
+
|
| 1036 |
+
# Event list below calendar
|
| 1037 |
+
st.markdown("---")
|
| 1038 |
+
for e in cal_filtered:
|
| 1039 |
+
color = _cal_colors.get(e.get("event_type", ""), "#CFB991")
|
| 1040 |
+
url = e.get("bill_url", "")
|
| 1041 |
+
link_html = f' β <a href="{url}" target="_blank" style="color:#8B6914;">View on LegiScan</a>' if url else ""
|
| 1042 |
+
st.markdown(
|
| 1043 |
+
f'<div style="padding:0.4rem 0;border-bottom:1px solid #F1F5F9;">'
|
| 1044 |
+
f'<span style="display:inline-block;width:8px;height:8px;border-radius:2px;'
|
| 1045 |
+
f'background:{color};margin-right:6px;vertical-align:middle;"></span>'
|
| 1046 |
+
f'<strong>{e.get("event_date", "")}</strong> '
|
| 1047 |
+
f'<span style="color:#0F172A;font-weight:600;">{e.get("state", "")} {e.get("bill_number", "")}</span> '
|
| 1048 |
+
f'<span style="color:#64748B;">{e.get("event_description", "")}</span>'
|
| 1049 |
+
f'{link_html}</div>',
|
| 1050 |
+
unsafe_allow_html=True,
|
| 1051 |
+
)
|
| 1052 |
+
else:
|
| 1053 |
+
st.info("No events match the selected filters.")
|
| 1054 |
+
else:
|
| 1055 |
+
st.info("No recent legislative milestones available. Calendar data will populate after the next pipeline run.")
|
| 1056 |
+
|
| 1057 |
+
|
| 1058 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1059 |
# TAB 2 β INSIGHTS (Charts)
|
| 1060 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|