"""HTML renderer: a self-contained dashboard page (no server, no deps). Generates one index.html with sortable metric tables, per-benchmark bars, per-sample drill-down and error browser. Consumed by CLI --out-dir. """ import html import json from typing import Dict, List, Union from ...eval.record import EvalReport from .. import register_renderer @register_renderer('html') def html_dashboard(target: Union[EvalReport, List[EvalReport]], opts: Dict) -> str: reports = target if isinstance(target, list) else [target] reports = [r for r in reports if isinstance(r, EvalReport)] cards = [] for rep in reports: cards.append(_card(rep)) rows_json = json.dumps([_row(r) for r in reports], ensure_ascii=False) return _PAGE.replace('__ROWS__', rows_json).replace('__CARDS__', '\n'.join(cards)) def _row(rep: EvalReport) -> Dict: primary = next((k for k in rep.metrics if k != 'extraction_failure_rate'), '') return { 'dataset': rep.dataset, 'recipe': rep.recipe, 'model': rep.model, 'n': rep.num_samples, 'primary': primary, 'value': rep.metrics.get(primary, 0.0), 'metrics': {k: v for k, v in rep.metrics.items()}, 'groups': {k: v for k, v in rep.metric_groups.items() if isinstance(v, dict)}, 'extract_fail': rep.num_failed_extractions, 'samples': [ {'id': s.sample_id, 'ok': s.extraction_ok, 'extracted': s.extracted_prediction[:200], 'raw': s.raw_prediction[:300], 'target': str(s.target)[:120], 'scores': s.scores, 'error': s.error[:200]} for s in rep.samples[:200] ], } def _card(rep: EvalReport) -> str: primary = next((k for k in rep.metrics if k != 'extraction_failure_rate'), '') value = rep.metrics.get(primary, 0.0) pct = f'{value * 100:.1f}%' warn = (f'extraction failures: {rep.num_failed_extractions}' f'/{rep.num_samples}') if rep.num_failed_extractions else '' return (f'
{html.escape(rep.dataset)}
' f'
{pct}
{html.escape(primary)} · n={rep.num_samples}' f' · {html.escape(rep.model or "?")}
{warn}
') _PAGE = """ EvalHarness Dashboard

EvalHarness Dashboard generated by evalharness viz

__CARDS__
datasetmodel nmetric valueshareextract✗
click a row for per-sample details
"""