/** * ASPIC — contact_ia_test.js * * Logique de contact_ia_test.html — outil de mise au point interne * (4 septembre 2026), voir l'en-tête de contact_ia_interprete.php pour le * contexte et les choix d'architecture. * * Reprend, pour la partie micro/reconnaissance vocale, le squelette déjà * éprouvé de micro_test.js (redémarrage silencieux du moteur, coupe-circuit * anti-boucle, coupure après inactivité) — volontairement dupliqué plutôt * que factorisé : ce fichier doit pouvoir être abandonné/retravaillé sans * aucun risque de toucher à micro_test.js, qui continue d'alimenter la * banque de dictées brutes en production. Contrairement à micro_test.js, * pas de "mode adresse" ici : si un champ dicté (email, orthographe d'un nom * rare...) est mal capté, la correction se fait directement dans le champ * du formulaire affiché après interprétation — c'est précisément * l'échappatoire "passer en saisie" actée avec Jean-Marc le 4 septembre * (réflexions stratégiques, section 12) : chaque champ du formulaire EST * cette échappatoire, pas besoin d'un mécanisme dédié en plus. */ (function(){ "use strict"; const boutonMicro = document.getElementById("bouton-micro"); const boutonInterpreter = document.getElementById("bouton-interpreter"); const boutonReinitialiser = document.getElementById("bouton-reinitialiser"); const etatMicro = document.getElementById("etat-micro"); const zoneLignes = document.getElementById("zone-lignes"); const zoneResultat = document.getElementById("resultat"); const boutonCreer = document.getElementById("bouton-creer"); const messageCreation = document.getElementById("message-creation"); const selectStatut = document.getElementById("f-statut"); const selectProfession = document.getElementById("f-profession"); const blocNouvelleProfession = document.getElementById("bloc-nouvelle-profession"); const champNouvelleProfession = document.getElementById("f-profession-nouvelle"); // -------- Reconnaissance vocale (squelette repris de micro_test.js) -------- const ReconnaissanceVocale = window.SpeechRecognition || window.webkitSpeechRecognition; let reconnaissance = null; let ecouteVoulue = false; let erreurBloquante = false; let ligneEnCours = null; let texteComplet = ""; // buffer de toute la dictée depuis le dernier "Réinitialiser" let derniereExtraction = null; // dernière réponse de contact_ia_interprete.php, gardée pour comparaison avec ce que Jean-Marc valide ensuite const DUREE_INACTIVITE_MS = 10 * 60 * 1000; // 10 minutes, même prudence que micro_test.js let minuteurInactivite = null; function reinitialiserMinuteurInactivite(){ if(minuteurInactivite) clearTimeout(minuteurInactivite); minuteurInactivite = setTimeout(function(){ if(!ecouteVoulue) return; ecouteVoulue = false; reconnaissance.stop(); }, DUREE_INACTIVITE_MS); } function arreterMinuteurInactivite(){ if(minuteurInactivite){ clearTimeout(minuteurInactivite); minuteurInactivite = null; } } function afficherErreur(texte){ const div = document.createElement("div"); div.className = "erreur"; div.textContent = texte; zoneLignes.appendChild(div); zoneLignes.scrollTop = zoneLignes.scrollHeight; } function afficherEnCours(texte){ if(!ligneEnCours){ ligneEnCours = document.createElement("div"); ligneEnCours.className = "en-cours"; zoneLignes.appendChild(ligneEnCours); } ligneEnCours.textContent = texte; zoneLignes.scrollTop = zoneLignes.scrollHeight; } function figerLigne(texte){ if(!ligneEnCours){ ligneEnCours = document.createElement("div"); zoneLignes.appendChild(ligneEnCours); } ligneEnCours.className = ""; ligneEnCours.textContent = texte; ligneEnCours = null; zoneLignes.scrollTop = zoneLignes.scrollHeight; texteComplet = (texteComplet + " " + texte).trim(); boutonInterpreter.disabled = (texteComplet === ""); } if(!ReconnaissanceVocale){ boutonMicro.disabled = true; boutonMicro.title = "Reconnaissance vocale non disponible sur ce navigateur"; } else { reconnaissance = new ReconnaissanceVocale(); reconnaissance.lang = "fr-FR"; reconnaissance.continuous = true; reconnaissance.interimResults = true; reconnaissance.maxAlternatives = 1; let dernierDemarrageHorodatage = 0; let cyclesRapidesConsecutifs = 0; const SEUIL_CYCLE_RAPIDE_MS = 800; const MAX_CYCLES_RAPIDES = 5; function passerEnEcoute(){ boutonMicro.classList.add("actif"); boutonMicro.textContent = "🎤 Écoute en cours…"; etatMicro.textContent = "micro ouvert"; reinitialiserMinuteurInactivite(); } function revenirAuRepos(){ ecouteVoulue = false; arreterMinuteurInactivite(); boutonMicro.classList.remove("actif"); boutonMicro.textContent = "🎤 Micro"; etatMicro.textContent = ""; } boutonMicro.addEventListener("click", function(){ if(ecouteVoulue){ ecouteVoulue = false; reconnaissance.stop(); return; } ecouteVoulue = true; erreurBloquante = false; try{ reconnaissance.start(); }catch(erreur){ ecouteVoulue = false; afficherErreur("Impossible de démarrer le micro (" + erreur.name + ")."); } }); reconnaissance.addEventListener("start", function(){ dernierDemarrageHorodatage = Date.now(); passerEnEcoute(); }); reconnaissance.addEventListener("end", function(){ if(ecouteVoulue && !erreurBloquante){ const ecoule = Date.now() - dernierDemarrageHorodatage; cyclesRapidesConsecutifs = (ecoule < SEUIL_CYCLE_RAPIDE_MS) ? (cyclesRapidesConsecutifs + 1) : 0; if(cyclesRapidesConsecutifs >= MAX_CYCLES_RAPIDES){ erreurBloquante = true; revenirAuRepos(); afficherErreur("Le micro redémarre en boucle anormalement vite — arrêt automatique. Réessaie dans quelques secondes."); return; } try{ reconnaissance.start(); }catch(erreur){ revenirAuRepos(); afficherErreur("Le micro s'est arrêté et n'a pas pu redémarrer automatiquement."); } return; } revenirAuRepos(); }); reconnaissance.addEventListener("result", function(evenement){ const dernier = evenement.results[evenement.results.length - 1]; const texte = dernier[0].transcript.trim(); if(!texte) return; if(dernier.isFinal){ figerLigne(texte); reinitialiserMinuteurInactivite(); } else { afficherEnCours(texte); } }); reconnaissance.addEventListener("error", function(evenement){ const messagesBloquants = { "not-allowed": "Micro refusé — vérifie l'autorisation dans le navigateur.", "service-not-allowed": "Micro refusé — vérifie l'autorisation dans le navigateur.", "audio-capture": "Aucun micro détecté." }; if(messagesBloquants[evenement.error]){ erreurBloquante = true; afficherErreur(messagesBloquants[evenement.error]); } }); } // -------- Réinitialiser -------- boutonReinitialiser.addEventListener("click", function(){ if(ecouteVoulue && reconnaissance){ ecouteVoulue = false; reconnaissance.stop(); } texteComplet = ""; ligneEnCours = null; zoneLignes.innerHTML = ""; boutonInterpreter.disabled = true; zoneResultat.classList.remove("visible"); messageCreation.textContent = ""; messageCreation.className = ""; }); // -------- Interpréter -------- function viderFormulaire(){ document.querySelectorAll("[data-champ]").forEach(function(el){ el.value = ""; el.classList.remove("interprete"); }); selectStatut.innerHTML = ''; selectProfession.innerHTML = ''; blocNouvelleProfession.style.display = "none"; champNouvelleProfession.value = ""; } function remplirChampsTexte(champs){ document.querySelectorAll("[data-champ]").forEach(function(el){ const nomChamp = el.dataset.champ; if(nomChamp === "CNT_Statut") return; // géré à part (select) const valeur = champs[nomChamp]; if(valeur){ el.value = valeur; el.classList.add("interprete"); } }); } function remplirStatuts(statuts, statutChoisi){ statuts.forEach(function(s){ const option = document.createElement("option"); option.value = s.STA_Code; option.textContent = s.STA_Libellé; selectStatut.appendChild(option); }); if(statutChoisi){ selectStatut.value = statutChoisi; selectStatut.classList.add("interprete"); } } function remplirProfessions(professions, idExistant, libelleNouveau){ professions.forEach(function(p){ const option = document.createElement("option"); option.value = String(p.PRF_Incr); option.textContent = p.PRF_Libelle; // Insertion avant l'option "+ Nouvelle profession…", toujours en dernier. selectProfession.insertBefore(option, selectProfession.lastElementChild); }); if(idExistant){ selectProfession.value = String(idExistant); selectProfession.classList.add("interprete"); } else if(libelleNouveau){ selectProfession.value = "__nouvelle__"; selectProfession.classList.add("interprete"); blocNouvelleProfession.style.display = "block"; champNouvelleProfession.value = libelleNouveau; } } selectProfession.addEventListener("change", function(){ blocNouvelleProfession.style.display = (selectProfession.value === "__nouvelle__") ? "block" : "none"; }); boutonInterpreter.addEventListener("click", function(){ if(!texteComplet) return; boutonInterpreter.disabled = true; boutonInterpreter.textContent = "Interprétation en cours…"; messageCreation.textContent = ""; messageCreation.className = ""; fetch("contact_ia_interprete.php", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({texte: texteComplet}) }).then(function(reponse){ return reponse.json().then(function(corps){ if(!reponse.ok) throw new Error(corps.error || ("HTTP " + reponse.status)); return corps; }); }).then(function(corps){ derniereExtraction = corps.extraction; viderFormulaire(); remplirChampsTexte(corps.extraction.champs); remplirStatuts(corps.statuts, corps.extraction.champs.CNT_Statut); remplirProfessions( corps.professions, corps.extraction.profession_existante_id, corps.extraction.profession_nouvelle_libelle ); zoneResultat.classList.add("visible"); zoneResultat.scrollIntoView({behavior:"smooth", block:"start"}); }).catch(function(erreur){ afficherErreur("Échec de l'interprétation : " + erreur.message); }).finally(function(){ boutonInterpreter.disabled = (texteComplet === ""); boutonInterpreter.textContent = "Interpréter →"; }); }); // -------- Valider (4 septembre 2026) -------- // Correction explicite de Jean-Marc : cet outil ne doit JAMAIS créer de // contact réel (ni de profession réelle au passage) — contrairement à ce // qu'une première version faisait en appelant contacts.php directement. // L'objectif de cette étape est seulement d'accumuler, pour chaque essai, // trois choses côte à côte (texte brut / ce que le LLM a compris / ce que // Jean-Marc valide après relecture-correction) afin d'en tirer des // conclusions sur la qualité de l'interprétation — et, plus tard, de // servir de matière à la création d'un post-it par assistant pour chaque // instruction vocale (vision "bureau à l'ancienne", pas encore câblée : // ASP_ToDoList ne sait pas encore attribuer une tâche à un assistant, voir // le "reste à faire" du journal de développement). Rien n'est donc écrit // dans ASP_Contacts ni ASP_Professions depuis cette page — uniquement dans // la nouvelle table ASP_PropositionsContact, via contact_ia_valide.php. function collecterChamps(){ const champs = {}; document.querySelectorAll("[data-champ]").forEach(function(el){ champs[el.dataset.champ] = el.value.trim(); }); return champs; } function collecterProfessionValidee(){ if(selectProfession.value === "__nouvelle__"){ return {profession_existante_id: null, profession_nouvelle_libelle: champNouvelleProfession.value.trim() || null}; } if(selectProfession.value){ return {profession_existante_id: parseInt(selectProfession.value, 10), profession_nouvelle_libelle: null}; } return {profession_existante_id: null, profession_nouvelle_libelle: null}; } boutonCreer.addEventListener("click", function(){ const champs = collecterChamps(); if(!champs.CNT_Nom){ messageCreation.className = "ko"; messageCreation.textContent = "Le nom est obligatoire."; return; } boutonCreer.disabled = true; boutonCreer.textContent = "Enregistrement en cours…"; messageCreation.textContent = ""; messageCreation.className = ""; const valide = Object.assign({champs: champs}, collecterProfessionValidee()); fetch("contact_ia_valide.php", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ texteBrut: texteComplet, extraction: derniereExtraction, valide: valide }) }).then(function(reponse){ return reponse.json().then(function(corps){ if(!reponse.ok) throw new Error(corps.error || ("HTTP " + reponse.status)); return corps; }); }).then(function(){ messageCreation.className = "ok"; messageCreation.textContent = "Essai enregistré (aucun contact réel créé) — merci !"; }).catch(function(erreur){ messageCreation.className = "ko"; messageCreation.textContent = "Échec de l'enregistrement : " + erreur.message; }).finally(function(){ boutonCreer.disabled = false; boutonCreer.textContent = "Valider"; }); }); })();