feat(v3): Themen, Kernpunkte, Schwerpunkt, Konfidenz, fehlende Programme, Wahlperiode + Ähnliche Anträge

DB-Felder die bisher in der UI fehlten, jetzt in v3 sichtbar:

- _row_to_detail() liefert themen, kernpunkte, schwerpunkt, link,
  konfidenz, fehlende_programme, wahlperiode an's Frontend.
- _wahlperiode_silent() leitet die WP aus datum+bundesland ab via
  wahlperioden.wahlperiode_for() — silent-fail bei Lookup-Fehler.

v3-Template:
- Wahlperiode in der Antrag-ID-Zeile ("18. Wahlperiode")
- Themen-Chips als Reihe unter byline
- Kernforderungen als Bullet-Liste in der Zusammenfassungs-Sektion
- Konfidenz-Pille (hoch/mittel/niedrig) neben der Empfehlung
- Schwerpunkt-Felder (Top-Matrix-Cells) als Chips über der Matrix
- Disclaimer "fehlende Programme" am Programm-Treue-Block
- Original-PDF-Link im Aktions-Block
- Ähnliche Anträge als eigener Block, geladen via JS aus
  /api/assessment/similar (Re-Use des bestehenden Endpoints aus #108)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-07 11:01:18 +02:00
parent 4de2df9cea
commit f471586f33
3 changed files with 240 additions and 5 deletions

View File

@ -623,11 +623,31 @@ def _row_to_detail(row):
"share_threads": row.get("share_threads"), "share_threads": row.get("share_threads"),
"share_twitter": row.get("share_twitter"), "share_twitter": row.get("share_twitter"),
"share_mastodon": row.get("share_mastodon"), "share_mastodon": row.get("share_mastodon"),
# Zusatzfelder fuer v3 (Bürger:innen-Modus): Themen-Tags,
# Kernforderungen, Top-Matrix-Felder, Original-PDF-Link,
# Bewertungs-Konfidenz, fehlende Programme, Wahlperiode.
"themen": row.get("themen") or [],
"kernpunkte": row.get("antrag_kernpunkte") or [],
"schwerpunkt": row.get("gwoe_schwerpunkt") or [],
"link": row.get("link") or "",
"konfidenz": row.get("konfidenz") or "",
"fehlende_programme": row.get("fehlende_programme") or [],
"wahlperiode": _wahlperiode_silent(row.get("datum", ""), row.get("bundesland", "")),
# Roher ISO-Zeitstempel für OG-Cache-Key (#141) # Roher ISO-Zeitstempel für OG-Cache-Key (#141)
"updated_at_raw": row.get("updated_at", ""), "updated_at_raw": row.get("updated_at", ""),
} }
def _wahlperiode_silent(datum: str, bundesland: str) -> str:
"""Liefert die Wahlperiode zum Datum/Bundesland, oder leerer String wenn
das Lookup fehlschlaegt UI-only, soll keinen Render-Fail ausloesen."""
try:
from .wahlperioden import wahlperiode_for
return wahlperiode_for(datum, bundesland) or ""
except Exception:
return ""
@app.post("/analyze") @app.post("/analyze")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def start_analysis( async def start_analysis(

View File

@ -637,3 +637,140 @@
.v3-empfehlung { font-size: 18px; } .v3-empfehlung { font-size: 18px; }
.v3-userrow { flex-direction: column; align-items: flex-start; } .v3-userrow { flex-direction: column; align-items: flex-start; }
} }
/* ── Themen-Chips unter byline ───────────────────────────────────── */
.v3-themen {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 10px;
}
.v3-theme-chip {
font-family: var(--font-mono);
font-size: 11px;
padding: 2px 8px;
border: 1px solid var(--hairline);
border-radius: 2px;
background: var(--paper);
color: var(--ecg-dark);
}
/* ── Kernpunkte-Liste in Zusammenfassung ─────────────────────────── */
.v3-kernpunkte {
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed var(--hairline);
}
.v3-kernpunkte-label {
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--ecg-dark);
opacity: 0.6;
margin-bottom: 6px;
}
.v3-kernpunkte-list {
margin: 0;
padding-left: 18px;
}
.v3-kernpunkte-list li {
font-size: 13.5px;
line-height: 1.55;
margin-bottom: 4px;
}
/* ── Konfidenz-Pille bei Empfehlung ──────────────────────────────── */
.v3-empfehlung-wrap {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1 1 auto;
min-width: 0;
}
.v3-konfidenz {
display: inline-block;
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 1px 6px;
border-radius: 2px;
align-self: flex-start;
}
.v3-konfidenz-hoch { background: rgba(45, 164, 78, 0.12); color: #1a7f37; }
.v3-konfidenz-mittel { background: rgba(110, 119, 129, 0.12); color: #57606a; }
.v3-konfidenz-niedrig { background: rgba(207, 34, 46, 0.10); color: #a40e26; }
/* ── Schwerpunkt-Felder vor Matrix ───────────────────────────────── */
.v3-schwerpunkt {
margin-bottom: 10px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--ecg-dark);
display: flex;
align-items: baseline;
gap: 6px;
flex-wrap: wrap;
}
.v3-schwerpunkt-label {
text-transform: uppercase;
letter-spacing: 0.07em;
opacity: 0.6;
}
.v3-schwerpunkt-chip {
padding: 1px 7px;
background: var(--ecg-blue);
color: #fff;
border-radius: 2px;
font-weight: 700;
}
/* ── Disclaimer (fehlende Programme) ─────────────────────────────── */
.v3-disclaimer {
margin-bottom: 12px;
padding: 8px 12px;
background: rgba(247, 148, 29, 0.08);
border-left: 3px solid #bf6c10;
border-radius: 2px;
font-size: 12.5px;
line-height: 1.55;
color: var(--ecg-dark);
}
/* ── Ähnliche Anträge ────────────────────────────────────────────── */
.v3-similar-item {
display: block;
padding: 10px 12px;
margin-bottom: 6px;
border: 1px solid var(--hairline);
border-radius: 3px;
background: var(--paper);
color: var(--ecg-dark);
text-decoration: none;
}
.v3-similar-item:hover {
border-color: var(--ecg-blue);
background: var(--surface);
}
.v3-similar-meta {
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ecg-dark);
opacity: 0.65;
margin-bottom: 3px;
}
.v3-similar-title {
font-size: 13.5px;
line-height: 1.4;
font-weight: 600;
color: var(--ecg-dark);
margin-bottom: 3px;
}
.v3-similar-score {
font-family: var(--font-mono);
font-size: 11px;
color: var(--ecg-blue);
}

View File

@ -53,6 +53,7 @@
{{ antrag.bundesland | default("") }} {{ antrag.bundesland | default("") }}
{% if antrag.drucksache %} · Drs. {{ antrag.drucksache }}{% endif %} {% if antrag.drucksache %} · Drs. {{ antrag.drucksache }}{% endif %}
{% if antrag.typ %} · {{ antrag.typ }}{% endif %} {% if antrag.typ %} · {{ antrag.typ }}{% endif %}
{% if antrag.wahlperiode %} · {{ antrag.wahlperiode }}. Wahlperiode{% endif %}
{% if antrag.datum %} · eingebracht {{ antrag.datum }}{% endif %} {% if antrag.datum %} · eingebracht {{ antrag.datum }}{% endif %}
</div> </div>
<h1 class="v3-title">{{ antrag.title | default("Antrag") }}</h1> <h1 class="v3-title">{{ antrag.title | default("Antrag") }}</h1>
@ -64,13 +65,28 @@
{% if antrag.zitate_count %} · {{ antrag.zitate_count }} Zitat{{ "e" if antrag.zitate_count != 1 else "" }} verifiziert{% endif %} {% if antrag.zitate_count %} · {{ antrag.zitate_count }} Zitat{{ "e" if antrag.zitate_count != 1 else "" }} verifiziert{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if antrag.themen %}
<div class="v3-themen">
{% for t in antrag.themen %}<span class="v3-theme-chip">{{ t }}</span>{% endfor %}
</div>
{% endif %}
</section> </section>
{# 2 ── Zusammenfassung ───────────────────────────────────────────── #} {# 2 ── Zusammenfassung + Kernpunkte ─────────────────────────────── #}
{% if antrag.zusammenfassung %} {% if antrag.zusammenfassung or antrag.kernpunkte %}
<section class="v3-section"> <section class="v3-section">
<h3 class="v3-h3">Zusammenfassung</h3> <h3 class="v3-h3">Zusammenfassung</h3>
{% if antrag.zusammenfassung %}
<p class="v3-prose">{{ antrag.zusammenfassung }}</p> <p class="v3-prose">{{ antrag.zusammenfassung }}</p>
{% endif %}
{% if antrag.kernpunkte %}
<div class="v3-kernpunkte">
<div class="v3-kernpunkte-label">Kernforderungen</div>
<ul class="v3-kernpunkte-list">
{% for kp in antrag.kernpunkte %}<li>{{ kp }}</li>{% endfor %}
</ul>
</div>
{% endif %}
</section> </section>
{% endif %} {% endif %}
@ -82,10 +98,18 @@
{{ "%.1f"|format(s) }}<span class="v3-score-slash">/10</span> {{ "%.1f"|format(s) }}<span class="v3-score-slash">/10</span>
</div> </div>
{% if antrag.verdict_title %} {% if antrag.verdict_title %}
<div class="v3-empfehlung-wrap">
<div class="v3-empfehlung <div class="v3-empfehlung
{% if s >= 7 %}good{% elif s >= 4 %}mid{% else %}low{% endif %}"> {% if s >= 7 %}good{% elif s >= 4 %}mid{% else %}low{% endif %}">
{{ antrag.verdict_title }} {{ antrag.verdict_title }}
</div> </div>
{% if antrag.konfidenz %}
<span class="v3-konfidenz v3-konfidenz-{{ antrag.konfidenz | lower }}"
title="Bewertungs-Konfidenz: wie sicher ist das LLM in dieser Einschätzung?">
Konfidenz: {{ antrag.konfidenz }}
</span>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
{% if antrag.verdict_body %} {% if antrag.verdict_body %}
@ -116,6 +140,12 @@
{% if antrag.matrix %} {% if antrag.matrix %}
<section class="v3-section"> <section class="v3-section">
<h3 class="v3-h3">Matrix 2.0 · 25 Felder</h3> <h3 class="v3-h3">Matrix 2.0 · 25 Felder</h3>
{% if antrag.schwerpunkt %}
<div class="v3-schwerpunkt">
<span class="v3-schwerpunkt-label">Schwerpunkt-Felder:</span>
{% for f in antrag.schwerpunkt %}<span class="v3-schwerpunkt-chip">{{ f }}</span>{% endfor %}
</div>
{% endif %}
{{ matrix_mini(antrag.matrix) }} {{ matrix_mini(antrag.matrix) }}
</section> </section>
{% endif %} {% endif %}
@ -124,6 +154,12 @@
{% if antrag.fraktions_scores %} {% if antrag.fraktions_scores %}
<section class="v3-section"> <section class="v3-section">
<h3 class="v3-h3">Programm-Treue pro Fraktion</h3> <h3 class="v3-h3">Programm-Treue pro Fraktion</h3>
{% if antrag.fehlende_programme %}
<div class="v3-disclaimer">
<strong>Hinweis:</strong> Für folgende Parteien lag kein Wahl-/Parteiprogramm vor — keine Treue-Bewertung möglich:
{{ antrag.fehlende_programme | join(", ") }}.
</div>
{% endif %}
<div class="v3-fraktionen"> <div class="v3-fraktionen">
{% for fs in antrag.fraktions_scores %} {% for fs in antrag.fraktions_scores %}
<div class="v3-fraktion"> <div class="v3-fraktion">
@ -276,10 +312,21 @@
{# 9a Aktions-Links #} {# 9a Aktions-Links #}
<div class="v3-rest-aktions"> <div class="v3-rest-aktions">
<a href="/api/assessment/pdf?drucksache={{ antrag.drucksache | urlencode }}">PDF-Bericht</a> <a href="/api/assessment/pdf?drucksache={{ antrag.drucksache | urlencode }}">PDF-Bericht</a>
{% if antrag.link %}
<a href="{{ antrag.link }}" target="_blank" rel="noopener">Original-Antrag (Landtag)</a>
{% endif %}
<a href="/api/assessment?drucksache={{ antrag.drucksache | urlencode }}">JSON-Export</a> <a href="/api/assessment?drucksache={{ antrag.drucksache | urlencode }}">JSON-Export</a>
<a href="/antrag/{{ antrag.drucksache }}">Permalink</a> <a href="/antrag/{{ antrag.drucksache }}">Permalink</a>
</div> </div>
{# 9a' Ähnliche Anträge — per JS via /api/assessment/similar geladen #}
<div class="v3-rest-block v3-similar" id="v3-similar-box" data-drucksache="{{ antrag.drucksache | e }}">
<h3 class="v3-h3">Ähnliche Anträge</h3>
<div id="v3-similar-list">
<span class="v3-loading">Lade …</span>
</div>
</div>
{# 9b Teilen #} {# 9b Teilen #}
<div class="v3-rest-block"> <div class="v3-rest-block">
<h3 class="v3-h3">Teilen</h3> <h3 class="v3-h3">Teilen</h3>
@ -453,5 +500,36 @@
bar.appendChild(pill); bar.appendChild(pill);
bar.appendChild(toggle); bar.appendChild(toggle);
})(); })();
/* Ähnliche Anträge: /api/assessment/similar?drucksache=…&top_k=5 */
(async function () {
var box = document.getElementById('v3-similar-box');
if (!box) return;
var drs = box.dataset.drucksache;
var list = document.getElementById('v3-similar-list');
try {
var resp = await fetch('/api/assessment/similar?drucksache=' + encodeURIComponent(drs) + '&top_k=5');
var data = await resp.json();
var items = (data && data.results) || data || [];
if (!Array.isArray(items) || items.length === 0) {
list.innerHTML = '<span class="v3-loading">Keine ähnlichen Anträge gefunden.</span>';
return;
}
list.innerHTML = items.map(function (it) {
var sim = (it.similarity != null) ? Math.round(it.similarity * 100) + '%' : '';
var bl = it.bundesland || '';
var drs = it.drucksache || '';
var title = it.title || '';
var score = (it.gwoe_score != null) ? it.gwoe_score.toFixed(1) : '';
return '<a href="/v3/antrag/' + encodeURIComponent(drs) + '" class="v3-similar-item">'
+ '<div class="v3-similar-meta">' + bl + (drs ? ' · Drs. ' + drs : '') + (sim ? ' · ' + sim + ' ähnlich' : '') + '</div>'
+ '<div class="v3-similar-title">' + title + '</div>'
+ (score ? '<div class="v3-similar-score">Score ' + score + '/10</div>' : '')
+ '</a>';
}).join('');
} catch (e) {
list.innerHTML = '<span class="v3-loading">Konnte ähnliche Anträge nicht laden.</span>';
}
})();
</script> </script>
{% endblock %} {% endblock %}