'Non autorisé']); exit; } $user = currentUser(); $db = Database::getInstance(); // Récupérer et valider les données $id_tentative = filter_input(INPUT_POST, 'id_tentative', FILTER_VALIDATE_INT); $id_question = filter_input(INPUT_POST, 'id_question', FILTER_VALIDATE_INT); $message_eleve = trim($_POST['message'] ?? ''); if (!$id_tentative || !$id_question || empty($message_eleve)) { echo json_encode(['error' => 'Données manquantes']); exit; } // Vérifier que la tentative appartient à l'élève et est en cours $tentative = $db->fetchOne( "SELECT id_eleve FROM tentatives_eleves WHERE id_tentative = ? AND statut = 'en_cours'", [$id_tentative] ); if (!$tentative || $tentative['id_eleve'] != $user['id_utilisateur']) { echo json_encode(['error' => 'Tentative invalide']); exit; } // Vérifier limite (3 messages max par question) $usage = $db->fetchOne( "SELECT messages_count FROM chat_usage_limits WHERE id_tentative = ? AND id_question = ?", [$id_tentative, $id_question] ); $messages_count = $usage['messages_count'] ?? 0; if ($messages_count >= 3) { echo json_encode([ 'error' => 'Limite atteinte', 'message' => 'Tu as utilisé toute ton aide pour cette question (3/3). Continue, tu peux le faire ! 💪', 'remaining' => 0 ]); exit; } // Récupérer l'énoncé de la question (SANS la réponse) $question = $db->fetchOne( "SELECT q.enonce, q.type_question, e.titre as titre_eval FROM questions q JOIN evaluations e ON q.id_evaluation = e.id_evaluation WHERE q.id_question = ?", [$id_question] ); if (!$question) { echo json_encode(['error' => 'Question introuvable']); exit; } $enonce_question = $question['enonce']; $titre_eval = $question['titre_eval']; $type_question = $question['type_question']; // Récupérer ou créer la conversation $conversation = $db->fetchOne( "SELECT id FROM chat_conversations WHERE id_tentative = ? AND id_question = ?", [$id_tentative, $id_question] ); if (!$conversation) { // Créer nouvelle conversation $id_conversation = $db->insert( "INSERT INTO chat_conversations (id_tentative, id_question, id_eleve, modele_utilise) VALUES (?, ?, ?, 'qwen2-math:1.5b')", [$id_tentative, $id_question, $user['id_utilisateur']] ); } else { $id_conversation = $conversation['id']; } // Récupérer l'historique de la conversation $historique = $db->fetchAll( "SELECT role, message FROM chat_messages WHERE id_conversation = ? ORDER BY timestamp ASC", [$id_conversation] ); // Numéro d'aide actuel (1, 2 ou 3) $numero_aide = $messages_count + 1; $system_prompt = "Tu es un assistant pédagogique en mathématiques pour collège/lycée. L'élève a droit à exactement 3 aides par question. C'est son aide numéro {$numero_aide}/3. ═══════════════════════════════════════ RÈGLE ABSOLUE : NE JAMAIS DONNER LA RÉPONSE ═══════════════════════════════════════ Même si l'élève demande directement, même s'il insiste, même s'il dit avoir cherché : TU NE DONNES JAMAIS la réponse, ni le résultat, ni le mot/nombre final attendu. Cela inclut : nommer la transformation, donner le résultat d'un calcul, compléter la phrase avec la solution. LONGUEUR : Réponds en 2-4 phrases MAXIMUM. Sois direct et concis. SELON LE NUMÉRO D'AIDE : - Aide 1/3 : Question ouverte pour faire réfléchir. Ex: \"Qu'est-ce qui change entre la figure de départ et la figure d'arrivée ?\" - Aide 2/3 : Indice plus ciblé sur la méthode. Ex: \"Regarde si la figure a été déplacée, tournée, ou agrandie...\" - Aide 3/3 : Démarche pas-à-pas SANS donner le mot final. Ex: \"Compare les coordonnées : le point A(1,2) devient A'(4,2). Que remarques-tu sur x ? Sur y ?\" CE QUE TU NE FAIS JAMAIS : ❌ \"La réponse est...\" / \"C'est une translation\" / \"Le résultat est 17\" ❌ Confirmer si l'élève a trouvé la bonne réponse ❌ Faire le calcul complet à sa place ❌ Réponses de plus de 4 phrases CE QUE TU FAIS : ✅ Poser UNE question ciblée ✅ Donner UN indice de méthode ✅ Orienter sans révéler ÉNONCÉ DE LA QUESTION (contexte uniquement - NE PAS RÉSOUDRE) : " . strip_tags($enonce_question); // Construire l'historique pour Ollama $messages_ollama = []; foreach ($historique as $msg) { $messages_ollama[] = [ 'role' => $msg['role'], 'content' => $msg['message'] ]; } // Ajouter le nouveau message de l'élève $messages_ollama[] = [ 'role' => 'user', 'content' => $message_eleve ]; // Appel à Ollama (qwen2-math:1.5b) $start_time = microtime(true); $ch = curl_init('http://localhost:11434/api/chat'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_TIMEOUT => 30, CURLOPT_POSTFIELDS => json_encode([ 'model' => 'ministral-3:3b', 'messages' => array_merge([ ['role' => 'system', 'content' => $system_prompt] ], $messages_ollama), 'stream' => false, 'options' => [ 'temperature' => 0.7, 'num_predict' => 200, // Réponses courtes 'top_p' => 0.9 ] ]) ]); $response = curl_exec($ch); $curl_error = curl_error($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $end_time = microtime(true); $temps_reponse_ms = round(($end_time - $start_time) * 1000); // Gérer les erreurs if ($curl_error) { error_log("Erreur Ollama CURL: $curl_error"); echo json_encode(['error' => 'Service temporairement indisponible (connexion)']); exit; } if ($http_code !== 200) { error_log("Erreur Ollama HTTP $http_code: $response"); echo json_encode(['error' => 'Service temporairement indisponible (HTTP ' . $http_code . ')']); exit; } $data = json_decode($response, true); if (!isset($data['message']['content'])) { error_log("Réponse Ollama invalide: $response"); echo json_encode(['error' => 'Erreur de génération']); exit; } $reponse_ia = trim($data['message']['content']); // ============================================================ // DÉTECTION : la réponse de l'IA contient-elle la solution ? // Si oui, on remplace par un message neutre // ============================================================ function detecterReponseDirecte($texte, $enonce) { $texte_lower = mb_strtolower($texte); // Patterns suspects : formulations qui donnent directement la réponse $patterns_suspects = [ '/la\s+r[eé]ponse\s+est/i', '/c\'?est\s+(une?\s+)?(translation|rotation|homoth|sym[eé]trie|vecteur)/i', '/il\s+s\'?agit\s+(d\'?une?|du)/i', '/on\s+obtient\s*:/i', '/le\s+r[eé]sultat\s+est/i', '/donc\s*[=:]\s*\d/i', '/la\s+solution\s+est/i', '/r[eé]ponse\s+correcte/i', ]; foreach ($patterns_suspects as $pattern) { if (preg_match($pattern, $texte)) { error_log("Chatbot: réponse directe détectée, remplacement. Pattern: $pattern"); return true; } } return false; } if (detecterReponseDirecte($reponse_ia, $enonce_question)) { $reponse_ia = "Je ne peux pas te donner la réponse directement, mais voici un indice : relis bien l'énoncé et demande-toi quelle transformation modifie la position d'une figure sans changer sa forme ni sa taille. Qu'est-ce que tu remarques ?"; } // Sauvegarder le message de l'élève $db->query( "INSERT INTO chat_messages (id_conversation, role, message) VALUES (?, 'user', ?)", [$id_conversation, $message_eleve] ); // Sauvegarder la réponse de l'IA $db->query( "INSERT INTO chat_messages (id_conversation, role, message, temps_reponse_ms) VALUES (?, 'assistant', ?, ?)", [$id_conversation, $reponse_ia, $temps_reponse_ms] ); // Incrémenter le compteur d'utilisation $db->query( "INSERT INTO chat_usage_limits (id_tentative, id_question, messages_count) VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE messages_count = messages_count + 1", [$id_tentative, $id_question] ); $messages_restants = 2 - $messages_count; // Réponse succès echo json_encode([ 'success' => true, 'response' => $reponse_ia, 'remaining' => $messages_restants, 'temps_ms' => $temps_reponse_ms ]);