use crate::eval::EvalReport;
pub fn generate_html(report: &EvalReport, title: &str) -> String {
let min_score = report
.results
.iter()
.map(|r| r.score)
.fold(f64::INFINITY, f64::min);
let max_score = report
.results
.iter()
.map(|r| r.score)
.fold(f64::NEG_INFINITY, f64::max);
let rows: String = report
.results
.iter()
.map(|r| {
let status = if r.success { "PASS" } else { "FAIL" };
let color = if r.success { "#22c55e" } else { "#ef4444" };
let css_class = if r.success { "pass" } else { "fail" };
format!(
"<tr class='{css_class}' style='border-bottom:1px solid #eee'>\
<td style='color:{color};font-weight:bold'>{status}</td>\
<td>{}</td>\
<td style='text-align:right'>{:.2}</td>\
<td style='text-align:right'>{}</td>\
<td style='text-align:right'>{}</td>\
<td style='text-align:right'>{}ms</td>\
<td style='font-size:12px;color:#666'>{}</td>\
</tr>",
r.case_id,
r.score,
r.tool_calls,
r.file_changes,
r.duration_ms,
r.violations.first().unwrap_or(&String::new())
)
})
.collect::<Vec<_>>()
.join("\n");
format!(
r#"<!DOCTYPE html><html><head><meta charset="UTF-8">
<title>{title}</title>
<style>
body {{ font-family: system-ui, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; }}
h1 {{ color: #1e293b; }} .summary {{ display: flex; gap: 1rem; flex-wrap: wrap; margin: 1rem 0; }}
.card {{ background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; min-width: 120px; text-align: center; }}
.card .value {{ font-size: 1.5rem; font-weight: bold; color: #1e293b; }}
.card .label {{ font-size: 0.8rem; color: #64748b; }}
table {{ width: 100%; border-collapse: collapse; margin-top: 1rem; }}
th {{ text-align: left; padding: 8px; background: #f1f5f9; }}
td {{ padding: 8px; }}
.bar {{ display: inline-block; height: 12px; background: #3b82f6; border-radius: 2px; }}
.filters {{ margin: 1rem 0; }}
.filters button {{ padding: 6px 16px; margin-right: 8px; border: 1px solid #e2e8f0; border-radius: 4px; background: #fff; cursor: pointer; }}
.filters button.active {{ background: #3b82f6; color: #fff; border-color: #3b82f6; }}
tr.hidden {{ display: none; }}
</style></head><body>
<h1>{title}</h1>
<div class="summary">
<div class="card"><div class="value">{passed}/{total}</div><div class="label">Passed</div></div>
<div class="card"><div class="value">{avg_score:.2}</div><div class="label">Avg Score</div></div>
<div class="card"><div class="value">{min_score:.2} / {max_score:.2}</div><div class="label">Min / Max</div></div>
<div class="card"><div class="value">{std_dev:.3}</div><div class="label">Std Dev</div></div>
<div class="card"><div class="value">{total_tool_calls}</div><div class="label">Tool Calls</div></div>
<div class="card"><div class="value">{total_tokens_in}+{total_tokens_out}</div><div class="label">Tokens I/O</div></div>
</div>
<div class="filters">
<button class="active" onclick="filter('all')">All ({total})</button>
<button onclick="filter('pass')">Passed ({passed})</button>
<button onclick="filter('fail')">Failed ({failed})</button>
</div>
<table>
<tr><th>Status</th><th>Case</th><th>Score</th><th>Tools</th><th>Files</th><th>Time</th><th>Violations</th></tr>
{rows}
</table>
<script>
function filter(type) {{
document.querySelectorAll('.filters button').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
document.querySelectorAll('tbody tr, table tr:not(:first-child)').forEach(row => {{
if (type === 'all') row.classList.remove('hidden');
else if (type === 'pass') row.classList.toggle('hidden', !row.classList.contains('pass'));
else if (type === 'fail') row.classList.toggle('hidden', !row.classList.contains('fail'));
}});
}}
</script>
<p style="margin-top:2rem;font-size:12px;color:#94a3b8">Generated by echo-agent eval framework</p>
</body></html>"#,
title = title,
passed = report.passed,
failed = report.total.saturating_sub(report.passed),
total = report.total,
avg_score = report.avg_score,
min_score = if min_score.is_infinite() {
0.0
} else {
min_score
},
max_score = if max_score.is_infinite() {
0.0
} else {
max_score
},
std_dev = report.std_dev,
total_tool_calls = report.total_tool_calls,
total_tokens_in = report.total_tokens_in,
total_tokens_out = report.total_tokens_out,
rows = rows,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::{EvalReport, EvalResult};
#[test]
fn test_generate_html() {
let result = EvalResult::new("test1", true).with_metric("accuracy", 0.95, "good");
let mut r = result;
r.tool_calls = 3;
r.file_changes = 1;
r.duration_ms = 1200;
let report = EvalReport::new(vec![r]);
let html = generate_html(&report, "Test Report");
assert!(html.contains("Test Report"));
assert!(html.contains("PASS"));
assert!(html.contains("1/1"));
}
}