Live-Monitoring: Quellen-Namen pro Bullet (Prompt + Frontend-Parser)

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.
Dieser Commit ist enthalten in:
claude-dev
2026-04-18 20:27:16 +00:00
Ursprung 2ae8b9a341
Commit 5c95d85871
2 geänderte Dateien mit 68 neuen und 28 gelöschten Zeilen

Datei anzeigen

@@ -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}}
..."""

Datei anzeigen

@@ -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 '<span style="color:var(--text-disabled);">Noch keine Entwicklungen erfasst.</span>';
@@ -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 ? `<span class="dev-bias dev-bias-${biasClass}">${this._biasLabel(biasClass)}</span>` : '';
if (src && src.url) {
return `<a href="${this.escape(src.url)}" target="_blank" rel="noopener" class="dev-source-pill" title="${esc}">${esc}${biasHtml}</a>`;
}
return `<span class="dev-source-pill" title="${esc}">${esc}${biasHtml}</span>`;
};
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];
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('');
}
// 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]);
}
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 ? `<span class="dev-bias dev-bias-${biasClass}">${this._biasLabel(biasClass)}</span>` : '';
if (src.url) {
return `<a href="${this.escape(src.url)}" target="_blank" rel="noopener" class="dev-source-pill" title="${name}">${name}${biasHtml}</a>`;
}
return `<span class="dev-source-pill" title="${name}">${name}${biasHtml}</span>`;
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
? `<span class="dev-sources">${pills}</span>`
const cleanBody = this.escape(rawBody.trim());
const sourcesHtml = pillsHtml
? `<span class="dev-sources">${pillsHtml}</span>`
: `<span class="dev-sources dev-sources-empty">Keine Quelle</span>`;
const timeHtml = `<span class="dev-time" title="${this.escape(date + ' ' + time)}">${this.escape(time)} \u00b7 ${this.escape(date)}</span>`;