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:
parent
4de2df9cea
commit
f471586f33
20
app/main.py
20
app/main.py
@ -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(
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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,9 +98,17 @@
|
|||||||
{{ "%.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
|
<div class="v3-empfehlung-wrap">
|
||||||
{% if s >= 7 %}good{% elif s >= 4 %}mid{% else %}low{% endif %}">
|
<div class="v3-empfehlung
|
||||||
{{ antrag.verdict_title }}
|
{% if s >= 7 %}good{% elif s >= 4 %}mid{% else %}low{% endif %}">
|
||||||
|
{{ antrag.verdict_title }}
|
||||||
|
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -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 %}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user