Spaces:
Running
Running
Update index.html
Browse files- index.html +284 -77
index.html
CHANGED
@@ -94,6 +94,7 @@
|
|
94 |
display: grid;
|
95 |
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); /* Responsive grid */
|
96 |
gap: 25px; /* Space between plots */
|
|
|
97 |
}
|
98 |
.plot-container, .table-container {
|
99 |
background-color: var(--card-bg-color);
|
@@ -104,8 +105,9 @@
|
|
104 |
min-height: 450px; /* Ensure plots have some height */
|
105 |
display: flex; /* For centering loading/error inside */
|
106 |
flex-direction: column; /* Allow title and content stacking */
|
107 |
-
justify-content:
|
108 |
align-items: center;
|
|
|
109 |
}
|
110 |
.plot-container .plotly, .table-container table {
|
111 |
width: 100%;
|
@@ -117,6 +119,7 @@
|
|
117 |
margin-bottom: 15px;
|
118 |
color: var(--text-color);
|
119 |
align-self: flex-start; /* Align title left */
|
|
|
120 |
}
|
121 |
|
122 |
#loading, #error {
|
@@ -135,41 +138,128 @@
|
|
135 |
}
|
136 |
|
137 |
/* Table Styles */
|
138 |
-
.table-
|
139 |
-
|
140 |
-
|
|
|
141 |
}
|
142 |
table {
|
143 |
border-collapse: collapse;
|
144 |
font-size: 0.9rem;
|
|
|
145 |
}
|
146 |
th, td {
|
147 |
padding: 10px 12px;
|
148 |
text-align: left;
|
149 |
border-bottom: 1px solid var(--border-color);
|
|
|
150 |
}
|
151 |
th {
|
152 |
background-color: var(--bg-color);
|
153 |
font-weight: 500;
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
th:hover {
|
158 |
-
background-color: #e9ecef;
|
159 |
-
}
|
160 |
-
td {
|
161 |
-
white-space: nowrap; /* Prevent wrapping in cells */
|
162 |
}
|
163 |
tbody tr:hover {
|
164 |
background-color: #f1f1f1;
|
165 |
}
|
166 |
-
.error-count {
|
167 |
color: var(--danger-color);
|
168 |
font-weight: 500;
|
169 |
}
|
|
|
|
|
|
|
|
|
170 |
.success-rate-high { color: var(--success-color); font-weight: 500; }
|
171 |
.success-rate-medium { color: var(--warning-color); font-weight: 500; }
|
172 |
.success-rate-low { color: var(--danger-color); font-weight: 500; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
|
174 |
|
175 |
footer {
|
@@ -233,31 +323,79 @@
|
|
233 |
<div id="plotErrorTypesProvider" class="plot-container"></div>
|
234 |
<div id="modelDetailTableContainer" class="table-container" style="display: none;"> {/* Initially hidden */}
|
235 |
<h3 class="plot-title" id="table-title">Detailed Comparison</h3>
|
236 |
-
<
|
237 |
-
<
|
238 |
-
|
239 |
-
|
|
|
|
|
240 |
</div>
|
241 |
<div id="plotLatencyHeatmap" class="plot-container"></div> {/* Will be hidden when filtered */}
|
242 |
</div>
|
243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
244 |
<footer id="footer" style="display: none;">
|
245 |
Data fetched from: <a id="data-source-url" href="#" target="_blank">Hugging Face Datasets</a><br>
|
246 |
-
Last updated: <span id="last-updated"></span>
|
247 |
</footer>
|
248 |
</div>
|
249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
250 |
<script>
|
251 |
document.addEventListener('DOMContentLoaded', function() {
|
252 |
-
const apiUrl = "https://datasets-server.huggingface.co/rows?dataset=victor%2Fproviders-metrics&config=default&split=train&offset=0&length=100"; // Fetch
|
253 |
const loadingDiv = document.getElementById('loading');
|
254 |
const errorDiv = document.getElementById('error');
|
255 |
const kpiContainer = document.querySelector('.kpi-container');
|
256 |
const dashboardContainer = document.querySelector('.dashboard-container');
|
|
|
257 |
const footer = document.getElementById('footer');
|
258 |
const dataSourceUrlElement = document.getElementById('data-source-url');
|
259 |
const lastUpdatedElement = document.getElementById('last-updated');
|
|
|
260 |
const modelSelector = document.getElementById('modelSelector');
|
|
|
|
|
|
|
261 |
|
262 |
// Plot containers
|
263 |
const plotLatencyProviderDiv = document.getElementById('plotLatencyProvider');
|
@@ -271,10 +409,11 @@
|
|
271 |
dataSourceUrlElement.href = apiUrl; // Set link href
|
272 |
|
273 |
let allRows = []; // Store all fetched rows globally
|
|
|
274 |
let uniqueModels = [];
|
275 |
|
276 |
-
// Plotly layout defaults
|
277 |
-
|
278 |
margin: { l: 60, r: 30, b: 100, t: 60, pad: 4 },
|
279 |
legend: { bgcolor: 'rgba(255,255,255,0.5)', bordercolor: '#ccc', borderwidth: 1 },
|
280 |
colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
|
@@ -306,7 +445,8 @@
|
|
306 |
|
307 |
function calculateMedian(arr) {
|
308 |
if (!arr || arr.length === 0) return null;
|
309 |
-
const sortedArr = [...arr].sort((a, b) => a - b);
|
|
|
310 |
const mid = Math.floor(sortedArr.length / 2);
|
311 |
return sortedArr.length % 2 !== 0 ? sortedArr[mid] : (sortedArr[mid - 1] + sortedArr[mid]) / 2;
|
312 |
}
|
@@ -322,40 +462,67 @@
|
|
322 |
}
|
323 |
|
324 |
function updateDashboard(selectedModelId) {
|
325 |
-
|
326 |
? allRows
|
327 |
: allRows.filter(row => row.model_id === selectedModelId);
|
328 |
|
329 |
-
console.log(`Updating dashboard for: ${selectedModelId}, Rows: ${
|
|
|
330 |
|
331 |
// Update KPIs
|
332 |
-
calculateAndDisplayKPIs(
|
333 |
|
334 |
// Update Plots
|
335 |
-
createLatencyByProviderPlot(
|
336 |
-
createReliabilityByProviderPlot(
|
337 |
-
createErrorTypesByProviderPlot(
|
|
|
|
|
|
|
|
|
338 |
|
339 |
// Show/Hide plots based on selection
|
340 |
if (selectedModelId === 'all') {
|
341 |
plotLatencyModelDiv.style.display = 'flex';
|
342 |
plotLatencyHeatmapDiv.style.display = 'flex';
|
343 |
modelDetailTableContainerDiv.style.display = 'none';
|
344 |
-
createLatencyByModelPlot(
|
345 |
-
createLatencyHeatmap(
|
346 |
} else {
|
347 |
plotLatencyModelDiv.style.display = 'none';
|
348 |
plotLatencyHeatmapDiv.style.display = 'none';
|
349 |
modelDetailTableContainerDiv.style.display = 'flex'; // Show table
|
350 |
-
createModelDetailTable(
|
351 |
}
|
352 |
}
|
353 |
|
354 |
-
// --- Event
|
355 |
modelSelector.addEventListener('change', (event) => {
|
356 |
updateDashboard(event.target.value);
|
357 |
});
|
358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
359 |
// --- Initial Fetch ---
|
360 |
fetch(apiUrl)
|
361 |
.then(response => {
|
@@ -363,7 +530,7 @@
|
|
363 |
return response.json();
|
364 |
})
|
365 |
.then(data => {
|
366 |
-
allRows = data.rows.map(item => item.row); // Store globally
|
367 |
console.log(`Fetched ${allRows.length} rows.`);
|
368 |
lastUpdatedElement.textContent = new Date().toLocaleString();
|
369 |
|
@@ -384,7 +551,7 @@
|
|
384 |
errorDiv.style.display = 'block';
|
385 |
});
|
386 |
|
387 |
-
// --- KPI Calculation Function ---
|
388 |
function calculateAndDisplayKPIs(rows, selectedModelId) {
|
389 |
const context = selectedModelId === 'all' ? 'Overall' : `(${selectedModelId.split('/').pop()})`; // Shorten model name for title
|
390 |
|
@@ -435,8 +602,8 @@
|
|
435 |
|
436 |
|
437 |
// --- Plotting Functions (Modified to accept data and context) ---
|
438 |
-
|
439 |
-
|
440 |
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
|
441 |
const dataByProvider = {};
|
442 |
rows.forEach(row => {
|
@@ -608,84 +775,124 @@
|
|
608 |
Plotly.react(plotLatencyHeatmapDiv, plotData, layout, {responsive: true});
|
609 |
}
|
610 |
|
611 |
-
function createModelDetailTable(rows, selectedModelId) {
|
612 |
document.getElementById('table-title').textContent = `Detailed Comparison for ${selectedModelId.split('/').pop()}`;
|
613 |
const tableHead = document.querySelector('#modelDetailTable thead');
|
614 |
const tableBody = document.querySelector('#modelDetailTable tbody');
|
615 |
-
tableHead.innerHTML = ''; // Clear
|
616 |
-
tableBody.innerHTML = ''; // Clear previous body
|
617 |
|
618 |
-
const providerStats = {};
|
619 |
|
620 |
rows.forEach(row => {
|
621 |
const provider = row.provider_name;
|
622 |
-
if (!providerStats[provider]) {
|
623 |
-
providerStats[provider] = { total: 0, success: 0, errors: 0, latencies: [] };
|
624 |
-
}
|
625 |
providerStats[provider].total++;
|
626 |
-
if (row.response_status_code === 200)
|
627 |
-
|
628 |
-
|
629 |
-
providerStats[provider].errors++;
|
630 |
-
}
|
631 |
-
if (row.duration_ms !== null && row.duration_ms >= 0) {
|
632 |
-
providerStats[provider].latencies.push(row.duration_ms);
|
633 |
-
}
|
634 |
});
|
635 |
|
636 |
-
// Create Header
|
637 |
const headerRow = tableHead.insertRow();
|
638 |
const headers = ['Provider', 'Median Latency (ms)', 'Success Rate (%)', 'Error Count', 'Total Requests'];
|
639 |
-
headers.forEach(text => {
|
640 |
-
const th = document.createElement('th');
|
641 |
-
th.textContent = text;
|
642 |
-
headerRow.appendChild(th);
|
643 |
-
});
|
644 |
|
645 |
-
// Create Body Rows
|
646 |
const providerDataArray = [];
|
647 |
for (const provider in providerStats) {
|
648 |
const stats = providerStats[provider];
|
649 |
-
const medianLatency = calculateMedian(stats.latencies);
|
650 |
-
const successRate = stats.total > 0 ? (stats.success / stats.total * 100) : 0;
|
651 |
providerDataArray.push({
|
652 |
provider: provider,
|
653 |
-
medianLatency:
|
654 |
-
successRate:
|
655 |
-
errors: stats.errors,
|
656 |
-
total: stats.total
|
657 |
});
|
658 |
}
|
659 |
-
|
660 |
-
// Sort initially by median latency (ascending)
|
661 |
-
providerDataArray.sort((a, b) => {
|
662 |
-
if (a.medianLatency === null) return 1; // Nulls last
|
663 |
-
if (b.medianLatency === null) return -1;
|
664 |
-
return a.medianLatency - b.medianLatency;
|
665 |
-
});
|
666 |
-
|
667 |
|
668 |
providerDataArray.forEach(data => {
|
669 |
const row = tableBody.insertRow();
|
670 |
row.insertCell().textContent = data.provider;
|
671 |
row.insertCell().textContent = data.medianLatency !== null ? data.medianLatency.toFixed(0) : 'N/A';
|
672 |
-
|
673 |
const successCell = row.insertCell();
|
674 |
successCell.textContent = data.successRate.toFixed(1);
|
675 |
-
// Add color coding for success rate
|
676 |
if (data.successRate >= 95) successCell.className = 'success-rate-high';
|
677 |
else if (data.successRate >= 80) successCell.className = 'success-rate-medium';
|
678 |
else successCell.className = 'success-rate-low';
|
679 |
-
|
680 |
-
|
681 |
const errorCell = row.insertCell();
|
682 |
errorCell.textContent = data.errors;
|
683 |
if (data.errors > 0) errorCell.classList.add('error-count');
|
684 |
-
|
685 |
row.insertCell().textContent = data.total;
|
686 |
});
|
687 |
}
|
688 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
689 |
});
|
690 |
</script>
|
691 |
|
|
|
94 |
display: grid;
|
95 |
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); /* Responsive grid */
|
96 |
gap: 25px; /* Space between plots */
|
97 |
+
margin-bottom: 30px;
|
98 |
}
|
99 |
.plot-container, .table-container {
|
100 |
background-color: var(--card-bg-color);
|
|
|
105 |
min-height: 450px; /* Ensure plots have some height */
|
106 |
display: flex; /* For centering loading/error inside */
|
107 |
flex-direction: column; /* Allow title and content stacking */
|
108 |
+
justify-content: flex-start; /* Align content top */
|
109 |
align-items: center;
|
110 |
+
overflow: hidden; /* Prevent content spillover */
|
111 |
}
|
112 |
.plot-container .plotly, .table-container table {
|
113 |
width: 100%;
|
|
|
119 |
margin-bottom: 15px;
|
120 |
color: var(--text-color);
|
121 |
align-self: flex-start; /* Align title left */
|
122 |
+
width: 100%; /* Ensure title takes full width */
|
123 |
}
|
124 |
|
125 |
#loading, #error {
|
|
|
138 |
}
|
139 |
|
140 |
/* Table Styles */
|
141 |
+
.table-wrapper { /* Added wrapper for scrolling */
|
142 |
+
width: 100%;
|
143 |
+
overflow-x: auto;
|
144 |
+
flex-grow: 1;
|
145 |
}
|
146 |
table {
|
147 |
border-collapse: collapse;
|
148 |
font-size: 0.9rem;
|
149 |
+
width: 100%; /* Make table take full width of wrapper */
|
150 |
}
|
151 |
th, td {
|
152 |
padding: 10px 12px;
|
153 |
text-align: left;
|
154 |
border-bottom: 1px solid var(--border-color);
|
155 |
+
white-space: nowrap; /* Prevent wrapping */
|
156 |
}
|
157 |
th {
|
158 |
background-color: var(--bg-color);
|
159 |
font-weight: 500;
|
160 |
+
position: sticky; /* Sticky header */
|
161 |
+
top: 0;
|
162 |
+
z-index: 1;
|
|
|
|
|
|
|
|
|
|
|
163 |
}
|
164 |
tbody tr:hover {
|
165 |
background-color: #f1f1f1;
|
166 |
}
|
167 |
+
.error-count, .status-error {
|
168 |
color: var(--danger-color);
|
169 |
font-weight: 500;
|
170 |
}
|
171 |
+
.status-success {
|
172 |
+
color: var(--success-color);
|
173 |
+
font-weight: 500;
|
174 |
+
}
|
175 |
.success-rate-high { color: var(--success-color); font-weight: 500; }
|
176 |
.success-rate-medium { color: var(--warning-color); font-weight: 500; }
|
177 |
.success-rate-low { color: var(--danger-color); font-weight: 500; }
|
178 |
+
.inspect-button {
|
179 |
+
padding: 4px 8px;
|
180 |
+
font-size: 0.8rem;
|
181 |
+
cursor: pointer;
|
182 |
+
background-color: var(--primary-color);
|
183 |
+
color: white;
|
184 |
+
border: none;
|
185 |
+
border-radius: 4px;
|
186 |
+
}
|
187 |
+
.inspect-button:hover {
|
188 |
+
opacity: 0.85;
|
189 |
+
}
|
190 |
+
|
191 |
+
/* Inspector Modal Styles */
|
192 |
+
.modal-overlay {
|
193 |
+
display: none; /* Hidden by default */
|
194 |
+
position: fixed;
|
195 |
+
z-index: 1000;
|
196 |
+
left: 0;
|
197 |
+
top: 0;
|
198 |
+
width: 100%;
|
199 |
+
height: 100%;
|
200 |
+
overflow: auto; /* Enable scroll if needed */
|
201 |
+
background-color: rgba(0,0,0,0.5); /* Black w/ opacity */
|
202 |
+
}
|
203 |
+
.modal-content {
|
204 |
+
background-color: var(--card-bg-color);
|
205 |
+
margin: 5% auto; /* 5% from the top and centered */
|
206 |
+
padding: 25px;
|
207 |
+
border: 1px solid var(--border-color);
|
208 |
+
border-radius: 8px;
|
209 |
+
width: 85%; /* Could be more or less, depending on screen size */
|
210 |
+
max-width: 1000px;
|
211 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
212 |
+
position: relative;
|
213 |
+
max-height: 85vh; /* Limit height */
|
214 |
+
overflow-y: auto; /* Add scroll to modal content */
|
215 |
+
}
|
216 |
+
.modal-close {
|
217 |
+
color: #aaa;
|
218 |
+
position: absolute;
|
219 |
+
top: 10px;
|
220 |
+
right: 20px;
|
221 |
+
font-size: 28px;
|
222 |
+
font-weight: bold;
|
223 |
+
cursor: pointer;
|
224 |
+
}
|
225 |
+
.modal-close:hover,
|
226 |
+
.modal-close:focus {
|
227 |
+
color: var(--text-color);
|
228 |
+
text-decoration: none;
|
229 |
+
}
|
230 |
+
.modal-content h2 {
|
231 |
+
margin-top: 0;
|
232 |
+
font-weight: 500;
|
233 |
+
border-bottom: 1px solid var(--border-color);
|
234 |
+
padding-bottom: 10px;
|
235 |
+
margin-bottom: 20px;
|
236 |
+
}
|
237 |
+
.modal-content h3 {
|
238 |
+
font-size: 1.1rem;
|
239 |
+
font-weight: 500;
|
240 |
+
margin-top: 20px;
|
241 |
+
margin-bottom: 8px;
|
242 |
+
color: var(--primary-color);
|
243 |
+
}
|
244 |
+
.modal-content pre {
|
245 |
+
background-color: var(--bg-color);
|
246 |
+
border: 1px solid var(--border-color);
|
247 |
+
border-radius: 4px;
|
248 |
+
padding: 15px;
|
249 |
+
font-size: 0.85rem;
|
250 |
+
white-space: pre-wrap; /* Allow wrapping */
|
251 |
+
word-wrap: break-word;
|
252 |
+
max-height: 300px; /* Limit height of code blocks */
|
253 |
+
overflow-y: auto; /* Add scroll to code blocks */
|
254 |
+
}
|
255 |
+
.modal-content p {
|
256 |
+
margin-bottom: 5px;
|
257 |
+
}
|
258 |
+
.modal-content strong {
|
259 |
+
color: var(--muted-text-color);
|
260 |
+
min-width: 120px;
|
261 |
+
display: inline-block;
|
262 |
+
}
|
263 |
|
264 |
|
265 |
footer {
|
|
|
323 |
<div id="plotErrorTypesProvider" class="plot-container"></div>
|
324 |
<div id="modelDetailTableContainer" class="table-container" style="display: none;"> {/* Initially hidden */}
|
325 |
<h3 class="plot-title" id="table-title">Detailed Comparison</h3>
|
326 |
+
<div class="table-wrapper">
|
327 |
+
<table id="modelDetailTable">
|
328 |
+
<thead></thead>
|
329 |
+
<tbody></tbody>
|
330 |
+
</table>
|
331 |
+
</div>
|
332 |
</div>
|
333 |
<div id="plotLatencyHeatmap" class="plot-container"></div> {/* Will be hidden when filtered */}
|
334 |
</div>
|
335 |
|
336 |
+
<!-- Request Inspector Table -->
|
337 |
+
<div id="requestInspectorContainer" class="table-container" style="display: none; min-height: 300px;">
|
338 |
+
<h3 class="plot-title" id="request-table-title">Request Inspector</h3>
|
339 |
+
<div class="table-wrapper">
|
340 |
+
<table id="requestTable">
|
341 |
+
<thead></thead>
|
342 |
+
<tbody></tbody>
|
343 |
+
</table>
|
344 |
+
</div>
|
345 |
+
</div>
|
346 |
+
|
347 |
+
|
348 |
<footer id="footer" style="display: none;">
|
349 |
Data fetched from: <a id="data-source-url" href="#" target="_blank">Hugging Face Datasets</a><br>
|
350 |
+
Showing <span id="requests-count-footer">--</span> requests. Last updated: <span id="last-updated"></span>
|
351 |
</footer>
|
352 |
</div>
|
353 |
|
354 |
+
<!-- Inspector Modal -->
|
355 |
+
<div id="inspectorModal" class="modal-overlay">
|
356 |
+
<div class="modal-content">
|
357 |
+
<span class="modal-close" onclick="hideInspectorModal()">×</span>
|
358 |
+
<h2 id="modal-title">Request Details</h2>
|
359 |
+
|
360 |
+
<h3>Summary</h3>
|
361 |
+
<p><strong>Provider:</strong> <span id="modal-provider"></span></p>
|
362 |
+
<p><strong>Model:</strong> <span id="modal-model"></span></p>
|
363 |
+
<p><strong>Status:</strong> <span id="modal-status"></span></p>
|
364 |
+
<p><strong>Duration:</strong> <span id="modal-duration"></span> ms</p>
|
365 |
+
<p><strong>Error:</strong> <span id="modal-error"></span></p>
|
366 |
+
<p><strong>Timestamp (Start):</strong> <span id="modal-timestamp"></span></p>
|
367 |
+
|
368 |
+
<h3>Request Body</h3>
|
369 |
+
<pre><code id="modal-req-body"></code></pre>
|
370 |
+
|
371 |
+
<h3>Response Body</h3>
|
372 |
+
<pre><code id="modal-resp-body"></code></pre>
|
373 |
+
|
374 |
+
<h3>Request Headers (Sanitized)</h3>
|
375 |
+
<pre><code id="modal-req-headers"></code></pre>
|
376 |
+
|
377 |
+
<h3>Response Headers (Sanitized)</h3>
|
378 |
+
<pre><code id="modal-resp-headers"></code></pre>
|
379 |
+
</div>
|
380 |
+
</div>
|
381 |
+
|
382 |
+
|
383 |
<script>
|
384 |
document.addEventListener('DOMContentLoaded', function() {
|
385 |
+
const apiUrl = "https://datasets-server.huggingface.co/rows?dataset=victor%2Fproviders-metrics&config=default&split=train&offset=0&length=100"; // Fetch 100 rows
|
386 |
const loadingDiv = document.getElementById('loading');
|
387 |
const errorDiv = document.getElementById('error');
|
388 |
const kpiContainer = document.querySelector('.kpi-container');
|
389 |
const dashboardContainer = document.querySelector('.dashboard-container');
|
390 |
+
const requestInspectorContainer = document.getElementById('requestInspectorContainer');
|
391 |
const footer = document.getElementById('footer');
|
392 |
const dataSourceUrlElement = document.getElementById('data-source-url');
|
393 |
const lastUpdatedElement = document.getElementById('last-updated');
|
394 |
+
const requestsCountFooter = document.getElementById('requests-count-footer');
|
395 |
const modelSelector = document.getElementById('modelSelector');
|
396 |
+
const inspectorModal = document.getElementById('inspectorModal');
|
397 |
+
const requestTableBody = document.querySelector('#requestTable tbody');
|
398 |
+
|
399 |
|
400 |
// Plot containers
|
401 |
const plotLatencyProviderDiv = document.getElementById('plotLatencyProvider');
|
|
|
409 |
dataSourceUrlElement.href = apiUrl; // Set link href
|
410 |
|
411 |
let allRows = []; // Store all fetched rows globally
|
412 |
+
let currentFilteredRows = []; // Store currently filtered rows
|
413 |
let uniqueModels = [];
|
414 |
|
415 |
+
// Plotly layout defaults (same as before)
|
416 |
+
const baseLayout = {
|
417 |
margin: { l: 60, r: 30, b: 100, t: 60, pad: 4 },
|
418 |
legend: { bgcolor: 'rgba(255,255,255,0.5)', bordercolor: '#ccc', borderwidth: 1 },
|
419 |
colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
|
|
|
445 |
|
446 |
function calculateMedian(arr) {
|
447 |
if (!arr || arr.length === 0) return null;
|
448 |
+
const sortedArr = [...arr].filter(n => n !== null && n >= 0).sort((a, b) => a - b); // Filter nulls/negatives before sort
|
449 |
+
if (sortedArr.length === 0) return null;
|
450 |
const mid = Math.floor(sortedArr.length / 2);
|
451 |
return sortedArr.length % 2 !== 0 ? sortedArr[mid] : (sortedArr[mid - 1] + sortedArr[mid]) / 2;
|
452 |
}
|
|
|
462 |
}
|
463 |
|
464 |
function updateDashboard(selectedModelId) {
|
465 |
+
currentFilteredRows = selectedModelId === 'all' // Update global filtered rows
|
466 |
? allRows
|
467 |
: allRows.filter(row => row.model_id === selectedModelId);
|
468 |
|
469 |
+
console.log(`Updating dashboard for: ${selectedModelId}, Rows: ${currentFilteredRows.length}`);
|
470 |
+
requestsCountFooter.textContent = currentFilteredRows.length; // Update footer count
|
471 |
|
472 |
// Update KPIs
|
473 |
+
calculateAndDisplayKPIs(currentFilteredRows, selectedModelId);
|
474 |
|
475 |
// Update Plots
|
476 |
+
createLatencyByProviderPlot(currentFilteredRows, selectedModelId);
|
477 |
+
createReliabilityByProviderPlot(currentFilteredRows, selectedModelId);
|
478 |
+
createErrorTypesByProviderPlot(currentFilteredRows, selectedModelId);
|
479 |
+
|
480 |
+
// Update Request Inspector Table
|
481 |
+
createRequestTable(currentFilteredRows);
|
482 |
+
requestInspectorContainer.style.display = 'flex'; // Show inspector table
|
483 |
|
484 |
// Show/Hide plots based on selection
|
485 |
if (selectedModelId === 'all') {
|
486 |
plotLatencyModelDiv.style.display = 'flex';
|
487 |
plotLatencyHeatmapDiv.style.display = 'flex';
|
488 |
modelDetailTableContainerDiv.style.display = 'none';
|
489 |
+
createLatencyByModelPlot(currentFilteredRows); // Only create these for 'all'
|
490 |
+
createLatencyHeatmap(currentFilteredRows);
|
491 |
} else {
|
492 |
plotLatencyModelDiv.style.display = 'none';
|
493 |
plotLatencyHeatmapDiv.style.display = 'none';
|
494 |
modelDetailTableContainerDiv.style.display = 'flex'; // Show table
|
495 |
+
createModelDetailTable(currentFilteredRows, selectedModelId);
|
496 |
}
|
497 |
}
|
498 |
|
499 |
+
// --- Event Listeners ---
|
500 |
modelSelector.addEventListener('change', (event) => {
|
501 |
updateDashboard(event.target.value);
|
502 |
});
|
503 |
|
504 |
+
// Event listener for inspect buttons (delegated)
|
505 |
+
requestTableBody.addEventListener('click', (event) => {
|
506 |
+
if (event.target.classList.contains('inspect-button')) {
|
507 |
+
const rowIndex = parseInt(event.target.getAttribute('data-row-index'), 10);
|
508 |
+
// Find the original row index in allRows based on the filtered index
|
509 |
+
const originalRowData = currentFilteredRows[rowIndex]; // Get data from the *filtered* array using the index from the table
|
510 |
+
if (originalRowData) {
|
511 |
+
showInspectorModal(originalRowData);
|
512 |
+
} else {
|
513 |
+
console.error("Could not find row data for index:", rowIndex);
|
514 |
+
}
|
515 |
+
}
|
516 |
+
});
|
517 |
+
|
518 |
+
// Close modal if overlay is clicked
|
519 |
+
inspectorModal.addEventListener('click', (event) => {
|
520 |
+
if (event.target === inspectorModal) { // Check if the click was directly on the overlay
|
521 |
+
hideInspectorModal();
|
522 |
+
}
|
523 |
+
});
|
524 |
+
|
525 |
+
|
526 |
// --- Initial Fetch ---
|
527 |
fetch(apiUrl)
|
528 |
.then(response => {
|
|
|
530 |
return response.json();
|
531 |
})
|
532 |
.then(data => {
|
533 |
+
allRows = data.rows.map((item, index) => ({ ...item.row, originalIndex: index })); // Store globally, add original index if needed later, though direct filtering is fine here
|
534 |
console.log(`Fetched ${allRows.length} rows.`);
|
535 |
lastUpdatedElement.textContent = new Date().toLocaleString();
|
536 |
|
|
|
551 |
errorDiv.style.display = 'block';
|
552 |
});
|
553 |
|
554 |
+
// --- KPI Calculation Function (same as before) ---
|
555 |
function calculateAndDisplayKPIs(rows, selectedModelId) {
|
556 |
const context = selectedModelId === 'all' ? 'Overall' : `(${selectedModelId.split('/').pop()})`; // Shorten model name for title
|
557 |
|
|
|
602 |
|
603 |
|
604 |
// --- Plotting Functions (Modified to accept data and context) ---
|
605 |
+
// (Plotting functions: createLatencyByProviderPlot, createReliabilityByProviderPlot, createLatencyByModelPlot, createErrorTypesByProviderPlot, createLatencyHeatmap remain largely the same as previous version, just ensure they use Plotly.react for updates)
|
606 |
+
function createLatencyByProviderPlot(rows, selectedModelId) {
|
607 |
const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
|
608 |
const dataByProvider = {};
|
609 |
rows.forEach(row => {
|
|
|
775 |
Plotly.react(plotLatencyHeatmapDiv, plotData, layout, {responsive: true});
|
776 |
}
|
777 |
|
778 |
+
function createModelDetailTable(rows, selectedModelId) { // Only shown when filtered
|
779 |
document.getElementById('table-title').textContent = `Detailed Comparison for ${selectedModelId.split('/').pop()}`;
|
780 |
const tableHead = document.querySelector('#modelDetailTable thead');
|
781 |
const tableBody = document.querySelector('#modelDetailTable tbody');
|
782 |
+
tableHead.innerHTML = ''; tableBody.innerHTML = ''; // Clear
|
|
|
783 |
|
784 |
+
const providerStats = {};
|
785 |
|
786 |
rows.forEach(row => {
|
787 |
const provider = row.provider_name;
|
788 |
+
if (!providerStats[provider]) providerStats[provider] = { total: 0, success: 0, errors: 0, latencies: [] };
|
|
|
|
|
789 |
providerStats[provider].total++;
|
790 |
+
if (row.response_status_code === 200) providerStats[provider].success++;
|
791 |
+
else if (row.response_status_code !== null) providerStats[provider].errors++;
|
792 |
+
if (row.duration_ms !== null && row.duration_ms >= 0) providerStats[provider].latencies.push(row.duration_ms);
|
|
|
|
|
|
|
|
|
|
|
793 |
});
|
794 |
|
|
|
795 |
const headerRow = tableHead.insertRow();
|
796 |
const headers = ['Provider', 'Median Latency (ms)', 'Success Rate (%)', 'Error Count', 'Total Requests'];
|
797 |
+
headers.forEach(text => { headerRow.insertCell().textContent = text; });
|
|
|
|
|
|
|
|
|
798 |
|
|
|
799 |
const providerDataArray = [];
|
800 |
for (const provider in providerStats) {
|
801 |
const stats = providerStats[provider];
|
|
|
|
|
802 |
providerDataArray.push({
|
803 |
provider: provider,
|
804 |
+
medianLatency: calculateMedian(stats.latencies),
|
805 |
+
successRate: stats.total > 0 ? (stats.success / stats.total * 100) : 0,
|
806 |
+
errors: stats.errors, total: stats.total
|
|
|
807 |
});
|
808 |
}
|
809 |
+
providerDataArray.sort((a, b) => (a.medianLatency ?? Infinity) - (b.medianLatency ?? Infinity)); // Sort by latency
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
810 |
|
811 |
providerDataArray.forEach(data => {
|
812 |
const row = tableBody.insertRow();
|
813 |
row.insertCell().textContent = data.provider;
|
814 |
row.insertCell().textContent = data.medianLatency !== null ? data.medianLatency.toFixed(0) : 'N/A';
|
|
|
815 |
const successCell = row.insertCell();
|
816 |
successCell.textContent = data.successRate.toFixed(1);
|
|
|
817 |
if (data.successRate >= 95) successCell.className = 'success-rate-high';
|
818 |
else if (data.successRate >= 80) successCell.className = 'success-rate-medium';
|
819 |
else successCell.className = 'success-rate-low';
|
|
|
|
|
820 |
const errorCell = row.insertCell();
|
821 |
errorCell.textContent = data.errors;
|
822 |
if (data.errors > 0) errorCell.classList.add('error-count');
|
|
|
823 |
row.insertCell().textContent = data.total;
|
824 |
});
|
825 |
}
|
826 |
|
827 |
+
// --- Request Inspector Table ---
|
828 |
+
function createRequestTable(rows) {
|
829 |
+
const tableHead = document.querySelector('#requestTable thead');
|
830 |
+
const tableBody = document.querySelector('#requestTable tbody');
|
831 |
+
tableHead.innerHTML = ''; tableBody.innerHTML = ''; // Clear
|
832 |
+
|
833 |
+
document.getElementById('request-table-title').textContent = `Request Inspector (${rows.length} requests shown)`;
|
834 |
+
|
835 |
+
// Create Header
|
836 |
+
const headerRow = tableHead.insertRow();
|
837 |
+
const headers = ['Provider', 'Model', 'Status', 'Duration (ms)', 'Error', 'Action'];
|
838 |
+
headers.forEach(text => { headerRow.insertCell().textContent = text; });
|
839 |
+
|
840 |
+
// Create Body Rows
|
841 |
+
rows.forEach((row, index) => { // Use index within the *filtered* array
|
842 |
+
const tableRow = tableBody.insertRow();
|
843 |
+
tableRow.insertCell().textContent = row.provider_name;
|
844 |
+
tableRow.insertCell().textContent = row.model_id.split('/').pop(); // Shorten model name
|
845 |
+
const statusCell = tableRow.insertCell();
|
846 |
+
statusCell.textContent = row.response_status_code ?? 'N/A';
|
847 |
+
statusCell.classList.add(row.response_status_code === 200 ? 'status-success' : 'status-error');
|
848 |
+
|
849 |
+
tableRow.insertCell().textContent = row.duration_ms ?? 'N/A';
|
850 |
+
tableRow.insertCell().textContent = row.error_message ? 'Yes' : 'No';
|
851 |
+
if (row.error_message) tableRow.cells[4].style.fontWeight = 'bold';
|
852 |
+
|
853 |
+
const actionCell = tableRow.insertCell();
|
854 |
+
const inspectButton = document.createElement('button');
|
855 |
+
inspectButton.textContent = 'Inspect';
|
856 |
+
inspectButton.className = 'inspect-button';
|
857 |
+
inspectButton.setAttribute('data-row-index', index); // Store the index *within the filtered list*
|
858 |
+
actionCell.appendChild(inspectButton);
|
859 |
+
});
|
860 |
+
}
|
861 |
+
|
862 |
+
// --- Inspector Modal Functions ---
|
863 |
+
function formatJsonString(jsonString) {
|
864 |
+
if (!jsonString) return 'N/A';
|
865 |
+
try {
|
866 |
+
const parsed = JSON.parse(jsonString);
|
867 |
+
return JSON.stringify(parsed, null, 2); // Pretty print
|
868 |
+
} catch (e) {
|
869 |
+
return jsonString; // Return original string if not valid JSON (e.g., HTML error)
|
870 |
+
}
|
871 |
+
}
|
872 |
+
|
873 |
+
window.showInspectorModal = function(rowData) { // Make it global for inline onclick
|
874 |
+
if (!rowData) return;
|
875 |
+
|
876 |
+
document.getElementById('modal-title').textContent = `Details for Request to ${rowData.provider_name}`;
|
877 |
+
document.getElementById('modal-provider').textContent = rowData.provider_name ?? 'N/A';
|
878 |
+
document.getElementById('modal-model').textContent = rowData.model_id ?? 'N/A';
|
879 |
+
document.getElementById('modal-status').textContent = rowData.response_status_code ?? 'N/A';
|
880 |
+
document.getElementById('modal-duration').textContent = rowData.duration_ms ?? 'N/A';
|
881 |
+
document.getElementById('modal-error').textContent = rowData.error_message ?? 'None';
|
882 |
+
document.getElementById('modal-timestamp').textContent = rowData.request_start_iso ? new Date(rowData.request_start_iso).toLocaleString() : 'N/A';
|
883 |
+
|
884 |
+
document.getElementById('modal-req-body').textContent = formatJsonString(rowData.request_body);
|
885 |
+
document.getElementById('modal-resp-body').textContent = formatJsonString(rowData.response_body_raw);
|
886 |
+
document.getElementById('modal-req-headers').textContent = formatJsonString(rowData.request_headers_sanitized);
|
887 |
+
document.getElementById('modal-resp-headers').textContent = formatJsonString(rowData.response_headers_sanitized);
|
888 |
+
|
889 |
+
inspectorModal.style.display = 'block';
|
890 |
+
}
|
891 |
+
|
892 |
+
window.hideInspectorModal = function() { // Make it global for inline onclick
|
893 |
+
inspectorModal.style.display = 'none';
|
894 |
+
}
|
895 |
+
|
896 |
});
|
897 |
</script>
|
898 |
|