refactor(klassifikation): Klassifikation aus Monitor entfernt — Pflege jetzt in der Verwaltung
Endpoints unter /api/sources/classification/* weg, Service-Module (source_classifier, external_reputation) gelöscht. Quellen-Modal verliert Tab Klassifikations-Review, Klassifikations-Section in der Edit-Form, alle Bulk-Buttons (Sync, Klassifikation starten, Bulk-Approve). API-Methoden in api.js entfernt, alignment-Helper raus, saveSource entschlackt. Read-Only bleibt: Filter-Dropdowns über der Quellenliste (Politik, Medientyp, Reliability, Externe Reputation, Alignment) und Inline-Badges (_renderClassificationBadges + Label-Maps in components.js). Kunde sieht nur freigegebene Werte. GET /api/sources liefert weiter Klassifikations-Felder + alignments für die Anzeige; SourceCreate/SourceUpdate akzeptieren keine Klassifikations-Felder mehr. Bulk-Klassifikations-Skripte entfernt — Pflege läuft über Verwaltungs-UI.
Dieser Commit ist enthalten in:
12519
src/static/css/style.css
12519
src/static/css/style.css
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -456,15 +456,6 @@
|
||||
<!-- Stats-Leiste -->
|
||||
<div class="sources-stats-bar" id="sources-stats-bar"></div>
|
||||
|
||||
<!-- Tabs: Liste vs. Klassifikations-Review -->
|
||||
<div class="sources-tabs" role="tablist">
|
||||
<button type="button" class="sources-tab active" id="sources-tab-list" role="tab" aria-selected="true" onclick="App.switchSourcesTab('list')">Quellenliste</button>
|
||||
<button type="button" class="sources-tab" id="sources-tab-review" role="tab" aria-selected="false" onclick="App.switchSourcesTab('review')" style="display:none;">Klassifikations-Review <span id="sources-review-count" class="sources-tab-badge">0</span></button>
|
||||
</div>
|
||||
|
||||
<!-- View: Quellenliste -->
|
||||
<div id="sources-list-view">
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="sources-toolbar">
|
||||
<div class="sources-filters">
|
||||
@@ -627,89 +618,6 @@
|
||||
<input type="text" id="src-notes" placeholder="Optional">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sources-classification-section">
|
||||
<div class="sources-classification-header">Einordnung</div>
|
||||
<div class="sources-add-form-grid">
|
||||
<div class="form-group">
|
||||
<label for="src-political">Politische Ausrichtung</label>
|
||||
<select id="src-political">
|
||||
<option value="na">Nicht eingeordnet</option>
|
||||
<option value="links_extrem">Links (extrem)</option>
|
||||
<option value="links">Links</option>
|
||||
<option value="mitte_links">Mitte-Links</option>
|
||||
<option value="liberal">Liberal</option>
|
||||
<option value="mitte">Mitte</option>
|
||||
<option value="konservativ">Konservativ</option>
|
||||
<option value="mitte_rechts">Mitte-Rechts</option>
|
||||
<option value="rechts">Rechts</option>
|
||||
<option value="rechts_extrem">Rechts (extrem)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="src-mediatype">Medientyp</label>
|
||||
<select id="src-mediatype">
|
||||
<option value="sonstige">Sonstige</option>
|
||||
<option value="tageszeitung">Tageszeitung</option>
|
||||
<option value="wochenzeitung">Wochenzeitung</option>
|
||||
<option value="magazin">Magazin</option>
|
||||
<option value="tv_sender">TV-Sender</option>
|
||||
<option value="radio">Radio</option>
|
||||
<option value="oeffentlich_rechtlich">Öffentlich-Rechtlich</option>
|
||||
<option value="nachrichtenagentur">Nachrichtenagentur</option>
|
||||
<option value="online_only">Online-only</option>
|
||||
<option value="blog">Blog</option>
|
||||
<option value="telegram_kanal">Telegram-Kanal</option>
|
||||
<option value="telegram_bot">Telegram-Bot</option>
|
||||
<option value="podcast">Podcast</option>
|
||||
<option value="social_media">Social Media</option>
|
||||
<option value="imageboard">Imageboard</option>
|
||||
<option value="think_tank">Think Tank</option>
|
||||
<option value="ngo">NGO</option>
|
||||
<option value="behoerde">Behörde</option>
|
||||
<option value="staatsmedium">Staatsmedium</option>
|
||||
<option value="fachmedium">Fachmedium</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="src-reliability">Glaubwürdigkeit</label>
|
||||
<select id="src-reliability">
|
||||
<option value="na">Nicht eingeordnet</option>
|
||||
<option value="sehr_hoch">Sehr hoch</option>
|
||||
<option value="hoch">Hoch</option>
|
||||
<option value="gemischt">Gemischt</option>
|
||||
<option value="niedrig">Niedrig</option>
|
||||
<option value="sehr_niedrig">Sehr niedrig</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="src-country">Land (ISO 3166)</label>
|
||||
<input type="text" id="src-country" maxlength="2" placeholder="z.B. DE, RU, US" style="text-transform:uppercase;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label" style="display:flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" id="src-state-affiliated">
|
||||
<span>Staatsnah/-kontrolliert</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:8px;">
|
||||
<label>Geopolitische Nähe (Mehrfachauswahl)</label>
|
||||
<div id="src-alignments-chips" class="alignment-chips" onclick="App.handleAlignmentChipClick(event)">
|
||||
<button type="button" class="alignment-chip" data-alignment="prorussisch">prorussisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="proiranisch">proiranisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="prowestlich">prowestlich</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="proukrainisch">proukrainisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="prochinesisch">prochinesisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="projapanisch">projapanisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="proisraelisch">proisraelisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="propalaestinensisch">propalästinensisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="protuerkisch">protürkisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="panarabisch">panarabisch</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="neutral">neutral</button>
|
||||
<button type="button" class="alignment-chip" data-alignment="sonstige">sonstige</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sources-discovery-actions">
|
||||
<button class="btn btn-primary btn-small" onclick="App.saveSource()">Speichern</button>
|
||||
<button class="btn btn-secondary btn-small" onclick="App.toggleSourceForm(false)">Abbrechen</button>
|
||||
@@ -721,36 +629,6 @@
|
||||
<div class="sources-list" id="sources-list">
|
||||
<div class="empty-state-text" style="padding:var(--sp-3xl);text-align:center;">Lade Quellen...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- /sources-list-view -->
|
||||
|
||||
<!-- View: Klassifikations-Review (Admin-only) -->
|
||||
<div id="sources-review-view" style="display:none;">
|
||||
<div class="review-toolbar">
|
||||
<div class="review-toolbar-info">
|
||||
<span><strong id="review-pending-count">0</strong> Vorschlaege ausstehend</span>
|
||||
<label class="review-conf-filter">
|
||||
Mindest-Konfidenz:
|
||||
<select id="review-min-confidence" onchange="App.loadClassificationQueue()">
|
||||
<option value="0">alle</option>
|
||||
<option value="0.5">0.5+</option>
|
||||
<option value="0.7">0.7+</option>
|
||||
<option value="0.85">0.85+</option>
|
||||
<option value="0.9">0.9+</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="review-toolbar-actions">
|
||||
<button class="btn btn-small btn-secondary" onclick="App.triggerExternalReputationSync()" title="IFCN-Faktenchecker-Liste und EUvsDisinfo-Daten synchronisieren">Externe Daten syncen</button>
|
||||
<button class="btn btn-small btn-secondary" onclick="App.triggerBulkClassify()" title="LLM-Klassifikation fuer noch unklassifizierte Quellen starten">+ Klassifikation starten</button>
|
||||
<button class="btn btn-small btn-primary" onclick="App.bulkApproveHighConfidence()" title="Alle Vorschlaege ueber dem Konfidenz-Schwellwert genehmigen">Alle ≥ 0.85 genehmigen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-list" id="sources-review-list">
|
||||
<div class="empty-state-text" style="padding:var(--sp-3xl);text-align:center;">Lade Review-Queue...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -209,35 +209,6 @@ const API = {
|
||||
return this._request('GET', `/sources${qs ? '?' + qs : ''}`);
|
||||
},
|
||||
|
||||
// Sources: Klassifikations-Review (LLM)
|
||||
getClassificationStats() {
|
||||
return this._request('GET', '/sources/classification/stats');
|
||||
},
|
||||
getClassificationQueue(limit = 50, minConfidence = 0.0) {
|
||||
const qs = new URLSearchParams({ limit: String(limit), min_confidence: String(minConfidence) }).toString();
|
||||
return this._request('GET', `/sources/classification/queue?${qs}`);
|
||||
},
|
||||
approveClassification(id) {
|
||||
return this._request('POST', `/sources/${id}/classification/approve`);
|
||||
},
|
||||
rejectClassification(id) {
|
||||
return this._request('POST', `/sources/${id}/classification/reject`);
|
||||
},
|
||||
reclassifySource(id) {
|
||||
return this._request('POST', `/sources/${id}/classification/reclassify`);
|
||||
},
|
||||
triggerBulkClassify(limit = 50, onlyUnclassified = true) {
|
||||
const qs = new URLSearchParams({ limit: String(limit), only_unclassified: String(onlyUnclassified) }).toString();
|
||||
return this._request('POST', `/sources/classification/bulk-classify?${qs}`);
|
||||
},
|
||||
bulkApproveClassifications(minConfidence = 0.85) {
|
||||
const qs = new URLSearchParams({ min_confidence: String(minConfidence) }).toString();
|
||||
return this._request('POST', `/sources/classification/bulk-approve?${qs}`);
|
||||
},
|
||||
triggerExternalReputationSync() {
|
||||
return this._request('POST', '/sources/external-reputation/sync');
|
||||
},
|
||||
|
||||
createSource(data) {
|
||||
return this._request('POST', '/sources', data);
|
||||
},
|
||||
|
||||
@@ -2702,12 +2702,6 @@ async handleRefresh() {
|
||||
async openSourceManagement() {
|
||||
openModal('modal-sources');
|
||||
await this.loadSources();
|
||||
// Admin sieht den Review-Tab
|
||||
const reviewTab = document.getElementById('sources-tab-review');
|
||||
if (reviewTab && this.user && this.user.role === 'org_admin') {
|
||||
reviewTab.style.display = '';
|
||||
this._refreshReviewBadge().catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
async loadSources() {
|
||||
@@ -2728,122 +2722,6 @@ async handleRefresh() {
|
||||
}
|
||||
},
|
||||
|
||||
async _refreshReviewBadge() {
|
||||
try {
|
||||
const stats = await API.getClassificationStats();
|
||||
const badge = document.getElementById('sources-review-count');
|
||||
if (badge) badge.textContent = String(stats.pending_review || 0);
|
||||
} catch (_) { /* still ok */ }
|
||||
},
|
||||
|
||||
switchSourcesTab(tab) {
|
||||
const listView = document.getElementById('sources-list-view');
|
||||
const reviewView = document.getElementById('sources-review-view');
|
||||
const tabList = document.getElementById('sources-tab-list');
|
||||
const tabReview = document.getElementById('sources-tab-review');
|
||||
if (!listView || !reviewView) return;
|
||||
if (tab === 'review') {
|
||||
listView.style.display = 'none';
|
||||
reviewView.style.display = '';
|
||||
if (tabList) { tabList.classList.remove('active'); tabList.setAttribute('aria-selected', 'false'); }
|
||||
if (tabReview) { tabReview.classList.add('active'); tabReview.setAttribute('aria-selected', 'true'); }
|
||||
this.loadClassificationQueue();
|
||||
} else {
|
||||
listView.style.display = '';
|
||||
reviewView.style.display = 'none';
|
||||
if (tabList) { tabList.classList.add('active'); tabList.setAttribute('aria-selected', 'true'); }
|
||||
if (tabReview) { tabReview.classList.remove('active'); tabReview.setAttribute('aria-selected', 'false'); }
|
||||
}
|
||||
},
|
||||
|
||||
async loadClassificationQueue() {
|
||||
const list = document.getElementById('sources-review-list');
|
||||
if (!list) return;
|
||||
const minConf = parseFloat(document.getElementById('review-min-confidence')?.value || '0');
|
||||
list.innerHTML = '<div class="empty-state-text" style="padding:var(--sp-3xl);text-align:center;">Lade...</div>';
|
||||
try {
|
||||
const items = await API.getClassificationQueue(200, minConf);
|
||||
this._reviewItems = items;
|
||||
const countEl = document.getElementById('review-pending-count');
|
||||
if (countEl) countEl.textContent = String(items.length);
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state-text" style="padding:var(--sp-3xl);text-align:center;">Keine ausstehenden Vorschlaege.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = items.map(item => UI.renderClassificationQueueItem(item)).join('');
|
||||
} catch (err) {
|
||||
list.innerHTML = `<div class="empty-state-text" style="padding:var(--sp-3xl);text-align:center;color:var(--danger);">Fehler: ${err.message}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
async approveClassification(id) {
|
||||
try {
|
||||
await API.approveClassification(id);
|
||||
UI.showToast('Klassifikation uebernommen.', 'success');
|
||||
await this.loadClassificationQueue();
|
||||
this._refreshReviewBadge();
|
||||
} catch (err) {
|
||||
UI.showToast('Approve fehlgeschlagen: ' + err.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async rejectClassification(id) {
|
||||
try {
|
||||
await API.rejectClassification(id);
|
||||
UI.showToast('Vorschlag verworfen.', 'success');
|
||||
await this.loadClassificationQueue();
|
||||
this._refreshReviewBadge();
|
||||
} catch (err) {
|
||||
UI.showToast('Reject fehlgeschlagen: ' + err.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async reclassifySource(id) {
|
||||
const btn = document.querySelector(`[data-reclassify-id="${id}"]`);
|
||||
if (btn) { btn.disabled = true; btn.textContent = '...'; }
|
||||
try {
|
||||
await API.reclassifySource(id);
|
||||
UI.showToast('Neu klassifiziert.', 'success');
|
||||
await this.loadClassificationQueue();
|
||||
} catch (err) {
|
||||
UI.showToast('Reclassify fehlgeschlagen: ' + err.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Neu klassifizieren'; }
|
||||
}
|
||||
},
|
||||
|
||||
async triggerBulkClassify() {
|
||||
if (!confirm('Bulk-Klassifikation aller noch nicht klassifizierten Quellen starten? Lauft im Hintergrund (~3-5 Sek pro Quelle, ~0.02 USD pro Quelle).')) return;
|
||||
try {
|
||||
const r = await API.triggerBulkClassify(500, true);
|
||||
UI.showToast(`Bulk-Klassifikation gestartet (limit=${r.limit}). Nachschauen mit Reload.`, 'info');
|
||||
} catch (err) {
|
||||
UI.showToast('Start fehlgeschlagen: ' + err.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async bulkApproveHighConfidence() {
|
||||
if (!confirm('Alle Vorschlaege mit Konfidenz >= 0.85 genehmigen?')) return;
|
||||
try {
|
||||
const r = await API.bulkApproveClassifications(0.85);
|
||||
UI.showToast(`${r.approved_count} Vorschlaege uebernommen.`, 'success');
|
||||
await this.loadClassificationQueue();
|
||||
this._refreshReviewBadge();
|
||||
} catch (err) {
|
||||
UI.showToast('Bulk-Approve fehlgeschlagen: ' + err.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async triggerExternalReputationSync() {
|
||||
if (!confirm('IFCN- und EUvsDisinfo-Datenbanken jetzt syncen? Lauft im Hintergrund (~30 Sek).')) return;
|
||||
try {
|
||||
await API.triggerExternalReputationSync();
|
||||
UI.showToast('Externer Sync gestartet. Quellenliste in 30 Sek neu laden.', 'info');
|
||||
} catch (err) {
|
||||
UI.showToast('Sync fehlgeschlagen: ' + err.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
renderSourceStats(stats) {
|
||||
const bar = document.getElementById('sources-stats-bar');
|
||||
if (!bar) return;
|
||||
@@ -3200,13 +3078,6 @@ async handleRefresh() {
|
||||
document.getElementById('src-discover-btn').disabled = false;
|
||||
document.getElementById('src-discover-btn').textContent = 'Erkennen';
|
||||
document.getElementById('src-type-select').value = 'rss_feed';
|
||||
// Klassifikations-Felder auf Default zurücksetzen
|
||||
const polEl = document.getElementById('src-political'); if (polEl) polEl.value = 'na';
|
||||
const mtEl = document.getElementById('src-mediatype'); if (mtEl) mtEl.value = 'sonstige';
|
||||
const relEl = document.getElementById('src-reliability'); if (relEl) relEl.value = 'na';
|
||||
const ccEl = document.getElementById('src-country'); if (ccEl) ccEl.value = '';
|
||||
const saEl = document.getElementById('src-state-affiliated'); if (saEl) saEl.checked = false;
|
||||
this._setAlignmentChips([]);
|
||||
// Save-Button Text zurücksetzen
|
||||
const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
|
||||
if (saveBtn) saveBtn.textContent = 'Speichern';
|
||||
@@ -3388,19 +3259,6 @@ async handleRefresh() {
|
||||
rss_url: source.url,
|
||||
};
|
||||
|
||||
// Klassifikations-Felder setzen
|
||||
const polEl = document.getElementById('src-political');
|
||||
if (polEl) polEl.value = source.political_orientation || 'na';
|
||||
const mtEl = document.getElementById('src-mediatype');
|
||||
if (mtEl) mtEl.value = source.media_type || 'sonstige';
|
||||
const relEl = document.getElementById('src-reliability');
|
||||
if (relEl) relEl.value = source.reliability || 'na';
|
||||
const ccEl = document.getElementById('src-country');
|
||||
if (ccEl) ccEl.value = source.country_code || '';
|
||||
const saEl = document.getElementById('src-state-affiliated');
|
||||
if (saEl) saEl.checked = !!source.state_affiliated;
|
||||
this._setAlignmentChips(source.alignments || []);
|
||||
|
||||
// Submit-Button-Text ändern
|
||||
const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
|
||||
if (saveBtn) saveBtn.textContent = 'Quelle speichern';
|
||||
@@ -3409,27 +3267,6 @@ async handleRefresh() {
|
||||
if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
},
|
||||
|
||||
_setAlignmentChips(active) {
|
||||
const chips = document.querySelectorAll('#src-alignments-chips .alignment-chip');
|
||||
const set = new Set((active || []).map(a => (a || '').toLowerCase()));
|
||||
chips.forEach(chip => {
|
||||
if (set.has(chip.dataset.alignment)) chip.classList.add('active');
|
||||
else chip.classList.remove('active');
|
||||
});
|
||||
},
|
||||
|
||||
_getAlignmentChips() {
|
||||
return Array.from(document.querySelectorAll('#src-alignments-chips .alignment-chip.active'))
|
||||
.map(chip => chip.dataset.alignment);
|
||||
},
|
||||
|
||||
handleAlignmentChipClick(e) {
|
||||
const chip = e.target.closest('.alignment-chip');
|
||||
if (!chip) return;
|
||||
e.preventDefault();
|
||||
chip.classList.toggle('active');
|
||||
},
|
||||
|
||||
async saveSource() {
|
||||
const name = document.getElementById('src-name').value.trim();
|
||||
if (!name) {
|
||||
@@ -3445,12 +3282,6 @@ async handleRefresh() {
|
||||
url: discovered.rss_url || (discovered.source_type === 'telegram_channel' ? (document.getElementById('src-domain').value || null) : null),
|
||||
domain: document.getElementById('src-domain').value.trim() || discovered.domain || null,
|
||||
notes: document.getElementById('src-notes').value.trim() || null,
|
||||
political_orientation: document.getElementById('src-political')?.value || 'na',
|
||||
media_type: document.getElementById('src-mediatype')?.value || 'sonstige',
|
||||
reliability: document.getElementById('src-reliability')?.value || 'na',
|
||||
country_code: (document.getElementById('src-country')?.value || '').trim().toUpperCase() || null,
|
||||
state_affiliated: !!document.getElementById('src-state-affiliated')?.checked,
|
||||
alignments: this._getAlignmentChips(),
|
||||
};
|
||||
|
||||
if (!data.domain && discovered.domain) {
|
||||
|
||||
@@ -1119,71 +1119,6 @@ const UI = {
|
||||
sonstige: 'sonstige',
|
||||
},
|
||||
|
||||
/**
|
||||
* Eintrag in der Klassifikations-Review-Queue.
|
||||
* Zeigt Diff zwischen aktuellem Wert und LLM-Vorschlag.
|
||||
*/
|
||||
renderClassificationQueueItem(item) {
|
||||
const cur = item.current || {};
|
||||
const prop = item.proposed || {};
|
||||
const conf = prop.confidence || 0;
|
||||
const confPct = Math.round(conf * 100);
|
||||
const confClass = conf >= 0.85 ? 'high' : (conf >= 0.7 ? 'medium' : 'low');
|
||||
|
||||
const diffRow = (label, currentVal, proposedVal, formatter) => {
|
||||
const fmt = formatter || (v => v == null || v === '' ? '–' : String(v));
|
||||
const c = fmt(currentVal);
|
||||
const p = fmt(proposedVal);
|
||||
const changed = c !== p;
|
||||
return `<div class="review-diff-row${changed ? ' changed' : ''}">
|
||||
<span class="review-diff-label">${this.escape(label)}</span>
|
||||
<span class="review-diff-current">${this.escape(c)}</span>
|
||||
<span class="review-diff-arrow">→</span>
|
||||
<span class="review-diff-proposed">${this.escape(p)}</span>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const polFmt = v => (v && v !== 'na') ? (this._politicalLabels[v]?.full || v) : '–';
|
||||
const mtFmt = v => (v && v !== 'sonstige') ? (this._mediaTypeLabels[v] || v) : (v === 'sonstige' ? 'Sonstige' : '–');
|
||||
const relFmt = v => (v && v !== 'na') ? (this._reliabilityLabels[v] || v) : '–';
|
||||
const stateFmt = v => v ? 'ja' : 'nein';
|
||||
const ccFmt = v => v || '–';
|
||||
const alignFmt = v => (Array.isArray(v) && v.length > 0)
|
||||
? v.map(a => this._alignmentLabels[a] || a).join(', ')
|
||||
: '–';
|
||||
|
||||
const globalBadge = item.is_global ? '<span class="review-global-badge">Grundquelle</span>' : '';
|
||||
const reasoning = prop.reasoning ? this.escape(prop.reasoning) : '';
|
||||
|
||||
return `<div class="review-card" data-source-id="${item.id}">
|
||||
<div class="review-card-header">
|
||||
<div class="review-card-title">
|
||||
<span class="review-card-name">${this.escape(item.name)}</span>
|
||||
${globalBadge}
|
||||
<span class="review-card-domain">${this.escape(item.domain || '')}</span>
|
||||
</div>
|
||||
<div class="review-card-confidence conf-${confClass}" title="LLM-Konfidenz">
|
||||
<span class="conf-value">${confPct}%</span>
|
||||
<span class="conf-label">Konfidenz</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-card-diff">
|
||||
${diffRow('Politik', cur.political_orientation, prop.political_orientation, polFmt)}
|
||||
${diffRow('Medientyp', cur.media_type, prop.media_type, mtFmt)}
|
||||
${diffRow('Glaubwürdigkeit', cur.reliability, prop.reliability, relFmt)}
|
||||
${diffRow('Staatsnah', cur.state_affiliated, prop.state_affiliated, stateFmt)}
|
||||
${diffRow('Land', cur.country_code, prop.country_code, ccFmt)}
|
||||
${diffRow('Geopol. Nähe', cur.alignments, prop.alignments, alignFmt)}
|
||||
</div>
|
||||
${reasoning ? `<div class="review-card-reasoning"><strong>Begründung:</strong> ${reasoning}</div>` : ''}
|
||||
<div class="review-card-actions">
|
||||
<button class="btn btn-small btn-primary" onclick="App.approveClassification(${item.id})">Übernehmen</button>
|
||||
<button class="btn btn-small btn-secondary" onclick="App.rejectClassification(${item.id})">Verwerfen</button>
|
||||
<button class="btn btn-small btn-secondary" data-reclassify-id="${item.id}" onclick="App.reclassifySource(${item.id})">Neu klassifizieren</button>
|
||||
</div>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
_renderClassificationBadges(feed) {
|
||||
const parts = [];
|
||||
const pol = feed.political_orientation;
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren