/** * CHATBOT IA - Client JavaScript */ class ChatbotIA { constructor(tentativeId) { this.tentativeId = tentativeId; this.currentQuestionId = null; this.helpRemaining = 3; this.isLoading = false; this.initElements(); this.attachEvents(); } initElements() { this.btnOpenChat = document.getElementById('btn-open-chat'); this.modalChat = document.getElementById('modal-chat'); this.btnCloseChat = document.getElementById('btn-close-chat'); this.chatMessages = document.getElementById('chat-messages'); this.chatInput = document.getElementById('chat-input'); this.btnSend = document.getElementById('btn-send'); this.helpRemainingSpan = document.getElementById('help-remaining'); } attachEvents() { // Ouvrir chat this.btnOpenChat?.addEventListener('click', () => this.ouvrirChat()); // Fermer chat this.btnCloseChat?.addEventListener('click', () => this.fermerChat()); // Clic en dehors du modal this.modalChat?.addEventListener('click', (e) => { if (e.target === this.modalChat) this.fermerChat(); }); // Envoyer message this.btnSend?.addEventListener('click', () => this.envoyerMessage()); // Enter pour envoyer (Shift+Enter pour nouvelle ligne) this.chatInput?.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.envoyerMessage(); } }); } ouvrirChat() { // Utiliser la variable globale currentQuestion du formulaire if (typeof window.currentQuestion !== 'undefined' && window.questionsIds) { // currentQuestion est l'index (1-80), on récupère l'ID réel const questionIndex = window.currentQuestion - 1; this.currentQuestionId = window.questionsIds[questionIndex]; console.log('💬 Question détectée via currentQuestion:', window.currentQuestion, '→ ID:', this.currentQuestionId); } else { // Fallback sur la détection par classe active this.currentQuestionId = this.detectCurrentQuestion(); } if (!this.currentQuestionId) { alert('Erreur : impossible de déterminer la question actuelle'); return; } console.log('💬 Ouverture chat - Question:', this.currentQuestionId); // Charger l'aide restante pour cette question this.loadHelpRemaining(); this.modalChat.classList.add('show'); this.chatInput.focus(); } // NOUVELLE MÉTHODE : Détecter la question visible detectCurrentQuestion() { // Trouver la question la plus visible dans le viewport const questionCards = document.querySelectorAll('.question-card[data-question-id]'); if (questionCards.length === 0) { console.error('❌ Aucune question-card trouvée'); return null; } let closestQuestion = null; let minDistance = Infinity; const viewportCenter = window.innerHeight / 2; questionCards.forEach(card => { const rect = card.getBoundingClientRect(); // La question est visible dans le viewport if (rect.top < window.innerHeight && rect.bottom > 0) { const cardCenter = rect.top + (rect.height / 2); const distance = Math.abs(cardCenter - viewportCenter); if (distance < minDistance) { minDistance = distance; closestQuestion = card; } } }); if (closestQuestion) { const qId = parseInt(closestQuestion.dataset.questionId); console.log('✅ Question visible détectée:', qId); return qId; } // Fallback : première question const firstCard = questionCards[0]; const qId = parseInt(firstCard.dataset.questionId); console.log('⚠️ Fallback première question:', qId); return qId; } // NOUVELLE MÉTHODE : Charger l'aide restante via AJAX async loadHelpRemaining() { try { const response = await fetch('/mathematiques/api/chatbot_check_limit.php', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({ id_tentative: this.tentativeId, id_question: this.currentQuestionId }) }); const data = await response.json(); if (data.success) { this.helpRemaining = data.remaining; this.updateHelpBadge(); } } catch (error) { console.error('Erreur chargement limite aide:', error); } } fermerChat() { this.modalChat.classList.remove('show'); } getCurrentQuestionId() { // À adapter selon ton code de passer_evaluation.php // Option 1 : Variable globale JS if (typeof currentQuestionId !== 'undefined') { return currentQuestionId; } // Option 2 : Data attribute const questionElement = document.querySelector('[data-question-id]'); if (questionElement) { return questionElement.dataset.questionId; } // Option 3 : Input hidden const hiddenInput = document.querySelector('input[name="id_question"]'); if (hiddenInput) { return hiddenInput.value; } return null; } async envoyerMessage() { const message = this.chatInput.value.trim(); if (!message || this.isLoading) return; // Afficher message utilisateur this.ajouterMessage(message, 'user'); this.chatInput.value = ''; // Loading this.isLoading = true; this.btnSend.disabled = true; this.afficherTypingIndicator(); try { const response = await fetch('/mathematiques/api/chatbot.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ id_tentative: this.tentativeId, id_question: this.currentQuestionId, message: message }) }); const data = await response.json(); // Retirer typing indicator this.retirerTypingIndicator(); if (data.error) { this.ajouterMessage(data.message || data.error, 'system'); if (data.remaining === 0) { this.helpRemaining = 0; this.updateHelpBadge(); setTimeout(() => this.fermerChat(), 10000); // 10s au lieu de 3s } } else { this.ajouterMessage(data.response, 'assistant'); this.helpRemaining = data.remaining; this.updateHelpBadge(); // Afficher info modèle dans console console.log('✅ Réponse IA -', 'Modèle:', data.modele || 'qwen2-math:1.5b', '- Temps:', data.temps_ms, 'ms'); if (this.helpRemaining === 0) { setTimeout(() => { this.ajouterMessage( '⚠️ Tu as utilisé toute ton aide pour cette question (3/3).\n\nLe chat se fermera dans 10 secondes. Continue, tu peux le faire ! 💪', 'system' ); // Bouton fermeture manuelle const btnDiv = document.createElement('div'); btnDiv.className = 'message message-system'; btnDiv.style.textAlign = 'center'; btnDiv.innerHTML = ''; this.chatMessages.appendChild(btnDiv); this.scrollToBottom(); // Fermeture auto après 10s setTimeout(() => this.fermerChat(), 10000); }, 500); } } } catch (error) { console.error('Erreur chatbot:', error); this.retirerTypingIndicator(); this.ajouterMessage('❌ Erreur de connexion au serveur.', 'system'); } finally { this.isLoading = false; this.btnSend.disabled = false; this.chatInput.focus(); } } ajouterMessage(texte, type) { const msgDiv = document.createElement('div'); msgDiv.className = `message message-${type}`; // Convertir le texte brut en HTML avec support LaTeX const htmlContent = this.formatMathText(texte); msgDiv.innerHTML = htmlContent; this.chatMessages.appendChild(msgDiv); // Rendre les formules mathématiques avec KaTeX if (typeof renderMathInElement !== 'undefined') { renderMathInElement(msgDiv, { delimiters: [ {left: '$$', right: '$$', display: true}, // Mode display (centré) {left: '$', right: '$', display: false}, // Mode inline {left: '\\(', right: '\\)', display: false}, {left: '\\[', right: '\\]', display: true} ], throwOnError: false, trust: true }); } this.scrollToBottom(); } // NOUVELLE MÉTHODE : Formater le texte avec support maths formatMathText(texte) { // Convertir les retours à la ligne en
let html = texte.replace(/\n/g, '
'); // Détecter et convertir les fractions communes // 1/2 → $\frac{1}{2}$ html = html.replace(/(\d+)\/(\d+)/g, '$\\frac{$1}{$2}$'); // Convertir les puissances // x^2 → $x^2$ html = html.replace(/([a-z0-9]+)\^([0-9]+)/gi, '$$$1^{$2}$$'); // Convertir les indices // x_1 → $x_1$ html = html.replace(/([a-z0-9]+)_([0-9]+)/gi, '$$$1_{$2}$$'); // Symboles mathématiques courants const symbols = { '×': '\\times', '÷': '\\div', '≤': '\\leq', '≥': '\\geq', '≠': '\\neq', '≈': '\\approx', '∞': '\\infty', '√': '\\sqrt', 'π': '\\pi', '∑': '\\sum', '∫': '\\int' }; for (let [symbol, latex] of Object.entries(symbols)) { html = html.replace(new RegExp(symbol, 'g'), '$' + latex + '$'); } return html; } afficherTypingIndicator() { const typingDiv = document.createElement('div'); typingDiv.className = 'typing-indicator'; typingDiv.id = 'typing-indicator'; typingDiv.innerHTML = ` `; this.chatMessages.appendChild(typingDiv); this.scrollToBottom(); } retirerTypingIndicator() { document.getElementById('typing-indicator')?.remove(); } scrollToBottom() { this.chatMessages.scrollTop = this.chatMessages.scrollHeight; } updateHelpBadge() { if (this.helpRemainingSpan) { this.helpRemainingSpan.textContent = this.helpRemaining; } } }