|
<!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> |