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> | |
| <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-9935854434534629" | |
| crossorigin="anonymous"></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> | |
| </div> | |
| </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>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 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; | |
| // Chat management | |
| let currentChatId = null; | |
| function generateChatId() { | |
| return 'chat_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5); | |
| } | |
| function createNewChat() { | |
| currentChatId = generateChatId(); | |
| chatContainer.innerHTML = ''; | |
| addMessage('system', '🆕 New chat started.'); | |
| } | |
| // Load saved settings | |
| async function loadSettings() { | |
| // Clear any saved API key on load | |
| localStorage.removeItem('chatRouterApiKey'); | |
| const savedModel = localStorage.getItem('chatRouterModel'); | |
| const savedTheme = localStorage.getItem('chatRouterTheme') || 'indigo'; | |
| 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); | |
| // Only save model (API key is not saved) | |
| localStorage.setItem('chatRouterModel', model); | |
| sendBtn.disabled = false; | |
| modelIndicator.textContent = `Using: ${model.split('/').pop()}`; | |
| settingsModal.classList.add('hidden'); | |
| // Add confirmation message to chat | |
| addMessage('system', 'Model selected successfully. You can now start chatting! \n⚠️Warning: If you refresh or reload this page, you will lose your current chat history and need to re-enter your API key.'); | |
| } 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 = apiKeyInput.value.trim(); | |
| const model = localStorage.getItem('chatRouterModel'); | |
| if (!apiKey) { | |
| addMessage('system', 'Please enter your OpenRouter API key in settings.'); | |
| return; | |
| } | |
| if (!model) { | |
| addMessage('system', 'Please select a model in settings.'); | |
| 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 | |
| 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', () => { | |
| sendMessage(); | |
| }); | |
| // Initialize | |
| loadSettings(); | |
| // Welcome message | |
| setTimeout(() => { | |
| if (!localStorage.getItem('chatRouterModel')) { | |
| addMessage('system', 'Welcome to ChatRouter! Please enter your OpenRouter API key and select a model in 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'); | |
| } | |
| }); | |
| // Clear API key input on page refresh | |
| window.addEventListener('beforeunload', () => { | |
| apiKeyInput.value = ''; | |
| }); | |
| </script> | |
| </body> | |
| </html> |