|
<!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; |
|
} |
|
|
|
.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>Monthly Requests Comparison</h2> |
|
<div class="chart-wrapper"> |
|
<canvas id="requestsChart"></canvas> |
|
</div> |
|
</div> |
|
|
|
<div class="chart-container"> |
|
<h2>Historical Trends (Last 48 Hours)</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>HuggingFace Profile</th> |
|
</tr> |
|
</thead> |
|
<tbody id="tableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</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' }; |
|
} |
|
} |
|
|
|
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' |
|
}; |
|
|
|
for (const [provider, data] of Object.entries(historicalData)) { |
|
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 |
|
}); |
|
} |
|
} |
|
|
|
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: { |
|
unit: 'hour', |
|
displayFormats: { |
|
hour: 'MMM dd HH:mm' |
|
} |
|
}, |
|
title: { |
|
display: true, |
|
text: 'Time' |
|
}, |
|
grid: { |
|
color: 'rgba(0, 0, 0, 0.05)' |
|
} |
|
}, |
|
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; |
|
}); |
|
} |
|
|
|
async function loadData() { |
|
try { |
|
const loadingEl = document.getElementById('loading'); |
|
const contentEl = document.getElementById('content'); |
|
const errorEl = document.getElementById('error'); |
|
|
|
loadingEl.style.display = 'block'; |
|
contentEl.style.display = 'none'; |
|
errorEl.style.display = 'none'; |
|
|
|
|
|
const [data, historicalData] = await Promise.all([ |
|
fetchProviderData(), |
|
fetchHistoricalData() |
|
]); |
|
|
|
|
|
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.historical_data || {}); |
|
|
|
} 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); |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', loadData); |
|
|
|
|
|
setInterval(loadData, 10 * 60 * 1000); |
|
</script> |
|
</body> |
|
</html> |