Files
webval/assets/js/chatbot.js
2026-01-04 10:08:27 +01:00

339 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 = '<button onclick="window.chatbot.fermerChat()" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: 600; margin-top: 10px;">✕ Fermer maintenant</button>';
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 <br>
let html = texte.replace(/\n/g, '<br>');
// 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 = `
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
`;
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;
}
}
}