document.addEventListener('DOMContentLoaded', function() { // DOM Elements const chatForm = document.getElementById('chat-form'); const messageInput = document.getElementById('message-input'); const messagesContainer = document.getElementById('messages-container'); const newChatBtn = document.getElementById('new-chat-btn'); const modelSelect = document.getElementById('model-select'); const currentModelLabel = document.getElementById('current-model-label'); const historyList = document.getElementById('history-list'); // State variables let chatHistory = []; let conversations = loadConversations(); let currentConversationId = null; // Load available models loadModels(); // Start a new conversation startNewConversation(); // Event listeners chatForm.addEventListener('submit', handleChatSubmit); newChatBtn.addEventListener('click', startNewConversation); modelSelect.addEventListener('change', handleModelChange); messageInput.addEventListener('keydown', handleInputKeydown); // Auto-resize textarea as user types messageInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; // Cap the height if (parseInt(this.style.height) > 120) { this.style.height = '120px'; } }); // Load available models from the backend function loadModels() { fetch('/api/models') .then(response => response.json()) .then(data => { if (data.status === 'success') { modelSelect.innerHTML = ''; data.models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.name; modelSelect.appendChild(option); }); } }) .catch(error => { console.error('Error loading models:', error); }); } // Handle model change function handleModelChange() { const selectedModel = modelSelect.value; const selectedModelName = modelSelect.options[modelSelect.selectedIndex].text; currentModelLabel.textContent = selectedModelName; // Update current conversation model if (currentConversationId) { conversations[currentConversationId].model = selectedModel; saveConversations(); } } // Handle chat submission function handleChatSubmit(e) { e.preventDefault(); const message = messageInput.value.trim(); if (!message) return; // Add user message to UI addMessageToUI('user', message); // Add to chat history chatHistory.push({ role: 'user', content: message }); // Update conversation title if it's the first message if (chatHistory.length === 1) { const title = message.substring(0, 30) + (message.length > 30 ? '...' : ''); conversations[currentConversationId].title = title; updateConversationsList(); } // Save to local storage conversations[currentConversationId].messages = chatHistory; saveConversations(); // Clear input messageInput.value = ''; messageInput.style.height = 'auto'; // Show typing indicator showTypingIndicator(); // Send to backend const selectedModel = modelSelect.value; fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: chatHistory, model: selectedModel }) }) .then(response => response.json()) .then(data => { // Remove typing indicator hideTypingIndicator(); if (data.status === 'success') { // Add AI response to UI addMessageToUI('ai', data.message); // Add to chat history chatHistory.push({ role: 'assistant', content: data.message }); // Save to local storage conversations[currentConversationId].messages = chatHistory; saveConversations(); // Scroll to bottom scrollToBottom(); } else { // Show error addMessageToUI('system', `Error: ${data.message}`); } }) .catch(error => { hideTypingIndicator(); console.error('Error:', error); addMessageToUI('system', `Error: ${error.message || 'Failed to send message'}`); }); // Scroll to bottom scrollToBottom(); } // Handle input keydown (for Enter key submission with Shift+Enter for new line) function handleInputKeydown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); chatForm.dispatchEvent(new Event('submit')); } } // Add message to UI function addMessageToUI(sender, content) { // Create a wrapper for each message group let messageWrapper; if (sender === 'system') { messageWrapper = document.createElement('div'); messageWrapper.className = 'w-100 d-flex justify-content-center my-3'; const messageDiv = document.createElement('div'); messageDiv.className = 'system-message text-center'; messageDiv.innerHTML = `
${content}
`; messageWrapper.appendChild(messageDiv); } else { messageWrapper = document.createElement('div'); messageWrapper.className = 'w-100 d-flex ' + (sender === 'user' ? 'justify-content-end' : 'justify-content-start'); const messageDiv = renderMessage(content, sender === 'user'); messageWrapper.appendChild(messageDiv); } messagesContainer.appendChild(messageWrapper); scrollToBottom(); } // Show typing indicator function showTypingIndicator() { const typingWrapper = document.createElement('div'); typingWrapper.className = 'w-100 d-flex justify-content-start'; typingWrapper.id = 'typing-indicator-wrapper'; const typingDiv = document.createElement('div'); typingDiv.className = 'typing-indicator ai-message'; typingDiv.id = 'typing-indicator'; typingDiv.innerHTML = ` `; typingWrapper.appendChild(typingDiv); messagesContainer.appendChild(typingWrapper); scrollToBottom(); } // Hide typing indicator function hideTypingIndicator() { const typingWrapper = document.getElementById('typing-indicator-wrapper'); if (typingWrapper) { typingWrapper.remove(); } } // Scroll to bottom of messages container function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Start a new conversation function startNewConversation() { // Clear chat history chatHistory = []; // Clear messages container messagesContainer.innerHTML = ` `; // Create a new conversation ID currentConversationId = Date.now().toString(); // Add to conversations object conversations[currentConversationId] = { id: currentConversationId, title: 'New Conversation', model: modelSelect.value, messages: [] }; // Save to local storage saveConversations(); // Update UI updateConversationsList(); } // Load conversation by ID function loadConversation(id) { if (!conversations[id]) return; // Set current conversation ID currentConversationId = id; // Load chat history chatHistory = conversations[id].messages || []; // Set model if (conversations[id].model) { modelSelect.value = conversations[id].model; const selectedModelName = modelSelect.options[modelSelect.selectedIndex].text; currentModelLabel.textContent = selectedModelName; } // Clear messages container messagesContainer.innerHTML = ''; // Add messages to UI if (chatHistory.length === 0) { messagesContainer.innerHTML = ` `; } else { chatHistory.forEach(msg => { if (msg.role === 'user') { addMessageToUI('user', msg.content); } else if (msg.role === 'assistant') { addMessageToUI('ai', msg.content); } else if (msg.role === 'system') { addMessageToUI('system', msg.content); } }); } // Update UI updateConversationsList(); } // Update conversations list in sidebar function updateConversationsList() { historyList.innerHTML = ''; // Sort conversations by ID (newest first) const sortedIds = Object.keys(conversations).sort((a, b) => b - a); sortedIds.forEach(id => { const conv = conversations[id]; const item = document.createElement('li'); item.className = `list-group-item history-item d-flex justify-content-between align-items-center ${id === currentConversationId ? 'active' : ''}`; const titleSpan = document.createElement('span'); titleSpan.textContent = conv.title; titleSpan.style.cursor = 'pointer'; titleSpan.addEventListener('click', () => { loadConversation(id); }); const deleteBtn = document.createElement('button'); deleteBtn.className = 'btn btn-sm btn-danger'; deleteBtn.innerHTML = ''; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); deleteConversation(id); }); item.appendChild(titleSpan); item.appendChild(deleteBtn); item.dataset.id = id; historyList.appendChild(item); }); } // Load conversations from local storage function loadConversations() { try { const saved = localStorage.getItem('g4f_conversations'); return saved ? JSON.parse(saved) : {}; } catch (error) { console.error('Error loading conversations:', error); return {}; } } // Save conversations to local storage function saveConversations() { try { localStorage.setItem('g4f_conversations', JSON.stringify(conversations)); } catch (error) { console.error('Error saving conversations:', error); } } // Delete specific conversation function deleteConversation(id) { if (confirm('Are you sure you want to delete this conversation? This cannot be undone.')) { fetch(`/api/conversations/${id}`, { method: 'DELETE', }) .then(response => response.json()) .then(data => { if (data.status === 'success') { delete conversations[id]; // If current conversation was deleted, start a new one if (id === currentConversationId) { startNewConversation(); } saveConversations(); updateConversationsList(); addMessageToUI('system', 'Conversation deleted'); } }) .catch(error => { console.error('Error:', error); addMessageToUI('system', 'Failed to delete conversation'); }); } } function renderMessage(content, isUser = false) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${isUser ? 'user-message' : 'ai-message'}`; if (!isUser) { const formattedContent = marked.parse(content); messageDiv.innerHTML = formattedContent; // Add copy buttons and language labels to code blocks messageDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); const pre = block.parentElement; const language = block.className.split('-')[1] || 'plaintext'; pre.setAttribute('data-language', language); const copyBtn = document.createElement('button'); copyBtn.className = 'copy-btn'; copyBtn.innerHTML = ''; copyBtn.onclick = async () => { await navigator.clipboard.writeText(block.textContent); copyBtn.innerHTML = ''; setTimeout(() => { copyBtn.innerHTML = ''; }, 2000); }; pre.appendChild(copyBtn); }); } else { messageDiv.textContent = content; } return messageDiv; } });