Spaces:
Running
Running
Update index.html
Browse files- index.html +357 -204
index.html
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>Provider
|
7 |
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
|
8 |
<style>
|
9 |
:root {
|
@@ -14,6 +14,9 @@
|
|
14 |
--border-color: #dee2e6;
|
15 |
--shadow-color: rgba(0, 0, 0, 0.05);
|
16 |
--primary-color: #0d6efd;
|
|
|
|
|
|
|
17 |
--plot-colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']; /* Plotly default */
|
18 |
}
|
19 |
body {
|
@@ -24,16 +27,36 @@
|
|
24 |
line-height: 1.5;
|
25 |
}
|
26 |
.container {
|
27 |
-
max-width:
|
28 |
margin: 20px auto;
|
29 |
padding: 0 20px;
|
30 |
}
|
31 |
h1 {
|
32 |
text-align: center;
|
33 |
color: var(--text-color);
|
34 |
-
margin-bottom:
|
35 |
font-weight: 500;
|
36 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
.kpi-container {
|
38 |
display: grid;
|
39 |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
@@ -51,19 +74,20 @@
|
|
51 |
.kpi-card h3 {
|
52 |
margin-top: 0;
|
53 |
margin-bottom: 10px;
|
54 |
-
font-size:
|
55 |
color: var(--muted-text-color);
|
56 |
font-weight: 400;
|
57 |
}
|
58 |
.kpi-card .value {
|
59 |
-
font-size: 1.
|
60 |
font-weight: 600;
|
61 |
color: var(--text-color);
|
|
|
62 |
}
|
63 |
.kpi-card .unit {
|
64 |
-
font-size: 0.
|
65 |
color: var(--muted-text-color);
|
66 |
-
margin-left:
|
67 |
}
|
68 |
|
69 |
.dashboard-container {
|
@@ -71,7 +95,7 @@
|
|
71 |
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); /* Responsive grid */
|
72 |
gap: 25px; /* Space between plots */
|
73 |
}
|
74 |
-
.plot-container {
|
75 |
background-color: var(--card-bg-color);
|
76 |
padding: 20px;
|
77 |
border-radius: 8px;
|
@@ -79,13 +103,20 @@
|
|
79 |
border: 1px solid var(--border-color);
|
80 |
min-height: 450px; /* Ensure plots have some height */
|
81 |
display: flex; /* For centering loading/error inside */
|
|
|
82 |
justify-content: center;
|
83 |
align-items: center;
|
84 |
}
|
85 |
-
|
86 |
-
.plot-container .plotly {
|
87 |
width: 100%;
|
88 |
height: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
}
|
90 |
|
91 |
#loading, #error {
|
@@ -96,12 +127,51 @@
|
|
96 |
color: var(--muted-text-color);
|
97 |
}
|
98 |
#error {
|
99 |
-
color:
|
100 |
font-weight: 500;
|
101 |
background-color: #f8d7da;
|
102 |
border: 1px solid #f5c2c7;
|
103 |
border-radius: 8px;
|
104 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
footer {
|
106 |
text-align: center;
|
107 |
margin-top: 40px;
|
@@ -124,25 +194,33 @@
|
|
124 |
<div class="container">
|
125 |
<h1>Provider Inference Metrics Dashboard</h1>
|
126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
<div id="loading">Loading data... Please wait.</div>
|
128 |
<div id="error" style="display: none;"></div>
|
129 |
|
130 |
<!-- KPI Section -->
|
131 |
<div class="kpi-container" style="display: none;">
|
132 |
<div class="kpi-card">
|
133 |
-
<h3>Total Requests</h3>
|
134 |
<div class="value" id="kpi-total-requests">--</div>
|
135 |
</div>
|
136 |
<div class="kpi-card">
|
137 |
-
<h3>Success Rate</h3>
|
138 |
<div class="value" id="kpi-success-rate">--<span class="unit">%</span></div>
|
139 |
</div>
|
140 |
<div class="kpi-card">
|
141 |
-
<h3>Avg. Latency
|
142 |
<div class="value" id="kpi-avg-latency">--<span class="unit">ms</span></div>
|
143 |
</div>
|
144 |
<div class="kpi-card">
|
145 |
-
<h3>Fastest Provider (Median)</h3>
|
146 |
<div class="value" id="kpi-fastest-provider">--</div>
|
147 |
</div>
|
148 |
</div>
|
@@ -151,10 +229,16 @@
|
|
151 |
<div class="dashboard-container" style="display: none;">
|
152 |
<div id="plotLatencyProvider" class="plot-container"></div>
|
153 |
<div id="plotReliabilityProvider" class="plot-container"></div>
|
154 |
-
<div id="plotLatencyModel" class="plot-container"></div>
|
155 |
<div id="plotErrorTypesProvider" class="plot-container"></div>
|
156 |
-
<div id="
|
157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
</div>
|
159 |
|
160 |
<footer id="footer" style="display: none;">
|
@@ -173,43 +257,43 @@
|
|
173 |
const footer = document.getElementById('footer');
|
174 |
const dataSourceUrlElement = document.getElementById('data-source-url');
|
175 |
const lastUpdatedElement = document.getElementById('last-updated');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
|
177 |
dataSourceUrlElement.href = apiUrl; // Set link href
|
178 |
|
|
|
|
|
|
|
179 |
// Plotly layout defaults
|
180 |
const baseLayout = {
|
181 |
-
margin: { l: 60, r: 30, b: 100, t: 60, pad: 4 },
|
182 |
legend: { bgcolor: 'rgba(255,255,255,0.5)', bordercolor: '#ccc', borderwidth: 1 },
|
183 |
-
colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
|
184 |
-
paper_bgcolor: 'rgba(0,0,0,0)',
|
185 |
-
plot_bgcolor: 'rgba(0,0,0,0)',
|
186 |
font: {
|
187 |
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
188 |
color: '#212529'
|
189 |
},
|
190 |
title: {
|
191 |
font: { size: 16, weight: '500' },
|
192 |
-
x: 0.05,
|
193 |
-
xanchor: 'left'
|
194 |
-
},
|
195 |
-
xaxis: {
|
196 |
-
gridcolor: '#e9ecef', // Lighter grid lines
|
197 |
-
linecolor: '#adb5bd',
|
198 |
-
automargin: true,
|
199 |
-
tickfont: { size: 10 }
|
200 |
},
|
201 |
-
|
202 |
-
|
203 |
-
linecolor: '#adb5bd',
|
204 |
-
automargin: true,
|
205 |
-
tickfont: { size: 10 }
|
206 |
-
}
|
207 |
};
|
208 |
|
209 |
-
// Helper function to deep merge layout options
|
210 |
function mergeLayout(customLayout) {
|
211 |
-
|
212 |
-
let layout = JSON.parse(JSON.stringify(baseLayout)); // Deep copy base
|
213 |
for (const key in customLayout) {
|
214 |
if (typeof customLayout[key] === 'object' && customLayout[key] !== null && !Array.isArray(customLayout[key]) && layout[key]) {
|
215 |
Object.assign(layout[key], customLayout[key]);
|
@@ -220,7 +304,6 @@
|
|
220 |
return layout;
|
221 |
}
|
222 |
|
223 |
-
// Helper function to calculate median
|
224 |
function calculateMedian(arr) {
|
225 |
if (!arr || arr.length === 0) return null;
|
226 |
const sortedArr = [...arr].sort((a, b) => a - b);
|
@@ -228,45 +311,83 @@
|
|
228 |
return sortedArr.length % 2 !== 0 ? sortedArr[mid] : (sortedArr[mid - 1] + sortedArr[mid]) / 2;
|
229 |
}
|
230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
232 |
fetch(apiUrl)
|
233 |
.then(response => {
|
234 |
-
if (!response.ok) {
|
235 |
-
throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
|
236 |
-
}
|
237 |
return response.json();
|
238 |
})
|
239 |
.then(data => {
|
240 |
-
|
241 |
-
|
242 |
-
dashboardContainer.style.display = 'grid'; // Show dashboard plots
|
243 |
-
footer.style.display = 'block'; // Show footer
|
244 |
-
|
245 |
-
// Extract the actual row data
|
246 |
-
const rows = data.rows.map(item => item.row);
|
247 |
-
console.log(`Fetched ${rows.length} rows.`);
|
248 |
lastUpdatedElement.textContent = new Date().toLocaleString();
|
249 |
|
250 |
-
//
|
251 |
-
calculateAndDisplayKPIs(rows);
|
252 |
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
|
260 |
})
|
261 |
.catch(error => {
|
262 |
console.error('Error fetching or processing data:', error);
|
263 |
loadingDiv.style.display = 'none';
|
264 |
-
errorDiv.textContent = `Error loading data: ${error.message}. Please check the console
|
265 |
errorDiv.style.display = 'block';
|
266 |
});
|
267 |
|
268 |
// --- KPI Calculation Function ---
|
269 |
-
function calculateAndDisplayKPIs(rows) {
|
|
|
|
|
270 |
const totalRequests = rows.length;
|
271 |
const successfulRequests = rows.filter(r => r.response_status_code === 200).length;
|
272 |
const successRate = totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(1) : 0;
|
@@ -278,7 +399,7 @@
|
|
278 |
? (validLatencies.reduce((a, b) => a + b, 0) / validLatencies.length).toFixed(0)
|
279 |
: 0;
|
280 |
|
281 |
-
// Calculate median latency per provider
|
282 |
const latencyByProvider = {};
|
283 |
rows.forEach(row => {
|
284 |
if (row.duration_ms !== null && row.duration_ms >= 0) {
|
@@ -300,6 +421,12 @@
|
|
300 |
}
|
301 |
}
|
302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
303 |
document.getElementById('kpi-total-requests').textContent = totalRequests;
|
304 |
document.getElementById('kpi-success-rate').innerHTML = `${successRate}<span class="unit">%</span>`;
|
305 |
document.getElementById('kpi-avg-latency').innerHTML = `${avgLatency}<span class="unit">ms</span>`;
|
@@ -307,169 +434,134 @@
|
|
307 |
}
|
308 |
|
309 |
|
310 |
-
// --- Plotting Functions ---
|
311 |
|
312 |
-
function createLatencyByProviderPlot(rows) {
|
|
|
313 |
const dataByProvider = {};
|
314 |
rows.forEach(row => {
|
315 |
-
if (!dataByProvider[row.provider_name])
|
316 |
-
dataByProvider[row.provider_name] = [];
|
317 |
-
}
|
318 |
if (row.duration_ms !== null && row.duration_ms >= 0) {
|
319 |
dataByProvider[row.provider_name].push(row.duration_ms);
|
320 |
}
|
321 |
});
|
322 |
|
323 |
-
const plotData = Object.keys(dataByProvider).sort().map(provider => ({
|
324 |
-
y: dataByProvider[provider],
|
325 |
-
type: 'box',
|
326 |
-
name: provider,
|
327 |
-
boxpoints: 'Outliers',
|
328 |
-
marker: { size: 4 }
|
329 |
}));
|
330 |
|
331 |
const layout = mergeLayout({
|
332 |
-
title: { text:
|
333 |
yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
|
334 |
xaxis: { title: 'Provider', tickangle: -30 },
|
335 |
-
margin: { b: 120 }
|
336 |
});
|
337 |
-
|
338 |
-
Plotly.newPlot('plotLatencyProvider', plotData, layout, {responsive: true});
|
339 |
}
|
340 |
|
341 |
-
function createReliabilityByProviderPlot(rows) {
|
342 |
-
|
343 |
-
|
344 |
-
|
|
|
345 |
|
346 |
-
|
347 |
const provider = row.provider_name;
|
348 |
-
const status = row.response_status_code ?? 'Unknown';
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
if (!statusCountsByProvider[provider])
|
353 |
-
statusCountsByProvider[provider] = {};
|
354 |
-
}
|
355 |
-
if (!statusCountsByProvider[provider][status]) {
|
356 |
-
statusCountsByProvider[provider][status] = 0;
|
357 |
-
}
|
358 |
statusCountsByProvider[provider][status]++;
|
359 |
-
|
360 |
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
if (a ===
|
365 |
-
if (b === 200) return 1;
|
366 |
-
if (a === 'Unknown') return 1;
|
367 |
-
if (b === 'Unknown') return -1;
|
368 |
return a - b;
|
369 |
-
|
370 |
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
hovertemplate: `Provider: %{x}<br>Status: ${status}<br>Count: %{y}<extra></extra>`
|
378 |
-
};
|
379 |
-
});
|
380 |
|
381 |
-
|
382 |
-
title: { text:
|
383 |
barmode: 'stack',
|
384 |
xaxis: { title: 'Provider', tickangle: -30 },
|
385 |
yaxis: { title: 'Number of Requests', autorange: true },
|
386 |
margin: { b: 120 }
|
387 |
-
|
388 |
-
|
389 |
-
Plotly.newPlot('plotReliabilityProvider', plotData, layout, {responsive: true});
|
390 |
}
|
391 |
|
392 |
-
function createLatencyByModelPlot(rows) {
|
393 |
const dataByModel = {};
|
394 |
rows.forEach(row => {
|
395 |
const model = row.model_id;
|
396 |
-
if (!dataByModel[model])
|
397 |
-
dataByModel[model] = [];
|
398 |
-
}
|
399 |
if (row.duration_ms !== null && row.duration_ms >= 0) {
|
400 |
dataByModel[model].push(row.duration_ms);
|
401 |
}
|
402 |
});
|
403 |
|
404 |
-
const plotData = Object.keys(dataByModel).sort().map(model => ({
|
405 |
-
y: dataByModel[model],
|
406 |
-
type: 'box',
|
407 |
-
name: model,
|
408 |
-
boxpoints: 'Outliers',
|
409 |
-
marker: { size: 4 }
|
410 |
}));
|
411 |
|
412 |
const layout = mergeLayout({
|
413 |
-
title: { text: 'Latency Distribution by Model' },
|
414 |
yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
|
415 |
-
xaxis: {
|
416 |
-
|
417 |
-
tickangle: -30 // Angle labels if they overlap
|
418 |
-
},
|
419 |
-
margin: { b: 180 } // More bottom margin for potentially long/angled labels
|
420 |
});
|
421 |
-
|
422 |
-
Plotly.newPlot('plotLatencyModel', plotData, layout, {responsive: true});
|
423 |
}
|
424 |
|
425 |
-
function createErrorTypesByProviderPlot(rows) {
|
426 |
-
|
427 |
-
|
428 |
-
|
|
|
429 |
|
430 |
-
|
431 |
-
if (row.response_status_code !== 200 && row.response_status_code !== null) {
|
432 |
const provider = row.provider_name;
|
433 |
const status = row.response_status_code;
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
if (!errorCountsByProvider[provider])
|
438 |
-
errorCountsByProvider[provider] = {};
|
439 |
-
}
|
440 |
-
if (!errorCountsByProvider[provider][status]) {
|
441 |
-
errorCountsByProvider[provider][status] = 0;
|
442 |
-
}
|
443 |
errorCountsByProvider[provider][status]++;
|
444 |
}
|
445 |
-
|
446 |
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
const plotData = sortedErrorCodes.map(status => {
|
451 |
-
return {
|
452 |
-
x: sortedProviders,
|
453 |
-
y: sortedProviders.map(provider => errorCountsByProvider[provider]?.[status] || 0),
|
454 |
-
name: `Error ${status}`,
|
455 |
-
type: 'bar',
|
456 |
-
hovertemplate: `Provider: %{x}<br>Error: ${status}<br>Count: %{y}<extra></extra>`
|
457 |
-
};
|
458 |
-
});
|
459 |
|
460 |
-
|
461 |
-
|
462 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
463 |
xaxis: { title: 'Provider', tickangle: -30 },
|
464 |
yaxis: { title: 'Number of Errors', autorange: true },
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
Plotly.newPlot('plotErrorTypesProvider', plotData, layout, {responsive: true});
|
469 |
}
|
470 |
|
471 |
-
function createLatencyHeatmap(rows) {
|
472 |
-
const latencyData = {};
|
473 |
const allProviders = new Set();
|
474 |
const allModels = new Set();
|
475 |
|
@@ -479,10 +571,8 @@
|
|
479 |
const model = row.model_id;
|
480 |
allProviders.add(provider);
|
481 |
allModels.add(model);
|
482 |
-
|
483 |
if (!latencyData[provider]) latencyData[provider] = {};
|
484 |
if (!latencyData[provider][model]) latencyData[provider][model] = { sum: 0, count: 0 };
|
485 |
-
|
486 |
latencyData[provider][model].sum += row.duration_ms;
|
487 |
latencyData[provider][model].count++;
|
488 |
}
|
@@ -491,46 +581,109 @@
|
|
491 |
const sortedProviders = Array.from(allProviders).sort();
|
492 |
const sortedModels = Array.from(allModels).sort();
|
493 |
|
494 |
-
const zValues = sortedModels.map(model => {
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
500 |
});
|
501 |
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
508 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
509 |
});
|
510 |
|
511 |
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
type: 'heatmap',
|
517 |
-
hoverongaps: false,
|
518 |
-
colorscale: 'Viridis',
|
519 |
-
reversescale: true, // Often makes sense for latency (lower is better -> brighter)
|
520 |
-
colorbar: { title: 'Avg Latency (ms)', titleside: 'right', thickness: 15 },
|
521 |
-
xgap: 2, // Add gaps between cells
|
522 |
-
ygap: 2,
|
523 |
-
hovertemplate: hoverText // Custom hover text
|
524 |
-
}];
|
525 |
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
|
533 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
534 |
}
|
535 |
|
536 |
});
|
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Provider/Model Performance Dashboard</title>
|
7 |
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
|
8 |
<style>
|
9 |
:root {
|
|
|
14 |
--border-color: #dee2e6;
|
15 |
--shadow-color: rgba(0, 0, 0, 0.05);
|
16 |
--primary-color: #0d6efd;
|
17 |
+
--success-color: #198754;
|
18 |
+
--warning-color: #ffc107;
|
19 |
+
--danger-color: #dc3545;
|
20 |
--plot-colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']; /* Plotly default */
|
21 |
}
|
22 |
body {
|
|
|
27 |
line-height: 1.5;
|
28 |
}
|
29 |
.container {
|
30 |
+
max-width: 1700px; /* Wider container */
|
31 |
margin: 20px auto;
|
32 |
padding: 0 20px;
|
33 |
}
|
34 |
h1 {
|
35 |
text-align: center;
|
36 |
color: var(--text-color);
|
37 |
+
margin-bottom: 15px;
|
38 |
font-weight: 500;
|
39 |
}
|
40 |
+
.controls {
|
41 |
+
display: flex;
|
42 |
+
justify-content: center;
|
43 |
+
align-items: center;
|
44 |
+
margin-bottom: 25px;
|
45 |
+
gap: 10px;
|
46 |
+
}
|
47 |
+
.controls label {
|
48 |
+
font-weight: 500;
|
49 |
+
color: var(--muted-text-color);
|
50 |
+
}
|
51 |
+
.controls select {
|
52 |
+
padding: 8px 12px;
|
53 |
+
border: 1px solid var(--border-color);
|
54 |
+
border-radius: 6px;
|
55 |
+
background-color: var(--card-bg-color);
|
56 |
+
min-width: 300px;
|
57 |
+
font-size: 1rem;
|
58 |
+
}
|
59 |
+
|
60 |
.kpi-container {
|
61 |
display: grid;
|
62 |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
74 |
.kpi-card h3 {
|
75 |
margin-top: 0;
|
76 |
margin-bottom: 10px;
|
77 |
+
font-size: 0.95rem; /* Slightly smaller */
|
78 |
color: var(--muted-text-color);
|
79 |
font-weight: 400;
|
80 |
}
|
81 |
.kpi-card .value {
|
82 |
+
font-size: 1.7rem; /* Slightly smaller */
|
83 |
font-weight: 600;
|
84 |
color: var(--text-color);
|
85 |
+
word-wrap: break-word; /* Prevent long provider names from overflowing */
|
86 |
}
|
87 |
.kpi-card .unit {
|
88 |
+
font-size: 0.85rem;
|
89 |
color: var(--muted-text-color);
|
90 |
+
margin-left: 4px;
|
91 |
}
|
92 |
|
93 |
.dashboard-container {
|
|
|
95 |
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); /* Responsive grid */
|
96 |
gap: 25px; /* Space between plots */
|
97 |
}
|
98 |
+
.plot-container, .table-container {
|
99 |
background-color: var(--card-bg-color);
|
100 |
padding: 20px;
|
101 |
border-radius: 8px;
|
|
|
103 |
border: 1px solid var(--border-color);
|
104 |
min-height: 450px; /* Ensure plots have some height */
|
105 |
display: flex; /* For centering loading/error inside */
|
106 |
+
flex-direction: column; /* Allow title and content stacking */
|
107 |
justify-content: center;
|
108 |
align-items: center;
|
109 |
}
|
110 |
+
.plot-container .plotly, .table-container table {
|
|
|
111 |
width: 100%;
|
112 |
height: 100%;
|
113 |
+
flex-grow: 1; /* Allow content to fill space */
|
114 |
+
}
|
115 |
+
.plot-title { /* Optional: Style for titles inside containers */
|
116 |
+
font-weight: 500;
|
117 |
+
margin-bottom: 15px;
|
118 |
+
color: var(--text-color);
|
119 |
+
align-self: flex-start; /* Align title left */
|
120 |
}
|
121 |
|
122 |
#loading, #error {
|
|
|
127 |
color: var(--muted-text-color);
|
128 |
}
|
129 |
#error {
|
130 |
+
color: var(--danger-color);
|
131 |
font-weight: 500;
|
132 |
background-color: #f8d7da;
|
133 |
border: 1px solid #f5c2c7;
|
134 |
border-radius: 8px;
|
135 |
}
|
136 |
+
|
137 |
+
/* Table Styles */
|
138 |
+
.table-container {
|
139 |
+
overflow-x: auto; /* Allow horizontal scrolling on small screens */
|
140 |
+
align-items: flex-start; /* Align table top */
|
141 |
+
}
|
142 |
+
table {
|
143 |
+
border-collapse: collapse;
|
144 |
+
font-size: 0.9rem;
|
145 |
+
}
|
146 |
+
th, td {
|
147 |
+
padding: 10px 12px;
|
148 |
+
text-align: left;
|
149 |
+
border-bottom: 1px solid var(--border-color);
|
150 |
+
}
|
151 |
+
th {
|
152 |
+
background-color: var(--bg-color);
|
153 |
+
font-weight: 500;
|
154 |
+
cursor: pointer; /* Indicate sortable */
|
155 |
+
white-space: nowrap;
|
156 |
+
}
|
157 |
+
th:hover {
|
158 |
+
background-color: #e9ecef;
|
159 |
+
}
|
160 |
+
td {
|
161 |
+
white-space: nowrap; /* Prevent wrapping in cells */
|
162 |
+
}
|
163 |
+
tbody tr:hover {
|
164 |
+
background-color: #f1f1f1;
|
165 |
+
}
|
166 |
+
.error-count {
|
167 |
+
color: var(--danger-color);
|
168 |
+
font-weight: 500;
|
169 |
+
}
|
170 |
+
.success-rate-high { color: var(--success-color); font-weight: 500; }
|
171 |
+
.success-rate-medium { color: var(--warning-color); font-weight: 500; }
|
172 |
+
.success-rate-low { color: var(--danger-color); font-weight: 500; }
|
173 |
+
|
174 |
+
|
175 |
footer {
|
176 |
text-align: center;
|
177 |
margin-top: 40px;
|
|
|
194 |
<div class="container">
|
195 |
<h1>Provider Inference Metrics Dashboard</h1>
|
196 |
|
197 |
+
<div class="controls">
|
198 |
+
<label for="modelSelector">Select Model:</label>
|
199 |
+
<select id="modelSelector">
|
200 |
+
<option value="all">-- All Models --</option>
|
201 |
+
<!-- Options will be populated by JS -->
|
202 |
+
</select>
|
203 |
+
</div>
|
204 |
+
|
205 |
<div id="loading">Loading data... Please wait.</div>
|
206 |
<div id="error" style="display: none;"></div>
|
207 |
|
208 |
<!-- KPI Section -->
|
209 |
<div class="kpi-container" style="display: none;">
|
210 |
<div class="kpi-card">
|
211 |
+
<h3 id="kpi-title-requests">Total Requests</h3>
|
212 |
<div class="value" id="kpi-total-requests">--</div>
|
213 |
</div>
|
214 |
<div class="kpi-card">
|
215 |
+
<h3 id="kpi-title-success">Success Rate</h3>
|
216 |
<div class="value" id="kpi-success-rate">--<span class="unit">%</span></div>
|
217 |
</div>
|
218 |
<div class="kpi-card">
|
219 |
+
<h3 id="kpi-title-latency">Avg. Latency</h3>
|
220 |
<div class="value" id="kpi-avg-latency">--<span class="unit">ms</span></div>
|
221 |
</div>
|
222 |
<div class="kpi-card">
|
223 |
+
<h3 id="kpi-title-fastest">Fastest Provider (Median)</h3>
|
224 |
<div class="value" id="kpi-fastest-provider">--</div>
|
225 |
</div>
|
226 |
</div>
|
|
|
229 |
<div class="dashboard-container" style="display: none;">
|
230 |
<div id="plotLatencyProvider" class="plot-container"></div>
|
231 |
<div id="plotReliabilityProvider" class="plot-container"></div>
|
232 |
+
<div id="plotLatencyModel" class="plot-container"></div> {/* Will be hidden when filtered */}
|
233 |
<div id="plotErrorTypesProvider" class="plot-container"></div>
|
234 |
+
<div id="modelDetailTableContainer" class="table-container" style="display: none;"> {/* Initially hidden */}
|
235 |
+
<h3 class="plot-title" id="table-title">Detailed Comparison</h3>
|
236 |
+
<table id="modelDetailTable">
|
237 |
+
<thead></thead>
|
238 |
+
<tbody></tbody>
|
239 |
+
</table>
|
240 |
+
</div>
|
241 |
+
<div id="plotLatencyHeatmap" class="plot-container"></div> {/* Will be hidden when filtered */}
|
242 |
</div>
|
243 |
|
244 |
<footer id="footer" style="display: none;">
|
|
|
257 |
const footer = document.getElementById('footer');
|
258 |
const dataSourceUrlElement = document.getElementById('data-source-url');
|
259 |
const lastUpdatedElement = document.getElementById('last-updated');
|
260 |
+
const modelSelector = document.getElementById('modelSelector');
|
261 |
+
|
262 |
+
// Plot containers
|
263 |
+
const plotLatencyProviderDiv = document.getElementById('plotLatencyProvider');
|
264 |
+
const plotReliabilityProviderDiv = document.getElementById('plotReliabilityProvider');
|
265 |
+
const plotLatencyModelDiv = document.getElementById('plotLatencyModel');
|
266 |
+
const plotErrorTypesProviderDiv = document.getElementById('plotErrorTypesProvider');
|
267 |
+
const plotLatencyHeatmapDiv = document.getElementById('plotLatencyHeatmap');
|
268 |
+
const modelDetailTableContainerDiv = document.getElementById('modelDetailTableContainer');
|
269 |
+
|
270 |
|
271 |
dataSourceUrlElement.href = apiUrl; // Set link href
|
272 |
|
273 |
+
let allRows = []; // Store all fetched rows globally
|
274 |
+
let uniqueModels = [];
|
275 |
+
|
276 |
// Plotly layout defaults
|
277 |
const baseLayout = {
|
278 |
+
margin: { l: 60, r: 30, b: 100, t: 60, pad: 4 },
|
279 |
legend: { bgcolor: 'rgba(255,255,255,0.5)', bordercolor: '#ccc', borderwidth: 1 },
|
280 |
+
colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
|
281 |
+
paper_bgcolor: 'rgba(0,0,0,0)',
|
282 |
+
plot_bgcolor: 'rgba(0,0,0,0)',
|
283 |
font: {
|
284 |
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
285 |
color: '#212529'
|
286 |
},
|
287 |
title: {
|
288 |
font: { size: 16, weight: '500' },
|
289 |
+
x: 0.05, xanchor: 'left'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
290 |
},
|
291 |
+
xaxis: { gridcolor: '#e9ecef', linecolor: '#adb5bd', automargin: true, tickfont: { size: 10 } },
|
292 |
+
yaxis: { gridcolor: '#e9ecef', linecolor: '#adb5bd', automargin: true, tickfont: { size: 10 } }
|
|
|
|
|
|
|
|
|
293 |
};
|
294 |
|
|
|
295 |
function mergeLayout(customLayout) {
|
296 |
+
let layout = JSON.parse(JSON.stringify(baseLayout));
|
|
|
297 |
for (const key in customLayout) {
|
298 |
if (typeof customLayout[key] === 'object' && customLayout[key] !== null && !Array.isArray(customLayout[key]) && layout[key]) {
|
299 |
Object.assign(layout[key], customLayout[key]);
|
|
|
304 |
return layout;
|
305 |
}
|
306 |
|
|
|
307 |
function calculateMedian(arr) {
|
308 |
if (!arr || arr.length === 0) return null;
|
309 |
const sortedArr = [...arr].sort((a, b) => a - b);
|
|
|
311 |
return sortedArr.length % 2 !== 0 ? sortedArr[mid] : (sortedArr[mid - 1] + sortedArr[mid]) / 2;
|
312 |
}
|
313 |
|
314 |
+
function populateModelSelector() {
|
315 |
+
uniqueModels = [...new Set(allRows.map(r => r.model_id))].sort();
|
316 |
+
uniqueModels.forEach(modelId => {
|
317 |
+
const option = document.createElement('option');
|
318 |
+
option.value = modelId;
|
319 |
+
option.textContent = modelId;
|
320 |
+
modelSelector.appendChild(option);
|
321 |
+
});
|
322 |
+
}
|
323 |
|
324 |
+
function updateDashboard(selectedModelId) {
|
325 |
+
const filteredRows = selectedModelId === 'all'
|
326 |
+
? allRows
|
327 |
+
: allRows.filter(row => row.model_id === selectedModelId);
|
328 |
+
|
329 |
+
console.log(`Updating dashboard for: ${selectedModelId}, Rows: ${filteredRows.length}`);
|
330 |
+
|
331 |
+
// Update KPIs
|
332 |
+
calculateAndDisplayKPIs(filteredRows, selectedModelId);
|
333 |
+
|
334 |
+
// Update Plots
|
335 |
+
createLatencyByProviderPlot(filteredRows, selectedModelId);
|
336 |
+
createReliabilityByProviderPlot(filteredRows, selectedModelId);
|
337 |
+
createErrorTypesByProviderPlot(filteredRows, selectedModelId);
|
338 |
+
|
339 |
+
// Show/Hide plots based on selection
|
340 |
+
if (selectedModelId === 'all') {
|
341 |
+
plotLatencyModelDiv.style.display = 'flex';
|
342 |
+
plotLatencyHeatmapDiv.style.display = 'flex';
|
343 |
+
modelDetailTableContainerDiv.style.display = 'none';
|
344 |
+
createLatencyByModelPlot(filteredRows); // Only create these for 'all'
|
345 |
+
createLatencyHeatmap(filteredRows);
|
346 |
+
} else {
|
347 |
+
plotLatencyModelDiv.style.display = 'none';
|
348 |
+
plotLatencyHeatmapDiv.style.display = 'none';
|
349 |
+
modelDetailTableContainerDiv.style.display = 'flex'; // Show table
|
350 |
+
createModelDetailTable(filteredRows, selectedModelId);
|
351 |
+
}
|
352 |
+
}
|
353 |
+
|
354 |
+
// --- Event Listener ---
|
355 |
+
modelSelector.addEventListener('change', (event) => {
|
356 |
+
updateDashboard(event.target.value);
|
357 |
+
});
|
358 |
+
|
359 |
+
// --- Initial Fetch ---
|
360 |
fetch(apiUrl)
|
361 |
.then(response => {
|
362 |
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
|
|
|
|
|
363 |
return response.json();
|
364 |
})
|
365 |
.then(data => {
|
366 |
+
allRows = data.rows.map(item => item.row); // Store globally
|
367 |
+
console.log(`Fetched ${allRows.length} rows.`);
|
|
|
|
|
|
|
|
|
|
|
|
|
368 |
lastUpdatedElement.textContent = new Date().toLocaleString();
|
369 |
|
370 |
+
populateModelSelector(); // Populate dropdown
|
|
|
371 |
|
372 |
+
loadingDiv.style.display = 'none';
|
373 |
+
kpiContainer.style.display = 'grid';
|
374 |
+
dashboardContainer.style.display = 'grid';
|
375 |
+
footer.style.display = 'block';
|
376 |
+
|
377 |
+
updateDashboard('all'); // Initial load with all models
|
378 |
|
379 |
})
|
380 |
.catch(error => {
|
381 |
console.error('Error fetching or processing data:', error);
|
382 |
loadingDiv.style.display = 'none';
|
383 |
+
errorDiv.textContent = `Error loading data: ${error.message}. Please check the console. Is the dataset server reachable?`;
|
384 |
errorDiv.style.display = 'block';
|
385 |
});
|
386 |
|
387 |
// --- KPI Calculation Function ---
|
388 |
+
function calculateAndDisplayKPIs(rows, selectedModelId) {
|
389 |
+
const context = selectedModelId === 'all' ? 'Overall' : `(${selectedModelId.split('/').pop()})`; // Shorten model name for title
|
390 |
+
|
391 |
const totalRequests = rows.length;
|
392 |
const successfulRequests = rows.filter(r => r.response_status_code === 200).length;
|
393 |
const successRate = totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(1) : 0;
|
|
|
399 |
? (validLatencies.reduce((a, b) => a + b, 0) / validLatencies.length).toFixed(0)
|
400 |
: 0;
|
401 |
|
402 |
+
// Calculate median latency per provider for the filtered data
|
403 |
const latencyByProvider = {};
|
404 |
rows.forEach(row => {
|
405 |
if (row.duration_ms !== null && row.duration_ms >= 0) {
|
|
|
421 |
}
|
422 |
}
|
423 |
|
424 |
+
document.getElementById('kpi-title-requests').textContent = `Total Requests ${context}`;
|
425 |
+
document.getElementById('kpi-title-success').textContent = `Success Rate ${context}`;
|
426 |
+
document.getElementById('kpi-title-latency').textContent = `Avg. Latency ${context}`;
|
427 |
+
document.getElementById('kpi-title-fastest').textContent = `Fastest Provider ${context}`;
|
428 |
+
|
429 |
+
|
430 |
document.getElementById('kpi-total-requests').textContent = totalRequests;
|
431 |
document.getElementById('kpi-success-rate').innerHTML = `${successRate}<span class="unit">%</span>`;
|
432 |
document.getElementById('kpi-avg-latency').innerHTML = `${avgLatency}<span class="unit">ms</span>`;
|
|
|
434 |
}
|
435 |
|
436 |
|
437 |
+
// --- Plotting Functions (Modified to accept data and context) ---
|
438 |
|
439 |
+
function createLatencyByProviderPlot(rows, selectedModelId) {
|
440 |
+
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
|
441 |
const dataByProvider = {};
|
442 |
rows.forEach(row => {
|
443 |
+
if (!dataByProvider[row.provider_name]) dataByProvider[row.provider_name] = [];
|
|
|
|
|
444 |
if (row.duration_ms !== null && row.duration_ms >= 0) {
|
445 |
dataByProvider[row.provider_name].push(row.duration_ms);
|
446 |
}
|
447 |
});
|
448 |
|
449 |
+
const plotData = Object.keys(dataByProvider).sort().map(provider => ({
|
450 |
+
y: dataByProvider[provider], type: 'box', name: provider, boxpoints: 'Outliers', marker: { size: 4 }
|
|
|
|
|
|
|
|
|
451 |
}));
|
452 |
|
453 |
const layout = mergeLayout({
|
454 |
+
title: { text: `Latency Distribution by Provider ${titleContext}` },
|
455 |
yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
|
456 |
xaxis: { title: 'Provider', tickangle: -30 },
|
457 |
+
margin: { b: 120 }
|
458 |
});
|
459 |
+
Plotly.react(plotLatencyProviderDiv, plotData, layout, {responsive: true}); // Use react for updates
|
|
|
460 |
}
|
461 |
|
462 |
+
function createReliabilityByProviderPlot(rows, selectedModelId) {
|
463 |
+
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
|
464 |
+
const statusCountsByProvider = {};
|
465 |
+
const providersInSelection = new Set();
|
466 |
+
const statusCodesInSelection = new Set();
|
467 |
|
468 |
+
rows.forEach(row => {
|
469 |
const provider = row.provider_name;
|
470 |
+
const status = row.response_status_code ?? 'Unknown';
|
471 |
+
providersInSelection.add(provider);
|
472 |
+
statusCodesInSelection.add(status);
|
473 |
+
if (!statusCountsByProvider[provider]) statusCountsByProvider[provider] = {};
|
474 |
+
if (!statusCountsByProvider[provider][status]) statusCountsByProvider[provider][status] = 0;
|
|
|
|
|
|
|
|
|
|
|
475 |
statusCountsByProvider[provider][status]++;
|
476 |
+
});
|
477 |
|
478 |
+
const sortedProviders = Array.from(providersInSelection).sort();
|
479 |
+
const sortedStatusCodes = Array.from(statusCodesInSelection).sort((a, b) => {
|
480 |
+
if (a === 200) return -1; if (b === 200) return 1;
|
481 |
+
if (a === 'Unknown') return 1; if (b === 'Unknown') return -1;
|
|
|
|
|
|
|
482 |
return a - b;
|
483 |
+
});
|
484 |
|
485 |
+
const plotData = sortedStatusCodes.map(status => ({
|
486 |
+
x: sortedProviders,
|
487 |
+
y: sortedProviders.map(provider => statusCountsByProvider[provider]?.[status] || 0),
|
488 |
+
name: `Status ${status}`, type: 'bar',
|
489 |
+
hovertemplate: `Provider: %{x}<br>Status: ${status}<br>Count: %{y}<extra></extra>`
|
490 |
+
}));
|
|
|
|
|
|
|
491 |
|
492 |
+
const layout = mergeLayout({
|
493 |
+
title: { text: `Request Status Codes by Provider ${titleContext}` },
|
494 |
barmode: 'stack',
|
495 |
xaxis: { title: 'Provider', tickangle: -30 },
|
496 |
yaxis: { title: 'Number of Requests', autorange: true },
|
497 |
margin: { b: 120 }
|
498 |
+
});
|
499 |
+
Plotly.react(plotReliabilityProviderDiv, plotData, layout, {responsive: true});
|
|
|
500 |
}
|
501 |
|
502 |
+
function createLatencyByModelPlot(rows) { // Only shown for 'all'
|
503 |
const dataByModel = {};
|
504 |
rows.forEach(row => {
|
505 |
const model = row.model_id;
|
506 |
+
if (!dataByModel[model]) dataByModel[model] = [];
|
|
|
|
|
507 |
if (row.duration_ms !== null && row.duration_ms >= 0) {
|
508 |
dataByModel[model].push(row.duration_ms);
|
509 |
}
|
510 |
});
|
511 |
|
512 |
+
const plotData = Object.keys(dataByModel).sort().map(model => ({
|
513 |
+
y: dataByModel[model], type: 'box', name: model, boxpoints: 'Outliers', marker: { size: 4 }
|
|
|
|
|
|
|
|
|
514 |
}));
|
515 |
|
516 |
const layout = mergeLayout({
|
517 |
+
title: { text: 'Latency Distribution by Model (All Providers)' },
|
518 |
yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
|
519 |
+
xaxis: { title: 'Model ID', tickangle: -30 },
|
520 |
+
margin: { b: 180 }
|
|
|
|
|
|
|
521 |
});
|
522 |
+
Plotly.react(plotLatencyModelDiv, plotData, layout, {responsive: true});
|
|
|
523 |
}
|
524 |
|
525 |
+
function createErrorTypesByProviderPlot(rows, selectedModelId) {
|
526 |
+
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
|
527 |
+
const errorCountsByProvider = {};
|
528 |
+
const providersInSelection = new Set();
|
529 |
+
const errorCodesInSelection = new Set();
|
530 |
|
531 |
+
rows.forEach(row => {
|
532 |
+
if (row.response_status_code !== 200 && row.response_status_code !== null) {
|
533 |
const provider = row.provider_name;
|
534 |
const status = row.response_status_code;
|
535 |
+
providersInSelection.add(provider);
|
536 |
+
errorCodesInSelection.add(status);
|
537 |
+
if (!errorCountsByProvider[provider]) errorCountsByProvider[provider] = {};
|
538 |
+
if (!errorCountsByProvider[provider][status]) errorCountsByProvider[provider][status] = 0;
|
|
|
|
|
|
|
|
|
|
|
539 |
errorCountsByProvider[provider][status]++;
|
540 |
}
|
541 |
+
});
|
542 |
|
543 |
+
const sortedProviders = Array.from(providersInSelection).sort();
|
544 |
+
const sortedErrorCodes = Array.from(errorCodesInSelection).sort((a, b) => a - b);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
545 |
|
546 |
+
const plotData = sortedErrorCodes.map(status => ({
|
547 |
+
x: sortedProviders,
|
548 |
+
y: sortedProviders.map(provider => errorCountsByProvider[provider]?.[status] || 0),
|
549 |
+
name: `Error ${status}`, type: 'bar',
|
550 |
+
hovertemplate: `Provider: %{x}<br>Error: ${status}<br>Count: %{y}<extra></extra>`
|
551 |
+
}));
|
552 |
+
|
553 |
+
const layout = mergeLayout({
|
554 |
+
title: { text: `Error Types by Provider (Non-200 Status) ${titleContext}` },
|
555 |
+
barmode: 'group',
|
556 |
xaxis: { title: 'Provider', tickangle: -30 },
|
557 |
yaxis: { title: 'Number of Errors', autorange: true },
|
558 |
+
margin: { b: 120 }
|
559 |
+
});
|
560 |
+
Plotly.react(plotErrorTypesProviderDiv, plotData, layout, {responsive: true});
|
|
|
561 |
}
|
562 |
|
563 |
+
function createLatencyHeatmap(rows) { // Only shown for 'all'
|
564 |
+
const latencyData = {};
|
565 |
const allProviders = new Set();
|
566 |
const allModels = new Set();
|
567 |
|
|
|
571 |
const model = row.model_id;
|
572 |
allProviders.add(provider);
|
573 |
allModels.add(model);
|
|
|
574 |
if (!latencyData[provider]) latencyData[provider] = {};
|
575 |
if (!latencyData[provider][model]) latencyData[provider][model] = { sum: 0, count: 0 };
|
|
|
576 |
latencyData[provider][model].sum += row.duration_ms;
|
577 |
latencyData[provider][model].count++;
|
578 |
}
|
|
|
581 |
const sortedProviders = Array.from(allProviders).sort();
|
582 |
const sortedModels = Array.from(allModels).sort();
|
583 |
|
584 |
+
const zValues = sortedModels.map(model => sortedProviders.map(provider => {
|
585 |
+
const data = latencyData[provider]?.[model];
|
586 |
+
return data?.count > 0 ? data.sum / data.count : null;
|
587 |
+
}));
|
588 |
+
const hoverText = sortedModels.map(model => sortedProviders.map(provider => {
|
589 |
+
const data = latencyData[provider]?.[model];
|
590 |
+
const avg = data?.count > 0 ? (data.sum / data.count).toFixed(0) : 'N/A';
|
591 |
+
const count = data?.count || 0;
|
592 |
+
return `Model: ${model}<br>Provider: ${provider}<br>Avg Latency: ${avg} ms<br>Requests: ${count}<extra></extra>`;
|
593 |
+
}));
|
594 |
+
|
595 |
+
const plotData = [{
|
596 |
+
z: zValues, x: sortedProviders, y: sortedModels, type: 'heatmap',
|
597 |
+
hoverongaps: false, colorscale: 'Viridis', reversescale: true,
|
598 |
+
colorbar: { title: 'Avg Latency (ms)', titleside: 'right', thickness: 15 },
|
599 |
+
xgap: 2, ygap: 2, hovertemplate: hoverText
|
600 |
+
}];
|
601 |
+
|
602 |
+
const layout = mergeLayout({
|
603 |
+
title: { text: 'Average Latency (ms) - Model vs. Provider' },
|
604 |
+
xaxis: { title: '', side: 'top', tickangle: -30 },
|
605 |
+
yaxis: { title: '', autorange: 'reversed' },
|
606 |
+
margin: { l: 280, r: 50, b: 50, t: 120 }
|
607 |
+
});
|
608 |
+
Plotly.react(plotLatencyHeatmapDiv, plotData, layout, {responsive: true});
|
609 |
+
}
|
610 |
+
|
611 |
+
function createModelDetailTable(rows, selectedModelId) {
|
612 |
+
document.getElementById('table-title').textContent = `Detailed Comparison for ${selectedModelId.split('/').pop()}`;
|
613 |
+
const tableHead = document.querySelector('#modelDetailTable thead');
|
614 |
+
const tableBody = document.querySelector('#modelDetailTable tbody');
|
615 |
+
tableHead.innerHTML = ''; // Clear previous header
|
616 |
+
tableBody.innerHTML = ''; // Clear previous body
|
617 |
+
|
618 |
+
const providerStats = {}; // { provider: { total: 0, success: 0, errors: 0, latencies: [] } }
|
619 |
+
|
620 |
+
rows.forEach(row => {
|
621 |
+
const provider = row.provider_name;
|
622 |
+
if (!providerStats[provider]) {
|
623 |
+
providerStats[provider] = { total: 0, success: 0, errors: 0, latencies: [] };
|
624 |
+
}
|
625 |
+
providerStats[provider].total++;
|
626 |
+
if (row.response_status_code === 200) {
|
627 |
+
providerStats[provider].success++;
|
628 |
+
} else if (row.response_status_code !== null) {
|
629 |
+
providerStats[provider].errors++;
|
630 |
+
}
|
631 |
+
if (row.duration_ms !== null && row.duration_ms >= 0) {
|
632 |
+
providerStats[provider].latencies.push(row.duration_ms);
|
633 |
+
}
|
634 |
+
});
|
635 |
+
|
636 |
+
// Create Header
|
637 |
+
const headerRow = tableHead.insertRow();
|
638 |
+
const headers = ['Provider', 'Median Latency (ms)', 'Success Rate (%)', 'Error Count', 'Total Requests'];
|
639 |
+
headers.forEach(text => {
|
640 |
+
const th = document.createElement('th');
|
641 |
+
th.textContent = text;
|
642 |
+
headerRow.appendChild(th);
|
643 |
});
|
644 |
|
645 |
+
// Create Body Rows
|
646 |
+
const providerDataArray = [];
|
647 |
+
for (const provider in providerStats) {
|
648 |
+
const stats = providerStats[provider];
|
649 |
+
const medianLatency = calculateMedian(stats.latencies);
|
650 |
+
const successRate = stats.total > 0 ? (stats.success / stats.total * 100) : 0;
|
651 |
+
providerDataArray.push({
|
652 |
+
provider: provider,
|
653 |
+
medianLatency: medianLatency,
|
654 |
+
successRate: successRate,
|
655 |
+
errors: stats.errors,
|
656 |
+
total: stats.total
|
657 |
});
|
658 |
+
}
|
659 |
+
|
660 |
+
// Sort initially by median latency (ascending)
|
661 |
+
providerDataArray.sort((a, b) => {
|
662 |
+
if (a.medianLatency === null) return 1; // Nulls last
|
663 |
+
if (b.medianLatency === null) return -1;
|
664 |
+
return a.medianLatency - b.medianLatency;
|
665 |
});
|
666 |
|
667 |
|
668 |
+
providerDataArray.forEach(data => {
|
669 |
+
const row = tableBody.insertRow();
|
670 |
+
row.insertCell().textContent = data.provider;
|
671 |
+
row.insertCell().textContent = data.medianLatency !== null ? data.medianLatency.toFixed(0) : 'N/A';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
672 |
|
673 |
+
const successCell = row.insertCell();
|
674 |
+
successCell.textContent = data.successRate.toFixed(1);
|
675 |
+
// Add color coding for success rate
|
676 |
+
if (data.successRate >= 95) successCell.className = 'success-rate-high';
|
677 |
+
else if (data.successRate >= 80) successCell.className = 'success-rate-medium';
|
678 |
+
else successCell.className = 'success-rate-low';
|
679 |
|
680 |
+
|
681 |
+
const errorCell = row.insertCell();
|
682 |
+
errorCell.textContent = data.errors;
|
683 |
+
if (data.errors > 0) errorCell.classList.add('error-count');
|
684 |
+
|
685 |
+
row.insertCell().textContent = data.total;
|
686 |
+
});
|
687 |
}
|
688 |
|
689 |
});
|