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: #f8f9fa; | |
--card-bg-color: #ffffff; | |
--text-color: #212529; | |
--muted-text-color: #6c757d; | |
--border-color: #dee2e6; | |
--shadow-color: rgba(0, 0, 0, 0.05); | |
--primary-color: #0d6efd; | |
--success-color: #198754; | |
--warning-color: #ffc107; | |
--danger-color: #dc3545; | |
--plot-colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']; /* Plotly default */ | |
} | |
body { | |
font-family: -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.5; | |
} | |
.container { | |
max-width: 1700px; /* Wider container */ | |
margin: 20px auto; | |
padding: 0 20px; | |
} | |
h1 { | |
text-align: center; | |
color: var(--text-color); | |
margin-bottom: 15px; | |
font-weight: 500; | |
} | |
.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 select { | |
padding: 8px 12px; | |
border: 1px solid var(--border-color); | |
border-radius: 6px; | |
background-color: var(--card-bg-color); | |
min-width: 300px; | |
font-size: 1rem; | |
} | |
.kpi-container { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
gap: 20px; | |
margin-bottom: 30px; | |
} | |
.kpi-card { | |
background-color: var(--card-bg-color); | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 4px 8px var(--shadow-color); | |
text-align: center; | |
border: 1px solid var(--border-color); | |
} | |
.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(450px, 1fr)); /* Responsive grid */ | |
gap: 25px; /* Space between plots */ | |
margin-bottom: 30px; | |
} | |
.plot-container, .table-container { | |
background-color: var(--card-bg-color); | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 4px 8px var(--shadow-color); | |
border: 1px solid var(--border-color); | |
min-height: 450px; /* Ensure plots have some height */ | |
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: #f1f1f1; | |
} | |
.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: var(--primary-color); | |
color: white; | |
border: none; | |
border-radius: 4px; | |
} | |
.inspect-button:hover { | |
opacity: 0.85; | |
} | |
/* 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">Loading data... Please wait.</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> {/* Will be hidden when filtered */} | |
<div id="plotErrorTypesProvider" class="plot-container"></div> | |
<div id="modelDetailTableContainer" class="table-container" style="display: none;"> {/* Initially hidden */} | |
<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> {/* Will be hidden when filtered */} | |
</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 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'); | |
dataSourceUrlElement.href = apiUrl; // Set link href | |
let allRows = []; // Store all fetched rows globally | |
let currentFilteredRows = []; // Store currently filtered rows | |
let uniqueModels = []; | |
// Plotly layout defaults (same as before) | |
const baseLayout = { | |
margin: { l: 60, r: 30, b: 100, t: 60, pad: 4 }, | |
legend: { bgcolor: 'rgba(255,255,255,0.5)', bordercolor: '#ccc', 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: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', | |
color: '#212529' | |
}, | |
title: { | |
font: { size: 16, weight: '500' }, | |
x: 0.05, xanchor: 'left' | |
}, | |
xaxis: { gridcolor: '#e9ecef', linecolor: '#adb5bd', automargin: true, tickfont: { size: 10 } }, | |
yaxis: { gridcolor: '#e9ecef', linecolor: '#adb5bd', 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(); | |
} | |
}); | |
// --- Initial Fetch --- | |
fetch(apiUrl) | |
.then(response => { | |
if (!response.ok) throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`); | |
return response.json(); | |
}) | |
.then(data => { | |
allRows = data.rows.map((item, index) => ({ ...item.row, originalIndex: index })); // Store globally, add original index if needed later, though direct filtering is fine here | |
console.log(`Fetched ${allRows.length} rows.`); | |
lastUpdatedElement.textContent = new Date().toLocaleString(); | |
populateModelSelector(); // Populate dropdown | |
loadingDiv.style.display = 'none'; | |
kpiContainer.style.display = 'grid'; | |
dashboardContainer.style.display = 'grid'; | |
footer.style.display = 'block'; | |
updateDashboard('all'); // Initial load with all models | |
}) | |
.catch(error => { | |
console.error('Error fetching or processing data:', error); | |
loadingDiv.style.display = 'none'; | |
errorDiv.textContent = `Error loading data: ${error.message}. Please check the console. Is the dataset server reachable?`; | |
errorDiv.style.display = 'block'; | |
}); | |
// --- KPI Calculation Function (same as before) --- | |
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); | |
const avgLatency = validLatencies.length > 0 | |
? (validLatencies.reduce((a, b) => a + b, 0) / validLatencies.length).toFixed(0) | |
: 0; | |
// Calculate median latency per provider for the filtered data | |
const latencyByProvider = {}; | |
rows.forEach(row => { | |
if (row.duration_ms !== null && row.duration_ms >= 0) { | |
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 = {}; | |
rows.forEach(row => { | |
if (!dataByProvider[row.provider_name]) dataByProvider[row.provider_name] = []; | |
if (row.duration_ms !== null && row.duration_ms >= 0) { | |
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 = {}; | |
rows.forEach(row => { | |
const model = row.model_id; | |
if (!dataByModel[model]) dataByModel[model] = []; | |
if (row.duration_ms !== null && row.duration_ms >= 0) { | |
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(); | |
rows.forEach(row => { | |
if (row.duration_ms !== null && row.duration_ms >= 0) { | |
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, hovertemplate: 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'); | |
tableHead.innerHTML = ''; tableBody.innerHTML = ''; // Clear | |
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++; | |
else if (row.response_status_code !== null) providerStats[provider].errors++; | |
if (row.duration_ms !== null && row.duration_ms >= 0) providerStats[provider].latencies.push(row.duration_ms); | |
}); | |
const headerRow = tableHead.insertRow(); | |
const headers = ['Provider', 'Median Latency (ms)', 'Success Rate (%)', 'Error Count', 'Total Requests']; | |
headers.forEach(text => { headerRow.insertCell().textContent = text; }); | |
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 | |
}); | |
} | |
providerDataArray.sort((a, b) => (a.medianLatency ?? Infinity) - (b.medianLatency ?? Infinity)); // Sort by latency | |
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; | |
}); | |
} | |
// --- Request Inspector Table --- | |
function createRequestTable(rows) { | |
const tableHead = document.querySelector('#requestTable thead'); | |
const tableBody = document.querySelector('#requestTable tbody'); | |
tableHead.innerHTML = ''; tableBody.innerHTML = ''; // Clear | |
document.getElementById('request-table-title').textContent = `Request Inspector (${rows.length} requests shown)`; | |
// Create Header | |
const headerRow = tableHead.insertRow(); | |
const headers = ['Provider', 'Model', 'Status', 'Duration (ms)', 'Error', 'Action']; | |
headers.forEach(text => { headerRow.insertCell().textContent = text; }); | |
// 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> |