|
<!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; |
|
color: var(--muted-text-color); |
|
font-weight: 400; |
|
} |
|
.kpi-card .value { |
|
font-size: 1.7rem; |
|
font-weight: 600; |
|
color: var(--text-color); |
|
word-wrap: break-word; |
|
} |
|
.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; |
|
flex-direction: column; |
|
justify-content: flex-start; |
|
align-items: center; |
|
overflow: hidden; |
|
} |
|
.plot-container .plotly, .table-container table { |
|
width: 100%; |
|
height: 100%; |
|
flex-grow: 1; |
|
} |
|
.plot-title { |
|
font-weight: 500; |
|
margin-bottom: 15px; |
|
color: var(--text-color); |
|
align-self: flex-start; |
|
width: 100%; |
|
} |
|
|
|
#loading, #error { |
|
grid-column: 1 / -1; |
|
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-wrapper { |
|
width: 100%; |
|
overflow-x: auto; |
|
flex-grow: 1; |
|
} |
|
table { |
|
border-collapse: collapse; |
|
font-size: 0.9rem; |
|
width: 100%; |
|
} |
|
th, td { |
|
padding: 10px 12px; |
|
text-align: left; |
|
border-bottom: 1px solid var(--border-color); |
|
white-space: nowrap; |
|
} |
|
th { |
|
background-color: var(--bg-color); |
|
font-weight: 500; |
|
position: sticky; |
|
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; |
|
} |
|
|
|
|
|
.modal-overlay { |
|
display: none; |
|
position: fixed; |
|
z-index: 1000; |
|
left: 0; |
|
top: 0; |
|
width: 100%; |
|
height: 100%; |
|
overflow: auto; |
|
background-color: rgba(0,0,0,0.5); |
|
} |
|
.modal-content { |
|
background-color: var(--card-bg-color); |
|
margin: 5% auto; |
|
padding: 25px; |
|
border: 1px solid var(--border-color); |
|
border-radius: 8px; |
|
width: 85%; |
|
max-width: 1000px; |
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
|
position: relative; |
|
max-height: 85vh; |
|
overflow-y: auto; |
|
} |
|
.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; |
|
word-wrap: break-word; |
|
max-height: 300px; |
|
overflow-y: auto; |
|
} |
|
.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> |
|
|
|
</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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
const baseUrl = "https://datasets-server.huggingface.co/rows?dataset=providers-metrics%2Fproviders-metrics" |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
const apiUrl = `${baseUrl}&config=default&split=train&offset=0&length=100`; |
|
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'); |
|
const requestsCountFooter = document.getElementById('requests-count-footer'); |
|
const modelSelector = document.getElementById('modelSelector'); |
|
const inspectorModal = document.getElementById('inspectorModal'); |
|
const requestTableBody = document.querySelector('#requestTable tbody'); |
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
const baseApiUrlDisplay = `${baseUrl}&config=default&split=train`; |
|
dataSourceUrlElement.href = baseApiUrlDisplay; |
|
|
|
let allRows = []; |
|
let currentFilteredRows = []; |
|
let uniqueModels = []; |
|
let currentSortKey = null; |
|
let currentSortDirection = 'asc'; |
|
let modelDetailSortKey = 'medianLatency'; |
|
let modelDetailSortDirection = 'asc'; |
|
const rowsPerFetch = 100; |
|
|
|
|
|
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); |
|
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' |
|
? allRows |
|
: allRows.filter(row => row.model_id === selectedModelId); |
|
|
|
console.log(`Updating dashboard for: ${selectedModelId}, Rows: ${currentFilteredRows.length}`); |
|
requestsCountFooter.textContent = currentFilteredRows.length; |
|
|
|
|
|
calculateAndDisplayKPIs(currentFilteredRows, selectedModelId); |
|
|
|
|
|
createLatencyByProviderPlot(currentFilteredRows, selectedModelId); |
|
createReliabilityByProviderPlot(currentFilteredRows, selectedModelId); |
|
createErrorTypesByProviderPlot(currentFilteredRows, selectedModelId); |
|
|
|
|
|
createRequestTable(currentFilteredRows); |
|
requestInspectorContainer.style.display = 'flex'; |
|
|
|
|
|
if (selectedModelId === 'all') { |
|
plotLatencyModelDiv.style.display = 'flex'; |
|
plotLatencyHeatmapDiv.style.display = 'flex'; |
|
modelDetailTableContainerDiv.style.display = 'none'; |
|
createLatencyByModelPlot(currentFilteredRows); |
|
createLatencyHeatmap(currentFilteredRows); |
|
} else { |
|
plotLatencyModelDiv.style.display = 'none'; |
|
plotLatencyHeatmapDiv.style.display = 'none'; |
|
modelDetailTableContainerDiv.style.display = 'flex'; |
|
createModelDetailTable(currentFilteredRows, selectedModelId); |
|
} |
|
} |
|
|
|
|
|
modelSelector.addEventListener('change', (event) => { |
|
updateDashboard(event.target.value); |
|
}); |
|
|
|
|
|
requestTableBody.addEventListener('click', (event) => { |
|
if (event.target.classList.contains('inspect-button')) { |
|
const rowIndex = parseInt(event.target.getAttribute('data-row-index'), 10); |
|
|
|
const originalRowData = currentFilteredRows[rowIndex]; |
|
if (originalRowData) { |
|
showInspectorModal(originalRowData); |
|
} else { |
|
console.error("Could not find row data for index:", rowIndex); |
|
} |
|
} |
|
}); |
|
|
|
|
|
inspectorModal.addEventListener('click', (event) => { |
|
if (event.target === inspectorModal) { |
|
hideInspectorModal(); |
|
} |
|
}); |
|
|
|
|
|
document.querySelector('#requestTable thead').addEventListener('click', (event) => { |
|
const header = event.target.closest('th.sortable-header'); |
|
if (!header) return; |
|
|
|
const sortKey = header.dataset.sortKey; |
|
|
|
|
|
if (sortKey === currentSortKey) { |
|
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc'; |
|
} else { |
|
currentSortKey = sortKey; |
|
currentSortDirection = 'asc'; |
|
} |
|
|
|
sortTable(currentSortKey, currentSortDirection); |
|
}); |
|
|
|
|
|
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'; |
|
} |
|
|
|
|
|
const selectedModel = modelSelector.value; |
|
const rowsForTable = selectedModel === 'all' ? allRows : allRows.filter(row => row.model_id === selectedModel); |
|
createModelDetailTable(rowsForTable, selectedModel); |
|
}); |
|
|
|
|
|
|
|
function fetchAllRows(offset = 0) { |
|
const apiUrl = `${baseUrl}&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) { |
|
|
|
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); |
|
|
|
console.log(`Fetched ${fetchedRows.length} rows. Total rows: ${allRows.length}`); |
|
|
|
|
|
if (fetchedRows.length < rowsPerFetch || fetchedRows.length === 0) { |
|
console.log("Finished fetching all rows."); |
|
lastUpdatedElement.textContent = new Date().toLocaleString(); |
|
loadingDiv.style.display = 'none'; |
|
kpiContainer.style.display = 'grid'; |
|
dashboardContainer.style.display = 'grid'; |
|
footer.style.display = 'block'; |
|
|
|
populateModelSelector(); |
|
updateDashboard('all'); |
|
} else { |
|
|
|
fetchAllRows(offset + rowsPerFetch); |
|
} |
|
}) |
|
.catch(error => { |
|
console.error('Error fetching data:', error); |
|
loadingDiv.style.display = 'none'; |
|
errorDiv.textContent = `Error fetching data at offset ${offset}: ${error.message}. Displaying partial data (${allRows.length} rows). Please check console.`; |
|
errorDiv.style.display = 'block'; |
|
|
|
|
|
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'); |
|
} |
|
}); |
|
} |
|
|
|
|
|
fetchAllRows(); |
|
|
|
|
|
|
|
function calculateAndDisplayKPIs(rows, selectedModelId) { |
|
const context = selectedModelId === 'all' ? 'Overall' : `(${selectedModelId.split('/').pop()})`; |
|
|
|
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 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; |
|
|
|
|
|
const latencyByProvider = {}; |
|
successfulRows.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; |
|
} |
|
|
|
|
|
|
|
|
|
function createLatencyByProviderPlot(rows, selectedModelId) { |
|
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`; |
|
const dataByProvider = {}; |
|
|
|
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) { |
|
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}); |
|
} |
|
|
|
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) { |
|
const dataByModel = {}; |
|
|
|
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) { |
|
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) { |
|
const latencyData = {}; |
|
const allProviders = new Set(); |
|
const allModels = new Set(); |
|
|
|
|
|
rows.filter(r => r.response_status_code === 200).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, hovertext: hoverText, |
|
hoverinfo: 'text' |
|
}]; |
|
|
|
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) { |
|
document.getElementById('table-title').textContent = `Detailed Comparison for ${selectedModelId.split('/').pop()}`; |
|
const tableHead = document.querySelector('#modelDetailTable thead'); |
|
const tableBody = document.querySelector('#modelDetailTable tbody'); |
|
|
|
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++; |
|
|
|
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++; |
|
} |
|
|
|
}); |
|
|
|
|
|
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 |
|
}); |
|
} |
|
|
|
|
|
providerDataArray.sort((a, b) => { |
|
let valA = a[modelDetailSortKey]; |
|
let valB = b[modelDetailSortKey]; |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
if (typeof valA === 'string' && typeof valB === 'string') { |
|
return modelDetailSortDirection === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA); |
|
} else { |
|
return modelDetailSortDirection === 'asc' ? valA - valB : valB - valA; |
|
} |
|
}); |
|
|
|
|
|
updateModelDetailSortIndicators(); |
|
|
|
|
|
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'); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function sortTable(key, direction) { |
|
currentFilteredRows.sort((a, b) => { |
|
let valA = a[key]; |
|
let valB = b[key]; |
|
|
|
|
|
if (key === 'error_message') { |
|
valA = valA ? 1 : 0; |
|
valB = valB ? 1 : 0; |
|
} else if (key === 'model_id') { |
|
valA = valA ? valA.split('/').pop() : ''; |
|
valB = valB ? valB.split('/').pop() : ''; |
|
} |
|
|
|
|
|
const nullVal = direction === 'asc' ? Infinity : -Infinity; |
|
valA = (valA === null || valA === undefined) ? nullVal : valA; |
|
valB = (valB === null || valB === undefined) ? nullVal : valB; |
|
|
|
|
|
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); |
|
updateSortIndicators(); |
|
} |
|
|
|
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'); |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
function createRequestTable(rows) { |
|
const tableHead = document.querySelector('#requestTable thead'); |
|
const tableBody = document.querySelector('#requestTable tbody'); |
|
|
|
tableBody.innerHTML = ''; |
|
|
|
document.getElementById('request-table-title').textContent = `Request Inspector (${rows.length} requests shown)`; |
|
|
|
|
|
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 }, |
|
{ 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); |
|
}); |
|
} |
|
|
|
|
|
rows.forEach((row, index) => { |
|
const tableRow = tableBody.insertRow(); |
|
tableRow.insertCell().textContent = row.provider_name; |
|
tableRow.insertCell().textContent = row.model_id.split('/').pop(); |
|
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); |
|
actionCell.appendChild(inspectButton); |
|
}); |
|
} |
|
|
|
|
|
function formatJsonString(jsonString) { |
|
if (!jsonString) return 'N/A'; |
|
try { |
|
const parsed = JSON.parse(jsonString); |
|
return JSON.stringify(parsed, null, 2); |
|
} catch (e) { |
|
return jsonString; |
|
} |
|
} |
|
|
|
window.showInspectorModal = function(rowData) { |
|
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() { |
|
inspectorModal.style.display = 'none'; |
|
} |
|
|
|
}); |
|
</script> |
|
|
|
</body> |
|
</html> |
|
|