Spaces:
Sleeping
Sleeping
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const chatWindow = document.getElementById('chatWindow'); | |
| const userInput = document.getElementById('userInput'); | |
| const sendButton = document.getElementById('sendButton'); | |
| const clearButton = document.getElementById('clearButton'); | |
| const clearButtonNav = document.getElementById('clearButtonNav'); | |
| const imageInput = document.getElementById('imageInput'); | |
| const uploadImageButton = document.getElementById('uploadImageButton'); | |
| const imagePreviewArea = document.getElementById('imagePreviewArea'); | |
| const imagePreviewContainer = document.getElementById('imagePreviewContainer'); | |
| const chatHistoryList = document.getElementById('chatHistoryList'); | |
| const saveCurrentChatButton = document.getElementById('saveCurrentChatButton'); | |
| const settingsButton = document.getElementById('settingsButtonNav'); | |
| const toggleNavButton = document.getElementById('toggleNavButton'); | |
| const closeSideNavButton = document.getElementById('closeSideNavButton'); | |
| const sideNav = document.getElementById('sideNav'); | |
| const mainContent = document.querySelector('.main-content'); | |
| const welcomeContainer = document.querySelector('.welcome-container'); | |
| const suggestionBubbles = document.querySelectorAll('.suggestion-bubble'); | |
| const offlineIndicator = document.getElementById('offlineIndicator'); | |
| // Templates | |
| const loadingTemplate = document.getElementById('loadingTemplate'); | |
| const userMessageTemplate = document.getElementById('userMessageTemplate'); | |
| const userImageMessageTemplate = document.getElementById('userImageMessageTemplate'); | |
| const botMessageTemplate = document.getElementById('botMessageTemplate'); | |
| const errorMessageTemplate = document.getElementById('errorMessageTemplate'); | |
| // State variables | |
| let chatHistory = []; | |
| let isOffline = !navigator.onLine; // Vérifier l'état de la connexion au chargement | |
| // Gestion de l'état de la connexion pour PWA | |
| function updateOnlineStatus() { | |
| isOffline = !navigator.onLine; | |
| if (isOffline) { | |
| // L'utilisateur est hors ligne | |
| offlineIndicator.classList.add('visible'); | |
| // Désactiver temporairement le bouton d'envoi | |
| sendButton.disabled = true; | |
| sendButton.title = "Hors ligne - Reconnectez-vous pour envoyer des messages"; | |
| } else { | |
| // L'utilisateur est en ligne | |
| offlineIndicator.classList.remove('visible'); | |
| // Réactiver le bouton d'envoi | |
| sendButton.disabled = false; | |
| sendButton.title = "Envoyer"; | |
| } | |
| } | |
| // Installer les événements pour détecter les changements de connectivité | |
| window.addEventListener('online', updateOnlineStatus); | |
| window.addEventListener('offline', updateOnlineStatus); | |
| // Appliquer l'état initial | |
| updateOnlineStatus(); | |
| let selectedImagesData = []; // Pour stocker plusieurs images | |
| let conversationStarted = false; | |
| // --- IMAGE HANDLING --- | |
| // Upload image button click | |
| uploadImageButton.addEventListener('click', function() { | |
| imageInput.click(); | |
| }); | |
| // Image input change - support for multiple images | |
| imageInput.addEventListener('change', function(e) { | |
| const files = Array.from(e.target.files); | |
| if (!files.length) return; | |
| // Clear previous image container | |
| imagePreviewContainer.innerHTML = ''; | |
| selectedImagesData = []; | |
| // Process each file | |
| files.forEach(file => { | |
| // Check file type | |
| if (!file.type.startsWith('image/')) { | |
| alert('Veuillez sélectionner uniquement des fichiers image valides.'); | |
| return; | |
| } | |
| // Check file size (max 5MB) | |
| if (file.size > 5 * 1024 * 1024) { | |
| alert('La taille de chaque image ne doit pas dépasser 5 Mo.'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| // Create preview container | |
| const previewContainer = document.createElement('div'); | |
| previewContainer.className = 'image-preview-container'; | |
| // Create image preview | |
| const imgPreview = document.createElement('img'); | |
| imgPreview.src = event.target.result; | |
| imgPreview.alt = 'Image preview'; | |
| previewContainer.appendChild(imgPreview); | |
| // Create remove button | |
| const removeBtn = document.createElement('button'); | |
| removeBtn.className = 'remove-image-button'; | |
| removeBtn.innerHTML = '<i class="bi bi-x-circle-fill"></i>'; | |
| previewContainer.appendChild(removeBtn); | |
| // Add preview to container | |
| imagePreviewContainer.appendChild(previewContainer); | |
| // Store image data | |
| const imageData = event.target.result; | |
| selectedImagesData.push(imageData); | |
| // Add event listener to remove button | |
| removeBtn.addEventListener('click', () => { | |
| // Remove this specific image | |
| const index = selectedImagesData.indexOf(imageData); | |
| if (index > -1) { | |
| selectedImagesData.splice(index, 1); | |
| } | |
| previewContainer.remove(); | |
| // Hide preview area if no images left | |
| if (selectedImagesData.length === 0) { | |
| imagePreviewArea.classList.add('hidden'); | |
| imageInput.value = ''; | |
| } | |
| }); | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| // Show preview area | |
| imagePreviewArea.classList.remove('hidden'); | |
| // Focus on input for caption | |
| userInput.focus(); | |
| }); | |
| // --- SIDE NAVIGATION HANDLING --- | |
| // Toggle side navigation | |
| toggleNavButton.addEventListener('click', function() { | |
| sideNav.classList.add('active'); | |
| document.body.insertAdjacentHTML('beforeend', '<div class="overlay" id="navOverlay"></div>'); | |
| const overlay = document.getElementById('navOverlay'); | |
| overlay.classList.add('active'); | |
| loadChatHistoryList(); | |
| // Close side nav when clicking overlay | |
| overlay.addEventListener('click', closeSideNav); | |
| }); | |
| // Close side navigation | |
| closeSideNavButton.addEventListener('click', closeSideNav); | |
| function closeSideNav() { | |
| sideNav.classList.remove('active'); | |
| const overlay = document.getElementById('navOverlay'); | |
| if (overlay) { | |
| overlay.remove(); | |
| } | |
| } | |
| // Clear chat from nav button | |
| if (clearButtonNav) { | |
| clearButtonNav.addEventListener('click', function() { | |
| clearChat(true); // With confirmation | |
| closeSideNav(); | |
| }); | |
| } | |
| // Settings button in side nav | |
| if (settingsButtonNav) { | |
| settingsButtonNav.addEventListener('click', function() { | |
| Swal.fire({ | |
| icon: 'info', | |
| title: 'En cours de développement', | |
| text: 'Cette fonctionnalité sera disponible prochainement.' | |
| }); | |
| closeSideNav(); | |
| }); | |
| } | |
| // Load chat history list | |
| function loadChatHistoryList() { | |
| fetch('/api/load-chats') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.error) { | |
| chatHistoryList.innerHTML = `<div class="empty-state">${data.error}</div>`; | |
| return; | |
| } | |
| if (!data.chats || data.chats.length === 0) { | |
| chatHistoryList.innerHTML = '<div class="empty-state">Aucune conversation sauvegardée</div>'; | |
| return; | |
| } | |
| // Create history items | |
| let historyHTML = ''; | |
| data.chats.forEach(chat => { | |
| // Format timestamp (YYYYMMDD_HHMMSS to readable format) | |
| const timestamp = chat.timestamp; | |
| const year = timestamp.substring(0, 4); | |
| const month = timestamp.substring(4, 6); | |
| const day = timestamp.substring(6, 8); | |
| const hour = timestamp.substring(9, 11); | |
| const minute = timestamp.substring(11, 13); | |
| const formattedDate = `${day}/${month}/${year} ${hour}:${minute}`; | |
| historyHTML += ` | |
| <div class="chat-history-item" data-filename="${chat.filename}"> | |
| <div class="icon"><i class="bi bi-chat-dots"></i></div> | |
| <div class="details"> | |
| <div>Conversation</div> | |
| <div class="timestamp">${formattedDate}</div> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| chatHistoryList.innerHTML = historyHTML; | |
| // Add click event to history items | |
| const historyItems = chatHistoryList.querySelectorAll('.chat-history-item'); | |
| historyItems.forEach(item => { | |
| item.addEventListener('click', function() { | |
| const filename = this.getAttribute('data-filename'); | |
| loadChatHistory(filename); | |
| toggleHistoryDropdown(); | |
| }); | |
| }); | |
| }) | |
| .catch(error => { | |
| console.error('Error loading chat history:', error); | |
| chatHistoryList.innerHTML = '<div class="empty-state">Erreur lors du chargement de l\'historique</div>'; | |
| }); | |
| } | |
| // Load specific chat history | |
| function loadChatHistory(filename) { | |
| fetch(`/api/load-chat/${filename}`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.error) { | |
| Swal.fire({ | |
| icon: 'error', | |
| title: 'Erreur', | |
| text: data.error | |
| }); | |
| return; | |
| } | |
| if (data.history) { | |
| // Clear current chat | |
| clearChatContent(); // No confirmation needed | |
| // Load history | |
| chatHistory = data.history; | |
| // Display messages | |
| data.history.forEach(msg => { | |
| if (msg.sender === 'user') { | |
| const messageElement = userMessageTemplate.content.cloneNode(true); | |
| messageElement.querySelector('p').textContent = msg.text; | |
| chatWindow.appendChild(messageElement); | |
| } else { | |
| const messageElement = botMessageTemplate.content.cloneNode(true); | |
| const messageParagraph = messageElement.querySelector('p'); | |
| if (window.marked) { | |
| messageParagraph.innerHTML = marked.parse(msg.text); | |
| } else { | |
| messageParagraph.textContent = msg.text; | |
| } | |
| chatWindow.appendChild(messageElement); | |
| if (window.Prism) { | |
| const codeBlocks = messageParagraph.querySelectorAll('pre code'); | |
| codeBlocks.forEach(block => { | |
| Prism.highlightElement(block); | |
| }); | |
| } | |
| } | |
| }); | |
| // Scroll to bottom | |
| scrollToBottom(); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error loading chat:', error); | |
| Swal.fire({ | |
| icon: 'error', | |
| title: 'Erreur', | |
| text: 'Erreur lors du chargement de la conversation.' | |
| }); | |
| }); | |
| } | |
| // Save current chat | |
| saveCurrentChatButton.addEventListener('click', function() { | |
| if (chatHistory.length <= 1) { | |
| Swal.fire({ | |
| icon: 'info', | |
| title: 'Information', | |
| text: 'Aucune conversation à sauvegarder. Veuillez d\'abord discuter avec le chatbot.' | |
| }); | |
| return; | |
| } | |
| fetch('/api/save-chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| history: chatHistory | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.error) { | |
| Swal.fire({ | |
| icon: 'error', | |
| title: 'Erreur', | |
| text: data.error | |
| }); | |
| } else { | |
| Swal.fire({ | |
| icon: 'success', | |
| title: 'Sauvegardé', | |
| text: 'Conversation sauvegardée avec succès !', | |
| timer: 1500, | |
| showConfirmButton: false | |
| }); | |
| loadChatHistoryList(); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error saving chat:', error); | |
| Swal.fire({ | |
| icon: 'error', | |
| title: 'Erreur', | |
| text: 'Erreur lors de la sauvegarde de la conversation.' | |
| }); | |
| }); | |
| }); | |
| // --- TEXT INPUT HANDLING --- | |
| // Auto-resize textarea based on content | |
| userInput.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = (this.scrollHeight) + 'px'; | |
| // Reset height if empty | |
| if (this.value === '') { | |
| this.style.height = ''; | |
| } | |
| }); | |
| // Send message when Enter key is pressed (without shift) | |
| userInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| // Send message when send button is clicked | |
| sendButton.addEventListener('click', sendMessage); | |
| // Clear chat history when clear button is clicked | |
| if (clearButton) { | |
| clearButton.addEventListener('click', function() { | |
| clearChat(true); // With confirmation | |
| }); | |
| } | |
| // --- CHAT FUNCTIONS --- | |
| // Function to clear the chat history | |
| function clearChat(withConfirmation = true) { | |
| if (!withConfirmation) { | |
| // Clear chat immediately without confirmation | |
| clearChatContent(); | |
| return; | |
| } | |
| if (chatHistory.length > 0) { | |
| Swal.fire({ | |
| title: 'Êtes-vous sûr ?', | |
| text: 'Voulez-vous effacer toute la conversation ?', | |
| icon: 'warning', | |
| showCancelButton: true, | |
| confirmButtonColor: '#3085d6', | |
| cancelButtonColor: '#d33', | |
| confirmButtonText: 'Oui, effacer', | |
| cancelButtonText: 'Annuler' | |
| }).then((result) => { | |
| if (result.isConfirmed) { | |
| clearChatContent(); | |
| } | |
| }); | |
| } | |
| } | |
| // Helper function to clear chat content | |
| function clearChatContent() { | |
| // Clear the chat window except for the welcome message | |
| while (chatWindow.childElementCount > 1) { | |
| chatWindow.removeChild(chatWindow.lastChild); | |
| } | |
| // Reset chat history but keep the welcome message | |
| const welcomeMsg = chatHistory[0]; | |
| chatHistory = welcomeMsg ? [welcomeMsg] : []; | |
| // Show welcome container again | |
| if (welcomeContainer) { | |
| welcomeContainer.style.display = 'flex'; | |
| conversationStarted = false; | |
| } | |
| // Focus on input field | |
| userInput.focus(); | |
| } | |
| // Function to send message | |
| function sendMessage() { | |
| const message = userInput.value.trim(); | |
| // Require either text or images | |
| if (message === '' && selectedImagesData.length === 0) return; | |
| if (selectedImagesData.length > 0) { | |
| // Add user message with images to UI | |
| addUserImageMessage(message, selectedImagesData); // Maintenant on passe toutes les images | |
| } else { | |
| // Add user text message to UI | |
| addUserMessage(message); | |
| } | |
| // Clear input field and reset height | |
| userInput.value = ''; | |
| userInput.style.height = ''; | |
| // Show loading indicator | |
| const loadingElement = addLoadingIndicator(); | |
| // Send message to API | |
| const imagesToSend = selectedImagesData.length > 0 ? selectedImagesData : null; | |
| sendToAPI(message, loadingElement, imagesToSend); | |
| // Clear image previews if any | |
| if (selectedImagesData.length > 0) { | |
| imagePreviewContainer.innerHTML = ''; | |
| imagePreviewArea.classList.add('hidden'); | |
| selectedImagesData = []; | |
| imageInput.value = ''; | |
| } | |
| } | |
| // Add user message to chat window | |
| function addUserMessage(message) { | |
| // Hide welcome container if visible (first message) | |
| if (!conversationStarted && welcomeContainer) { | |
| welcomeContainer.style.display = 'none'; | |
| conversationStarted = true; | |
| } | |
| const messageElement = userMessageTemplate.content.cloneNode(true); | |
| messageElement.querySelector('p').textContent = message; | |
| chatWindow.appendChild(messageElement); | |
| // Add to chat history | |
| chatHistory.push({ | |
| sender: 'user', | |
| text: message | |
| }); | |
| // Scroll to bottom | |
| scrollToBottom(); | |
| } | |
| // Add user message with multiple images to chat window | |
| function addUserImageMessage(message, imagesData) { | |
| // Hide welcome container if visible (first message) | |
| if (!conversationStarted && welcomeContainer) { | |
| welcomeContainer.style.display = 'none'; | |
| conversationStarted = true; | |
| } | |
| const messageElement = userImageMessageTemplate.content.cloneNode(true); | |
| // Handle multiple images | |
| const imageContainer = messageElement.querySelector('.image-container'); | |
| // If imagesData is an array, add all images | |
| if (Array.isArray(imagesData)) { | |
| // Clear existing image elements | |
| imageContainer.innerHTML = ''; | |
| // Add each image | |
| imagesData.forEach(imgData => { | |
| const img = document.createElement('img'); | |
| img.className = 'chat-image'; | |
| img.src = imgData; | |
| img.alt = 'Chat image'; | |
| imageContainer.appendChild(img); | |
| }); | |
| } else if (imagesData) { | |
| // Single image case | |
| const imageElement = messageElement.querySelector('.chat-image'); | |
| imageElement.src = imagesData; | |
| } | |
| // Set message text if any | |
| const textElement = messageElement.querySelector('p'); | |
| if (message) { | |
| textElement.textContent = message; | |
| } else { | |
| textElement.style.display = 'none'; // Hide text element if no message | |
| } | |
| chatWindow.appendChild(messageElement); | |
| // Add to chat history (we don't add the image to history, just the text) | |
| chatHistory.push({ | |
| sender: 'user', | |
| text: message || 'Images envoyées' // Default text if no message provided | |
| }); | |
| // Scroll to bottom | |
| scrollToBottom(); | |
| } | |
| // Add bot message to chat window with markdown support | |
| function addBotMessage(message) { | |
| // Hide welcome container if visible (first message) | |
| if (!conversationStarted && welcomeContainer) { | |
| welcomeContainer.style.display = 'none'; | |
| conversationStarted = true; | |
| } | |
| const messageElement = botMessageTemplate.content.cloneNode(true); | |
| const messageParagraph = messageElement.querySelector('p'); | |
| // Use the marked library to parse Markdown if available | |
| if (window.marked) { | |
| messageParagraph.innerHTML = marked.parse(message); | |
| } else { | |
| messageParagraph.textContent = message; | |
| } | |
| // Add copy button functionality | |
| const copyButton = messageElement.querySelector('.copy-button'); | |
| if (copyButton) { | |
| copyButton.addEventListener('click', function() { | |
| // Get the text to copy (original message, not HTML) | |
| navigator.clipboard.writeText(message).then(() => { | |
| // Show success notification with SweetAlert2 | |
| Swal.fire({ | |
| toast: true, | |
| position: 'top-end', | |
| icon: 'success', | |
| title: 'Texte copié !', | |
| showConfirmButton: false, | |
| timer: 1500 | |
| }); | |
| }).catch(err => { | |
| console.error('Erreur lors de la copie :', err); | |
| Swal.fire({ | |
| toast: true, | |
| position: 'top-end', | |
| icon: 'error', | |
| title: 'Erreur lors de la copie', | |
| showConfirmButton: false, | |
| timer: 1500 | |
| }); | |
| }); | |
| }); | |
| } | |
| chatWindow.appendChild(messageElement); | |
| // Add syntax highlighting to code blocks if Prism is available | |
| if (window.Prism) { | |
| const codeBlocks = messageParagraph.querySelectorAll('pre code'); | |
| codeBlocks.forEach(block => { | |
| Prism.highlightElement(block); | |
| }); | |
| } | |
| // Add to chat history | |
| chatHistory.push({ | |
| sender: 'bot', | |
| text: message | |
| }); | |
| // Scroll to bottom | |
| scrollToBottom(); | |
| } | |
| // Add loading indicator to chat window | |
| function addLoadingIndicator() { | |
| const loadingElement = loadingTemplate.content.cloneNode(true); | |
| chatWindow.appendChild(loadingElement); | |
| // Scroll to bottom | |
| scrollToBottom(); | |
| // Return the loading element so it can be removed later | |
| return chatWindow.lastElementChild; | |
| } | |
| // Add error message to chat window | |
| function addErrorMessage(error, retryMessage, retryImage) { | |
| const errorElement = errorMessageTemplate.content.cloneNode(true); | |
| errorElement.querySelector('p').textContent = error; | |
| // Add retry functionality if a message to retry is provided | |
| if (retryMessage || retryImage) { | |
| const retryButton = errorElement.querySelector('.retry-button'); | |
| retryButton.addEventListener('click', function() { | |
| // Remove the error message | |
| this.closest('.message-container').remove(); | |
| // Show loading indicator | |
| const loadingElement = addLoadingIndicator(); | |
| // Retry sending the message | |
| sendToAPI(retryMessage, loadingElement, retryImage); | |
| }); | |
| } else { | |
| // Hide retry button if no retry message | |
| errorElement.querySelector('.retry-button').style.display = 'none'; | |
| } | |
| chatWindow.appendChild(errorElement); | |
| // Scroll to bottom | |
| scrollToBottom(); | |
| } | |
| // Send message to API | |
| function sendToAPI(message, loadingElement, imageData = null) { | |
| // Disable input while processing | |
| userInput.disabled = true; | |
| sendButton.disabled = true; | |
| uploadImageButton.disabled = true; | |
| if (clearButton) clearButton.disabled = true; | |
| // Prepare request data | |
| const requestData = { | |
| message: message, | |
| history: chatHistory | |
| }; | |
| // Add image data if provided | |
| if (imageData) { | |
| // If imageData is an array of images, send them all | |
| requestData.image = imageData; | |
| } | |
| fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(requestData) | |
| }) | |
| .then(response => { | |
| if (!response.ok) { | |
| throw new Error(`Server responded with status: ${response.status}`); | |
| } | |
| return response.json(); | |
| }) | |
| .then(data => { | |
| // Remove loading indicator | |
| if (loadingElement) { | |
| loadingElement.remove(); | |
| } | |
| // Check for error | |
| if (data.error) { | |
| addErrorMessage(data.error, message, imageData); | |
| } else { | |
| // Add bot response | |
| addBotMessage(data.response); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| // Remove loading indicator | |
| if (loadingElement) { | |
| loadingElement.remove(); | |
| } | |
| // Add different error message based on online/offline status | |
| if (!navigator.onLine) { | |
| // Custom error message for offline mode | |
| addErrorMessage('Vous êtes actuellement hors ligne. Connectez-vous à Internet pour discuter avec Mariam AI.', message, imageData); | |
| // Update the status indicator | |
| updateOnlineStatus(); | |
| } else { | |
| // Generic error for other connection issues | |
| addErrorMessage('Désolé, il y a eu un problème de connexion avec le serveur. Veuillez réessayer.', message, imageData); | |
| } | |
| }) | |
| .finally(() => { | |
| // Re-enable input | |
| userInput.disabled = false; | |
| sendButton.disabled = false; | |
| uploadImageButton.disabled = false; | |
| if (clearButton) clearButton.disabled = false; | |
| userInput.focus(); | |
| }); | |
| } | |
| // Scroll chat window to bottom | |
| function scrollToBottom() { | |
| chatWindow.scrollTop = chatWindow.scrollHeight; | |
| } | |
| // Setup suggestion bubbles click handlers | |
| if (suggestionBubbles) { | |
| suggestionBubbles.forEach(bubble => { | |
| bubble.addEventListener('click', function() { | |
| const prompt = this.getAttribute('data-prompt'); | |
| if (prompt) { | |
| userInput.value = prompt; | |
| sendMessage(); | |
| } | |
| }); | |
| }); | |
| } | |
| // Initial focus on input field | |
| userInput.focus(); | |
| }); | |