fmlemos's picture
Add 2 files
cd24728 verified
<!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 styling for responses */
.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 -->
<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">
<!-- Models will be dynamically loaded here -->
</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>
<!-- Chat Container -->
<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">
<!-- Welcome message -->
<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>
<!-- Input Area -->
<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>
<!-- Settings Modal -->
<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>
// Comprehensive list of OpenRouter models with categories
const openRouterModels = [
// OpenAI Models
{ 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' },
// Anthropic Models
{ 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' },
// Google Models - including all Gemma 3 models
{ 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'] },
// Meta Models
{ 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' },
// Mistral Models
{ 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' },
// Other Models
{ 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' },
// Enterprise Models
{ id: 'anthropic/claude-v2:enterprise', name: 'Claude Enterprise', provider: 'Anthropic', category: 'enterprise' },
{ id: 'cohere/command-nightly', name: 'Command', provider: 'Cohere', category: 'enterprise' },
// Coding Models
{ 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' },
// Creative Writing Models
{ id: 'lizpreciatior/lzlv-70b-fp16-hf', name: 'LZLV 70B', provider: 'Preciatior', category: 'creative' },
{ id: 'togethercomputer/alpaca-7b', name: 'Alpaca 7B', provider: 'Together', category: 'creative' },
];
// Category names for display
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'
};
// Main application object
const app = {
currentModel: 'openai/gpt-4',
apiKey: null,
temperature: 0.7,
conversationHistory: [],
init: function() {
// Set current time
const now = new Date();
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
document.getElementById('currentTime').textContent = timeString;
// Load saved settings
this.loadSettings();
// Initialize UI components
this.initModelSelection();
this.initChat();
this.initSettings();
// Auto-focus input
document.getElementById('userInput').focus();
},
loadSettings: function() {
// Load API key from localStorage if available
if (localStorage.getItem('openRouterApiKey')) {
this.apiKey = localStorage.getItem('openRouterApiKey');
document.getElementById('apiKey').value = this.apiKey;
}
// Load other settings
if (localStorage.getItem('openRouterTemperature')) {
this.temperature = parseFloat(localStorage.getItem('openRouterTemperature'));
document.getElementById('temperature').value = this.temperature;
document.getElementById('tempValue').textContent = this.temperature.toFixed(1);
}
// Check if saveKey was enabled
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');
// Group models by category
const groupedModels = {};
openRouterModels.forEach(model => {
if (!groupedModels[model.category]) {
groupedModels[model.category] = [];
}
groupedModels[model.category].push(model);
});
// Sort categories in specific order
const categoryOrder = ['popular', 'openai', 'anthropic', 'google', 'meta', 'mistral', 'coding', 'creative', 'enterprise', 'other'];
const sortedCategories = categoryOrder.filter(cat => groupedModels[cat]);
// Render model list
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`;
// Set up model selection
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');
// Notify user of model change
app.addSystemMessage(`Changed model to: ${selectedModelObj.name} (${selectedModelObj.provider})`);
});
});
// Model search functionality
modelSearch.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const options = modelList.querySelectorAll('.model-option, .model-category');
if (searchTerm.trim() === '') {
// Show all models with their original categories
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; // Skip category headings initially
}
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++;
// Show the category heading for this model if not already shown
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';
}
});
// Hide all category headings except those with visible models
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';
});
// Update model count in footer
modelCount.textContent = `${visibleCount} of ${openRouterModels.length} models`;
// Scroll to first result if search isn't empty
if (searchTerm.trim() !== '') {
const firstVisible = modelList.querySelector('.model-option[style="display: block;"]');
if (firstVisible) {
firstVisible.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
});
// Close dropdown when clicking outside
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();
// Call OpenRouter API
app.callOpenRouter(message, typingIndicator);
}
});
userInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendButton.click();
}
});
// Auto-resize textarea
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');
// Show settings modal
settingsButton.addEventListener('click', function() {
settingsModal.classList.remove('hidden');
});
// Close settings modal
closeSettings.addEventListener('click', function() {
settingsModal.classList.add('hidden');
});
cancelSettings.addEventListener('click', function() {
settingsModal.classList.add('hidden');
});
// Save settings
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.');
});
// Update temperature value display
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 {
// Process potential markdown in responses
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;
// Add to conversation history
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) {
// Very basic markdown to HTML conversion
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>');
// Simple list detection
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}`);
}
}
};
// Initialize the app
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>