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)}`;