providers-metrics / index.html
victor's picture
victor HF Staff
style: Improve UI with enhanced visuals, responsiveness, and loading
e5231eb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Provider/Model Performance Dashboard</title>
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
<style>
:root {
--bg-color: #0d1117;
--card-bg-color: #161b22;
--text-color: #e6edf3;
--muted-text-color: #7d8590;
--border-color: #30363d;
--shadow-color: rgba(1, 4, 9, 0.8);
--primary-color: #58a6ff;
--success-color: #3fb950;
--warning-color: #d29922;
--danger-color: #f85149;
--plot-colorway: ['#58a6ff', '#bc8cff', '#ff7b72', '#ffa657', '#7ee787', '#a5d6ff', '#79c0ff', '#d2a8ff', '#ffa198', '#ffdfb6'];
--glass-effect: rgba(22, 27, 34, 0.7);
--highlight-color: rgba(88, 166, 255, 0.1);
}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
font-weight: 400;
background-image:
radial-gradient(circle at 25% 25%, rgba(88, 166, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(188, 140, 255, 0.1) 0%, transparent 50%);
}
.container {
max-width: 1800px;
margin: 0 auto;
padding: 0 30px;
}
h1 {
text-align: center;
color: var(--text-color);
margin-bottom: 15px;
font-weight: 400;
letter-spacing: 0.5px;
font-size: 1.8rem;
}
.controls {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 25px;
gap: 10px;
}
.controls label {
font-weight: 500;
color: var(--muted-text-color);
}
.controls {
margin-bottom: 40px;
padding: 20px;
background: var(--glass-effect);
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(8px);
max-width: fit-content;
margin-left: auto;
margin-right: auto;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
@media (max-width: 768px) {
.controls {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.controls select {
width: 100%;
min-width: auto;
}
}
.controls select {
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 12px;
background: var(--glass-effect);
min-width: 320px;
font-size: 1rem;
color: var(--text-color);
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237d8590' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 16px center;
background-size: 16px;
transition: all 0.2s ease;
}
.controls select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.3);
}
.kpi-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.kpi-card {
background: var(--glass-effect);
padding: 24px;
border-radius: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
text-align: center;
border: 1px solid var(--border-color);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(8px);
position: relative;
overflow: hidden;
}
.kpi-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary-color), transparent);
opacity: 0.8;
}
.kpi-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
border-color: var(--primary-color);
}
.kpi-card:active {
transform: translateY(0);
}
.kpi-card h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 0.95rem; /* Slightly smaller */
color: var(--muted-text-color);
font-weight: 400;
}
.kpi-card .value {
font-size: 1.7rem; /* Slightly smaller */
font-weight: 600;
color: var(--text-color);
word-wrap: break-word; /* Prevent long provider names from overflowing */
}
.kpi-card .unit {
font-size: 0.85rem;
color: var(--muted-text-color);
margin-left: 4px;
}
.dashboard-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 450px), 1fr));
gap: 25px;
margin-bottom: 30px;
}
@media (max-width: 768px) {
.dashboard-container {
grid-template-columns: 1fr;
gap: 20px;
}
.plot-container, .table-container {
min-height: 400px;
}
}
.plot-container, .table-container {
background: var(--glass-effect);
padding: 24px;
border-radius: 16px;
box-shadow: 0 8px 32px var(--shadow-color);
border: 1px solid var(--border-color);
transition: all 0.3s ease;
min-height: 500px;
backdrop-filter: blur(8px);
position: relative;
display: flex; /* For centering loading/error inside */
flex-direction: column; /* Allow title and content stacking */
justify-content: flex-start; /* Align content top */
align-items: center;
overflow: hidden; /* Prevent content spillover */
}
.plot-container .plotly, .table-container table {
width: 100%;
height: 100%;
flex-grow: 1; /* Allow content to fill space */
}
.plot-title { /* Optional: Style for titles inside containers */
font-weight: 500;
margin-bottom: 15px;
color: var(--text-color);
align-self: flex-start; /* Align title left */
width: 100%; /* Ensure title takes full width */
}
#loading, #error {
grid-column: 1 / -1; /* Span full width if grid is active */
text-align: center;
font-size: 1.2em;
padding: 40px;
color: var(--muted-text-color);
}
#error {
color: var(--danger-color);
font-weight: 500;
background-color: #f8d7da;
border: 1px solid #f5c2c7;
border-radius: 8px;
}
/* Table Styles */
.table-wrapper { /* Added wrapper for scrolling */
width: 100%;
overflow-x: auto;
flex-grow: 1;
}
table {
border-collapse: collapse;
font-size: 0.9rem;
width: 100%; /* Make table take full width of wrapper */
}
th, td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
white-space: nowrap; /* Prevent wrapping */
}
th {
background-color: var(--bg-color);
font-weight: 500;
position: sticky; /* Sticky header */
top: 0;
z-index: 1;
}
tbody tr:hover {
background-color: var(--highlight-color);
}
.error-count, .status-error {
color: var(--danger-color);
font-weight: 500;
}
.status-success {
color: var(--success-color);
font-weight: 500;
}
.success-rate-high { color: var(--success-color); font-weight: 500; }
.success-rate-medium { color: var(--warning-color); font-weight: 500; }
.success-rate-low { color: var(--danger-color); font-weight: 500; }
.inspect-button {
padding: 4px 8px;
font-size: 0.8rem;
cursor: pointer;
background-color: #000000;
color: #ffffff;
border: 1px solid #ffffff;
border-radius: 4px;
transition: all 0.2s ease;
}
.inspect-button:hover {
background-color: #222222;
}
/* Inspector Modal Styles */
.modal-overlay {
display: none; /* Hidden by default */
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto; /* Enable scroll if needed */
background-color: rgba(0,0,0,0.5); /* Black w/ opacity */
}
.modal-content {
background-color: var(--card-bg-color);
margin: 5% auto; /* 5% from the top and centered */
padding: 25px;
border: 1px solid var(--border-color);
border-radius: 8px;
width: 85%; /* Could be more or less, depending on screen size */
max-width: 1000px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
position: relative;
max-height: 85vh; /* Limit height */
overflow-y: auto; /* Add scroll to modal content */
}
.modal-close {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.modal-close:hover,
.modal-close:focus {
color: var(--text-color);
text-decoration: none;
}
.modal-content h2 {
margin-top: 0;
font-weight: 500;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
margin-bottom: 20px;
}
.modal-content h3 {
font-size: 1.1rem;
font-weight: 500;
margin-top: 20px;
margin-bottom: 8px;
color: var(--primary-color);
}
.modal-content pre {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 15px;
font-size: 0.85rem;
white-space: pre-wrap; /* Allow wrapping */
word-wrap: break-word;
max-height: 300px; /* Limit height of code blocks */
overflow-y: auto; /* Add scroll to code blocks */
}
.modal-content p {
margin-bottom: 5px;
}
.modal-content strong {
color: var(--muted-text-color);
min-width: 120px;
display: inline-block;
}
footer {
text-align: center;
margin-top: 40px;
padding: 20px;
font-size: 0.9em;
color: var(--muted-text-color);
border-top: 1px solid var(--border-color);
}
footer a {
color: var(--primary-color);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>Provider Inference Metrics Dashboard</h1>
<div class="controls">
<label for="modelSelector">Select Model:</label>
<select id="modelSelector">
<option value="all">-- All Models --</option>
<!-- Options will be populated by JS -->
</select>
</div>
<div id="loading">
<div class="spinner"></div>
<div id="loading-message">Loading data... Please wait.</div>
</div>
<div id="error" style="display: none;"></div>
<!-- KPI Section -->
<div class="kpi-container" style="display: none;">
<div class="kpi-card">
<h3 id="kpi-title-requests">Total Requests</h3>
<div class="value" id="kpi-total-requests">--</div>
</div>
<div class="kpi-card">
<h3 id="kpi-title-success">Success Rate</h3>
<div class="value" id="kpi-success-rate">--<span class="unit">%</span></div>
</div>
<div class="kpi-card">
<h3 id="kpi-title-latency">Avg. Latency</h3>
<div class="value" id="kpi-avg-latency">--<span class="unit">ms</span></div>
</div>
<div class="kpi-card">
<h3 id="kpi-title-fastest">Fastest Provider (Median)</h3>
<div class="value" id="kpi-fastest-provider">--</div>
</div>
</div>
<!-- Dashboard Plots -->
<div class="dashboard-container" style="display: none;">
<div id="plotLatencyProvider" class="plot-container"></div>
<div id="plotReliabilityProvider" class="plot-container"></div>
<div id="plotLatencyModel" class="plot-container"></div>
<div id="plotErrorTypesProvider" class="plot-container"></div>
<div id="modelDetailTableContainer" class="table-container" style="display: none;">
<h3 class="plot-title" id="table-title">Detailed Comparison</h3>
<div class="table-wrapper">
<table id="modelDetailTable">
<thead></thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="plotLatencyHeatmap" class="plot-container"></div>
</div>
<!-- Request Inspector Table -->
<div id="requestInspectorContainer" class="table-container" style="display: none; min-height: 300px;">
<h3 class="plot-title" id="request-table-title">Request Inspector</h3>
<div class="table-wrapper">
<table id="requestTable">
<thead></thead>
<tbody></tbody>
</table>
</div>
</div>
<footer id="footer" style="display: none;">
Data fetched from: <a id="data-source-url" href="#" target="_blank">Hugging Face Datasets</a><br>
Showing <span id="requests-count-footer">--</span> requests. Last updated: <span id="last-updated"></span>
</footer>
</div>
<!-- Inspector Modal -->
<div id="inspectorModal" class="modal-overlay">
<div class="modal-content">
<span class="modal-close" onclick="hideInspectorModal()">×</span>
<h2 id="modal-title">Request Details</h2>
<h3>Summary</h3>
<p><strong>Provider:</strong> <span id="modal-provider"></span></p>
<p><strong>Model:</strong> <span id="modal-model"></span></p>
<p><strong>Status:</strong> <span id="modal-status"></span></p>
<p><strong>Duration:</strong> <span id="modal-duration"></span> ms</p>
<p><strong>Error:</strong> <span id="modal-error"></span></p>
<p><strong>Timestamp (Start):</strong> <span id="modal-timestamp"></span></p>
<h3>Request Body</h3>
<pre><code id="modal-req-body"></code></pre>
<h3>Response Body</h3>
<pre><code id="modal-resp-body"></code></pre>
<h3>Request Headers (Sanitized)</h3>
<pre><code id="modal-req-headers"></code></pre>
<h3>Response Headers (Sanitized)</h3>
<pre><code id="modal-resp-headers"></code></pre>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const apiUrl = "https://datasets-server.huggingface.co/rows?dataset=victor%2Fproviders-metrics&config=default&split=train&offset=0&length=100"; // Fetch 100 rows
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const kpiContainer = document.querySelector('.kpi-container');
const dashboardContainer = document.querySelector('.dashboard-container');
const requestInspectorContainer = document.getElementById('requestInspectorContainer');
const footer = document.getElementById('footer');
const dataSourceUrlElement = document.getElementById('data-source-url');
const lastUpdatedElement = document.getElementById('last-updated');
const loadingMessage = document.getElementById('loading-message'); // Get loading message element
const requestsCountFooter = document.getElementById('requests-count-footer');
const modelSelector = document.getElementById('modelSelector');
const inspectorModal = document.getElementById('inspectorModal');
const requestTableBody = document.querySelector('#requestTable tbody');
// Plot containers
const plotLatencyProviderDiv = document.getElementById('plotLatencyProvider');
const plotReliabilityProviderDiv = document.getElementById('plotReliabilityProvider');
const plotLatencyModelDiv = document.getElementById('plotLatencyModel');
const plotErrorTypesProviderDiv = document.getElementById('plotErrorTypesProvider');
const plotLatencyHeatmapDiv = document.getElementById('plotLatencyHeatmap');
const modelDetailTableContainerDiv = document.getElementById('modelDetailTableContainer');
// Base URL without offset/length for display
const baseApiUrlDisplay = "https://datasets-server.huggingface.co/rows?dataset=victor%2Fproviders-metrics&config=default&split=train";
dataSourceUrlElement.href = baseApiUrlDisplay; // Set link href
let allRows = []; // Store all fetched rows globally
let currentFilteredRows = []; // Store currently filtered rows
let uniqueModels = [];
let currentSortKey = null; // Track current sort column for requestTable
let currentSortDirection = 'asc'; // Track current sort direction for requestTable
let modelDetailSortKey = 'medianLatency'; // Default sort for detail table
let modelDetailSortDirection = 'asc'; // Default direction for detail table
const rowsPerFetch = 100; // How many rows to fetch per API call
// Plotly layout defaults (same as before)
const baseLayout = {
margin: { l: 60, r: 30, b: 100, t: 60, pad: 4 },
legend: { bgcolor: 'var(--card-bg-color)', bordercolor: 'var(--border-color)', borderwidth: 1 },
colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: {
family: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
color: '#e0e0e0'
},
title: {
font: { size: 16, weight: '500' },
x: 0.05, xanchor: 'left'
},
xaxis: { gridcolor: '#2a2a2a', linecolor: '#3a3a3a', automargin: true, tickfont: { size: 10 } },
yaxis: { gridcolor: '#2a2a2a', linecolor: '#3a3a3a', automargin: true, tickfont: { size: 10 } }
};
function mergeLayout(customLayout) {
let layout = JSON.parse(JSON.stringify(baseLayout));
for (const key in customLayout) {
if (typeof customLayout[key] === 'object' && customLayout[key] !== null && !Array.isArray(customLayout[key]) && layout[key]) {
Object.assign(layout[key], customLayout[key]);
} else {
layout[key] = customLayout[key];
}
}
return layout;
}
function calculateMedian(arr) {
if (!arr || arr.length === 0) return null;
const sortedArr = [...arr].filter(n => n !== null && n >= 0).sort((a, b) => a - b); // Filter nulls/negatives before sort
if (sortedArr.length === 0) return null;
const mid = Math.floor(sortedArr.length / 2);
return sortedArr.length % 2 !== 0 ? sortedArr[mid] : (sortedArr[mid - 1] + sortedArr[mid]) / 2;
}
function populateModelSelector() {
uniqueModels = [...new Set(allRows.map(r => r.model_id))].sort();
uniqueModels.forEach(modelId => {
const option = document.createElement('option');
option.value = modelId;
option.textContent = modelId;
modelSelector.appendChild(option);
});
}
function updateDashboard(selectedModelId) {
currentFilteredRows = selectedModelId === 'all' // Update global filtered rows
? allRows
: allRows.filter(row => row.model_id === selectedModelId);
console.log(`Updating dashboard for: ${selectedModelId}, Rows: ${currentFilteredRows.length}`);
requestsCountFooter.textContent = currentFilteredRows.length; // Update footer count
// Update KPIs
calculateAndDisplayKPIs(currentFilteredRows, selectedModelId);
// Update Plots
createLatencyByProviderPlot(currentFilteredRows, selectedModelId);
createReliabilityByProviderPlot(currentFilteredRows, selectedModelId);
createErrorTypesByProviderPlot(currentFilteredRows, selectedModelId);
// Update Request Inspector Table
createRequestTable(currentFilteredRows);
requestInspectorContainer.style.display = 'flex'; // Show inspector table
// Show/Hide plots based on selection
if (selectedModelId === 'all') {
plotLatencyModelDiv.style.display = 'flex';
plotLatencyHeatmapDiv.style.display = 'flex';
modelDetailTableContainerDiv.style.display = 'none';
createLatencyByModelPlot(currentFilteredRows); // Only create these for 'all'
createLatencyHeatmap(currentFilteredRows);
} else {
plotLatencyModelDiv.style.display = 'none';
plotLatencyHeatmapDiv.style.display = 'none';
modelDetailTableContainerDiv.style.display = 'flex'; // Show table
createModelDetailTable(currentFilteredRows, selectedModelId);
}
}
// --- Event Listeners ---
modelSelector.addEventListener('change', (event) => {
updateDashboard(event.target.value);
});
// Event listener for inspect buttons (delegated)
requestTableBody.addEventListener('click', (event) => {
if (event.target.classList.contains('inspect-button')) {
const rowIndex = parseInt(event.target.getAttribute('data-row-index'), 10);
// Find the original row index in allRows based on the filtered index
const originalRowData = currentFilteredRows[rowIndex]; // Get data from the *filtered* array using the index from the table
if (originalRowData) {
showInspectorModal(originalRowData);
} else {
console.error("Could not find row data for index:", rowIndex);
}
}
});
// Close modal if overlay is clicked
inspectorModal.addEventListener('click', (event) => {
if (event.target === inspectorModal) { // Check if the click was directly on the overlay
hideInspectorModal();
}
});
// Sorting listener for Request Inspector Table Header
document.querySelector('#requestTable thead').addEventListener('click', (event) => {
const header = event.target.closest('th.sortable-header');
if (!header) return; // Clicked outside a sortable header
const sortKey = header.dataset.sortKey;
// Determine new sort direction
if (sortKey === currentSortKey) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
currentSortKey = sortKey;
currentSortDirection = 'asc';
}
sortTable(currentSortKey, currentSortDirection);
});
// Sorting listener for Model Detail Table Header
document.querySelector('#modelDetailTable thead').addEventListener('click', (event) => {
const header = event.target.closest('th.sortable-header');
if (!header) return;
const sortKey = header.dataset.sortKey;
if (sortKey === modelDetailSortKey) {
modelDetailSortDirection = modelDetailSortDirection === 'asc' ? 'desc' : 'asc';
} else {
modelDetailSortKey = sortKey;
modelDetailSortDirection = 'asc';
}
// Re-render the table which includes sorting
const selectedModel = modelSelector.value;
const rowsForTable = selectedModel === 'all' ? allRows : allRows.filter(row => row.model_id === selectedModel);
createModelDetailTable(rowsForTable, selectedModel);
});
// --- Fetching Logic ---
function fetchAllRows(offset = 0) {
const apiUrl = `https://datasets-server.huggingface.co/rows?dataset=victor%2Fproviders-metrics&config=default&split=train&offset=${offset}&length=${rowsPerFetch}`;
loadingMessage.textContent = `Fetching rows ${offset + 1} - ${offset + rowsPerFetch}... (Total: ${allRows.length})`;
fetch(apiUrl)
.then(response => {
if (!response.ok) {
// Handle specific errors if needed, e.g., 404 might mean end of data for some APIs
throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
}
return response.json();
})
.then(data => {
const fetchedRows = data.rows.map((item, index) => ({ ...item.row, originalIndex: offset + index }));
allRows = allRows.concat(fetchedRows); // Append new rows
console.log(`Fetched ${fetchedRows.length} rows. Total rows: ${allRows.length}`);
// Check if we got fewer rows than requested, or zero rows, indicating the end
if (fetchedRows.length < rowsPerFetch || fetchedRows.length === 0) {
console.log("Finished fetching all rows.");
lastUpdatedElement.textContent = new Date().toLocaleString();
loadingDiv.style.display = 'none'; // Hide loading indicator
kpiContainer.style.display = 'grid';
dashboardContainer.style.display = 'grid';
footer.style.display = 'block';
populateModelSelector(); // Populate dropdown *after* all data is fetched
updateDashboard('all'); // Initial load with all models
} else {
// Fetch the next batch
fetchAllRows(offset + rowsPerFetch);
}
})
.catch(error => {
console.error('Error fetching data:', error);
loadingDiv.style.display = 'none'; // Hide loading on error
errorDiv.textContent = `Error fetching data at offset ${offset}: ${error.message}. Displaying partial data (${allRows.length} rows). Please check console.`;
errorDiv.style.display = 'block';
// Optionally, display the dashboard with partial data
if (allRows.length > 0) {
console.log("Displaying dashboard with partial data.");
lastUpdatedElement.textContent = new Date().toLocaleString() + " (Partial Data)";
kpiContainer.style.display = 'grid';
dashboardContainer.style.display = 'grid';
footer.style.display = 'block';
populateModelSelector();
updateDashboard('all');
}
});
}
// --- Start the fetching process ---
fetchAllRows();
// --- KPI Calculation Function ---
function calculateAndDisplayKPIs(rows, selectedModelId) {
const context = selectedModelId === 'all' ? 'Overall' : `(${selectedModelId.split('/').pop()})`; // Shorten model name for title
const totalRequests = rows.length;
const successfulRequests = rows.filter(r => r.response_status_code === 200).length;
const successRate = totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(1) : 0;
const validLatencies = rows
.map(r => r.duration_ms)
.filter(d => d !== null && d >= 0); // Keep this filter for null/negative durations
// Filter for successful requests *before* calculating average latency
const successfulRows = rows.filter(r => r.response_status_code === 200);
const successfulLatencies = successfulRows
.map(r => r.duration_ms)
.filter(d => d !== null && d >= 0);
const avgLatency = successfulLatencies.length > 0
? (successfulLatencies.reduce((a, b) => a + b, 0) / successfulLatencies.length).toFixed(0)
: 0;
// Calculate median latency per provider using only successful requests
const latencyByProvider = {};
successfulRows.forEach(row => { // Iterate over successful rows only
if (row.duration_ms !== null && row.duration_ms >= 0) { // Still check for valid duration
if (!latencyByProvider[row.provider_name]) {
latencyByProvider[row.provider_name] = [];
}
latencyByProvider[row.provider_name].push(row.duration_ms);
}
});
let fastestProvider = '--';
let minMedianLatency = Infinity;
for (const provider in latencyByProvider) {
const median = calculateMedian(latencyByProvider[provider]);
if (median !== null && median < minMedianLatency) {
minMedianLatency = median;
fastestProvider = provider;
}
}
document.getElementById('kpi-title-requests').textContent = `Total Requests ${context}`;
document.getElementById('kpi-title-success').textContent = `Success Rate ${context}`;
document.getElementById('kpi-title-latency').textContent = `Avg. Latency ${context}`;
document.getElementById('kpi-title-fastest').textContent = `Fastest Provider ${context}`;
document.getElementById('kpi-total-requests').textContent = totalRequests;
document.getElementById('kpi-success-rate').innerHTML = `${successRate}<span class="unit">%</span>`;
document.getElementById('kpi-avg-latency').innerHTML = `${avgLatency}<span class="unit">ms</span>`;
document.getElementById('kpi-fastest-provider').textContent = fastestProvider;
}
// --- Plotting Functions (Modified to accept data and context) ---
// (Plotting functions: createLatencyByProviderPlot, createReliabilityByProviderPlot, createLatencyByModelPlot, createErrorTypesByProviderPlot, createLatencyHeatmap remain largely the same as previous version, just ensure they use Plotly.react for updates)
function createLatencyByProviderPlot(rows, selectedModelId) {
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
const dataByProvider = {};
// Filter for successful requests before processing
rows.filter(r => r.response_status_code === 200).forEach(row => {
if (!dataByProvider[row.provider_name]) dataByProvider[row.provider_name] = [];
if (row.duration_ms !== null && row.duration_ms >= 0) { // Still check for valid duration
dataByProvider[row.provider_name].push(row.duration_ms);
}
});
const plotData = Object.keys(dataByProvider).sort().map(provider => ({
y: dataByProvider[provider], type: 'box', name: provider, boxpoints: 'Outliers', marker: { size: 4 }
}));
const layout = mergeLayout({
title: { text: `Latency Distribution by Provider ${titleContext}` },
yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
xaxis: { title: 'Provider', tickangle: -30 },
margin: { b: 120 }
});
Plotly.react(plotLatencyProviderDiv, plotData, layout, {responsive: true}); // Use react for updates
}
function createReliabilityByProviderPlot(rows, selectedModelId) {
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
const statusCountsByProvider = {};
const providersInSelection = new Set();
const statusCodesInSelection = new Set();
rows.forEach(row => {
const provider = row.provider_name;
const status = row.response_status_code ?? 'Unknown';
providersInSelection.add(provider);
statusCodesInSelection.add(status);
if (!statusCountsByProvider[provider]) statusCountsByProvider[provider] = {};
if (!statusCountsByProvider[provider][status]) statusCountsByProvider[provider][status] = 0;
statusCountsByProvider[provider][status]++;
});
const sortedProviders = Array.from(providersInSelection).sort();
const sortedStatusCodes = Array.from(statusCodesInSelection).sort((a, b) => {
if (a === 200) return -1; if (b === 200) return 1;
if (a === 'Unknown') return 1; if (b === 'Unknown') return -1;
return a - b;
});
const plotData = sortedStatusCodes.map(status => ({
x: sortedProviders,
y: sortedProviders.map(provider => statusCountsByProvider[provider]?.[status] || 0),
name: `Status ${status}`, type: 'bar',
hovertemplate: `Provider: %{x}<br>Status: ${status}<br>Count: %{y}<extra></extra>`
}));
const layout = mergeLayout({
title: { text: `Request Status Codes by Provider ${titleContext}` },
barmode: 'stack',
xaxis: { title: 'Provider', tickangle: -30 },
yaxis: { title: 'Number of Requests', autorange: true },
margin: { b: 120 }
});
Plotly.react(plotReliabilityProviderDiv, plotData, layout, {responsive: true});
}
function createLatencyByModelPlot(rows) { // Only shown for 'all'
const dataByModel = {};
// Filter for successful requests before processing
rows.filter(r => r.response_status_code === 200).forEach(row => {
const model = row.model_id;
if (!dataByModel[model]) dataByModel[model] = [];
if (row.duration_ms !== null && row.duration_ms >= 0) { // Still check for valid duration
dataByModel[model].push(row.duration_ms);
}
});
const plotData = Object.keys(dataByModel).sort().map(model => ({
y: dataByModel[model], type: 'box', name: model, boxpoints: 'Outliers', marker: { size: 4 }
}));
const layout = mergeLayout({
title: { text: 'Latency Distribution by Model (All Providers)' },
yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
xaxis: { title: 'Model ID', tickangle: -30 },
margin: { b: 180 }
});
Plotly.react(plotLatencyModelDiv, plotData, layout, {responsive: true});
}
function createErrorTypesByProviderPlot(rows, selectedModelId) {
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
const errorCountsByProvider = {};
const providersInSelection = new Set();
const errorCodesInSelection = new Set();
rows.forEach(row => {
if (row.response_status_code !== 200 && row.response_status_code !== null) {
const provider = row.provider_name;
const status = row.response_status_code;
providersInSelection.add(provider);
errorCodesInSelection.add(status);
if (!errorCountsByProvider[provider]) errorCountsByProvider[provider] = {};
if (!errorCountsByProvider[provider][status]) errorCountsByProvider[provider][status] = 0;
errorCountsByProvider[provider][status]++;
}
});
const sortedProviders = Array.from(providersInSelection).sort();
const sortedErrorCodes = Array.from(errorCodesInSelection).sort((a, b) => a - b);
const plotData = sortedErrorCodes.map(status => ({
x: sortedProviders,
y: sortedProviders.map(provider => errorCountsByProvider[provider]?.[status] || 0),
name: `Error ${status}`, type: 'bar',
hovertemplate: `Provider: %{x}<br>Error: ${status}<br>Count: %{y}<extra></extra>`
}));
const layout = mergeLayout({
title: { text: `Error Types by Provider (Non-200 Status) ${titleContext}` },
barmode: 'group',
xaxis: { title: 'Provider', tickangle: -30 },
yaxis: { title: 'Number of Errors', autorange: true },
margin: { b: 120 }
});
Plotly.react(plotErrorTypesProviderDiv, plotData, layout, {responsive: true});
}
function createLatencyHeatmap(rows) { // Only shown for 'all'
const latencyData = {};
const allProviders = new Set();
const allModels = new Set();
// Filter for successful requests before processing
rows.filter(r => r.response_status_code === 200).forEach(row => {
if (row.duration_ms !== null && row.duration_ms >= 0) { // Still check for valid duration
const provider = row.provider_name;
const model = row.model_id;
allProviders.add(provider);
allModels.add(model);
if (!latencyData[provider]) latencyData[provider] = {};
if (!latencyData[provider][model]) latencyData[provider][model] = { sum: 0, count: 0 };
latencyData[provider][model].sum += row.duration_ms;
latencyData[provider][model].count++;
}
});
const sortedProviders = Array.from(allProviders).sort();
const sortedModels = Array.from(allModels).sort();
const zValues = sortedModels.map(model => sortedProviders.map(provider => {
const data = latencyData[provider]?.[model];
return data?.count > 0 ? data.sum / data.count : null;
}));
const hoverText = sortedModels.map(model => sortedProviders.map(provider => {
const data = latencyData[provider]?.[model];
const avg = data?.count > 0 ? (data.sum / data.count).toFixed(0) : 'N/A';
const count = data?.count || 0;
return `Model: ${model}<br>Provider: ${provider}<br>Avg Latency: ${avg} ms<br>Requests: ${count}<extra></extra>`;
}));
const plotData = [{
z: zValues, x: sortedProviders, y: sortedModels, type: 'heatmap',
hoverongaps: false, colorscale: 'Viridis', reversescale: true,
colorbar: { title: 'Avg Latency (ms)', titleside: 'right', thickness: 15 },
xgap: 2, ygap: 2, hovertext: hoverText, // Use hovertext instead of hovertemplate
hoverinfo: 'text' // Tell plotly to use the hovertext
}];
const layout = mergeLayout({
title: { text: 'Average Latency (ms) - Model vs. Provider' },
xaxis: { title: '', side: 'top', tickangle: -30 },
yaxis: { title: '', autorange: 'reversed' },
margin: { l: 280, r: 50, b: 50, t: 120 }
});
Plotly.react(plotLatencyHeatmapDiv, plotData, layout, {responsive: true});
}
function createModelDetailTable(rows, selectedModelId) { // Only shown when filtered
document.getElementById('table-title').textContent = `Detailed Comparison for ${selectedModelId.split('/').pop()}`;
const tableHead = document.querySelector('#modelDetailTable thead');
const tableBody = document.querySelector('#modelDetailTable tbody');
// Clear only the body, keep the header for event listeners
tableBody.innerHTML = '';
const providerStats = {};
rows.forEach(row => {
const provider = row.provider_name;
if (!providerStats[provider]) providerStats[provider] = { total: 0, success: 0, errors: 0, latencies: [] };
providerStats[provider].total++;
if (row.response_status_code === 200) {
providerStats[provider].success++;
// Only add latency for successful requests with valid duration
if (row.duration_ms !== null && row.duration_ms >= 0) {
providerStats[provider].latencies.push(row.duration_ms);
}
} else if (row.response_status_code !== null) {
providerStats[provider].errors++;
}
// Note: We no longer add latency if the request was not successful (status != 200)
});
// Create Header (only if it doesn't exist)
if (tableHead.rows.length === 0) {
const headerRow = tableHead.insertRow();
const headers = [
{ text: 'Provider', key: 'provider', sortable: true },
{ text: 'Median Latency (ms)', key: 'medianLatency', sortable: true },
{ text: 'Success Rate (%)', key: 'successRate', sortable: true },
{ text: 'Error Count', key: 'errors', sortable: true },
{ text: 'Total Requests', key: 'total', sortable: true }
];
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header.text;
if (header.sortable) {
th.classList.add('sortable-header');
th.dataset.sortKey = header.key;
}
headerRow.appendChild(th);
});
}
const providerDataArray = [];
for (const provider in providerStats) {
const stats = providerStats[provider];
providerDataArray.push({
provider: provider,
medianLatency: calculateMedian(stats.latencies),
successRate: stats.total > 0 ? (stats.success / stats.total * 100) : 0,
errors: stats.errors, total: stats.total
});
}
// Sort the data based on current state
providerDataArray.sort((a, b) => {
let valA = a[modelDetailSortKey];
let valB = b[modelDetailSortKey];
// Handle null/undefined for numeric sorts (e.g., medianLatency)
const nullVal = modelDetailSortDirection === 'asc' ? Infinity : -Infinity;
if (typeof valA === 'number' || valA === null || valA === undefined) {
valA = (valA === null || valA === undefined) ? nullVal : valA;
}
if (typeof valB === 'number' || valB === null || valB === undefined) {
valB = (valB === null || valB === undefined) ? nullVal : valB;
}
// Comparison logic
if (typeof valA === 'string' && typeof valB === 'string') {
return modelDetailSortDirection === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
} else {
return modelDetailSortDirection === 'asc' ? valA - valB : valB - valA;
}
});
// Update visual indicators *after* sorting
updateModelDetailSortIndicators();
// Render rows
providerDataArray.forEach(data => {
const row = tableBody.insertRow();
row.insertCell().textContent = data.provider;
row.insertCell().textContent = data.medianLatency !== null ? data.medianLatency.toFixed(0) : 'N/A';
const successCell = row.insertCell();
successCell.textContent = data.successRate.toFixed(1);
if (data.successRate >= 95) successCell.className = 'success-rate-high';
else if (data.successRate >= 80) successCell.className = 'success-rate-medium';
else successCell.className = 'success-rate-low';
const errorCell = row.insertCell();
errorCell.textContent = data.errors;
if (data.errors > 0) errorCell.classList.add('error-count');
row.insertCell().textContent = data.total;
});
}
function updateModelDetailSortIndicators() {
document.querySelectorAll('#modelDetailTable th.sortable-header').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
if (th.dataset.sortKey === modelDetailSortKey) {
th.classList.add(modelDetailSortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
}
});
}
// --- Sorting Function for Request Inspector ---
function sortTable(key, direction) {
currentFilteredRows.sort((a, b) => {
let valA = a[key];
let valB = b[key];
// Handle specific cases
if (key === 'error_message') { // Sort by presence (Yes/No)
valA = valA ? 1 : 0; // Error present = 1, No error = 0
valB = valB ? 1 : 0;
} else if (key === 'model_id') { // Sort by shortened model name
valA = valA ? valA.split('/').pop() : '';
valB = valB ? valB.split('/').pop() : '';
}
// Handle null/undefined values (treat them as lowest or highest depending on direction)
const nullVal = direction === 'asc' ? Infinity : -Infinity;
valA = (valA === null || valA === undefined) ? nullVal : valA;
valB = (valB === null || valB === undefined) ? nullVal : valB;
// Comparison logic
if (typeof valA === 'string' && typeof valB === 'string') {
return direction === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
} else {
return direction === 'asc' ? valA - valB : valB - valA;
}
});
createRequestTable(currentFilteredRows); // Re-render the table body
updateSortIndicators(); // Update visual indicators
}
function updateSortIndicators() {
document.querySelectorAll('#requestTable th.sortable-header').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
if (th.dataset.sortKey === currentSortKey) {
th.classList.add(currentSortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
}
});
}
// --- Request Inspector Table ---
function createRequestTable(rows) {
const tableHead = document.querySelector('#requestTable thead');
const tableBody = document.querySelector('#requestTable tbody');
// Clear only the body, keep the header for event listeners
tableBody.innerHTML = '';
document.getElementById('request-table-title').textContent = `Request Inspector (${rows.length} requests shown)`;
// Create Header (only if it doesn't exist)
if (tableHead.rows.length === 0) {
const headerRow = tableHead.insertRow();
const headers = [
{ text: 'Provider', key: 'provider_name', sortable: true },
{ text: 'Model', key: 'model_id', sortable: true },
{ text: 'Status', key: 'response_status_code', sortable: true },
{ text: 'Duration (ms)', key: 'duration_ms', sortable: true },
{ text: 'Error', key: 'error_message', sortable: true }, // Sort by presence of error msg
{ text: 'Action', key: null, sortable: false }
];
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header.text;
if (header.sortable) {
th.classList.add('sortable-header');
th.dataset.sortKey = header.key;
}
headerRow.appendChild(th);
});
}
// Create Body Rows
rows.forEach((row, index) => { // Use index within the *filtered* array
const tableRow = tableBody.insertRow();
tableRow.insertCell().textContent = row.provider_name;
tableRow.insertCell().textContent = row.model_id.split('/').pop(); // Shorten model name
const statusCell = tableRow.insertCell();
statusCell.textContent = row.response_status_code ?? 'N/A';
statusCell.classList.add(row.response_status_code === 200 ? 'status-success' : 'status-error');
tableRow.insertCell().textContent = row.duration_ms ?? 'N/A';
tableRow.insertCell().textContent = row.error_message ? 'Yes' : 'No';
if (row.error_message) tableRow.cells[4].style.fontWeight = 'bold';
const actionCell = tableRow.insertCell();
const inspectButton = document.createElement('button');
inspectButton.textContent = 'Inspect';
inspectButton.className = 'inspect-button';
inspectButton.setAttribute('data-row-index', index); // Store the index *within the filtered list*
actionCell.appendChild(inspectButton);
});
}
// --- Inspector Modal Functions ---
function formatJsonString(jsonString) {
if (!jsonString) return 'N/A';
try {
const parsed = JSON.parse(jsonString);
return JSON.stringify(parsed, null, 2); // Pretty print
} catch (e) {
return jsonString; // Return original string if not valid JSON (e.g., HTML error)
}
}
window.showInspectorModal = function(rowData) { // Make it global for inline onclick
if (!rowData) return;
document.getElementById('modal-title').textContent = `Details for Request to ${rowData.provider_name}`;
document.getElementById('modal-provider').textContent = rowData.provider_name ?? 'N/A';
document.getElementById('modal-model').textContent = rowData.model_id ?? 'N/A';
document.getElementById('modal-status').textContent = rowData.response_status_code ?? 'N/A';
document.getElementById('modal-duration').textContent = rowData.duration_ms ?? 'N/A';
document.getElementById('modal-error').textContent = rowData.error_message ?? 'None';
document.getElementById('modal-timestamp').textContent = rowData.request_start_iso ? new Date(rowData.request_start_iso).toLocaleString() : 'N/A';
document.getElementById('modal-req-body').textContent = formatJsonString(rowData.request_body);
document.getElementById('modal-resp-body').textContent = formatJsonString(rowData.response_body_raw);
document.getElementById('modal-req-headers').textContent = formatJsonString(rowData.request_headers_sanitized);
document.getElementById('modal-resp-headers').textContent = formatJsonString(rowData.response_headers_sanitized);
inspectorModal.style.display = 'block';
}
window.hideInspectorModal = function() { // Make it global for inline onclick
inspectorModal.style.display = 'none';
}
});
</script>
</body>
</html>