gwoe-antragspruefer/app/templates/quellen.html

326 lines
11 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quellen — {{ app_name }}</title>
<style>
:root {
--color-darkgray: #5a5a5a;
--color-green: #889e33;
--color-blue: #009da5;
--color-lightgray: #bfbfbf;
--color-bg: #f5f5f5;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Avenir', 'Segoe UI', sans-serif;
color: var(--color-darkgray);
line-height: 1.6;
background: var(--color-bg);
}
.header {
background: white;
padding: 1rem 2rem;
border-bottom: 1px solid var(--color-lightgray);
display: flex;
align-items: center;
gap: 1rem;
}
.header h1 {
color: var(--color-blue);
font-size: 1.5rem;
}
.header a {
color: var(--color-blue);
text-decoration: none;
}
.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
}
h2 {
color: var(--color-blue);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--color-blue);
}
.intro {
background: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.programme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.programme-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.programme-card h3 {
color: var(--color-darkgray);
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.programme-meta {
color: #888;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.programme-badge {
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: bold;
margin-right: 0.5rem;
}
.badge-spd { background: #e3000f; color: white; }
.badge-cdu { background: #000000; color: white; }
.badge-gruene { background: #46962b; color: white; }
.badge-fdp { background: #ffed00; color: black; }
.badge-afd { background: #009ee0; color: white; }
.badge-wahlprogramm { background: var(--color-blue); color: white; }
.badge-parteiprogramm { background: var(--color-green); color: white; }
.btn {
display: inline-block;
padding: 0.5rem 1rem;
border-radius: 4px;
text-decoration: none;
font-size: 0.9rem;
cursor: pointer;
border: none;
}
.btn-primary {
background: var(--color-blue);
color: white;
}
.btn-primary:hover {
background: #007b82;
}
.btn-secondary {
background: var(--color-lightgray);
color: var(--color-darkgray);
}
.status-box {
background: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.status-item {
text-align: center;
padding: 1rem;
background: var(--color-bg);
border-radius: 4px;
}
.status-value {
font-size: 2rem;
font-weight: bold;
color: var(--color-blue);
}
.status-label {
font-size: 0.85rem;
color: #888;
}
.indexed { color: var(--color-green); }
.not-indexed { color: #888; }
.back-link {
display: inline-block;
margin-bottom: 1rem;
color: var(--color-blue);
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<header class="header">
<h1><a href="/">{{ app_name }}</a></h1>
<span>→ Quellen</span>
</header>
<div class="container">
<a href="/" class="back-link">← Zurück zur Übersicht</a>
<div class="intro">
<h2>Quellen & Referenzdokumente</h2>
<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="margin-top: 0.5rem; color: #888;">
Die Programme werden semantisch indexiert, um relevante Passagen für jeden Antrag zu finden.
</p>
</div>
<div class="status-box">
<h3>Indexierungsstatus</h3>
<div class="status-grid">
<div class="status-item">
<div class="status-value">{{ status.indexed }}</div>
<div class="status-label">Indexiert</div>
</div>
<div class="status-item">
<div class="status-value">{{ status.total }}</div>
<div class="status-label">Gesamt</div>
</div>
</div>
<div style="margin-top: 1rem;">
<button class="btn btn-primary" onclick="indexAll()">
🔄 Alle Programme indexieren
</button>
<span id="index-status" style="margin-left: 1rem; color: #888;"></span>
</div>
</div>
{% for bl_name, bl_progs in wahlprogramme_grouped %}
<h2>{{ bl_name }}</h2>
<div class="programme-grid">
{% for prog in bl_progs %}
<div class="programme-card" style="display: flex; gap: 1rem;">
<a href="{{ prog.pdf_url }}" target="_blank" style="flex-shrink: 0;">
<img src="/api/programme/thumbnail/{{ prog.id }}" alt="{{ prog.name }}"
style="width: 80px; border: 1px solid #ddd; border-radius: 4px;"
loading="lazy" onerror="this.style.display='none'">
</a>
<div>
<h3>
<span class="programme-badge badge-{{ prog.partei|lower }}">{{ prog.partei }}</span>
{{ prog.name }}
</h3>
<div class="programme-meta">
<span class="programme-badge badge-wahlprogramm">Wahlprogramm</span>
{% if prog.bundesland %}{{ prog.bundesland }}{% endif %}
</div>
<div style="margin-top: 0.5rem;">
<a href="{{ prog.pdf_url }}" target="_blank" class="btn btn-primary">
📄 PDF herunterladen
</a>
{% for s in status.programmes if s.id == prog.id %}
<span style="margin-left: 0.5rem; font-size: 0.85rem;" class="{% if s.indexed %}indexed{% else %}not-indexed{% endif %}">
{% if s.indexed %}✓ {{ s.chunks }} Chunks{% else %}○ Nicht indexiert{% endif %}
</span>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
<h2>Grundsatzprogramme (Bundesebene)</h2>
<div class="programme-grid">
{% for prog in grundsatzprogramme %}
<div class="programme-card" style="display: flex; gap: 1rem;">
<a href="{{ prog.pdf_url }}" target="_blank" style="flex-shrink: 0;">
<img src="/api/programme/thumbnail/{{ prog.id }}" alt="{{ prog.name }}"
style="width: 80px; border: 1px solid #ddd; border-radius: 4px;"
loading="lazy" onerror="this.style.display='none'">
</a>
<div>
<h3>
<span class="programme-badge badge-{{ prog.partei|lower }}">{{ prog.partei }}</span>
{{ prog.name }}
</h3>
<div class="programme-meta">
<span class="programme-badge badge-parteiprogramm">Grundsatzprogramm</span>
</div>
<div style="margin-top: 0.5rem;">
<a href="{{ prog.pdf_url }}" target="_blank" class="btn btn-primary">
📄 PDF herunterladen
</a>
{% for s in status.programmes if s.id == prog.id %}
<span style="margin-left: 0.5rem; font-size: 0.85rem;" class="{% if s.indexed %}indexed{% else %}not-indexed{% endif %}">
{% if s.indexed %}✓ {{ s.chunks }} Chunks{% else %}○ Nicht indexiert{% endif %}
</span>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="intro" style="margin-top: 2rem;">
<h3>Hinweise zur Methodik</h3>
<ul style="margin-left: 1.5rem; margin-top: 0.5rem;">
<li>Die Programme werden in semantische Chunks aufgeteilt (~400 Wörter)</li>
<li>Jeder Chunk wird mit einem Embedding-Modell (Qwen) 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>
</div>
<script>
async function indexAll() {
const statusEl = document.getElementById('index-status');
statusEl.textContent = '⏳ Indexierung gestartet...';
try {
const formData = new FormData();
formData.append('all_programmes', 'true');
const resp = await fetch('/api/programme/index', {
method: 'POST',
body: formData
});
const data = await resp.json();
statusEl.textContent = `✓ Indexierung läuft für ${data.programmes.length} Programme`;
// Reload after a delay
setTimeout(() => location.reload(), 30000);
} catch (e) {
statusEl.textContent = `✗ Fehler: ${e.message}`;
}
}
</script>
</body>
</html>