/**
* Report Generator - HTML dashboard reports with charts
*
* Generates comprehensive HTML reports with visualizations.
*/
import * as fs from 'fs';
import * as path from 'path';
import { BenchResult, BenchSummary } from './bench';
interface ModelStats {
model: string;
total_tests: number;
success_count: number;
success_rate: number;
pass_count: number;
pass_rate: number;
avg_score: number;
avg_time: number;
}
/**
* Generate comprehensive HTML reports with all UI features.
*/
export class ReportGenerator {
/**
* Generate enhanced HTML report.
*/
static generate(
results: BenchResult[],
summary: BenchSummary,
outputPath?: string
): string | null {
if (!results.length) {
console.log('No results to generate report from');
return null;
}
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, '')
.slice(0, 15);
const finalPath =
outputPath || `output/reports/benchmark_report_${timestamp}.html`;
// Ensure directory exists
const outputDir = path.dirname(finalPath);
fs.mkdirSync(outputDir, { recursive: true });
// Generate HTML content
const htmlContent = ReportGenerator.buildHtml(results, summary);
// Write to file
fs.writeFileSync(finalPath, htmlContent);
console.log(`๐ Report generated: ${finalPath}`);
return finalPath;
}
private static buildHtml(
results: BenchResult[],
summary: BenchSummary
): string {
const stats = ReportGenerator.calculateStats(results);
const css = ReportGenerator.generateCss();
const summaryCards = ReportGenerator.generateSummaryCards(summary, stats);
const resultsTable = ReportGenerator.generateResultsTable(results);
const leaderboard = ReportGenerator.generateLeaderboard(stats);
const javascript = ReportGenerator.generateJavaScript(stats);
return `
PraisonAI Bench TypeScript - Report
๐ Dashboard
${summaryCards}
Status Distribution
Execution Time by Model
๐ Leaderboard
${leaderboard}
๐ All Test Results
${resultsTable}
`;
}
private static calculateStats(results: BenchResult[]): {
modelStats: ModelStats[];
successCount: number;
failureCount: number;
} {
const byModel: Record = {};
for (const r of results) {
const model = r.model || 'unknown';
if (!byModel[model]) {
byModel[model] = [];
}
byModel[model].push(r);
}
const modelStats: ModelStats[] = [];
for (const [model, modelResults] of Object.entries(byModel)) {
const total = modelResults.length;
const success = modelResults.filter((r) => r.status === 'success').length;
const withEval = modelResults.filter((r) => r.evaluation);
const passed = withEval.filter(
(r) => (r.evaluation as Record)?.passed
).length;
const scores = withEval
.map(
(r) =>
(r.evaluation as Record)?.score ||
(r.evaluation as Record)?.overall_score ||
0
)
.filter((s) => s > 0);
const avgScore = scores.length
? scores.reduce((a, b) => a + b, 0) / scores.length
: 0;
const avgTime =
modelResults.reduce((sum, r) => sum + r.execution_time, 0) / total;
modelStats.push({
model,
total_tests: total,
success_count: success,
success_rate: (success / total) * 100,
pass_count: passed,
pass_rate: withEval.length ? (passed / withEval.length) * 100 : 0,
avg_score: avgScore,
avg_time: avgTime,
});
}
// Sort by average score
modelStats.sort((a, b) => b.avg_score - a.avg_score);
return {
modelStats,
successCount: results.filter((r) => r.status === 'success').length,
failureCount: results.filter((r) => r.status !== 'success').length,
};
}
private static generateSummaryCards(
summary: BenchSummary,
_stats: { modelStats: ModelStats[]; successCount: number; failureCount: number }
): string {
const totalCost = summary.cost_summary?.total_cost_usd || 0;
const totalTokens = summary.cost_summary?.total_tokens || 0;
return `
๐งช
${summary.total_tests}
Total Tests
๐ค
${summary.models_tested.length}
Models
โ
${summary.success_rate}
Success Rate
โฑ๏ธ
${summary.average_execution_time}
Avg Time
๐ฐ
$${totalCost.toFixed(4)}
Total Cost
๐
${totalTokens.toLocaleString()}
Total Tokens
`;
}
private static generateLeaderboard(stats: {
modelStats: ModelStats[];
}): string {
const medals = ['๐ฅ', '๐ฅ', '๐ฅ'];
const rows = stats.modelStats
.map((m, i) => {
const medal = i < 3 ? medals[i] : '';
return `
| ${medal} ${m.model} |
${m.avg_score.toFixed(1)} |
${m.pass_rate.toFixed(1)}% |
${m.success_rate.toFixed(1)}% |
${m.avg_time.toFixed(2)}s |
${m.total_tests} |
`;
})
.join('');
return `
| Model |
Avg Score |
Pass Rate |
Success Rate |
Avg Time |
Tests |
${rows}
`;
}
private static generateResultsTable(results: BenchResult[]): string {
const rows = results
.map((r) => {
const statusIcon = r.status === 'success' ? 'โ
' : 'โ';
const score =
(r.evaluation as Record)?.score ||
(r.evaluation as Record)?.overall_score ||
'-';
const passed =
r.evaluation !== undefined
? (r.evaluation as Record)?.passed
? 'โ
'
: 'โ'
: '-';
return `
| ${r.test_name} |
${statusIcon} ${r.status} |
${r.model} |
${score} |
${passed} |
${r.execution_time.toFixed(2)}s |
${r.token_usage?.total_tokens || '-'} |
$${r.cost?.total_usd?.toFixed(6) || '-'} |
`;
})
.join('');
return `
| Test Name |
Status |
Model |
Score |
Passed |
Time |
Tokens |
Cost |
${rows}
`;
}
private static generateCss(): string {
return `
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #2c3e50; line-height: 1.6; }
.header { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 2rem; text-align: center; }
.header h1 { font-size: 2.5em; margin-bottom: 0.5rem; }
.header p { opacity: 0.9; }
.nav-tabs { background: white; border-bottom: 2px solid #e5e7eb; display: flex; justify-content: center; }
.nav-tab { padding: 1rem 2rem; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.3s; font-weight: 500; }
.nav-tab:hover { background: #f9fafb; }
.nav-tab.active { border-bottom-color: #3b82f6; color: #3b82f6; }
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.section-title { font-size: 1.5em; margin-bottom: 1.5rem; color: #1f2937; }
.summary-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.card { background: white; border-radius: 12px; padding: 1.5rem; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.card-icon { font-size: 2em; margin-bottom: 0.5rem; }
.card-value { font-size: 1.8em; font-weight: bold; color: #3b82f6; }
.card-label { color: #6b7280; font-size: 0.9em; }
.charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
.chart-container { background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.chart-container h3 { margin-bottom: 1rem; color: #374151; }
.results-table { width: 100%; border-collapse: collapse; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.results-table th, .results-table td { padding: 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
.results-table th { background: #f9fafb; font-weight: 600; color: #374151; }
.results-table tr:hover { background: #f9fafb; }
.results-table .top-1 { background: #fef3c7; }
.results-table .top-2 { background: #e5e7eb; }
.results-table .top-3 { background: #fed7aa; }
footer { text-align: center; padding: 2rem; color: #6b7280; }
footer a { color: #3b82f6; text-decoration: none; }
`;
}
private static generateJavaScript(stats: {
modelStats: ModelStats[];
successCount: number;
failureCount: number;
}): string {
const modelNames = stats.modelStats.map((m) => m.model);
const avgTimes = stats.modelStats.map((m) => m.avg_time);
return `
function showTab(tabId) {
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
event.target.classList.add('active');
}
// Status Chart
new Chart(document.getElementById('statusChart'), {
type: 'doughnut',
data: {
labels: ['Success', 'Failed'],
datasets: [{
data: [${stats.successCount}, ${stats.failureCount}],
backgroundColor: ['#10b981', '#ef4444']
}]
},
options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
});
// Time Chart
new Chart(document.getElementById('timeChart'), {
type: 'bar',
data: {
labels: ${JSON.stringify(modelNames)},
datasets: [{
label: 'Avg Time (s)',
data: ${JSON.stringify(avgTimes)},
backgroundColor: '#3b82f6'
}]
},
options: { responsive: true, plugins: { legend: { display: false } } }
});
`;
}
}