constantinSch's picture
Refactor RFA filter population in loadEntries function
1539de0
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zusammenfassungs-Evaluation</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; color: #222; }
header {
background: #1a1a2e; color: #fff; padding: 12px 24px;
display: flex; align-items: center; gap: 24px; flex-wrap: wrap;
position: sticky; top: 0; z-index: 10;
}
header h1 { font-size: 17px; font-weight: 600; white-space: nowrap; }
.btn-export {
padding: 4px 12px; border: 1px solid rgba(255,255,255,.3); border-radius: 4px;
background: transparent; color: #fff; font-size: 12px; cursor: pointer;
}
.btn-export:hover { background: rgba(255,255,255,.1); }
.progress-box { margin-left: auto; font-size: 13px; opacity: .85; display: flex; align-items: center; gap: 10px; }
.progress-bar { width: 120px; height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; background: #22c55e; transition: width .3s; }
.nav {
display: flex; align-items: center; gap: 12px;
padding: 10px 24px; background: #fff; border-bottom: 1px solid #ddd;
position: sticky; top: 52px; z-index: 9; flex-wrap: wrap;
}
.nav button {
padding: 6px 16px; border: 1px solid #ccc; border-radius: 4px;
background: #fff; cursor: pointer; font-size: 14px;
}
.nav button:hover { background: #eee; }
.nav button:disabled { opacity: .4; cursor: default; }
.nav .counter { font-size: 14px; font-weight: 600; min-width: 80px; text-align: center; }
.filters { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.filters label { font-size: 12px; font-weight: 600; color: #666; }
.filters select { padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; }
.card {
max-width: 960px; margin: 20px auto; background: #fff;
border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1); overflow: hidden;
}
.card-header {
padding: 14px 20px; border-bottom: 1px solid #eee;
display: flex; justify-content: space-between; align-items: center; gap: 12px;
}
.card-header h2 { font-size: 15px; font-weight: 600; }
.card-header .meta { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.card-header .id { font-size: 12px; color: #888; font-family: monospace; }
.badge {
padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600;
}
.badge-DRA { background: #ecfdf5; color: #065f46; }
.badge-DW { background: #eff6ff; color: #1e40af; }
.badge-NDR { background: #fef3c7; color: #92400e; }
.section { padding: 14px 20px; border-bottom: 1px solid #f0f0f0; }
.section-label {
font-size: 11px; font-weight: 700; color: #666;
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px;
}
.section-content { font-size: 14px; line-height: 1.65; white-space: pre-wrap; }
.summary-section {
background: #eff6ff; border-left: 4px solid #2563eb;
}
.summary-section .section-content { font-size: 15px; font-weight: 500; }
.transcript-section .section-content {
max-height: 260px; overflow-y: auto; font-size: 13px; color: #444;
}
.original-toggle {
padding: 8px 20px; background: #f8f8f8; border-bottom: 1px solid #f0f0f0;
cursor: pointer; display: flex; align-items: center; gap: 8px;
font-size: 12px; font-weight: 600; color: #666; user-select: none;
}
.original-toggle:hover { background: #f0f0f0; }
.original-toggle .arrow { transition: transform .2s; display: inline-block; }
.original-toggle .arrow.open { transform: rotate(90deg); }
.original-transcript {
display: none; padding: 14px 20px; border-bottom: 1px solid #f0f0f0;
}
.original-transcript.open { display: block; }
.original-transcript .section-content {
max-height: 260px; overflow-y: auto; font-size: 13px; color: #444;
white-space: pre-wrap; line-height: 1.65;
}
/* --- Evaluation controls --- */
.eval-section {
padding: 20px; border-bottom: 1px solid #f0f0f0;
border: 2px solid #2563eb; border-radius: 0 0 0 0;
margin: 0; background: #fafbff;
}
.eval-section .section-label { font-size: 13px; margin-bottom: 12px; color: #1e40af; }
.rating-row { margin-bottom: 16px; }
.rating-row label { font-size: 13px; font-weight: 600; color: #444; display: block; margin-bottom: 4px; }
.rating-row select { padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
.criteria { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 16px; }
.criterion { display: flex; flex-direction: column; gap: 4px; min-width: 120px; }
.criterion label { font-size: 12px; font-weight: 700; color: #555; text-transform: uppercase; }
.criterion select {
padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;
}
.criterion select.val-ja { border-color: #22c55e; background: #f0fdf4; }
.criterion select.val-aus { border-color: #f59e0b; background: #fffbeb; }
.criterion select.val-nein { border-color: #ef4444; background: #fef2f2; }
.criterion select.val-none { border-color: #ccc; background: #f9fafb; }
.criterion-desc {
font-size: 11px; color: #777; font-weight: 400; line-height: 1.4; margin-bottom: 2px;
}
/* Collapsible guidelines */
.guidelines-toggle {
padding: 10px 20px; background: #f0f4ff; border-bottom: 1px solid #d0d8f0;
cursor: pointer; display: flex; align-items: center; gap: 8px;
font-size: 13px; font-weight: 600; color: #1e40af; user-select: none;
}
.guidelines-toggle:hover { background: #e0e8ff; }
.guidelines-toggle .arrow { transition: transform .2s; display: inline-block; }
.guidelines-toggle .arrow.open { transform: rotate(90deg); }
.guidelines-body {
display: none; padding: 14px 20px; background: #f8f9ff; border-bottom: 1px solid #d0d8f0;
font-size: 13px; line-height: 1.7; color: #444;
}
.guidelines-body.open { display: block; }
.guidelines-body h3 { font-size: 13px; font-weight: 700; color: #1e40af; margin: 12px 0 4px 0; }
.guidelines-body h3:first-child { margin-top: 0; }
.guidelines-body ul { margin: 4px 0 8px 18px; }
.guidelines-body li { margin-bottom: 2px; }
.guidelines-body .scale-label { font-weight: 600; }
.comments-row { margin-top: 4px; }
.comments-row label { font-size: 12px; font-weight: 700; color: #555; text-transform: uppercase; display: block; margin-bottom: 4px; }
.comments-row textarea {
width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px;
font-family: inherit; font-size: 14px; line-height: 1.5; resize: vertical;
}
.comments-row textarea:focus { outline: 2px solid #2563eb; border-color: #2563eb; }
.actions { padding: 14px 20px; display: flex; justify-content: flex-end; gap: 12px; align-items: center; }
.save-hint { font-size: 12px; color: #888; margin-right: auto; }
.btn-save {
padding: 8px 24px; border: none; border-radius: 4px;
background: #2563eb; color: #fff; font-size: 14px; cursor: pointer; font-weight: 500;
}
.btn-save:hover { background: #1d4ed8; }
.btn-save:disabled { opacity: .4; cursor: default; }
.toast {
position: fixed; bottom: 24px; right: 24px; padding: 12px 20px;
border-radius: 6px; color: #fff; font-size: 14px;
opacity: 0; transition: opacity .3s; pointer-events: none;
}
.toast.show { opacity: 1; }
.toast.success { background: #22c55e; }
.toast.error { background: #ef4444; }
.empty-state {
text-align: center; padding: 60px 20px; color: #666; font-size: 15px;
}
/* Login overlay */
.login-overlay {
position: fixed; inset: 0; background: #f5f5f5; z-index: 100;
display: flex; align-items: center; justify-content: center;
}
.login-overlay.hidden { display: none; }
.login-box {
max-width: 320px; width: 100%; background: #fff;
padding: 32px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1);
text-align: center;
}
.login-box h2 { margin-bottom: 16px; font-size: 20px; }
.login-box input {
padding: 10px; width: 100%; border: 1px solid #ccc; border-radius: 4px;
font-size: 15px; margin-bottom: 12px; box-sizing: border-box;
}
.login-box button {
padding: 10px 24px; border: none; border-radius: 4px;
background: #2563eb; color: #fff; font-size: 15px; cursor: pointer;
}
.login-box button:hover { background: #1d4ed8; }
.login-error { color: #dc2626; font-size: 13px; margin-bottom: 8px; }
.badge-evaluated {
background: #dcfce7; color: #166534; padding: 3px 10px;
border-radius: 12px; font-size: 12px; font-weight: 600;
}
.badge-promptd {
background: #fef3c7; color: #92400e; padding: 3px 10px;
border-radius: 12px; font-size: 12px; font-weight: 600;
}
.reference-banner {
padding: 10px 20px; background: #fef9c3; border-bottom: 1px solid #fde68a;
font-size: 13px; color: #92400e; font-weight: 500;
}
.reference-banner strong { font-weight: 700; }
.prior-judgement { padding: 14px 20px; border-bottom: 1px solid #f0f0f0; background: #fffbeb; }
.prior-judgement .section-label { color: #92400e; }
.prior-judgement .pj-grid {
display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; margin-top: 6px;
}
.prior-judgement .pj-item { display: flex; flex-direction: column; gap: 2px; }
.prior-judgement .pj-label { font-size: 11px; font-weight: 700; color: #92400e; text-transform: uppercase; }
.prior-judgement .pj-value { font-weight: 600; }
</style>
</head>
<body>
<div class="login-overlay" id="login-overlay">
<div class="login-box">
<h2>Zusammenfassungs-Evaluation</h2>
<p class="login-error" id="login-error" style="display:none">Falsches Passwort.</p>
<form onsubmit="handleLogin(event)">
<input type="password" id="login-password" placeholder="Passwort" autofocus>
<button type="submit">Weiter</button>
</form>
</div>
</div>
<header>
<h1>Zusammenfassungs-Evaluation</h1>
<button class="btn-export" onclick="exportAnnotations()">Export</button>
<div class="progress-box">
<span id="progress-text">-</span>
<div class="progress-bar"><div class="progress-fill" id="progress-fill" style="width:0"></div></div>
</div>
</header>
<div class="nav">
<button id="btn-prev" onclick="navigate(-1)">Zurueck</button>
<span class="counter" id="counter">-</span>
<button id="btn-next" onclick="navigate(1)">Weiter</button>
<div class="filters">
<label>Bearbeitungsstatus:</label>
<select id="filter-status" onchange="applyFilters()">
<option value="">alle</option>
<option value="pending" selected>offen</option>
<option value="done">bewertet</option>
</select>
<label>LRA:</label>
<select id="filter-rfa" onchange="applyFilters()">
<option value="">alle</option>
</select>
<label>Bewertungsrunde:</label>
<select id="filter-type" onchange="applyFilters()">
<option value="">alle</option>
<option value="new" selected>Neu (zu bewerten)</option>
<option value="promptd">Alter Prompt (bereits evaluiert)</option>
</select>
</div>
</div>
<div class="card" id="card">
<div class="card-header">
<h2 id="title">-</h2>
<div class="meta">
<span class="badge" id="badge-rfa"></span>
<span class="badge-evaluated" id="badge-evaluated" style="display:none">Bewertet</span>
<span class="badge-promptd" id="badge-promptd" style="display:none">Alter Prompt</span>
<span class="id" id="eval-id">-</span>
</div>
</div>
<div class="reference-banner" id="reference-banner" style="display:none">
<strong>Alter Prompt:</strong> Dieser Eintrag wurde bereits in einer frueheren Evaluation bewertet. Die bestehende Bewertung wird unten angezeigt.
</div>
<div class="prior-judgement" id="prior-judgement" style="display:none">
<div class="section-label">Bestehende Bewertung (Alter Prompt)</div>
<div class="pj-grid" id="pj-grid"></div>
<div id="pj-anmerkungen" style="margin-top:8px; font-size:13px; color:#444;"></div>
</div>
<div class="section summary-section">
<div class="section-label">Zusammenfassung</div>
<div class="section-content" id="summary">-</div>
</div>
<div class="section">
<div class="section-label">Referenz</div>
<div class="section-content" id="referenz">-</div>
</div>
<div class="section transcript-section">
<div class="section-label" id="transcript-label">Transkript</div>
<div class="section-content" id="transcript">-</div>
</div>
<div class="original-toggle" id="original-toggle" style="display:none" onclick="toggleOriginal()">
<span class="arrow" id="original-arrow">&#9654;</span>
Original-Transkript (Fremdsprache) anzeigen
</div>
<div class="original-transcript" id="original-transcript">
<div class="section-label">Original-Transkript</div>
<div class="section-content" id="original-text">-</div>
</div>
<div class="guidelines-toggle" onclick="toggleGuidelines()">
<span class="arrow" id="guidelines-arrow">&#9654;</span>
Bewertungsrichtlinien anzeigen
</div>
<div class="guidelines-body" id="guidelines-body">
<h3>Vorgehensweise</h3>
<p>Lies das Transkript vollstaendig (oder ueberfliege es gruendlich). Lies dann die Zusammenfassung und waehle fuer jedes Kriterium eine Stufe (ja / ausreichend / nein). Vergib abschliessend eine Gesamtbewertung als Schulnote.</p>
<h3>Stufen</h3>
<ul>
<li><span class="scale-label">Ja</span> -- Perfekt oder nahezu perfekt, keine nennenswerten Schwaechen</li>
<li><span class="scale-label">Ausreichend</span> -- Mit Schwaechen, aber noch brauchbar</li>
<li><span class="scale-label">Nein</span> -- Inakzeptabel, voellig unbrauchbar</li>
</ul>
<h3>1. Korrekt</h3>
<p>Faktische Korrektheit: Werden Fakten, Namen und Orte korrekt wiedergegeben? Gibt es Halluzinationen oder Informationen, die nicht im Transkript vorkommen?</p>
<ul>
<li><span class="scale-label">Ja:</span> Alle Aussagen faktisch korrekt und durch das Transkript gestuetzt. Keine Halluzinationen.</li>
<li><span class="scale-label">Ausreichend:</span> Einige Ungenauigkeiten, Kernaussagen stimmen groesstenteils.</li>
<li><span class="scale-label">Nein:</span> Faktische Fehler und Halluzinationen. Ueberwiegend falsche Informationen.</li>
</ul>
<h3>2. Relevant</h3>
<p>Auswahl der bedeutsamen Inhalte: Enthaelt die Zusammenfassung nur wichtige Informationen? Gibt es unnoetige Wiederholungen?</p>
<ul>
<li><span class="scale-label">Ja:</span> Konzentriert sich ausschliesslich auf die wichtigsten Informationen. Keine Redundanzen.</li>
<li><span class="scale-label">Ausreichend:</span> Enthaelt auch weniger relevante Informationen. Einige unwichtige Details.</li>
<li><span class="scale-label">Nein:</span> Konzentriert sich auf nebensaechliche Aspekte statt Kernthemen.</li>
</ul>
<h3>3. Vollstaendig</h3>
<p>Sind alle wesentlichen Fakten, Kernaussagen und relevanten Entitaeten enthalten?</p>
<ul>
<li><span class="scale-label">Ja:</span> Alle wesentlichen Informationen und Kernaussagen enthalten. Wichtige Entitaeten vollstaendig erfasst.</li>
<li><span class="scale-label">Ausreichend:</span> Grundlegender Ueberblick, aber einzelne bedeutsame Aspekte fehlen.</li>
<li><span class="scale-label">Nein:</span> Wichtige Kernaussagen oder zentrale Entitaeten nicht erfasst. Zu lueckenhaft.</li>
</ul>
<h3>4. Kohaerent</h3>
<p>Ist die Zusammenfassung gut strukturiert und logisch nachvollziehbar? Bilden die Saetze einen zusammenhaengenden Text?</p>
<ul>
<li><span class="scale-label">Ja:</span> Strukturiert und organisiert. Saetze bauen logisch aufeinander auf.</li>
<li><span class="scale-label">Ausreichend:</span> Grundsaetzlich verstaendlich, aber stellenweise sprunghaft.</li>
<li><span class="scale-label">Nein:</span> Unstrukturiert, Zusammenhang nur schwer nachvollziehbar.</li>
</ul>
<h3>Gesamtbewertung</h3>
<p>Schulnote 1 (sehr gut) bis 6 (ungenuegend). Bezieht sich auf den generellen Eindruck, wie gut das Transkript zusammengefasst wurde. Muss kein Durchschnitt der Kriterien sein -- z.B. kann fehlende Korrektheit staerker wiegen als fehlende Kohaerenz.</p>
<h3>Anmerkungen</h3>
<p>Optional: Besondere Fehler, Staerken, Halluzinationen, Begruendungen oder konkrete Verbesserungsvorschlaege fuer den Prompt.</p>
</div>
<div class="eval-section">
<div class="section-label">Bewertung</div>
<div class="rating-row">
<label>Gesamtbewertung (1 = sehr gut, 6 = ungenuegend)</label>
<select id="bewertung" onchange="markDirty()">
<option value="">(bitte waehlen)</option>
<option value="1">1 - sehr gut</option>
<option value="2">2 - gut</option>
<option value="3">3 - befriedigend</option>
<option value="4">4 - ausreichend</option>
<option value="5">5 - mangelhaft</option>
<option value="6">6 - ungenuegend</option>
</select>
</div>
<div class="criteria" id="criteria"></div>
<div class="comments-row">
<label for="anmerkungen">Anmerkungen</label>
<textarea id="anmerkungen" rows="3" placeholder="Optionale Anmerkungen..." oninput="markDirty()"></textarea>
</div>
</div>
<div class="actions">
<span class="save-hint" id="save-hint"></span>
<button class="btn-save" id="btn-save" onclick="saveAnnotation()" disabled>Speichern</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const CRITERIA = ["korrekt", "relevant", "vollstaendig", "kohaerenz"];
const CRITERIA_LABEL = {
korrekt: "Korrekt",
relevant: "Relevant",
vollstaendig: "Vollstaendig",
kohaerenz: "Kohaerent",
};
const CRITERIA_DESC = {
korrekt: "Faktisch korrekt? Keine Halluzinationen?",
relevant: "Nur wichtige Informationen? Keine Redundanzen?",
vollstaendig: "Alle wesentlichen Fakten und Entitaeten enthalten?",
kohaerenz: "Gut strukturiert und logisch nachvollziehbar?",
};
let allRows = [];
let filteredRows = [];
let currentIdx = 0;
let dirty = false;
/* ---------- Auth ---------- */
function getAuthToken() {
return localStorage.getItem("auth_token") || "";
}
function authHeaders() {
const token = getAuthToken();
const h = {};
if (token) h["Authorization"] = "Bearer " + token;
return h;
}
async function handleLogin(e) {
e.preventDefault();
const pw = document.getElementById("login-password").value;
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: pw }),
});
if (res.ok) {
const data = await res.json();
localStorage.setItem("auth_token", data.token);
document.getElementById("login-overlay").classList.add("hidden");
loadEntries();
} else {
document.getElementById("login-error").style.display = "";
}
}
async function checkAuth() {
const res = await fetch("/api/progress", { headers: authHeaders() });
if (res.status === 401) {
document.getElementById("login-overlay").classList.remove("hidden");
} else {
document.getElementById("login-overlay").classList.add("hidden");
loadEntries();
}
}
/* ---------- Data loading ---------- */
async function loadEntries() {
const res = await fetch("/api/entries", { headers: authHeaders() });
if (!res.ok) { allRows = []; applyFilters(); return; }
allRows = await res.json();
populateRfaFilter();
applyFilters();
refreshProgress();
}
function populateRfaFilter() {
const values = [...new Set(allRows.map(r => r.rfa).filter(Boolean))].sort();
const select = document.getElementById("filter-rfa");
select.innerHTML = '<option value="">alle</option>';
values.forEach(val => {
const opt = document.createElement("option");
opt.value = val;
opt.textContent = val;
select.appendChild(opt);
});
}
function applyFilters() {
if (dirty && !confirm("Ungespeicherte Aenderungen verwerfen?")) return;
dirty = false;
const status = document.getElementById("filter-status").value;
const rfa = document.getElementById("filter-rfa").value;
filteredRows = [...allRows];
if (status === "pending") {
filteredRows = filteredRows.filter(r => !r.evaluated);
} else if (status === "done") {
filteredRows = filteredRows.filter(r => r.evaluated);
}
if (rfa) {
filteredRows = filteredRows.filter(r => r.rfa === rfa);
}
const typeFilter = document.getElementById("filter-type").value;
if (typeFilter === "promptd") {
filteredRows = filteredRows.filter(r => r.has_prior_judgement);
} else if (typeFilter === "new") {
filteredRows = filteredRows.filter(r => !r.has_prior_judgement);
}
currentIdx = 0;
render();
}
async function refreshProgress() {
const res = await fetch("/api/progress", { headers: authHeaders() });
if (!res.ok) return;
const p = await res.json();
document.getElementById("progress-text").textContent = p.annotated + " / " + p.total;
const pct = p.total ? Math.round(100 * p.annotated / p.total) : 0;
document.getElementById("progress-fill").style.width = pct + "%";
}
/* ---------- Rendering ---------- */
function render() {
const card = document.getElementById("card");
if (filteredRows.length === 0) {
card.innerHTML = '<div class="empty-state">Keine Eintraege gefunden.</div>';
document.getElementById("counter").textContent = "0 / 0";
return;
}
// Ensure card structure is present (may have been replaced by empty state)
if (!document.getElementById("title")) { location.reload(); return; }
const row = filteredRows[currentIdx];
document.getElementById("counter").textContent = (currentIdx + 1) + " / " + filteredRows.length;
document.getElementById("btn-prev").disabled = currentIdx === 0;
document.getElementById("btn-next").disabled = currentIdx === filteredRows.length - 1;
document.getElementById("eval-id").textContent = row.eval_id;
document.getElementById("title").textContent =
(row.sende_haupttitel || "") + " \u2013 " + (row.beitragstitel || "");
const badge = document.getElementById("badge-rfa");
badge.textContent = row.rfa;
badge.className = "badge badge-" + row.rfa;
document.getElementById("badge-evaluated").style.display = row.evaluated ? "" : "none";
const isPromptD = row.has_prior_judgement;
document.getElementById("badge-promptd").style.display = isPromptD ? "" : "none";
document.getElementById("reference-banner").style.display = isPromptD ? "" : "none";
const pjEl = document.getElementById("prior-judgement");
if (isPromptD) {
pjEl.style.display = "";
const grid = document.getElementById("pj-grid");
grid.innerHTML =
'<div class="pj-item"><span class="pj-label">Gesamt</span><span class="pj-value">' + (row.prior_bewertung || "-") + '</span></div>' +
CRITERIA.map(c =>
'<div class="pj-item"><span class="pj-label">' + CRITERIA_LABEL[c] +
'</span><span class="pj-value">' + (row["prior_" + c] || "-") + '</span></div>'
).join("");
const pjAnm = document.getElementById("pj-anmerkungen");
pjAnm.textContent = row.prior_anmerkungen ? "Anmerkungen: " + row.prior_anmerkungen : "";
} else {
pjEl.style.display = "none";
}
document.getElementById("summary").textContent = row.summary || "-";
document.getElementById("referenz").textContent = row.referenz || "-";
const hasTranslation = row.rfa === "DW" && row.translation;
if (hasTranslation) {
document.getElementById("transcript-label").textContent = "Transkript (Uebersetzung)";
document.getElementById("transcript").textContent = row.translation;
document.getElementById("original-toggle").style.display = "";
document.getElementById("original-text").textContent = row.transkript || "-";
document.getElementById("original-transcript").classList.remove("open");
document.getElementById("original-arrow").classList.remove("open");
} else {
document.getElementById("transcript-label").textContent = "Transkript";
document.getElementById("transcript").textContent = row.transkript || "-";
document.getElementById("original-toggle").style.display = "none";
document.getElementById("original-transcript").classList.remove("open");
}
// Bewertung dropdown
document.getElementById("bewertung").value = row.bewertung || "";
// Criteria
const criteriaEl = document.getElementById("criteria");
criteriaEl.innerHTML = "";
for (const c of CRITERIA) {
const val = (row[c] || "").toLowerCase();
const div = document.createElement("div");
div.className = "criterion";
div.innerHTML =
'<label>' + CRITERIA_LABEL[c] + '</label>' +
'<span class="criterion-desc">' + CRITERIA_DESC[c] + '</span>' +
'<select data-criterion="' + c + '" onchange="onCriterionChange(this)">' +
'<option value=""' + (!val ? ' selected' : '') + '>(bitte waehlen)</option>' +
'<option value="ja"' + (val === 'ja' ? ' selected' : '') + '>ja</option>' +
'<option value="ausreichend"' + (val === 'ausreichend' ? ' selected' : '') + '>ausreichend</option>' +
'<option value="nein"' + (val === 'nein' ? ' selected' : '') + '>nein</option>' +
'</select>';
const sel = div.querySelector("select");
applyCriterionStyle(sel);
criteriaEl.appendChild(div);
}
// Anmerkungen
document.getElementById("anmerkungen").value = row.anmerkungen || "";
// Save hint
document.getElementById("save-hint").textContent = row.bewertung ? "Bereits bewertet" : "";
dirty = false;
document.getElementById("btn-save").disabled = true;
}
function applyCriterionStyle(sel) {
const v = sel.value;
sel.className = v === "ja" ? "val-ja" : v === "ausreichend" ? "val-aus" : v === "nein" ? "val-nein" : "val-none";
}
/* ---------- Interaction ---------- */
function markDirty() {
dirty = true;
document.getElementById("btn-save").disabled = false;
document.getElementById("save-hint").textContent = "";
}
function onCriterionChange(sel) {
applyCriterionStyle(sel);
markDirty();
}
function navigate(dir) {
if (dirty && !confirm("Ungespeicherte Aenderungen verwerfen?")) return;
dirty = false;
currentIdx = Math.max(0, Math.min(filteredRows.length - 1, currentIdx + dir));
render();
}
async function saveAnnotation() {
const row = filteredRows[currentIdx];
const data = {
eval_id: row.eval_id,
bewertung: document.getElementById("bewertung").value || null,
anmerkungen: document.getElementById("anmerkungen").value || null,
};
document.querySelectorAll("#criteria select").forEach(sel => {
data[sel.dataset.criterion] = sel.value || null;
});
const res = await fetch("/api/annotate", {
method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders() },
body: JSON.stringify(data),
});
if (res.ok) {
// Update local row data
for (const key of ["bewertung", "korrekt", "relevant", "vollstaendig", "kohaerenz", "anmerkungen"]) {
row[key] = data[key];
}
row.evaluated = true;
// Also update in allRows
const src = allRows.find(r => r.eval_id === row.eval_id);
if (src) { Object.assign(src, data); src.evaluated = true; }
dirty = false;
document.getElementById("btn-save").disabled = true;
document.getElementById("save-hint").textContent = "Gespeichert";
showToast("Gespeichert", "success");
refreshProgress();
} else {
showToast("Fehler beim Speichern", "error");
}
}
/* ---------- Utilities ---------- */
function showToast(msg, type) {
const el = document.getElementById("toast");
el.textContent = msg;
el.className = "toast " + type + " show";
setTimeout(() => el.classList.remove("show"), 2000);
}
document.addEventListener("keydown", e => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
e.preventDefault();
if (!document.getElementById("btn-save").disabled) saveAnnotation();
return;
}
if (e.target.tagName === "SELECT" || e.target.tagName === "TEXTAREA") return;
if (e.key === "ArrowLeft") navigate(-1);
if (e.key === "ArrowRight") navigate(1);
});
/* ---------- Guidelines toggle ---------- */
function toggleGuidelines() {
const body = document.getElementById("guidelines-body");
const arrow = document.getElementById("guidelines-arrow");
body.classList.toggle("open");
arrow.classList.toggle("open");
}
function toggleOriginal() {
const body = document.getElementById("original-transcript");
const arrow = document.getElementById("original-arrow");
body.classList.toggle("open");
arrow.classList.toggle("open");
}
/* ---------- Export ---------- */
async function exportAnnotations() {
const res = await fetch("/api/export", { headers: authHeaders() });
if (!res.ok) { showToast("Export fehlgeschlagen", "error"); return; }
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "annotations.jsonl";
a.click();
URL.revokeObjectURL(url);
}
/* ---------- Init ---------- */
checkAuth();
</script>
</body>
</html>