Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion generative/eval_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,12 @@ def _chart_longitudinal(log_data: dict) -> dict:
n_pts = [len(vm[v]) if vm.get(v) else 0 for v in versions]
datasets.append(
{
"label": _PDF_LABELS.get(key, key),
# Multi-Perspektiven-Review 2026-07-15 U4: fuer PDFs ausserhalb der
# 3 registrierten _PDF_LABELS-Eintraege zeigte das Trade-off-Chart-2
# (Akzeptanzrate ueber Versionen) den rohen, kleingeschriebenen Key
# (z.B. "cobaltite-paper") -- ueber DB-Fallback-Runs inzwischen der
# Regelfall statt die Ausnahme. Fallback prettified statt roh.
"label": _PDF_LABELS.get(key) or re.sub(r"[-_]+", " ", key).strip().title(),
"data": data_pts,
"n": n_pts,
"color": _pdf_color(key),
Expand Down
5 changes: 5 additions & 0 deletions generative/eval_dashboard_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,11 +604,16 @@ def build_data(
# all_log_runs — so erscheinen nur PDFs mit echten Daten im View (sonst
# listet das Dropdown z. B. foss-only evaluierte PDFs, die „0 Notes" ergeben,
# solange kein foss-Modell gewählt ist). Volltitel, dedupliziert.
# D6 (Multi-Perspektiven-Review 2026-07-15): Sprachfilter fehlte hier — das
# Versions-Dropdown direkt darunter berücksichtigt ihn bereits (`if
# language: _pver_rows = ...`), dieselbe Einschränkung fehlte am
# PDF-Dropdown, das dadurch auch PDFs der jeweils anderen Sprache listete.
_all_pdfs_opts = D._dedupe_pdf_options(
r.get("pdf")
for r in all_quality_rows
if r.get("eval_version") == eval_version
and r.get("pdf")
and (not language or r.get("language") == language)
and not (_exclude_foss and D.is_foss_version(r.get("version") or r.get("pipeline_version") or ""))
)
# Versions-Dropdown: nur Pipeline-Versionen MIT Eval-Daten in der aktiven
Expand Down
22 changes: 22 additions & 0 deletions generative/tests/test_dashboard_ch1_zero_n_marker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Test für #U5 (Multi-Perspektiven-Review 2026-07-15): n=0-Balken im
Akzeptanz-Chart (ch1) — das aufgedruckte Wert-Label ("100,0 %") wirkte wie ein
echter Wert, obwohl 0 Notes evaluiert wurden (nur Routing-Daten, siehe #249).
Die Balkenfarbe war für diesen Fall bereits grau (barColor), das Wert-Label
selbst trug aber kein eigenes Signal.

Fix: bei nMap[label]==0 (dieselbe Bedingung wie barColor) wird das Wert-Label
zusätzlich per globalAlpha gedimmt UND mit einem Sternchen ("100,0 %*")
gekennzeichnet.
"""

from __future__ import annotations

from generative.eval_dashboard_server import _build_live_html


def test_ch1_value_label_marks_zero_n_bars_with_asterisk_and_dimming():
html = _build_live_html()
assert "isEmpty?.55:1" in html or "isEmpty ? .55 : 1" in html
assert "isEmpty?'*':''" in html or "isEmpty ? '*' : ''" in html
# Dieselbe n===0-Bedingung wie barColor -- kein zweiter Datenpfad.
assert "const isEmpty = (nMap[rawPairs[i]?.l]||0)===0;" in html
14 changes: 14 additions & 0 deletions generative/tests/test_dashboard_filter_badges.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ def test_filter_badges_have_always_visible_reset_button():
html = _build_live_html()
assert "fbadge-reset" in html
assert "resetAllFilters()" in html


def test_run_filter_badge_uses_option_text_not_raw_id():
"""U3 (Multi-Perspektiven-Review 2026-07-15): Der Lauf-Filter-Badge zeigte
die rohe Run-ID aus _globalFilters.run (select-value, z.B.
"20260713-084724") statt des sichtbaren Options-Texts ("13.07. 08:47 ·
Schlebbe…"). Fix: für den run-Filter wird der Options-Text
(selectedOptions[0].text) ins Badge übernommen."""
html = _build_live_html()
assert "selectedOptions" in html
assert "opt.text" in html
# Regressions-Waechter: das alte Verhalten (immer roher select-value `v`
# ohne Sonderfall fuer 'run') darf nicht wiederkehren.
assert "${_escHTML(labels[k])}: <strong>${_escHTML(v)}</strong>" not in html
25 changes: 25 additions & 0 deletions generative/tests/test_dashboard_followups.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,31 @@ def test_chart_longitudinal_n_zero_for_missing_version():
assert ds["n"] == [1]


# ------------------------------------------ Multi-Perspektiven-Review 2026-07-15 U4
def test_chart_longitudinal_prettifies_unregistered_pdf_key():
"""Trade-off-Chart-2 (Akzeptanzrate ueber Versionen, ch4/leg4) zeigte fuer PDFs
ausserhalb der 3 registrierten _PDF_LABELS-Eintraege (bates/kuhlthau/schlebbe)
den rohen, kleingeschriebenen Key im Chart-Label (z.B. "cobaltite-paper" statt
"Cobaltite Paper") -- inzwischen werden ueber die DB-Fallback-Runs zahlreiche
weitere PDFs evaluiert, fuer die dieser Fallback der Regelfall ist."""
runs = [{"key": "cobaltite-paper", "label": "irrelevant", "ver": "v1", "accept_pct": 50.0}]
from generative.eval_dashboard import _build_log_data

log_data = _build_log_data(runs)
out = _chart_longitudinal(log_data)
assert out["datasets"][0]["label"] == "Cobaltite Paper"


def test_chart_longitudinal_keeps_registered_pdf_label():
"""Regression: registrierte Kurzschluessel (_PDF_LABELS) bleiben unveraendert."""
runs = [{"key": "bates", "label": "irrelevant", "ver": "v1", "accept_pct": 50.0}]
from generative.eval_dashboard import _build_log_data

log_data = _build_log_data(runs)
out = _chart_longitudinal(log_data)
assert out["datasets"][0]["label"] == "Bates 2017"


# ----------------------------------------------------- foss/generative-Trennung
def test_is_foss_version_detects_foss_prefix():
assert is_foss_version("foss-v0.1.1") is True
Expand Down
68 changes: 68 additions & 0 deletions generative/tests/test_dashboard_pdf_dropdown_language_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Test für #D6 (Multi-Perspektiven-Review 2026-07-15): PDF-Dropdown ignoriert
aktiven Sprachfilter.

Befund: Das Versions-Dropdown (`_all_pvers_opts`, eval_dashboard_server.py)
schränkt sich bei aktivem Sprachfilter bereits auf die passenden Zeilen ein
(`if language: _pver_rows = [...]`). Das PDF-Dropdown direkt darüber
(`_all_pdfs_opts`) tat das nicht — bei z.B. Sprachfilter "DE→DE" erschienen
weiterhin PDFs, die nur in anderen Sprachen evaluiert wurden.
"""

from __future__ import annotations


def _eval_row(run_id, ver, pdf, language, eval_version="4.1"):
return {
"run_id": run_id,
"note_path": f"notes/{run_id}.md",
"acceptance_status": "vault",
"hallucination_rate": 0.05,
"coverage_factual": 0.8,
"pipeline_version": ver,
"version": ver,
"pdf": pdf,
"language": language,
"eval_version": eval_version,
"anchors_total": 10,
"anchors_hallucinated": 1,
}


def test_pdf_dropdown_respects_active_language_filter(monkeypatch):
from generative import db as _gdb
from generative import eval_dashboard as D
from generative import eval_dashboard_server as S

evals = [
_eval_row("r-de", "v0.3.140", "Deutsches-Paper.pdf", "DE→DE"),
_eval_row("r-en", "v0.3.140", "English-Paper.pdf", "EN→DE"),
]
monkeypatch.setattr(_gdb, "query_pipeline_runs", lambda *a, **k: [])
monkeypatch.setattr(_gdb, "query_note_evals", lambda *a, **k: [dict(r) for r in evals])
monkeypatch.setattr(D, "_read_all_log_runs", lambda: [])
monkeypatch.setattr(D, "_read_token_runs", lambda: [])

data = S.build_data(language="DE→DE")

assert any("Deutsches-Paper" in o for o in data["all_pdfs"])
assert not any("English-Paper" in o for o in data["all_pdfs"])


def test_pdf_dropdown_shows_all_pdfs_without_language_filter(monkeypatch):
from generative import db as _gdb
from generative import eval_dashboard as D
from generative import eval_dashboard_server as S

evals = [
_eval_row("r-de", "v0.3.140", "Deutsches-Paper.pdf", "DE→DE"),
_eval_row("r-en", "v0.3.140", "English-Paper.pdf", "EN→DE"),
]
monkeypatch.setattr(_gdb, "query_pipeline_runs", lambda *a, **k: [])
monkeypatch.setattr(_gdb, "query_note_evals", lambda *a, **k: [dict(r) for r in evals])
monkeypatch.setattr(D, "_read_all_log_runs", lambda: [])
monkeypatch.setattr(D, "_read_token_runs", lambda: [])

data = S.build_data()

assert any("Deutsches-Paper" in o for o in data["all_pdfs"])
assert any("English-Paper" in o for o in data["all_pdfs"])
29 changes: 29 additions & 0 deletions generative/tests/test_dashboard_version_label_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Test für #U7 (Multi-Perspektiven-Review 2026-07-15): kpi-ver-label wird per
CSS uppercase gerendert.

Befund: `.sub-label` hat `text-transform: uppercase` (Sub-Head-Beschriftungen
wie "QUALITÄT" sollen groß sein). Das trifft aber auch die eingebettete
Versionsnummer `#kpi-ver-label` ("V0.3.143" statt "v0.3.143") -- Pipeline-
Versionen sind kleingeschrieben (`v0.3.143`), Großschreibung verfälscht sie.
Fix: `text-transform: none` auf dem Versions-Span, damit `.sub-label`s
Uppercase-Regel es nicht mehr trifft.
"""

from __future__ import annotations

from generative.eval_dashboard_server import _build_live_html


def test_kpi_ver_label_excluded_from_uppercase_transform():
html = _build_live_html()
assert "#kpi-ver-label" in html
assert "text-transform: none" in html


def test_kpi_ver_label_perf_also_excluded_for_future_pr291_label():
# #291 (offen, Stand dieser Änderung) fügt ein zweites Versions-Label
# id="kpi-ver-label-perf" am "Kosten & Performance"-Header hinzu -- der
# CSS-Selektor deckt es bereits ab, damit es denselben Fix erbt sobald
# #291 gemergt ist (kein Nacharbeits-Bedarf).
html = _build_live_html()
assert "#kpi-ver-label-perf" in html
33 changes: 27 additions & 6 deletions internal/dashboard/eval_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@
font-family: var(--f-mono); font-size: 11px; font-weight: 600;
color: var(--ink-2); text-transform: uppercase; letter-spacing: .10em;
}
/* U7 (Multi-Perspektiven-Review 2026-07-15): .sub-label ist uppercase, trifft
aber auch die eingebettete Versionsnummer ("V0.3.143" statt "v0.3.143") --
Versions-Spans von der Transformation ausnehmen. */
#kpi-ver-label, #kpi-ver-label-perf { text-transform: none; }
.sub-meta {
font-family: var(--f-body); font-size: 11px;
color: var(--ink-4); font-style: italic;
Expand Down Expand Up @@ -1192,14 +1196,23 @@
const labels = { pver: 'Version', pdf: 'PDF', lang: 'Sprache', model: 'Modell', run: 'Lauf' };
const active = Object.entries(_globalFilters)
.filter(([k, v]) => v && v !== '__all__' && labels[k]);
const badges = active.map(([k, v]) =>
`<span class="fbadge">
${_escHTML(labels[k])}: <strong>${_escHTML(v)}</strong>
const badges = active.map(([k, v]) => {
// U3 (Multi-Perspektiven-Review 2026-07-15): Lauf-Filter speichert im
// select-value die rohe Run-ID ("20260713-084724") -- im Badge soll der
// sichtbare Options-Text stehen ("13.07. 08:47 · ..."), nicht die ID.
let display = v;
if (k === 'run') {
const runSel = document.getElementById('global-run');
const opt = runSel && runSel.selectedOptions && runSel.selectedOptions[0];
if (opt && opt.value === v) display = opt.text;
}
return `<span class="fbadge">
${_escHTML(labels[k])}: <strong>${_escHTML(display)}</strong>
<button onclick="_clearFilter('${_escHTML(k)}')" title="Filter entfernen" aria-label="Filter ${_escHTML(labels[k])} entfernen">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M2 2l6 6M8 2l-6 6"/></svg>
</button>
</span>`
).join('');
</span>`;
}).join('');
// #236: Reset auch außerhalb des Empty-State (0 Treffer) erreichbar, sobald
// mindestens ein Filter aktiv ist — vorher steckte der einzige Reset im
// Empty-Banner (nur bei 0 Treffern sichtbar).
Expand Down Expand Up @@ -1780,7 +1793,15 @@
const {ctx}=chart; ctx.save(); ctx.font='600 10.5px "IBM Plex Sans"'; ctx.textAlign='left'; ctx.textBaseline='middle';
chart.getDatasetMeta(0).data.forEach((bar,i)=>{
const val=rawPairs[i]?.v??0; const col=barColor(rawPairs[i]);
ctx.fillStyle=col; ctx.fillText(_fmtDE(val)+' %', bar.x+6, bar.y);
// U5 (Multi-Perspektiven-Review 2026-07-15): bei n=0 (keine Eval-
// Basis) wirkte das aufgedruckte "100,0 %" wie ein echter Wert —
// dieselbe Bedingung wie barColor (n===0), zusätzlich per Sternchen
// im Text UND per globalAlpha gedimmt gekennzeichnet (die graue
// Balkenfarbe allein reicht als Signal nicht).
const isEmpty = (nMap[rawPairs[i]?.l]||0)===0;
ctx.fillStyle=col; ctx.globalAlpha=isEmpty?.55:1;
ctx.fillText(_fmtDE(val)+' %'+(isEmpty?'*':''), bar.x+6, bar.y);
ctx.globalAlpha=1;
}); ctx.restore();
}}]
});
Expand Down
Loading