gwoe-antragspruefer/app/templates/v2/screens/quellen.html
Dotty Dotter 565849bd84 feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
  (ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
  Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze

Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
  /api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie

Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00

220 lines
7.7 KiB
HTML

{% extends "v2/base.html" %}
{% block title %}Quellen — GWÖ-Antragsprüfer{% endblock %}
{% set v2_active_nav = "" %}
{% block head_extra %}
<style>
.quellen-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 12px;
margin-bottom: 1.5rem;
}
.prog-card {
display: flex;
gap: 12px;
padding: 14px;
background: var(--ecg-card-bg);
border: 1px solid var(--ecg-border);
border-radius: 6px;
}
.prog-card img {
width: 64px;
height: auto;
border: 1px solid var(--ecg-border);
border-radius: 3px;
flex-shrink: 0;
object-fit: cover;
}
.prog-meta { font-size: 11px; opacity: 0.6; margin-top: 2px; margin-bottom: 6px; }
.prog-badge {
display: inline-block;
padding: 1px 5px;
border-radius: 3px;
font-size: 10px;
font-weight: 700;
margin-right: 4px;
font-family: var(--font-mono);
}
.badge-spd { background: #e3000f; color: #fff; }
.badge-cdu { background: #2c2c2c; color: #fff; }
.badge-gruene,.badge-grüne { background: #46962b; color: #fff; }
.badge-fdp { background: #ffed00; color: #222; }
.badge-afd { background: #009ee0; color: #fff; }
.badge-linke { background: #be3075; color: #fff; }
.badge-type-wp { background: var(--ecg-teal); color: #fff; }
.badge-type-pp { background: var(--ecg-green); color: #fff; }
.indexed-ok { color: var(--ecg-green); font-size: 11px; font-family: var(--font-mono); }
.indexed-no { color: var(--ecg-dark); opacity: 0.4; font-size: 11px; font-family: var(--font-mono); }
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
margin: 10px 0;
}
.stat-cell {
text-align: center;
padding: 12px;
background: var(--ecg-bg-subtle);
border-radius: 4px;
}
.stat-val { font-family: var(--font-display); font-size: 28px; color: var(--ecg-teal); font-weight: 900; }
.stat-lbl { font-size: 11px; opacity: 0.6; margin-top: 2px; }
.section-h2 {
font-family: var(--font-display);
font-size: 16px;
color: var(--ecg-teal);
margin: 1.5rem 0 0.75rem;
padding-bottom: 4px;
border-bottom: 2px solid var(--ecg-teal);
}
</style>
{% endblock %}
{% block main %}
<div style="padding:0 0 2rem;">
<h1 style="font-family:var(--font-display);font-size:22px;color:var(--ecg-teal);margin:0 0 4px;">Quellen &amp; Referenzdokumente</h1>
<p style="font-size:12px;font-family:var(--font-mono);color:var(--ecg-dark);opacity:0.6;">
Wahl- und Grundsatzprogramme · semantisch indexiert
</p>
</div>
<div class="v2-kasten outline-blue" style="margin-bottom:1.5rem;">
<p>
Der GWÖ-Antragsprüfer vergleicht parlamentarische Anträge mit den Wahl- und Grundsatzprogrammen
der Parteien. Hier finden Sie alle verwendeten Originaldokumente zum Download.
</p>
<p style="font-size:11px;opacity:0.7;margin-top:6px;">
Die Programme werden semantisch indexiert, um relevante Passagen für jeden Antrag zu finden.
</p>
</div>
<!-- Indexierungsstatus -->
<div class="v2-kasten outline-green" style="margin-bottom:1.5rem;">
<h4>Indexierungsstatus</h4>
<div class="stat-grid">
<div class="stat-cell">
<div class="stat-val">{{ status.indexed }}</div>
<div class="stat-lbl">Indexiert</div>
</div>
<div class="stat-cell">
<div class="stat-val">{{ status.total }}</div>
<div class="stat-lbl">Gesamt</div>
</div>
</div>
<div style="margin-top:8px;">
<button onclick="indexAll()"
style="font-family:var(--font-mono);font-size:11px;padding:5px 12px;background:var(--ecg-teal);color:#fff;border:none;border-radius:3px;cursor:pointer;">
Alle Programme indexieren (Admin)
</button>
<span id="index-status" style="margin-left:10px;font-size:11px;opacity:0.7;"></span>
</div>
</div>
<!-- Wahlprogramme nach BL -->
{% for bl_name, bl_progs in wahlprogramme_grouped %}
<h2 class="section-h2">{{ bl_name }}</h2>
<div class="quellen-grid">
{% for prog in bl_progs %}
<div class="prog-card">
<a href="{{ prog.pdf_url }}" target="_blank" style="flex-shrink:0;">
<img src="/api/programme/thumbnail/{{ prog.id }}" alt="{{ prog.name }}"
loading="lazy" onerror="this.style.display='none'">
</a>
<div style="min-width:0;">
<div style="font-family:var(--font-display);font-size:13px;margin-bottom:3px;">
<span class="prog-badge badge-{{ prog.partei|lower|replace('ü','ue')|replace('ä','ae')|replace('ö','oe') }}">{{ prog.partei }}</span>
{{ prog.name }}
</div>
<div class="prog-meta">
<span class="prog-badge badge-type-wp">Wahlprogramm</span>
{% if prog.bundesland %}{{ prog.bundesland }}{% endif %}
</div>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<a href="{{ prog.pdf_url }}" target="_blank"
style="font-size:11px;font-family:var(--font-mono);color:var(--ecg-teal);">
PDF herunterladen
</a>
{% for s in status.programmes if s.id == prog.id %}
{% if s.indexed %}
<span class="indexed-ok">✓ {{ s.chunks }} Chunks</span>
{% else %}
<span class="indexed-no">○ Nicht indexiert</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
<!-- Grundsatzprogramme -->
<h2 class="section-h2">Grundsatzprogramme (Bundesebene)</h2>
<div class="quellen-grid">
{% for prog in grundsatzprogramme %}
<div class="prog-card">
<a href="{{ prog.pdf_url }}" target="_blank" style="flex-shrink:0;">
<img src="/api/programme/thumbnail/{{ prog.id }}" alt="{{ prog.name }}"
loading="lazy" onerror="this.style.display='none'">
</a>
<div style="min-width:0;">
<div style="font-family:var(--font-display);font-size:13px;margin-bottom:3px;">
<span class="prog-badge badge-{{ prog.partei|lower|replace('ü','ue')|replace('ä','ae')|replace('ö','oe') }}">{{ prog.partei }}</span>
{{ prog.name }}
</div>
<div class="prog-meta">
<span class="prog-badge badge-type-pp">Grundsatzprogramm</span>
</div>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<a href="{{ prog.pdf_url }}" target="_blank"
style="font-size:11px;font-family:var(--font-mono);color:var(--ecg-teal);">
PDF herunterladen
</a>
{% for s in status.programmes if s.id == prog.id %}
{% if s.indexed %}
<span class="indexed-ok">✓ {{ s.chunks }} Chunks</span>
{% else %}
<span class="indexed-no">○ Nicht indexiert</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="v2-kasten outline-green" style="margin-top:1rem;">
<h4>Hinweise zur Methodik</h4>
<ul style="margin:6px 0 0 1.2rem;font-size:12px;">
<li>Die Programme werden in semantische Chunks aufgeteilt (~400 Wörter)</li>
<li>Jeder Chunk wird mit einem Embedding-Modell vektorisiert</li>
<li>Bei der Analyse wird der Antrag ebenfalls vektorisiert</li>
<li>Die ähnlichsten Passagen werden als Kontext an das LLM übergeben</li>
<li>Das LLM zitiert nur, wenn eine Passage wirklich zur Argumentation passt</li>
</ul>
</div>
{% endblock %}
{% block body_scripts %}
<script>
async function indexAll() {
const statusEl = document.getElementById('index-status');
statusEl.textContent = 'Indexierung gestartet …';
try {
const fd = new FormData();
fd.append('all_programmes', 'true');
const resp = await fetch('/api/programme/index', { method: 'POST', body: fd });
const data = await resp.json();
statusEl.textContent = 'Indexierung läuft für ' + data.programmes.length + ' Programme';
setTimeout(() => location.reload(), 30000);
} catch (e) {
statusEl.textContent = 'Fehler: ' + e.message;
}
}
</script>
{% endblock %}