Spaces:
Running
Running
| 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 = `<p>${content}</p>`; | |
| 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 = ` | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| `; | |
| 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 = ` | |
| <div class="system-message text-center my-5"> | |
| <h3>Welcome to AI Chat</h3> | |
| <p class="text-muted">Ask me anything! I'm powered by g4f and ready to help.</p> | |
| </div> | |
| `; | |
| // 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 = ` | |
| <div class="system-message text-center my-5"> | |
| <h3>Welcome to AI Chat</h3> | |
| <p class="text-muted">Ask me anything! I'm powered by g4f and ready to help.</p> | |
| </div> | |
| `; | |
| } 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 = '<i class="fas fa-trash"></i>'; | |
| 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 = '<i class="fas fa-copy"></i>'; | |
| copyBtn.onclick = async () => { | |
| await navigator.clipboard.writeText(block.textContent); | |
| copyBtn.innerHTML = '<i class="fas fa-check"></i>'; | |
| setTimeout(() => { | |
| copyBtn.innerHTML = '<i class="fas fa-copy"></i>'; | |
| }, 2000); | |
| }; | |
| pre.appendChild(copyBtn); | |
| }); | |
| } else { | |
| messageDiv.textContent = content; | |
| } | |
| return messageDiv; | |
| } | |
| }); |