Spaces:
Running
Running
<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> | |