Spaces:
Running
Running
<html> | |
<head> | |
<title>HuggingFace Models - Enriched</title> | |
<meta charset="UTF-8"> | |
<style> | |
body { | |
font-family: monospace; | |
margin: 20px; | |
} | |
input { | |
font-family: monospace; | |
border: 1px solid #000; | |
padding: 4px 8px; | |
width: 300px; | |
} | |
table { | |
border-collapse: collapse; | |
width: 100%; | |
} | |
thead { | |
position: sticky; | |
top: 0; | |
z-index: 10; | |
} | |
th, td { | |
border: 1px solid #000; | |
padding: 4px 8px; | |
text-align: left; | |
} | |
tr.model-group-start td { | |
border-top: 2px solid #000; | |
} | |
th { | |
background: #f0f0f0; | |
font-weight: bold; | |
cursor: pointer; | |
user-select: none; | |
position: relative; | |
} | |
th:hover { | |
background: #e0e0e0; | |
} | |
th::after { | |
content: ' ↕'; | |
color: #999; | |
font-size: 0.8em; | |
} | |
th.sort-asc::after { | |
content: ' ↑'; | |
color: #333; | |
} | |
th.sort-desc::after { | |
content: ' ↓'; | |
color: #333; | |
} | |
tr:hover { | |
background: #f9f9f9; | |
} | |
.hidden { | |
display: none; | |
} | |
.highlighted { | |
background: #fffacd ; | |
} | |
.best-value { | |
color: #008000; | |
font-weight: bold; | |
} | |
.header-container { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
gap: 20px; | |
margin-bottom: 10px; | |
} | |
.generation-date { | |
color: #666; | |
font-size: 0.9em; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header-container"> | |
<input type="search" id="filterInput" placeholder="Filter by model or provider..."> | |
<span class="generation-date" id="generationDate"></span> | |
</div> | |
<table id="modelsTable"> | |
<thead> | |
<tr> | |
<th>Model</th> | |
<th>Provider</th> | |
<th>Status</th> | |
<th>Uptime %</th> | |
<th>Input $/1M</th> | |
<th>Output $/1M</th> | |
<th>Context</th> | |
<th>Quant</th> | |
<th>Latency (s)</th> | |
<th>Throughput (t/s)</th> | |
<th>Tools</th> | |
<th>Structured</th> | |
</tr> | |
</thead> | |
<tbody id="tableBody"> | |
<tr><td colspan="12">Loading...</td></tr> | |
</tbody> | |
</table> | |
<script> | |
// Get query parameters | |
const urlParams = new URLSearchParams(window.location.search); | |
const highlightModelId = urlParams.get('model'); | |
fetch('enriched_models_enhanced.json') | |
.then(response => response.json()) | |
.then(data => { | |
// Display generation date | |
if (data.generated_at) { | |
const date = new Date(data.generated_at); | |
const dateStr = date.toLocaleString('en-US', { | |
year: 'numeric', | |
month: 'short', | |
day: 'numeric', | |
hour: '2-digit', | |
minute: '2-digit', | |
timeZoneName: 'short' | |
}); | |
document.getElementById('generationDate').textContent = `Last update: ${dateStr}`; | |
} | |
const tbody = document.getElementById('tableBody'); | |
tbody.innerHTML = ''; | |
let firstHighlightedRow = null; | |
// Handle both old format (direct array) and new format (with metadata) | |
const models = Array.isArray(data) ? data : data.data; | |
models.forEach((model, modelIndex) => { | |
if (model.providers) { | |
model.providers.forEach((provider, providerIndex) => { | |
const row = document.createElement('tr'); | |
// Add class for first provider of each model to create visual separation | |
if (providerIndex === 0 && modelIndex > 0) { | |
row.classList.add('model-group-start'); | |
} | |
// Highlight if model matches query parameter | |
if (highlightModelId && model.id === highlightModelId) { | |
row.classList.add('highlighted'); | |
if (!firstHighlightedRow) { | |
firstHighlightedRow = row; | |
} | |
} | |
row.innerHTML = ` | |
<td>${model.id}</td> | |
<td>${provider.provider}</td> | |
<td>${provider.endpoint_status_name || provider.status || '-'}</td> | |
<td>${provider.uptime_30d !== undefined ? provider.uptime_30d : '-'}</td> | |
<td>${provider.pricing?.input !== undefined ? provider.pricing.input : '-'}</td> | |
<td>${provider.pricing?.output !== undefined ? provider.pricing.output : '-'}</td> | |
<td>${provider.context_length || '-'}</td> | |
<td>${provider.quantization || '-'}</td> | |
<td>${provider.latency_s !== undefined ? provider.latency_s : '-'}</td> | |
<td>${provider.throughput_tps !== undefined ? provider.throughput_tps : '-'}</td> | |
<td>${provider.supports_tools ? 'Yes' : 'No'}</td> | |
<td>${provider.supports_structured_output ? 'Yes' : 'No'}</td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} | |
}); | |
// Store original data for sorting | |
window.tableData = models; | |
// Function to find and mark best values | |
function markBestValues() { | |
const rows = Array.from(tbody.getElementsByTagName('tr')); | |
const highlightedRows = rows.filter(row => row.classList.contains('highlighted')); | |
if (highlightedRows.length === 0) return; | |
// Define which columns need min vs max for best value | |
const columnConfig = { | |
4: 'min', // Input $/1M - lower is better | |
5: 'min', // Output $/1M - lower is better | |
6: 'max', // Context - higher is better | |
8: 'min', // Latency - lower is better | |
9: 'max', // Throughput - higher is better | |
3: 'max' // Uptime % - higher is better | |
}; | |
// For each configured column, find the best value among highlighted rows | |
Object.entries(columnConfig).forEach(([colIndex, type]) => { | |
const values = highlightedRows | |
.map(row => { | |
const cellText = row.cells[colIndex].textContent.trim(); | |
const value = cellText === '-' ? null : parseFloat(cellText); | |
return { row, value, cell: row.cells[colIndex] }; | |
}) | |
.filter(item => item.value !== null && !isNaN(item.value)); | |
if (values.length === 0) return; | |
// Find best value | |
let bestValue; | |
if (type === 'min') { | |
bestValue = Math.min(...values.map(v => v.value)); | |
} else { | |
bestValue = Math.max(...values.map(v => v.value)); | |
} | |
// Mark cells with best value | |
values.forEach(item => { | |
if (item.value === bestValue) { | |
item.cell.classList.add('best-value'); | |
} | |
}); | |
}); | |
} | |
// Call markBestValues if model is highlighted | |
if (highlightModelId) { | |
markBestValues(); | |
} | |
// Scroll to highlighted model if present | |
if (firstHighlightedRow) { | |
setTimeout(() => { | |
firstHighlightedRow.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
}, 100); | |
} | |
// Filter functionality | |
document.getElementById('filterInput').addEventListener('input', function(e) { | |
const filter = e.target.value.toLowerCase(); | |
const rows = tbody.getElementsByTagName('tr'); | |
for (let row of rows) { | |
const modelText = row.cells[0].textContent.toLowerCase(); | |
const providerText = row.cells[1].textContent.toLowerCase(); | |
if (modelText.includes(filter) || providerText.includes(filter)) { | |
row.classList.remove('hidden'); | |
} else { | |
row.classList.add('hidden'); | |
} | |
} | |
}); | |
// Sorting functionality | |
let sortColumn = -1; | |
let sortDirection = 'asc'; | |
const headers = document.querySelectorAll('th'); | |
headers.forEach((header, index) => { | |
header.addEventListener('click', () => { | |
// Remove sort classes from all headers | |
headers.forEach(h => { | |
h.classList.remove('sort-asc', 'sort-desc'); | |
}); | |
// Determine sort direction | |
if (sortColumn === index) { | |
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; | |
} else { | |
sortColumn = index; | |
sortDirection = 'asc'; | |
} | |
// Add sort class to current header | |
header.classList.add(sortDirection === 'asc' ? 'sort-asc' : 'sort-desc'); | |
// Sort the table | |
sortTable(index, sortDirection); | |
}); | |
}); | |
function sortTable(columnIndex, direction) { | |
const rows = Array.from(tbody.getElementsByTagName('tr')); | |
rows.sort((a, b) => { | |
const aText = a.cells[columnIndex].textContent.trim(); | |
const bText = b.cells[columnIndex].textContent.trim(); | |
// Handle special cases | |
if (aText === '-' && bText !== '-') return direction === 'asc' ? 1 : -1; | |
if (aText !== '-' && bText === '-') return direction === 'asc' ? -1 : 1; | |
if (aText === '-' && bText === '-') return 0; | |
// Try to parse as number | |
const aNum = parseFloat(aText); | |
const bNum = parseFloat(bText); | |
let comparison = 0; | |
if (!isNaN(aNum) && !isNaN(bNum)) { | |
comparison = aNum - bNum; | |
} else { | |
// Handle Yes/No specially | |
if (aText === 'Yes' || aText === 'No') { | |
comparison = aText === bText ? 0 : (aText === 'Yes' ? -1 : 1); | |
} else { | |
comparison = aText.localeCompare(bText); | |
} | |
} | |
return direction === 'asc' ? comparison : -comparison; | |
}); | |
// Clear tbody and re-append sorted rows | |
tbody.innerHTML = ''; | |
rows.forEach((row, index) => { | |
// Re-apply model-group-start class based on model changes | |
if (index > 0 && rows[index].cells[0].textContent !== rows[index-1].cells[0].textContent) { | |
row.classList.add('model-group-start'); | |
} else if (index > 0) { | |
row.classList.remove('model-group-start'); | |
} | |
tbody.appendChild(row); | |
}); | |
} | |
}) | |
.catch(error => { | |
console.error('Error loading data:', error); | |
document.getElementById('tableBody').innerHTML = '<tr><td colspan="12">Error loading data</td></tr>'; | |
}); | |
</script> | |
</body> | |
</html> |