Mridul2004's picture
Initial commit - Model Speed Comparator
97f0024
Raw
History Blame Contribute Delete
35.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Model Speed Comparator</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<style>
:root {
--bg: #0a0a0f;
--surface: #111118;
--surface2: #1a1a24;
--border: #2a2a3a;
--accent: #00e5ff;
--accent2: #7c3aed;
--green: #00ff87;
--yellow: #ffd600;
--red: #ff4757;
--text: #e8e8f0;
--muted: #6b6b80;
--font-mono: 'Space Mono', monospace;
--font-sans: 'DM Sans', sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
min-height: 100vh;
overflow-x: hidden;
}
/* Grid background */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(0,229,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,229,255,0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
.container {
max-width: 960px;
margin: 0 auto;
padding: 48px 24px;
position: relative;
z-index: 1;
}
/* Header */
header {
margin-bottom: 48px;
}
.tag {
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent);
letter-spacing: 3px;
text-transform: uppercase;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.tag::before {
content: '';
width: 24px;
height: 1px;
background: var(--accent);
}
h1 {
font-family: var(--font-mono);
font-size: clamp(28px, 5vw, 44px);
font-weight: 700;
line-height: 1.1;
letter-spacing: -1px;
color: #fff;
}
h1 span {
color: var(--accent);
}
.subtitle {
color: var(--muted);
font-size: 15px;
margin-top: 12px;
font-weight: 300;
max-width: 520px;
line-height: 1.6;
}
/* Input section */
.input-section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px;
margin-bottom: 32px;
position: relative;
overflow: hidden;
}
.input-section::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
}
.input-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--muted);
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 12px;
}
textarea {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-family: var(--font-sans);
font-size: 15px;
padding: 16px;
resize: vertical;
min-height: 90px;
transition: border-color 0.2s;
outline: none;
}
textarea:focus {
border-color: var(--accent);
}
.examples {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.example-chip {
font-size: 12px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 20px;
padding: 4px 12px;
color: var(--muted);
cursor: pointer;
transition: all 0.15s;
font-family: var(--font-mono);
}
.example-chip:hover {
border-color: var(--accent);
color: var(--accent);
}
.action-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
}
.run-btn {
margin-top: 20px;
background: var(--accent);
color: #000;
border: none;
border-radius: 8px;
padding: 14px 32px;
font-family: var(--font-mono);
font-size: 13px;
font-weight: 700;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 10px;
}
.action-row .run-btn {
margin-top: 0;
}
.benchmark-btn {
background: transparent;
color: var(--accent);
border: 1px solid var(--accent);
}
.benchmark-btn:hover {
background: rgba(0,229,255,0.08);
color: var(--text);
}
.secondary-btn {
background: var(--surface2);
color: var(--text);
border: 1px solid var(--border);
}
.secondary-btn:hover {
background: rgba(255,255,255,0.04);
color: var(--accent);
border-color: var(--accent);
}
.toggle-active {
background: var(--green);
color: #000;
border-color: var(--green);
}
.run-btn:hover { background: #33eaff; transform: translateY(-1px); }
.run-btn:active { transform: translateY(0); }
.run-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
/* Spinner */
.spinner {
width: 16px; height: 16px;
border: 2px solid rgba(0,0,0,0.3);
border-top-color: #000;
border-radius: 50%;
animation: spin 0.7s linear infinite;
display: none;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Results */
#results { display: none; }
.summary-banner {
background: linear-gradient(135deg, rgba(0,229,255,0.08), rgba(124,58,237,0.08));
border: 1px solid rgba(0,229,255,0.2);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.speedup-badge {
font-family: var(--font-mono);
font-size: 36px;
font-weight: 700;
color: var(--green);
line-height: 1;
}
.summary-text {
font-size: 14px;
color: var(--muted);
line-height: 1.6;
}
.summary-text strong { color: var(--text); }
/* Cards grid */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
position: relative;
overflow: hidden;
animation: fadeUp 0.4s ease forwards;
opacity: 0;
}
.card:nth-child(1) { animation-delay: 0.05s; }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.15s; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.card.winner {
border-color: rgba(0,255,135,0.3);
background: linear-gradient(135deg, var(--surface), rgba(0,255,135,0.04));
}
.card.winner::before {
content: '⚡ FASTEST';
position: absolute;
top: 12px; right: 12px;
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 2px;
color: var(--green);
background: rgba(0,255,135,0.1);
border: 1px solid rgba(0,255,135,0.3);
border-radius: 4px;
padding: 3px 8px;
}
.card-name {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 4px;
}
.card-format {
font-size: 13px;
color: var(--accent);
margin-bottom: 20px;
font-weight: 500;
}
.card-label {
font-size: 22px;
font-weight: 600;
margin-bottom: 4px;
color: #fff;
}
.card-confidence {
font-family: var(--font-mono);
font-size: 12px;
color: var(--muted);
margin-bottom: 20px;
}
.metrics {
display: flex;
flex-direction: column;
gap: 12px;
}
.metric {
display: flex;
justify-content: space-between;
align-items: center;
}
.metric-name {
font-size: 12px;
color: var(--muted);
font-family: var(--font-mono);
}
.metric-value {
font-family: var(--font-mono);
font-size: 14px;
font-weight: 700;
color: var(--text);
}
.bar-wrap {
height: 4px;
background: var(--surface2);
border-radius: 2px;
margin-top: 4px;
overflow: hidden;
}
.bar {
height: 100%;
border-radius: 2px;
transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1);
background: var(--accent);
}
.bar.size { background: var(--accent2); }
/* Chart section */
.chart-section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px;
margin-bottom: 24px;
}
.section-title {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 24px;
}
.chart-row {
display: flex;
flex-direction: column;
gap: 16px;
}
.chart-item {
display: grid;
grid-template-columns: 90px 1fr 80px;
align-items: center;
gap: 12px;
}
.chart-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--muted);
text-align: right;
}
.chart-bar-wrap {
height: 28px;
background: var(--surface2);
border-radius: 4px;
overflow: hidden;
}
.chart-bar {
height: 100%;
border-radius: 4px;
display: flex;
align-items: center;
padding-left: 10px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 700;
color: #000;
transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1);
min-width: 0;
white-space: nowrap;
}
.chart-val {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text);
font-weight: 700;
}
.trend-canvas {
width: 100%;
height: 220px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
display: block;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
}
.stat-tile {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
}
.stat-label {
font-family: var(--font-mono);
font-size: 10px;
color: var(--muted);
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: 8px;
}
.stat-value {
font-family: var(--font-mono);
font-size: 18px;
font-weight: 700;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 640px;
}
th, td {
border-bottom: 1px solid var(--border);
padding: 12px 10px;
text-align: left;
font-size: 13px;
vertical-align: top;
}
tbody tr:hover {
background: rgba(255,255,255,0.025);
}
th {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--muted);
}
td {
color: var(--text);
}
.mono-cell {
font-family: var(--font-mono);
font-size: 12px;
}
.history-input {
max-width: 320px;
color: var(--muted);
}
.empty-state {
color: var(--muted);
font-size: 13px;
}
/* Explain box */
.explain-box {
background: var(--surface);
border: 1px solid var(--border);
border-left: 3px solid var(--accent2);
border-radius: 12px;
padding: 24px 28px;
}
.explain-box h3 {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--accent2);
margin-bottom: 16px;
}
.explain-item {
display: flex;
gap: 12px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1.6;
color: var(--muted);
}
.explain-item strong { color: var(--text); }
.explain-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent2);
flex-shrink: 0;
margin-top: 7px;
}
/* Error */
.error-box {
background: rgba(255,71,87,0.08);
border: 1px solid rgba(255,71,87,0.3);
border-radius: 8px;
padding: 16px 20px;
color: var(--red);
font-family: var(--font-mono);
font-size: 13px;
display: none;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="tag">AI Inference Optimization</div>
<h1>Model Speed<br/><span>Comparator</span></h1>
<p class="subtitle">Compare PyTorch baseline vs ONNX vs INT8 Quantized inference — same model, same prediction, dramatically different performance.</p>
</header>
<div class="input-section">
<div class="input-label">Input Text</div>
<textarea id="inputText" placeholder="Type any sentence to classify as positive or negative...">This product exceeded all my expectations. Absolutely love it!</textarea>
<div class="examples">
<span class="example-chip" onclick="setExample(this)">The movie was terrible</span>
<span class="example-chip" onclick="setExample(this)">Best experience ever!</span>
<span class="example-chip" onclick="setExample(this)">It was okay, nothing special</span>
<span class="example-chip" onclick="setExample(this)">Completely disappointed</span>
</div>
<div class="action-row">
<button class="run-btn" id="runBtn" onclick="runComparison()">
<div class="spinner" id="spinner"></div>
<span id="btnText">RUN COMPARISON</span>
</button>
<button class="run-btn benchmark-btn" id="benchmarkBtn" onclick="runBenchmark()">
<div class="spinner" id="benchmarkSpinner"></div>
<span id="benchmarkBtnText">RUN 20X BENCHMARK</span>
</button>
<button class="run-btn secondary-btn" id="autoRefreshBtn" onclick="toggleAutoRefresh()">AUTO REFRESH OFF</button>
<button class="run-btn secondary-btn" onclick="downloadHistoryCSV()">DOWNLOAD CSV</button>
</div>
<div class="error-box" id="errorBox"></div>
</div>
<div id="results">
<div class="summary-banner">
<div class="speedup-badge" id="speedupBadge"></div>
<div class="summary-text">
<strong>Speedup achieved</strong> — Quantized INT8 vs PyTorch baseline<br/>
Smallest model: <strong id="smallestModel"></strong> &nbsp;·&nbsp; Fastest: <strong id="fastestModel"></strong>
</div>
<button class="run-btn secondary-btn" id="copyJsonBtn" onclick="copyCurrentResult()">COPY RESULT JSON</button>
</div>
<div class="cards" id="cardsContainer"></div>
<div class="chart-section">
<div class="section-title">Session Stats</div>
<div class="stats-grid" id="statsGrid"></div>
</div>
<div class="chart-section">
<div class="section-title">Latency Comparison (ms) — lower is better</div>
<div class="chart-row" id="latencyChart"></div>
</div>
<div class="chart-section">
<div class="section-title">Live Latency Trend (last 10 runs)</div>
<canvas class="trend-canvas" id="trendCanvas" width="900" height="220"></canvas>
</div>
<div class="chart-section">
<div class="section-title">Model Size (MB) — lower is better</div>
<div class="chart-row" id="sizeChart"></div>
</div>
<div class="chart-section" id="benchmarkSection" style="display:none">
<div class="section-title">20-Run Benchmark Latency (ms)</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Model</th>
<th>Average</th>
<th>Min</th>
<th>Max</th>
<th>P95</th>
</tr>
</thead>
<tbody id="benchmarkTable"></tbody>
</table>
</div>
</div>
<div class="explain-box">
<h3>What This Demonstrates</h3>
<div class="explain-item">
<div class="explain-dot"></div>
<div><strong>ONNX Export</strong> — Converts PyTorch model to a hardware-agnostic format. ONNX Runtime applies graph optimizations (operator fusion, memory planning) that PyTorch doesn't do by default.</div>
</div>
<div class="explain-item">
<div class="explain-dot"></div>
<div><strong>INT8 Quantization</strong> — Reduces weight precision from 32-bit floats to 8-bit integers. 4x smaller model, faster memory bandwidth, same accuracy on most NLP tasks.</div>
</div>
<div class="explain-item">
<div class="explain-dot"></div>
<div><strong>Why it matters</strong> — AI accelerator teams (like HCL's) optimize model inference for deployment at scale. These techniques are the foundation of production ML systems.</div>
</div>
</div>
</div>
<div class="chart-section" id="historySection">
<div class="section-title">Recent Comparisons</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Time</th>
<th>Input</th>
<th>Fastest</th>
<th>Speedup</th>
<th>Baseline</th>
<th>ONNX</th>
<th>Quantized</th>
<th>Copy</th>
</tr>
</thead>
<tbody id="historyTable"></tbody>
</table>
</div>
<div class="empty-state" id="historyEmpty">No comparisons yet.</div>
</div>
</div>
<script>
function setExample(el) {
document.getElementById('inputText').value = el.textContent;
}
const COLORS = {
baseline: '#6b6b80',
onnx: '#00e5ff',
quantized: '#00ff87'
};
const NAMES = {
baseline: 'BASELINE',
onnx: 'ONNX',
quantized: 'QUANTIZED'
};
let currentResult = null;
let currentHistory = [];
let autoRefreshTimer = null;
let latencyTrend = [];
function escapeHTML(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
async function parseResponse(res) {
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.detail || `Server error: ${res.status}`);
}
return data;
}
function setButtonLoading(buttonId, spinnerId, textId, loadingText, idleText, isLoading) {
document.getElementById(buttonId).disabled = isLoading;
document.getElementById(spinnerId).style.display = isLoading ? 'block' : 'none';
document.getElementById(textId).textContent = isLoading ? loadingText : idleText;
}
function showError(message) {
const errorBox = document.getElementById('errorBox');
errorBox.textContent = `Error: ${message}`;
errorBox.style.display = 'block';
}
function clearError() {
document.getElementById('errorBox').style.display = 'none';
}
async function runComparison() {
const text = document.getElementById('inputText').value.trim();
if (!text) return;
const btn = document.getElementById('runBtn');
const spinner = document.getElementById('spinner');
const btnText = document.getElementById('btnText');
const errorBox = document.getElementById('errorBox');
btn.disabled = true;
spinner.style.display = 'block';
btnText.textContent = 'RUNNING...';
errorBox.style.display = 'none';
document.getElementById('results').style.display = 'none';
try {
const res = await fetch('/compare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!res.ok) throw new Error(`Server error: ${res.status}`);
const data = await res.json();
renderResults(data);
} catch (err) {
errorBox.textContent = `Error: ${err.message}`;
errorBox.style.display = 'block';
} finally {
btn.disabled = false;
spinner.style.display = 'none';
btnText.textContent = '▶ RUN COMPARISON';
}
}
function renderResults(data) {
const { results, summary } = data;
const resultsEl = document.getElementById('results');
resultsEl.style.display = 'block';
// Summary banner
document.getElementById('speedupBadge').textContent = `${summary.speedup_vs_baseline}×`;
document.getElementById('smallestModel').textContent = NAMES[summary.smallest];
document.getElementById('fastestModel').textContent = NAMES[summary.fastest];
// Cards
const maxLatency = Math.max(...Object.values(results).map(r => r.latency_ms));
const maxSize = Math.max(...Object.values(results).map(r => r.model_size_mb));
const cardsEl = document.getElementById('cardsContainer');
cardsEl.innerHTML = '';
Object.entries(results).forEach(([key, val]) => {
const isWinner = key === summary.fastest;
const card = document.createElement('div');
card.className = `card${isWinner ? ' winner' : ''}`;
card.innerHTML = `
<div class="card-name">${NAMES[key]}</div>
<div class="card-format">${val.format}</div>
<div class="card-label">${val.label}</div>
<div class="card-confidence">confidence: ${(val.confidence * 100).toFixed(1)}%</div>
<div class="metrics">
<div class="metric">
<span class="metric-name">LATENCY</span>
<span class="metric-value" style="color:${COLORS[key]}">${val.latency_ms}ms</span>
</div>
<div class="bar-wrap"><div class="bar" style="width:${(val.latency_ms/maxLatency)*100}%;background:${COLORS[key]}"></div></div>
<div class="metric" style="margin-top:8px">
<span class="metric-name">MODEL SIZE</span>
<span class="metric-value">${val.model_size_mb} MB</span>
</div>
<div class="bar-wrap"><div class="bar size" style="width:${(val.model_size_mb/maxSize)*100}%"></div></div>
</div>`;
cardsEl.appendChild(card);
});
// Charts
renderBarChart('latencyChart', results, 'latency_ms', 'ms', maxLatency);
renderBarChart('sizeChart', results, 'model_size_mb', 'MB', maxSize);
}
function renderBarChart(elId, results, field, unit, maxVal) {
const el = document.getElementById(elId);
el.innerHTML = '';
Object.entries(results).forEach(([key, val]) => {
const pct = Math.max(4, (val[field] / maxVal) * 100);
const row = document.createElement('div');
row.className = 'chart-item';
row.innerHTML = `
<div class="chart-label">${NAMES[key]}</div>
<div class="chart-bar-wrap">
<div class="chart-bar" style="width:${pct}%;background:${COLORS[key]}">
${val[field]}${unit}
</div>
</div>
<div class="chart-val">${val[field]} ${unit}</div>`;
el.appendChild(row);
});
}
async function runComparison() {
const text = document.getElementById('inputText').value.trim();
if (!text) return;
setButtonLoading('runBtn', 'spinner', 'btnText', 'RUNNING...', 'RUN COMPARISON', true);
clearError();
document.getElementById('results').style.display = 'none';
document.getElementById('benchmarkSection').style.display = 'none';
try {
const res = await fetch('/compare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const data = await parseResponse(res);
renderResults(data);
await loadHistory();
} catch (err) {
showError(err.message);
} finally {
setButtonLoading('runBtn', 'spinner', 'btnText', 'RUNNING...', 'RUN COMPARISON', false);
}
}
async function runBenchmark() {
const text = document.getElementById('inputText').value.trim();
if (!text) return;
setButtonLoading('benchmarkBtn', 'benchmarkSpinner', 'benchmarkBtnText', 'BENCHMARKING...', 'RUN 20X BENCHMARK', true);
clearError();
try {
const res = await fetch('/benchmark', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const data = await parseResponse(res);
renderResults({ results: data.latest_results, summary: data.summary });
renderBenchmark(data);
} catch (err) {
showError(err.message);
} finally {
setButtonLoading('benchmarkBtn', 'benchmarkSpinner', 'benchmarkBtnText', 'BENCHMARKING...', 'RUN 20X BENCHMARK', false);
}
}
function renderBenchmark(data) {
document.getElementById('results').style.display = 'block';
document.getElementById('benchmarkSection').style.display = 'block';
const table = document.getElementById('benchmarkTable');
table.innerHTML = '';
Object.entries(data.results).forEach(([key, val]) => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="mono-cell" style="color:${COLORS[key]}">${NAMES[key]}</td>
<td class="mono-cell">${val.avg_latency_ms} ms</td>
<td class="mono-cell">${val.min_latency_ms} ms</td>
<td class="mono-cell">${val.max_latency_ms} ms</td>
<td class="mono-cell">${val.p95_latency_ms} ms</td>`;
table.appendChild(row);
});
}
async function loadHistory() {
try {
const res = await fetch('/history');
const data = await parseResponse(res);
renderHistory(data.history || []);
} catch (err) {
renderHistory([]);
}
}
function renderHistory(history) {
const table = document.getElementById('historyTable');
const empty = document.getElementById('historyEmpty');
table.innerHTML = '';
empty.style.display = history.length ? 'none' : 'block';
history.forEach(item => {
const row = document.createElement('tr');
const time = new Date(item.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
row.innerHTML = `
<td class="mono-cell">${time}</td>
<td class="history-input">${escapeHTML(item.input)}</td>
<td class="mono-cell">${NAMES[item.summary.fastest]}</td>
<td class="mono-cell">${item.summary.speedup_vs_baseline}x</td>
<td class="mono-cell">${item.results.baseline.latency_ms} ms</td>
<td class="mono-cell">${item.results.onnx.latency_ms} ms</td>
<td class="mono-cell">${item.results.quantized.latency_ms} ms</td>`;
table.appendChild(row);
});
}
async function runComparison() {
const text = document.getElementById('inputText').value.trim();
if (!text) return;
setButtonLoading('runBtn', 'spinner', 'btnText', 'RUNNING...', 'RUN COMPARISON', true);
clearError();
document.getElementById('results').style.display = 'none';
document.getElementById('benchmarkSection').style.display = 'none';
try {
const res = await fetch('/compare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const data = await parseResponse(res);
renderResults(data);
addTrendPoint(data.results);
await Promise.all([loadHistory(), loadStats()]);
} catch (err) {
showError(err.message);
} finally {
setButtonLoading('runBtn', 'spinner', 'btnText', 'RUNNING...', 'RUN COMPARISON', false);
}
}
function renderResults(data) {
const { results, summary } = data;
currentResult = data;
document.getElementById('results').style.display = 'block';
document.getElementById('speedupBadge').textContent = `${summary.speedup_vs_baseline}x`;
document.getElementById('smallestModel').textContent = NAMES[summary.smallest];
document.getElementById('fastestModel').textContent = NAMES[summary.fastest];
const maxLatency = Math.max(...Object.values(results).map(r => r.latency_ms));
const maxSize = Math.max(...Object.values(results).map(r => r.model_size_mb));
const cardsEl = document.getElementById('cardsContainer');
cardsEl.innerHTML = '';
Object.entries(results).forEach(([key, val]) => {
const isWinner = key === summary.fastest;
const card = document.createElement('div');
card.className = `card${isWinner ? ' winner' : ''}`;
card.innerHTML = `
<div class="card-name">${NAMES[key]}</div>
<div class="card-format">${escapeHTML(val.format)}</div>
<div class="card-label">${escapeHTML(val.label)}</div>
<div class="card-confidence">confidence: ${(val.confidence * 100).toFixed(1)}%</div>
<div class="metrics">
<div class="metric">
<span class="metric-name">LATENCY</span>
<span class="metric-value" style="color:${COLORS[key]}">${val.latency_ms}ms</span>
</div>
<div class="bar-wrap"><div class="bar" style="width:${(val.latency_ms/maxLatency)*100}%;background:${COLORS[key]}"></div></div>
<div class="metric" style="margin-top:8px">
<span class="metric-name">MODEL SIZE</span>
<span class="metric-value">${val.model_size_mb} MB</span>
</div>
<div class="bar-wrap"><div class="bar size" style="width:${(val.model_size_mb/maxSize)*100}%"></div></div>
</div>`;
cardsEl.appendChild(card);
});
renderBarChart('latencyChart', results, 'latency_ms', 'ms', maxLatency);
renderBarChart('sizeChart', results, 'model_size_mb', 'MB', maxSize);
}
async function runBenchmark() {
const text = document.getElementById('inputText').value.trim();
if (!text) return;
setButtonLoading('benchmarkBtn', 'benchmarkSpinner', 'benchmarkBtnText', 'BENCHMARKING...', 'RUN 20X BENCHMARK', true);
clearError();
try {
const res = await fetch('/benchmark', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const data = await parseResponse(res);
renderResults({ input: data.input, results: data.latest_results, summary: data.summary });
renderBenchmark(data);
} catch (err) {
showError(err.message);
} finally {
setButtonLoading('benchmarkBtn', 'benchmarkSpinner', 'benchmarkBtnText', 'BENCHMARKING...', 'RUN 20X BENCHMARK', false);
}
}
async function loadStats() {
const res = await fetch('/stats');
const stats = await parseResponse(res);
renderStats(stats);
}
function renderStats(stats) {
const grid = document.getElementById('statsGrid');
grid.innerHTML = `
<div class="stat-tile">
<div class="stat-label">Total Requests</div>
<div class="stat-value">${stats.total_requests}</div>
</div>
${Object.entries(stats.avg_latency).map(([key, value]) => `
<div class="stat-tile">
<div class="stat-label">${NAMES[key]} Avg</div>
<div class="stat-value" style="color:${COLORS[key]}">${value} ms</div>
</div>
`).join('')}
${Object.entries(stats.fastest_count).map(([key, value]) => `
<div class="stat-tile">
<div class="stat-label">${NAMES[key]} Wins</div>
<div class="stat-value">${value}</div>
</div>
`).join('')}`;
}
function addTrendPoint(results) {
latencyTrend.push({
baseline: results.baseline.latency_ms,
onnx: results.onnx.latency_ms,
quantized: results.quantized.latency_ms
});
latencyTrend = latencyTrend.slice(-10);
drawTrend();
}
function drawTrend() {
const canvas = document.getElementById('trendCanvas');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const padding = 34;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a1a24';
ctx.fillRect(0, 0, width, height);
if (!latencyTrend.length) {
ctx.fillStyle = '#6b6b80';
ctx.font = '13px monospace';
ctx.fillText('Run comparisons to build a live latency trend.', padding, height / 2);
return;
}
const allValues = latencyTrend.flatMap(point => Object.values(point));
const maxValue = Math.max(...allValues, 1);
ctx.strokeStyle = '#2a2a3a';
ctx.lineWidth = 1;
for (let i = 0; i < 4; i++) {
const y = padding + ((height - padding * 2) / 3) * i;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
}
Object.keys(COLORS).forEach(modelName => {
ctx.strokeStyle = COLORS[modelName];
ctx.lineWidth = 2;
ctx.beginPath();
latencyTrend.forEach((point, index) => {
const x = padding + (index / Math.max(latencyTrend.length - 1, 1)) * (width - padding * 2);
const y = height - padding - (point[modelName] / maxValue) * (height - padding * 2);
if (index === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
});
}
async function toggleAutoRefresh() {
const btn = document.getElementById('autoRefreshBtn');
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
btn.textContent = 'AUTO REFRESH OFF';
btn.classList.remove('toggle-active');
return;
}
btn.textContent = 'AUTO REFRESH ON';
btn.classList.add('toggle-active');
await runComparison();
autoRefreshTimer = setInterval(runComparison, 10000);
}
function downloadHistoryCSV() {
const rows = [
['timestamp', 'input text', 'baseline ms', 'onnx ms', 'quantized ms'],
...currentHistory.map(item => [
item.timestamp,
item.input,
item.results.baseline.latency_ms,
item.results.onnx.latency_ms,
item.results.quantized.latency_ms
])
];
const csv = rows.map(row => row.map(value => `"${String(value).replaceAll('"', '""')}"`).join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'model-speed-history.csv';
link.click();
URL.revokeObjectURL(url);
}
async function copyCurrentResult() {
if (!currentResult) return;
await copyJSON(currentResult);
}
async function copyJSON(value) {
const json = JSON.stringify(value, null, 2);
if (navigator.clipboard) {
await navigator.clipboard.writeText(json);
} else {
const textarea = document.createElement('textarea');
textarea.value = json;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
}
}
function renderHistory(history) {
currentHistory = history;
const table = document.getElementById('historyTable');
const empty = document.getElementById('historyEmpty');
table.innerHTML = '';
empty.style.display = history.length ? 'none' : 'block';
history.forEach((item, index) => {
const row = document.createElement('tr');
const time = new Date(item.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
row.innerHTML = `
<td class="mono-cell">${time}</td>
<td class="history-input">${escapeHTML(item.input)}</td>
<td class="mono-cell">${NAMES[item.summary.fastest]}</td>
<td class="mono-cell">${item.summary.speedup_vs_baseline}x</td>
<td class="mono-cell">${item.results.baseline.latency_ms} ms</td>
<td class="mono-cell">${item.results.onnx.latency_ms} ms</td>
<td class="mono-cell">${item.results.quantized.latency_ms} ms</td>
<td><button class="run-btn secondary-btn" style="padding:8px 10px;font-size:10px" onclick="copyJSON(currentHistory[${index}])">JSON</button></td>`;
table.appendChild(row);
});
}
// Allow Enter key to submit
document.getElementById('inputText').addEventListener('keydown', e => {
if (e.key === 'Enter' && e.ctrlKey) runComparison();
});
loadHistory();
loadStats();
drawTrend();
</script>
</body>
</html>