|
|
|
|
@ -929,62 +929,90 @@ def build_svd_components_tab(db_path: str) -> None: |
|
|
|
|
) |
|
|
|
|
comp_sel = comp_options[comp_sel_idx] |
|
|
|
|
|
|
|
|
|
# Show theme explanation + poles |
|
|
|
|
# Show theme explanation |
|
|
|
|
theme = SVD_THEMES.get(comp_sel, {}) |
|
|
|
|
if theme: |
|
|
|
|
st.info(f"**{theme['label']}** — {theme['explanation']}") |
|
|
|
|
pos = theme.get("positive_pole", "") |
|
|
|
|
neg = theme.get("negative_pole", "") |
|
|
|
|
if pos or neg: |
|
|
|
|
pcol, ncol = st.columns(2) |
|
|
|
|
with pcol: |
|
|
|
|
st.success(f"▲ **Positieve pool:** {pos}") |
|
|
|
|
with ncol: |
|
|
|
|
st.error(f"▼ **Negatieve pool:** {neg}") |
|
|
|
|
|
|
|
|
|
motions = comp_map.get(comp_sel, []) |
|
|
|
|
|
|
|
|
|
col1, col2 = st.columns([1, 2]) |
|
|
|
|
with col1: |
|
|
|
|
st.markdown("**Top-moties (titels)**") |
|
|
|
|
for m in motions: |
|
|
|
|
mid = m.get("motion_id") |
|
|
|
|
score = m.get("score", 0.0) |
|
|
|
|
title = m.get("title") or f"Motie #{mid}" |
|
|
|
|
sign = "▲" if score >= 0 else "▼" |
|
|
|
|
if st.button(f"{sign} {mid}: {title[:72]}", key=f"btn_{comp_sel}_{mid}"): |
|
|
|
|
st.session_state["svd_selected_mid"] = mid |
|
|
|
|
# Party axis chart |
|
|
|
|
party_scores = load_party_axis_scores(db_path) |
|
|
|
|
_render_party_axis_chart(party_scores, comp_sel) |
|
|
|
|
|
|
|
|
|
with col2: |
|
|
|
|
sel_mid = st.session_state.get("svd_selected_mid") |
|
|
|
|
if not sel_mid and motions: |
|
|
|
|
sel_mid = motions[0].get("motion_id") |
|
|
|
|
if sel_mid: |
|
|
|
|
# fetch motion metadata from DB for completeness |
|
|
|
|
# Batch-fetch motion details (title, date, policy_area, url, body_text, voting_results) |
|
|
|
|
motion_ids = [m.get("motion_id") for m in motions if m.get("motion_id") is not None] |
|
|
|
|
motion_details: Dict[int, tuple] = {} |
|
|
|
|
if motion_ids: |
|
|
|
|
try: |
|
|
|
|
placeholders = ", ".join("?" for _ in motion_ids) |
|
|
|
|
con = duckdb.connect(database=db_path, read_only=True) |
|
|
|
|
row = con.execute( |
|
|
|
|
"SELECT id, title, date, policy_area, url, body_text FROM motions WHERE id=?", |
|
|
|
|
[int(sel_mid)], |
|
|
|
|
).fetchone() |
|
|
|
|
db_rows = con.execute( |
|
|
|
|
f"SELECT id, title, date, policy_area, url, body_text, voting_results " |
|
|
|
|
f"FROM motions WHERE id IN ({placeholders})", |
|
|
|
|
[int(mid) for mid in motion_ids], |
|
|
|
|
).fetchall() |
|
|
|
|
con.close() |
|
|
|
|
motion_details = {r[0]: r for r in db_rows} |
|
|
|
|
except Exception: |
|
|
|
|
row = None |
|
|
|
|
|
|
|
|
|
if row: |
|
|
|
|
st.markdown(f"### {row[1] or f'Motie #{row[0]}'}") |
|
|
|
|
try: |
|
|
|
|
date_str = str(row[2])[:10] |
|
|
|
|
except Exception: |
|
|
|
|
date_str = "?" |
|
|
|
|
st.caption(f"📅 {date_str} | {row[3]}") |
|
|
|
|
if row[4] and str(row[4]).startswith("http"): |
|
|
|
|
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})") |
|
|
|
|
if row[5]: |
|
|
|
|
with st.expander("Toon volledige tekst"): |
|
|
|
|
st.write(row[5]) |
|
|
|
|
else: |
|
|
|
|
st.info(f"Metadata not found in DB for motion {sel_mid}") |
|
|
|
|
logger.exception("Failed to batch-fetch motion details") |
|
|
|
|
|
|
|
|
|
# Split motions by pole sign |
|
|
|
|
pos_motions = [m for m in motions if float(m.get("score", 0.0)) >= 0] |
|
|
|
|
neg_motions = [m for m in motions if float(m.get("score", 0.0)) < 0] |
|
|
|
|
|
|
|
|
|
pos_pole = ( |
|
|
|
|
theme.get("positive_pole", "Positieve pool") if theme else "Positieve pool" |
|
|
|
|
) |
|
|
|
|
neg_pole = ( |
|
|
|
|
theme.get("negative_pole", "Negatieve pool") if theme else "Negatieve pool" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
pcol, ncol = st.columns(2) |
|
|
|
|
|
|
|
|
|
with pcol: |
|
|
|
|
st.success(f"▲ **Positieve pool:** {pos_pole}") |
|
|
|
|
for m in pos_motions: |
|
|
|
|
mid = m.get("motion_id") |
|
|
|
|
raw_title = m.get("title") or f"Motie #{mid}" |
|
|
|
|
with st.expander(f"▲ {raw_title[:80]}"): |
|
|
|
|
row = motion_details.get(int(mid)) if mid is not None else None |
|
|
|
|
if row: |
|
|
|
|
try: |
|
|
|
|
date_str = str(row[2])[:10] |
|
|
|
|
except Exception: |
|
|
|
|
date_str = "?" |
|
|
|
|
st.caption(f"📅 {date_str} | {row[3] or '—'}") |
|
|
|
|
if row[4] and str(row[4]).startswith("http"): |
|
|
|
|
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})") |
|
|
|
|
if row[5]: |
|
|
|
|
with st.expander("Toon volledige tekst"): |
|
|
|
|
st.write(row[5]) |
|
|
|
|
_render_voting_results(row[6]) |
|
|
|
|
else: |
|
|
|
|
st.caption("_Geen metadata beschikbaar_") |
|
|
|
|
|
|
|
|
|
with ncol: |
|
|
|
|
st.error(f"▼ **Negatieve pool:** {neg_pole}") |
|
|
|
|
for m in neg_motions: |
|
|
|
|
mid = m.get("motion_id") |
|
|
|
|
raw_title = m.get("title") or f"Motie #{mid}" |
|
|
|
|
with st.expander(f"▼ {raw_title[:80]}"): |
|
|
|
|
row = motion_details.get(int(mid)) if mid is not None else None |
|
|
|
|
if row: |
|
|
|
|
try: |
|
|
|
|
date_str = str(row[2])[:10] |
|
|
|
|
except Exception: |
|
|
|
|
date_str = "?" |
|
|
|
|
st.caption(f"📅 {date_str} | {row[3] or '—'}") |
|
|
|
|
if row[4] and str(row[4]).startswith("http"): |
|
|
|
|
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})") |
|
|
|
|
if row[5]: |
|
|
|
|
with st.expander("Toon volledige tekst"): |
|
|
|
|
st.write(row[5]) |
|
|
|
|
_render_voting_results(row[6]) |
|
|
|
|
else: |
|
|
|
|
st.caption("_Geen metadata beschikbaar_") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_mp_quiz_tab(db_path: str) -> None: |
|
|
|
|
|