|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Inference Provider Dashboard</title> |
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
min-height: 100vh; |
|
padding: 10px; |
|
margin: 0; |
|
} |
|
|
|
.container { |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
background: white; |
|
border-radius: 15px; |
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); |
|
overflow: hidden; |
|
max-height: calc(100vh - 20px); |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.header { |
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
|
color: white; |
|
padding: 20px; |
|
text-align: center; |
|
flex-shrink: 0; |
|
} |
|
|
|
.header h1 { |
|
font-size: 2rem; |
|
font-weight: 300; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.header p { |
|
font-size: 1rem; |
|
opacity: 0.9; |
|
} |
|
|
|
.controls { |
|
padding: 15px 20px; |
|
background: #f8f9fa; |
|
border-bottom: 1px solid #e9ecef; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
flex-shrink: 0; |
|
} |
|
|
|
.refresh-btn { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
border: none; |
|
padding: 12px 24px; |
|
border-radius: 25px; |
|
cursor: pointer; |
|
font-size: 1rem; |
|
transition: transform 0.2s, box-shadow 0.2s; |
|
} |
|
|
|
.refresh-btn:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); |
|
} |
|
|
|
.refresh-btn:disabled { |
|
opacity: 0.6; |
|
cursor: not-allowed; |
|
} |
|
|
|
.last-updated { |
|
color: #6c757d; |
|
font-size: 0.9rem; |
|
} |
|
|
|
.content { |
|
padding: 20px; |
|
flex: 1; |
|
overflow-y: auto; |
|
display: grid; |
|
grid-template-columns: 1fr; |
|
grid-template-rows: auto auto auto 1fr; |
|
gap: 15px; |
|
max-height: calc(100vh - 200px); |
|
} |
|
|
|
.stats-row { |
|
display: grid; |
|
grid-template-columns: repeat(3, 1fr); |
|
gap: 15px; |
|
margin-bottom: 0; |
|
} |
|
|
|
.stat-card { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
padding: 15px; |
|
border-radius: 10px; |
|
text-align: center; |
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); |
|
} |
|
|
|
.stat-card h3 { |
|
font-size: 1.5rem; |
|
margin-bottom: 3px; |
|
} |
|
|
|
.stat-card p { |
|
opacity: 0.9; |
|
font-size: 0.9rem; |
|
} |
|
|
|
.chart-container { |
|
background: white; |
|
border-radius: 10px; |
|
padding: 20px; |
|
margin: 0 20px; |
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.chart-container h2 { |
|
margin-bottom: 15px; |
|
color: #333; |
|
font-weight: 600; |
|
font-size: 1.2rem; |
|
} |
|
|
|
.chart-wrapper { |
|
flex: 1; |
|
position: relative; |
|
min-height: 350px; |
|
} |
|
|
|
|
|
.chart-container canvas { |
|
cursor: crosshair; |
|
} |
|
|
|
|
|
.chart-instructions { |
|
font-size: 0.8rem; |
|
color: #6c757d; |
|
text-align: center; |
|
margin-top: 8px; |
|
font-style: italic; |
|
} |
|
|
|
.table-container { |
|
background: white; |
|
border-radius: 10px; |
|
padding: 20px; |
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); |
|
overflow: auto; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.table-container h2 { |
|
margin-bottom: 15px; |
|
color: #333; |
|
font-weight: 600; |
|
font-size: 1.2rem; |
|
} |
|
|
|
table { |
|
width: 100%; |
|
border-collapse: collapse; |
|
font-size: 0.9rem; |
|
} |
|
|
|
th, td { |
|
padding: 10px 12px; |
|
text-align: left; |
|
border-bottom: 1px solid #e9ecef; |
|
} |
|
|
|
th { |
|
background: #f8f9fa; |
|
font-weight: 600; |
|
color: #495057; |
|
position: sticky; |
|
top: 0; |
|
} |
|
|
|
tr:hover { |
|
background: #f8f9fa; |
|
} |
|
|
|
.matrix-cell { |
|
text-align: center; |
|
padding: 4px 2px; |
|
min-width: 60px; |
|
max-width: 60px; |
|
font-size: 0.8rem; |
|
} |
|
|
|
.matrix-cell.supported { |
|
color: #28a745; |
|
font-weight: bold; |
|
} |
|
|
|
.matrix-cell.not-supported { |
|
color: #dc3545; |
|
font-weight: bold; |
|
} |
|
|
|
.model-id-cell { |
|
position: sticky; |
|
left: 0; |
|
background: white; |
|
z-index: 5; |
|
border-right: 2px solid #dee2e6; |
|
font-family: monospace; |
|
font-size: 0.75rem; |
|
max-width: 250px; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
} |
|
|
|
.total-cell { |
|
font-weight: bold; |
|
background: #e9ecef; |
|
} |
|
|
|
.loading { |
|
text-align: center; |
|
padding: 20px; |
|
color: #6c757d; |
|
} |
|
|
|
.spinner { |
|
border: 3px solid #f3f3f3; |
|
border-top: 3px solid #667eea; |
|
border-radius: 50%; |
|
width: 30px; |
|
height: 30px; |
|
animation: spin 1s linear infinite; |
|
margin: 0 auto 15px; |
|
} |
|
|
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
|
|
.error { |
|
background: #f8d7da; |
|
border: 1px solid #f5c6cb; |
|
color: #721c24; |
|
padding: 15px; |
|
border-radius: 10px; |
|
margin: 15px 20px; |
|
} |
|
|
|
@media (max-width: 1024px) { |
|
.chart-container { |
|
margin: 0 10px; |
|
} |
|
|
|
.chart-wrapper { |
|
min-height: 300px; |
|
} |
|
|
|
.error { |
|
margin: 15px 10px; |
|
} |
|
} |
|
|
|
@media (max-width: 768px) { |
|
body { |
|
padding: 5px; |
|
} |
|
|
|
.container { |
|
max-height: calc(100vh - 10px); |
|
border-radius: 10px; |
|
} |
|
|
|
.controls { |
|
flex-direction: column; |
|
gap: 10px; |
|
padding: 10px 15px; |
|
} |
|
|
|
.header { |
|
padding: 15px; |
|
} |
|
|
|
.header h1 { |
|
font-size: 1.5rem; |
|
} |
|
|
|
.content { |
|
padding: 15px; |
|
gap: 10px; |
|
max-height: calc(100vh - 160px); |
|
} |
|
|
|
.stats-row { |
|
grid-template-columns: 1fr; |
|
gap: 10px; |
|
} |
|
|
|
.stat-card { |
|
padding: 12px; |
|
} |
|
|
|
.chart-container { |
|
margin: 0 5px; |
|
} |
|
|
|
.chart-wrapper { |
|
min-height: 250px; |
|
} |
|
|
|
.error { |
|
margin: 15px 5px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<h1>Inference Provider Dashboard</h1> |
|
<p>Compare monthly requests across different AI inference providers</p> |
|
</div> |
|
|
|
<div class="controls"> |
|
<div class="last-updated" id="lastUpdated">Loading...</div> |
|
<button class="refresh-btn" id="refreshBtn" onclick="refreshData()"> |
|
Refresh Data |
|
</button> |
|
</div> |
|
|
|
<div class="content"> |
|
<div id="loading" class="loading"> |
|
<div class="spinner"></div> |
|
<p>Loading provider data...</p> |
|
</div> |
|
|
|
<div id="error" class="error" style="display: none;"> |
|
<strong>Error:</strong> <span id="errorMessage"></span> |
|
</div> |
|
|
|
<div id="content" style="display: none;"> |
|
<div class="stats-row" id="statsRow"> |
|
|
|
</div> |
|
|
|
<div class="chart-container"> |
|
<h2>Live Comparison of Monthly Requests by Provider</h2> |
|
<div class="chart-wrapper"> |
|
<canvas id="requestsChart"></canvas> |
|
</div> |
|
</div> |
|
|
|
<div class="chart-container"> |
|
<h2 id="historicalChartTitle">Historical Trends</h2> |
|
<div class="chart-wrapper"> |
|
<canvas id="historicalChart"></canvas> |
|
</div> |
|
<div class="chart-instructions"> |
|
Hover for detailed coordinates • Click legend items to show/hide providers |
|
</div> |
|
</div> |
|
|
|
<div class="table-container"> |
|
<h2>Provider Details</h2> |
|
<table id="providersTable"> |
|
<thead> |
|
<tr> |
|
<th>Provider</th> |
|
<th>Monthly Requests</th> |
|
<th>Hugging Face Profile</th> |
|
</tr> |
|
</thead> |
|
<tbody id="tableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
|
|
<div class="table-container"> |
|
<h2>Model Support Matrix</h2> |
|
<div id="modelsLoading" class="loading" style="display: none;"> |
|
<div class="spinner"></div> |
|
<p>Loading models data...</p> |
|
</div> |
|
<div style="overflow-x: auto;"> |
|
<table id="modelsTable" style="width: 100%; table-layout: fixed;"> |
|
<thead> |
|
<tr id="modelsTableHeader"> |
|
<th style="width: 250px; position: sticky; left: 0; background: #f8f9fa; z-index: 10;">Model ID</th> |
|
|
|
<th style="width: 60px;">Total</th> |
|
</tr> |
|
</thead> |
|
<tbody id="modelsTableBody"> |
|
|
|
</tbody> |
|
<tfoot id="modelsTableFoot"> |
|
|
|
</tfoot> |
|
</table> |
|
</div> |
|
<div id="providerLinks" style="margin-top: 20px; font-size: 0.9rem; color: #6c757d;"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script> |
|
<script> |
|
let chart = null; |
|
let historicalChart = null; |
|
|
|
async function fetchProviderData() { |
|
try { |
|
const response = await fetch('/api/providers'); |
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
return await response.json(); |
|
} catch (error) { |
|
console.error('Error fetching data:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
async function fetchHistoricalData() { |
|
try { |
|
const response = await fetch('/api/historical'); |
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
return await response.json(); |
|
} catch (error) { |
|
console.error('Error fetching historical data:', error); |
|
return { historical_data: {}, error: 'Failed to load historical data' }; |
|
} |
|
} |
|
|
|
async function fetchModelsData() { |
|
try { |
|
const response = await fetch('/api/models'); |
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
return await response.json(); |
|
} catch (error) { |
|
console.error('Error fetching models data:', error); |
|
return { models: {}, error: 'Failed to load models data' }; |
|
} |
|
} |
|
|
|
function showError(message) { |
|
document.getElementById('loading').style.display = 'none'; |
|
document.getElementById('content').style.display = 'none'; |
|
document.getElementById('error').style.display = 'block'; |
|
document.getElementById('errorMessage').textContent = message; |
|
} |
|
|
|
function formatNumber(num) { |
|
if (num >= 1000000) { |
|
return (num / 1000000).toFixed(1) + 'M'; |
|
} else if (num >= 1000) { |
|
return (num / 1000).toFixed(1) + 'K'; |
|
} |
|
return num.toString(); |
|
} |
|
|
|
function updateStats(data) { |
|
const statsRow = document.getElementById('statsRow'); |
|
const totalRequests = data.providers.reduce((sum, provider) => sum + provider.monthly_requests_int, 0); |
|
const topProvider = data.providers[0]; |
|
|
|
|
|
requestAnimationFrame(() => { |
|
statsRow.innerHTML = ` |
|
<div class="stat-card"> |
|
<h3>${data.total_providers}</h3> |
|
<p>Total Providers</p> |
|
</div> |
|
<div class="stat-card"> |
|
<h3>${formatNumber(totalRequests)}</h3> |
|
<p>Total Monthly Requests</p> |
|
</div> |
|
<div class="stat-card"> |
|
<h3>${topProvider.provider}</h3> |
|
<p>Top Provider</p> |
|
</div> |
|
`; |
|
}); |
|
} |
|
|
|
function updateChart(data) { |
|
const ctx = document.getElementById('requestsChart').getContext('2d'); |
|
|
|
if (chart) { |
|
chart.destroy(); |
|
} |
|
|
|
|
|
const providerColors = { |
|
'fireworks-ai': '#6830E0', |
|
'nebius': '#D9FE00', |
|
'novita': '#26D57A', |
|
'fal': '#D9304D', |
|
'togethercomputer': '#0F6FFF', |
|
'groq': '#FF6B6B', |
|
'cerebras': '#4ECDC4', |
|
'sambanovasystems': '#45B7D1', |
|
'replicate': '#96CEB4', |
|
'Hyperbolic': '#FFEAA7', |
|
'featherless-ai': '#DDA0DD', |
|
'CohereLabs': '#98D8C8', |
|
'nscale': '#F7DC6F' |
|
}; |
|
|
|
const labels = data.providers.map(p => p.provider); |
|
const values = data.providers.map(p => p.monthly_requests_int); |
|
|
|
|
|
const backgroundColors = data.providers.map(p => { |
|
const color = providerColors[p.provider] || '#667eea'; |
|
return color + '80'; |
|
}); |
|
|
|
const borderColors = data.providers.map(p => { |
|
return providerColors[p.provider] || '#667eea'; |
|
}); |
|
|
|
chart = new Chart(ctx, { |
|
type: 'bar', |
|
data: { |
|
labels: labels, |
|
datasets: [{ |
|
label: 'Monthly Requests', |
|
data: values, |
|
backgroundColor: backgroundColors, |
|
borderColor: borderColors, |
|
borderWidth: 1, |
|
borderRadius: 5 |
|
}] |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
animation: { |
|
duration: 300 |
|
}, |
|
interaction: { |
|
intersect: false, |
|
mode: 'index' |
|
}, |
|
plugins: { |
|
legend: { |
|
display: false |
|
} |
|
}, |
|
scales: { |
|
x: { |
|
grid: { |
|
display: false |
|
} |
|
}, |
|
y: { |
|
beginAtZero: true, |
|
grid: { |
|
color: 'rgba(0, 0, 0, 0.05)' |
|
}, |
|
ticks: { |
|
maxTicksLimit: 6, |
|
callback: function(value) { |
|
return formatNumber(value); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
|
|
function updateHistoricalChart(historicalData) { |
|
const ctx = document.getElementById('historicalChart').getContext('2d'); |
|
|
|
if (historicalChart) { |
|
historicalChart.destroy(); |
|
} |
|
|
|
|
|
const datasets = []; |
|
|
|
|
|
const providerColors = { |
|
'fireworks-ai': '#6830E0', |
|
'nebius': '#D9FE00', |
|
'novita': '#26D57A', |
|
'fal': '#D9304D', |
|
'togethercomputer': '#0F6FFF', |
|
|
|
'groq': '#FF6B6B', |
|
'cerebras': '#4ECDC4', |
|
'sambanovasystems': '#45B7D1', |
|
'replicate': '#96CEB4', |
|
'Hyperbolic': '#FFEAA7', |
|
'featherless-ai': '#DDA0DD', |
|
'CohereLabs': '#98D8C8', |
|
'nscale': '#F7DC6F' |
|
}; |
|
|
|
|
|
const providerData = historicalData.historical_data || {}; |
|
|
|
for (const [provider, data] of Object.entries(providerData)) { |
|
if (data && data.length > 0) { |
|
const providerColor = providerColors[provider] || '#667eea'; |
|
|
|
datasets.push({ |
|
label: provider, |
|
data: data, |
|
borderColor: providerColor, |
|
backgroundColor: providerColor + '20', |
|
borderWidth: 2, |
|
fill: false, |
|
tension: 0.4, |
|
pointRadius: 3, |
|
pointHoverRadius: 6, |
|
pointBackgroundColor: providerColor, |
|
pointBorderColor: '#ffffff', |
|
pointBorderWidth: 2, |
|
pointHoverBackgroundColor: '#ffffff', |
|
pointHoverBorderColor: providerColor, |
|
pointHoverBorderWidth: 3 |
|
}); |
|
} |
|
} |
|
|
|
|
|
console.log('Creating historical chart with', datasets.length, 'datasets'); |
|
|
|
historicalChart = new Chart(ctx, { |
|
type: 'line', |
|
data: { |
|
datasets: datasets |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
animation: { |
|
duration: 300 |
|
}, |
|
interaction: { |
|
intersect: false, |
|
mode: 'index' |
|
}, |
|
plugins: { |
|
legend: { |
|
display: true, |
|
position: 'top', |
|
labels: { |
|
boxWidth: 12, |
|
padding: 15, |
|
usePointStyle: true, |
|
generateLabels: function(chart) { |
|
const original = Chart.defaults.plugins.legend.labels.generateLabels; |
|
const labels = original.call(this, chart); |
|
|
|
|
|
labels.forEach(label => { |
|
label.fillStyle = label.strokeStyle; |
|
}); |
|
|
|
return labels; |
|
} |
|
}, |
|
onClick: function(event, legendItem, legend) { |
|
const index = legendItem.datasetIndex; |
|
const chart = legend.chart; |
|
const meta = chart.getDatasetMeta(index); |
|
|
|
|
|
meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null; |
|
|
|
|
|
const legendItems = legend.legendItems; |
|
if (legendItems && legendItems[index]) { |
|
legendItems[index].fillStyle = meta.hidden ? |
|
'rgba(128, 128, 128, 0.4)' : |
|
chart.data.datasets[index].borderColor; |
|
legendItems[index].strokeStyle = meta.hidden ? |
|
'rgba(128, 128, 128, 0.4)' : |
|
chart.data.datasets[index].borderColor; |
|
} |
|
|
|
chart.update(); |
|
} |
|
}, |
|
tooltip: { |
|
mode: 'nearest', |
|
intersect: false, |
|
backgroundColor: 'rgba(0, 0, 0, 0.8)', |
|
titleColor: 'white', |
|
bodyColor: 'white', |
|
borderColor: 'rgba(255, 255, 255, 0.2)', |
|
borderWidth: 1, |
|
cornerRadius: 8, |
|
padding: 12, |
|
displayColors: true, |
|
callbacks: { |
|
title: function(tooltipItems) { |
|
if (tooltipItems.length > 0) { |
|
const date = new Date(tooltipItems[0].parsed.x); |
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); |
|
} |
|
return ''; |
|
}, |
|
label: function(context) { |
|
const provider = context.dataset.label; |
|
const value = formatNumber(context.parsed.y); |
|
const date = new Date(context.parsed.x); |
|
const timeStr = date.toLocaleTimeString(); |
|
|
|
return [ |
|
`Provider: ${provider}`, |
|
`Requests: ${value}`, |
|
`Time: ${timeStr}`, |
|
`Coordinates: (${date.toLocaleDateString()}, ${context.parsed.y})` |
|
]; |
|
}, |
|
afterBody: function(tooltipItems) { |
|
return 'Click legend to toggle provider visibility'; |
|
} |
|
} |
|
} |
|
}, |
|
scales: { |
|
x: { |
|
type: 'time', |
|
time: { |
|
|
|
displayFormats: { |
|
minute: 'HH:mm', |
|
hour: 'MMM dd HH:mm', |
|
day: 'MMM dd', |
|
week: 'MMM dd', |
|
month: 'MMM yyyy', |
|
year: 'yyyy' |
|
} |
|
}, |
|
title: { |
|
display: true, |
|
text: 'Time' |
|
}, |
|
grid: { |
|
color: 'rgba(0, 0, 0, 0.05)' |
|
}, |
|
|
|
adapters: { |
|
date: {} |
|
} |
|
}, |
|
y: { |
|
beginAtZero: true, |
|
title: { |
|
display: true, |
|
text: 'Monthly Requests' |
|
}, |
|
grid: { |
|
color: 'rgba(0, 0, 0, 0.05)' |
|
}, |
|
ticks: { |
|
maxTicksLimit: 6, |
|
callback: function(value) { |
|
return formatNumber(value); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
|
|
function updateTable(data) { |
|
const tableBody = document.getElementById('tableBody'); |
|
const rows = data.providers.map(provider => ` |
|
<tr> |
|
<td><strong>${provider.provider}</strong></td> |
|
<td>${formatNumber(provider.monthly_requests_int)}</td> |
|
<td><a href="https://huggingface.co/${provider.provider}" target="_blank">View Profile</a></td> |
|
</tr> |
|
`).join(''); |
|
|
|
|
|
requestAnimationFrame(() => { |
|
tableBody.innerHTML = rows; |
|
}); |
|
} |
|
|
|
function updateModelsTable(modelsData) { |
|
const modelsTableBody = document.getElementById('modelsTableBody'); |
|
const modelsTableHeader = document.getElementById('modelsTableHeader'); |
|
const modelsTableFoot = document.getElementById('modelsTableFoot'); |
|
const modelsLoading = document.getElementById('modelsLoading'); |
|
|
|
if (modelsData.error) { |
|
const errorRow = ` |
|
<tr> |
|
<td colspan="100%" style="text-align: center; color: #6c757d; font-style: italic;"> |
|
${modelsData.error === 'HF_TOKEN required for models data' ? |
|
'HF Token required to display models data' : |
|
'Failed to load models data'} |
|
</td> |
|
</tr> |
|
`; |
|
requestAnimationFrame(() => { |
|
modelsLoading.style.display = 'none'; |
|
modelsTableBody.innerHTML = errorRow; |
|
modelsTableFoot.innerHTML = ''; |
|
}); |
|
return; |
|
} |
|
|
|
const providers = modelsData.providers || []; |
|
const matrix = modelsData.matrix || []; |
|
const providerTotals = modelsData.provider_totals || {}; |
|
const providerMapping = modelsData.provider_mapping || {}; |
|
|
|
|
|
const providerShortNames = { |
|
'togethercomputer': 'Together', |
|
'fireworks-ai': 'Fireworks', |
|
'nebius': 'Nebius', |
|
'fal': 'FAL', |
|
'groq': 'Groq', |
|
'cerebras': 'Cerebras', |
|
'sambanovasystems': 'SambaNova', |
|
'replicate': 'Replicate', |
|
'novita': 'Novita', |
|
'Hyperbolic': 'Hyperbolic', |
|
'featherless-ai': 'Featherless', |
|
'CohereLabs': 'Cohere', |
|
'nscale': 'NScale' |
|
}; |
|
|
|
|
|
const providerHeaders = providers.map(provider => { |
|
const shortName = providerShortNames[provider] || provider; |
|
return `<th class="matrix-cell" style="width: 60px; font-size: 0.7rem; padding: 2px;" title="${provider}">${shortName}</th>`; |
|
}).join(''); |
|
|
|
|
|
const headerHtml = ` |
|
<th style="width: 250px; position: sticky; left: 0; background: #f8f9fa; z-index: 10;">Model ID</th> |
|
${providerHeaders} |
|
<th style="width: 60px;" class="total-cell">Total</th> |
|
`; |
|
|
|
|
|
const matrixRows = matrix.map(row => { |
|
const providerCells = providers.map(provider => { |
|
const isSupported = row.providers[provider]; |
|
return `<td class="matrix-cell ${isSupported ? 'supported' : 'not-supported'}"> |
|
${isSupported ? '✓' : '✗'} |
|
</td>`; |
|
}).join(''); |
|
|
|
return ` |
|
<tr> |
|
<td class="model-id-cell">${row.model_id}</td> |
|
${providerCells} |
|
<td class="matrix-cell total-cell">${row.total_providers}</td> |
|
</tr> |
|
`; |
|
}).join(''); |
|
|
|
|
|
const totalCells = providers.map(provider => |
|
`<td class="matrix-cell total-cell">${providerTotals[provider] || 0}</td>` |
|
).join(''); |
|
|
|
const footerHtml = ` |
|
<tr> |
|
<td class="model-id-cell total-cell">Total Models</td> |
|
${totalCells} |
|
<td class="matrix-cell total-cell">${modelsData.total_models || 0}</td> |
|
</tr> |
|
`; |
|
|
|
|
|
const providerLinksDiv = document.getElementById('providerLinks'); |
|
const providerLinksHtml = providers |
|
.filter(provider => providerMapping[provider]) |
|
.map(provider => { |
|
const inferenceProvider = providerMapping[provider]; |
|
const url = `https://huggingface.co/models?inference_provider=${inferenceProvider}&sort=trending`; |
|
return `<strong>${provider}:</strong> To see all supported models, <a href="${url}" target="_blank" style="color: #667eea;">click here</a>`; |
|
}) |
|
.join('<br>'); |
|
|
|
|
|
requestAnimationFrame(() => { |
|
modelsLoading.style.display = 'none'; |
|
modelsTableHeader.innerHTML = headerHtml; |
|
modelsTableBody.innerHTML = matrixRows; |
|
modelsTableFoot.innerHTML = footerHtml; |
|
if (providerLinksDiv) { |
|
providerLinksDiv.innerHTML = providerLinksHtml; |
|
} |
|
}); |
|
} |
|
|
|
async function loadData() { |
|
try { |
|
const loadingEl = document.getElementById('loading'); |
|
const contentEl = document.getElementById('content'); |
|
const errorEl = document.getElementById('error'); |
|
const modelsLoading = document.getElementById('modelsLoading'); |
|
|
|
loadingEl.style.display = 'block'; |
|
contentEl.style.display = 'none'; |
|
errorEl.style.display = 'none'; |
|
modelsLoading.style.display = 'block'; |
|
|
|
|
|
const [data, historicalData, modelsData] = await Promise.all([ |
|
fetchProviderData(), |
|
fetchHistoricalData(), |
|
fetchModelsData() |
|
]); |
|
|
|
|
|
requestAnimationFrame(() => { |
|
loadingEl.style.display = 'none'; |
|
contentEl.style.display = 'block'; |
|
document.getElementById('lastUpdated').textContent = `Last updated: ${data.last_updated}`; |
|
}); |
|
|
|
|
|
updateStats(data); |
|
updateChart(data); |
|
updateTable(data); |
|
updateHistoricalChart(historicalData); |
|
updateModelsTable(modelsData); |
|
|
|
|
|
const titleElement = document.getElementById('historicalChartTitle'); |
|
if (historicalData.data_range && historicalData.data_range !== 'No data') { |
|
titleElement.textContent = `Historical Trends (${historicalData.data_range})`; |
|
} else { |
|
titleElement.textContent = 'Historical Trends'; |
|
} |
|
|
|
} catch (error) { |
|
showError('Failed to load provider data. Please try again.'); |
|
} |
|
} |
|
|
|
async function refreshData() { |
|
const refreshBtn = document.getElementById('refreshBtn'); |
|
refreshBtn.disabled = true; |
|
refreshBtn.textContent = 'Refreshing...'; |
|
|
|
try { |
|
await loadData(); |
|
} finally { |
|
|
|
setTimeout(() => { |
|
refreshBtn.disabled = false; |
|
refreshBtn.textContent = 'Refresh Data'; |
|
}, 100); |
|
} |
|
} |
|
|
|
|
|
function disableNavigationGestures() { |
|
|
|
let startX = 0; |
|
let startY = 0; |
|
|
|
document.addEventListener('touchstart', function(e) { |
|
startX = e.touches[0].clientX; |
|
startY = e.touches[0].clientY; |
|
}, { passive: false }); |
|
|
|
document.addEventListener('touchmove', function(e) { |
|
if (e.touches.length > 1) return; |
|
|
|
const deltaX = Math.abs(e.touches[0].clientX - startX); |
|
const deltaY = Math.abs(e.touches[0].clientY - startY); |
|
|
|
|
|
if (deltaX > 10 && deltaX > deltaY) { |
|
e.preventDefault(); |
|
} |
|
}, { passive: false }); |
|
|
|
|
|
document.addEventListener('wheel', function(e) { |
|
|
|
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) { |
|
const target = e.target.closest('.table-container, [style*="overflow"]'); |
|
if (target) { |
|
e.stopPropagation(); |
|
} |
|
} |
|
}, { passive: false }); |
|
|
|
|
|
document.addEventListener('keydown', function(e) { |
|
|
|
if ((e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) || |
|
(e.metaKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight'))) { |
|
e.preventDefault(); |
|
} |
|
|
|
|
|
if (e.key === 'Backspace' && |
|
!['INPUT', 'TEXTAREA'].includes(e.target.tagName) && |
|
!e.target.isContentEditable) { |
|
e.preventDefault(); |
|
} |
|
}); |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
disableNavigationGestures(); |
|
loadData(); |
|
}); |
|
|
|
|
|
setInterval(loadData, 10 * 60 * 1000); |
|
</script> |
|
</body> |
|
</html> |