vectorplasticity's picture
Upload templates/index.html with huggingface_hub
e1e24cf verified
Raw
History Blame Contribute Delete
15.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenRouter Models Explorer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--accent-primary: #3b82f6;
--accent-secondary: #8b5cf6;
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
padding: 40px 20px;
background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
border-radius: 16px;
margin-bottom: 30px;
border: 1px solid rgba(255,255,255,0.1);
}
header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
header p {
color: var(--text-secondary);
font-size: 1.1rem;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
padding: 20px;
background: var(--bg-secondary);
border-radius: 12px;
align-items: center;
}
.search-box {
flex: 1;
min-width: 280px;
position: relative;
}
.search-box input {
width: 100%;
padding: 12px 20px;
border: 2px solid var(--bg-tertiary);
border-radius: 8px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 1rem;
transition: all 0.3s ease;
}
.search-box input:focus {
outline: none;
border-color: var(--accent-primary);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
font-size: 0.95rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-secondary:hover {
background: var(--accent-primary);
}
.stats-bar {
background: var(--bg-secondary);
padding: 15px 20px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
gap: 30px;
flex-wrap: wrap;
justify-content: center;
}
.stat {
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--accent-primary);
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9rem;
}
.table-container {
overflow-x: auto;
background: var(--bg-secondary);
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.1);
}
table {
width: 100%;
border-collapse: collapse;
min-width: 900px;
}
th {
background: var(--bg-tertiary);
padding: 16px;
text-align: left;
font-weight: 600;
cursor: pointer;
user-select: none;
position: relative;
transition: background 0.2s;
}
th:hover {
background: var(--accent-primary);
}
th .sort-indicator {
margin-left: 8px;
opacity: 0.5;
}
th.sort-asc .sort-indicator::after {
content: '▲';
opacity: 1;
}
th.sort-desc .sort-indicator::after {
content: '▼';
opacity: 1;
}
td {
padding: 14px 16px;
border-bottom: 1px solid rgba(255,255,255,0.05);
color: var(--text-secondary);
}
tr:hover {
background: rgba(59, 130, 246, 0.05);
}
.model-name {
color: var(--text-primary);
font-weight: 600;
}
.model-id {
font-size: 0.85rem;
color: var(--text-secondary);
font-family: monospace;
}
.architecture {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-primary {
background: rgba(59, 130, 246, 0.2);
color: var(--accent-primary);
}
.badge-success {
background: rgba(34, 197, 94, 0.2);
color: var(--success);
}
.badge-warning {
background: rgba(245, 158, 11, 0.2);
color: var(--warning);
}
.context {
font-family: monospace;
color: var(--accent-secondary);
}
.price {
font-family: monospace;
font-weight: 600;
}
.price input {
color: var(--success);
}
.price output {
color: var(--success);
}
.status {
display: inline-flex;
align-items: center;
gap: 6px;
}
.status::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
}
footer {
text-align: center;
padding: 40px;
color: var(--text-secondary);
margin-top: 20px;
}
.no-results {
text-align: center;
padding: 60px;
color: var(--text-secondary);
}
@media (max-width: 768px) {
header h1 {
font-size: 1.8rem;
}
.controls {
flex-direction: column;
width: 100%;
}
.search-box {
min-width: 100%;
}
.btn {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🔮 OpenRouter Models Explorer</h1>
<p>Explore {{ models|length }} AI models with real-time sorting and search</p>
</header>
<div class="stats-bar">
<div class="stat">
<div class="stat-value">{{ models|length }}</div>
<div class="stat-label">Total Models</div>
</div>
<div class="stat">
<div class="stat-value">{{ unique_providers }}</div>
<div class="stat-label">Providers</div>
</div>
<div class="stat">
<div class="stat-value" id="visible-count">{{ models|length }}</div>
<div class="stat-label">Visible</div>
</div>
</div>
<div class="controls">
<div class="search-box">
<input type="text" id="searchInput" placeholder="🔍 Search models, providers, or architecture...">
</div>
<a href="/api/raw" target="_blank" class="btn btn-secondary">📄 View JSON</a>
<a href="/api/download" class="btn btn-primary">⬇️ Download</a>
<a href="https://openrouter.ai" target="_blank" class="btn btn-secondary">OpenRouter →</a>
</div>
<div class="table-container">
<table id="modelsTable">
<thead>
<tr>
<th onclick="sortTable(0)">Model <span class="sort-indicator"></span></th>
<th onclick="sortTable(1)">Provider <span class="sort-indicator"></span></th>
<th onclick="sortTable(2)">Architecture <span class="sort-indicator"></span></th>
<th onclick="sortTable(3)" style="text-align: right;">Context <span class="sort-indicator"></span></th>
<th onclick="sortTable(4)" style="text-align: right;">Prompt $/1M <span class="sort-indicator"></span></th>
<th onclick="sortTable(5)" style="text-align: right;">Completion $/1M <span class="sort-indicator"></span></th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for model in models %}
<tr>
<td>
<div class="model-name">{{ model.name.split('/', 1)[-1] if '/' in model.name else model.name }}</div>
<div class="model-id">{{ model.id }}</div>
</td>
<td>
<span class="badge badge-primary">{{ model.provider }}</span>
</td>
<td>
<div class="architecture">
{% for arch in model.architecture %}
<span class="badge badge-success">{{ arch }}</span>
{% endfor %}
</div>
</td>
<td style="text-align: right;" class="context">{{ "{:,}".format(model.context) }}</td>
<td style="text-align: right;" class="price">${{ "{:.4f}".format(model.prompt_price) }}</td>
<td style="text-align: right;" class="price">${{ "{:.4f}".format(model.completion_price) }}</td>
<td><span class="status">Available</span></td>
</tr>
{% endfor %}
</tbody>
</table>
<div id="noResults" class="no-results" style="display: none;">
<h3>No models found</h3>
<p>Try adjusting your search criteria</p>
</div>
</div>
<footer>
<p>Data updates every 3 hours • Powered by OpenRouter API</p>
<p style="margin-top: 10px; font-size: 0.85rem;">Built with Flask on HuggingFace Spaces</p>
</footer>
</div>
<script>
let currentSort = { column: -1, direction: 'asc' };
// Search functionality
document.getElementById('searchInput').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#modelsTable tbody tr');
let visibleCount = 0;
rows.forEach(row => {
const text = row.textContent.toLowerCase();
if (text.includes(searchTerm)) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
});
document.getElementById('visible-count').textContent = visibleCount;
document.getElementById('noResults').style.display = visibleCount === 0 ? 'block' : 'none';
});
// Sort functionality
function sortTable(columnIndex) {
const table = document.getElementById('modelsTable');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const headers = table.querySelectorAll('th');
// Update sort direction
if (currentSort.column === columnIndex) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.direction = 'asc';
currentSort.column = columnIndex;
}
// Update header classes
headers.forEach((header, index) => {
header.classList.remove('sort-asc', 'sort-desc');
if (index === columnIndex) {
header.classList.add(currentSort.direction === 'asc' ? 'sort-asc' : 'sort-desc');
}
});
// Sort rows
rows.sort((a, b) => {
const aText = a.cells[columnIndex].textContent.trim();
const bText = b.cells[columnIndex].textContent.trim();
// Check if numeric
const aNum = parseFloat(aText.replace(/[$,]/g, ''));
const bNum = parseFloat(bText.replace(/[$,]/g, ''));
let comparison;
if (!isNaN(aNum) && !isNaN(bNum)) {
comparison = aNum - bNum;
} else {
comparison = aText.localeCompare(bText);
}
return currentSort.direction === 'asc' ? comparison : -comparison;
});
// Reorder rows
rows.forEach(row => tbody.appendChild(row));
}
// Auto-refresh indicator
let lastUpdate = new Date();
setInterval(() => {
const now = new Date();
const elapsed = Math.floor((now - lastUpdate) / 1000);
const hours = Math.floor(elapsed / 3600);
const minutes = Math.floor((elapsed % 3600) / 60);
// Could update a "last updated" timestamp here
}, 60000); // Every minute
</script>
</body>
</html>