nbroad commited on
Commit
a4e6d46
·
verified ·
1 Parent(s): c52a04e

Upload templates/dashboard.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. templates/dashboard.html +784 -0
templates/dashboard.html ADDED
@@ -0,0 +1,784 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Inference Provider Dashboard</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ padding: 10px;
20
+ margin: 0;
21
+ }
22
+
23
+ .container {
24
+ max-width: 1400px;
25
+ margin: 0 auto;
26
+ background: white;
27
+ border-radius: 15px;
28
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
29
+ overflow: hidden;
30
+ max-height: calc(100vh - 20px);
31
+ display: flex;
32
+ flex-direction: column;
33
+ }
34
+
35
+ .header {
36
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
37
+ color: white;
38
+ padding: 20px;
39
+ text-align: center;
40
+ flex-shrink: 0;
41
+ }
42
+
43
+ .header h1 {
44
+ font-size: 2rem;
45
+ font-weight: 300;
46
+ margin-bottom: 5px;
47
+ }
48
+
49
+ .header p {
50
+ font-size: 1rem;
51
+ opacity: 0.9;
52
+ }
53
+
54
+ .controls {
55
+ padding: 15px 20px;
56
+ background: #f8f9fa;
57
+ border-bottom: 1px solid #e9ecef;
58
+ display: flex;
59
+ justify-content: space-between;
60
+ align-items: center;
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ .refresh-btn {
65
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
66
+ color: white;
67
+ border: none;
68
+ padding: 12px 24px;
69
+ border-radius: 25px;
70
+ cursor: pointer;
71
+ font-size: 1rem;
72
+ transition: transform 0.2s, box-shadow 0.2s;
73
+ }
74
+
75
+ .refresh-btn:hover {
76
+ transform: translateY(-2px);
77
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
78
+ }
79
+
80
+ .refresh-btn:disabled {
81
+ opacity: 0.6;
82
+ cursor: not-allowed;
83
+ }
84
+
85
+ .last-updated {
86
+ color: #6c757d;
87
+ font-size: 0.9rem;
88
+ }
89
+
90
+ .content {
91
+ padding: 20px;
92
+ flex: 1;
93
+ overflow-y: auto;
94
+ display: grid;
95
+ grid-template-columns: 1fr;
96
+ grid-template-rows: auto auto auto 1fr;
97
+ gap: 15px;
98
+ max-height: calc(100vh - 200px);
99
+ }
100
+
101
+ .stats-row {
102
+ display: grid;
103
+ grid-template-columns: repeat(3, 1fr);
104
+ gap: 15px;
105
+ margin-bottom: 0;
106
+ }
107
+
108
+ .stat-card {
109
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
110
+ color: white;
111
+ padding: 15px;
112
+ border-radius: 10px;
113
+ text-align: center;
114
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
115
+ }
116
+
117
+ .stat-card h3 {
118
+ font-size: 1.5rem;
119
+ margin-bottom: 3px;
120
+ }
121
+
122
+ .stat-card p {
123
+ opacity: 0.9;
124
+ font-size: 0.9rem;
125
+ }
126
+
127
+ .chart-container {
128
+ background: white;
129
+ border-radius: 10px;
130
+ padding: 20px;
131
+ margin: 0 20px;
132
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
133
+ display: flex;
134
+ flex-direction: column;
135
+ }
136
+
137
+ .chart-container h2 {
138
+ margin-bottom: 15px;
139
+ color: #333;
140
+ font-weight: 600;
141
+ font-size: 1.2rem;
142
+ }
143
+
144
+ .chart-wrapper {
145
+ flex: 1;
146
+ position: relative;
147
+ min-height: 350px;
148
+ }
149
+
150
+ /* Enhanced chart legend styling */
151
+ .chart-container canvas {
152
+ cursor: crosshair;
153
+ }
154
+
155
+ /* Add a subtle instruction text */
156
+ .chart-instructions {
157
+ font-size: 0.8rem;
158
+ color: #6c757d;
159
+ text-align: center;
160
+ margin-top: 8px;
161
+ font-style: italic;
162
+ }
163
+
164
+ .table-container {
165
+ background: white;
166
+ border-radius: 10px;
167
+ padding: 20px;
168
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
169
+ overflow: auto;
170
+ display: flex;
171
+ flex-direction: column;
172
+ }
173
+
174
+ .table-container h2 {
175
+ margin-bottom: 15px;
176
+ color: #333;
177
+ font-weight: 600;
178
+ font-size: 1.2rem;
179
+ }
180
+
181
+ table {
182
+ width: 100%;
183
+ border-collapse: collapse;
184
+ font-size: 0.9rem;
185
+ }
186
+
187
+ th, td {
188
+ padding: 10px 12px;
189
+ text-align: left;
190
+ border-bottom: 1px solid #e9ecef;
191
+ }
192
+
193
+ th {
194
+ background: #f8f9fa;
195
+ font-weight: 600;
196
+ color: #495057;
197
+ position: sticky;
198
+ top: 0;
199
+ }
200
+
201
+ tr:hover {
202
+ background: #f8f9fa;
203
+ }
204
+
205
+ .loading {
206
+ text-align: center;
207
+ padding: 20px;
208
+ color: #6c757d;
209
+ }
210
+
211
+ .spinner {
212
+ border: 3px solid #f3f3f3;
213
+ border-top: 3px solid #667eea;
214
+ border-radius: 50%;
215
+ width: 30px;
216
+ height: 30px;
217
+ animation: spin 1s linear infinite;
218
+ margin: 0 auto 15px;
219
+ }
220
+
221
+ @keyframes spin {
222
+ 0% { transform: rotate(0deg); }
223
+ 100% { transform: rotate(360deg); }
224
+ }
225
+
226
+ .error {
227
+ background: #f8d7da;
228
+ border: 1px solid #f5c6cb;
229
+ color: #721c24;
230
+ padding: 15px;
231
+ border-radius: 10px;
232
+ margin: 15px 20px;
233
+ }
234
+
235
+ @media (max-width: 1024px) {
236
+ .chart-container {
237
+ margin: 0 10px;
238
+ }
239
+
240
+ .chart-wrapper {
241
+ min-height: 300px;
242
+ }
243
+
244
+ .error {
245
+ margin: 15px 10px;
246
+ }
247
+ }
248
+
249
+ @media (max-width: 768px) {
250
+ body {
251
+ padding: 5px;
252
+ }
253
+
254
+ .container {
255
+ max-height: calc(100vh - 10px);
256
+ border-radius: 10px;
257
+ }
258
+
259
+ .controls {
260
+ flex-direction: column;
261
+ gap: 10px;
262
+ padding: 10px 15px;
263
+ }
264
+
265
+ .header {
266
+ padding: 15px;
267
+ }
268
+
269
+ .header h1 {
270
+ font-size: 1.5rem;
271
+ }
272
+
273
+ .content {
274
+ padding: 15px;
275
+ gap: 10px;
276
+ max-height: calc(100vh - 160px);
277
+ }
278
+
279
+ .stats-row {
280
+ grid-template-columns: 1fr;
281
+ gap: 10px;
282
+ }
283
+
284
+ .stat-card {
285
+ padding: 12px;
286
+ }
287
+
288
+ .chart-container {
289
+ margin: 0 5px;
290
+ }
291
+
292
+ .chart-wrapper {
293
+ min-height: 250px;
294
+ }
295
+
296
+ .error {
297
+ margin: 15px 5px;
298
+ }
299
+ }
300
+ </style>
301
+ </head>
302
+ <body>
303
+ <div class="container">
304
+ <div class="header">
305
+ <h1>Inference Provider Dashboard</h1>
306
+ <p>Compare monthly requests across different AI inference providers</p>
307
+ </div>
308
+
309
+ <div class="controls">
310
+ <div class="last-updated" id="lastUpdated">Loading...</div>
311
+ <button class="refresh-btn" id="refreshBtn" onclick="refreshData()">
312
+ Refresh Data
313
+ </button>
314
+ </div>
315
+
316
+ <div class="content">
317
+ <div id="loading" class="loading">
318
+ <div class="spinner"></div>
319
+ <p>Loading provider data...</p>
320
+ </div>
321
+
322
+ <div id="error" class="error" style="display: none;">
323
+ <strong>Error:</strong> <span id="errorMessage"></span>
324
+ </div>
325
+
326
+ <div id="content" style="display: none;">
327
+ <div class="stats-row" id="statsRow">
328
+ <!-- Stats cards will be populated here -->
329
+ </div>
330
+
331
+ <div class="chart-container">
332
+ <h2>Monthly Requests Comparison</h2>
333
+ <div class="chart-wrapper">
334
+ <canvas id="requestsChart"></canvas>
335
+ </div>
336
+ </div>
337
+
338
+ <div class="chart-container">
339
+ <h2>Historical Trends (Last 48 Hours)</h2>
340
+ <div class="chart-wrapper">
341
+ <canvas id="historicalChart"></canvas>
342
+ </div>
343
+ <div class="chart-instructions">
344
+ Hover for detailed coordinates • Click legend items to show/hide providers
345
+ </div>
346
+ </div>
347
+
348
+ <div class="table-container">
349
+ <h2>Provider Details</h2>
350
+ <table id="providersTable">
351
+ <thead>
352
+ <tr>
353
+ <th>Provider</th>
354
+ <th>Monthly Requests</th>
355
+ <th>HuggingFace Profile</th>
356
+ </tr>
357
+ </thead>
358
+ <tbody id="tableBody">
359
+ <!-- Table rows will be populated here -->
360
+ </tbody>
361
+ </table>
362
+ </div>
363
+ </div>
364
+ </div>
365
+ </div>
366
+
367
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
368
+ <script>
369
+ let chart = null;
370
+ let historicalChart = null;
371
+
372
+ async function fetchProviderData() {
373
+ try {
374
+ const response = await fetch('/api/providers');
375
+ if (!response.ok) {
376
+ throw new Error(`HTTP error! status: ${response.status}`);
377
+ }
378
+ return await response.json();
379
+ } catch (error) {
380
+ console.error('Error fetching data:', error);
381
+ throw error;
382
+ }
383
+ }
384
+
385
+ async function fetchHistoricalData() {
386
+ try {
387
+ const response = await fetch('/api/historical');
388
+ if (!response.ok) {
389
+ throw new Error(`HTTP error! status: ${response.status}`);
390
+ }
391
+ return await response.json();
392
+ } catch (error) {
393
+ console.error('Error fetching historical data:', error);
394
+ return { historical_data: {}, error: 'Failed to load historical data' };
395
+ }
396
+ }
397
+
398
+ function showError(message) {
399
+ document.getElementById('loading').style.display = 'none';
400
+ document.getElementById('content').style.display = 'none';
401
+ document.getElementById('error').style.display = 'block';
402
+ document.getElementById('errorMessage').textContent = message;
403
+ }
404
+
405
+ function formatNumber(num) {
406
+ if (num >= 1000000) {
407
+ return (num / 1000000).toFixed(1) + 'M';
408
+ } else if (num >= 1000) {
409
+ return (num / 1000).toFixed(1) + 'K';
410
+ }
411
+ return num.toString();
412
+ }
413
+
414
+ function updateStats(data) {
415
+ const statsRow = document.getElementById('statsRow');
416
+ const totalRequests = data.providers.reduce((sum, provider) => sum + provider.monthly_requests_int, 0);
417
+ const topProvider = data.providers[0];
418
+
419
+ // Use requestAnimationFrame for smooth DOM updates
420
+ requestAnimationFrame(() => {
421
+ statsRow.innerHTML = `
422
+ <div class="stat-card">
423
+ <h3>${data.total_providers}</h3>
424
+ <p>Total Providers</p>
425
+ </div>
426
+ <div class="stat-card">
427
+ <h3>${formatNumber(totalRequests)}</h3>
428
+ <p>Total Monthly Requests</p>
429
+ </div>
430
+ <div class="stat-card">
431
+ <h3>${topProvider.provider}</h3>
432
+ <p>Top Provider</p>
433
+ </div>
434
+ `;
435
+ });
436
+ }
437
+
438
+ function updateChart(data) {
439
+ const ctx = document.getElementById('requestsChart').getContext('2d');
440
+
441
+ if (chart) {
442
+ chart.destroy();
443
+ }
444
+
445
+ // Use the same color mapping as the line chart
446
+ const providerColors = {
447
+ 'fireworks-ai': '#6830E0',
448
+ 'nebius': '#D9FE00',
449
+ 'novita': '#26D57A',
450
+ 'fal': '#D9304D',
451
+ 'togethercomputer': '#0F6FFF',
452
+ 'groq': '#FF6B6B',
453
+ 'cerebras': '#4ECDC4',
454
+ 'sambanovasystems': '#45B7D1',
455
+ 'replicate': '#96CEB4',
456
+ 'Hyperbolic': '#FFEAA7',
457
+ 'featherless-ai': '#DDA0DD',
458
+ 'CohereLabs': '#98D8C8',
459
+ 'nscale': '#F7DC6F'
460
+ };
461
+
462
+ const labels = data.providers.map(p => p.provider);
463
+ const values = data.providers.map(p => p.monthly_requests_int);
464
+
465
+ // Map colors to match providers
466
+ const backgroundColors = data.providers.map(p => {
467
+ const color = providerColors[p.provider] || '#667eea';
468
+ return color + '80'; // Add transparency
469
+ });
470
+
471
+ const borderColors = data.providers.map(p => {
472
+ return providerColors[p.provider] || '#667eea';
473
+ });
474
+
475
+ chart = new Chart(ctx, {
476
+ type: 'bar',
477
+ data: {
478
+ labels: labels,
479
+ datasets: [{
480
+ label: 'Monthly Requests',
481
+ data: values,
482
+ backgroundColor: backgroundColors,
483
+ borderColor: borderColors,
484
+ borderWidth: 1,
485
+ borderRadius: 5
486
+ }]
487
+ },
488
+ options: {
489
+ responsive: true,
490
+ maintainAspectRatio: false,
491
+ animation: {
492
+ duration: 300
493
+ },
494
+ interaction: {
495
+ intersect: false,
496
+ mode: 'index'
497
+ },
498
+ plugins: {
499
+ legend: {
500
+ display: false
501
+ }
502
+ },
503
+ scales: {
504
+ x: {
505
+ grid: {
506
+ display: false
507
+ }
508
+ },
509
+ y: {
510
+ beginAtZero: true,
511
+ grid: {
512
+ color: 'rgba(0, 0, 0, 0.05)'
513
+ },
514
+ ticks: {
515
+ maxTicksLimit: 6,
516
+ callback: function(value) {
517
+ return formatNumber(value);
518
+ }
519
+ }
520
+ }
521
+ }
522
+ }
523
+ });
524
+ }
525
+
526
+ function updateHistoricalChart(historicalData) {
527
+ const ctx = document.getElementById('historicalChart').getContext('2d');
528
+
529
+ if (historicalChart) {
530
+ historicalChart.destroy();
531
+ }
532
+
533
+ // Create datasets for each provider
534
+ const datasets = [];
535
+
536
+ // Specific color mapping for inference providers
537
+ const providerColors = {
538
+ 'fireworks-ai': '#6830E0',
539
+ 'nebius': '#D9FE00',
540
+ 'novita': '#26D57A',
541
+ 'fal': '#D9304D',
542
+ 'togethercomputer': '#0F6FFF',
543
+ // Additional colors for other providers
544
+ 'groq': '#FF6B6B',
545
+ 'cerebras': '#4ECDC4',
546
+ 'sambanovasystems': '#45B7D1',
547
+ 'replicate': '#96CEB4',
548
+ 'Hyperbolic': '#FFEAA7',
549
+ 'featherless-ai': '#DDA0DD',
550
+ 'CohereLabs': '#98D8C8',
551
+ 'nscale': '#F7DC6F'
552
+ };
553
+
554
+ for (const [provider, data] of Object.entries(historicalData)) {
555
+ if (data && data.length > 0) {
556
+ const providerColor = providerColors[provider] || '#667eea'; // fallback color
557
+
558
+ datasets.push({
559
+ label: provider,
560
+ data: data,
561
+ borderColor: providerColor,
562
+ backgroundColor: providerColor + '20',
563
+ borderWidth: 2,
564
+ fill: false,
565
+ tension: 0.4,
566
+ pointRadius: 3,
567
+ pointHoverRadius: 6,
568
+ pointBackgroundColor: providerColor,
569
+ pointBorderColor: '#ffffff',
570
+ pointBorderWidth: 2,
571
+ pointHoverBackgroundColor: '#ffffff',
572
+ pointHoverBorderColor: providerColor,
573
+ pointHoverBorderWidth: 3
574
+ });
575
+ }
576
+ }
577
+
578
+ historicalChart = new Chart(ctx, {
579
+ type: 'line',
580
+ data: {
581
+ datasets: datasets
582
+ },
583
+ options: {
584
+ responsive: true,
585
+ maintainAspectRatio: false,
586
+ animation: {
587
+ duration: 300
588
+ },
589
+ interaction: {
590
+ intersect: false,
591
+ mode: 'index'
592
+ },
593
+ plugins: {
594
+ legend: {
595
+ display: true,
596
+ position: 'top',
597
+ labels: {
598
+ boxWidth: 12,
599
+ padding: 15,
600
+ usePointStyle: true,
601
+ generateLabels: function(chart) {
602
+ const original = Chart.defaults.plugins.legend.labels.generateLabels;
603
+ const labels = original.call(this, chart);
604
+
605
+ // Add click handlers and visual feedback
606
+ labels.forEach(label => {
607
+ label.fillStyle = label.strokeStyle;
608
+ });
609
+
610
+ return labels;
611
+ }
612
+ },
613
+ onClick: function(event, legendItem, legend) {
614
+ const index = legendItem.datasetIndex;
615
+ const chart = legend.chart;
616
+ const meta = chart.getDatasetMeta(index);
617
+
618
+ // Toggle visibility
619
+ meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null;
620
+
621
+ // Update legend appearance
622
+ const legendItems = legend.legendItems;
623
+ if (legendItems && legendItems[index]) {
624
+ legendItems[index].fillStyle = meta.hidden ?
625
+ 'rgba(128, 128, 128, 0.4)' :
626
+ chart.data.datasets[index].borderColor;
627
+ legendItems[index].strokeStyle = meta.hidden ?
628
+ 'rgba(128, 128, 128, 0.4)' :
629
+ chart.data.datasets[index].borderColor;
630
+ }
631
+
632
+ chart.update();
633
+ }
634
+ },
635
+ tooltip: {
636
+ mode: 'nearest',
637
+ intersect: false,
638
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
639
+ titleColor: 'white',
640
+ bodyColor: 'white',
641
+ borderColor: 'rgba(255, 255, 255, 0.2)',
642
+ borderWidth: 1,
643
+ cornerRadius: 8,
644
+ padding: 12,
645
+ displayColors: true,
646
+ callbacks: {
647
+ title: function(tooltipItems) {
648
+ if (tooltipItems.length > 0) {
649
+ const date = new Date(tooltipItems[0].parsed.x);
650
+ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
651
+ }
652
+ return '';
653
+ },
654
+ label: function(context) {
655
+ const provider = context.dataset.label;
656
+ const value = formatNumber(context.parsed.y);
657
+ const date = new Date(context.parsed.x);
658
+ const timeStr = date.toLocaleTimeString();
659
+
660
+ return [
661
+ `Provider: ${provider}`,
662
+ `Requests: ${value}`,
663
+ `Time: ${timeStr}`,
664
+ `Coordinates: (${date.toLocaleDateString()}, ${context.parsed.y})`
665
+ ];
666
+ },
667
+ afterBody: function(tooltipItems) {
668
+ return 'Click legend to toggle provider visibility';
669
+ }
670
+ }
671
+ }
672
+ },
673
+ scales: {
674
+ x: {
675
+ type: 'time',
676
+ time: {
677
+ unit: 'hour',
678
+ displayFormats: {
679
+ hour: 'MMM dd HH:mm'
680
+ }
681
+ },
682
+ title: {
683
+ display: true,
684
+ text: 'Time'
685
+ },
686
+ grid: {
687
+ color: 'rgba(0, 0, 0, 0.05)'
688
+ }
689
+ },
690
+ y: {
691
+ beginAtZero: true,
692
+ title: {
693
+ display: true,
694
+ text: 'Monthly Requests'
695
+ },
696
+ grid: {
697
+ color: 'rgba(0, 0, 0, 0.05)'
698
+ },
699
+ ticks: {
700
+ maxTicksLimit: 6,
701
+ callback: function(value) {
702
+ return formatNumber(value);
703
+ }
704
+ }
705
+ }
706
+ }
707
+ }
708
+ });
709
+ }
710
+
711
+ function updateTable(data) {
712
+ const tableBody = document.getElementById('tableBody');
713
+ const rows = data.providers.map(provider => `
714
+ <tr>
715
+ <td><strong>${provider.provider}</strong></td>
716
+ <td>${formatNumber(provider.monthly_requests_int)}</td>
717
+ <td><a href="https://huggingface.co/${provider.provider}" target="_blank">View Profile</a></td>
718
+ </tr>
719
+ `).join('');
720
+
721
+ // Use requestAnimationFrame for smooth DOM updates
722
+ requestAnimationFrame(() => {
723
+ tableBody.innerHTML = rows;
724
+ });
725
+ }
726
+
727
+ async function loadData() {
728
+ try {
729
+ const loadingEl = document.getElementById('loading');
730
+ const contentEl = document.getElementById('content');
731
+ const errorEl = document.getElementById('error');
732
+
733
+ loadingEl.style.display = 'block';
734
+ contentEl.style.display = 'none';
735
+ errorEl.style.display = 'none';
736
+
737
+ // Fetch both current and historical data
738
+ const [data, historicalData] = await Promise.all([
739
+ fetchProviderData(),
740
+ fetchHistoricalData()
741
+ ]);
742
+
743
+ // Batch DOM updates using requestAnimationFrame
744
+ requestAnimationFrame(() => {
745
+ loadingEl.style.display = 'none';
746
+ contentEl.style.display = 'block';
747
+ document.getElementById('lastUpdated').textContent = `Last updated: ${data.last_updated}`;
748
+ });
749
+
750
+ // Update components
751
+ updateStats(data);
752
+ updateChart(data);
753
+ updateTable(data);
754
+ updateHistoricalChart(historicalData.historical_data || {});
755
+
756
+ } catch (error) {
757
+ showError('Failed to load provider data. Please try again.');
758
+ }
759
+ }
760
+
761
+ async function refreshData() {
762
+ const refreshBtn = document.getElementById('refreshBtn');
763
+ refreshBtn.disabled = true;
764
+ refreshBtn.textContent = 'Refreshing...';
765
+
766
+ try {
767
+ await loadData();
768
+ } finally {
769
+ // Small delay to prevent rapid clicking
770
+ setTimeout(() => {
771
+ refreshBtn.disabled = false;
772
+ refreshBtn.textContent = 'Refresh Data';
773
+ }, 100);
774
+ }
775
+ }
776
+
777
+ // Load data when page loads
778
+ document.addEventListener('DOMContentLoaded', loadData);
779
+
780
+ // Auto-refresh every 10 minutes (reduced from 5 to improve performance)
781
+ setInterval(loadData, 10 * 60 * 1000);
782
+ </script>
783
+ </body>
784
+ </html>