| <!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' }; |
| |
| |
| 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'; |
| }); |
| |
| |
| 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'); |
| |
| |
| if (currentSort.column === columnIndex) { |
| currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; |
| } else { |
| currentSort.direction = 'asc'; |
| currentSort.column = columnIndex; |
| } |
| |
| |
| headers.forEach((header, index) => { |
| header.classList.remove('sort-asc', 'sort-desc'); |
| if (index === columnIndex) { |
| header.classList.add(currentSort.direction === 'asc' ? 'sort-asc' : 'sort-desc'); |
| } |
| }); |
| |
| |
| rows.sort((a, b) => { |
| const aText = a.cells[columnIndex].textContent.trim(); |
| const bText = b.cells[columnIndex].textContent.trim(); |
| |
| |
| 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; |
| }); |
| |
| |
| rows.forEach(row => tbody.appendChild(row)); |
| } |
| |
| |
| 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); |
| |
| |
| }, 60000); |
| </script> |
| </body> |
| </html> |