feat(#170): Chart-Click-Tag-Filter + Transparenz-Banner + top_k 50 default
User-Feedback: "Welche Meldungen werden da angezeigt? Es wurden ja viel mehr indiziert." **1. Transparenz-Banner im News-Tab** Zeigt jetzt explizit: - "X News angezeigt" - "Y News im Zeitraum (mit Embedding)" - "Z News insgesamt embedded" - Hinweis wenn only_relevant aktiv ist - Hinweis wenn top_k limitierend ist **2. Chart als Filter** — Klick auf einen Tag im News-Volumen-Chart wechselt zum News-Tab und filtert auf diesen Tag. - Chart bekommt onClick-Handler ueber getElementsAtEventForMode - Cursor wechselt bei Hover ueber Datenpunkte - Im News-Tab erscheint Pill "Tag: 2026-05-01 [× Tag-Filter entfernen]" **3. Backend `single_date`-Param** `aggregate_top_themen(single_date="YYYY-MM-DD")` filtert auf genau diesen Tag (overrides days_window). Endpoint: `/api/aktuelle-themen/top ?date=YYYY-MM-DD`. Response neu: `n_in_window`, `n_shown`, `filter.single_date`. **4. Default top_k 20 → 50** (max 200), damit weniger oft auf "top_k limitierend" gestoßen wird. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3bf1de15b5
commit
7c1e0fa0b0
@ -2031,11 +2031,15 @@ async def api_aktuelle_themen_top(
|
|||||||
min_similarity: float = 0.4,
|
min_similarity: float = 0.4,
|
||||||
matches_per_news: int = 3,
|
matches_per_news: int = 3,
|
||||||
only_relevant: bool = False,
|
only_relevant: bool = False,
|
||||||
|
date: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Top-K News der letzten N Tage mit Antrags-Match.
|
"""Top-K News der letzten N Tage mit Antrags-Match.
|
||||||
|
|
||||||
Mit `only_relevant=true` werden News mit Relevance-Level "low" oder
|
Mit `only_relevant=true` werden News mit Relevance-Level "low" oder
|
||||||
"none" rausgefiltert.
|
"none" rausgefiltert.
|
||||||
|
|
||||||
|
Mit `date=YYYY-MM-DD` werden nur News dieses Tages angezeigt
|
||||||
|
(overrides `days`).
|
||||||
"""
|
"""
|
||||||
from .themen_matching import aggregate_top_themen
|
from .themen_matching import aggregate_top_themen
|
||||||
return aggregate_top_themen(
|
return aggregate_top_themen(
|
||||||
@ -2044,6 +2048,7 @@ async def api_aktuelle_themen_top(
|
|||||||
min_similarity=min_similarity,
|
min_similarity=min_similarity,
|
||||||
matches_per_news=matches_per_news,
|
matches_per_news=matches_per_news,
|
||||||
only_relevant=only_relevant,
|
only_relevant=only_relevant,
|
||||||
|
single_date=date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -168,7 +168,7 @@
|
|||||||
<option value="30">30 Tage</option>
|
<option value="30">30 Tage</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="at-topk">Top-N News:</label>
|
<label for="at-topk">Top-N News:</label>
|
||||||
<input type="number" id="at-topk" value="20" min="3" max="50" style="width:60px;" onchange="loadActiveTab()" />
|
<input type="number" id="at-topk" value="50" min="3" max="200" style="width:60px;" onchange="loadActiveTab()" />
|
||||||
<label for="at-minsim">Min. Similarity:</label>
|
<label for="at-minsim">Min. Similarity:</label>
|
||||||
<select id="at-minsim" onchange="loadActiveTab()">
|
<select id="at-minsim" onchange="loadActiveTab()">
|
||||||
<option value="0.30">0.30 (locker)</option>
|
<option value="0.30">0.30 (locker)</option>
|
||||||
@ -193,6 +193,7 @@
|
|||||||
|
|
||||||
<!-- Tab Panels -->
|
<!-- Tab Panels -->
|
||||||
<div id="at-tab-news" class="at-panel">
|
<div id="at-tab-news" class="at-panel">
|
||||||
|
<div id="at-news-banner" style="font-family:var(--font-mono);font-size:11px;opacity:0.65;margin-bottom:1rem;"></div>
|
||||||
<div id="at-themen-list">
|
<div id="at-themen-list">
|
||||||
<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade …</div>
|
<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade …</div>
|
||||||
</div>
|
</div>
|
||||||
@ -253,6 +254,7 @@
|
|||||||
<script>
|
<script>
|
||||||
let _atZeitreiheChart = null;
|
let _atZeitreiheChart = null;
|
||||||
let _atActiveTab = 'news';
|
let _atActiveTab = 'news';
|
||||||
|
let _atSingleDate = null; // YYYY-MM-DD wenn aus Chart geklickt
|
||||||
|
|
||||||
function atScoreClass(score) {
|
function atScoreClass(score) {
|
||||||
if (score == null) return '';
|
if (score == null) return '';
|
||||||
@ -303,20 +305,47 @@ function loadActiveTab() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearDateFilter() {
|
||||||
|
_atSingleDate = null;
|
||||||
|
loadActiveTab();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadThemen() {
|
async function loadThemen() {
|
||||||
const days = document.getElementById('at-days').value;
|
const days = document.getElementById('at-days').value;
|
||||||
const topk = document.getElementById('at-topk').value;
|
const topk = document.getElementById('at-topk').value;
|
||||||
const minsim = document.getElementById('at-minsim').value;
|
const minsim = document.getElementById('at-minsim').value;
|
||||||
const onlyRel = document.getElementById('at-only-relevant').checked ? '1' : '0';
|
const onlyRel = document.getElementById('at-only-relevant').checked ? '1' : '0';
|
||||||
const list = document.getElementById('at-themen-list');
|
const list = document.getElementById('at-themen-list');
|
||||||
|
const banner = document.getElementById('at-news-banner');
|
||||||
list.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade …</div>';
|
list.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade …</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/aktuelle-themen/top?days=${days}&top_k=${topk}&min_similarity=${minsim}&matches_per_news=3&only_relevant=${onlyRel}`);
|
let url = `/api/aktuelle-themen/top?days=${days}&top_k=${topk}&min_similarity=${minsim}&matches_per_news=3&only_relevant=${onlyRel}`;
|
||||||
|
if (_atSingleDate) url += `&date=${encodeURIComponent(_atSingleDate)}`;
|
||||||
|
const r = await fetch(url);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
|
|
||||||
|
// Banner mit Anzeige-Info
|
||||||
|
let bannerHtml = '';
|
||||||
|
if (_atSingleDate) {
|
||||||
|
bannerHtml += `<span style="display:inline-block;padding:3px 10px;background:rgba(0,157,165,0.18);color:var(--ecg-teal);border-radius:11px;font-weight:700;margin-right:8px;">Tag: ${_atSingleDate}</span>`;
|
||||||
|
bannerHtml += `<button onclick="clearDateFilter()" style="font-family:var(--font-mono);font-size:11px;padding:2px 8px;border:1px solid var(--ecg-border);border-radius:3px;background:var(--ecg-card-bg);cursor:pointer;">× Tag-Filter entfernen</button>`;
|
||||||
|
bannerHtml += '<br>';
|
||||||
|
}
|
||||||
|
bannerHtml += `<strong>${data.n_shown || 0}</strong> News angezeigt · ${data.n_in_window || 0} News im Zeitraum (mit Embedding) · ${data.n_total_news || 0} insgesamt embedded`;
|
||||||
|
if (onlyRel === '1' && data.n_in_window > data.n_shown) {
|
||||||
|
bannerHtml += ` · <em>Filter "nur GWÖ-relevant" aktiv</em>`;
|
||||||
|
}
|
||||||
|
if (data.n_in_window > parseInt(topk)) {
|
||||||
|
bannerHtml += ` · top_k=${topk} ist limitierend, mehr News verfügbar`;
|
||||||
|
}
|
||||||
|
banner.innerHTML = bannerHtml;
|
||||||
|
|
||||||
if (!data.buckets || !data.buckets.length) {
|
if (!data.buckets || !data.buckets.length) {
|
||||||
list.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Keine News im Zeitfenster ' + (onlyRel === '1' ? '(Filter: nur GWÖ-relevant aktiv — versuch ohne Filter)' : 'oder noch nicht embedded') + '.</div>';
|
list.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">' + (
|
||||||
|
_atSingleDate ? `Keine News am ${_atSingleDate}` :
|
||||||
|
(onlyRel === '1' ? 'Keine News mit GWÖ-Relevanz im Zeitfenster — versuch Filter aus' : 'Keine News im Zeitfenster oder noch nicht embedded')
|
||||||
|
) + '.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,9 +513,27 @@ async function loadZeitreihe() {
|
|||||||
data: { labels: data.buckets, datasets: datasets },
|
data: { labels: data.buckets, datasets: datasets },
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
|
onClick: (event, elements) => {
|
||||||
|
// Aus Chart.getElementsAtEventForMode: nehme das X-Achsen-Element
|
||||||
|
const points = _atZeitreiheChart.getElementsAtEventForMode(
|
||||||
|
event, 'index', { intersect: false }, false,
|
||||||
|
);
|
||||||
|
if (points.length > 0) {
|
||||||
|
const dayLabel = data.buckets[points[0].index];
|
||||||
|
if (dayLabel) {
|
||||||
|
_atSingleDate = dayLabel;
|
||||||
|
// Auf den News-Tab wechseln
|
||||||
|
const newsTabBtn = document.querySelector('.at-tab[data-tab="news"]');
|
||||||
|
if (newsTabBtn) newsTabBtn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHover: (event, elements) => {
|
||||||
|
event.native.target.style.cursor = elements.length > 0 ? 'pointer' : 'default';
|
||||||
|
},
|
||||||
scales: {
|
scales: {
|
||||||
y: { beginAtZero: true, stacked: true, title: { display: true, text: 'Artikel/Tag' } },
|
y: { beginAtZero: true, stacked: true, title: { display: true, text: 'Artikel/Tag' } },
|
||||||
x: { title: { display: true, text: 'Datum' } },
|
x: { title: { display: true, text: 'Datum (klick öffnet den News-Tab gefiltert)' } },
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { position: 'bottom' }
|
legend: { position: 'bottom' }
|
||||||
@ -495,7 +542,7 @@ async function loadZeitreihe() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const total = Object.values(data.series).reduce((s, arr) => s + arr.reduce((a, b) => a + b, 0), 0);
|
const total = Object.values(data.series).reduce((s, arr) => s + arr.reduce((a, b) => a + b, 0), 0);
|
||||||
meta.textContent = `${total} News-Artikel über ${data.buckets.length} Tage, ${data.sources.length} Quellen.`;
|
meta.innerHTML = `${total} News-Artikel über ${data.buckets.length} Tage, ${data.sources.length} Quellen. <strong>Klick auf einen Tag</strong> wechselt zum News-Tab gefiltert auf diesen Tag.`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
meta.textContent = 'Fehler: ' + e;
|
meta.textContent = 'Fehler: ' + e;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -270,6 +270,7 @@ def aggregate_top_themen(
|
|||||||
min_similarity: float = 0.4,
|
min_similarity: float = 0.4,
|
||||||
matches_per_news: int = 3,
|
matches_per_news: int = 3,
|
||||||
only_relevant: bool = False,
|
only_relevant: bool = False,
|
||||||
|
single_date: Optional[str] = None,
|
||||||
db_path: Optional[Path] = None,
|
db_path: Optional[Path] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Top-K aktuelle News (letzte N Tage) mit jeweils ihren passendsten
|
"""Top-K aktuelle News (letzte N Tage) mit jeweils ihren passendsten
|
||||||
@ -313,10 +314,17 @@ def aggregate_top_themen(
|
|||||||
).timestamp()
|
).timestamp()
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
continue
|
continue
|
||||||
if news_ts < cutoff:
|
# single_date hat Vorrang: nur News dieses Tages
|
||||||
|
if single_date:
|
||||||
|
if not n["datum"].startswith(single_date):
|
||||||
|
continue
|
||||||
|
elif news_ts < cutoff:
|
||||||
continue
|
continue
|
||||||
n["_ts"] = news_ts
|
n["_ts"] = news_ts
|
||||||
fresh.append(n)
|
fresh.append(n)
|
||||||
|
|
||||||
|
n_in_window = len(fresh)
|
||||||
|
|
||||||
# Nach Datum desc sortieren, top_k cutten
|
# Nach Datum desc sortieren, top_k cutten
|
||||||
fresh.sort(key=lambda x: x["_ts"], reverse=True)
|
fresh.sort(key=lambda x: x["_ts"], reverse=True)
|
||||||
fresh = fresh[:top_k]
|
fresh = fresh[:top_k]
|
||||||
@ -387,12 +395,15 @@ def aggregate_top_themen(
|
|||||||
return {
|
return {
|
||||||
"buckets": buckets,
|
"buckets": buckets,
|
||||||
"n_total_news": len(news_rows),
|
"n_total_news": len(news_rows),
|
||||||
|
"n_in_window": n_in_window,
|
||||||
|
"n_shown": len(buckets),
|
||||||
"filter": {
|
"filter": {
|
||||||
"days_window": days_window,
|
"days_window": days_window,
|
||||||
"top_k": top_k,
|
"top_k": top_k,
|
||||||
"min_similarity": min_similarity,
|
"min_similarity": min_similarity,
|
||||||
"matches_per_news": matches_per_news,
|
"matches_per_news": matches_per_news,
|
||||||
"only_relevant": only_relevant,
|
"only_relevant": only_relevant,
|
||||||
|
"single_date": single_date,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user