339 lines
11 KiB
JavaScript
339 lines
11 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|
||
}
|
||
}
|
||
|