From 5c95d85871aec645f70a19b66fae025ac6ce91ac Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 18 Apr 2026 20:27:16 +0000 Subject: [PATCH] Live-Monitoring: Quellen-Namen pro Bullet (Prompt + Frontend-Parser) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der LATEST_DEVELOPMENTS-Prompt produzierte Bullets ohne Citations — das Frontend zeigte daher "Keine Quelle". Prompt ergaenzt: jedes Bullet endet mit {Quellenname1, Quellenname2} (geschweifte Klammern, exakte Schreibweise aus Quelle:-Zeile). Frontend-Parser extrahiert diese Klammer, matcht Namen case-insensitive gegen sources_json und erstellt klickbare Pills. Fallback fuer Legacy-Bullets: Inline-[N]-Citations werden weiterhin erkannt. Altbestand-Bullets ohne Marker erhalten beim naechsten Refresh Quellen. --- src/agents/analyzer.py | 9 ++-- src/static/js/components.js | 87 +++++++++++++++++++++++++++---------- 2 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index ddc8fee..d74d0e9 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -226,8 +226,9 @@ Extrahiere aus den NEUEN Meldungen konkrete Ereignisse und aktualisiere die List REGELN: - Jedes Bullet = EIN konkretes Ereignis (1-2 Sätze, faktenbasiert). Keine Themen-Zusammenfassungen. - Jedes Bullet beginnt mit dem Zeitstempel der frühesten belegenden Quelle im Format "[DD.MM. HH:MM]". -- Wenn mehrere Meldungen dasselbe Ereignis betreffen: EIN Bullet, Zeitstempel = frühester Zeitpunkt. -- Bestehende Bullets aus BISHERIGE ENTWICKLUNGEN sinngemäß übernehmen, NICHT umformulieren. Nur entfernen, wenn sie durch neue Meldungen nachweislich überholt sind oder die 8-Bullet-Grenze überschritten wird (dann älteste fallen raus). +- Jedes Bullet endet mit den Quellen-Namen in geschweiften Klammern, kommasepariert. Beispiel: "{{Reuters, Rybar}}". Verwende EXAKT die Schreibweise aus der "Quelle:"-Zeile der Meldung (z.B. "Der Tagesspiegel", nicht "Tagesspiegel"). Keine Abkuerzungen, keine Umformulierungen. +- Wenn mehrere Meldungen dasselbe Ereignis betreffen: EIN Bullet, Zeitstempel = frühester Zeitpunkt, ALLE belegenden Quellen-Namen in den Klammern. +- Bestehende Bullets aus BISHERIGE ENTWICKLUNGEN sinngemäß übernehmen, NICHT umformulieren. Nur entfernen, wenn sie durch neue Meldungen nachweislich überholt sind oder die 8-Bullet-Grenze überschritten wird (dann älteste fallen raus). Wenn einem uebernommenen Bullet die Quellen-Klammer fehlt (Altformat), ergaenze sie anhand der URSPRUENGLICHEN Beleg-Meldung so gut moeglich; ansonsten "{{Unbekannt}}". - Wenn eine Quelle eine erkennbare politische Ausrichtung hat (z.B. pro-russisch, staatsnah, rechtsextrem), im Bullet-Text erwähnen ("laut pro-russischem Telegram-Kanal Rybar..."). - Neutral und sachlich — keine Wertungen oder Spekulationen. - KEINE Gedankenstriche (—, –) — stattdessen Kommas, Doppelpunkte oder neue Sätze. @@ -236,8 +237,8 @@ REGELN: - Wenn aus den neuen Meldungen kein neues Ereignis extrahierbar ist: BISHERIGE ENTWICKLUNGEN unverändert zurückgeben. OUTPUT-FORMAT (ausschliesslich, keine Anführungszeichen, kein Code-Fence): -- [DD.MM. HH:MM] Erster Ereignistext. -- [DD.MM. HH:MM] Zweiter Ereignistext. +- [DD.MM. HH:MM] Erster Ereignistext. {{Quellenname1}} +- [DD.MM. HH:MM] Zweiter Ereignistext. {{Quellenname1, Quellenname2}} ...""" diff --git a/src/static/js/components.js b/src/static/js/components.js index 375eb97..0aaab5d 100644 --- a/src/static/js/components.js +++ b/src/static/js/components.js @@ -746,8 +746,8 @@ const UI = { /** * Rendert "Neueste Entwicklungen" für Live-Monitoring (adhoc). - * Erwartet Bullets im Format "- [DD.MM. HH:MM] Text [N][M]". - * Erzeugt Karten mit sichtbaren Quellen-Pills + dezentem Zeitstempel. + * Erwartet Bullets im Format "- [DD.MM. HH:MM] Text {Quelle1, Quelle2}". + * Legacy: Inline-[N]-Citations werden als Fallback ebenfalls erkannt. */ renderLatestDevelopments(text, sourcesJson) { if (!text) return 'Noch keine Entwicklungen erfasst.'; @@ -756,14 +756,14 @@ const UI = { const bulletLines = text.split("\n").map(l => l.trim()).filter(l => l.startsWith("- ")); if (bulletLines.length === 0) { - // Fallback: kein Bullet-Format erkannt, alten Render verwenden return this.renderZusammenfassung(text, sourcesJson); } const bulletRe = /^-\s*\[(\d{1,2}\.\d{1,2}\.)\s+(\d{1,2}:\d{2})\]\s*(.+?)\s*$/; const citationRe = /\[(\d+[a-z]?)\]/g; + const trailingNamesRe = /\s*\{([^{}]+)\}\s*\.?\s*$/; - const lookupSource = (num) => { + const lookupByNum = (num) => { let src = sources.find(s => String(s.nr) === num || Number(s.nr) === Number(num)); if (!src && /[a-z]$/.test(num)) { const baseNum = num.replace(/[a-z]$/, ''); @@ -772,6 +772,30 @@ const UI = { return src || null; }; + const normalize = (s) => (s || '').toLowerCase().replace(/^(der|die|das)\s+/, '').replace(/\s+/g, ' ').trim(); + const lookupByName = (name) => { + const n = normalize(name); + if (!n) return null; + let src = sources.find(s => normalize(s.name) === n); + if (src) return src; + src = sources.find(s => { + const sn = normalize(s.name); + return sn.includes(n) || n.includes(sn); + }); + return src || null; + }; + + const buildPill = (src, fallbackName) => { + const displayName = src ? (src.name || fallbackName) : fallbackName; + const esc = this.escape(displayName); + const biasClass = this._classifyBias(displayName); + const biasHtml = biasClass ? `${this._biasLabel(biasClass)}` : ''; + if (src && src.url) { + return `${esc}${biasHtml}`; + } + return `${esc}${biasHtml}`; + }; + const cards = bulletLines.map(line => { const m = bulletRe.exec(line); if (!m) { @@ -780,31 +804,46 @@ const UI = { } const date = m[1]; const time = m[2]; - const rawBody = m[3]; + let rawBody = m[3]; - const nums = []; - let cm; - while ((cm = citationRe.exec(rawBody)) !== null) { - if (!nums.includes(cm[1])) nums.push(cm[1]); + let pillsHtml = ''; + + // Primär: {Name1, Name2} am Bullet-Ende + const trailing = trailingNamesRe.exec(rawBody); + if (trailing) { + rawBody = rawBody.replace(trailingNamesRe, '').trim(); + const names = trailing[1].split(',').map(s => s.trim()).filter(Boolean); + const seen = new Set(); + pillsHtml = names.map(name => { + const key = normalize(name); + if (seen.has(key)) return ''; + seen.add(key); + if (/^(unbekannt|unknown|n\/a|keine)$/i.test(name)) return ''; + const src = lookupByName(name); + return buildPill(src, name); + }).filter(Boolean).join(''); } - citationRe.lastIndex = 0; - const cleanBody = this.escape(rawBody.replace(citationRe, '').replace(/\s+/g, ' ').trim()); - - const pills = nums.map(num => { - const src = lookupSource(num); - if (!src) return ''; - const name = this.escape(src.name || `Quelle ${num}`); - const biasClass = this._classifyBias(src.name || ''); - const biasHtml = biasClass ? `${this._biasLabel(biasClass)}` : ''; - if (src.url) { - return `${name}${biasHtml}`; + // Fallback: Inline-[N]-Citations (Legacy-Recherche-Format) + if (!pillsHtml) { + const nums = []; + let cm; + while ((cm = citationRe.exec(rawBody)) !== null) { + if (!nums.includes(cm[1])) nums.push(cm[1]); } - return `${name}${biasHtml}`; - }).filter(Boolean).join(''); + citationRe.lastIndex = 0; + if (nums.length > 0) { + rawBody = rawBody.replace(citationRe, '').replace(/\s+/g, ' ').trim(); + pillsHtml = nums.map(num => { + const src = lookupByNum(num); + return src ? buildPill(src, src.name || `Quelle ${num}`) : ''; + }).filter(Boolean).join(''); + } + } - const sourcesHtml = pills - ? `${pills}` + const cleanBody = this.escape(rawBody.trim()); + const sourcesHtml = pillsHtml + ? `${pillsHtml}` : `Keine Quelle`; const timeHtml = `${this.escape(time)} \u00b7 ${this.escape(date)}`;