Files
ASPIC/contact_ia_test.js

365 lines
14 KiB
JavaScript

/**
* 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 = '<option value="">—</option>';
selectProfession.innerHTML = '<option value="">—</option><option value="__nouvelle__">+ Nouvelle profession…</option>';
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";
});
});
})();