From c61dceec59ae2adb69135dd1f0da7591b976602d Mon Sep 17 00:00:00 2001 From: TillQuandel Date: Wed, 15 Jul 2026 11:07:49 +0200 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20UX-Politur=20=E2=80=94=20Run-?= =?UTF-8?q?Badge-Label,=20Trade-off-Legenden,=20Versions-Label-Case,=20n?= =?UTF-8?q?=3D0-Wertkennzeichnung,=20PDF-Dropdown-Sprachfilter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fünf unabhängig verifizierte Fixes aus dem Multi-Perspektiven-Review 2026-07-15: U3: Lauf-Filter-Badge zeigte die rohe Run-ID (select-value) statt des lesbaren Options-Texts. _renderFilterBadges() übernimmt für den run-Filter jetzt selectedOptions[0].text. U4: Trade-off-Chart-2 ("Akzeptanzrate über Versionen", ch4/leg4) zeigte für PDFs außerhalb der 3 registrierten _PDF_LABELS-Einträge den rohen, kleingeschriebenen Key (z.B. "cobaltite-paper") — inzwischen über die DB-Fallback-Runs der Regelfall statt die Ausnahme. Fallback prettified ("Cobaltite Paper") statt roh. U7: .sub-label ist uppercase, traf auch die eingebettete Versionsnummer ("V0.3.143" statt "v0.3.143"). text-transform:none auf #kpi-ver-label; #kpi-ver-label-perf (PR #291, offen) vorsorglich mit abgedeckt. U5: ch1-Wert-Labels bei n=0 (keine Eval-Basis) wirkten wie echte Werte. Dieselbe Bedingung wie barColor zusätzlich per Sternchen ("100,0 %*") und globalAlpha-Dimmung im Text markiert. D6: PDF-Dropdown ignorierte den aktiven Sprachfilter — das Versions-Dropdown direkt darunter berücksichtigte ihn bereits. Gleiche Einschränkung jetzt auch auf die PDF-Optionen angewandt. --- generative/eval_dashboard.py | 7 +- generative/eval_dashboard_server.py | 5 ++ .../tests/test_dashboard_ch1_zero_n_marker.py | 22 ++++++ .../tests/test_dashboard_filter_badges.py | 14 ++++ generative/tests/test_dashboard_followups.py | 25 +++++++ ..._dashboard_pdf_dropdown_language_filter.py | 68 +++++++++++++++++++ .../test_dashboard_version_label_case.py | 29 ++++++++ internal/dashboard/eval_dashboard.html | 33 +++++++-- 8 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 generative/tests/test_dashboard_ch1_zero_n_marker.py create mode 100644 generative/tests/test_dashboard_pdf_dropdown_language_filter.py create mode 100644 generative/tests/test_dashboard_version_label_case.py diff --git a/generative/eval_dashboard.py b/generative/eval_dashboard.py index 59090b5..310f1dd 100644 --- a/generative/eval_dashboard.py +++ b/generative/eval_dashboard.py @@ -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), diff --git a/generative/eval_dashboard_server.py b/generative/eval_dashboard_server.py index 1efdd3a..a50e415 100644 --- a/generative/eval_dashboard_server.py +++ b/generative/eval_dashboard_server.py @@ -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 diff --git a/generative/tests/test_dashboard_ch1_zero_n_marker.py b/generative/tests/test_dashboard_ch1_zero_n_marker.py new file mode 100644 index 0000000..314ed23 --- /dev/null +++ b/generative/tests/test_dashboard_ch1_zero_n_marker.py @@ -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 diff --git a/generative/tests/test_dashboard_filter_badges.py b/generative/tests/test_dashboard_filter_badges.py index f400df4..4cddf3c 100644 --- a/generative/tests/test_dashboard_filter_badges.py +++ b/generative/tests/test_dashboard_filter_badges.py @@ -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])}: ${_escHTML(v)}" not in html diff --git a/generative/tests/test_dashboard_followups.py b/generative/tests/test_dashboard_followups.py index 67b4dc6..539b02e 100644 --- a/generative/tests/test_dashboard_followups.py +++ b/generative/tests/test_dashboard_followups.py @@ -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 diff --git a/generative/tests/test_dashboard_pdf_dropdown_language_filter.py b/generative/tests/test_dashboard_pdf_dropdown_language_filter.py new file mode 100644 index 0000000..a074a8e --- /dev/null +++ b/generative/tests/test_dashboard_pdf_dropdown_language_filter.py @@ -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"]) diff --git a/generative/tests/test_dashboard_version_label_case.py b/generative/tests/test_dashboard_version_label_case.py new file mode 100644 index 0000000..8194fcd --- /dev/null +++ b/generative/tests/test_dashboard_version_label_case.py @@ -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 diff --git a/internal/dashboard/eval_dashboard.html b/internal/dashboard/eval_dashboard.html index 43cbdab..b1a9c26 100644 --- a/internal/dashboard/eval_dashboard.html +++ b/internal/dashboard/eval_dashboard.html @@ -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; @@ -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]) => - ` - ${_escHTML(labels[k])}: ${_escHTML(v)} + 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 ` + ${_escHTML(labels[k])}: ${_escHTML(display)} - ` - ).join(''); + `; + }).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). @@ -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(); }}] });