jsy09/simple-ai-chatbot-1ohrt
0
1// Chat state2let isTyping = false;3let chatHistory = [];4let currentChatId = Date.now();5let currentProvider = localStorage.getItem('api-provider') || 'openai';6 7// Built-in API Keys - Replace these with your actual API keys8const BUILT_IN_KEYS = {9 'openai': 'sk-your-openai-api-key-here', // Replace with your OpenAI API key10 'gemini': 'AIzaSy-your-gemini-api-key-here' // Replace with your Gemini API key11};12 13// Check if a key is valid (not empty and not a placeholder)14function isValidKey(key) {15 if (!key || typeof key !== 'string') return false;16 if (key.includes('your-') || key.includes('your_')) return false;17 if (key.trim() === '') return false;18 return true;19}20 21// DOM Elements22const chatContainer = document.getElementById('chat-container');23const chatForm = document.getElementById('chat-form');24const userInput = document.getElementById('user-input');25const sendBtn = document.getElementById('send-btn');26const sidebar = document.getElementById('sidebar');27const overlay = document.getElementById('overlay');28const apiProviderSelect = document.getElementById('api-provider');29 30// Initialize31document.addEventListener('DOMContentLoaded', () => {32 loadTheme();33 loadApiSettings();34 updateConnectionStatus();35 if (userInput) userInput.focus();36});37 38// API Settings Management39function loadApiSettings() {40 // Load saved provider41 const savedProvider = localStorage.getItem('api-provider') || 'openai';42 currentProvider = savedProvider;43 if (apiProviderSelect) {44 apiProviderSelect.value = savedProvider;45 }46}47 48function switchProvider() {49 const newProvider = apiProviderSelect.value;50 currentProvider = newProvider;51 localStorage.setItem('api-provider', newProvider);52 53 // Update connection status indicator54 updateConnectionStatus();55 56 // Show notification of provider switch57 showProviderNotification(newProvider);58}59 60function updateConnectionStatus() {61 const statusEl = document.getElementById('connection-status');62 const indicatorEl = statusEl?.parentElement?.querySelector('.rounded-full');63 64 if (statusEl) {65 const providerDisplay = currentProvider === 'openai' ? 'OpenAI GPT-4' : 66 currentProvider === 'gemini' ? 'Google Gemini' : 67 'Neural Network';68 statusEl.textContent = `Connected to ${providerDisplay}`;69 }70 71 if (indicatorEl) {72 // Check if key is actually valid73 if (isKeyConfigured()) {74 indicatorEl.classList.remove('bg-red-500', 'bg-yellow-500');75 indicatorEl.classList.add('bg-green-500');76 } else {77 indicatorEl.classList.remove('bg-green-500', 'bg-yellow-500');78 indicatorEl.classList.add('bg-red-500');79 }80 }81}82 83function showProviderNotification(provider) {84 // Create temporary notification85 const notification = document.createElement('div');86 notification.className = 'fixed top-4 right-4 glass-panel px-4 py-3 rounded-xl z-50 message-bubble';87 notification.innerHTML = `88 <div class="flex items-center gap-2 text-sm text-gray-300">89 <i data-lucide="orbit" class="w-4 h-4 text-cyan-400"></i>90 <span>Switched to ${provider === 'openai' ? 'OpenAI GPT-4' : 'Google Gemini'}</span>91 </div>92 `;93 document.body.appendChild(notification);94 lucide.createIcons();95 96 // Remove after 2 seconds97 setTimeout(() => {98 notification.style.opacity = '0';99 notification.style.transform = 'translateY(-10px)';100 notification.style.transition = 'all 0.3s ease';101 setTimeout(() => notification.remove(), 300);102 }, 2000);103}104 105function getApiKey() {106 // Return the built-in key for the current provider107 const provider = currentProvider || 'openai';108 const key = BUILT_IN_KEYS[provider];109 return key || '';110}111 112function isKeyConfigured() {113 const key = getApiKey();114 return isValidKey(key);115}116 117// Auto-resize textarea118function autoResize(textarea) {119 textarea.style.height = 'auto';120 textarea.style.height = Math.min(textarea.scrollHeight, 128) + 'px';121}122 123// Handle Enter key (send on Enter, new line on Shift+Enter)124function handleKeyDown(e) {125 if (e.key === 'Enter' && !e.shiftKey) {126 e.preventDefault();127 if (!isTyping && userInput.value.trim()) {128 chatForm.dispatchEvent(new Event('submit'));129 }130 }131}132 133// Toggle sidebar on mobile134function toggleSidebar() {135 const isClosed = sidebar.classList.contains('-translate-x-full');136 if (isClosed) {137 sidebar.classList.remove('-translate-x-full');138 overlay.classList.remove('hidden');139 } else {140 sidebar.classList.add('-translate-x-full');141 overlay.classList.add('hidden');142 }143}144 145// Toggle dark mode146function toggleDarkMode() {147 document.documentElement.classList.toggle('dark');148 const isDark = document.documentElement.classList.contains('dark');149 localStorage.setItem('theme', isDark ? 'dark' : 'light');150 151 const themeIcon = document.getElementById('theme-icon');152 const themeText = document.getElementById('theme-text');153 if (isDark) {154 themeIcon.setAttribute('data-lucide', 'sun');155 themeText.textContent = 'Light Mode';156 } else {157 themeIcon.setAttribute('data-lucide', 'moon');158 themeText.textContent = 'Dark Mode';159 }160 lucide.createIcons();161}162 163// Load saved theme164function loadTheme() {165 const savedTheme = localStorage.getItem('theme') || 'light';166 if (savedTheme === 'dark') {167 document.documentElement.classList.add('dark');168 document.getElementById('theme-icon').setAttribute('data-lucide', 'sun');169 document.getElementById('theme-text').textContent = 'Light Mode';170 }171}172 173// Add message to chat174function addMessage(text, isUser = false) {175 const messageDiv = document.createElement('div');176 messageDiv.className = `flex ${isUser ? 'justify-end' : 'justify-start'} message-bubble items-end gap-3`;177 178 const avatar = isUser ? 179 `<div class="w-10 h-10 rounded-full bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center order-2 avatar-glow flex-shrink-0"><i data-lucide="user" class="w-5 h-5 text-white"></i></div>` :180 `<div class="w-10 h-10 rounded-full bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center mr-0 avatar-glow-cyan flex-shrink-0"><i data-lucide="bot" class="w-5 h-5 text-white"></i></div>`;181 182 const bubbleClass = isUser ? 183 'user-bubble rounded-2xl rounded-br-sm' : 184 'ai-bubble rounded-2xl rounded-bl-sm';185 186 const bubble = `187 ${!isUser ? avatar : ''}188 <div class="max-w-[85%] sm:max-w-[75%] px-5 py-3.5 ${bubbleClass}">189 <p class="text-sm leading-relaxed whitespace-pre-wrap text-gray-100">${formatMessage(text)}</p>190 </div>191 ${isUser ? avatar : ''}192 `;193 194 messageDiv.innerHTML = bubble;195 chatContainer.appendChild(messageDiv);196 lucide.createIcons();197 scrollToBottom();198 199 // Save to history200 chatHistory.push({ role: isUser ? 'user' : 'assistant', content: text, timestamp: Date.now() });201}202 203// Format message (basic markdown support)204function formatMessage(text) {205 return text206 .replace(/&/g, '&')207 .replace(/</g, '<')208 .replace(/>/g, '>')209 .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')210 .replace(/\*(.*?)\*/g, '<em>$1</em>')211 .replace(/`(.*?)`/g, '<code class="bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded text-sm font-mono">$1</code>')212 .replace(/\n/g, '<br>');213}214 215// Show typing indicator216function showTypingIndicator() {217 const typingDiv = document.createElement('div');218 typingDiv.id = 'typing-indicator';219 typingDiv.className = 'flex justify-start message-bubble items-end gap-3';220 typingDiv.innerHTML = `221 <div class="w-10 h-10 rounded-full bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center avatar-glow-cyan flex-shrink-0"><i data-lucide="bot" class="w-5 h-5 text-white"></i></div>222 <div class="px-5 py-4 rounded-2xl rounded-bl-sm ai-bubble flex items-center gap-2">223 <div class="w-2 h-2 rounded-full typing-dot"></div>224 <div class="w-2 h-2 rounded-full typing-dot"></div>225 <div class="w-2 h-2 rounded-full typing-dot"></div>226 </div>227 `;228 chatContainer.appendChild(typingDiv);229 lucide.createIcons();230 scrollToBottom();231}232 233// Remove typing indicator234function removeTypingIndicator() {235 const indicator = document.getElementById('typing-indicator');236 if (indicator) indicator.remove();237}238 239// Scroll to bottom240function scrollToBottom() {241 chatContainer.scrollTop = chatContainer.scrollHeight;242}243 244// Generate AI response using selected API245async function generateAIResponse(userMessage) {246 const apiKey = getApiKey();247 248 if (!isValidKey(apiKey)) {249 const providerName = currentProvider === 'openai' ? 'OpenAI' : (currentProvider === 'gemini' ? 'Google Gemini' : 'the selected provider');250 return `⚠️ **API Key Not Configured**\n\nPlease add your API key for ${providerName} in the BUILT_IN_KEYS object in script.js.`;251 }252 253 // Show typing for realistic delay254 await new Promise(resolve => setTimeout(resolve, 500));255 256 try {257 if (currentProvider === 'openai') {258 return await callOpenAI(userMessage, apiKey);259 } else if (currentProvider === 'gemini') {260 return await callGemini(userMessage, apiKey);261 }262 } catch (error) {263 console.error('API Error:', error);264 if (error.message.includes('401') || error.message.includes('403')) {265 return "❌ Authentication failed. Please check that your API key is correct and has not expired.";266 } else if (error.message.includes('429')) {267 return "⏳ Rate limit exceeded. Please wait a moment before sending more messages.";268 } else if (error.message.includes('fetch') || error.message.includes('network')) {269 return "🌐 Network error. Please check your internet connection and try again.";270 }271 return `❌ Error: ${error.message}`;272 }273}274 275// OpenAI GPT-4 API Call276async function callOpenAI(userMessage, apiKey) {277 const API_URL = 'https://api.openai.com/v1/chat/completions';278 279 const messages = [280 { role: 'system', content: 'You are a helpful AI assistant. Be concise but thorough in your responses.' },281 ...chatHistory.slice(-20).map(h => ({ role: h.role, content: h.content })),282 { role: 'user', content: userMessage }283 ];284 285 const response = await fetch(API_URL, {286 method: 'POST',287 headers: {288 'Content-Type': 'application/json',289 'Authorization': `Bearer ${apiKey}`290 },291 body: JSON.stringify({292 model: 'gpt-4',293 messages: messages,294 max_tokens: 2000,295 temperature: 0.7296 })297 });298 299 if (!response.ok) {300 const error = await response.json().catch(() => ({}));301 throw new Error(error.error?.message || `HTTP ${response.status}`);302 }303 304 const data = await response.json();305 return data.choices[0].message.content;306}307 308// Google Gemini API Call309async function callGemini(userMessage, apiKey) {310 const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${apiKey}`;311 312 // Format history for Gemini313 const contents = [];314 315 // Add system instruction as first user message (Gemini doesn't have system role)316 if (chatHistory.length === 0) {317 contents.push({318 role: 'user',319 parts: [{ text: 'You are a helpful AI assistant. Be concise but thorough in your responses.' }]320 });321 contents.push({322 role: 'model',323 parts: [{ text: 'Understood. I am a helpful AI assistant.' }]324 });325 }326 327 // Convert chat history to Gemini format328 chatHistory.slice(-20).forEach(h => {329 contents.push({330 role: h.role === 'user' ? 'user' : 'model',331 parts: [{ text: h.content }]332 });333 });334 335 // Add current message336 contents.push({337 role: 'user',338 parts: [{ text: userMessage }]339 });340 341 const response = await fetch(API_URL, {342 method: 'POST',343 headers: {344 'Content-Type': 'application/json'345 },346 body: JSON.stringify({347 contents: contents,348 generationConfig: {349 temperature: 0.7,350 maxOutputTokens: 2048,351 }352 })353 });354 355 if (!response.ok) {356 const error = await response.json().catch(() => ({}));357 throw new Error(error.error?.message || `HTTP ${response.status}`);358 }359 360 const data = await response.json();361 362 if (data.candidates && data.candidates[0] && data.candidates[0].content) {363 return data.candidates[0].content.parts[0].text;364 } else if (data.promptFeedback && data.promptFeedback.blockReason) {365 return "⚠️ Response blocked by safety filters. Please try rephrasing your message.";366 } else {367 throw new Error('Unexpected response format from Gemini API');368 }369}370 371// Handle form submission372chatForm.addEventListener('submit', async (e) => {373 e.preventDefault();374 375 const message = userInput.value.trim();376 if (!message || isTyping) return;377 378 // Check if API key is configured for current provider379 if (!isKeyConfigured()) {380 // Clear welcome screen on first message381 if (chatHistory.length === 0) {382 const welcomeScreen = chatContainer.querySelector('.flex.justify-center');383 if (welcomeScreen) welcomeScreen.remove();384 }385 386 // Add user message387 addMessage(message, true);388 userInput.value = '';389 userInput.style.height = 'auto';390 391 // Show configuration warning with current provider name392 const providerName = currentProvider === 'openai' ? 'OpenAI' : (currentProvider === 'gemini' ? 'Google Gemini' : 'Selected Provider');393 const warningMsg = `⚠️ **API Key Not Configured**\n\nPlease configure your API key for ${providerName} in the code. Edit the \`BUILT_IN_KEYS\` object in \`script.js\` to add your actual API key:\n\n• OpenAI: https://platform.openai.com/api-keys\n• Google Gemini: https://makersuite.google.com/app/apikey\n\nThe keys switch automatically when you change the neural network.`;394 395 addMessage(warningMsg, false);396 397 return;398 }399 400 // Clear welcome screen on first message401 if (chatHistory.length === 0) {402 const welcomeScreen = chatContainer.querySelector('.flex.justify-center');403 if (welcomeScreen) welcomeScreen.remove();404 }405 406 // Add user message407 addMessage(message, true);408 userInput.value = '';409 userInput.style.height = 'auto';410 isTyping = true;411 sendBtn.disabled = true;412 413 // Show typing indicator414 showTypingIndicator();415 416 // Get AI response417 try {418 const response = await generateAIResponse(message);419 removeTypingIndicator();420 addMessage(response, false);421 } catch (error) {422 removeTypingIndicator();423 addMessage("❌ Sorry, something went wrong. Please check your API key and try again.\n\nError: " + error.message, false);424 console.error('Error:', error);425 } finally {426 isTyping = false;427 sendBtn.disabled = false;428 userInput.focus();429 }430});431 432// Quick message buttons with cosmic effect433function sendQuickMessage(text) {434 // Add a brief glow effect to input435 userInput.parentElement.classList.add('pulse-glow');436 setTimeout(() => {437 userInput.parentElement.classList.remove('pulse-glow');438 }, 500);439 440 userInput.value = text;441 chatForm.dispatchEvent(new Event('submit'));442}443 444// Clear chat445function clearChat() {446 if (confirm('Are you sure you want to collapse this dimension?')) {447 chatContainer.innerHTML = `448 <div class="flex justify-center py-12">449 <div class="text-center max-w-lg">450 <div class="w-24 h-24 mx-auto mb-6 relative">451 <div class="absolute inset-0 bg-gradient-to-br from-purple-600 to-cyan-500 rounded-full blur-xl opacity-50 animate-pulse"></div>452 <div class="relative w-full h-full bg-gradient-to-br from-purple-600 to-cyan-500 rounded-full flex items-center justify-center avatar-glow">453 <i data-lucide="sparkles" class="w-12 h-12 text-white"></i>454 </div>455 </div>456 <h2 class="title-font text-4xl font-bold mb-4 glow-text">New Dimension</h2>457 <p class="text-gray-400 mb-8 text-lg">The cosmos awaits your questions. Begin your journey through universal intelligence.</p>458 </div>459 </div>460 `;461 lucide.createIcons();462 chatHistory = [];463 }464}465 466// New chat467function newChat() {468 if (chatHistory.length > 0) {469 // Save current chat to sidebar (simplified)470 const historyContainer = document.getElementById('chat-history');471 const chatPreview = chatHistory[0].content.substring(0, 30) + (chatHistory[0].content.length > 30 ? '...' : '');472 473 const historyItem = document.createElement('div');474 historyItem.className = 'p-3 rounded-lg bg-gray-100 dark:bg-gray-700 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors';475 historyItem.innerHTML = `476 <div class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">477 <i data-lucide="message-square" class="w-4 h-4"></i>478 <span class="truncate">${chatPreview}</span>479 </div>480 `;481 historyContainer.insertBefore(historyItem, historyContainer.children[1]);482 lucide.createIcons();483 }484 485 clearChat();486 if (window.innerWidth < 768) toggleSidebar();487}488 489// Close sidebar when clicking outside on mobile490document.addEventListener('click', (e) => {491 if (window.innerWidth < 768 && 492 !sidebar.contains(e.target) && 493 !e.target.closest('button[onclick="toggleSidebar()"]') &&494 !overlay.classList.contains('hidden')) {495 toggleSidebar();496 }497});