|
|
# app.py
|
|
|
import streamlit as st
|
|
|
import pandas as pd
|
|
|
from datetime import datetime
|
|
|
from database import db
|
|
|
from summarizer import summarizer
|
|
|
from config import config
|
|
|
import json
|
|
|
|
|
|
# Page config
|
|
|
st.set_page_config(
|
|
|
page_title="Nederlandse Politieke Kompas", page_icon="🇳🇱", layout="wide"
|
|
|
)
|
|
|
|
|
|
|
|
|
def main():
|
|
|
st.title("🇳🇱 Nederlandse Politieke Kompas")
|
|
|
st.markdown(
|
|
|
"Ontdek welke politieke partij het beste bij jouw idealen past door te stemmen op echte Tweede Kamer moties."
|
|
|
)
|
|
|
|
|
|
# Initialize session state
|
|
|
if "session_id" not in st.session_state:
|
|
|
st.session_state.session_id = None
|
|
|
if "current_motion_index" not in st.session_state:
|
|
|
st.session_state.current_motion_index = 0
|
|
|
if "motions" not in st.session_state:
|
|
|
st.session_state.motions = []
|
|
|
if "show_results" not in st.session_state:
|
|
|
st.session_state.show_results = False
|
|
|
|
|
|
# Sidebar configuration
|
|
|
with st.sidebar:
|
|
|
st.header("Instellingen")
|
|
|
|
|
|
motion_count = st.slider(
|
|
|
"Aantal moties",
|
|
|
min_value=5,
|
|
|
max_value=25,
|
|
|
value=config.DEFAULT_MOTION_COUNT,
|
|
|
)
|
|
|
|
|
|
policy_area = st.selectbox("Beleidsgebied", config.POLICY_AREAS)
|
|
|
|
|
|
margin_range = st.slider(
|
|
|
"Controversiële moties (%)",
|
|
|
min_value=0,
|
|
|
max_value=100,
|
|
|
value=(
|
|
|
config.DEFAULT_WINNING_MARGIN_MIN,
|
|
|
config.DEFAULT_WINNING_MARGIN_MAX,
|
|
|
),
|
|
|
)
|
|
|
|
|
|
if st.button("Start Nieuwe Sessie"):
|
|
|
start_new_session(motion_count, policy_area, margin_range)
|
|
|
|
|
|
if st.button("Genereer AI Samenvattingen"):
|
|
|
with st.spinner("Genereren van samenvattingen..."):
|
|
|
summarizer.update_motion_summaries()
|
|
|
st.success("Samenvattingen bijgewerkt!")
|
|
|
|
|
|
# Main content
|
|
|
if not st.session_state.session_id:
|
|
|
show_welcome_screen(motion_count, policy_area, margin_range)
|
|
|
elif st.session_state.show_results:
|
|
|
show_results()
|
|
|
else:
|
|
|
show_motion_interface()
|
|
|
|
|
|
|
|
|
def start_new_session(motion_count, policy_area, margin_range):
|
|
|
"""Start a new voting session"""
|
|
|
# Get filtered motions
|
|
|
motions = db.get_filtered_motions(
|
|
|
policy_area=policy_area,
|
|
|
min_margin=margin_range[0] / 100,
|
|
|
max_margin=margin_range[1] / 100,
|
|
|
limit=motion_count,
|
|
|
)
|
|
|
|
|
|
if len(motions) < motion_count:
|
|
|
st.warning(
|
|
|
f"Slechts {len(motions)} moties gevonden met de geselecteerde criteria."
|
|
|
)
|
|
|
|
|
|
# Create session
|
|
|
session_id = db.create_session(motion_count)
|
|
|
|
|
|
# Update session state
|
|
|
st.session_state.session_id = session_id
|
|
|
st.session_state.motions = motions[:motion_count]
|
|
|
st.session_state.current_motion_index = 0
|
|
|
st.session_state.show_results = False
|
|
|
|
|
|
st.rerun()
|
|
|
|
|
|
|
|
|
def show_welcome_screen(motion_count, policy_area, margin_range):
|
|
|
"""Show welcome screen with start button"""
|
|
|
col1, col2, col3 = st.columns([1, 2, 1])
|
|
|
|
|
|
with col2:
|
|
|
st.markdown("### Welkom bij de Nederlandse Politieke Kompas!")
|
|
|
|
|
|
st.markdown(f"""
|
|
|
**Jouw instellingen:**
|
|
|
- 📊 **{motion_count} moties** uit het beleidsgebied **{policy_area}**
|
|
|
- 🎯 **Controversiële moties** tussen {margin_range[0]}% en {margin_range[1]}% marge
|
|
|
|
|
|
Klik op "Start Nieuwe Sessie" in de zijbalk om te beginnen met stemmen.
|
|
|
""")
|
|
|
|
|
|
st.info(
|
|
|
"💡 **Tip**: Kies 'Alle' als beleidsgebied voor een breed overzicht van verschillende onderwerpen."
|
|
|
)
|
|
|
|
|
|
|
|
|
def show_motion_interface():
|
|
|
"""Show motion voting interface"""
|
|
|
if not st.session_state.motions:
|
|
|
st.error("Geen moties gevonden. Start een nieuwe sessie.")
|
|
|
return
|
|
|
|
|
|
current_index = st.session_state.current_motion_index
|
|
|
total_motions = len(st.session_state.motions)
|
|
|
|
|
|
# Progress bar
|
|
|
progress = (current_index) / total_motions
|
|
|
st.progress(progress, text=f"Motie {current_index + 1} van {total_motions}")
|
|
|
|
|
|
if current_index >= total_motions:
|
|
|
st.session_state.show_results = True
|
|
|
st.rerun()
|
|
|
return
|
|
|
|
|
|
motion = st.session_state.motions[current_index]
|
|
|
|
|
|
# Motion display
|
|
|
st.header(f"Motie {current_index + 1}: {motion['title']}")
|
|
|
|
|
|
# Policy area tag
|
|
|
st.markdown(f"**Beleidsgebied:** {motion['policy_area']}")
|
|
|
|
|
|
# Layman explanation (prominent)
|
|
|
if motion.get("layman_explanation"):
|
|
|
st.markdown("### 📝 Uitleg in begrijpelijke taal:")
|
|
|
st.markdown(f"*{motion['layman_explanation']}*")
|
|
|
|
|
|
# Original description (collapsible)
|
|
|
motion_text = motion.get("body_text") or motion.get("description", "")
|
|
|
if motion_text:
|
|
|
label = (
|
|
|
"📋 Volledige motietekst"
|
|
|
if motion.get("body_text")
|
|
|
else "📋 Originele motiebeschrijving"
|
|
|
)
|
|
|
with st.expander(label):
|
|
|
st.write(motion_text)
|
|
|
|
|
|
# Voting buttons
|
|
|
st.markdown("### 🗳️ Hoe zou jij stemmen?")
|
|
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
|
|
|
|
with col1:
|
|
|
if st.button("✅ Voor", use_container_width=True, type="primary"):
|
|
|
cast_vote("Voor")
|
|
|
|
|
|
with col2:
|
|
|
if st.button("❌ Tegen", use_container_width=True):
|
|
|
cast_vote("Tegen")
|
|
|
|
|
|
with col3:
|
|
|
if st.button("🚫 Geen stem", use_container_width=True):
|
|
|
cast_vote("Geen stem")
|
|
|
|
|
|
|
|
|
def cast_vote(vote_choice):
|
|
|
"""Record user vote and move to next motion"""
|
|
|
current_motion = st.session_state.motions[st.session_state.current_motion_index]
|
|
|
|
|
|
# Save vote to database
|
|
|
db.update_user_vote(st.session_state.session_id, current_motion["id"], vote_choice)
|
|
|
|
|
|
# Move to next motion
|
|
|
st.session_state.current_motion_index += 1
|
|
|
st.rerun()
|
|
|
|
|
|
|
|
|
def show_results():
|
|
|
"""Show voting results and party matches"""
|
|
|
st.header("🎯 Jouw Resultaten")
|
|
|
|
|
|
# Calculate party matches
|
|
|
party_matches = db.calculate_party_matches(st.session_state.session_id)
|
|
|
|
|
|
if not party_matches:
|
|
|
st.error("Geen resultaten beschikbaar.")
|
|
|
return
|
|
|
|
|
|
# Party ranking table
|
|
|
st.subheader("📊 Partij Overeenkomsten (van hoog naar laag)")
|
|
|
|
|
|
df = pd.DataFrame(party_matches)
|
|
|
df.columns = ["Partij", "Overeenkomst %", "Eens", "Totaal"]
|
|
|
|
|
|
# Style the dataframe
|
|
|
def color_agreement(val):
|
|
|
if val >= 80:
|
|
|
return "background-color: #d4edda"
|
|
|
elif val >= 60:
|
|
|
return "background-color: #fff3cd"
|
|
|
else:
|
|
|
return "background-color: #f8d7da"
|
|
|
|
|
|
styled_df = df.style.applymap(color_agreement, subset=["Overeenkomst %"])
|
|
|
st.dataframe(styled_df, use_container_width=True, hide_index=True)
|
|
|
|
|
|
# Top match highlight
|
|
|
top_match = party_matches[0]
|
|
|
st.success(
|
|
|
f"🏆 **Beste match:** {top_match['party']} ({top_match['agreement_percentage']}% overeenkomst)"
|
|
|
)
|
|
|
|
|
|
# Detailed motion overview
|
|
|
st.subheader("📋 Gedetailleerd Overzicht per Motie")
|
|
|
show_detailed_motion_results()
|
|
|
|
|
|
# New session button
|
|
|
if st.button("🔄 Start Nieuwe Sessie"):
|
|
|
# Clear session state
|
|
|
for key in ["session_id", "motions", "current_motion_index", "show_results"]:
|
|
|
if key in st.session_state:
|
|
|
del st.session_state[key]
|
|
|
st.rerun()
|
|
|
|
|
|
|
|
|
def show_detailed_motion_results():
|
|
|
"""Show detailed voting results for each motion"""
|
|
|
import duckdb
|
|
|
|
|
|
conn = duckdb.connect(config.DATABASE_PATH)
|
|
|
|
|
|
# Get user votes
|
|
|
user_data = conn.execute(
|
|
|
"""
|
|
|
SELECT user_votes FROM user_sessions WHERE session_id = ?
|
|
|
""",
|
|
|
(st.session_state.session_id,),
|
|
|
).fetchone()
|
|
|
|
|
|
if not user_data:
|
|
|
return
|
|
|
|
|
|
user_votes = json.loads(user_data[0])
|
|
|
|
|
|
# Get motion details
|
|
|
motion_ids = list(user_votes.keys())
|
|
|
if motion_ids:
|
|
|
placeholders = ",".join(["?" for _ in motion_ids])
|
|
|
motions = conn.execute(
|
|
|
f"""
|
|
|
SELECT id, title, layman_explanation, body_text, description, voting_results FROM motions
|
|
|
WHERE id IN ({placeholders})
|
|
|
""",
|
|
|
motion_ids,
|
|
|
).fetchall()
|
|
|
|
|
|
for (
|
|
|
motion_id,
|
|
|
title,
|
|
|
layman_explanation,
|
|
|
body_text,
|
|
|
description,
|
|
|
voting_results_json,
|
|
|
) in motions:
|
|
|
voting_results = json.loads(voting_results_json)
|
|
|
user_vote = user_votes[str(motion_id)]
|
|
|
|
|
|
with st.expander(f"**{title}** (Jouw stem: {user_vote})"):
|
|
|
# Show layman explanation prominently
|
|
|
if layman_explanation:
|
|
|
st.markdown("**📝 Uitleg:**")
|
|
|
st.markdown(f"*{layman_explanation}*")
|
|
|
|
|
|
# Show full motion body text if available, otherwise description
|
|
|
motion_text = body_text or description
|
|
|
if motion_text:
|
|
|
st.markdown("**📋 Motiebeschrijving:**")
|
|
|
st.write(motion_text)
|
|
|
|
|
|
# Create voting overview
|
|
|
parties_voor = [p for p, v in voting_results.items() if v == "voor"]
|
|
|
parties_tegen = [p for p, v in voting_results.items() if v == "tegen"]
|
|
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
with col1:
|
|
|
st.markdown("**Voor:**")
|
|
|
st.write(", ".join(parties_voor) if parties_voor else "Geen")
|
|
|
|
|
|
with col2:
|
|
|
st.markdown("**Tegen:**")
|
|
|
st.write(", ".join(parties_tegen) if parties_tegen else "Geen")
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
main()
|
|
|
|