| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>OpenRouter Chat Interface</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| } |
| .typing-indicator { |
| display: inline-flex; |
| align-items: center; |
| } |
| .typing-dot { |
| width: 8px; |
| height: 8px; |
| margin: 0 2px; |
| background-color: #9CA3AF; |
| border-radius: 50%; |
| animation: pulse 1.5s infinite ease-in-out; |
| } |
| .typing-dot:nth-child(1) { animation-delay: 0s; } |
| .typing-dot:nth-child(2) { animation-delay: 0.3s; } |
| .typing-dot:nth-child(3) { animation-delay: 0.6s; } |
| .chat-container { |
| height: calc(100vh - 180px); |
| } |
| .message-transition { |
| transition: all 0.3s ease; |
| } |
| .model-selector:hover .model-dropdown { |
| display: block; |
| } |
| .scrollbar-thin::-webkit-scrollbar { |
| width: 4px; |
| } |
| .scrollbar-thin::-webkit-scrollbar-track { |
| background: #f1f1f1; |
| } |
| .scrollbar-thin::-webkit-scrollbar-thumb { |
| background: #888; |
| border-radius: 2px; |
| } |
| .scrollbar-thin::-webkit-scrollbar-thumb:hover { |
| background: #555; |
| } |
| |
| .markdown-response strong { |
| font-weight: bold; |
| } |
| .markdown-response em { |
| font-style: italic; |
| } |
| .markdown-response code { |
| background-color: #f3f4f6; |
| padding: 0.2em 0.4em; |
| border-radius: 3px; |
| font-family: monospace; |
| } |
| .markdown-response pre { |
| background-color: #f3f4f6; |
| padding: 1em; |
| border-radius: 4px; |
| overflow-x: auto; |
| margin: 0.5em 0; |
| } |
| .markdown-response ul, |
| .markdown-response ol { |
| padding-left: 1.5em; |
| margin: 0.5em 0; |
| } |
| .markdown-response ul { |
| list-style-type: disc; |
| } |
| .markdown-response ol { |
| list-style-type: decimal; |
| } |
| |
| .model-category { |
| padding: 8px 12px; |
| font-size: 0.75rem; |
| font-weight: 600; |
| color: #6b7280; |
| background-color: #f9fafb; |
| border-bottom: 1px solid #e5e7eb; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| } |
| |
| .model-popular { |
| position: relative; |
| } |
| |
| .model-popular::after { |
| content: "★ Popular"; |
| position: absolute; |
| right: 10px; |
| top: 10px; |
| font-size: 0.65rem; |
| background-color: #fef3c7; |
| color: #92400e; |
| padding: 2px 4px; |
| border-radius: 4px; |
| } |
| |
| .model-new::after { |
| content: "🆕 New"; |
| position: absolute; |
| right: 10px; |
| top: 10px; |
| font-size: 0.65rem; |
| background-color: #dbeafe; |
| color: #1e40af; |
| padding: 2px 4px; |
| border-radius: 4px; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 font-sans"> |
| <div class="container mx-auto max-w-4xl p-4"> |
| |
| <header class="bg-white rounded-lg shadow-md p-4 mb-4 flex justify-between items-center"> |
| <div class="flex items-center"> |
| <i class="fas fa-robot text-2xl text-indigo-600 mr-3"></i> |
| <h1 class="text-xl font-bold text-gray-800">OpenRouter Chat</h1> |
| </div> |
| <div class="relative model-selector"> |
| <button id="modelButton" class="flex items-center bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-medium py-2 px-4 rounded-lg transition"> |
| <span id="selectedModel">GPT-4</span> |
| <i class="fas fa-chevron-down ml-2 text-sm"></i> |
| </button> |
| <div class="model-dropdown hidden absolute right-0 mt-2 w-96 bg-white rounded-md shadow-lg z-10 border border-gray-200"> |
| <div class="p-2 border-b border-gray-200"> |
| <input type="text" id="modelSearch", placeholder="Search 100+ models...", class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
| </div> |
| <div class="overflow-y-auto max-h-96 scrollbar-thin" id="modelList"> |
| |
| </div> |
| <div class="p-2 bg-gray-50 text-xs text-gray-500 border-t border-gray-200 flex justify-between"> |
| <span id="modelCount">Loading models...</span> |
| <span>OpenRouter.ai</span> |
| </div> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <div class="chat-container bg-white rounded-lg shadow-md p-4 mb-4 overflow-y-auto scrollbar-thin"> |
| <div id="chatMessages" class="space-y-4"> |
| |
| <div class="message-transition flex justify-start"> |
| <div class="flex-shrink-0 h-10 w-10 rounded-full bg-indigo-100 flex items-center justify center"> |
| <i class="fas fa-robot text-indigo-600"></i> |
| </div> |
| <div class="ml-3"> |
| <div class="bg-gray-100 rounded-lg py-2 px-4 inline-block"> |
| <p class="text-gray-800">Hello! I'm your AI assistant powered by <strong>OpenRouter</strong>. Here's what you can do:</p> |
| <ul class="list-disc pl-5 mt-2 space-y-1"> |
| <li>Select different AI models from the dropdown above</li> |
| <li>Search through 100+ available models</li> |
| <li>Your messages will be sent to the selected model</li> |
| <li>All interaction happens through the OpenRouter API</li> |
| </ul> |
| <p class="mt-2 text-gray-800">Please enter your <strong>OpenRouter API Key</strong> in the settings to get started.</p> |
| </div> |
| <p class="text-xs text-gray-500 mt-1">Today at <span id="currentTime"></span></p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg shadow-md p-4"> |
| <div class="flex items-center"> |
| <textarea id="userInput" rows="1" class="flex-grow border border-gray-300 rounded-l-lg py-2 px-4 focus:outline-none focus:ring-2 focus:ring-indigo-500 resize-none" placeholder="Type your message here..."></textarea> |
| <button id="sendButton" class="bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-r-lg transition h-10 flex items-center justify-center"> |
| <i class="fas fa-paper-plane"></i> |
| </button> |
| </div> |
| <div class="flex justify-between items-center mt-2"> |
| <div class="text-xs text-gray-500"> |
| <span id="charCount">0</span>/1000 |
| </div> |
| <div class="flex space-x-2"> |
| <button id="settingsButton" class="text-gray-500 hover:text-indigo-600 transition"> |
| <i class="fas fa-cog"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="settingsModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50 hidden"> |
| <div class="bg-white rounded-lg p-6 w-96"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="text-lg font-semibold">Settings</h3> |
| <button id="closeSettings" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div class="space-y-4"> |
| <div> |
| <label for="apiKey" class="block text-sm font-medium text-gray-700 mb-1">OpenRouter API Key</label> |
| <input type="password" id="apiKey" placeholder="sk-or-XXXXXXXXXXXXXXXXXXXXXXXX", class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
| </div> |
| <div> |
| <label class="inline-flex items-center"> |
| <input type="checkbox" id="saveKey" checked class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"> |
| <span class="ml-2 text-sm text-gray-600">Save API Key (uses localStorage)</span> |
| </label> |
| </div> |
| <div> |
| <label for="temperature" class="block text-sm font-medium text-gray-700 mb-1">Temperature: <span id="tempValue">0.7</span></label> |
| <input type="range" id="temperature", min="0", max="2", step="0.1", value="0.7", class="w-full"> |
| </div> |
| </div> |
| <div class="mt-6 flex justify-end space-x-3"> |
| <button id="saveSettings" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition"> |
| Save |
| </button> |
| <button id="cancelSettings" class="bg-gray-200 text-gray-800 px-4 py-2 rounded-md hover:bg-gray-300 transition"> |
| Cancel |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const openRouterModels = [ |
| |
| { id: 'openai/gpt-4', name: 'GPT-4', provider: 'OpenAI', category: 'popular' }, |
| { id: 'openai/gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'OpenAI', category: 'openai' }, |
| { id: 'openai/gpt-4-32k', name: 'GPT-4 32k', provider: 'OpenAI', category: 'openai' }, |
| { id: 'openai/gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'OpenAI', category: 'openai' }, |
| { id: 'openai/gpt-3.5-turbo-16k', name: 'GPT-3.5 Turbo 16k', provider: 'OpenAI', category: 'openai' }, |
| |
| |
| { id: 'anthropic/claude-2', name: 'Claude 2', provider: 'Anthropic', category: 'popular' }, |
| { id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', provider: 'Anthropic', category: 'anthropic' }, |
| { id: 'anthropic/claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'Anthropic', category: 'anthropic', tags: ['new'] }, |
| { id: 'anthropic/claude-3-haiku', name: 'Claude 3 Haiku', provider: 'Anthropic', category: 'anthropic', tags: ['new'] }, |
| { id: 'anthropic/claude-2.1', name: 'Claude 2.1', provider: 'Anthropic', category: 'anthropic' }, |
| { id: 'anthropic/claude-instant-v1', name: 'Claude Instant', provider: 'Anthropic', category: 'anthropic' }, |
| |
| |
| { id: 'google/gemini-pro', name: 'Gemini Pro', provider: 'Google', category: 'google' }, |
| { id: 'google/palm-2-chat-bison', name: 'PaLM 2 Chat', provider: 'Google', category: 'google' }, |
| { id: 'google/palm-2-codechat-bison', name: 'PaLM 2 Code Chat', provider: 'Google', category: 'google' }, |
| { id: 'google/gemma-7b-it', name: 'Gemma 7B Instruct', provider: 'Google', category: 'google', tags: ['new'] }, |
| { id: 'google/gemma-2b-it', name: 'Gemma 2B Instruct', provider: 'Google', category: 'google', tags: ['new'] }, |
| { id: 'google/gemma-3-2b-instruct', name: 'Gemma 3 2B Instruct', provider: 'Google', category: 'google', tags: ['new'] }, |
| { id: 'google/gemma-3-7b-instruct', name: 'Gemma 3 7B Instruct', provider: 'Google', category: 'google', tags: ['new'] }, |
| { id: 'google/gemma-3-22b-instruct', name: 'Gemma 3 22B Instruct', provider: 'Google', category: 'google', tags: ['new'] }, |
| |
| |
| { id: 'meta-llama/llama-2-70b-chat', name: 'Llama 2 70B', provider: 'Meta', category: 'popular' }, |
| { id: 'meta-llama/llama-2-13b-chat', name: 'Llama 2 13B', provider: 'Meta', category: 'meta' }, |
| { id: 'meta-llama/llama-2-7b-chat', name: 'Llama 2 7B', provider: 'Meta', category: 'meta' }, |
| { id: 'meta-llama/codellama-34b-instruct', name: 'CodeLlama 34B', provider: 'Meta', category: 'meta' }, |
| |
| |
| { id: 'mistralai/mistral-7b-instruct', name: 'Mistral 7B', provider: 'Mistral', category: 'popular' }, |
| { id: 'mistralai/mixtral-8x7b-instruct', name: 'Mixtral 8x7B', provider: 'Mistral', category: 'mistral' }, |
| { id: 'mistralai/mistral-medium', name: 'Mistral Medium', provider: 'Mistral', category: 'mistral' }, |
| |
| |
| { id: 'nousresearch/nous-hermes-2-mixtral-8x7b-dpo', name: 'Hermes 2 Mixtral', provider: 'Nous', category: 'other' }, |
| { id: 'openchat/openchat-7b', name: 'OpenChat 7B', provider: 'OpenChat', category: 'other' }, |
| { id: 'phind/phind-codellama-34b', name: 'Phind CodeLlama', provider: 'Phind', category: 'other' }, |
| { id: 'intel/neural-chat-7b', name: 'Neural Chat 7B', provider: 'Intel', category: 'other' }, |
| { id: 'upstage/solar-10.7b-instruct-v1.0', name: 'Solar 10.7B', provider: 'Upstage', category: 'other' }, |
| { id: 'jondurbin/airoboros-l2-70b-2.2.1', name: 'Airoboros L2 70B', provider: 'Nous', category: 'other' }, |
| { id: 'gryphe/mythomax-l2-13b', name: 'MythoMax L2 13B', provider: 'Gryphe', category: 'other' }, |
| { id: 'undi95/remm-slerp-l2-13b', name: 'ReMM L2 13B', provider: 'Undi', category: 'other' }, |
| { id: 'migtissera/synthia-70b-v1.2b', name: 'Synthia 70B', provider: 'Migtissera', category: 'other' }, |
| { id: 'pygmalionai/mythalion-13b', name: 'Mythalion 13B', provider: 'Pygmalion', category: 'other' }, |
| |
| |
| { id: 'anthropic/claude-v2:enterprise', name: 'Claude Enterprise', provider: 'Anthropic', category: 'enterprise' }, |
| { id: 'cohere/command-nightly', name: 'Command', provider: 'Cohere', category: 'enterprise' }, |
| |
| |
| { id: 'deepseek-ai/deepseek-coder-33b-instruct', name: 'DeepSeek Coder 33B', provider: 'DeepSeek', category: 'coding' }, |
| { id: 'bigcode/starcoder', name: 'StarCoder', provider: 'BigCode', category: 'coding' }, |
| { id: 'codellama/codellama-34b-instruct', name: 'CodeLlama 34B (Fireworks)', provider: 'Meta', category: 'coding' }, |
| |
| |
| { id: 'lizpreciatior/lzlv-70b-fp16-hf', name: 'LZLV 70B', provider: 'Preciatior', category: 'creative' }, |
| { id: 'togethercomputer/alpaca-7b', name: 'Alpaca 7B', provider: 'Together', category: 'creative' }, |
| ]; |
| |
| |
| const categoryNames = { |
| 'popular': '⭐ Popular Models', |
| 'openai': 'OpenAI', |
| 'anthropic': 'Anthropic', |
| 'google': 'Google', |
| 'meta': 'Meta', |
| 'mistral': 'Mistral', |
| 'other': 'Other Models', |
| 'enterprise': 'Enterprise Models', |
| 'coding': 'Coding Models', |
| 'creative': 'Creative Writing' |
| }; |
| |
| |
| const app = { |
| currentModel: 'openai/gpt-4', |
| apiKey: null, |
| temperature: 0.7, |
| conversationHistory: [], |
| |
| init: function() { |
| |
| const now = new Date(); |
| const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); |
| document.getElementById('currentTime').textContent = timeString; |
| |
| |
| this.loadSettings(); |
| |
| |
| this.initModelSelection(); |
| this.initChat(); |
| this.initSettings(); |
| |
| |
| document.getElementById('userInput').focus(); |
| }, |
| |
| loadSettings: function() { |
| |
| if (localStorage.getItem('openRouterApiKey')) { |
| this.apiKey = localStorage.getItem('openRouterApiKey'); |
| document.getElementById('apiKey').value = this.apiKey; |
| } |
| |
| |
| if (localStorage.getItem('openRouterTemperature')) { |
| this.temperature = parseFloat(localStorage.getItem('openRouterTemperature')); |
| document.getElementById('temperature').value = this.temperature; |
| document.getElementById('tempValue').textContent = this.temperature.toFixed(1); |
| } |
| |
| |
| if (localStorage.getItem('openRouterSaveKey') === 'true') { |
| document.getElementById('saveKey').checked = true; |
| } |
| }, |
| |
| initModelSelection: function() { |
| const modelButton = document.getElementById('modelButton'); |
| const modelDropdown = document.querySelector('.model-dropdown'); |
| const selectedModel = document.getElementById('selectedModel'); |
| const modelSearch = document.getElementById('modelSearch'); |
| const modelList = document.getElementById('modelList'); |
| const modelCount = document.getElementById('modelCount'); |
| |
| |
| const groupedModels = {}; |
| openRouterModels.forEach(model => { |
| if (!groupedModels[model.category]) { |
| groupedModels[model.category] = []; |
| } |
| groupedModels[model.category].push(model); |
| }); |
| |
| |
| const categoryOrder = ['popular', 'openai', 'anthropic', 'google', 'meta', 'mistral', 'coding', 'creative', 'enterprise', 'other']; |
| const sortedCategories = categoryOrder.filter(cat => groupedModels[cat]); |
| |
| |
| let modelListHTML = ''; |
| sortedCategories.forEach(category => { |
| modelListHTML += `<div class="model-category">${categoryNames[category]}</div>`; |
| groupedModels[category].forEach(model => { |
| const providerColor = { |
| 'OpenAI': 'indigo', |
| 'Anthropic': 'purple', |
| 'Google': 'blue', |
| 'Meta': 'orange', |
| 'Mistral': 'green', |
| 'Nous': 'yellow', |
| 'OpenChat': 'cyan', |
| 'Phind': 'pink', |
| 'Intel': 'blue', |
| 'Upstage': 'gray', |
| 'DeepSeek': 'teal', |
| 'BigCode': 'indigo', |
| 'Pygmalion': 'fuchsia', |
| 'Undi': 'violet', |
| 'Gryphe': 'amber', |
| 'Preciatior': 'rose', |
| 'Migtissera': 'sky', |
| 'Together': 'emerald' |
| }[model.provider] || 'gray'; |
| |
| const tagsHTML = model.tags?.includes('new') ? |
| '<span class="absolute right-2 top-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">New</span>' : ''; |
| |
| const isPopular = model.category === 'popular' ? |
| '<span class="absolute right-2 top-2 text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Popular</span>' : ''; |
| |
| modelListHTML += ` |
| <div class="model-option relative py-2 px-4 hover:bg-indigo-50 cursor-pointer" data-model="${model.id}"> |
| ${tagsHTML} |
| ${isPopular} |
| <div class="flex justify-between items-center"> |
| <span>${model.name}</span> |
| <span class="text-xs bg-${providerColor}-100 text-${providerColor}-800 px-2 py-1 rounded">${model.provider}</span> |
| </div> |
| </div> |
| `; |
| }); |
| }); |
| |
| modelList.innerHTML = modelListHTML; |
| modelCount.textContent = `${openRouterModels.length} models available`; |
| |
| |
| const modelOptions = document.querySelectorAll('.model-option'); |
| |
| modelButton.addEventListener('click', function(e) { |
| e.stopPropagation(); |
| modelDropdown.classList.toggle('hidden'); |
| if (!modelDropdown.classList.contains('hidden')) { |
| modelSearch.focus(); |
| } |
| }); |
| |
| modelOptions.forEach(option => { |
| option.addEventListener('click', function() { |
| const modelName = this.getAttribute('data-model'); |
| const selectedModelObj = openRouterModels.find(m => m.id === modelName); |
| app.currentModel = modelName; |
| selectedModel.textContent = selectedModelObj.name; |
| modelDropdown.classList.add('hidden'); |
| |
| |
| app.addSystemMessage(`Changed model to: ${selectedModelObj.name} (${selectedModelObj.provider})`); |
| }); |
| }); |
| |
| |
| modelSearch.addEventListener('input', function() { |
| const searchTerm = this.value.toLowerCase(); |
| const options = modelList.querySelectorAll('.model-option, .model-category'); |
| |
| if (searchTerm.trim() === '') { |
| |
| const categories = modelList.querySelectorAll('.model-category'); |
| categories.forEach(cat => cat.style.display = 'block'); |
| |
| modelList.querySelectorAll('.model-option').forEach(opt => { |
| opt.style.display = 'block'; |
| }); |
| return; |
| } |
| |
| let visibleCount = 0; |
| let lastVisibleCategory = null; |
| |
| options.forEach(option => { |
| if (option.classList.contains('model-category')) { |
| return; |
| } |
| |
| const modelName = option.textContent.toLowerCase(); |
| const modelId = option.getAttribute('data-model'); |
| const modelObj = openRouterModels.find(m => m.id === modelId); |
| |
| if (modelName.includes(searchTerm) || |
| modelObj.provider.toLowerCase().includes(searchTerm) || |
| modelObj.id.toLowerCase().includes(searchTerm)) { |
| |
| option.style.display = 'block'; |
| visibleCount++; |
| |
| |
| const category = modelObj.category; |
| const categoryHeading = Array.from(modelList.querySelectorAll('.model-category')) |
| .find(cat => cat.textContent === categoryNames[category]); |
| |
| if (categoryHeading && categoryHeading.style.display !== 'block') { |
| categoryHeading.style.display = 'block'; |
| lastVisibleCategory = categoryHeading; |
| } |
| } else { |
| option.style.display = 'none'; |
| } |
| }); |
| |
| |
| modelList.querySelectorAll('.model-category').forEach(cat => { |
| let hasVisible = false; |
| let nextElement = cat.nextElementSibling; |
| |
| while (nextElement && !nextElement.classList.contains('model-category')) { |
| if (nextElement.style.display === 'block') { |
| hasVisible = true; |
| break; |
| } |
| nextElement = nextElement.nextElementSibling; |
| } |
| |
| cat.style.display = hasVisible ? 'block' : 'none'; |
| }); |
| |
| |
| modelCount.textContent = `${visibleCount} of ${openRouterModels.length} models`; |
| |
| |
| if (searchTerm.trim() !== '') { |
| const firstVisible = modelList.querySelector('.model-option[style="display: block;"]'); |
| if (firstVisible) { |
| firstVisible.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
| } |
| } |
| }); |
| |
| |
| document.addEventListener('click', function(event) { |
| if (!modelButton.contains(event.target) && !modelDropdown.contains(event.target)) { |
| modelDropdown.classList.add('hidden'); |
| } |
| }); |
| }, |
| |
| initChat: function() { |
| const userInput = document.getElementById('userInput'); |
| const sendButton = document.getElementById('sendButton'); |
| const chatMessages = document.getElementById('chatMessages'); |
| const charCount = document.getElementById('charCount'); |
| |
| userInput.addEventListener('input', function() { |
| charCount.textContent = this.value.length; |
| }); |
| |
| sendButton.addEventListener('click', function() { |
| const message = userInput.value.trim(); |
| if (message) { |
| if (!app.apiKey) { |
| app.addSystemMessage('Please enter your OpenRouter API Key in settings first.'); |
| document.getElementById('settingsButton').click(); |
| return; |
| } |
| |
| app.addMessage(message, true); |
| userInput.value = ''; |
| charCount.textContent = '0'; |
| |
| const typingIndicator = app.showTypingIndicator(); |
| |
| |
| app.callOpenRouter(message, typingIndicator); |
| } |
| }); |
| |
| userInput.addEventListener('keypress', function(e) { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| sendButton.click(); |
| } |
| }); |
| |
| |
| userInput.addEventListener('input', function() { |
| this.style.height = 'auto'; |
| this.style.height = (this.scrollHeight) + 'px'; |
| }); |
| }, |
| |
| initSettings: function() { |
| const settingsModal = document.getElementById('settingsModal'); |
| const settingsButton = document.getElementById('settingsButton'); |
| const closeSettings = document.getElementById('closeSettings'); |
| const saveSettings = document.getElementById('saveSettings'); |
| const cancelSettings = document.getElementById('cancelSettings'); |
| const temperatureSlider = document.getElementById('temperature'); |
| const tempValue = document.getElementById('tempValue'); |
| |
| |
| settingsButton.addEventListener('click', function() { |
| settingsModal.classList.remove('hidden'); |
| }); |
| |
| |
| closeSettings.addEventListener('click', function() { |
| settingsModal.classList.add('hidden'); |
| }); |
| |
| cancelSettings.addEventListener('click', function() { |
| settingsModal.classList.add('hidden'); |
| }); |
| |
| |
| saveSettings.addEventListener('click', function() { |
| const apiKey = document.getElementById('apiKey').value.trim(); |
| const saveKey = document.getElementById('saveKey').checked; |
| |
| if (!apiKey) { |
| alert('Please enter your OpenRouter API Key'); |
| return; |
| } |
| |
| app.apiKey = apiKey; |
| app.temperature = parseFloat(temperatureSlider.value); |
| |
| if (saveKey) { |
| localStorage.setItem('openRouterApiKey', apiKey); |
| localStorage.setItem('openRouterSaveKey', 'true'); |
| } else { |
| localStorage.removeItem('openRouterApiKey'); |
| localStorage.removeItem('openRouterSaveKey'); |
| } |
| |
| localStorage.setItem('openRouterTemperature', app.temperature); |
| |
| settingsModal.classList.add('hidden'); |
| app.addSystemMessage('Settings saved successfully.'); |
| }); |
| |
| |
| temperatureSlider.addEventListener('input', function() { |
| tempValue.textContent = this.value; |
| }); |
| }, |
| |
| addMessage: function(content, isUser) { |
| const chatMessages = document.getElementById('chatMessages'); |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `message-transition flex ${isUser ? 'justify-end' : 'justify-start'}`; |
| |
| if (isUser) { |
| messageDiv.innerHTML = ` |
| <div class="mr-3"> |
| <div class="bg-indigo-600 text-white rounded-lg py-2 px-4 inline-block max-w-[90%]"> |
| <p>${content}</p> |
| </div> |
| <p class="text-xs text-gray-500 mt-1 text-right">Just now</p> |
| </div> |
| <div class="flex-shrink-0 h-10 w-10 rounded-full bg-indigo-600 flex items-center justify-center"> |
| <i class="fas fa-user text-white"></i> |
| </div> |
| `; |
| } else { |
| |
| const mdContent = this.simpleMarkdown(content); |
| |
| messageDiv.innerHTML = ` |
| <div class="flex-shrink-0 h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center"> |
| <i class="fas fa-robot text-indigo-600"></i> |
| </div> |
| <div class="ml-3"> |
| <div class="bg-gray-100 rounded-lg py-2 px-4 inline-block max-w-[90%] markdown-response"> |
| ${mdContent} |
| </div> |
| <p class="text-xs text-gray-500 mt-1">Just now</p> |
| </div> |
| `; |
| } |
| |
| chatMessages.appendChild(messageDiv); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| |
| |
| this.conversationHistory.push({ |
| role: isUser ? 'user' : 'assistant', |
| content: content |
| }); |
| }, |
| |
| addSystemMessage: function(content) { |
| const chatMessages = document.getElementById('chatMessages'); |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = 'message-transition flex justify-start'; |
| |
| messageDiv.innerHTML = ` |
| <div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center"> |
| <i class="fas fa-info-circle text-gray-600"></i> |
| </div> |
| <div class="ml-3"> |
| <div class="bg-gray-200 rounded-lg py-2 px-4 inline-block"> |
| <p class="text-gray-800">${content}</p> |
| </div> |
| <p class="text-xs text-gray-500 mt-1">System</p> |
| </div> |
| `; |
| |
| chatMessages.appendChild(messageDiv); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| }, |
| |
| showTypingIndicator: function() { |
| const chatMessages = document.getElementById('chatMessages'); |
| const typingDiv = document.createElement('div'); |
| typingDiv.className = 'message-transition flex justify-start'; |
| typingDiv.innerHTML = ` |
| <div class="flex-shrink-0 h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center"> |
| <i class="fas fa-robot text-indigo-600"></i> |
| </div> |
| <div class="ml-3"> |
| <div class="bg-gray-100 rounded-lg py-2 px-4 inline-block"> |
| <div class="typing-indicator"> |
| <div class="typing-dot"></div> |
| <div class="typing-dot"></div> |
| <div class="typing-dot"></div> |
| </div> |
| </div> |
| </div> |
| `; |
| chatMessages.appendChild(typingDiv); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| return typingDiv; |
| }, |
| |
| removeTypingIndicator: function(typingDiv) { |
| typingDiv.remove(); |
| }, |
| |
| simpleMarkdown: function(text) { |
| |
| text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); |
| text = text.replace(/\*(.*?)\*/g, '<em>$1</em>'); |
| text = text.replace(/`([^`]+)`/g, '<code>$1</code>'); |
| text = text.replace(/\n/g, '<br>'); |
| |
| |
| text = text.replace(/^\s*\*\s(.*)$/gm, '<li>$1</li>'); |
| text = text.replace(/<li>.*<\/li>/g, function(match) { |
| return '<ul>' + match + '</ul>'; |
| }); |
| |
| return text; |
| }, |
| |
| callOpenRouter: async function(message, typingIndicator) { |
| try { |
| const messages = [...this.conversationHistory]; |
| messages.push({ |
| role: 'user', |
| content: message |
| }); |
| |
| const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${this.apiKey}`, |
| 'Content-Type': 'application/json', |
| 'HTTP-Referer': window.location.href, |
| 'X-Title': 'OpenRouter Chat Interface' |
| }, |
| body: JSON.stringify({ |
| model: this.currentModel, |
| messages: messages, |
| temperature: this.temperature |
| }) |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); |
| } |
| |
| const data = await response.json(); |
| const aiResponse = data.choices[0].message.content; |
| |
| this.removeTypingIndicator(typingIndicator); |
| this.addMessage(aiResponse, false); |
| |
| } catch (error) { |
| console.error('Error calling OpenRouter:', error); |
| this.removeTypingIndicator(typingIndicator); |
| this.addSystemMessage(`Error: ${error.message}`); |
| } |
| } |
| }; |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| app.init(); |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=fmlemos/zeroshot-chatbot-openrouter" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
| </html> |