Queue (queue.py):
- QUEUE_CONCURRENCY ENV (default 3) statt hartcodiert 1
- N Worker-Coroutines via asyncio tasks (nicht Semaphore — jeder
Worker pickt eigenständig von der Queue)
- Per-Job-Tracking: job_id → {status, drucksache, duration, error}
- get_queue_status() liefert jobs-Array für UI-Tabelle
Visualisierung (index.html):
- Fortschrittsbalken (X/Y fertig, grün)
- Job-Tabelle: Drucksache + Status-Icon + Dauer
- Fertige Jobs klickbar → Detail-Ansicht
- Auto-Refresh alle 3s
Admin-Schutz (auth.py + main.py):
- Neue require_admin Dependency: prüft Keycloak-Rolle "admin" oder
"gwoe-admin". Im Dev-Modus durchlassen.
- Batch-Analyse, Programme-Index, Assessment-Delete: require_admin
- Einzelanalyse, Bookmarks, Kommentare: bleiben require_auth
- Keycloak: Rolle "admin" erstellt + User tobias zugewiesen
Tests: 206 passed.
Refs: #99
2234 lines
98 KiB
HTML
2234 lines
98 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{{ 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);
|
||
min-height: 100vh;
|
||
min-height: 100dvh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Header */
|
||
.header {
|
||
background: white;
|
||
padding: 1rem 2rem;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.header h1 {
|
||
color: var(--color-blue);
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.header .subtitle {
|
||
color: var(--color-darkgray);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.bundesland-select {
|
||
padding: 0.5rem 1rem;
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
background: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.bundesland-select:focus {
|
||
outline: none;
|
||
border-color: var(--color-blue);
|
||
}
|
||
|
||
/* Main Layout */
|
||
.main-container {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* Left Panel - List */
|
||
.list-panel {
|
||
width: 400px;
|
||
min-width: 300px;
|
||
background: white;
|
||
border-right: 1px solid var(--color-lightgray);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.list-header {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
}
|
||
|
||
.search-row {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.search-box {
|
||
flex: 1;
|
||
padding: 0.75rem;
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.btn-landtag {
|
||
padding: 0.5rem 0.75rem;
|
||
background: var(--color-green);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn-landtag:hover {
|
||
background: #728a2b;
|
||
}
|
||
|
||
.btn-landtag:disabled {
|
||
background: #ccc;
|
||
cursor: wait;
|
||
}
|
||
|
||
.list-filters {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-top: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-btn {
|
||
padding: 0.25rem 0.75rem;
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 20px;
|
||
background: white;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.filter-btn.active {
|
||
background: var(--color-blue);
|
||
color: white;
|
||
border-color: var(--color-blue);
|
||
}
|
||
|
||
.list-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.list-item {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.list-item:hover {
|
||
background: var(--color-bg);
|
||
}
|
||
|
||
.list-item.active {
|
||
background: #e8f4f5;
|
||
border-left: 3px solid var(--color-blue);
|
||
}
|
||
|
||
.list-item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.list-item-id {
|
||
font-weight: bold;
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
/* Bundesland-Badge: Im Listen-Item links neben der Drucksachen-Nummer.
|
||
Im Bundesland-spezifischen Modus per data-mode="single" am Container
|
||
ausgeblendet (redundant, da alle Einträge demselben Land zugehören). */
|
||
.bl-badge {
|
||
display: inline-block;
|
||
padding: 1px 6px;
|
||
margin-right: 0.4rem;
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
color: var(--color-blue);
|
||
border: 1px solid var(--color-blue);
|
||
border-radius: 3px;
|
||
vertical-align: middle;
|
||
}
|
||
.list-content[data-mode="single"] .bl-badge { display: none; }
|
||
|
||
/* Detail-Header: Parlament-Name unter dem Titel, vor der Drucksache-Zeile */
|
||
.detail-parlament {
|
||
font-size: 0.85rem;
|
||
color: var(--color-blue);
|
||
font-weight: 600;
|
||
margin: 0.2rem 0 0.1rem;
|
||
}
|
||
|
||
.list-item-score {
|
||
font-size: 0.9rem;
|
||
font-weight: bold;
|
||
padding: 0.1rem 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* Neue Skala -5 bis +5 */
|
||
.score-high { background: #155724; color: white; } /* +4 bis +5: dunkelgrün */
|
||
.score-mid { background: #889e33; color: white; } /* +2 bis +3: GWÖ-grün */
|
||
.score-neutral { background: #6c757d; color: white; } /* 0 bis +1: grau */
|
||
.score-low { background: #fd7e14; color: white; } /* -1 bis -2: orange */
|
||
.score-negative { background: #dc3545; color: white; } /* -3 bis -5: rot */
|
||
.score-none { background: #e2e3e5; color: #383d41; }
|
||
|
||
.status-unchecked {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
font-size: 0.75rem;
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.status-checked {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
font-size: 0.75rem;
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.btn-check-now {
|
||
display: inline-block;
|
||
padding: 0.3rem 0.75rem;
|
||
background: var(--color-blue);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.btn-check-now:hover {
|
||
background: #007b82;
|
||
}
|
||
|
||
.btn-check-now:disabled {
|
||
background: #ccc;
|
||
cursor: wait;
|
||
}
|
||
|
||
.list-item-title {
|
||
font-size: 0.9rem;
|
||
color: var(--color-darkgray);
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.list-item-meta {
|
||
font-size: 0.8rem;
|
||
color: #888;
|
||
}
|
||
|
||
.list-item-tags {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
margin-top: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tag {
|
||
font-size: 0.7rem;
|
||
padding: 0.1rem 0.4rem;
|
||
background: var(--color-bg);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Right Panel - Detail */
|
||
.detail-panel {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.detail-placeholder {
|
||
text-align: center;
|
||
color: #888;
|
||
padding: 4rem;
|
||
}
|
||
|
||
.detail-card {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 2rem;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.detail-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.detail-title {
|
||
font-size: 1.3rem;
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
.detail-id {
|
||
font-size: 0.9rem;
|
||
color: #888;
|
||
}
|
||
|
||
.score-display {
|
||
text-align: center;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.score-big {
|
||
font-size: 3rem;
|
||
font-weight: bold;
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
.score-label {
|
||
font-size: 0.9rem;
|
||
color: #888;
|
||
}
|
||
|
||
.matrix-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 0.5rem;
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
.matrix-item {
|
||
padding: 0.75rem;
|
||
background: var(--color-bg);
|
||
border-radius: 4px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.matrix-label {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.matrix-rating {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.rating-pos { color: var(--color-green); }
|
||
.rating-neg { color: #dc3545; }
|
||
.rating-neutral { color: #888; }
|
||
|
||
/* Matrix Table (5x5 Grid wie im PDF) */
|
||
.matrix-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 0.5rem 0;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.matrix-table th, .matrix-table td {
|
||
border: 1px solid var(--color-lightgray);
|
||
padding: 0.4rem 0.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.matrix-table thead th {
|
||
background: var(--color-blue);
|
||
color: white;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.matrix-table tbody th {
|
||
background: var(--color-bg);
|
||
text-align: left;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.matrix-table .positive {
|
||
background: var(--color-green);
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.matrix-table .negative {
|
||
background: #dc3545;
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.matrix-table .neutral {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
/* Themen Tags in Detail */
|
||
.themen-tags .tag {
|
||
display: inline-block;
|
||
margin-right: 0.5rem;
|
||
margin-bottom: 0.25rem;
|
||
padding: 0.25rem 0.5rem;
|
||
background: var(--color-blue);
|
||
color: white;
|
||
border-radius: 3px;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
/* Kernpunkte Liste */
|
||
.kernpunkte-list {
|
||
margin-left: 1.5rem;
|
||
}
|
||
|
||
.kernpunkte-list li {
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 1.1rem;
|
||
color: var(--color-darkgray);
|
||
margin: 1.5rem 0 0.75rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
}
|
||
|
||
.text-block {
|
||
background: var(--color-bg);
|
||
padding: 1rem;
|
||
border-radius: 4px;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.strength-list, .weakness-list {
|
||
list-style: none;
|
||
}
|
||
|
||
.strength-list li::before {
|
||
content: "✓ ";
|
||
color: var(--color-green);
|
||
}
|
||
|
||
.weakness-list li::before {
|
||
content: "✗ ";
|
||
color: #dc3545;
|
||
}
|
||
|
||
.btn-pdf {
|
||
display: inline-block;
|
||
padding: 0.75rem 1.5rem;
|
||
background: var(--color-blue);
|
||
color: white;
|
||
text-decoration: none;
|
||
border-radius: 4px;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.btn-pdf:hover {
|
||
background: #007b82;
|
||
}
|
||
|
||
/* Upload Tab */
|
||
.upload-section {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 2rem;
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.tab-btn {
|
||
padding: 0.5rem 1rem;
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
color: var(--color-darkgray);
|
||
}
|
||
|
||
.tab-btn.active {
|
||
border-bottom-color: var(--color-blue);
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
min-height: 200px;
|
||
padding: 1rem;
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 4px;
|
||
font-family: inherit;
|
||
resize: vertical;
|
||
}
|
||
|
||
.file-drop {
|
||
border: 2px dashed var(--color-lightgray);
|
||
border-radius: 8px;
|
||
padding: 3rem;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.file-drop:hover {
|
||
border-color: var(--color-blue);
|
||
}
|
||
|
||
.btn-analyze {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 1rem;
|
||
background: var(--color-green);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.btn-analyze:hover {
|
||
background: #728a2b;
|
||
}
|
||
|
||
.btn-analyze:disabled {
|
||
background: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Mode Toggle */
|
||
.mode-toggle {
|
||
display: flex;
|
||
background: var(--color-bg);
|
||
border-radius: 4px;
|
||
padding: 0.25rem;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.mode-btn {
|
||
padding: 0.5rem 1rem;
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
color: var(--color-darkgray);
|
||
}
|
||
|
||
.mode-btn.active {
|
||
background: white;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.hamburger-dropdown {
|
||
display: none;
|
||
position: absolute;
|
||
right: 0;
|
||
top: 100%;
|
||
background: white;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
min-width: 180px;
|
||
z-index: 100;
|
||
padding: 0.3rem 0;
|
||
}
|
||
.hamburger-dropdown.open { display: block; }
|
||
.hamburger-dropdown a,
|
||
.hamburger-dropdown button {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 0.5rem 1rem;
|
||
text-align: left;
|
||
text-decoration: none;
|
||
color: var(--color-darkgray);
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
}
|
||
.hamburger-dropdown a:hover,
|
||
.hamburger-dropdown button:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
/* Loading */
|
||
.loading {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.spinner {
|
||
width: 30px;
|
||
height: 30px;
|
||
border: 3px solid var(--color-lightgray);
|
||
border-top-color: var(--color-blue);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Mobile Back-to-List Button (nur Mobile sichtbar) */
|
||
.btn-back-mobile {
|
||
display: none;
|
||
padding: 0.5rem 0.75rem;
|
||
background: var(--color-bg);
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
margin-bottom: 1rem;
|
||
font-size: 0.85rem;
|
||
color: var(--color-darkgray);
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 900px) {
|
||
.header {
|
||
padding: 0.75rem 1rem;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.header .subtitle {
|
||
font-size: 0.8rem;
|
||
flex-basis: 100%;
|
||
order: 10;
|
||
}
|
||
|
||
.mode-toggle {
|
||
margin-left: auto;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mode-btn {
|
||
padding: 0.4rem 0.6rem;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.main-container {
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.list-panel {
|
||
width: 100%;
|
||
min-width: 0;
|
||
border-right: none;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
}
|
||
|
||
.list-content {
|
||
max-height: 50vh;
|
||
}
|
||
|
||
.detail-panel {
|
||
padding: 1rem;
|
||
overflow: visible;
|
||
flex: none;
|
||
}
|
||
|
||
.btn-back-mobile {
|
||
display: inline-block;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.header h1 { font-size: 1.1rem; }
|
||
.header .subtitle { display: none; }
|
||
.detail-card { padding: 1rem; }
|
||
.matrix-table { font-size: 0.7rem; }
|
||
.matrix-table th, .matrix-table td { padding: 0.25rem 0.3rem; }
|
||
.matrix-grid { grid-template-columns: 1fr; }
|
||
.upload-section { padding: 1rem; }
|
||
.file-drop { padding: 1.5rem; }
|
||
.score-big { font-size: 2.2rem; }
|
||
.detail-header { flex-wrap: wrap; gap: 0.5rem; }
|
||
}
|
||
|
||
/* Stats Bar */
|
||
.stats-bar {
|
||
display: flex;
|
||
gap: 2rem;
|
||
padding: 1rem;
|
||
background: var(--color-bg);
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
}
|
||
|
||
.stat {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.5rem;
|
||
font-weight: bold;
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.8rem;
|
||
color: #888;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="header">
|
||
<h1>{{ app_name }}</h1>
|
||
<span class="subtitle">Gemeinwohl-Matrix 2.0 für Gemeinden</span>
|
||
<select class="bundesland-select" id="bundesland-select" onchange="changeBundesland(this.value)">
|
||
{% for bl in bundeslaender %}
|
||
<option value="{{ bl.code }}" {% if bl.active %}{% else %}disabled{% endif %}>
|
||
{{ bl.name }}{% if not bl.active %} (bald){% endif %}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
<div class="mode-toggle">
|
||
<button class="mode-btn active" onclick="showMode('browse')">📋 Durchsuchen</button>
|
||
<button class="mode-btn" onclick="showMode('bookmarks')">⭐ Merkliste</button>
|
||
<button class="mode-btn" onclick="showMode('tags')">🏷️ Tags</button>
|
||
<button class="mode-btn" onclick="showMode('upload')">📤 Prüfen</button>
|
||
<div class="hamburger-wrapper" style="position:relative;display:inline-block;">
|
||
<button class="mode-btn" onclick="document.getElementById('hamburger-menu').classList.toggle('open')" style="font-size:1.2rem;padding:0.3rem 0.6rem;">☰</button>
|
||
<div id="hamburger-menu" class="hamburger-dropdown">
|
||
<a href="/auswertungen">📈 Auswertungen</a>
|
||
<a href="/quellen">📚 Quellen</a>
|
||
<a href="/methodik">🔍 Methodik</a>
|
||
<hr style="margin:0.3rem 0;border:none;border-top:1px solid #eee;">
|
||
<button id="auth-btn" onclick="event.stopPropagation();">🔑 Anmelden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-container" id="browse-mode">
|
||
<!-- Left: List -->
|
||
<aside class="list-panel">
|
||
<div class="list-header">
|
||
<!--
|
||
#16: zwei klar getrennte Suchfelder. Das erste filtert
|
||
in der DB der bereits geprüften Anträge (Live, debounced).
|
||
Das zweite triggert per Enter oder Button eine Live-
|
||
Anfrage gegen den Landtag-Adapter. Beide schreiben in
|
||
dieselbe Liste, unterscheiden sich aber visuell und
|
||
semantisch klar.
|
||
-->
|
||
<div class="search-row" style="flex-direction: column; gap: 0.4rem;">
|
||
<div style="display: flex; gap: 0.4rem; width: 100%;">
|
||
<input type="text" class="search-box" id="search-input"
|
||
placeholder="📊 Suche in geprüften Anträgen (DB)…"
|
||
oninput="debounceSearch(this.value)"
|
||
style="flex: 1;">
|
||
</div>
|
||
<div style="display: flex; gap: 0.4rem; width: 100%;">
|
||
<input type="text" class="search-box" id="landtag-search-input"
|
||
placeholder="🏛️ Im Landtag suchen (live)…"
|
||
onkeydown="if(event.key==='Enter')searchLandtag()"
|
||
style="flex: 1;">
|
||
<button class="btn-landtag" id="btn-landtag" onclick="searchLandtag()">🔍 Suchen</button>
|
||
</div>
|
||
</div>
|
||
<div class="list-filters">
|
||
<button class="filter-btn active" data-filter="all" onclick="setScoreFilter('all', this)">Alle</button>
|
||
<button class="filter-btn" data-filter="high" onclick="setScoreFilter('high', this)">8-10</button>
|
||
<button class="filter-btn" data-filter="mid" onclick="setScoreFilter('mid', this)">5-7</button>
|
||
<button class="filter-btn" data-filter="low" onclick="setScoreFilter('low', this)">0-4</button>
|
||
<select id="partei-filter" onchange="setParteiFilter(this.value)" style="padding: 0.25rem 0.5rem; border-radius: 20px; border: 1px solid var(--color-lightgray); font-size: 0.8rem; cursor: pointer;">
|
||
<option value="">Alle Parteien</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="stats-bar" style="padding: 0.5rem 1rem; gap: 1rem; flex-wrap: wrap; align-items: center;">
|
||
<span style="font-size: 0.8rem;"><strong id="stat-total">0</strong> geprüft · <strong id="stat-high">0</strong> vorbildlich · Ø <strong id="stat-avg">0</strong></span>
|
||
<span id="bundesland-stats" style="font-size: 0.8rem; color: var(--color-darkgray); display: none; gap: 0.6rem; flex-wrap: wrap;"></span>
|
||
<span style="color: var(--color-lightgray);">|</span>
|
||
<span id="partei-stats" style="font-size: 0.8rem; display: flex; gap: 0.75rem; flex-wrap: wrap;"></span>
|
||
</div>
|
||
<div class="list-content" id="list-content">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>Lade Bewertungen...</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Right: Detail -->
|
||
<main class="detail-panel" id="detail-panel">
|
||
<div class="detail-placeholder">
|
||
<p>👈 Wähle einen Antrag aus der Liste</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Bookmarks / Merkliste Mode -->
|
||
<div class="main-container" id="bookmarks-mode" style="display: none;">
|
||
<div class="list-panel" style="max-width: 100%;">
|
||
<h2 style="color: var(--color-blue); margin-bottom: 1rem;">⭐ Merkliste</h2>
|
||
<div id="bookmarks-content">
|
||
<p style="color: #888;">Lade Merkliste...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tags Mode -->
|
||
<div class="main-container" id="tags-mode" style="display: none;">
|
||
<aside class="list-panel" style="width: 100%; max-width: 400px;">
|
||
<div class="list-header">
|
||
<h3 style="margin-bottom: 0.5rem;">🏷️ Filter nach Tags</h3>
|
||
<p style="font-size: 0.85rem; color: #666; margin-bottom: 1rem;">Klicke auf Tags um zu filtern. Mehrfachauswahl zeigt Schnittmenge.</p>
|
||
<input type="text" class="search-box" id="tag-search-input" placeholder="Tags durchsuchen..." oninput="filterTagCloud(this.value)">
|
||
<div id="active-tags" style="margin-top: 0.75rem; display: flex; flex-wrap: wrap; gap: 0.5rem;"></div>
|
||
<button onclick="clearTagFilters()" style="margin-top: 0.75rem; padding: 0.5rem 1rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; display: none;" id="clear-tags-btn">✕ Filter zurücksetzen</button>
|
||
</div>
|
||
<div class="list-content" id="tag-cloud" style="padding: 1rem;">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>Lade Tags...</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
<main class="detail-panel" id="tag-results-panel">
|
||
<div class="detail-card">
|
||
<h3 style="margin-bottom: 1rem;">📋 Gefilterte Anträge</h3>
|
||
<div id="tag-results-list">
|
||
<p style="color: #888;">Wähle Tags aus der Wolke um Anträge zu filtern.</p>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Upload Mode -->
|
||
<div class="main-container" id="upload-mode" style="display: none;">
|
||
<div class="detail-panel">
|
||
<div class="upload-section">
|
||
<h2 style="margin-bottom: 1rem; color: var(--color-blue);">Neuen Antrag prüfen</h2>
|
||
|
||
<div class="tabs">
|
||
<button class="tab-btn active" onclick="showTab('text')">Text eingeben</button>
|
||
<button class="tab-btn" onclick="showTab('file')">PDF hochladen</button>
|
||
</div>
|
||
|
||
<div id="tab-text">
|
||
<textarea id="antrag-text" placeholder="Antragstext hier einfügen..."></textarea>
|
||
</div>
|
||
|
||
<div id="tab-file" style="display: none;">
|
||
<div class="file-drop" onclick="document.getElementById('file-input').click()">
|
||
<p>📄 PDF hier ablegen oder klicken</p>
|
||
<input type="file" id="file-input" accept=".pdf" style="display: none" onchange="handleFile(this)">
|
||
<p id="file-name" style="margin-top: 0.5rem; color: var(--color-blue);"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 1rem;">
|
||
<label>Bundesland:</label>
|
||
<select id="bundesland" style="padding: 0.5rem; margin-left: 0.5rem;">
|
||
<option value="" disabled selected>— Bundesland wählen —</option>
|
||
{% for bl in bundeslaender if bl.code != 'ALL' %}
|
||
<option value="{{ bl.code }}" {% if not bl.active %}disabled{% endif %}>
|
||
{{ bl.name }}{% if not bl.active %} (bald){% endif %}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<button class="btn-analyze" id="analyze-btn" onclick="startAnalysis()">
|
||
🔍 GWÖ-Analyse starten
|
||
</button>
|
||
|
||
<div id="analysis-status" style="display: none;">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span id="status-text">Analyse läuft...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="analysis-result" style="display: none; margin-top: 1rem;"></div>
|
||
|
||
<!-- Batch-Analyse -->
|
||
<div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 2px solid var(--color-lightgray);">
|
||
<h2 style="margin-bottom: 0.5rem; color: var(--color-blue);">📦 Batch-Analyse</h2>
|
||
<p style="font-size: 0.85rem; color: #666; margin-bottom: 1rem;">
|
||
Analysiert automatisch die neuesten ungeprüften Anträge eines Bundeslandes.
|
||
</p>
|
||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||
<select id="batch-bundesland" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
|
||
{% for bl in bundeslaender if bl.code != 'ALL' and bl.active %}
|
||
<option value="{{ bl.code }}">{{ bl.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<select id="batch-limit" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
|
||
<option value="5">5 Anträge</option>
|
||
<option value="10" selected>10 Anträge</option>
|
||
<option value="20">20 Anträge</option>
|
||
<option value="50">50 Anträge</option>
|
||
</select>
|
||
<button id="batch-btn" onclick="startBatch()"
|
||
${currentUser ? '' : 'disabled title="Nur nach Anmeldung"'}
|
||
style="padding:0.4rem 1rem;background:var(--color-green);color:white;border:none;border-radius:4px;cursor:pointer;">
|
||
🚀 Batch starten
|
||
</button>
|
||
</div>
|
||
<div id="batch-status" style="margin-top:0.75rem;font-size:0.85rem;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let allAssessments = [];
|
||
let currentScoreFilter = 'all';
|
||
let currentParteiFilter = '';
|
||
let currentBundesland = 'ALL';
|
||
let searchTimeout = null;
|
||
let isSearching = false;
|
||
let selectedTags = new Set();
|
||
let allTags = {};
|
||
let currentUser = null; // #43: Auth-State
|
||
|
||
// #43: Auth prüfen beim Load. Steuert ob "Jetzt prüfen" aktiv ist.
|
||
async function initAuth() {
|
||
try {
|
||
const resp = await fetch('/api/auth/me');
|
||
const data = await resp.json();
|
||
currentUser = data.authenticated ? data : null;
|
||
} catch { currentUser = null; }
|
||
updateAuthUI();
|
||
}
|
||
function updateAuthUI() {
|
||
const authBtn = document.getElementById('auth-btn');
|
||
if (!authBtn) return;
|
||
if (currentUser) {
|
||
authBtn.textContent = '✓ Angemeldet (Logout)';
|
||
authBtn.classList.add('logged-in');
|
||
authBtn.style.color = '#889e33';
|
||
authBtn.onclick = () => {
|
||
// Cookie löschen + Keycloak-Logout
|
||
document.cookie = 'access_token=; Max-Age=0; path=/; secure; samesite=lax';
|
||
currentUser = null;
|
||
updateAuthUI();
|
||
loadAssessments(); // Liste neu rendern (Buttons deaktivieren)
|
||
};
|
||
// Bestehende Liste neu rendern damit Buttons aktiv werden
|
||
if (allAssessments.length > 0) renderList(allAssessments);
|
||
} else {
|
||
authBtn.textContent = '🔑 Anmelden';
|
||
authBtn.style.color = '';
|
||
authBtn.classList.remove('logged-in');
|
||
authBtn.onclick = async () => {
|
||
const resp = await fetch(`/api/auth/login-url?redirect=${encodeURIComponent(window.location.pathname)}`);
|
||
const data = await resp.json();
|
||
if (data.url) window.location.href = data.url;
|
||
};
|
||
}
|
||
}
|
||
|
||
// Hamburger-Menü schließen bei Klick außerhalb
|
||
document.addEventListener('click', (e) => {
|
||
const menu = document.getElementById('hamburger-menu');
|
||
if (menu && !e.target.closest('.hamburger-wrapper')) {
|
||
menu.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
// Partei-Normalisierung (global, für Stats + Labels)
|
||
function normalizePartei(f) {
|
||
const u = f.toUpperCase();
|
||
if (u === 'AFD') return 'AfD';
|
||
if (u === 'GRÜNE' || u === 'GRUENE' || u === 'BÜNDNIS 90/DIE GRÜNEN') return 'GRÜNE';
|
||
if (u === 'DIE LINKE') return 'LINKE';
|
||
return f;
|
||
}
|
||
|
||
// Map code → parlament_name, vom Backend mit dem Initial-Render geliefert.
|
||
// Wird im Detail-Header und im Listen-Item-Badge-Tooltip verwendet.
|
||
const PARLAMENT_NAMES = {{ parlament_names | tojson }};
|
||
|
||
// Load assessments on page load — localStorage-Auswahl wiederherstellen
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initAuth(); // #43: Auth-State prüfen
|
||
const saved = localStorage.getItem('selectedBundesland');
|
||
const select = document.getElementById('bundesland-select');
|
||
if (saved) {
|
||
// Validieren: existiert die Option?
|
||
const exists = Array.from(select.options).some(
|
||
o => o.value === saved && !o.disabled
|
||
);
|
||
if (exists) {
|
||
currentBundesland = saved;
|
||
select.value = saved;
|
||
}
|
||
}
|
||
// Modus-Klasse für CSS (Badges aus/an)
|
||
document.getElementById('list-content').dataset.mode =
|
||
(currentBundesland === 'ALL') ? 'all' : 'single';
|
||
// Landtag-Button-State für Initial-Auswahl
|
||
const btnLandtag = document.getElementById('btn-landtag');
|
||
if (currentBundesland === 'ALL') {
|
||
btnLandtag.disabled = true;
|
||
btnLandtag.title = 'Landtag-Suche nur mit konkretem Bundesland möglich';
|
||
}
|
||
loadAssessments();
|
||
});
|
||
|
||
function buildParteienFilter() {
|
||
// Alle Fraktionen sammeln
|
||
const parteien = new Set();
|
||
allAssessments.forEach(a => {
|
||
(a.fraktionen || []).forEach(f => parteien.add(f));
|
||
});
|
||
|
||
const select = document.getElementById('partei-filter');
|
||
select.innerHTML = '<option value="">Alle Parteien</option>';
|
||
Array.from(parteien).sort().forEach(p => {
|
||
select.innerHTML += `<option value="${p}">${p}</option>`;
|
||
});
|
||
}
|
||
|
||
function buildTagCloud() {
|
||
allTags = {};
|
||
allAssessments.forEach(a => {
|
||
(a.themen || []).forEach(tag => {
|
||
allTags[tag] = (allTags[tag] || 0) + 1;
|
||
});
|
||
});
|
||
renderTagCloud();
|
||
}
|
||
|
||
function renderTagCloud(filter = '') {
|
||
const container = document.getElementById('tag-cloud');
|
||
const filterLower = filter.toLowerCase();
|
||
|
||
// Sortiert nach Häufigkeit
|
||
const sorted = Object.entries(allTags)
|
||
.filter(([tag]) => tag.toLowerCase().includes(filterLower))
|
||
.sort((a, b) => b[1] - a[1]);
|
||
|
||
if (sorted.length === 0) {
|
||
container.innerHTML = '<p style="color: #888;">Keine Tags gefunden</p>';
|
||
return;
|
||
}
|
||
|
||
const maxCount = Math.max(...sorted.map(([, c]) => c));
|
||
|
||
container.innerHTML = '<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">' +
|
||
sorted.map(([tag, count]) => {
|
||
const size = 0.75 + (count / maxCount) * 0.75;
|
||
const isActive = selectedTags.has(tag);
|
||
return `<button
|
||
onclick="toggleTag('${tag.replace(/'/g, "\\'")}')"
|
||
style="
|
||
font-size: ${size}rem;
|
||
padding: 0.3rem 0.6rem;
|
||
border-radius: 20px;
|
||
border: 1px solid ${isActive ? 'var(--color-blue)' : 'var(--color-lightgray)'};
|
||
background: ${isActive ? 'var(--color-blue)' : 'white'};
|
||
color: ${isActive ? 'white' : 'var(--color-darkgray)'};
|
||
cursor: pointer;
|
||
"
|
||
title="${count} Anträge"
|
||
>${tag} <span style="font-size: 0.7rem; opacity: 0.7;">(${count})</span></button>`;
|
||
}).join('') +
|
||
'</div>';
|
||
}
|
||
|
||
function filterTagCloud(query) {
|
||
renderTagCloud(query);
|
||
}
|
||
|
||
function toggleTag(tag) {
|
||
if (selectedTags.has(tag)) {
|
||
selectedTags.delete(tag);
|
||
} else {
|
||
selectedTags.add(tag);
|
||
}
|
||
updateTagFilterUI();
|
||
filterByTags();
|
||
}
|
||
|
||
function updateTagFilterUI() {
|
||
const container = document.getElementById('active-tags');
|
||
const clearBtn = document.getElementById('clear-tags-btn');
|
||
|
||
if (selectedTags.size === 0) {
|
||
container.innerHTML = '';
|
||
clearBtn.style.display = 'none';
|
||
} else {
|
||
container.innerHTML = Array.from(selectedTags).map(tag =>
|
||
`<span style="padding: 0.25rem 0.5rem; background: var(--color-blue); color: white; border-radius: 4px; font-size: 0.85rem;">
|
||
${tag} <span style="cursor: pointer; margin-left: 0.25rem;" onclick="toggleTag('${tag.replace(/'/g, "\\'")}')">×</span>
|
||
</span>`
|
||
).join('');
|
||
clearBtn.style.display = 'block';
|
||
}
|
||
renderTagCloud(document.getElementById('tag-search-input')?.value || '');
|
||
}
|
||
|
||
function clearTagFilters() {
|
||
selectedTags.clear();
|
||
updateTagFilterUI();
|
||
filterByTags();
|
||
}
|
||
|
||
function filterByTags() {
|
||
const resultsContainer = document.getElementById('tag-results-list');
|
||
|
||
if (selectedTags.size === 0) {
|
||
resultsContainer.innerHTML = '<p style="color: #888;">Wähle Tags aus der Wolke um Anträge zu filtern.</p>';
|
||
return;
|
||
}
|
||
|
||
// Schnittmenge: Anträge müssen ALLE ausgewählten Tags haben
|
||
const filtered = allAssessments.filter(a => {
|
||
const tags = new Set(a.themen || []);
|
||
return Array.from(selectedTags).every(t => tags.has(t));
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
resultsContainer.innerHTML = '<p style="color: #888;">Keine Anträge mit dieser Tag-Kombination gefunden.</p>';
|
||
return;
|
||
}
|
||
|
||
resultsContainer.innerHTML = `
|
||
<p style="margin-bottom: 1rem; color: #666;">${filtered.length} Anträge gefunden</p>
|
||
${filtered.map(item => `
|
||
<div style="padding: 0.75rem; border-bottom: 1px solid var(--color-lightgray); cursor: pointer;" onclick="showMode('browse'); setTimeout(() => showDetail('${item.drucksache}'), 100);">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span style="font-weight: bold; color: var(--color-blue);">${item.drucksache}</span>
|
||
<span style="padding: 0.1rem 0.5rem; border-radius: 4px; font-size: 0.85rem; ${item.gwoeScore >= 8 ? 'background: #155724; color: white;' : item.gwoeScore >= 5 ? 'background: #889e33; color: white;' : 'background: #dc3545; color: white;'}">${item.gwoeScore}/10</span>
|
||
</div>
|
||
<div style="font-size: 0.9rem; margin-top: 0.25rem;">${item.title || 'Ohne Titel'}</div>
|
||
<div style="font-size: 0.8rem; color: #888;">${(item.fraktionen || []).join(', ')}</div>
|
||
</div>
|
||
`).join('')}
|
||
`;
|
||
}
|
||
|
||
async function loadAssessments() {
|
||
try {
|
||
const url = `/api/assessments?bundesland=${encodeURIComponent(currentBundesland)}`;
|
||
const resp = await fetch(url);
|
||
allAssessments = await resp.json();
|
||
updateStats();
|
||
renderList(allAssessments);
|
||
buildParteienFilter();
|
||
buildTagCloud();
|
||
} catch (e) {
|
||
console.error('loadAssessments error:', e);
|
||
document.getElementById('list-content').innerHTML =
|
||
`<p style="padding: 1rem; color: #c00;">Fehler beim Laden: ${e.message}</p>`;
|
||
}
|
||
}
|
||
|
||
function updateStats() {
|
||
const checked = allAssessments.filter(a => a.status !== 'unchecked').length;
|
||
const high = allAssessments.filter(a => a.gwoeScore >= 8).length;
|
||
const avg = checked > 0 ? (allAssessments.filter(a => a.gwoeScore != null).reduce((s, a) => s + (a.gwoeScore || 0), 0) / checked).toFixed(1) : 0;
|
||
|
||
document.getElementById('stat-total').textContent = checked;
|
||
document.getElementById('stat-high').textContent = high;
|
||
document.getElementById('stat-avg').textContent = avg;
|
||
|
||
// Pro-Bundesland-Aufschlüsselung — nur im Bundesweit-Modus, und nur
|
||
// wenn tatsächlich mehr als ein Bundesland in der Liste vorkommt.
|
||
const blContainer = document.getElementById('bundesland-stats');
|
||
if (currentBundesland === 'ALL') {
|
||
const blStats = {};
|
||
allAssessments.forEach(a => {
|
||
if (a.gwoeScore == null || !a.bundesland) return;
|
||
if (!blStats[a.bundesland]) blStats[a.bundesland] = { sum: 0, count: 0 };
|
||
blStats[a.bundesland].sum += a.gwoeScore;
|
||
blStats[a.bundesland].count += 1;
|
||
});
|
||
const codes = Object.keys(blStats);
|
||
if (codes.length > 1) {
|
||
const sortedBl = codes
|
||
.map(c => ({ code: c, avg: blStats[c].sum / blStats[c].count, count: blStats[c].count }))
|
||
.sort((a, b) => b.avg - a.avg);
|
||
blContainer.innerHTML = sortedBl.map(b =>
|
||
`<span title="${PARLAMENT_NAMES[b.code] || b.code}">Ø <strong>${b.code}</strong> ${b.avg.toFixed(1)} <span style="color:#888">(n=${b.count})</span></span>`
|
||
).join(' · ');
|
||
blContainer.style.display = 'inline-flex';
|
||
} else {
|
||
blContainer.style.display = 'none';
|
||
blContainer.innerHTML = '';
|
||
}
|
||
} else {
|
||
blContainer.style.display = 'none';
|
||
blContainer.innerHTML = '';
|
||
}
|
||
|
||
// Partei-Durchschnitte berechnen (Normalisierung via globaler normalizePartei)
|
||
const parteiStats = {};
|
||
allAssessments.forEach(a => {
|
||
if (a.gwoeScore == null) return;
|
||
(a.fraktionen || []).forEach(f => {
|
||
const norm = normalizePartei(f);
|
||
if (!parteiStats[norm]) parteiStats[norm] = { sum: 0, count: 0 };
|
||
parteiStats[norm].sum += a.gwoeScore;
|
||
parteiStats[norm].count += 1;
|
||
});
|
||
});
|
||
|
||
// Sortiert nach Durchschnitt (absteigend)
|
||
const sorted = Object.entries(parteiStats)
|
||
.map(([partei, data]) => ({ partei, avg: data.sum / data.count, count: data.count }))
|
||
.sort((a, b) => b.avg - a.avg);
|
||
|
||
const container = document.getElementById('partei-stats');
|
||
container.innerHTML = sorted.map(p => {
|
||
const color = p.avg >= 7 ? '#889e33' : p.avg >= 4 ? '#fd7e14' : '#dc3545';
|
||
return `<span style="color: ${color};"><strong>${p.partei}</strong> ${p.avg.toFixed(1)}</span>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderList(items) {
|
||
const container = document.getElementById('list-content');
|
||
if (items.length === 0) {
|
||
container.innerHTML = '<p style="padding: 1rem; color: #888;">Keine Ergebnisse</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = items.map(item => {
|
||
const isUnchecked = item.status === 'unchecked';
|
||
// Skala 0-10
|
||
const scoreClass = isUnchecked ? 'status-unchecked' :
|
||
item.gwoeScore >= 8 ? 'score-high' :
|
||
item.gwoeScore >= 5 ? 'score-mid' :
|
||
item.gwoeScore >= 3 ? 'score-low' : 'score-negative';
|
||
const fraktionen = (item.fraktionen || []).join(', ') || 'k.A.';
|
||
const themen = (item.themen || []).slice(0, 3);
|
||
const scoreText = isUnchecked ? '⏳' : `${item.gwoeScore}/10`;
|
||
|
||
const blBadge = item.bundesland
|
||
? `<span class="bl-badge" title="${PARLAMENT_NAMES[item.bundesland] || item.bundesland}">${item.bundesland}</span>`
|
||
: '';
|
||
return `
|
||
<div class="list-item ${isUnchecked ? 'unchecked' : ''}" data-drucksache="${item.drucksache}" onclick="${isUnchecked ? '' : `showDetail('${item.drucksache}')`}">
|
||
<div class="list-item-header">
|
||
<span class="list-item-id">${blBadge}${item.drucksache}</span>
|
||
<span class="list-item-score ${scoreClass}">${scoreText}</span>
|
||
</div>
|
||
<div class="list-item-title">${(item.title || 'Ohne Titel').length > 80 ? (item.title.substring(0, 80) + '…') : (item.title || 'Ohne Titel')}</div>
|
||
<div class="list-item-meta">${fraktionen} · ${item.datum || ''}</div>
|
||
${isUnchecked ? `
|
||
<button class="btn-check-now"
|
||
${currentUser ? '' : 'disabled title="Nur nach Anmeldung verfügbar" style="opacity:0.5;cursor:not-allowed;"'}
|
||
onclick="event.stopPropagation(); checkNow('${item.drucksache}', this)">
|
||
🔍 Jetzt prüfen
|
||
</button>
|
||
` : `
|
||
<div class="list-item-tags">
|
||
${themen.map(t => `<span class="tag">${t}</span>`).join('')}
|
||
</div>
|
||
`}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function debounceSearch(query) {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => performSearch(query), 300);
|
||
}
|
||
|
||
async function performSearch(query) {
|
||
if (query.length < 2) {
|
||
// Show all from DB with current filters
|
||
applyAllFilters();
|
||
return;
|
||
}
|
||
|
||
isSearching = true;
|
||
document.getElementById('list-content').innerHTML = `
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>Suche...</span>
|
||
</div>
|
||
`;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/search?q=${encodeURIComponent(query)}&bundesland=${currentBundesland}`);
|
||
let results = await resp.json();
|
||
|
||
// Score-Filter anwenden
|
||
if (currentScoreFilter !== 'all') {
|
||
results = applyScoreFilter(results, currentScoreFilter);
|
||
}
|
||
|
||
// Partei-Filter anwenden
|
||
if (currentParteiFilter) {
|
||
results = results.filter(a =>
|
||
(a.fraktionen || []).includes(currentParteiFilter)
|
||
);
|
||
}
|
||
|
||
renderList(results);
|
||
} catch (e) {
|
||
document.getElementById('list-content').innerHTML =
|
||
'<p style="padding: 1rem; color: #888;">Suchfehler</p>';
|
||
}
|
||
|
||
isSearching = false;
|
||
}
|
||
|
||
async function checkNow(drucksache, btn) {
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Prüfe...';
|
||
|
||
try {
|
||
const resp = await fetch('/api/analyze-drucksache', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `drucksache=${encodeURIComponent(drucksache)}&bundesland=${currentBundesland}`
|
||
});
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'queued') {
|
||
btn.textContent = '⏳ Analysiere...';
|
||
pollAnalysis(data.job_id, drucksache, btn);
|
||
} else if (data.status === 'already_checked') {
|
||
btn.textContent = '✓ Bereits geprüft';
|
||
setTimeout(() => loadAssessments(), 500);
|
||
} else {
|
||
btn.textContent = '✗ Fehler';
|
||
btn.disabled = false;
|
||
}
|
||
} catch (e) {
|
||
btn.textContent = '✗ Fehler';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// ─── Batch-Analyse ──────────────────────────────────────────
|
||
async function startBatch() {
|
||
const bl = document.getElementById('batch-bundesland').value;
|
||
const limit = document.getElementById('batch-limit').value;
|
||
const btn = document.getElementById('batch-btn');
|
||
const status = document.getElementById('batch-status');
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Wird gestartet...';
|
||
status.innerHTML = '';
|
||
|
||
try {
|
||
const resp = await fetch('/api/batch-analyze', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `bundesland=${bl}&limit=${limit}`
|
||
});
|
||
if (resp.status === 401) {
|
||
status.innerHTML = '<span style="color:#dc3545;">🔒 Bitte zuerst anmelden.</span>';
|
||
btn.disabled = false; btn.textContent = '🚀 Batch starten';
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
if (data.status === 'batch_enqueued') {
|
||
status.innerHTML = `
|
||
<span style="color:var(--color-green);">✓ ${data.enqueued} Anträge in die Queue eingereiht</span>
|
||
${data.skipped_existing > 0 ? `<br><span style="color:#888;">${data.skipped_existing} bereits bewertet (übersprungen)</span>` : ''}
|
||
<br><span style="color:#888;">Die Analyse läuft im Hintergrund. Ergebnisse erscheinen nach und nach in der Liste.</span>
|
||
`;
|
||
// Queue-Status pollen
|
||
pollBatchQueue(status);
|
||
} else {
|
||
status.innerHTML = `<span style="color:#dc3545;">❌ ${data.detail || 'Fehler'}</span>`;
|
||
}
|
||
} catch (e) {
|
||
status.innerHTML = `<span style="color:#dc3545;">❌ ${e.message}</span>`;
|
||
}
|
||
btn.disabled = false; btn.textContent = '🚀 Batch starten';
|
||
}
|
||
|
||
async function pollBatchQueue(statusEl) {
|
||
for (let i = 0; i < 200; i++) {
|
||
await new Promise(r => setTimeout(r, 3000));
|
||
try {
|
||
const qs = await fetch('/api/queue/status').then(r => r.json());
|
||
|
||
// Job-Tabelle rendern
|
||
const jobs = qs.jobs || [];
|
||
const jobsHtml = jobs.length > 0 ? `
|
||
<table style="width:100%;font-size:0.8rem;border-collapse:collapse;margin-top:0.5rem;">
|
||
<tr style="color:#888;"><td>Drucksache</td><td>Status</td><td>Dauer</td></tr>
|
||
${jobs.map(j => {
|
||
const statusIcon = j.status === 'completed' ? '✅' : j.status === 'processing' ? '⏳' : j.status === 'failed' ? '❌' : '⏸';
|
||
const dur = j.duration ? j.duration + 's' : '';
|
||
const link = j.status === 'completed' ? `<a href="#" onclick="showMode('browse');setTimeout(()=>showDetail('${j.drucksache}'),100);return false;" style="color:var(--color-blue);">${j.drucksache}</a>` : j.drucksache;
|
||
return `<tr style="border-top:1px solid #f0f0f0;"><td>${link}</td><td>${statusIcon} ${j.status}</td><td>${dur}</td></tr>`;
|
||
}).join('')}
|
||
</table>` : '';
|
||
|
||
// Fortschrittsbalken
|
||
const completed = jobs.filter(j => j.status === 'completed').length;
|
||
const total = jobs.length;
|
||
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
||
|
||
statusEl.innerHTML = `
|
||
<div style="margin:0.5rem 0;">
|
||
<div style="background:#eee;border-radius:4px;height:8px;overflow:hidden;">
|
||
<div style="background:var(--color-green);height:100%;width:${pct}%;transition:width 0.5s;"></div>
|
||
</div>
|
||
<span style="font-size:0.8rem;color:#888;">${completed}/${total} fertig · ${qs.concurrency} Worker · ~${Math.round(qs.estimated_wait_seconds/60)} Min.</span>
|
||
</div>
|
||
${jobsHtml}
|
||
`;
|
||
|
||
if (qs.pending === 0 && completed > 0) {
|
||
statusEl.innerHTML += `<br><span style="color:var(--color-green);">✓ Alle Jobs abgeschlossen</span>`;
|
||
loadAssessments();
|
||
return;
|
||
}
|
||
} catch { break; }
|
||
}
|
||
}
|
||
|
||
// ─── Merkliste (#94) ────────────────────────────────────────
|
||
async function loadBookmarksList() {
|
||
const container = document.getElementById('bookmarks-content');
|
||
if (!currentUser) {
|
||
container.innerHTML = '<p style="color:#888;">Bitte anmelden um die Merkliste zu sehen.</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = '<p style="color:#888;">Lade...</p>';
|
||
try {
|
||
const bookmarkIds = await fetch('/api/bookmarks').then(r => r.json());
|
||
if (bookmarkIds.length === 0) {
|
||
container.innerHTML = '<p style="color:#888;">Keine gemerkten Anträge. Klicke auf "🔖 Merken" bei einem Antrag.</p>';
|
||
return;
|
||
}
|
||
// Finde die Assessment-Daten für die gebookmarkten Drucksachen
|
||
const bookmarked = allAssessments.filter(a => bookmarkIds.includes(a.drucksache));
|
||
if (bookmarked.length === 0) {
|
||
container.innerHTML = '<p style="color:#888;">Gemerkte Anträge nicht in der aktuellen Auswahl gefunden. Wechsle zu "Bundesweit".</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = bookmarked.map(item => {
|
||
const scoreClass = item.gwoeScore >= 8 ? 'score-high' : item.gwoeScore >= 5 ? 'score-mid' : item.gwoeScore >= 3 ? 'score-low' : 'score-negative';
|
||
const blBadge = item.bundesland ? `<span class="bl-badge">${item.bundesland}</span>` : '';
|
||
return `
|
||
<div class="list-item" onclick="showMode('browse'); setTimeout(() => showDetail('${item.drucksache}'), 100);" style="cursor:pointer;">
|
||
<div class="list-item-header">
|
||
<span class="list-item-id">${blBadge}${item.drucksache}</span>
|
||
<span class="list-item-score ${scoreClass}">${item.gwoeScore}/10</span>
|
||
</div>
|
||
<div class="list-item-title">${(item.title || '').substring(0, 80)}${(item.title || '').length > 80 ? '…' : ''}</div>
|
||
<div class="list-item-meta">${(item.fraktionen || []).join(', ')} · ${item.datum || ''}</div>
|
||
</div>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
container.innerHTML = '<p style="color:#c00;">Fehler beim Laden der Merkliste.</p>';
|
||
}
|
||
}
|
||
|
||
// ─── Bookmarks + Comments (#94) ─────────────────────────────
|
||
async function toggleBookmark(drucksache, btn) {
|
||
const resp = await fetch('/api/bookmark', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `drucksache=${encodeURIComponent(drucksache)}`
|
||
});
|
||
if (resp.ok) {
|
||
const data = await resp.json();
|
||
btn.textContent = data.bookmarked ? '⭐ Gemerkt' : '🔖 Merken';
|
||
btn.style.background = data.bookmarked ? '#fff3cd' : 'none';
|
||
}
|
||
}
|
||
|
||
async function loadBookmarkState(drucksache) {
|
||
if (!currentUser) return;
|
||
const bookmarks = await fetch('/api/bookmarks').then(r => r.json());
|
||
const btn = document.getElementById('bookmark-btn-' + drucksache.replace('/', '-'));
|
||
if (btn && bookmarks.includes(drucksache)) {
|
||
btn.textContent = '⭐ Gemerkt';
|
||
btn.style.background = '#fff3cd';
|
||
}
|
||
}
|
||
|
||
async function loadComments(drucksache) {
|
||
const container = document.getElementById('comments-' + drucksache.replace('/', '-'));
|
||
if (!container) return;
|
||
try {
|
||
const comments = await fetch(`/api/comments?drucksache=${encodeURIComponent(drucksache)}`).then(r => r.json());
|
||
// Client-seitig filtern nach Sichtbarkeit
|
||
const visible = comments.filter(c => {
|
||
if (c.visibility === 'all') return true;
|
||
if (!currentUser) return false;
|
||
if (c.visibility === 'authenticated') return true;
|
||
if (c.visibility === 'private') return c.user_id === currentUser.sub;
|
||
// group:XYZ — TODO: gegen Keycloak-Gruppen prüfen
|
||
if (c.visibility?.startsWith('group:')) return true; // Platzhalter
|
||
return false;
|
||
});
|
||
if (visible.length === 0) {
|
||
container.innerHTML = '<span style="color:#aaa;font-size:0.85rem;">Noch keine Kommentare.</span>';
|
||
return;
|
||
}
|
||
const visBadge = (v) => {
|
||
if (v === 'private') return '<span style="font-size:0.7rem;background:#f0f0f0;padding:0.1rem 0.3rem;border-radius:2px;" title="Nur für dich sichtbar">👤</span>';
|
||
if (v === 'authenticated') return '<span style="font-size:0.7rem;background:#e8f4f8;padding:0.1rem 0.3rem;border-radius:2px;" title="Nur für angemeldete Nutzer">🔒</span>';
|
||
if (v?.startsWith('group:')) return `<span style="font-size:0.7rem;background:#f0e6ff;padding:0.1rem 0.3rem;border-radius:2px;" title="Gruppe: ${v.substring(6)}">👥 ${v.substring(6)}</span>`;
|
||
return '<span style="font-size:0.7rem;background:#e8ffe8;padding:0.1rem 0.3rem;border-radius:2px;" title="Öffentlich sichtbar">🌐</span>';
|
||
};
|
||
container.innerHTML = visible.map(c => `
|
||
<div style="padding:0.4rem 0;border-bottom:1px solid #f0f0f0;font-size:0.85rem;">
|
||
<strong>${c.user_name || 'Anonym'}</strong>
|
||
${visBadge(c.visibility)}
|
||
<span style="color:#aaa;font-size:0.75rem;margin-left:0.3rem;">${new Date(c.created_at).toLocaleString('de-DE')}</span>
|
||
${currentUser && currentUser.sub === c.user_id ? `<button onclick="deleteCommentUI(${c.id},'${drucksache}')" style="float:right;background:none;border:none;color:#dc3545;cursor:pointer;font-size:0.75rem;">✕</button>` : ''}
|
||
<div style="margin-top:0.2rem;">${c.text}</div>
|
||
</div>
|
||
`).join('');
|
||
} catch { container.innerHTML = ''; }
|
||
}
|
||
|
||
async function addCommentUI(drucksache) {
|
||
const safeDrs = drucksache.replace('/', '-');
|
||
const input = document.getElementById('comment-input-' + safeDrs);
|
||
const visSelect = document.getElementById('comment-visibility-' + safeDrs);
|
||
if (!input || !input.value.trim()) return;
|
||
const visibility = visSelect ? visSelect.value : 'all';
|
||
await fetch('/api/comment', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `drucksache=${encodeURIComponent(drucksache)}&text=${encodeURIComponent(input.value)}&visibility=${visibility}`
|
||
});
|
||
input.value = '';
|
||
loadComments(drucksache);
|
||
}
|
||
|
||
async function deleteCommentUI(commentId, drucksache) {
|
||
await fetch(`/api/comment/${commentId}`, {method: 'DELETE'});
|
||
loadComments(drucksache);
|
||
}
|
||
|
||
async function reAnalyze(drucksache, bundesland, btn) {
|
||
if (!currentUser) {
|
||
alert('Bitte zuerst anmelden.');
|
||
return;
|
||
}
|
||
btn.disabled = true;
|
||
btn.style.background = '#ffc107';
|
||
btn.textContent = '⏳ Lösche alte Bewertung...';
|
||
try {
|
||
// Altes Assessment löschen
|
||
const delResp = await fetch(`/api/assessment/delete?drucksache=${encodeURIComponent(drucksache)}`, {method: 'DELETE'});
|
||
if (delResp.status === 401) {
|
||
btn.textContent = '🔒 Nicht angemeldet';
|
||
btn.style.background = '#dc3545';
|
||
setTimeout(() => { btn.textContent = '🔄 Neu bewerten'; btn.style.background = '#6c757d'; btn.disabled = false; }, 3000);
|
||
return;
|
||
}
|
||
btn.textContent = '⏳ Analyse gestartet...';
|
||
// Neue Analyse enqueuen
|
||
const resp = await fetch('/api/analyze-drucksache', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `drucksache=${encodeURIComponent(drucksache)}&bundesland=${encodeURIComponent(bundesland)}`
|
||
});
|
||
if (resp.status === 401) {
|
||
btn.textContent = '🔒 Nicht angemeldet';
|
||
btn.style.background = '#dc3545';
|
||
setTimeout(() => { btn.textContent = '🔄 Neu bewerten'; btn.style.background = '#6c757d'; btn.disabled = false; }, 3000);
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
if (data.status === 'queued') {
|
||
btn.textContent = '⏳ Wird analysiert...';
|
||
btn.style.background = '#009da5';
|
||
pollAnalysis(data.job_id, drucksache, btn);
|
||
} else {
|
||
btn.textContent = '❌ ' + (data.detail || 'Fehler');
|
||
btn.style.background = '#dc3545';
|
||
setTimeout(() => { btn.textContent = '🔄 Neu bewerten'; btn.style.background = '#6c757d'; btn.disabled = false; }, 3000);
|
||
}
|
||
} catch (e) {
|
||
btn.textContent = '❌ ' + e.message;
|
||
btn.style.background = '#dc3545';
|
||
setTimeout(() => { btn.textContent = '🔄 Neu bewerten'; btn.style.background = '#6c757d'; btn.disabled = false; }, 3000);
|
||
}
|
||
}
|
||
|
||
async function pollAnalysis(jobId, drucksache, btn) {
|
||
try {
|
||
const resp = await fetch(`/status/${jobId}`);
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'completed') {
|
||
btn.textContent = '✓ Geprüft';
|
||
btn.style.background = '#889e33'; // Green
|
||
// Update this item in current list to show as checked
|
||
const listItem = btn.closest('.list-item');
|
||
if (listItem) {
|
||
listItem.classList.remove('unchecked');
|
||
listItem.onclick = () => showDetail(drucksache);
|
||
}
|
||
// Reload assessments in background (for internal list)
|
||
loadAssessments();
|
||
// Show detail for this item
|
||
setTimeout(() => showDetail(drucksache), 500);
|
||
} else if (data.status === 'failed') {
|
||
btn.textContent = '✗ Fehlgeschlagen';
|
||
btn.disabled = false;
|
||
} else {
|
||
setTimeout(() => pollAnalysis(jobId, drucksache, btn), 2000);
|
||
}
|
||
} catch (e) {
|
||
btn.textContent = '✗ Fehler';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function changeBundesland(code) {
|
||
currentBundesland = code;
|
||
localStorage.setItem('selectedBundesland', code);
|
||
|
||
// Filter zurücksetzen — Parteien & Tags pro Bundesland unterschiedlich,
|
||
// ein "LINKE"-Filter aus LSA würde in NRW eine leere Liste zeigen.
|
||
currentScoreFilter = 'all';
|
||
currentParteiFilter = '';
|
||
selectedTags.clear();
|
||
document.getElementById('search-input').value = '';
|
||
const landtagInput = document.getElementById('landtag-search-input');
|
||
if (landtagInput) landtagInput.value = '';
|
||
document.querySelectorAll('.filter-btn').forEach(b => {
|
||
b.classList.toggle('active', b.dataset.filter === 'all');
|
||
});
|
||
const parteiSelect = document.getElementById('partei-filter');
|
||
if (parteiSelect) parteiSelect.value = '';
|
||
|
||
// Upload-Mode-Dropdown synchronisieren. Bei "ALL" KEIN automatischer
|
||
// Default — der User soll im Upload-Form bewusst ein Bundesland wählen.
|
||
const uploadDropdown = document.getElementById('bundesland');
|
||
if (uploadDropdown) {
|
||
if (code === 'ALL') {
|
||
uploadDropdown.value = '';
|
||
} else {
|
||
uploadDropdown.value = code;
|
||
}
|
||
}
|
||
|
||
// Landtag-Suche-Button im Bundesweit-Modus deaktivieren
|
||
const btnLandtag = document.getElementById('btn-landtag');
|
||
if (code === 'ALL') {
|
||
btnLandtag.disabled = true;
|
||
btnLandtag.title = 'Landtag-Suche nur mit konkretem Bundesland möglich';
|
||
} else {
|
||
btnLandtag.disabled = false;
|
||
btnLandtag.title = '';
|
||
}
|
||
|
||
// Modus-Klasse für CSS (Badges aus/an im Single-Modus)
|
||
document.getElementById('list-content').dataset.mode =
|
||
(code === 'ALL') ? 'all' : 'single';
|
||
|
||
loadAssessments();
|
||
}
|
||
|
||
async function searchLandtag() {
|
||
if (currentBundesland === 'ALL') {
|
||
alert('Landtag-Suche ist nur mit Auswahl eines konkreten Bundeslands möglich.\nBitte oben ein Bundesland auswählen.');
|
||
return;
|
||
}
|
||
// #16: Landtag-Suche zieht aus dem dedizierten Landtag-Input,
|
||
// nicht mehr aus dem DB-Suchfeld.
|
||
const query = document.getElementById('landtag-search-input').value.trim();
|
||
if (query.length < 2) {
|
||
alert('Bitte mindestens 2 Zeichen ins Landtag-Suchfeld eingeben');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('btn-landtag');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Suche...';
|
||
|
||
document.getElementById('list-content').innerHTML = `
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>Suche im Landtag ${currentBundesland}...</span>
|
||
</div>
|
||
`;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/search-landtag?q=${encodeURIComponent(query)}&bundesland=${currentBundesland}`);
|
||
const results = await resp.json();
|
||
|
||
if (results.error) {
|
||
document.getElementById('list-content').innerHTML = `
|
||
<p style="padding: 1rem; color: #888;">${results.error}</p>
|
||
`;
|
||
} else if (results.length === 0) {
|
||
document.getElementById('list-content').innerHTML = `
|
||
<p style="padding: 1rem; color: #888;">Keine Treffer im Landtag für "${query}"</p>
|
||
`;
|
||
} else {
|
||
// Merge with checked status from DB
|
||
const checkedIds = new Set(allAssessments.map(a => a.drucksache));
|
||
const merged = results.map(r => ({
|
||
...r,
|
||
gwoeScore: checkedIds.has(r.drucksache)
|
||
? allAssessments.find(a => a.drucksache === r.drucksache)?.gwoeScore
|
||
: null,
|
||
status: checkedIds.has(r.drucksache) ? 'checked' : 'unchecked'
|
||
}));
|
||
renderList(merged);
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('list-content').innerHTML = `
|
||
<p style="padding: 1rem; color: #888;">Suchfehler: ${e.message}</p>
|
||
`;
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = '🔍 Im Landtag';
|
||
}
|
||
|
||
function filterList(query) {
|
||
const q = query.toLowerCase();
|
||
let filtered = allAssessments.filter(a =>
|
||
(a.title || '').toLowerCase().includes(q) ||
|
||
(a.drucksache || '').toLowerCase().includes(q) ||
|
||
(a.fraktionen || []).join(' ').toLowerCase().includes(q) ||
|
||
(a.themen || []).join(' ').toLowerCase().includes(q)
|
||
);
|
||
|
||
// Score-Filter anwenden
|
||
if (currentScoreFilter !== 'all') {
|
||
filtered = applyScoreFilter(filtered, currentScoreFilter);
|
||
}
|
||
|
||
// Partei-Filter anwenden
|
||
if (currentParteiFilter) {
|
||
filtered = filtered.filter(a =>
|
||
(a.fraktionen || []).includes(currentParteiFilter)
|
||
);
|
||
}
|
||
|
||
renderList(filtered);
|
||
}
|
||
|
||
function setScoreFilter(filter, btn) {
|
||
currentScoreFilter = filter;
|
||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
applyAllFilters();
|
||
}
|
||
|
||
function setParteiFilter(partei) {
|
||
currentParteiFilter = partei;
|
||
applyAllFilters();
|
||
}
|
||
|
||
function applyAllFilters() {
|
||
let filtered = allAssessments;
|
||
|
||
// Score-Filter
|
||
if (currentScoreFilter !== 'all') {
|
||
filtered = applyScoreFilter(filtered, currentScoreFilter);
|
||
}
|
||
|
||
// Partei-Filter
|
||
if (currentParteiFilter) {
|
||
filtered = filtered.filter(a =>
|
||
(a.fraktionen || []).includes(currentParteiFilter)
|
||
);
|
||
}
|
||
|
||
renderList(filtered);
|
||
}
|
||
|
||
function applyScoreFilter(items, filter) {
|
||
switch (filter) {
|
||
case 'high': return items.filter(a => a.gwoeScore >= 8);
|
||
case 'mid': return items.filter(a => a.gwoeScore >= 5 && a.gwoeScore < 8);
|
||
case 'low': return items.filter(a => a.gwoeScore < 5);
|
||
default: return items;
|
||
}
|
||
}
|
||
|
||
function showDetail(drucksache) {
|
||
const item = allAssessments.find(a => a.drucksache === drucksache);
|
||
if (!item) return;
|
||
|
||
// Highlight active item
|
||
document.querySelectorAll('.list-item').forEach(el => el.classList.remove('active'));
|
||
// Find and highlight the list item by drucksache
|
||
const listItem = document.querySelector(`.list-item[data-drucksache="${drucksache}"]`);
|
||
if (listItem) listItem.classList.add('active');
|
||
|
||
// Skala 0-10
|
||
const scoreClass = item.gwoeScore >= 8 ? 'score-high' :
|
||
item.gwoeScore >= 5 ? 'score-mid' :
|
||
item.gwoeScore >= 3 ? 'score-low' : 'score-negative';
|
||
|
||
// Matrix als 5x5-Tabelle wie im PDF
|
||
const matrixData = {};
|
||
(item.gwoeMatrix || []).forEach(m => { matrixData[m.field] = m; });
|
||
|
||
// GWÖ-Matrix Definitionen (aus models.py)
|
||
const rowLabels = {
|
||
'A': 'Ausgelagerte Betriebe, Lieferant:innen',
|
||
'B': 'Finanzpartner:innen, Steuerzahler:innen',
|
||
'C': 'Politische Führung, Verwaltung',
|
||
'D': 'Bürger:innen und Wirtschaft',
|
||
'E': 'Staat, Gesellschaft und Natur'
|
||
};
|
||
const rowTooltips = {
|
||
'A': 'Externe Beschaffung, Lieferketten, Dienstleister:innen',
|
||
'B': 'Umgang mit öffentlichen Mitteln, Haushalt, Geldgeber:innen',
|
||
'C': 'Mandatsträger:innen, Mitarbeitende, Ehrenamtliche',
|
||
'D': 'Wirkung innerhalb der Grenzen, Daseinsvorsorge',
|
||
'E': 'Wirkung über die Grenzen hinaus, Zukunft'
|
||
};
|
||
const colLabels = {
|
||
1: 'Menschenwürde',
|
||
2: 'Solidarität',
|
||
3: 'Ökol. Nachhaltigkeit',
|
||
4: 'Soz. Gerechtigkeit',
|
||
5: 'Transparenz'
|
||
};
|
||
const colFull = {
|
||
1: 'Menschenwürde',
|
||
2: 'Solidarität',
|
||
3: 'Ökologische Nachhaltigkeit',
|
||
4: 'Soziale Gerechtigkeit',
|
||
5: 'Transparenz & Mitbestimmung'
|
||
};
|
||
const colPrinzip = {
|
||
1: 'Rechtsstaatsprinzip — Werden Grundrechte geschützt? Rechtliche Gleichstellung?',
|
||
2: 'Gemeinnutz — Wird das Gemeinwohl gefördert? Mehrwert für die Gemeinschaft?',
|
||
3: 'Umwelt-Verantwortung — Klimaschutz? Ressourcenschonung? Biodiversität?',
|
||
4: 'Sozialstaatsprinzip — Gerechte Verteilung? Daseinsvorsorge? Soziale Absicherung?',
|
||
5: 'Demokratie — Bürgerbeteiligung? Offenlegung? Demokratische Prozesse?'
|
||
};
|
||
const fieldLabels = {{ matrix_labels | tojson }};
|
||
const ratingExplain = (r) => r >= 4 ? '++ stark fördernd' : r >= 1 ? '+ fördernd' : r === 0 ? '○ neutral' : r >= -3 ? '− widersprechend' : '−− stark widersprechend';
|
||
|
||
let matrixTableHtml = '<table class="matrix-table"><thead><tr><th style="min-width:120px;"></th>';
|
||
for (let col = 1; col <= 5; col++) matrixTableHtml += `<th title="${colPrinzip[col]}" style="cursor:help;">${colLabels[col]}</th>`;
|
||
matrixTableHtml += '</tr></thead><tbody>';
|
||
|
||
['A', 'B', 'C', 'D', 'E'].forEach(row => {
|
||
matrixTableHtml += `<tr><th title="${rowTooltips[row]}" style="cursor:help;text-align:left;font-size:0.8rem;">${row}: ${rowLabels[row]}</th>`;
|
||
for (let col = 1; col <= 5; col++) {
|
||
const field = `${row}${col}`;
|
||
const entry = matrixData[field];
|
||
const fullLabel = fieldLabels[field] || field;
|
||
if (entry) {
|
||
const cssClass = entry.rating > 0 ? 'positive' : (entry.rating < 0 ? 'negative' : 'neutral');
|
||
const tooltip = `${field}: ${fullLabel}\n\n${entry.aspect || ''}\n\nBewertung: ${entry.rating} (${ratingExplain(entry.rating)})`;
|
||
matrixTableHtml += `<td class="${cssClass}" title="${tooltip.replace(/"/g,'"')}" style="cursor:help;">${entry.symbol}</td>`;
|
||
} else {
|
||
matrixTableHtml += `<td title="${field}: ${fullLabel}\n\n○ Nicht bewertet (Antrag berührt dieses Feld nicht)" style="cursor:help;color:#ccc;">○</td>`;
|
||
}
|
||
}
|
||
matrixTableHtml += '</tr>';
|
||
});
|
||
matrixTableHtml += '</tbody></table>';
|
||
|
||
// Detail-Liste der bewerteten Felder mit vollen Beschreibungen
|
||
const matrixDetailHtml = (item.gwoeMatrix || []).map(m => `
|
||
<div class="matrix-item" title="${ratingExplain(m.rating)}">
|
||
<span class="matrix-label"><strong>${m.field}</strong> ${fieldLabels[m.field] || m.label}: <em>${m.aspect || ''}</em></span>
|
||
<span class="matrix-rating ${m.rating > 0 ? 'rating-pos' : m.rating < 0 ? 'rating-neg' : 'rating-neutral'}">${m.symbol} (${m.rating})</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
const stärkenHtml = (item.stärken || []).map(s => `<li>${s}</li>`).join('');
|
||
const schwächenHtml = (item.schwächen || []).map(s => `<li>${s}</li>`).join('');
|
||
|
||
// Verbesserungsvorschläge formatieren
|
||
const verbesserungenHtml = (item.verbesserungen || []).map(v => {
|
||
// Redline-Format: **fett** = grün/neu, ~~durchgestrichen~~ = rot/gelöscht
|
||
let vorschlag = v.vorschlag || '';
|
||
vorschlag = vorschlag.replace(/\*\*([^*]+)\*\*/g, '<span style="color: #889e33; font-weight: bold;">$1</span>');
|
||
vorschlag = vorschlag.replace(/~~([^~]+)~~/g, '<span style="color: #d00000; text-decoration: line-through;">$1</span>');
|
||
|
||
return `
|
||
<div style="margin: 0.75rem 0; padding: 0.75rem; border: 1px solid var(--color-lightgray); border-radius: 4px;">
|
||
<div style="background: #f5f5f5; padding: 0.5rem; margin-bottom: 0.5rem; font-size: 0.9rem;">
|
||
<strong>Original:</strong><br>${v.original || '-'}
|
||
</div>
|
||
<div style="background: rgba(136, 158, 51, 0.1); border-left: 3px solid #889e33; padding: 0.5rem; font-size: 0.9rem;">
|
||
<strong>Vorschlag:</strong><br>${vorschlag}
|
||
</div>
|
||
<div style="font-size: 0.85rem; color: #666; margin-top: 0.5rem; font-style: italic;">
|
||
${v.begruendung || ''}
|
||
</div>
|
||
</div>
|
||
`}).join('');
|
||
|
||
// Issue #47: Zitat-URLs zu Cite-Endpoint umschreiben für gelbes
|
||
// Highlighting. Funktioniert retroaktiv für Pre-#47-Assessments
|
||
// (statische /static/referenzen/X.pdf#page=N) und nativ für
|
||
// Post-#47 (die schon /api/wahlprogramm-cite enthalten).
|
||
// makeCiteUrl baut die Highlight-URL und hängt ds+bl an,
|
||
// damit der Server bei nicht-auffindbaren Zitaten automatisch
|
||
// eine Re-Analyse triggern kann (#47 + #60).
|
||
function makeCiteUrl(z, ds, bl) {
|
||
if (!z || !z.url) return '#';
|
||
const extra = (ds && bl) ? `&ds=${encodeURIComponent(ds)}&bl=${encodeURIComponent(bl)}` : '';
|
||
// Schon eine Cite-URL? ds/bl anhängen + #page=N.
|
||
if (z.url.includes('/api/wahlprogramm-cite')) {
|
||
const m = z.url.match(/seite=(\d+)/);
|
||
const page = m ? m[1] : '';
|
||
const base = z.url.split('#')[0];
|
||
return base + extra + (page ? '#page=' + page : '');
|
||
}
|
||
// Statische URL umschreiben
|
||
const m = z.url.match(/\/static\/referenzen\/(.+\.pdf)#page=(\d+)/);
|
||
if (m && z.text) {
|
||
const pdf = m[1];
|
||
const page = m[2];
|
||
const q = encodeURIComponent((z.text || '').substring(0, 200));
|
||
return `/api/wahlprogramm-cite?pdf=${encodeURIComponent(pdf)}&seite=${page}&q=${q}${extra}#page=${page}`;
|
||
}
|
||
return z.url;
|
||
}
|
||
|
||
const wahlprogrammHtml = (item.wahlprogrammScores || []).map(wp => {
|
||
// Zitate formatieren mit klickbaren Links + Highlighting
|
||
const zitateHtml = (wp.wahlprogramm?.zitate || []).map(z => {
|
||
const isVerified = z.verified !== false;
|
||
const borderColor = isVerified ? '#889e33' : '#ffc107';
|
||
const bgColor = isVerified ? '#f8f9fa' : '#fffbf0';
|
||
const badge = isVerified
|
||
? '<span style="font-size:0.7rem;color:#889e33;">✓ verifiziert</span>'
|
||
: '<span style="font-size:0.7rem;color:#b8860b;">~ paraphrasiert (nicht wörtlich im Programm)</span>';
|
||
return `
|
||
<div style="margin: 0.5rem 0; padding: 0.5rem; background: ${bgColor}; border-left: 3px ${isVerified ? 'solid' : 'dashed'} ${borderColor}; font-size: 0.85rem;">
|
||
<em>"${z.text}"</em><br>
|
||
${z.quelle ? `<a href="${makeCiteUrl(z, item.drucksache, item.bundesland)}" target="_blank" style="color: #009da5; font-size: 0.8rem;">
|
||
📄 ${z.quelle}
|
||
</a>` : ''}
|
||
${badge}
|
||
</div>`;
|
||
}).join('');
|
||
|
||
// Issue #63: Transparenz-Warnung bei Score > 0 ohne Zitate.
|
||
// Differenziert zwischen "Score 0 = keine Quellen" (LLM hat
|
||
// Force-Honesty befolgt) und "Score > 0 aber 0 Zitate" (LLM
|
||
// hat trotzdem geratet oder Zitate wurden von reconstruct_zitate
|
||
// verworfen). Nur bei Scores > 0 warnen, weil Score 0 schon
|
||
// selbsterklärend ist.
|
||
const wpScore = wp.wahlprogramm?.score ?? 0;
|
||
const wpZitateCount = (wp.wahlprogramm?.zitate || []).length;
|
||
const noQuotesWarning = (wpScore > 0 && wpZitateCount === 0) ? `
|
||
<div style="margin: 0.5rem 0; padding: 0.5rem; background: #fff3cd; border-left: 3px solid #ffc107; font-size: 0.8rem; color: #856404;">
|
||
⚠ Zu diesem Themenkomplex konnten keine konkreten Formulierungen im Wahlprogramm gefunden werden — Score basiert auf der allgemeinen Programmatik der Partei.
|
||
</div>
|
||
` : '';
|
||
|
||
// Labels: Antragsteller:in (aus item.fraktionen) und Landesregierung
|
||
// istAntragsteller/istRegierung aus dem LLM ist oft null — ableiten.
|
||
const isAntragsteller = (item.fraktionen || []).some(f => normalizePartei(f) === normalizePartei(wp.fraktion));
|
||
const roleLabels = [];
|
||
if (wp.istAntragsteller || isAntragsteller) roleLabels.push('<span style="background:#889e33;color:white;padding:0.1rem 0.4rem;border-radius:3px;font-size:0.75rem;">Antragsteller:in</span>');
|
||
if (wp.istRegierung) roleLabels.push('<span style="background:#009da5;color:white;padding:0.1rem 0.4rem;border-radius:3px;font-size:0.75rem;">Landesregierung</span>');
|
||
|
||
return `
|
||
<div style="margin: 0.75rem 0; padding: 0.75rem; background: var(--color-bg); border-radius: 4px;">
|
||
<strong>${wp.fraktion}</strong> ${roleLabels.join(' ')}<br>
|
||
<div style="margin: 0.5rem 0;">
|
||
Wahlprogramm: <strong>${wp.wahlprogramm?.score || '-'}/10</strong> ·
|
||
Parteiprogramm: <strong>${wp.parteiprogramm?.score || '-'}/10</strong>
|
||
</div>
|
||
${wp.wahlprogramm?.begründung ? `<div style="font-size: 0.9rem; color: #555; margin-bottom: 0.5rem;">${wp.wahlprogramm.begründung}</div>` : ''}
|
||
${noQuotesWarning}
|
||
${zitateHtml}
|
||
</div>
|
||
`}).join('');
|
||
|
||
document.getElementById('detail-panel').innerHTML = `
|
||
<div class="detail-card">
|
||
<button class="btn-back-mobile" onclick="document.querySelector('.list-panel').scrollIntoView({behavior:'smooth', block:'start'})">← Zur Liste</button>
|
||
<div class="detail-header">
|
||
<div>
|
||
<div class="detail-title">${item.title || 'Ohne Titel'}</div>
|
||
${item.bundesland && PARLAMENT_NAMES[item.bundesland] ? `<div class="detail-parlament">${PARLAMENT_NAMES[item.bundesland]}</div>` : ''}
|
||
<div class="detail-id">${item.drucksache} · ${(item.fraktionen || []).join(', ')} · ${item.datum || ''}${item.updatedAt ? ` · Bewertet ${new Date(item.updatedAt).toLocaleDateString('de-DE')}` : ''}</div>
|
||
</div>
|
||
<div class="score-display">
|
||
<div class="score-big ${scoreClass}">${item.gwoeScore}</div>
|
||
<div class="score-label">GWÖ-Score</div>
|
||
</div>
|
||
</div>
|
||
|
||
${item.themen && item.themen.length > 0 ? `
|
||
<div class="themen-tags" style="margin-bottom: 1rem;">
|
||
${item.themen.map(t => `<span class="tag">${t}</span>`).join(' ')}
|
||
</div>
|
||
` : ''}
|
||
|
||
<h3 class="section-title">Zusammenfassung</h3>
|
||
<div class="text-block">${item.antragZusammenfassung || '-'}</div>
|
||
|
||
${item.antragKernpunkte && item.antragKernpunkte.length > 0 ? `
|
||
<h3 class="section-title">Kernpunkte</h3>
|
||
<ul class="kernpunkte-list">${item.antragKernpunkte.map(k => `<li>${k}</li>`).join('')}</ul>
|
||
` : ''}
|
||
|
||
<h3 class="section-title">GWÖ-Begründung</h3>
|
||
<div class="text-block">${item.gwoeBegründung || '-'}</div>
|
||
|
||
${item.gwoeSchwerpunkt ? `
|
||
<h3 class="section-title">GWÖ-Schwerpunkt</h3>
|
||
<div class="text-block">${item.gwoeSchwerpunkt}</div>
|
||
` : ''}
|
||
|
||
${item.gwoeMatrix && item.gwoeMatrix.length > 0 ? `
|
||
<h3 class="section-title">GWÖ-Matrix</h3>
|
||
${matrixTableHtml}
|
||
<div class="matrix-grid" style="margin-top: 0.75rem;">${matrixDetailHtml}</div>
|
||
` : ''}
|
||
|
||
${wahlprogrammHtml ? `
|
||
<h3 class="section-title">Programmtreue</h3>
|
||
${wahlprogrammHtml}
|
||
` : ''}
|
||
|
||
${stärkenHtml ? `
|
||
<h3 class="section-title">Stärken</h3>
|
||
<ul class="strength-list">${stärkenHtml}</ul>
|
||
` : ''}
|
||
|
||
${schwächenHtml ? `
|
||
<h3 class="section-title">Schwächen</h3>
|
||
<ul class="weakness-list">${schwächenHtml}</ul>
|
||
` : ''}
|
||
|
||
${verbesserungenHtml ? `
|
||
<h3 class="section-title">Verbesserungsvorschläge</h3>
|
||
${verbesserungenHtml}
|
||
` : ''}
|
||
|
||
${item.verbesserungspotenzial ? `
|
||
<h3 class="section-title">Verbesserungspotenzial</h3>
|
||
<div class="text-block">${item.verbesserungspotenzial}</div>
|
||
` : ''}
|
||
|
||
<h3 class="section-title">Empfehlung</h3>
|
||
<div class="text-block">
|
||
<strong>${item.empfehlungSymbol || ''} ${item.empfehlung || '-'}</strong>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem;">
|
||
<a href="${item.link}" target="_blank" class="btn-pdf">📄 Original-PDF</a>
|
||
<a href="/api/assessment/pdf?drucksache=${encodeURIComponent(item.drucksache)}" class="btn-pdf" style="background: var(--color-green);">📥 GWÖ-Report</a>
|
||
<button class="btn-pdf" style="background: #6c757d; border: none; cursor: pointer;"
|
||
${currentUser ? '' : 'disabled title="Nur nach Anmeldung verfügbar" style="background:#6c757d;opacity:0.5;cursor:not-allowed;"'}
|
||
onclick="reAnalyze('${item.drucksache}', '${item.bundesland}', this)">
|
||
🔄 Neu bewerten
|
||
</button>
|
||
</div>
|
||
<div style="margin-top: 0.75rem; font-size: 0.8rem; color: #999; border-top: 1px solid #eee; padding-top: 0.5rem;">
|
||
Bewertet am ${item.updatedAt ? new Date(item.updatedAt).toLocaleDateString('de-DE') + ', ' + new Date(item.updatedAt).toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit'}) + ' Uhr' : '–'}
|
||
${item.source ? ` · Quelle: ${item.source}` : ''}
|
||
${item.model ? ` · Modell: ${item.model}` : ''}
|
||
</div>
|
||
|
||
<!-- Bookmark + Kommentare (#94) -->
|
||
<div style="margin-top: 1rem; border-top: 2px solid var(--color-lightgray); padding-top: 1rem;">
|
||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
|
||
<button id="bookmark-btn-${item.drucksache.replace('/','-')}"
|
||
style="background:none;border:1px solid #ddd;border-radius:4px;padding:0.3rem 0.8rem;cursor:pointer;font-size:0.9rem;"
|
||
onclick="toggleBookmark('${item.drucksache}', this)"
|
||
${currentUser ? '' : 'disabled title="Nur nach Anmeldung"'}>
|
||
🔖 Merken
|
||
</button>
|
||
<span style="font-size: 0.85rem; color: #888;">Kommentare:</span>
|
||
</div>
|
||
<div id="comments-${item.drucksache.replace('/','-')}" style="margin-bottom: 0.75rem;">
|
||
<span style="color:#aaa;font-size:0.85rem;">Lade Kommentare...</span>
|
||
</div>
|
||
${currentUser ? `
|
||
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
|
||
<input id="comment-input-${item.drucksache.replace('/','-')}" type="text"
|
||
placeholder="Kommentar schreiben..."
|
||
style="flex:1;min-width:150px;padding:0.4rem;border:1px solid #ddd;border-radius:4px;font-size:0.85rem;"
|
||
onkeydown="if(event.key==='Enter')addCommentUI('${item.drucksache}')">
|
||
<select id="comment-visibility-${item.drucksache.replace('/','-')}"
|
||
style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;font-size:0.8rem;color:#666;">
|
||
<option value="all">🌐 Öffentlich</option>
|
||
<option value="authenticated">🔒 Angemeldete</option>
|
||
<option value="private">👤 Nur ich</option>
|
||
</select>
|
||
<button onclick="addCommentUI('${item.drucksache}')"
|
||
style="padding:0.4rem 0.8rem;background:var(--color-blue);color:white;border:none;border-radius:4px;cursor:pointer;font-size:0.85rem;">
|
||
Senden
|
||
</button>
|
||
</div>
|
||
` : '<span style="font-size:0.8rem;color:#aaa;">Anmelden um zu kommentieren</span>'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Kommentare + Bookmark-Status laden
|
||
loadComments('${item.drucksache}');
|
||
loadBookmarkState('${item.drucksache}');
|
||
|
||
// Auf Mobile: zum Detail-Panel scrollen, damit der gewählte Antrag sichtbar wird
|
||
if (window.matchMedia('(max-width: 900px)').matches) {
|
||
document.getElementById('detail-panel').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
}
|
||
|
||
// Mode Toggle
|
||
function showMode(mode) {
|
||
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
|
||
if (event && event.currentTarget) {
|
||
event.currentTarget.classList.add('active');
|
||
} else {
|
||
// Find and activate the right button
|
||
document.querySelectorAll('.mode-btn').forEach(b => {
|
||
if (b.textContent.includes(mode === 'browse' ? 'Durchsuchen' : mode === 'tags' ? 'Tags' : 'Prüfen')) {
|
||
b.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
document.getElementById('browse-mode').style.display = mode === 'browse' ? 'flex' : 'none';
|
||
document.getElementById('bookmarks-mode').style.display = mode === 'bookmarks' ? 'flex' : 'none';
|
||
document.getElementById('tags-mode').style.display = mode === 'tags' ? 'flex' : 'none';
|
||
document.getElementById('upload-mode').style.display = mode === 'upload' ? 'flex' : 'none';
|
||
if (mode === 'bookmarks') loadBookmarksList();
|
||
}
|
||
|
||
// Upload Tab Toggle
|
||
function showTab(tab) {
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||
event.currentTarget.classList.add('active');
|
||
|
||
document.getElementById('tab-text').style.display = tab === 'text' ? 'block' : 'none';
|
||
document.getElementById('tab-file').style.display = tab === 'file' ? 'block' : 'none';
|
||
}
|
||
|
||
function handleFile(input) {
|
||
if (input.files[0]) {
|
||
document.getElementById('file-name').textContent = input.files[0].name;
|
||
}
|
||
}
|
||
|
||
async function startAnalysis() {
|
||
const btn = document.getElementById('analyze-btn');
|
||
const statusDiv = document.getElementById('analysis-status');
|
||
const resultDiv = document.getElementById('analysis-result');
|
||
|
||
const text = document.getElementById('antrag-text').value;
|
||
const file = document.getElementById('file-input').files[0];
|
||
const bundesland = document.getElementById('bundesland').value;
|
||
|
||
if (!text && !file) {
|
||
alert('Bitte Text eingeben oder PDF hochladen');
|
||
return;
|
||
}
|
||
if (!bundesland) {
|
||
alert('Bitte ein Bundesland wählen.');
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
statusDiv.style.display = 'block';
|
||
resultDiv.style.display = 'none';
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
if (text) formData.append('text', text);
|
||
if (file) formData.append('file', file);
|
||
formData.append('bundesland', bundesland);
|
||
formData.append('model', 'qwen-plus');
|
||
|
||
const resp = await fetch('/analyze', { method: 'POST', body: formData });
|
||
const data = await resp.json();
|
||
|
||
if (data.job_id) {
|
||
pollStatus(data.job_id);
|
||
} else {
|
||
throw new Error(data.detail || 'Fehler');
|
||
}
|
||
} catch (e) {
|
||
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: ${e.message}</p>`;
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function pollStatus(jobId) {
|
||
const statusText = document.getElementById('status-text');
|
||
const statusDiv = document.getElementById('analysis-status');
|
||
const resultDiv = document.getElementById('analysis-result');
|
||
const btn = document.getElementById('analyze-btn');
|
||
|
||
try {
|
||
const resp = await fetch(`/status/${jobId}`);
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'completed') {
|
||
statusDiv.style.display = 'none';
|
||
resultDiv.innerHTML = `
|
||
<p style="color: var(--color-green);">✓ Analyse abgeschlossen!</p>
|
||
<a href="/result/${jobId}" class="btn-pdf" style="background: var(--color-green);">📊 Ergebnis ansehen</a>
|
||
<a href="/result/${jobId}/pdf" class="btn-pdf">📄 PDF herunterladen</a>
|
||
`;
|
||
resultDiv.style.display = 'block';
|
||
btn.disabled = false;
|
||
} else if (data.status === 'failed') {
|
||
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: Analyse fehlgeschlagen. Bitte erneut versuchen.</p>`;
|
||
btn.disabled = false;
|
||
} else {
|
||
statusText.textContent = `Analysiere... (${data.status})`;
|
||
setTimeout(() => pollStatus(jobId), 2000);
|
||
}
|
||
} catch (e) {
|
||
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: ${e.message}</p>`;
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|