feat(fimi): Frontend Andockpunkte 1-3 + Verifizierer-Robustheit

- Andockpunkt 1: dezenter Inline-Hinweis am Artikel (Quellen-Detailliste)
  mit Provenienz (EUvsDisinfo) + Case-Link, nur bei bestaetigtem Treffer.
- Andockpunkt 2: Track-Record-Badge pro Quelle in der Quellenuebersicht.
- Andockpunkt 3: Qualitaetsleiste ueber dem Lagebild (geprueft/Treffer/
  Narrative), aufklappbare Top-Narrative mit Belegen.
- fimi_matcher: URLs aus dem Artikeltext entfernen + Prompt-Praeambel gegen
  Tool-Nutzung, sonst scheiterte die Haiku-Verifikation an WebFetch-Versuchen
  (error_max_turns).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Code
2026-06-14 09:43:11 +00:00
Ursprung 46b2acfc36
Commit acac401034
6 geänderte Dateien mit 244 neuen und 4 gelöschten Zeilen

Datei anzeigen

@@ -1058,8 +1058,14 @@ const UI = {
const langs = (s.languages || ['de']).map(l => (l || 'de').toUpperCase()).join('/');
const sourceName = this.escape(s.source || 'Unbekannt');
const sType = s.source_type || 'web';
html += `<div class="source-overview-item" data-source="${sourceName}" data-type="${sType}" tabindex="0" role="button" aria-expanded="false" onclick="App.toggleSourceOverviewDetail(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();App.toggleSourceOverviewDetail(this);}">
// Andockpunkt 2: empirischer Track-Record. Nur bei Treffern, dezent.
const fimiN = s.fimi_match_count || 0;
const fimiBadge = fimiN > 0
? `<span class="fimi-source-badge" title="${fimiN} ${fimiN === 1 ? 'Artikel dieser Quelle deckt' : 'Artikel dieser Quelle decken'} sich mit einer bei EUvsDisinfo widerlegten Falschbehauptung">${fimiN}&nbsp;FIMI</span>`
: '';
html += `<div class="source-overview-item${fimiN > 0 ? ' has-fimi' : ''}" data-source="${sourceName}" data-type="${sType}" tabindex="0" role="button" aria-expanded="false" onclick="App.toggleSourceOverviewDetail(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();App.toggleSourceOverviewDetail(this);}">
<span class="source-overview-name">${sourceName}</span>
${fimiBadge}
<span class="source-overview-lang">${langs}</span>
<span class="source-overview-count">${s.article_count}</span>
</div>`;
@@ -1069,6 +1075,67 @@ const UI = {
return html;
},
/**
* Andockpunkt 1: dezenter Inline-Hinweis an einem Artikel, der sich mit
* einer bei EUvsDisinfo widerlegten Falschbehauptung deckt. Provenienz-
* Leitplanke: nennt die Quelle (EUvsDisinfo), verlinkt den Case, wertet
* nicht selbst. matches: Array aus dem fimi-matches-Endpunkt.
*/
renderFimiHint(matches) {
if (!matches || matches.length === 0) return '';
const n = matches.length;
const top = matches[0];
const claimText = this.escape(top.claim_text || '');
const passage = top.passage ? this.escape(top.passage) : '';
let tip = `Bei EUvsDisinfo als widerlegt geführte Behauptung: ${claimText}`;
if (passage) tip += ` | Im Artikel: ${passage}`;
const label = n === 1
? 'Deckt sich mit einer von EUvsDisinfo widerlegten Falschbehauptung'
: `Deckt sich mit ${n} von EUvsDisinfo widerlegten Falschbehauptungen`;
const link = top.case_url
? `<a href="${this.escape(top.case_url)}" target="_blank" rel="noopener" class="fimi-hint-link" onclick="event.stopPropagation()">Beleg ansehen</a>`
: '';
return `<div class="fimi-hint" title="${tip}">
<span class="fimi-hint-icon" aria-hidden="true">&#9888;</span>
<span class="fimi-hint-text">${label}</span>
${link}
</div>`;
},
/**
* Andockpunkt 3: Qualitaetsachse fuers Lagebild. Verdichtet die
* Einzeltreffer auf Lage-Ebene. Bei 0 Treffern eine ruhige Entwarnung,
* sonst eine zurueckhaltende Hinweisleiste mit aufklappbaren Narrativen.
*/
renderFimiSummaryBar(s) {
if (!s || !s.articles_checked) return '';
const matched = s.articles_with_match || 0;
const checked = s.articles_checked || 0;
const distinct = s.distinct_claims || 0;
if (matched === 0) {
return `<div class="fimi-summary-bar fimi-summary-bar--clear">
<span class="fimi-summary-icon" aria-hidden="true">&#10003;</span>
<span>Keine bekannten Falschbehauptungen unter ${checked} geprüften Artikeln.</span>
</div>`;
}
const topClaims = (s.top_claims || []).slice(0, 6);
const claimList = topClaims.map(c => {
const txt = this.escape(c.claim_text || '');
const link = c.case_url
? `<a href="${this.escape(c.case_url)}" target="_blank" rel="noopener" class="fimi-hint-link">Beleg</a>`
: '';
return `<li><span class="fimi-claim-count">${c.article_count}&times;</span> <span class="fimi-claim-text">${txt}</span> ${link}</li>`;
}).join('');
return `<div class="fimi-summary-bar fimi-summary-bar--alert">
<div class="fimi-summary-head">
<span class="fimi-summary-icon" aria-hidden="true">&#9888;</span>
<span class="fimi-summary-lead"><strong>${matched}</strong> von ${checked} geprüften Artikeln decken sich mit <strong>${distinct}</strong> bei EUvsDisinfo widerlegten Falschbehauptungen.</span>
<button type="button" class="fimi-summary-toggle" onclick="App.toggleFimiDetail(this)">Narrative anzeigen</button>
</div>
<ul class="fimi-summary-claims" style="display:none;">${claimList}</ul>
</div>`;
},
renderSourceOverview(articles) {
if (!articles || articles.length === 0) return '';