victor HF Staff commited on
Commit
7270fb0
·
verified ·
1 Parent(s): ba507da

Update index.html

Browse files
Files changed (1) hide show
  1. 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: center;
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-container {
139
- overflow-x: auto; /* Allow horizontal scrolling on small screens */
140
- align-items: flex-start; /* Align table top */
 
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
- cursor: pointer; /* Indicate sortable */
155
- white-space: nowrap;
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
- <table id="modelDetailTable">
237
- <thead></thead>
238
- <tbody></tbody>
239
- </table>
 
 
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 1000 rows
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
- const baseLayout = {
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
- const filteredRows = selectedModelId === 'all'
326
  ? allRows
327
  : allRows.filter(row => row.model_id === selectedModelId);
328
 
329
- console.log(`Updating dashboard for: ${selectedModelId}, Rows: ${filteredRows.length}`);
 
330
 
331
  // Update KPIs
332
- calculateAndDisplayKPIs(filteredRows, selectedModelId);
333
 
334
  // Update Plots
335
- createLatencyByProviderPlot(filteredRows, selectedModelId);
336
- createReliabilityByProviderPlot(filteredRows, selectedModelId);
337
- createErrorTypesByProviderPlot(filteredRows, selectedModelId);
 
 
 
 
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(filteredRows); // Only create these for 'all'
345
- createLatencyHeatmap(filteredRows);
346
  } else {
347
  plotLatencyModelDiv.style.display = 'none';
348
  plotLatencyHeatmapDiv.style.display = 'none';
349
  modelDetailTableContainerDiv.style.display = 'flex'; // Show table
350
- createModelDetailTable(filteredRows, selectedModelId);
351
  }
352
  }
353
 
354
- // --- Event Listener ---
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
- function createLatencyByProviderPlot(rows, selectedModelId) {
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 previous header
616
- tableBody.innerHTML = ''; // Clear previous body
617
 
618
- const providerStats = {}; // { provider: { total: 0, success: 0, errors: 0, latencies: [] } }
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
- providerStats[provider].success++;
628
- } else if (row.response_status_code !== null) {
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: medianLatency,
654
- successRate: 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