CoolFace
Apppublic

jsy09/simple-ai-chatbot-1ohrt

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
script.js497 linesDownload Raw Back to root
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, '&amp;')207        .replace(/</g, '&lt;')208        .replace(/>/g, '&gt;')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});