mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 18:17:09 +00:00
365 lines
14 KiB
JavaScript
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";
|
|
});
|
|
});
|
|
})();
|