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> | |
| /* Mobile sidebar positioning */ | |
| @media (max-width: 767px) { | |
| #sidebar { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| height: 100vh; | |
| width: 280px; | |
| z-index: 30; | |
| transform: translateX(-100%); | |
| transition: transform 0.3s ease; | |
| } | |
| #sidebar:not(.hidden) { | |
| transform: translateX(0); | |
| } | |
| } | |
| .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-7xl"> | |
| <!-- Sidebar Toggle for Mobile --> | |
| <button id="sidebarToggle" class="fixed left-4 top-4 z-40 md:hidden bg-gray-800 p-2 rounded-lg"> | |
| <i data-feather="menu"></i> | |
| </button> | |
| <header class="flex justify-between items-center mb-8 ml-12 md:ml-0"> | |
| <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> | |
| <div class="flex items-center space-x-2"> | |
| <button id="newChatBtn" 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="plus" class="w-5 h-5"></i> | |
| <span>New Chat</span> | |
| </button> | |
| <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> | |
| <button id="authBtn" 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="user" class="w-5 h-5"></i> | |
| <span>Login</span> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Auth Modal --> | |
| <div id="authModal" 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" id="authModalTitle">Login</h2> | |
| <button id="closeAuth" class="text-gray-400 hover:text-white"> | |
| <i data-feather="x"></i> | |
| </button> | |
| </div> | |
| <div id="authForm" class="space-y-4"> | |
| <div id="registerFields" class="hidden"> | |
| <label class="block text-sm font-medium mb-1">Username</label> | |
| <input type="text" id="regUsername" placeholder="Choose a username" 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"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Password</label> | |
| <input type="password" id="authPassword" placeholder="Enter your password" 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"> | |
| </div> | |
| <button id="authActionBtn" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white py-2 rounded-lg transition-colors">Login</button> | |
| <div class="text-center text-sm"> | |
| <span id="authToggleText">Don't have an account? </span> | |
| <button id="authToggleBtn" class="text-indigo-400 hover:text-indigo-300">Register</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 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>Select a free model</option> | |
| <option value="tongyi/deepresearch-30b-a3b">Tongyi DeepResearch 30B A3B (FREE)</option> | |
| <option value="nvidia/nemotron-nano-9b-v2">NVIDIA: Nemotron Nano 9B V2 (FREE)</option> | |
| <option value="meta/llama-3.3-70b-instruct">Meta: Llama 3.3 70B Instruct (FREE)</option> | |
| </select> | |
| <p class="text-xs text-gray-400 mt-1">Free models available without token charges. <a href="https://openrouter.ai/" target="_blank" class="text-indigo-400 hover:text-indigo-300">Get your API key here</a></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> | |
| <div class="flex flex-col md:flex-row gap-6"> | |
| <!-- Sidebar --> | |
| <div id="sidebar" class="hidden md:block w-64 bg-gray-800 bg-opacity-60 backdrop-blur-lg rounded-2xl shadow-xl h-[80vh] p-4 overflow-y-auto"> | |
| <div class="flex flex-col h-full"> | |
| <button id="newSidebarChat" class="flex items-center space-x-2 bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-lg mb-4"> | |
| <i data-feather="plus" class="w-4 h-4"></i> | |
| <span>New Chat</span> | |
| </button> | |
| <div id="chatList" class="flex-1 overflow-y-auto space-y-1"> | |
| <!-- Chat items will be dynamically added here --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Chat Container --> | |
| <div class="flex-1 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> | |
| Select a free model from 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> | |
| </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 - New Sidebar Elements | |
| const sidebarToggle = document.getElementById('sidebarToggle'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const newSidebarChat = document.getElementById('newSidebarChat'); | |
| const chatList = document.getElementById('chatList'); | |
| // DOM Elements - Original | |
| const authBtn = document.getElementById('authBtn'); | |
| const authModal = document.getElementById('authModal'); | |
| const authModalTitle = document.getElementById('authModalTitle'); | |
| const closeAuth = document.getElementById('closeAuth'); | |
| const regUsername = document.getElementById('regUsername'); | |
| const authPassword = document.getElementById('authPassword'); | |
| const authActionBtn = document.getElementById('authActionBtn'); | |
| const authToggleBtn = document.getElementById('authToggleBtn'); | |
| const authToggleText = document.getElementById('authToggleText'); | |
| const registerFields = document.getElementById('registerFields'); | |
| const newChatBtn = document.getElementById('newChatBtn'); | |
| 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'); | |
| // User management | |
| let isRegistering = false; | |
| function updateAuthUI() { | |
| const loggedInUser = localStorage.getItem('chatRouterUser'); | |
| if (loggedInUser) { | |
| authBtn.innerHTML = `<i data-feather="log-out" class="w-5 h-5"></i><span>Logout</span>`; | |
| authBtn.dataset.state = 'logout'; | |
| document.querySelector('#newChatBtn').disabled = false; | |
| document.querySelector('#settingsBtn').disabled = false; | |
| sendBtn.disabled = false; | |
| } else { | |
| authBtn.innerHTML = `<i data-feather="user" class="w-5 h-5"></i><span>Login</span>`; | |
| authBtn.dataset.state = 'login'; | |
| document.querySelector('#newChatBtn').disabled = true; | |
| document.querySelector('#settingsBtn').disabled = true; | |
| sendBtn.disabled = true; | |
| showAuthModal(); | |
| } | |
| feather.replace(); | |
| } | |
| function showAuthModal(register = false) { | |
| isRegistering = register; | |
| authModalTitle.textContent = register ? 'Register' : 'Login'; | |
| authActionBtn.textContent = register ? 'Register' : 'Login'; | |
| authToggleText.textContent = register ? 'Already have an account? ' : 'Don\'t have an account? '; | |
| authToggleBtn.textContent = register ? 'Login' : 'Register'; | |
| registerFields.classList.toggle('hidden', !register); | |
| authModal.classList.remove('hidden'); | |
| } | |
| // Chat management | |
| let currentChatId = null; | |
| function generateChatId() { | |
| return 'chat_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5); | |
| } | |
| function loginUser(username, password) { | |
| // Simple auth - store user in localStorage | |
| localStorage.setItem('chatRouterUser', username); | |
| localStorage.setItem(`chatRouterPass_${username}`, password); // Not secure for production! | |
| updateAuthUI(); | |
| // Create a new chat session | |
| createNewChat(); | |
| // Load user's chat list | |
| loadChatList(username); | |
| } | |
| function logoutUser() { | |
| const username = localStorage.getItem('chatRouterUser'); | |
| localStorage.removeItem('chatRouterUser'); | |
| updateAuthUI(); | |
| // Clear current chat and sidebar | |
| chatContainer.innerHTML = ''; | |
| chatList.innerHTML = ''; | |
| currentChatId = null; | |
| addMessage('system', 'Please log in to start chatting.'); | |
| } | |
| function createNewChat() { | |
| const username = localStorage.getItem('chatRouterUser'); | |
| if (!username) return; | |
| currentChatId = generateChatId(); | |
| chatContainer.innerHTML = ''; | |
| addMessage('system', 'π New chat started.'); | |
| saveChatHistory(username, currentChatId); | |
| addChatToList(username, currentChatId, 'New Chat'); | |
| } | |
| function loadChatHistory(username, chatId) { | |
| const history = localStorage.getItem(`chatHistory_${username}_${chatId}`); | |
| if (history) { | |
| chatContainer.innerHTML = history; | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| currentChatId = chatId; | |
| } | |
| } | |
| function saveChatHistory(username, chatId) { | |
| if (!chatId) chatId = currentChatId; | |
| if (!username || !chatId) return; | |
| localStorage.setItem(`chatHistory_${username}_${chatId}`, chatContainer.innerHTML); | |
| // Update the chat title if it's the first message | |
| const messages = chatContainer.querySelectorAll('.message-bubble'); | |
| if (messages.length === 1) { | |
| updateChatTitle(username, chatId, messages[0].textContent.substring(0, 30)); | |
| } | |
| } | |
| function loadChatList(username) { | |
| chatList.innerHTML = ''; | |
| const chats = []; | |
| // Find all chats for this user | |
| for (let i = 0; i < localStorage.length; i++) { | |
| const key = localStorage.key(i); | |
| if (key.startsWith(`chatHistory_${username}_`)) { | |
| const chatId = key.split('_')[2]; | |
| const title = localStorage.getItem(`chatTitle_${username}_${chatId}`) || 'New Chat'; | |
| chats.push({ id: chatId, title }); | |
| } | |
| } | |
| // Sort by most recent first | |
| chats.sort((a, b) => b.id.localeCompare(a.id)); | |
| // Add to sidebar | |
| chats.forEach(chat => { | |
| addChatToList(username, chat.id, chat.title); | |
| }); | |
| } | |
| function addChatToList(username, chatId, title) { | |
| const chatItem = document.createElement('div'); | |
| chatItem.className = 'flex justify-between items-center p-2 hover:bg-gray-700 rounded-lg cursor-pointer'; | |
| chatItem.dataset.chatId = chatId; | |
| const titleSpan = document.createElement('span'); | |
| titleSpan.className = 'truncate flex-1'; | |
| titleSpan.textContent = title; | |
| const actionsDiv = document.createElement('div'); | |
| actionsDiv.className = 'flex space-x-2'; | |
| const renameBtn = document.createElement('button'); | |
| renameBtn.className = 'text-gray-400 hover:text-white'; | |
| renameBtn.innerHTML = '<i data-feather="edit-2" class="w-4 h-4"></i>'; | |
| renameBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| renameChat(username, chatId, titleSpan); | |
| }; | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.className = 'text-gray-400 hover:text-white'; | |
| deleteBtn.innerHTML = '<i data-feather="trash-2" class="w-4 h-4"></i>'; | |
| deleteBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| deleteChat(username, chatId, chatItem); | |
| }; | |
| actionsDiv.appendChild(renameBtn); | |
| actionsDiv.appendChild(deleteBtn); | |
| chatItem.appendChild(titleSpan); | |
| chatItem.appendChild(actionsDiv); | |
| chatItem.addEventListener('click', () => { | |
| loadChatHistory(username, chatId); | |
| }); | |
| chatList.prepend(chatItem); | |
| feather.replace(); | |
| } | |
| function updateChatTitle(username, chatId, title) { | |
| localStorage.setItem(`chatTitle_${username}_${chatId}`, title); | |
| const chatItem = chatList.querySelector(`[data-chat-id="${chatId}"]`); | |
| if (chatItem) { | |
| chatItem.querySelector('span').textContent = title; | |
| } | |
| } | |
| function renameChat(username, chatId, titleElement) { | |
| const newTitle = prompt('Enter new chat title:', titleElement.textContent); | |
| if (newTitle && newTitle.trim()) { | |
| localStorage.setItem(`chatTitle_${username}_${chatId}`, newTitle.trim()); | |
| titleElement.textContent = newTitle.trim(); | |
| } | |
| } | |
| function deleteChat(username, chatId, element) { | |
| if (confirm('Are you sure you want to delete this chat?')) { | |
| localStorage.removeItem(`chatHistory_${username}_${chatId}`); | |
| localStorage.removeItem(`chatTitle_${username}_${chatId}`); | |
| element.remove(); | |
| if (currentChatId === chatId) { | |
| createNewChat(); | |
| } | |
| } | |
| } | |
| // 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`; | |
| } | |
| // Free models from OpenRouter | |
| const freeModels = [ | |
| 'alibaba/tongyi-deepresearch-30b-a3b:free', | |
| 'nvidia/nemotron-nano-9b-v2:free', | |
| 'openai/gpt-oss-20b:free', | |
| 'z-ai/glm-4.5-air:free', | |
| 'tngtech/deepseek-r1t2-chimera:free', | |
| 'mistralai/mistral-small-3.2-24b-instruct:free', | |
| 'moonshotai/kimi-dev-72b:free', | |
| 'mistralai/devstral-small-2505:free', | |
| 'google/gemma-3n-e4b-it:free', | |
| 'meta-llama/llama-3.3-8b-instruct:free', | |
| 'qwen/qwen3-4b:free', | |
| 'qwen/qwen3-30b-a3b:free', | |
| 'qwen/qwen3-8b:free', | |
| 'qwen/qwen3-14b:free', | |
| 'qwen/qwen3-235b-a22b:free', | |
| 'tngtech/deepseek-r1t-chimera:free', | |
| 'microsoft/mai-ds-r1:free', | |
| 'shisa-ai/shisa-v2-llama3.3-70b:free', | |
| 'meta-llama/llama-4-maverick:free', | |
| 'meta-llama/llama-4-scout:free', | |
| 'qwen/qwen2.5-vl-32b-instruct:free', | |
| 'mistralai/mistral-small-3.1-24b-instruct:free', | |
| 'google/gemma-3-4b-it:free', | |
| 'google/gemma-3-12b-it:free', | |
| 'google/gemma-3-27b-it:free', | |
| 'nousresearch/deephermes-3-llama-3-8b-preview:free', | |
| 'cognitivecomputations/dolphin3.0-mistral-24b:free', | |
| 'qwen/qwen2.5-vl-72b-instruct:free', | |
| 'mistralai/mistral-small-24b-instruct-2501:free', | |
| 'deepseek/deepseek-r1-distill-llama-70b:free', | |
| 'meta-llama/llama-3.3-70b-instruct:free', | |
| 'meta-llama/llama-3.2-3b-instruct:free', | |
| 'qwen/qwen-2.5-72b-instruct:free', | |
| 'mistralai/mistral-nemo:free', | |
| 'google/gemma-2-9b-it:free' | |
| ]; | |
| // Fetch available free models | |
| async function fetchModels(apiKey) { | |
| try { | |
| // 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 free models | |
| freeModels.forEach(modelId => { | |
| const option = document.createElement('option'); | |
| option.value = modelId; | |
| option.textContent = modelId; | |
| modelSelect.appendChild(option); | |
| }); | |
| // Verify API key is still needed for these free models | |
| if (!apiKey) { | |
| addMessage('system', 'Free models selected. Some features may require an API key.'); | |
| } | |
| } 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; | |
| // Save to user's chat history | |
| const username = localStorage.getItem('chatRouterUser'); | |
| if (username) { | |
| saveChatHistory(username); | |
| } | |
| } | |
| // Handle API errors gracefully | |
| function handleApiError(error) { | |
| const errorMsg = error.message.toLowerCase(); | |
| if (errorMsg.includes('payment') || errorMsg.includes('unauthorized')) { | |
| addMessage('system', 'β οΈ This model requires a paid OpenRouter plan. Please select a free model to continue.'); | |
| } else { | |
| addMessage('system', `Error: ${error.message}`); | |
| } | |
| } | |
| // 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 | |
| authBtn.addEventListener('click', () => { | |
| if (authBtn.dataset.state === 'logout') { | |
| logoutUser(); | |
| } else { | |
| showAuthModal(); | |
| } | |
| }); | |
| closeAuth.addEventListener('click', () => { | |
| authModal.classList.add('hidden'); | |
| }); | |
| authToggleBtn.addEventListener('click', () => { | |
| showAuthModal(!isRegistering); | |
| }); | |
| authActionBtn.addEventListener('click', () => { | |
| if (isRegistering) { | |
| const username = regUsername.value.trim(); | |
| const password = authPassword.value.trim(); | |
| if (!username || !password) { | |
| alert('Please enter both username and password'); | |
| return; | |
| } | |
| if (localStorage.getItem(`chatRouterPass_${username}`)) { | |
| alert('Username already exists'); | |
| return; | |
| } | |
| loginUser(username, password); | |
| } else { | |
| const username = regUsername.value.trim(); | |
| const password = authPassword.value.trim(); | |
| const storedPass = localStorage.getItem(`chatRouterPass_${username}`); | |
| if (!storedPass || storedPass !== password) { | |
| alert('Invalid username or password'); | |
| return; | |
| } | |
| loginUser(username, password); | |
| } | |
| authModal.classList.add('hidden'); | |
| }); | |
| newChatBtn.addEventListener('click', createNewChat); | |
| newSidebarChat.addEventListener('click', createNewChat); | |
| // Sidebar toggle for mobile | |
| sidebarToggle.addEventListener('click', () => { | |
| sidebar.classList.toggle('hidden'); | |
| }); | |
| messageInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| sendBtn.addEventListener('click', () => { | |
| const username = localStorage.getItem('chatRouterUser'); | |
| if (username) { | |
| sendMessage(); | |
| saveChatHistory(username, currentChatId); | |
| } | |
| }); | |
| // Initialize | |
| loadSettings(); | |
| updateAuthUI(); | |
| // Welcome message | |
| setTimeout(() => { | |
| if (!localStorage.getItem('chatRouterUser')) { | |
| addMessage('system', 'Welcome to ChatRouter! Please log in to start chatting.'); | |
| } else if (!localStorage.getItem('chatRouterModel')) { | |
| addMessage('system', 'Please select a free model from settings to begin.'); | |
| } else if (!currentChatId) { | |
| createNewChat(); | |
| } | |
| }, 1000); | |
| // Close sidebar when clicking outside on mobile | |
| document.addEventListener('click', (e) => { | |
| if (window.innerWidth < 768 && !sidebar.contains(e.target) && e.target !== sidebarToggle) { | |
| sidebar.classList.add('hidden'); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |