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,
|
||||
matches_per_news: int = 3,
|
||||
only_relevant: bool = False,
|
||||
date: Optional[str] = None,
|
||||
):
|
||||
"""Top-K News der letzten N Tage mit Antrags-Match.
|
||||
|
||||
Mit `only_relevant=true` werden News mit Relevance-Level "low" oder
|
||||
"none" rausgefiltert.
|
||||
|
||||
Mit `date=YYYY-MM-DD` werden nur News dieses Tages angezeigt
|
||||
(overrides `days`).
|
||||
"""
|
||||
from .themen_matching import aggregate_top_themen
|
||||
return aggregate_top_themen(
|
||||
@ -2044,6 +2048,7 @@ async def api_aktuelle_themen_top(
|
||||
min_similarity=min_similarity,
|
||||
matches_per_news=matches_per_news,
|
||||
only_relevant=only_relevant,
|
||||
single_date=date,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -168,7 +168,7 @@
|
||||
<option value="30">30 Tage</option>
|
||||
</select>
|
||||
<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>
|
||||
<select id="at-minsim" onchange="loadActiveTab()">
|
||||
<option value="0.30">0.30 (locker)</option>
|
||||
@ -193,6 +193,7 @@
|
||||
|
||||
<!-- Tab Panels -->
|
||||
<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 style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade …</div>
|
||||
</div>
|
||||
@ -253,6 +254,7 @@
|
||||
<script>
|
||||
let _atZeitreiheChart = null;
|
||||
let _atActiveTab = 'news';
|
||||
let _atSingleDate = null; // YYYY-MM-DD wenn aus Chart geklickt
|
||||
|
||||
function atScoreClass(score) {
|
||||
if (score == null) return '';
|
||||
@ -303,20 +305,47 @@ function loadActiveTab() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearDateFilter() {
|
||||
_atSingleDate = null;
|
||||
loadActiveTab();
|
||||
}
|
||||
|
||||
async function loadThemen() {
|
||||
const days = document.getElementById('at-days').value;
|
||||
const topk = document.getElementById('at-topk').value;
|
||||
const minsim = document.getElementById('at-minsim').value;
|
||||
const onlyRel = document.getElementById('at-only-relevant').checked ? '1' : '0';
|
||||
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>';
|
||||
|
||||
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();
|
||||
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -484,9 +513,27 @@ async function loadZeitreihe() {
|
||||
data: { labels: data.buckets, datasets: datasets },
|
||||
options: {
|
||||
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: {
|
||||
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: {
|
||||
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);
|
||||
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) {
|
||||
meta.textContent = 'Fehler: ' + e;
|
||||
}
|
||||
|
||||
@ -270,6 +270,7 @@ def aggregate_top_themen(
|
||||
min_similarity: float = 0.4,
|
||||
matches_per_news: int = 3,
|
||||
only_relevant: bool = False,
|
||||
single_date: Optional[str] = None,
|
||||
db_path: Optional[Path] = None,
|
||||
) -> dict:
|
||||
"""Top-K aktuelle News (letzte N Tage) mit jeweils ihren passendsten
|
||||
@ -313,10 +314,17 @@ def aggregate_top_themen(
|
||||
).timestamp()
|
||||
except (ValueError, AttributeError):
|
||||
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
|
||||
n["_ts"] = news_ts
|
||||
fresh.append(n)
|
||||
|
||||
n_in_window = len(fresh)
|
||||
|
||||
# Nach Datum desc sortieren, top_k cutten
|
||||
fresh.sort(key=lambda x: x["_ts"], reverse=True)
|
||||
fresh = fresh[:top_k]
|
||||
@ -387,12 +395,15 @@ def aggregate_top_themen(
|
||||
return {
|
||||
"buckets": buckets,
|
||||
"n_total_news": len(news_rows),
|
||||
"n_in_window": n_in_window,
|
||||
"n_shown": len(buckets),
|
||||
"filter": {
|
||||
"days_window": days_window,
|
||||
"top_k": top_k,
|
||||
"min_similarity": min_similarity,
|
||||
"matches_per_news": matches_per_news,
|
||||
"only_relevant": only_relevant,
|
||||
"single_date": single_date,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user