310 lines
10 KiB
HTML
310 lines
10 KiB
HTML
|
|
<!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>
|
||
|
|
|
||
|
|
<h2>Wahlprogramme NRW 2022</h2>
|
||
|
|
<div class="programme-grid">
|
||
|
|
{% for prog in programmes if prog.typ == 'wahlprogramm' %}
|
||
|
|
<div class="programme-card">
|
||
|
|
<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: 1rem;">
|
||
|
|
<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>
|
||
|
|
{% endfor %}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h2>Grundsatzprogramme</h2>
|
||
|
|
<div class="programme-grid">
|
||
|
|
{% for prog in programmes if prog.typ == 'parteiprogramm' %}
|
||
|
|
<div class="programme-card">
|
||
|
|
<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: 1rem;">
|
||
|
|
<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>
|
||
|
|
{% 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>
|