Conversation réelle, sous-agents (J3) et missions longues (J5) vers les exécuteurs configurés ; toute écriture dans les apps passe par une approbation.
' +
+ '
' +
+ '
' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '
' +
+ ' ' +
' ' +
'
';
document.body.appendChild(root);
msgsEl = root.querySelector(".draco-panneau-msgs");
- etatEl = root.querySelector(".draco-panneau-etat");
- inputEl = root.querySelector("input");
- sendBtn = root.querySelector("button[type=submit]");
+ etatEl = root.querySelector(".draco-panneau-hd .soustitre");
+ inputEl = root.querySelector(".draco-composer textarea");
+ sendBtn = root.querySelector(".draco-composer button[type=submit]");
+ microEl = root.querySelector('[data-comp="micro"]');
+ piecesEl = root.querySelector(".draco-composer-pieces");
+ fichierEl = root.querySelector(".draco-composer input[type=file]");
titreEl = root.querySelector(".d-titre");
tiroirEl = root.querySelector(".draco-discussions");
versionEl = root.querySelector(".draco-panneau-version");
@@ -288,6 +329,42 @@
root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations);
root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions);
root.querySelector("form").addEventListener("submit", envoyer);
+ inputEl.addEventListener("input", majComposer);
+ inputEl.addEventListener("keydown", function (ev) {
+ if (ev.key === "Enter" && !ev.shiftKey) { ev.preventDefault(); envoyerComposer(); }
+ });
+ root.querySelector('[data-comp="plus"]').addEventListener("click", function () { fichierEl.click(); });
+ fichierEl.addEventListener("change", function (ev) {
+ ajouterFichiers(ev.target.files);
+ ev.target.value = "";
+ });
+ microEl.addEventListener("click", demarrerDictee);
+ root.querySelector('[data-comp="enreg-stop"]').addEventListener("click", function () { arreterDictee("stop"); });
+ root.querySelector('[data-comp="enreg-annuler"]').addEventListener("click", function () { arreterDictee("annuler"); });
+ root.querySelector('[data-comp="enreg-envoyer"]').addEventListener("click", function () { arreterDictee("envoyer"); });
+ Array.prototype.forEach.call(root.querySelectorAll("[data-sugg]"), function (b) {
+ b.addEventListener("click", function () {
+ var mode = b.getAttribute("data-sugg");
+ if (mode === "resume") {
+ envoyerMessage("Résume ma journée : mes tâches du jour, les cartes en Révision et l'activité récente.", []);
+ } else if (mode === "projets") {
+ envoyerMessage("Où en sont mes projets ? Fais le point sur l'état des projets et du board.", []);
+ } else {
+ inputEl.value = "Cherche « » partout : ";
+ majComposer();
+ inputEl.focus();
+ inputEl.setSelectionRange(9, 9);
+ }
+ });
+ });
+ /* Domaine changé dans un autre onglet pendant que le panneau
+ * existe : l'événement storage ne part que vers les autres
+ * documents, donc ici il arrive bien. Le changement dans le
+ * même onglet est rattrapé à chaque ouverture (ouvrir()). */
+ window.addEventListener("storage", function (ev) {
+ if (ev.key === "draco_domain") resynchroniserDomaine();
+ });
+ majComposer();
brancherPoignee(root.querySelector(".draco-poignee"));
appliquerTheme();
surveillerTheme();
@@ -326,8 +403,13 @@
}
function majEtat(cls, texte) {
- etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls;
- etatEl.querySelector(".txt").textContent = texte;
+ /* Statut intégré au titre (sous-titre de l'en-tête) : plus de
+ * barre d'état séparée. Le point vert/rouge/gris reste lisible
+ * sans prendre une rangée entière. */
+ if (!etatEl) return;
+ etatEl.className = "soustitre " + cls;
+ var t = etatEl.querySelector(".txt");
+ if (t) t.textContent = texte;
}
function majTitre() {
titreEl.textContent = convTitre || "Discussion principale";
@@ -400,9 +482,21 @@
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); });
}
+ /* Recharge la discussion du domaine courant quand il a changé
+ * depuis le dernier chargement (ouverture du panneau, autre
+ * onglet, retour du mode console). Sans changement : rien —
+ * on ne casse jamais une conversation en cours pour rien. */
+ function resynchroniserDomaine() {
+ if (!root || domaine() === domaineCharge) return;
+ msgsEl.innerHTML = "";
+ tiroirEl.hidden = true;
+ demarrerDiscussions();
+ }
+
function demarrerDiscussions() {
chargerListeDiscussions()
.then(function (d) {
+ domaineCharge = domaine();
var precedente = convStockee();
var candidates = (d.paralleles || []).concat(d.principale ? [d.principale] : []);
var trouvee = candidates.filter(function (c) { return c.id === precedente; })[0];
@@ -413,6 +507,7 @@
chargerHistorique();
})
.catch(function () {
+ domaineCharge = null; // échec : la prochaine ouverture réessaiera
ajouterMsg("sys", "Discussions : service injoignable pour le moment.");
});
}
@@ -780,19 +875,232 @@
.catch(function (e) { ajouterMsg("sys", "Lancement refusé : " + e.message); });
}
- function envoyer(ev) {
- ev.preventDefault();
- var texte = inputEl.value.trim();
- if (!texte) return;
+ /* ── Composer façon console ────────────────────────────────
+ * Mêmes règles que la console : + (images/documents, 4 max),
+ * textarea auto-extensible, micro affiché seulement sans texte
+ * ni pièce et si le navigateur le supporte, envoi rond bleu
+ * affiché seulement avec texte ou pièce. */
+ var piecesEnAttente = [];
+ var TYPES_IMAGES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
+ var TYPES_DOCUMENTS = ["application/pdf",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/vnd.oasis.opendocument.text",
+ "text/plain", "text/markdown", "text/html", "text/csv"];
+ var TAILLE_MAX_IMAGE = 5 * 1024 * 1024;
+ var TAILLE_MAX_DOC = 10 * 1024 * 1024;
+
+ function majComposer() {
+ if (!inputEl) return;
+ var aTexte = !!(inputEl.value && inputEl.value.trim());
+ sendBtn.hidden = !(aTexte || piecesEnAttente.length);
+ if (microEl) microEl.hidden = !dicteeSupportee || aTexte || piecesEnAttente.length;
+ inputEl.style.height = "auto";
+ var sh = inputEl.scrollHeight;
+ inputEl.style.height = sh ? Math.min(sh, 152) + "px" : "";
+ }
+
+ function rendrePiecesEnAttente() {
+ piecesEl.innerHTML = "";
+ piecesEl.hidden = !piecesEnAttente.length;
+ piecesEnAttente.forEach(function (p, i) {
+ var carte = document.createElement("div");
+ carte.className = "piece-carte";
+ var apercu;
+ if (TYPES_IMAGES.indexOf(p.type) >= 0) {
+ apercu = document.createElement("img");
+ apercu.src = p.dataUrl; apercu.alt = p.nom;
+ } else {
+ apercu = document.createElement("span");
+ apercu.className = "piece-doc-ext";
+ var ext = document.createElement("span");
+ ext.className = "piece-ext";
+ ext.textContent = (p.nom || "").split(".").pop() || "doc";
+ apercu.appendChild(ext);
+ }
+ var nom = document.createElement("span");
+ nom.className = "piece-nom"; nom.textContent = p.nom; nom.title = p.nom;
+ var x = document.createElement("button");
+ x.type = "button"; x.className = "piece-retirer"; x.textContent = "✕";
+ x.title = "Retirer " + p.nom;
+ x.setAttribute("aria-label", "Retirer " + p.nom);
+ x.addEventListener("click", function () {
+ piecesEnAttente.splice(i, 1);
+ rendrePiecesEnAttente(); majComposer();
+ });
+ carte.appendChild(apercu); carte.appendChild(nom); carte.appendChild(x);
+ piecesEl.appendChild(carte);
+ });
+ }
+
+ function ajouterFichiers(fichiers) {
+ Array.prototype.forEach.call(fichiers, function (f) {
+ if (piecesEnAttente.length >= 4) {
+ ajouterMsg("sys", "Au plus 4 pièces par message.");
+ return;
+ }
+ var estImage = TYPES_IMAGES.indexOf(f.type) >= 0;
+ var estDoc = TYPES_DOCUMENTS.indexOf(f.type) >= 0;
+ if (!estImage && !estDoc) {
+ ajouterMsg("sys", "« " + f.name + " » refusée : images PNG, JPEG, GIF ou WebP, ou documents PDF, DOCX, ODT, TXT, MD, HTML ou CSV seulement.");
+ return;
+ }
+ if (estImage && f.size > TAILLE_MAX_IMAGE) {
+ ajouterMsg("sys", "« " + f.name + " » refusée : 5 Mo maximum par image.");
+ return;
+ }
+ if (estDoc && f.size > TAILLE_MAX_DOC) {
+ ajouterMsg("sys", "« " + f.name + " » refusée : 10 Mo maximum par document.");
+ return;
+ }
+ var lecteur = new FileReader();
+ lecteur.onload = function () {
+ var dataUrl = String(lecteur.result || "");
+ piecesEnAttente.push({
+ nom: f.name, type: f.type,
+ base64: dataUrl.split(",")[1] || "", dataUrl: dataUrl
+ });
+ rendrePiecesEnAttente(); majComposer();
+ };
+ lecteur.readAsDataURL(f);
+ });
+ }
+
+ function envoyerComposer() {
+ var t = inputEl.value;
+ var pieces = piecesEnAttente;
+ piecesEnAttente = [];
inputEl.value = "";
- ajouterMsg("user", texte);
+ rendrePiecesEnAttente();
+ majComposer();
+ envoyerMessage(t, pieces);
+ }
+
+ /* ── Dictée vocale — Web Speech API, 100 % navigateur, comme
+ * la console (fr-CA, résultats intérimaires en direct). */
+ var RecoVoc = window.SpeechRecognition || window.webkitSpeechRecognition;
+ var dicteeSupportee = !!RecoVoc;
+ var reco = null, enregEnCours = false, enregDebut = 0, enregMinuteur = null;
+ var texteAvantDictee = "", transcriptionFinale = "";
+ var modeFinDictee = "stop", dicteeErreur = null;
+
+ function demarrerDictee() {
+ if (!dicteeSupportee || enregEnCours) return;
+ texteAvantDictee = inputEl.value;
+ transcriptionFinale = "";
+ modeFinDictee = "stop";
+ dicteeErreur = null;
+ try { reco = new RecoVoc(); } catch (e) { reco = null; return; }
+ reco.lang = "fr-CA";
+ reco.interimResults = true;
+ reco.continuous = true;
+ reco.onresult = function (ev) {
+ var interim = "";
+ for (var i = ev.resultIndex; i < ev.results.length; i++) {
+ var r = ev.results[i];
+ if (r.isFinal) transcriptionFinale += r[0].transcript;
+ else interim += r[0].transcript;
+ }
+ var affiche = (texteAvantDictee + " " + transcriptionFinale + interim).trim();
+ root.querySelector(".enreg-texte").textContent = affiche;
+ inputEl.value = affiche;
+ };
+ reco.onerror = function (ev) {
+ if (ev.error === "aborted") return;
+ dicteeErreur = ev.error || "erreur";
+ modeFinDictee = "annuler";
+ };
+ reco.onend = terminerDictee;
+ try { reco.start(); } catch (e) { reco = null; return; }
+ enregEnCours = true;
+ enregDebut = Date.now();
+ root.querySelector(".enreg-temps").textContent = "0:00";
+ root.querySelector(".enreg-texte").textContent = "";
+ var onde = root.querySelector(".enreg-onde");
+ if (!onde.children.length) {
+ for (var j = 0; j < 14; j++) onde.appendChild(document.createElement("span"));
+ }
+ root.querySelector(".draco-composer-ligne").hidden = true;
+ root.querySelector(".draco-composer-enreg").hidden = false;
+ root.querySelector(".draco-composer").classList.add("en-cours");
+ enregMinuteur = window.setInterval(function () {
+ var s = Math.floor((Date.now() - enregDebut) / 1000);
+ root.querySelector(".enreg-temps").textContent = Math.floor(s / 60) + ":" + ("0" + (s % 60)).slice(-2);
+ }, 250);
+ }
+
+ function arreterDictee(mode) {
+ modeFinDictee = mode;
+ if (reco) {
+ try { reco.stop(); } catch (e) { terminerDictee(); }
+ } else {
+ terminerDictee();
+ }
+ }
+
+ function terminerDictee() {
+ if (!enregEnCours) return;
+ enregEnCours = false;
+ if (enregMinuteur) { window.clearInterval(enregMinuteur); enregMinuteur = null; }
+ reco = null;
+ root.querySelector(".draco-composer-ligne").hidden = false;
+ root.querySelector(".draco-composer-enreg").hidden = true;
+ root.querySelector(".draco-composer").classList.remove("en-cours");
+ if (modeFinDictee === "annuler") {
+ inputEl.value = texteAvantDictee;
+ if (dicteeErreur) {
+ ajouterMsg("sys", "Dictée vocale interrompue (" + dicteeErreur + ") — micro refusé ou indisponible ?");
+ dicteeErreur = null;
+ }
+ } else {
+ inputEl.value = (texteAvantDictee + " " + transcriptionFinale).trim();
+ }
+ var mode = modeFinDictee;
+ modeFinDictee = "stop";
+ majComposer();
+ if (mode === "envoyer") envoyerComposer();
+ else inputEl.focus();
+ }
+
+ function envoyer(ev) {
+ if (ev) ev.preventDefault();
+ envoyerComposer();
+ }
+
+ function envoyerMessage(texte, pieces) {
+ texte = (texte || "").trim();
+ pieces = pieces || [];
+ if (!texte && !pieces.length) return;
+ var el = ajouterMsg("user", texte);
+ if (pieces.length) {
+ var zone = document.createElement("div");
+ zone.className = "draco-msg-pieces";
+ pieces.forEach(function (p) {
+ if (TYPES_IMAGES.indexOf(p.type) >= 0) {
+ var img = document.createElement("img");
+ img.src = p.dataUrl; img.alt = p.nom;
+ zone.appendChild(img);
+ } else {
+ var past = document.createElement("span");
+ past.className = "piece-fichier";
+ past.textContent = p.nom;
+ zone.appendChild(past);
+ }
+ });
+ el.appendChild(zone);
+ }
sendBtn.disabled = true;
var attente = ajouterMsg("agent", "Spark réfléchit…");
+ var corps = { message: texte, domain: domaine(), conversation_id: convId };
+ if (pieces.length) {
+ corps.pieces = pieces.map(function (p) {
+ return { nom: p.nom, type: p.type, donnees_base64: p.base64 };
+ });
+ }
fetch(API + "/api/v1/chat", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ message: texte, domain: domaine(), conversation_id: convId })
+ body: JSON.stringify(corps)
})
.then(function (r) {
if (r.status === 401) {
@@ -831,11 +1139,12 @@
attente.textContent = "Service injoignable. Vérifie que le conteneur dracodev-agent est démarré, puis réessaie.";
majEtat("err", "Service d'agents injoignable");
})
- .finally(function () { sendBtn.disabled = false; inputEl.focus(); });
+ .finally(function () { sendBtn.disabled = false; majComposer(); inputEl.focus(); });
}
function ouvrir() {
if (!root) { construire(); demarrerDiscussions(); }
+ else resynchroniserDomaine();
root.classList.add("open");
appliquerTheme();
verifierIdentite();