nexuschat / index.html
kokofixcomputers's picture
undefined - Follow Up Deployment
ef12d89 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NexusChat - Advanced AI Client</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
},
dark: {
800: '#1e293b',
900: '#0f172a',
}
}
}
}
}
</script>
<style>
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Dark mode scrollbar */
.dark ::-webkit-scrollbar-track {
background: #1e293b;
}
.dark ::-webkit-scrollbar-thumb {
background: #64748b;
}
/* Chat message typing animation */
@keyframes typing {
from { width: 0 }
to { width: 100% }
}
.typing-animation {
overflow: hidden;
white-space: nowrap;
animation: typing 2s steps(40, end);
}
/* Smooth transitions */
.transition-all {
transition: all 0.3s ease;
}
/* Markdown styling */
.prose pre {
background-color: rgba(0,0,0,0.05);
padding: 1em;
border-radius: 0.5em;
overflow-x: auto;
margin: 1em 0;
}
.dark .prose pre {
background-color: rgba(255,255,255,0.05);
}
.prose code {
background-color: rgba(0,0,0,0.1);
padding: 0.2em 0.4em;
border-radius: 0.25em;
font-family: monospace;
}
.dark .prose code {
background-color: rgba(255,255,255,0.1);
}
.prose ul, .prose ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
.prose li {
margin: 0.25em 0;
}
.prose table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
.prose th, .prose td {
border: 1px solid rgba(0,0,0,0.1);
padding: 0.5em;
}
.dark .prose th, .dark .prose td {
border-color: rgba(255,255,255,0.1);
}
</style>
</head>
<body class="bg-gray-50 dark:bg-dark-900 text-gray-800 dark:text-gray-200 min-h-screen">
<div class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<div class="w-64 bg-white dark:bg-dark-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
<button id="newChatBtn" class="w-full flex items-center justify-center gap-2 bg-primary-500 hover:bg-primary-600 text-white py-2 px-4 rounded-lg transition-all">
<i class="fas fa-plus"></i>
<span>New Chat</span>
</button>
</div>
<div class="flex-1 overflow-y-auto p-2" id="chatList">
<!-- Chat items will be added here dynamically -->
</div>
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3 mb-4 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-lg transition-all">
<div class="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center text-white">
<i class="fas fa-user"></i>
</div>
<span class="font-medium">User Account</span>
</div>
<div id="settingsBtn" class="flex items-center gap-3 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-lg transition-all">
<div class="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center">
<i class="fas fa-cog"></i>
</div>
<span>Settings</span>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Header -->
<header class="bg-white dark:bg-dark-800 border-b border-gray-200 dark:border-gray-700 p-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<button id="sidebarToggle" class="md:hidden text-gray-500 dark:text-gray-400">
<i class="fas fa-bars text-xl"></i>
</button>
<h1 class="text-xl font-bold">NexusChat</h1>
</div>
<div class="flex items-center gap-4">
<div class="relative">
<button id="modelDropdownBtn" class="flex items-center gap-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 py-2 px-3 rounded-lg transition-all">
<span id="currentModel">GPT-4</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
</div>
<div class="relative">
<button id="chatSettingsBtn" class="flex items-center gap-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 py-2 px-3 rounded-lg transition-all">
<i class="fas fa-cog"></i>
</button>
<div id="modelDropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-dark-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
<div class="p-2">
<div class="text-xs uppercase text-gray-500 dark:text-gray-400 px-2 py-1">Default Models</div>
<div class="model-option cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" data-model="gpt-3.5">GPT-3.5</div>
<div class="model-option cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" data-model="gpt-4">GPT-4</div>
<div class="text-xs uppercase text-gray-500 dark:text-gray-400 px-2 py-1 mt-2">Custom Models</div>
<div id="customModelsList">
<!-- Custom models will be added here -->
</div>
<div class="border-t border-gray-200 dark:border-gray-700 mt-2 pt-2">
<div id="addModelBtn" class="cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex items-center gap-2 text-primary-500">
<i class="fas fa-plus"></i>
<span>Add Custom Model</span>
</div>
</div>
<div class="text-xs uppercase text-gray-500 dark:text-gray-400 px-2 py-1 mt-2">Custom Providers</div>
<div id="customProvidersList">
<!-- Custom providers will be added here -->
</div>
<div class="border-t border-gray-200 dark:border-gray-700 mt-2 pt-2">
<div id="addProviderBtn" class="cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex items-center gap-2 text-primary-500">
<i class="fas fa-plus"></i>
<span>Add Custom Provider</span>
</div>
</div>
</div>
</div>
</div>
<button id="darkModeToggle" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200">
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:block"></i>
</button>
</div>
</header>
<!-- Chat Area -->
<div class="flex-1 overflow-y-auto p-4 bg-gray-50 dark:bg-dark-900" id="chatArea">
<div class="max-w-3xl mx-auto">
<!-- Welcome message - shown when no chat or empty chat -->
<div id="welcomeMessage" class="text-center py-10">
<div class="w-16 h-16 mx-auto mb-4 bg-primary-500 rounded-full flex items-center justify-center text-white">
<i class="fas fa-robot text-2xl"></i>
</div>
<h2 class="text-2xl font-bold mb-2">Welcome to NexusChat</h2>
<p class="text-gray-600 dark:text-gray-400 mb-6">Start a new conversation or select one from your history</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto mb-8">
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-all example-prompt">
<h3 class="font-medium mb-1">Explain quantum computing</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">In simple terms</p>
</div>
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-all example-prompt">
<h3 class="font-medium mb-1">Write a poem</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">About artificial intelligence</p>
</div>
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-all example-prompt">
<h3 class="font-medium mb-1">Plan a trip</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">To Japan for 2 weeks</p>
</div>
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-all example-prompt">
<h3 class="font-medium mb-1">Help me debug</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">This JavaScript code</p>
</div>
</div>
<button id="quickStartBtn" class="bg-primary-500 hover:bg-primary-600 text-white py-2 px-6 rounded-lg transition-all">
Quick Start
</button>
</div>
<!-- Chat messages will be added here dynamically -->
</div>
</div>
<!-- Input Area -->
<div class="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-dark-800">
<div class="max-w-3xl mx-auto">
<form id="messageForm" class="relative">
<textarea id="messageInput" rows="1" class="w-full p-4 pr-16 bg-gray-100 dark:bg-gray-700 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder="Type your message here..."></textarea>
<button type="submit" class="absolute right-4 bottom-4 w-8 h-8 bg-primary-500 hover:bg-primary-600 text-white rounded-full flex items-center justify-center transition-all">
<i class="fas fa-paper-plane"></i>
</button>
</form>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
NexusChat may produce inaccurate information about people, places, or facts.
</div>
</div>
</div>
</div>
</div>
<!-- Add Model Modal -->
<div id="addModelModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold">Add Custom Model</h3>
<button id="closeModelModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<form id="addModelForm">
<div class="mb-4">
<label for="modelName" class="block text-sm font-medium mb-1">Model Name</label>
<input type="text" id="modelName" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="e.g. My Custom Model" required>
</div>
<div class="mb-4">
<label for="modelEndpoint" class="block text-sm font-medium mb-1">API Endpoint</label>
<input type="text" id="modelEndpoint" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="https://api.example.com/v1/chat" required>
</div>
<div class="mb-4">
<label for="modelKey" class="block text-sm font-medium mb-1">API Key (optional)</label>
<input type="password" id="modelKey" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="sk-...">
</div>
<div class="mb-4 flex items-center">
<input type="checkbox" id="modelStreaming" class="w-4 h-4 text-primary-600 rounded border-gray-300 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700">
<label for="modelStreaming" class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Supports Streaming</label>
</div>
<input type="hidden" id="editModelId" value="">
<input type="hidden" id="editProviderId" value="">
<div class="flex justify-end gap-2">
<button type="button" id="cancelAddModel" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded transition-all">
Add Model
</button>
</div>
</form>
</div>
</div>
<!-- API Key Modal -->
<div id="apiKeyModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold">OpenAI API Key Required</h3>
<button id="closeApiKeyModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<p class="mb-4">To use OpenAI models, please enter your API key:</p>
<form id="apiKeyForm">
<div class="mb-4">
<input type="password" id="apiKeyInput" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="sk-..." required>
</div>
<div class="flex justify-end gap-2">
<button type="button" id="cancelApiKey" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded transition-all">
Save Key
</button>
</div>
</form>
</div>
</div>
<!-- Settings Page -->
<div id="settingsPage" class="hidden fixed inset-0 bg-gray-50 dark:bg-dark-900 z-40 flex flex-col">
<div class="max-w-3xl mx-auto w-full p-6 overflow-y-auto">
<div class="sticky top-0 bg-gray-50 dark:bg-dark-900 pt-6 pb-4 z-10">
<div class="flex items-center gap-4 mb-2">
<button id="backToChatBtn" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200">
<i class="fas fa-arrow-left"></i>
</button>
<h2 class="text-2xl font-bold">Settings</h2>
</div>
</div>
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-sm p-6 mb-6">
<h3 class="text-lg font-semibold mb-4">API Configuration</h3>
<form id="apiKeySettingsForm">
<div class="mb-4">
<label for="providerUrl" class="block text-sm font-medium mb-1">Custom Provider URL</label>
<input type="text" id="providerUrl" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="https://example.com/v1" value="">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Base URL for your custom provider</p>
</div>
<div class="mb-4">
<label for="settingsApiKey" class="block text-sm font-medium mb-1">OpenAI API Key</label>
<div class="relative">
<input type="password" id="settingsApiKey" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="sk-..." value="">
<button type="button" id="toggleApiKeyVisibility" class="absolute right-2 top-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-eye"></i>
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Your API key is stored locally in your browser</p>
</div>
<button type="submit" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded transition-all">
Save API Key
</button>
</form>
</div>
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-sm p-6 mb-6">
<h3 class="text-lg font-semibold mb-4">Appearance</h3>
<div class="flex items-center justify-between mb-2">
<span>Dark Mode</span>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="darkModeToggleSetting" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-500"></div>
</label>
</div>
</div>
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-sm p-6 mb-6">
<h3 class="text-lg font-semibold mb-4">Chat Defaults</h3>
<form id="defaultChatSettingsForm">
<div class="mb-4">
<label for="defaultTemperature" class="block text-sm font-medium mb-1">Default Temperature (0-1)</label>
<input type="number" id="defaultTemperature" min="0" max="1" step="0.1"
class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500"
placeholder="0.7" required>
</div>
<div class="mb-4">
<label for="defaultHistoryCount" class="block text-sm font-medium mb-1">Default Message History Count</label>
<input type="number" id="defaultHistoryCount" min="1" max="20"
class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500"
placeholder="5" required>
</div>
<div class="mb-4">
<label for="defaultSystemMessage" class="block text-sm font-medium mb-1">Default System Message</label>
<textarea id="defaultSystemMessage" rows="3"
class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500"
placeholder="You are a helpful assistant..."></textarea>
</div>
<button type="submit" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded transition-all">
Save Defaults
</button>
</form>
</div>
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-sm p-6">
<h3 class="text-lg font-semibold mb-4">About</h3>
<p class="text-sm text-gray-600 dark:text-gray-300 mb-2">NexusChat v1.0.0</p>
<p class="text-sm text-gray-600 dark:text-gray-300">An advanced AI client for modern workflows</p>
</div>
</div>
</div>
<!-- Add Provider Modal -->
<div id="addProviderModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold">Add Custom Provider</h3>
<button id="closeProviderModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<form id="addProviderForm">
<div class="mb-4">
<label for="providerName" class="block text-sm font-medium mb-1">Provider Name</label>
<input type="text" id="providerName" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="e.g. My AI Provider" required>
</div>
<div class="mb-4">
<label for="providerBaseUrl" class="block text-sm font-medium mb-1">Base URL</label>
<input type="text" id="providerBaseUrl" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="https://api.example.com/v1" required>
</div>
<div class="mb-4">
<label for="providerApiKey" class="block text-sm font-medium mb-1">API Key (optional)</label>
<input type="password" id="providerApiKey" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="sk-...">
</div>
<div class="mb-4 flex items-center">
<input type="checkbox" id="providerStreaming" class="w-4 h-4 text-primary-600 rounded border-gray-300 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700">
<label for="providerStreaming" class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Supports Streaming</label>
</div>
<div class="flex justify-end gap-2">
<button type="button" id="cancelAddProvider" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded transition-all">
Add Provider
</button>
</div>
</form>
</div>
</div>
<!-- Provider Models Modal -->
<div id="providerModelsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6 max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold" id="providerModelsTitle">Provider Models</h3>
<button id="closeProviderModelsModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<div id="providerModelsList" class="space-y-2">
<!-- Models will be loaded here -->
</div>
<div class="flex justify-end mt-4">
<button type="button" id="cancelProviderModels" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Close
</button>
</div>
</div>
</div>
<!-- Chat Settings Modal -->
<div id="chatSettingsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold">Chat Settings</h3>
<button id="closeChatSettingsModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<form id="chatSettingsForm">
<div class="mb-4">
<label for="chatTemperature" class="block text-sm font-medium mb-1">Temperature (0-1)</label>
<input type="number" id="chatTemperature" min="0" max="1" step="0.1"
class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500"
placeholder="0.7" required>
</div>
<div class="mb-4">
<label for="chatHistoryCount" class="block text-sm font-medium mb-1">Message History Count</label>
<input type="number" id="chatHistoryCount" min="1" max="20"
class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500"
placeholder="5" required>
</div>
<div class="mb-4">
<label for="chatSystemMessage" class="block text-sm font-medium mb-1">System Message</label>
<textarea id="chatSystemMessage" rows="3"
class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500"
placeholder="You are a helpful assistant..."></textarea>
</div>
<div class="flex justify-end gap-2">
<button type="button" id="cancelChatSettings" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded transition-all">
Save
</button>
</div>
</form>
</div>
</div>
<!-- Delete Chat Confirmation Modal -->
<div id="deleteChatModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold">Delete Chat</h3>
<button id="closeDeleteModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<p class="mb-6">Are you sure you want to delete this chat? This action cannot be undone.</p>
<div class="flex justify-end gap-2">
<button type="button" id="cancelDelete" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button type="button" id="confirmDelete" class="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded transition-all">
Delete
</button>
</div>
</div>
</div>
<script>
// DOM Elements
const apiKeyModal = document.getElementById('apiKeyModal');
const closeApiKeyModal = document.getElementById('closeApiKeyModal');
const cancelApiKey = document.getElementById('cancelApiKey');
const apiKeyForm = document.getElementById('apiKeyForm');
const apiKeyInput = document.getElementById('apiKeyInput');
const sidebarToggle = document.getElementById('sidebarToggle');
const darkModeToggle = document.getElementById('darkModeToggle');
const newChatBtn = document.getElementById('newChatBtn');
const chatList = document.getElementById('chatList');
const chatArea = document.getElementById('chatArea');
const welcomeMessage = document.getElementById('welcomeMessage');
const messageForm = document.getElementById('messageForm');
const messageInput = document.getElementById('messageInput');
const quickStartBtn = document.getElementById('quickStartBtn');
const modelDropdownBtn = document.getElementById('modelDropdownBtn');
const modelDropdown = document.getElementById('modelDropdown');
const currentModel = document.getElementById('currentModel');
const customModelsList = document.getElementById('customModelsList');
const addModelBtn = document.getElementById('addModelBtn');
const addModelModal = document.getElementById('addModelModal');
const closeModelModal = document.getElementById('closeModelModal');
const cancelAddModel = document.getElementById('cancelAddModel');
const addModelForm = document.getElementById('addModelForm');
const deleteChatModal = document.getElementById('deleteChatModal');
const closeDeleteModal = document.getElementById('closeDeleteModal');
const cancelDelete = document.getElementById('cancelDelete');
const confirmDelete = document.getElementById('confirmDelete');
// State
let chats = JSON.parse(localStorage.getItem('chats')) || [];
let currentChatId = null;
let defaultChatSettings = {
temperature: 0.7,
historyCount: 5,
systemMessage: 'You are a helpful assistant.'
};
let models = [
{ name: 'GPT-3.5', id: 'gpt-3.5', type: 'default' },
{ name: 'GPT-4', id: 'gpt-4', type: 'default' }
];
let customModels = JSON.parse(localStorage.getItem('customModels')) || [];
let customProviders = JSON.parse(localStorage.getItem('customProviders')) || [];
let selectedModel = 'gpt-4';
let chatToDelete = null;
let currentProvider = null;
// Initialize
function init() {
// Load default chat settings
const savedDefaults = localStorage.getItem('defaultChatSettings');
if (savedDefaults) {
defaultChatSettings = JSON.parse(savedDefaults);
}
// Load custom models and providers from localStorage
const savedCustomModels = localStorage.getItem('customModels');
if (savedCustomModels) {
customModels = JSON.parse(savedCustomModels);
customModels.forEach(model => {
models.push({
id: model.id,
name: model.name,
endpoint: model.endpoint,
key: model.key,
streaming: model.streaming || false,
type: 'custom'
});
});
}
// Load custom providers
const savedCustomProviders = localStorage.getItem('customProviders');
if (savedCustomProviders) {
customProviders = JSON.parse(savedCustomProviders);
customProviders.forEach(provider => {
models.push({
id: provider.id,
name: provider.name,
type: 'provider',
baseUrl: provider.baseUrl,
apiKey: provider.apiKey,
streaming: provider.streaming || false
});
});
}
// Configure marked.js
marked.setOptions({
breaks: true,
gfm: true,
highlight: function(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
});
// Load custom models
customModels.forEach(model => {
models.push({ ...model, type: 'custom' });
});
// Render chat list
renderChatList();
// Set dark mode if preferred
if (localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
// Set current model
const savedModel = localStorage.getItem('selectedModel');
if (savedModel) {
try {
const modelData = JSON.parse(savedModel);
const model = models.find(m => m.id === modelData.id);
if (model) {
selectedModel = modelData.id;
currentModel.textContent = modelData.name;
// For provider models, ensure the provider is still available
if (modelData.type === 'provider-model') {
const providerId = modelData.id.split('-')[0];
const providerExists = customProviders.some(p => p.id === providerId);
if (!providerExists) {
// Fall back to default if provider is gone
selectModel('gpt-4');
}
}
}
} catch (e) {
// Fallback for old string format
const model = models.find(m => m.id === savedModel);
if (model) {
selectedModel = savedModel;
currentModel.textContent = model.name;
// Migrate old chats to new modelData format
chats.forEach(chat => {
if (!chat.modelData && chat.model) {
chat.modelData = {
id: chat.model,
name: models.find(m => m.id === chat.model)?.name || 'GPT-4'
};
}
});
saveChats();
}
}
}
// Render custom models and providers in dropdown
renderCustomModels();
renderCustomProviders();
}
// Render chat list
function renderChatList() {
chatList.innerHTML = '';
if (chats.length === 0) {
chatList.innerHTML = `
<div class="p-4 text-center text-gray-500 dark:text-gray-400">
No chats yet
</div>
`;
return;
}
chats.forEach(chat => {
// Function to handle edit mode
const setupEditMode = (chatElement, chat) => {
const titleElement = chatElement.querySelector('.chat-title');
const originalTitle = titleElement.textContent;
const input = document.createElement('input');
input.type = 'text';
input.value = originalTitle;
input.className = 'w-full bg-transparent border-b border-blue-500 focus:outline-none';
titleElement.replaceWith(input);
input.focus();
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
finishEdit();
} else if (e.key === 'Escape') {
input.value = originalTitle;
finishEdit();
}
};
const finishEdit = () => {
const newTitle = input.value.trim() || 'New Chat';
chat.title = newTitle;
saveChats();
titleElement.textContent = newTitle;
input.replaceWith(titleElement);
input.removeEventListener('blur', finishEdit);
input.removeEventListener('keydown', handleKeyDown);
};
input.addEventListener('blur', finishEdit);
input.addEventListener('keydown', handleKeyDown);
};
const chatElement = document.createElement('div');
chatElement.className = `group relative flex items-center gap-3 p-2 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-all ${currentChatId === chat.id ? 'bg-gray-100 dark:bg-gray-700' : ''}`;
chatElement.dataset.id = chat.id;
chatElement.innerHTML = `
<div class="w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-500 dark:text-primary-300 flex items-center justify-center">
<i class="fas fa-comment"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-1">
<p class="truncate font-medium chat-title">${chat.title || 'New Chat'}</p>
<button class="edit-chat opacity-0 group-hover:opacity-100 text-gray-400 hover:text-blue-500 transition-all p-1">
<i class="fas fa-edit text-xs"></i>
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">${chat.lastMessage || ''}</p>
</div>
<button class="delete-chat opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 transition-all p-1">
<i class="fas fa-trash text-sm"></i>
</button>
`;
chatElement.addEventListener('click', () => loadChat(chat.id));
const deleteBtn = chatElement.querySelector('.delete-chat');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
showDeleteModal(chat.id);
});
chatList.appendChild(chatElement);
// Add edit button event listener
const editBtn = chatElement.querySelector('.edit-chat');
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
setupEditMode(chatElement, chat);
});
});
}
// Render custom providers in dropdown
function renderCustomProviders() {
customProvidersList.innerHTML = '';
if (customProviders.length === 0) {
customProvidersList.innerHTML = `
<div class="text-center text-gray-500 dark:text-gray-400 py-2 text-sm">
No custom providers added
</div>
`;
return;
}
customProviders.forEach(provider => {
const providerElement = document.createElement('div');
providerElement.className = 'provider-option cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex justify-between items-center';
providerElement.dataset.provider = provider.id;
providerElement.innerHTML = `
<span>${provider.name}</span>
<div class="flex gap-1">
<button class="edit-provider text-gray-400 hover:text-blue-500 transition-all p-1" title="Edit">
<i class="fas fa-edit text-xs"></i>
</button>
<button class="view-provider-models text-gray-400 hover:text-blue-500 transition-all p-1" title="View Models">
<i class="fas fa-list text-xs"></i>
</button>
<button class="delete-provider text-gray-400 hover:text-red-500 transition-all p-1">
<i class="fas fa-trash text-xs"></i>
</button>
</div>
`;
const deleteBtn = providerElement.querySelector('.delete-provider');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteCustomProvider(provider.id);
});
const viewModelsBtn = providerElement.querySelector('.view-provider-models');
viewModelsBtn.addEventListener('click', (e) => {
e.stopPropagation();
showProviderModels(provider);
});
const editBtn = providerElement.querySelector('.edit-provider');
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
showAddProviderModal(provider);
});
customProvidersList.appendChild(providerElement);
});
}
// Render custom models in dropdown
function renderCustomModels() {
customModelsList.innerHTML = '';
// Reload custom models from localStorage to ensure we have latest
const savedCustomModels = localStorage.getItem('customModels');
if (savedCustomModels) {
customModels = JSON.parse(savedCustomModels);
}
if (customModels.length === 0) {
customModelsList.innerHTML = `
<div class="text-center text-gray-500 dark:text-gray-400 py-2 text-sm">
No custom models added
</div>
`;
return;
}
customModels.forEach(model => {
const modelElement = document.createElement('div');
modelElement.className = 'model-option cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex justify-between items-center';
modelElement.dataset.model = model.id;
modelElement.innerHTML = `
<span>${model.name}</span>
<div class="flex gap-1">
<button class="edit-model text-gray-400 hover:text-blue-500 transition-all p-1">
<i class="fas fa-edit text-xs"></i>
</button>
<button class="delete-model text-gray-400 hover:text-red-500 transition-all p-1">
<i class="fas fa-trash text-xs"></i>
</button>
</div>
`;
modelElement.addEventListener('click', (e) => {
if (!e.target.classList.contains('delete-model')) {
selectModel(model.id);
}
});
const deleteBtn = modelElement.querySelector('.delete-model');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteCustomModel(model.id);
});
const editBtn = modelElement.querySelector('.edit-model');
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
showEditModelModal(model);
});
customModelsList.appendChild(modelElement);
});
}
// Show chat settings modal
function showChatSettingsModal() {
if (!currentChatId) return;
const chat = chats.find(c => c.id === currentChatId);
if (!chat) return;
// Initialize settings if they don't exist
if (!chat.settings) {
chat.settings = { ...defaultChatSettings };
}
document.getElementById('chatTemperature').value = chat.settings.temperature;
document.getElementById('chatHistoryCount').value = chat.settings.historyCount;
document.getElementById('chatSystemMessage').value = chat.settings.systemMessage;
chatSettingsModal.classList.remove('hidden');
}
// Hide chat settings modal
function hideChatSettingsModal() {
chatSettingsModal.classList.add('hidden');
}
// Save chat settings
function saveChatSettings() {
if (!currentChatId) return;
const chat = chats.find(c => c.id === currentChatId);
if (!chat) return;
chat.settings = {
temperature: parseFloat(document.getElementById('chatTemperature').value),
historyCount: parseInt(document.getElementById('chatHistoryCount').value),
systemMessage: document.getElementById('chatSystemMessage').value
};
saveChats();
hideChatSettingsModal();
}
// Save default chat settings
function saveDefaultChatSettings() {
defaultChatSettings = {
temperature: parseFloat(document.getElementById('defaultTemperature').value),
historyCount: parseInt(document.getElementById('defaultHistoryCount').value),
systemMessage: document.getElementById('defaultSystemMessage').value
};
localStorage.setItem('defaultChatSettings', JSON.stringify(defaultChatSettings));
// Show success feedback
const submitBtn = document.querySelector('#defaultChatSettingsForm button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.textContent = 'Saved!';
submitBtn.classList.remove('bg-primary-500', 'hover:bg-primary-600');
submitBtn.classList.add('bg-green-500', 'hover:bg-green-600');
setTimeout(() => {
submitBtn.textContent = originalText;
submitBtn.classList.remove('bg-green-500', 'hover:bg-green-600');
submitBtn.classList.add('bg-primary-500', 'hover:bg-primary-600');
}, 2000);
}
// Create new chat
function createNewChat() {
const newChat = {
id: Date.now().toString(),
title: 'New Chat',
model: selectedModel,
messages: [],
lastMessage: '',
createdAt: new Date().toISOString(),
modelData: {
id: selectedModel,
name: models.find(m => m.id === selectedModel)?.name || 'GPT-4'
},
settings: { ...defaultChatSettings }
};
chats.unshift(newChat);
saveChats();
loadChat(newChat.id);
renderChatList();
// Welcome message will be shown automatically since messages array is empty
}
// Load chat
function loadChat(chatId) {
hideSettingsPage();
currentChatId = chatId;
const chat = chats.find(c => c.id === chatId);
if (!chat) {
createNewChat();
return;
}
// Update UI and model selection
document.querySelectorAll('[data-id]').forEach(el => {
el.classList.toggle('bg-gray-100', el.dataset.id === chatId);
el.classList.toggle('dark:bg-gray-700', el.dataset.id === chatId);
});
// Update model selection to match this chat's model
if (chat.modelData) {
selectedModel = chat.modelData.id;
currentModel.textContent = chat.modelData.name;
localStorage.setItem('selectedModel', JSON.stringify(chat.modelData));
}
// Render messages
renderMessages(chat.messages);
// Show welcome message if chat is empty
welcomeMessage.classList.toggle('hidden', chat.messages.length > 0);
}
// Render messages
function renderMessages(messages) {
chatArea.innerHTML = '';
if (messages.length === 0) {
welcomeMessage.classList.remove('hidden');
return;
}
const messagesContainer = document.createElement('div');
messagesContainer.className = 'space-y-6';
messages.forEach((msg, index) => {
const messageElement = document.createElement('div');
messageElement.className = `flex gap-4 ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`;
if (msg.role === 'assistant') {
messageElement.innerHTML = `
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-500 text-white flex items-center justify-center">
<i class="fas fa-robot"></i>
</div>
<div class="max-w-[80%] bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm relative group">
<div class="prose dark:prose-invert max-w-none break-words">${marked.parse(msg.content)}</div>
<div class="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
<button class="message-action bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 p-1 rounded text-gray-700 dark:text-gray-300" data-action="regenerate" title="Regenerate">
<i class="fas fa-sync-alt text-xs"></i>
</button>
<button class="message-action bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 p-1 rounded text-gray-700 dark:text-gray-300" data-action="edit" title="Edit">
<i class="fas fa-edit text-xs"></i>
</button>
</div>
</div>
`;
} else {
messageElement.innerHTML = `
<div class="max-w-[80%] bg-primary-500 text-white rounded-lg p-4 shadow-sm relative group">
<div class="prose prose-white whitespace-pre-wrap">${msg.content.replace(/\n/g, '<br>')}</div>
<div class="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button class="message-action bg-primary-600 hover:bg-primary-700 p-1 rounded text-white" data-action="edit" title="Edit">
<i class="fas fa-edit text-xs"></i>
</button>
</div>
</div>
`;
}
messagesContainer.appendChild(messageElement);
});
chatArea.appendChild(messagesContainer);
chatArea.scrollTop = chatArea.scrollHeight;
}
// Send message
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
// If no chat is selected, create a new one
if (!currentChatId) {
createNewChat();
// Wait a moment for the chat to be created
await new Promise(resolve => setTimeout(resolve, 50));
}
// Get current chat
let chatIndex = chats.findIndex(c => c.id === currentChatId);
if (chatIndex === -1) return;
const chat = chats[chatIndex];
const model = models.find(m => m.id === chat.model);
if (!model) return;
if (model.type === 'default' && !localStorage.getItem('openaiApiKey')) {
showApiKeyModal();
return;
}
// Add user message to chat
const userMessage = {
role: 'user',
content: message,
timestamp: new Date().toISOString()
};
// Ensure we have the latest chat reference
chatIndex = chats.findIndex(c => c.id === currentChatId);
if (chatIndex === -1) return;
let currentChat = chats[chatIndex];
currentChat.messages.push(userMessage);
currentChat.lastMessage = message.length > 30 ? message.substring(0, 30) + '...' : message;
// Save chat immediately after adding user message
chats[chatIndex] = currentChat;
saveChats();
// Render all messages including the new user message
renderMessages(chat.messages);
messageInput.value = '';
chatArea.scrollTop = chatArea.scrollHeight;
// Create assistant message element for streaming/response
const assistantMessageElement = document.createElement('div');
assistantMessageElement.className = 'flex gap-4 justify-start';
assistantMessageElement.innerHTML = `
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-500 text-white flex items-center justify-center">
<i class="fas fa-robot"></i>
</div>
<div class="max-w-[80%] bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm relative group">
<div class="flex gap-2" id="loading-indicator">
<div class="w-2 h-2 rounded-full bg-gray-300 animate-pulse"></div>
<div class="w-2 h-2 rounded-full bg-gray-300 animate-pulse delay-75"></div>
<div class="w-2 h-2 rounded-full bg-gray-300 animate-pulse delay-150"></div>
</div>
<div class="prose dark:prose-invert hidden" id="streaming-content"></div>
</div>
`;
chatArea.appendChild(assistantMessageElement);
const contentElement = assistantMessageElement.querySelector('#streaming-content');
const loadingIndicator = assistantMessageElement.querySelector('#loading-indicator');
try {
// Ensure we have the latest chat reference first
chatIndex = chats.findIndex(c => c.id === currentChatId);
if (chatIndex === -1) throw new Error('Chat not found');
currentChat = chats[chatIndex];
// Get the current model
const model = models.find(m => m.id === currentChat.model);
if (!model) throw new Error('Model not found');
// Prepare messages for API
const messages = currentChat.messages.map(msg => ({
role: msg.role,
content: msg.content
}));
let response;
response = '';
const onStream = (content) => {
response = content;
contentElement.innerHTML = marked.parse(response);
chatArea.scrollTop = chatArea.scrollHeight;
};
if (model.type === 'default') {
// Call OpenAI API
response = await callOpenAIAPI(messages, model.name, null, onStream);
} else if (model.type === 'provider-model') {
// Call provider model API
response = await callProviderModelAPI(messages, model, onStream);
} else {
// Call custom model API
response = await callCustomModelAPI(messages, model, onStream);
}
// Hide loading indicator and show content
loadingIndicator.classList.add('hidden');
contentElement.classList.remove('hidden');
// Ensure we have the latest chat reference
chatIndex = chats.findIndex(c => c.id === currentChatId);
if (chatIndex === -1) return;
currentChat = chats[chatIndex];
// Update the assistant message in the chat
const assistantMessage = {
role: 'assistant',
content: response,
timestamp: new Date().toISOString()
};
// Replace the last message (which was the loading one) with the actual response
currentChat.messages.push(assistantMessage);
saveChats();
// Update chat list
renderChatList();
// Force re-render of messages to ensure UI is in sync
renderMessages(currentChat.messages);
} catch (error) {
console.error('API Error:', error);
// Remove the message element if there was an error
messageElement.remove();
// Show error message
const errorElement = document.createElement('div');
errorElement.className = 'flex gap-4 justify-start';
errorElement.innerHTML = `
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-red-500 text-white flex items-center justify-center">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="max-w-[80%] bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="text-red-500">Error: ${error.message}</div>
</div>
`;
chatArea.appendChild(errorElement);
chatArea.scrollTop = chatArea.scrollHeight;
}
}
// Call OpenAI API
async function callOpenAIAPI(messages, model, apiKey, onStream) {
const isStreaming = model.streaming || false;
// Get current chat settings
const chat = chats.find(c => c.id === currentChatId);
const settings = chat?.settings || defaultChatSettings;
// Limit messages to history count
const limitedMessages = messages.slice(-settings.historyCount);
// Add system message if provided
if (settings.systemMessage) {
limitedMessages.unshift({
role: 'system',
content: settings.systemMessage
});
}
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey || localStorage.getItem('openaiApiKey')}`
},
body: JSON.stringify({
model: model === 'gpt-3.5' ? 'gpt-3.5-turbo' : 'gpt-4',
messages: limitedMessages,
temperature: settings.temperature,
stream: isStreaming
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || 'Failed to call OpenAI API');
}
if (isStreaming && onStream) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content || '';
result += content;
onStream(result);
} catch (e) {
console.error('Error parsing stream data:', e);
}
}
}
}
return result;
} else {
const data = await response.json();
return data.choices[0]?.message?.content || 'No response from model';
}
}
// Call provider model API
async function callProviderModelAPI(messages, model, onStream) {
const endpoint = `${model.baseUrl}/chat/completions`;
if (model.streaming) {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(model.apiKey ? { 'Authorization': `Bearer ${model.apiKey}` } : {})
},
body: JSON.stringify({
model: model.modelName,
messages,
temperature: 0.7,
stream: true
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to call provider model API');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content || '';
result += content;
if (onStream) onStream(result);
} catch (e) {
console.error('Error parsing stream data:', e);
}
}
}
}
return result;
} else {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(model.apiKey ? { 'Authorization': `Bearer ${model.apiKey}` } : {})
},
body: JSON.stringify({
model: model.modelName,
messages,
temperature: 0.7
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to call provider model API');
}
const data = await response.json();
return data.choices?.[0]?.message?.content || data.response || 'No response from model';
}
}
// Call custom model API
async function callCustomModelAPI(messages, model) {
if (model.streaming) {
// Handle streaming response
const response = await fetch(model.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(model.key ? { 'Authorization': `Bearer ${model.key}` } : {})
},
body: JSON.stringify({
model: model.name,
messages,
temperature: 0.7,
stream: true
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to call custom model API');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = '';
// Create message element for streaming
const messageElement = document.createElement('div');
messageElement.className = 'flex gap-4 justify-start';
messageElement.innerHTML = `
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-500 text-white flex items-center justify-center">
<i class="fas fa-robot"></i>
</div>
<div class="max-w-[80%] bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm relative group">
<div class="prose dark:prose-invert" id="streaming-content"></div>
</div>
`;
chatArea.appendChild(messageElement);
const contentElement = messageElement.querySelector('#streaming-content');
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content || '';
result += content;
// Process markdown in streaming response
contentElement.innerHTML = marked.parse(result);
chatArea.scrollTop = chatArea.scrollHeight;
} catch (e) {
console.error('Error parsing stream data:', e);
}
}
}
}
} catch (e) {
console.error('Stream error:', e);
throw e;
}
return result;
} else {
// Handle regular response
const response = await fetch(model.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(model.key ? { 'Authorization': `Bearer ${model.key}` } : {})
},
body: JSON.stringify({
model: model.name,
messages,
temperature: 0.7,
stream: false
})
});
if (!response.ok) {
try {
const errorData = await response.json();
throw new Error(errorData.error?.message || 'Failed to call custom model API');
} catch (e) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to call custom model API');
}
}
try {
const data = await response.json();
return data.choices[0]?.message?.content || data.response || 'No response from model';
} catch (e) {
const text = await response.text();
return text || 'No response from model';
}
}
}
// Select model
function selectModel(modelId) {
// Ensure models array is up to date
const savedCustomModels = localStorage.getItem('customModels');
if (savedCustomModels) {
customModels = JSON.parse(savedCustomModels);
// Update models array with latest custom models
models = models.filter(m => m.type !== 'custom');
customModels.forEach(model => {
models.push({
id: model.id,
name: model.name,
endpoint: model.endpoint,
key: model.key,
streaming: model.streaming || false,
type: 'custom'
});
});
}
const model = models.find(m => m.id === modelId);
if (!model) return;
selectedModel = modelId;
currentModel.textContent = model.name;
modelDropdown.classList.add('hidden');
// Save to localStorage - include both model ID and name for provider models
const modelData = {
id: modelId,
name: model.name,
type: model.type
};
localStorage.setItem('selectedModel', JSON.stringify(modelData));
// Update current chat's model if one is active
if (currentChatId) {
const chat = chats.find(c => c.id === currentChatId);
if (chat) {
chat.model = modelId;
chat.modelData = {
id: modelId,
name: model.name
};
saveChats();
}
}
}
// Add custom model
function addCustomModel(name, endpoint, key, streaming) {
const newModel = {
id: `custom-${Date.now()}`,
name,
endpoint,
key,
streaming: streaming || false,
type: 'custom'
};
customModels.push(newModel);
models.push(newModel);
// Save to localStorage
localStorage.setItem('customModels', JSON.stringify(customModels));
// Update UI
renderCustomModels();
hideAddModelModal();
}
// Edit custom model
function editCustomModel(modelId, name, endpoint, key, streaming) {
const modelIndex = customModels.findIndex(m => m.id === modelId);
if (modelIndex === -1) return;
customModels[modelIndex] = {
...customModels[modelIndex],
name,
endpoint,
key,
streaming: streaming || false
};
// Update in models array
const globalModelIndex = models.findIndex(m => m.id === modelId);
if (globalModelIndex !== -1) {
models[globalModelIndex] = {
...models[globalModelIndex],
name,
endpoint,
key
};
}
// Save to localStorage
localStorage.setItem('customModels', JSON.stringify(customModels));
// Update UI
renderCustomModels();
hideAddModelModal();
}
// Edit custom provider
function editCustomProvider(providerId, name, baseUrl, apiKey, streaming) {
const providerIndex = customProviders.findIndex(p => p.id === providerId);
if (providerIndex === -1) return;
const updatedProvider = {
...customProviders[providerIndex],
name,
baseUrl,
apiKey,
streaming: streaming || false
};
customProviders[providerIndex] = updatedProvider;
// Update in models array
const globalModelIndex = models.findIndex(m => m.id === providerId);
if (globalModelIndex !== -1) {
models[globalModelIndex] = {
...models[globalModelIndex],
name: updatedProvider.name,
baseUrl: updatedProvider.baseUrl,
apiKey: updatedProvider.apiKey,
streaming: updatedProvider.streaming
};
}
// Also update any provider models that use this provider
models.forEach((model, index) => {
if (model.type === 'provider-model' && model.id.startsWith(providerId)) {
models[index] = {
...model,
baseUrl: updatedProvider.baseUrl,
apiKey: updatedProvider.apiKey,
streaming: updatedProvider.streaming
};
}
});
localStorage.setItem('customProviders', JSON.stringify(customProviders));
localStorage.setItem('models', JSON.stringify(models));
renderCustomProviders();
hideAddProviderModal();
}
// Add custom provider
function addCustomProvider(name, baseUrl, apiKey, streaming) {
const newProvider = {
id: `provider-${Date.now()}`,
name,
baseUrl,
apiKey,
streaming: streaming || false
};
customProviders.push(newProvider);
models.push({
id: newProvider.id,
name: newProvider.name,
type: 'provider',
baseUrl: newProvider.baseUrl,
apiKey: newProvider.apiKey,
streaming: newProvider.streaming
});
localStorage.setItem('customProviders', JSON.stringify(customProviders));
localStorage.setItem('models', JSON.stringify(models));
renderCustomProviders();
}
// Delete custom provider
function deleteCustomProvider(providerId) {
customProviders = customProviders.filter(p => p.id !== providerId);
models = models.filter(m => m.id !== providerId);
localStorage.setItem('customProviders', JSON.stringify(customProviders));
renderCustomProviders();
if (selectedModel === providerId) {
selectModel('gpt-4');
}
}
// Show provider models
async function showProviderModels(provider) {
currentProvider = provider;
document.getElementById('providerModelsTitle').textContent = `${provider.name} Models`;
document.getElementById('providerModelsList').innerHTML = '<div class="text-center py-4">Loading models...</div>';
providerModelsModal.classList.remove('hidden');
try {
const response = await fetch(`${provider.baseUrl}/models`, {
headers: {
'Authorization': `Bearer ${provider.apiKey || ''}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch models');
}
const data = await response.json();
const modelsList = document.getElementById('providerModelsList');
modelsList.innerHTML = '';
// OpenAI models endpoint returns { data: [ ... ] }
if (data.data && data.data.length > 0) {
data.data.forEach(modelObj => {
const modelElement = document.createElement('div');
modelElement.className = 'model-option cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded';
modelElement.textContent = modelObj.id;
modelElement.addEventListener('click', () => {
// Create a model entry for this provider model
const providerModel = {
id: `${currentProvider.id}-${modelObj.id}`,
name: `${currentProvider.name} - ${modelObj.id}`,
type: 'provider-model',
baseUrl: currentProvider.baseUrl,
apiKey: currentProvider.apiKey,
modelName: modelObj.id,
streaming: currentProvider.streaming || false
};
// Add or update in models array
const existingIndex = models.findIndex(m => m.id === providerModel.id);
if (existingIndex !== -1) {
models[existingIndex] = providerModel;
} else {
models.push(providerModel);
}
selectModel(providerModel.id);
providerModelsModal.classList.add('hidden');
});
modelsList.appendChild(modelElement);
});
} else {
modelsList.innerHTML = '<div class="text-center py-4 text-gray-500">No models available</div>';
}
} catch (error) {
document.getElementById('providerModelsList').innerHTML = `
<div class="text-center py-4 text-red-500">
Error loading models: ${error.message}
</div>
`;
}
}
// Delete custom model
function deleteCustomModel(modelId) {
customModels = customModels.filter(m => m.id !== modelId);
models = models.filter(m => m.id !== modelId);
// Save to localStorage
localStorage.setItem('customModels', JSON.stringify(customModels));
// Update UI
renderCustomModels();
// If the deleted model was selected, switch to default
if (selectedModel === modelId) {
selectModel('gpt-4');
}
}
// Delete chat
function deleteChat(chatId) {
chats = chats.filter(c => c.id !== chatId);
saveChats();
if (currentChatId === chatId) {
currentChatId = null;
chatArea.innerHTML = '';
welcomeMessage.classList.remove('hidden');
}
renderChatList();
hideDeleteModal();
}
// Show delete modal
function showDeleteModal(chatId) {
chatToDelete = chatId;
deleteChatModal.classList.remove('hidden');
}
// Hide delete modal
function hideDeleteModal() {
deleteChatModal.classList.add('hidden');
chatToDelete = null;
}
// Show add model modal
function showAddModelModal() {
addModelModal.classList.remove('hidden');
}
// Show edit model modal
function showEditModelModal(model) {
document.getElementById('modelName').value = model.name;
document.getElementById('modelEndpoint').value = model.endpoint;
document.getElementById('modelKey').value = model.key || '';
document.getElementById('modelStreaming').checked = model.streaming || false;
document.getElementById('editModelId').value = model.id;
// Change modal title and submit button text
document.querySelector('#addModelModal h3').textContent = 'Edit Custom Model';
document.querySelector('#addModelForm button[type="submit"]').textContent = 'Save Changes';
showAddModelModal();
}
// Hide add model modal
function hideAddModelModal() {
addModelModal.classList.add('hidden');
addModelForm.reset();
// Reset modal title and submit button text
document.querySelector('#addModelModal h3').textContent = 'Add Custom Model';
document.querySelector('#addModelForm button[type="submit"]').textContent = 'Add Model';
}
// Save chats to localStorage
function saveChats() {
localStorage.setItem('chats', JSON.stringify(chats));
}
// Toggle dark mode
function toggleDarkMode() {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', isDark);
// Sync settings toggle if it exists
const darkModeToggle = document.getElementById('darkModeToggleSetting');
if (darkModeToggle) {
darkModeToggle.checked = isDark;
}
}
// Toggle sidebar on mobile
function toggleSidebar() {
document.querySelector('.w-64').classList.toggle('hidden');
document.querySelector('.w-64').classList.toggle('block');
}
// Show API key modal
function showApiKeyModal() {
apiKeyModal.classList.remove('hidden');
}
// Hide API key modal
function hideApiKeyModal() {
apiKeyModal.classList.add('hidden');
apiKeyInput.value = '';
}
// Save API key
function saveApiKey(key) {
localStorage.setItem('openaiApiKey', key);
hideApiKeyModal();
}
// Show add provider modal
function showAddProviderModal(provider = null) {
// Reset form first
const form = document.getElementById('addProviderForm');
form.reset();
document.getElementById('editProviderId').value = '';
if (provider) {
// Editing existing provider
document.getElementById('providerName').value = provider.name;
document.getElementById('providerBaseUrl').value = provider.baseUrl;
document.getElementById('providerApiKey').value = provider.apiKey || '';
document.getElementById('providerStreaming').checked = provider.streaming || false;
document.getElementById('editProviderId').value = provider.id;
// Update UI for edit mode
document.querySelector('#addProviderModal h3').textContent = 'Edit Provider';
document.querySelector('#addProviderForm button[type="submit"]').textContent = 'Save Changes';
} else {
// Creating new provider
document.querySelector('#addProviderModal h3').textContent = 'Add Custom Provider';
document.querySelector('#addProviderForm button[type="submit"]').textContent = 'Add Provider';
document.getElementById('editProviderId').value = '';
}
addProviderModal.classList.remove('hidden');
}
// Hide add provider modal
function hideAddProviderModal() {
addProviderModal.classList.add('hidden');
// Don't reset form here - let the submit handler or showAddProviderModal handle it
}
// Event Listeners
closeApiKeyModal.addEventListener('click', hideApiKeyModal);
// Chat settings events
document.getElementById('chatSettingsBtn').addEventListener('click', showChatSettingsModal);
document.getElementById('closeChatSettingsModal').addEventListener('click', hideChatSettingsModal);
document.getElementById('cancelChatSettings').addEventListener('click', hideChatSettingsModal);
document.getElementById('chatSettingsForm').addEventListener('submit', (e) => {
e.preventDefault();
saveChatSettings();
});
// Default chat settings events
document.getElementById('defaultChatSettingsForm').addEventListener('submit', (e) => {
e.preventDefault();
saveDefaultChatSettings();
});
// Initialize default settings form
document.getElementById('defaultTemperature').value = defaultChatSettings.temperature;
document.getElementById('defaultHistoryCount').value = defaultChatSettings.historyCount;
document.getElementById('defaultSystemMessage').value = defaultChatSettings.systemMessage;
// Provider modal events
document.getElementById('addProviderBtn').addEventListener('click', () => showAddProviderModal(null));
document.getElementById('closeProviderModal').addEventListener('click', hideAddProviderModal);
document.getElementById('cancelAddProvider').addEventListener('click', hideAddProviderModal);
document.getElementById('addProviderForm').addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('providerName').value.trim();
const baseUrl = document.getElementById('providerBaseUrl').value.trim();
const apiKey = document.getElementById('providerApiKey').value.trim();
const streaming = document.getElementById('providerStreaming').checked;
const editProviderId = document.getElementById('editProviderId').value;
if (!name || !baseUrl) {
alert('Provider name and base URL are required');
return;
}
// Clear any previous error state
const errorElements = document.querySelectorAll('.provider-error');
errorElements.forEach(el => el.remove());
// Validate base URL format
try {
new URL(baseUrl);
} catch (e) {
const baseUrlInput = document.getElementById('providerBaseUrl');
const errorElement = document.createElement('p');
errorElement.className = 'text-red-500 text-xs mt-1 provider-error';
errorElement.textContent = 'Please enter a valid URL (e.g. https://api.example.com)';
baseUrlInput.parentNode.appendChild(errorElement);
return;
}
if (editProviderId) {
editCustomProvider(editProviderId, name, baseUrl, apiKey, streaming);
} else {
addCustomProvider(name, baseUrl, apiKey, streaming);
}
});
// Provider models modal events
document.getElementById('closeProviderModelsModal').addEventListener('click', () => {
providerModelsModal.classList.add('hidden');
});
document.getElementById('cancelProviderModels').addEventListener('click', () => {
providerModelsModal.classList.add('hidden');
});
cancelApiKey.addEventListener('click', hideApiKeyModal);
apiKeyForm.addEventListener('submit', (e) => {
e.preventDefault();
const key = apiKeyInput.value.trim();
if (key) {
saveApiKey(key);
// Retry sending the message
setTimeout(() => {
sendMessage();
}, 100);
}
});
sidebarToggle.addEventListener('click', toggleSidebar);
darkModeToggle.addEventListener('click', (e) => {
e.stopPropagation();
toggleDarkMode();
});
newChatBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
hideSettingsPage();
createNewChat();
// Hide welcome message when new chat is created
welcomeMessage.classList.add('hidden');
// Close sidebar on mobile after creating new chat
if (window.innerWidth < 768) {
document.querySelector('.w-64').classList.add('hidden');
}
});
quickStartBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
hideSettingsPage();
createNewChat();
// Hide welcome message when quick start is clicked
welcomeMessage.classList.add('hidden');
// Close sidebar on mobile after quick start
if (window.innerWidth < 768) {
document.querySelector('.w-64').classList.add('hidden');
}
});
messageForm.addEventListener('submit', (e) => {
e.preventDefault();
sendMessage();
});
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
// Check for Ctrl+Enter or Command+Enter (Mac)
if (e.ctrlKey || e.metaKey) {
// Insert new line
const start = messageInput.selectionStart;
const end = messageInput.selectionEnd;
messageInput.value = messageInput.value.substring(0, start) + '\n' + messageInput.value.substring(end);
messageInput.selectionStart = messageInput.selectionEnd = start + 1;
// Prevent default to avoid submitting
e.preventDefault();
} else if (!e.shiftKey) {
// Regular Enter - submit form
e.preventDefault();
sendMessage();
}
}
});
// Auto-resize textarea
messageInput.addEventListener('input', () => {
messageInput.style.height = 'auto';
messageInput.style.height = `${messageInput.scrollHeight}px`;
});
// Model dropdown
modelDropdownBtn.addEventListener('click', (e) => {
e.stopPropagation();
modelDropdown.classList.toggle('hidden');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!modelDropdown.contains(e.target) && e.target !== modelDropdownBtn && !e.target.closest('.model-option')) {
modelDropdown.classList.add('hidden');
}
});
// Handle model selection - delegated event listener
document.addEventListener('click', (e) => {
const modelOption = e.target.closest('.model-option');
if (modelOption && !e.target.classList.contains('delete-model')) {
const modelId = modelOption.dataset.model;
selectModel(modelId);
}
});
// Add model
addModelBtn.addEventListener('click', showAddModelModal);
closeModelModal.addEventListener('click', hideAddModelModal);
cancelAddModel.addEventListener('click', hideAddModelModal);
addModelForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('modelName').value;
const endpoint = document.getElementById('modelEndpoint').value;
const key = document.getElementById('modelKey').value;
const streaming = document.getElementById('modelStreaming').checked;
const editModelId = document.getElementById('editModelId').value;
if (editModelId) {
editCustomModel(editModelId, name, endpoint, key, streaming);
} else {
addCustomModel(name, endpoint, key);
}
});
// Delete chat
closeDeleteModal.addEventListener('click', hideDeleteModal);
cancelDelete.addEventListener('click', hideDeleteModal);
confirmDelete.addEventListener('click', () => {
if (chatToDelete) {
deleteChat(chatToDelete);
}
});
// Settings button
document.getElementById('settingsBtn').addEventListener('click', (e) => {
e.stopPropagation();
showSettingsPage();
});
// Back to chat button
document.getElementById('backToChatBtn').addEventListener('click', (e) => {
e.stopPropagation();
hideSettingsPage();
});
// Show settings page
function showSettingsPage() {
document.getElementById('settingsPage').classList.remove('hidden');
document.body.classList.add('overflow-hidden');
// Load current settings
const apiKey = localStorage.getItem('openaiApiKey') || '';
document.getElementById('settingsApiKey').value = apiKey;
// Set dark mode toggle state
const isDark = document.documentElement.classList.contains('dark');
document.getElementById('darkModeToggleSetting').checked = isDark;
// Update state to remember we're in settings
localStorage.setItem('inSettings', 'true');
}
// Hide settings page
function hideSettingsPage() {
document.getElementById('settingsPage').classList.add('hidden');
document.body.classList.remove('overflow-hidden');
localStorage.removeItem('inSettings');
}
// Check if we should show settings on load
if (localStorage.getItem('inSettings') === 'true') {
showSettingsPage();
}
// Toggle API key visibility
document.getElementById('toggleApiKeyVisibility').addEventListener('click', function() {
const input = document.getElementById('settingsApiKey');
const icon = this.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
input.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
});
// Save API key from settings
document.getElementById('apiKeySettingsForm').addEventListener('submit', function(e) {
e.preventDefault();
const key = document.getElementById('settingsApiKey').value.trim();
saveApiKey(key);
// Show success feedback
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.textContent = 'Saved!';
submitBtn.classList.remove('bg-primary-500', 'hover:bg-primary-600');
submitBtn.classList.add('bg-green-500', 'hover:bg-green-600');
setTimeout(() => {
submitBtn.textContent = originalText;
submitBtn.classList.remove('bg-green-500', 'hover:bg-green-600');
submitBtn.classList.add('bg-primary-500', 'hover:bg-primary-600');
}, 2000);
});
// Dark mode toggle in settings
document.getElementById('darkModeToggleSetting').addEventListener('change', function() {
toggleDarkMode();
});
// Handle message actions
function handleMessageAction(action, messageIndex) {
if (!currentChatId) return;
const chat = chats.find(c => c.id === currentChatId);
if (!chat || !chat.messages[messageIndex]) return;
if (action === 'regenerate') {
// Only regenerate if this is the last assistant message
if (messageIndex === chat.messages.length - 1 &&
chat.messages[messageIndex].role === 'assistant') {
chat.messages.splice(messageIndex, 1);
renderMessages(chat.messages);
// Resend last user message
const lastUserMessage = chat.messages[chat.messages.length - 1];
if (lastUserMessage && lastUserMessage.role === 'user') {
sendMessage(lastUserMessage.content);
}
}
} else if (action === 'edit') {
// TODO: Implement edit functionality
alert('Edit functionality will be implemented here');
}
}
// Initialize the app
init();
// Add delegated event listener for message actions
document.addEventListener('click', (e) => {
const actionBtn = e.target.closest('.message-action');
if (actionBtn) {
e.preventDefault();
e.stopPropagation();
const messageElement = actionBtn.closest('.flex.gap-4');
const messageIndex = Array.from(messageElement.parentNode.children).indexOf(messageElement);
const action = actionBtn.dataset.action;
handleMessageAction(action, messageIndex);
}
});
</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=kokofixcomputers/nexuschat" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>