Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ChatRouter - AI Chat Interface</title> | |
| <link rel="icon" type="image/x-icon" href="https://static.photos/technology/200x200/42"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <style> | |
| .chat-height { | |
| height: calc(100vh - 200px); | |
| } | |
| .message-bubble { | |
| max-width: 85%; | |
| word-wrap: break-word; | |
| } | |
| .typing-indicator { | |
| display: inline-block; | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background-color: #9ca3af; | |
| margin-right: 4px; | |
| } | |
| .typing-indicator:nth-child(1) { | |
| animation: typing 1s infinite; | |
| } | |
| .typing-indicator:nth-child(2) { | |
| animation: typing 1s infinite 0.2s; | |
| } | |
| .typing-indicator:nth-child(3) { | |
| animation: typing 1s infinite 0.4s; | |
| margin-right: 0; | |
| } | |
| @keyframes typing { | |
| 0% { transform: translateY(0); } | |
| 50% { transform: translateY(-5px); } | |
| 100% { transform: translateY(0); } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-indigo-900 to-purple-900 text-white"> | |
| <div class="container mx-auto px-4 py-8 max-w-5xl"> | |
| <header class="flex justify-between items-center mb-8"> | |
| <div class="flex items-center space-x-3"> | |
| <i data-feather="cpu" class="w-8 h-8 text-indigo-300"></i> | |
| <h1 class="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-300 to-purple-300">ChatRouter</h1> | |
| </div> | |
| <button id="settingsBtn" class="flex items-center space-x-2 bg-indigo-800 hover:bg-indigo-700 px-4 py-2 rounded-lg transition-all"> | |
| <i data-feather="settings" class="w-5 h-5"></i> | |
| <span>Settings</span> | |
| </button> | |
| </header> | |
| <!-- Settings Modal --> | |
| <div id="settingsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-gray-800 rounded-xl p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-bold">Chat Settings</h2> | |
| <button id="closeSettings" class="text-gray-400 hover:text-white"> | |
| <i data-feather="x"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">OpenRouter API Key</label> | |
| <input type="password" id="apiKeyInput" placeholder="Enter your API key" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <p class="text-xs text-gray-400 mt-1">Your key is stored locally and never sent to our servers</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Select Model</label> | |
| <select id="modelSelect" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <option value="" disabled selected>Enter API key to load models</option> | |
| </select> | |
| <p class="text-xs text-gray-400 mt-1">Models will be loaded after entering API key</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Chat Theme</label> | |
| <div class="flex space-x-2"> | |
| <button data-theme="indigo" class="theme-btn w-8 h-8 bg-indigo-600 rounded-full border-2 border-transparent focus:border-white"></button> | |
| <button data-theme="emerald" class="theme-btn w-8 h-8 bg-emerald-600 rounded-full border-2 border-transparent focus:border-white"></button> | |
| <button data-theme="rose" class="theme-btn w-8 h-8 bg-rose-600 rounded-full border-2 border-transparent focus:border-white"></button> | |
| <button data-theme="amber" class="theme-btn w-8 h-8 bg-amber-600 rounded-full border-2 border-transparent focus:border-white"></button> | |
| <button data-theme="cyan" class="theme-btn w-8 h-8 bg-cyan-600 rounded-full border-2 border-transparent focus:border-white"></button> | |
| </div> | |
| </div> | |
| <button id="saveSettings" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white py-2 rounded-lg transition-colors">Save Settings</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat Container --> | |
| <div class="bg-gray-800 bg-opacity-60 backdrop-blur-lg rounded-2xl shadow-xl overflow-hidden"> | |
| <!-- Chat Messages --> | |
| <div id="chatContainer" class="chat-height overflow-y-auto p-4 space-y-4"> | |
| <div class="flex justify-center"> | |
| <div class="bg-gray-700 bg-opacity-50 px-4 py-2 rounded-lg text-sm text-gray-300"> | |
| <i data-feather="info" class="inline mr-2 w-4 h-4"></i> | |
| Enter your API key in settings to start chatting | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="border-t border-gray-700 p-4 bg-gray-800"> | |
| <div class="flex items-center space-x-2"> | |
| <textarea id="messageInput" placeholder="Type your message..." rows="1" class="flex-1 bg-gray-700 border border-gray-600 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-indigo-500 resize-none max-h-32"></textarea> | |
| <button id="sendBtn" class="bg-indigo-600 hover:bg-indigo-500 text-white p-3 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
| <i data-feather="send" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| <div class="flex justify-between items-center mt-2 text-xs text-gray-400"> | |
| <div id="typingIndicator" class="flex items-center hidden"> | |
| <span class="mr-2">AI is typing</span> | |
| <div class="typing-indicator"></div> | |
| <div class="typing-indicator"></div> | |
| <div class="typing-indicator"></div> | |
| </div> | |
| <div> | |
| <span id="modelIndicator">No model selected</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <footer class="mt-8 text-center text-sm text-gray-400"> | |
| <p>ChatRouter connects you to the best AI models via OpenRouter</p> | |
| <p class="mt-1">Your conversations stay private and secure</p> | |
| </footer> | |
| </div> | |
| <script> | |
| feather.replace(); | |
| // DOM Elements | |
| const settingsBtn = document.getElementById('settingsBtn'); | |
| const settingsModal = document.getElementById('settingsModal'); | |
| const closeSettings = document.getElementById('closeSettings'); | |
| const apiKeyInput = document.getElementById('apiKeyInput'); | |
| const modelSelect = document.getElementById('modelSelect'); | |
| const saveSettings = document.getElementById('saveSettings'); | |
| const messageInput = document.getElementById('messageInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const chatContainer = document.getElementById('chatContainer'); | |
| const typingIndicator = document.getElementById('typingIndicator'); | |
| const modelIndicator = document.getElementById('modelIndicator'); | |
| const themeBtns = document.querySelectorAll('.theme-btn'); | |
| // Load saved settings | |
| async function loadSettings() { | |
| const savedApiKey = localStorage.getItem('chatRouterApiKey'); | |
| const savedModel = localStorage.getItem('chatRouterModel'); | |
| const savedTheme = localStorage.getItem('chatRouterTheme') || 'indigo'; | |
| if (savedApiKey) { | |
| apiKeyInput.value = savedApiKey; | |
| sendBtn.disabled = false; | |
| await fetchModels(savedApiKey); // Load models when API key exists | |
| } | |
| if (savedModel) { | |
| modelSelect.value = savedModel; | |
| modelIndicator.textContent = `Using: ${savedModel.split('/').pop()}`; | |
| } | |
| // Apply theme | |
| document.body.className = `bg-gradient-to-br from-${savedTheme}-900 to-purple-900 text-white`; | |
| } | |
| // Fetch available models from OpenRouter | |
| async function fetchModels(apiKey) { | |
| try { | |
| const response = await fetch('https://openrouter.ai/api/v1/models', { | |
| headers: { | |
| 'Authorization': `Bearer ${apiKey}`, | |
| 'HTTP-Referer': window.location.href, | |
| 'X-Title': 'ChatRouter' | |
| } | |
| }); | |
| const data = await response.json(); | |
| if (data.error) { | |
| addMessage('system', `Error loading models: ${data.error.message}`); | |
| return; | |
| } | |
| // Clear existing options | |
| modelSelect.innerHTML = ''; | |
| // Add default option | |
| const defaultOption = document.createElement('option'); | |
| defaultOption.value = ''; | |
| defaultOption.textContent = 'Select a model'; | |
| defaultOption.disabled = true; | |
| defaultOption.selected = true; | |
| modelSelect.appendChild(defaultOption); | |
| // Add chat models with pricing info | |
| data.data | |
| .filter(model => model.description.includes('chat')) | |
| .sort((a, b) => b.top_provider.max_completion_tokens - a.top_provider.max_completion_tokens) | |
| .forEach(model => { | |
| const option = document.createElement('option'); | |
| option.value = model.id; | |
| option.textContent = `${model.name} (${(model.pricing.completion / 1000).toFixed(4)}/token)`; | |
| modelSelect.appendChild(option); | |
| }); | |
| } catch (error) { | |
| addMessage('system', `Error loading models: ${error.message}`); | |
| } | |
| } | |
| // Save settings | |
| saveSettings.addEventListener('click', async () => { | |
| const apiKey = apiKeyInput.value.trim(); | |
| const model = modelSelect.value; | |
| if (!apiKey) { | |
| alert('Please enter your OpenRouter API key'); | |
| return; | |
| } | |
| if (!model) { | |
| alert('Please select a model'); | |
| return; | |
| } | |
| try { | |
| // Verify API key by fetching models | |
| await fetchModels(apiKey); | |
| // If successful, save settings | |
| localStorage.setItem('chatRouterApiKey', apiKey); | |
| localStorage.setItem('chatRouterModel', model); | |
| sendBtn.disabled = false; | |
| modelIndicator.textContent = `Using: ${model.split('/').pop()}`; | |
| settingsModal.classList.add('hidden'); | |
| // Add confirmation message to chat | |
| addMessage('system', 'Settings saved successfully. You can now start chatting!'); | |
| } catch (error) { | |
| addMessage('system', `Error verifying API key: ${error.message}`); | |
| } | |
| }); | |
| // Load models when API key changes | |
| apiKeyInput.addEventListener('blur', async () => { | |
| const apiKey = apiKeyInput.value.trim(); | |
| if (apiKey) { | |
| await fetchModels(apiKey); | |
| } | |
| }); | |
| // Theme selection | |
| themeBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const theme = btn.dataset.theme; | |
| localStorage.setItem('chatRouterTheme', theme); | |
| document.body.className = `bg-gradient-to-br from-${theme}-900 to-purple-900 text-white`; | |
| // Update button colors to match theme | |
| document.querySelectorAll('.bg-indigo-600').forEach(el => { | |
| el.classList.remove('bg-indigo-600', 'hover:bg-indigo-500'); | |
| el.classList.add(`bg-${theme}-600`, `hover:bg-${theme}-500`); | |
| }); | |
| }); | |
| }); | |
| // Modal controls | |
| settingsBtn.addEventListener('click', () => { | |
| settingsModal.classList.remove('hidden'); | |
| }); | |
| closeSettings.addEventListener('click', () => { | |
| settingsModal.classList.add('hidden'); | |
| }); | |
| // Auto-resize textarea | |
| messageInput.addEventListener('input', () => { | |
| messageInput.style.height = 'auto'; | |
| messageInput.style.height = (messageInput.scrollHeight) + 'px'; | |
| }); | |
| // Add message to chat | |
| function addMessage(role, content) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`; | |
| const bubble = document.createElement('div'); | |
| bubble.className = `message-bubble rounded-2xl px-4 py-3 ${role === 'user' ? 'bg-indigo-600 rounded-tr-none' : 'bg-gray-700 rounded-tl-none'}`; | |
| bubble.textContent = content; | |
| messageDiv.appendChild(bubble); | |
| chatContainer.appendChild(messageDiv); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| // Send message to OpenRouter | |
| async function sendMessage() { | |
| const message = messageInput.value.trim(); | |
| if (!message) return; | |
| const apiKey = localStorage.getItem('chatRouterApiKey'); | |
| const model = localStorage.getItem('chatRouterModel'); | |
| if (!apiKey || !model) { | |
| addMessage('system', 'Please configure your API key and model in settings first.'); | |
| return; | |
| } | |
| // Add user message to chat | |
| addMessage('user', message); | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| // Show typing indicator | |
| typingIndicator.classList.remove('hidden'); | |
| try { | |
| const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${apiKey}`, | |
| 'HTTP-Referer': window.location.href, | |
| 'X-Title': 'ChatRouter' | |
| }, | |
| body: JSON.stringify({ | |
| model: model, | |
| messages: [{ role: 'user', content: message }] | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.error) { | |
| addMessage('system', `Error: ${data.error.message}`); | |
| } else if (data.choices && data.choices[0].message.content) { | |
| addMessage('assistant', data.choices[0].message.content); | |
| } | |
| } catch (error) { | |
| addMessage('system', `Error: ${error.message}`); | |
| } finally { | |
| typingIndicator.classList.add('hidden'); | |
| } | |
| } | |
| // Event listeners | |
| messageInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| sendBtn.addEventListener('click', sendMessage); | |
| // Initialize | |
| loadSettings(); | |
| // Welcome message | |
| setTimeout(() => { | |
| if (!localStorage.getItem('chatRouterApiKey')) { | |
| addMessage('system', 'Welcome to ChatRouter! Please enter your OpenRouter API key in settings to begin.'); | |
| } | |
| }, 1000); | |
| </script> | |
| </body> | |
| </html> | |