/* Panneau Spark — branchement au service d'agents (Fusion, J1 → Panneau v2). * * Parle à l'API du service d'agents (contrat de dracodev-commons) : * GET /api/v1/me — identité (cookie draco_session) * POST /api/v1/chat — conversation réelle du noyau (J2) : * LLM DeepSeek + outils MCP des apps ; * la réponse arrive avec son HTML * Markdown rendu côté service (v2) * GET /api/v1/conversations — discussions façon Muse (v2) : une * principale perpétuelle + des * parallèles créées/archivées/ * supprimées par l'utilisateur * GET /api/v1/conversations/… — historique d'une discussion (les * messages agent portent leur html) * GET /api/v1/executors — registre (noyau, sous-agents J3, * externes J4 : état de configuration) * POST /api/v1/missions — missions longues (J5) * Panneau v2 (Lot 1) : rendu Markdown, largeur réglable à la souris * (persistée), thème clair/sombre aligné sur l'app hôte * (html[data-theme], repli prefers-color-scheme, suivi en direct). * Bouton « Console ↗ » (barre des discussions) : ouvre la console web * du même service (agent.dracodev.net) dans un nouvel onglet, sur la * discussion courante (?disc=) et avec l'app d'origine (?app=), * déduite du sous-domaine de l'hôte. * États honnêtes : service injoignable / session non reconnue / LLM non * configuré / exécuteur non configuré sont affichés tels quels. */ (function () { "use strict"; var API = "https://agent.dracodev.net"; var convId = null; var convKind = null; // 'principale' | 'parallele' var convTitre = ""; var etatConnu = false; var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null; var titreEl = null, tiroirEl = null; function domaine() { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } } function stockerConv() { try { if (convId) sessionStorage.setItem("draco_panneau_conv", convId); else sessionStorage.removeItem("draco_panneau_conv"); } catch (e) { /* stockage indisponible */ } } function convStockee() { try { return sessionStorage.getItem("draco_panneau_conv") || null; } catch (e) { return null; } } /* ── Console web (même service, nouvel onglet) ─────────────── */ function appHote() { var h = (window.location.hostname || "").split(".")[0]; var correspondance = { tiktak: "tiktak", flowdeck: "flowdeck", kanbante: "kanbante", og: "obsigate" }; return correspondance[h] || null; } function ouvrirConsole() { var params = []; var id = convId || convStockee(); if (id) params.push("disc=" + encodeURIComponent(id)); var app = appHote(); if (app) params.push("app=" + encodeURIComponent(app)); window.open(API + "/" + (params.length ? "?" + params.join("&") : ""), "_blank", "noopener"); } /* ── Thème : suivre l'app hôte ─────────────────────────────── */ function themeHote() { var t = document.documentElement.getAttribute("data-theme"); if (t === "dark" || t === "light") return t; if (document.documentElement.classList.contains("dark") || document.body.classList.contains("dark")) return "dark"; try { if (window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark"; } catch (e) { /* pas de matchMedia */ } return "light"; } function appliquerTheme() { if (root) root.setAttribute("data-draco-theme", themeHote()); } function surveillerTheme() { try { new MutationObserver(appliquerTheme).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] }); window.matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", appliquerTheme); } catch (e) { /* surveillance facultative */ } } /* ── Largeur réglable ──────────────────────────────────────── */ function largeurStockee() { try { var v = parseInt(localStorage.getItem("draco_panneau_largeur") || "", 10); return isNaN(v) ? null : v; } catch (e) { return null; } } function appliquerLargeur() { if (!root) return; var v = largeurStockee(); root.style.width = v ? Math.min(v, Math.round(window.innerWidth * 0.96)) + "px" : ""; } function brancherPoignee(poignee) { poignee.addEventListener("pointerdown", function (ev) { if (window.innerWidth <= 700) return; // mobile : pleine largeur ev.preventDefault(); var x0 = ev.clientX, w0 = root.getBoundingClientRect().width; function bouger(e2) { var w = Math.max(300, Math.min(760, w0 + (x0 - e2.clientX))); root.style.width = w + "px"; } function lacher(e2) { document.removeEventListener("pointermove", bouger); document.removeEventListener("pointerup", lacher); var w = Math.round(root.getBoundingClientRect().width); try { localStorage.setItem("draco_panneau_largeur", String(w)); } catch (e) { /* ok */ } } document.addEventListener("pointermove", bouger); document.addEventListener("pointerup", lacher); }); poignee.addEventListener("dblclick", function () { try { localStorage.removeItem("draco_panneau_largeur"); } catch (e) { /* ok */ } appliquerLargeur(); }); } function construire() { root = document.createElement("aside"); root.className = "draco-panneau"; root.setAttribute("aria-label", "Spark, l'agent commun"); root.innerHTML = '
' + '
' + ' ✦' + '
Spark — agent commun
' + '
Service d\'agents dracodev
' + ' ' + '
' + '
' + ' Connexion au service…' + '
' + '
' + ' Discussion principale' + ' ' + ' ' + ' ' + '
' + '' + '
' + ' ' + ' ' + ' ' + '
' + '
' + '
' + ' ' + '
' + '
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]"); titreEl = root.querySelector(".d-titre"); tiroirEl = root.querySelector(".draco-discussions"); root.querySelector(".fermer").addEventListener("click", fermer); root.querySelector('[data-disc="liste"]').addEventListener("click", basculerTiroir); root.querySelector('[data-disc="nouvelle"]').addEventListener("click", nouvelleDiscussion); root.querySelector('[data-disc="console"]').addEventListener("click", ouvrirConsole); root.querySelector('[data-outil="aujourdhui"]').addEventListener("click", chargerAujourdhui); root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations); root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions); root.querySelector("form").addEventListener("submit", envoyer); brancherPoignee(root.querySelector(".draco-poignee")); appliquerTheme(); surveillerTheme(); appliquerLargeur(); window.addEventListener("resize", appliquerLargeur); document.addEventListener("keydown", function (ev) { if (ev.key === "Escape" && root.classList.contains("open")) fermer(); }); } function ajouterMsg(kind, texte) { var d = document.createElement("div"); d.className = "draco-msg " + kind; d.textContent = texte; msgsEl.appendChild(d); msgsEl.scrollTop = msgsEl.scrollHeight; return d; } /* Message agent rendu en Markdown : le HTML vient du service * (rendu + sanitizer côté serveur) ; repli texte si absent. */ function ajouterMsgMd(texte, html) { var d = document.createElement("div"); d.className = "draco-msg agent md"; if (html) d.innerHTML = html; else d.textContent = texte; msgsEl.appendChild(d); msgsEl.scrollTop = msgsEl.scrollHeight; return d; } function majEtat(cls, texte) { etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls; etatEl.querySelector(".txt").textContent = texte; } function majTitre() { titreEl.textContent = convTitre || "Discussion principale"; titreEl.title = titreEl.textContent; } function verifierIdentite() { if (etatConnu) return; fetch(API + "/api/v1/me", { credentials: "include" }) .then(function (r) { if (r.status === 401) { majEtat("err", "Session partagée non reconnue"); ajouterMsg("sys", "Le service ne reconnaît pas ta session. Connecte-toi à ObsiGate (session unique draco_session), puis rouvre le panneau. Si le SSO n'est pas encore activé, c'est l'étape d'activation du programme."); etatConnu = true; return null; } if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (me) { if (!me) return; etatConnu = true; majEtat("ok", "Connecté au service — " + (me.name || ("utilisateur " + me.sub))); }) .catch(function () { majEtat("err", "Service d'agents injoignable"); ajouterMsg("sys", "Le service d'agents (agent.dracodev.net) ne répond pas. Vérifie que le conteneur dracodev-agent est démarré."); }); } function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); } /* ── Discussions (façon Muse) ────────────────────────────── */ function chargerListeDiscussions() { return fetch(API + "/api/v1/conversations?domain=" + encodeURIComponent(domaine()), { credentials: "include" }) .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }); } function demarrerDiscussions() { chargerListeDiscussions() .then(function (d) { var precedente = convStockee(); var candidates = (d.paralleles || []).concat(d.principale ? [d.principale] : []); var trouvee = candidates.filter(function (c) { return c.id === precedente; })[0]; var active = trouvee || d.principale; if (!active) return; convId = active.id; convKind = active.kind; convTitre = active.title; stockerConv(); majTitre(); chargerHistorique(); }) .catch(function () { ajouterMsg("sys", "Discussions : service injoignable pour le moment."); }); } function basculerDiscussion(c) { convId = c.id; convKind = c.kind; convTitre = c.title; stockerConv(); majTitre(); tiroirEl.hidden = true; msgsEl.innerHTML = ""; ajouterMsg("sys", (c.kind === "principale" ? "Discussion principale — le fil sans fin." : "Discussion parallèle : " + c.title) + " · domaine : " + domaine() + "."); chargerHistorique(); inputEl.focus(); } function nouvelleDiscussion() { fetch(API + "/api/v1/conversations", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ domain: domaine() }) }) .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (c) { basculerDiscussion(c); ajouterMsg("sys", "Nouvelle discussion parallèle créée — elle prendra le titre de ton premier message."); }) .catch(function () { ajouterMsg("sys", "Création impossible (service injoignable ?)."); }); } function actionDiscussion(id, action) { var methode = action === "supprimer" ? "DELETE" : "POST"; var url = API + "/api/v1/conversations/" + id + (action === "supprimer" ? "" : "/" + action); return fetch(url, { method: methode, credentials: "include" }) .then(function (r) { if (!r.ok) return r.json().catch(function () { return null; }).then(function (e) { throw new Error(e && e.detail ? e.detail : "HTTP " + r.status); }); return r.json(); }); } function ligneDiscussion(c, actions) { var ligne = document.createElement("div"); ligne.className = "d-item" + (c.id === convId ? " actif" : ""); var meta = document.createElement("div"); meta.className = "d-meta"; var quand = ""; try { quand = new Date(c.updated_at * 1000).toLocaleDateString("fr-CA", { day: "numeric", month: "short" }); } catch (e) { /* date illisible */ } meta.textContent = (c.kind === "principale" ? "📌 " : "") + c.title; ligne.appendChild(meta); var sous = document.createElement("div"); sous.className = "d-sous"; sous.textContent = c.nb_messages + " message(s)" + (quand ? " · " + quand : "") + (c.kind === "principale" ? " · perpétuelle" : ""); ligne.appendChild(sous); meta.addEventListener("click", function () { basculerDiscussion(c); }); sous.addEventListener("click", function () { basculerDiscussion(c); }); if (actions && actions.length) { var btns = document.createElement("div"); btns.className = "d-actions"; actions.forEach(function (a) { var b = document.createElement("button"); b.type = "button"; b.textContent = a.label; if (a.danger) b.className = "danger"; b.addEventListener("click", function (ev) { ev.stopPropagation(); if (a.confirmer && !window.confirm(a.confirmer)) return; actionDiscussion(c.id, a.action) .then(function () { if (c.id === convId && a.action !== "desarchiver") { chargerListeDiscussions().then(function (d) { if (d.principale) { basculerDiscussion(d.principale); ajouterMsg("sys", a.action === "supprimer" ? "Discussion supprimée — retour à la discussion principale." : "Discussion archivée — retour à la discussion principale."); } }); } remplirTiroir(); }) .catch(function (e) { ajouterMsg("sys", "Action refusée : " + e.message); }); }); btns.appendChild(b); }); ligne.appendChild(btns); } return ligne; } function remplirTiroir() { tiroirEl.innerHTML = "
Chargement des discussions…
"; chargerListeDiscussions() .then(function (d) { tiroirEl.innerHTML = ""; var h = document.createElement("div"); h.className = "d-section"; h.textContent = "Discussion principale"; tiroirEl.appendChild(h); if (d.principale) tiroirEl.appendChild(ligneDiscussion(d.principale, [])); h = document.createElement("div"); h.className = "d-section"; h.textContent = "Discussions parallèles"; tiroirEl.appendChild(h); if (!d.paralleles.length) { var vide = document.createElement("div"); vide.className = "d-vide"; vide.textContent = "Aucune pour le moment — bouton « + Nouvelle »."; tiroirEl.appendChild(vide); } d.paralleles.forEach(function (c) { tiroirEl.appendChild(ligneDiscussion(c, [ { label: "Archiver", action: "archive" }, { label: "Supprimer", action: "supprimer", danger: true, confirmer: "Supprimer définitivement la discussion « " + c.title + " » ?" } ])); }); if (d.archivees && d.archivees.length) { h = document.createElement("div"); h.className = "d-section"; h.textContent = "Archivées"; tiroirEl.appendChild(h); d.archivees.forEach(function (c) { tiroirEl.appendChild(ligneDiscussion(c, [ { label: "Désarchiver", action: "desarchiver" }, { label: "Supprimer", action: "supprimer", danger: true, confirmer: "Supprimer définitivement la discussion « " + c.title + " » ?" } ])); }); } }) .catch(function () { tiroirEl.innerHTML = "
Discussions : service injoignable.
"; }); } function basculerTiroir() { tiroirEl.hidden = !tiroirEl.hidden; if (!tiroirEl.hidden) remplirTiroir(); } function chargerHistorique() { if (!convId) return; fetch(API + "/api/v1/conversations/" + convId, { credentials: "include" }) .then(function (r) { if (r.status === 404) { convId = null; stockerConv(); return null; } if (!r.ok) return null; return r.json(); }) .then(function (d) { if (!d) return; convKind = d.kind; convTitre = d.title || convTitre; majTitre(); if (!msgsEl.querySelector(".draco-msg")) { ajouterMsg("sys", (d.kind === "principale" ? "Discussion principale — le fil sans fin." : "Discussion parallèle : " + (d.title || "")) + " · domaine : " + domaine() + "."); } var sys = msgsEl.querySelector(".draco-msg.sys"); (d.messages || []).forEach(function (m) { var el = m.role === "user" ? ajouterMsg("user", m.content) : ajouterMsgMd(m.content, m.html); if (sys) { msgsEl.insertBefore(el, sys.nextSibling); sys = el; } }); msgsEl.scrollTop = msgsEl.scrollHeight; }) .catch(function () { /* historique indisponible : on repart à neuf */ }); } function chargerAujourdhui() { var bloc = document.createElement("div"); bloc.className = "draco-auj"; bloc.innerHTML = "
Aujourd'hui — vu du service d'agents
Chargement…
"; msgsEl.appendChild(bloc); msgsEl.scrollTop = msgsEl.scrollHeight; fetch(API + "/api/v1/aujourdhui", { credentials: "include" }) .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (d) { var h = "
Aujourd'hui — " + esc(d.date) + " · domaine " + esc(d.domain) + "
"; var t = d.sources.tiktak; h += "
Tâches du jour — TikTak
"; if (t.status === "ok") { if (!t.tasks.length) h += "
Aucune tâche échue aujourd'hui. 🎉
"; else h += ""; } else if (t.status === "non_configure") { h += "
Source non configurée : il manque la clé tck_ du service (voir le .env du conteneur agent).
"; } else if (t.status === "non_autorise") { h += "
Source non autorisée : la clé du service est refusée par TikTak (clé révoquée ?).
"; } else { h += "
Source TikTak " + esc(t.status) + ".
"; } var k = d.sources.kanbante; h += "
Cartes en Révision — Kanbante
"; if (k.status === "ok") { if (!k.review.length) h += "
Rien à valider. 🎉
"; else h += ""; } else if (k.status === "non_autorise") { h += "
Source non autorisée : Kanbante exige un jeton d'API pour le service — il arrive avec Kanbante 0.25.0, puis AGENT_MCP_KANBANTE_KEY dans le .env de l'agent.
"; } else { h += "
Source Kanbante " + esc(k.status) + ".
"; } bloc.innerHTML = h; msgsEl.scrollTop = msgsEl.scrollHeight; }) .catch(function () { bloc.innerHTML = "
Aujourd'hui
Service injoignable.
"; }); } function chargerApprobations() { fetch(API + "/api/v1/approvals?status=pending", { credentials: "include" }) .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (d) { if (!d.approvals.length) { ajouterMsg("sys", "Aucune approbation en attente."); return; } d.approvals.forEach(function (a) { var bloc = document.createElement("div"); bloc.className = "draco-appro"; bloc.innerHTML = "
Approbation demandée — " + esc(a.action) + "
" + "
" + esc(a.summary) + "
" + "
" + "
"; bloc.querySelector(".go").addEventListener("click", function () { decider(a.id, "approved", bloc); }); bloc.querySelector(".non").addEventListener("click", function () { decider(a.id, "rejected", bloc); }); msgsEl.appendChild(bloc); }); msgsEl.scrollTop = msgsEl.scrollHeight; }) .catch(function () { ajouterMsg("sys", "Approbations : service injoignable."); }); } function decider(id, decision, bloc) { fetch(API + "/api/v1/approvals/" + id + "/decide", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ decision: decision }) }) .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (d) { bloc.style.opacity = ".55"; bloc.querySelector(".btns").innerHTML = "Décision enregistrée : " + esc(d.status) + ""; }) .catch(function () { ajouterMsg("sys", "La décision n'a pas pu être envoyée (service injoignable ?)."); }); } /* ── Missions longues (J5) ─────────────────────────────────── * Sélecteur d'exécuteur alimenté par le registre du service : * les externes non configurés sont montrés mais désactivés — * jamais un lancement qui simule. La liste se rafraîchit à * l'ouverture et après chaque lancement/annulation. */ var STATUTS_MISSION = { queued: "En file", running: "En cours…", paused_question: "Question en attente", succeeded: "Terminée", failed: "Échouée", cancelled: "Annulée" }; function ouvrirMissions() { var bloc = document.createElement("div"); bloc.className = "draco-missions"; bloc.innerHTML = "
Missions longues
" + "" + "" + "" + "" + "
" + "
" + "
Chargement des missions…
"; msgsEl.appendChild(bloc); msgsEl.scrollTop = msgsEl.scrollHeight; fetch(API + "/api/v1/executors", { credentials: "include" }) .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (d) { var sel = bloc.querySelector(".m-exec"); sel.innerHTML = ""; d.executors.forEach(function (e) { if (e.family !== "sous-agent" && e.family !== "externe") return; var opt = document.createElement("option"); opt.value = e.id; opt.textContent = e.name + (e.family === "sous-agent" ? " (sous-agent)" : "") + (e.status === "non_configure" ? " — non configuré" : ""); if (e.status === "non_configure") opt.disabled = true; sel.appendChild(opt); }); if (!sel.options.length) sel.innerHTML = ""; }) .catch(function () { bloc.querySelector(".m-exec").innerHTML = ""; }); bloc.querySelector(".go").addEventListener("click", function () { lancerMission(bloc); }); bloc.querySelector(".rafraichir").addEventListener("click", function () { chargerMissions(bloc); }); chargerMissions(bloc); } function chargerMissions(bloc) { var zone = bloc.querySelector(".m-liste"); fetch(API + "/api/v1/missions", { credentials: "include" }) .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (d) { if (!d.missions.length) { zone.innerHTML = "
Aucune mission pour le moment.
"; return; } zone.innerHTML = ""; d.missions.slice(0, 10).forEach(function (m) { var ligne = document.createElement("div"); ligne.className = "m-item"; var entete = document.createElement("div"); entete.className = "m-entete"; entete.innerHTML = "" + esc(m.title) + "" + "" + esc(STATUTS_MISSION[m.status] || m.status) + ""; ligne.appendChild(entete); var meta = document.createElement("div"); meta.className = "m-meta"; meta.textContent = m.executor + (m.meta && m.meta.duree_s ? " · " + m.meta.duree_s + " s" : "") + (m.meta && m.meta.cout ? " · coût : " + m.meta.cout : ""); ligne.appendChild(meta); if (m.result) { var rapport = document.createElement("div"); rapport.className = "m-rapport md"; if (m.result_html) rapport.innerHTML = m.result_html; else rapport.textContent = String(m.result).slice(0, 1200); ligne.appendChild(rapport); } if (m.status === "queued" || m.status === "running" || m.status === "paused_question") { var btn = document.createElement("button"); btn.type = "button"; btn.className = "m-annuler"; btn.textContent = "Annuler"; btn.addEventListener("click", function () { fetch(API + "/api/v1/missions/" + m.id + "/cancel", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: "{}" }).then(function () { chargerMissions(bloc); }) .catch(function () { ajouterMsg("sys", "Annulation non envoyée (service injoignable ?)."); }); }); ligne.appendChild(btn); } zone.appendChild(ligne); }); }) .catch(function () { zone.innerHTML = "
Missions : service injoignable.
"; }); } function lancerMission(bloc) { var executor = bloc.querySelector(".m-exec").value; var titre = bloc.querySelector(".m-titre").value.trim(); var prompt = bloc.querySelector(".m-prompt").value.trim(); var carte = bloc.querySelector(".m-carte").value; if (!executor) { ajouterMsg("sys", "Choisis un exécuteur configuré pour lancer une mission."); return; } if (!titre || !prompt) { ajouterMsg("sys", "Mission : il faut un titre et une consigne."); return; } var corps = { executor: executor, title: titre, prompt: prompt, domain: domaine() }; if (carte) corps.kanbante_number = parseInt(carte, 10); fetch(API + "/api/v1/missions", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(corps) }) .then(function (r) { if (!r.ok) return r.json().catch(function () { return null; }).then(function (e) { throw new Error(e && e.detail ? e.detail : "HTTP " + r.status); }); return r.json(); }) .then(function () { ajouterMsg("sys", "Mission « " + titre + " » lancée — suis-la dans la liste des missions."); chargerMissions(bloc); }) .catch(function (e) { ajouterMsg("sys", "Lancement refusé : " + e.message); }); } function envoyer(ev) { ev.preventDefault(); var texte = inputEl.value.trim(); if (!texte) return; inputEl.value = ""; ajouterMsg("user", texte); sendBtn.disabled = true; var attente = ajouterMsg("agent", "Spark réfléchit…"); fetch(API + "/api/v1/chat", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: texte, domain: domaine(), conversation_id: convId }) }) .then(function (r) { if (r.status === 401) { attente.textContent = "Le service ne reconnaît pas ta session partagée (401). Connecte-toi à ObsiGate puis réessaie."; majEtat("err", "Session partagée non reconnue"); return null; } if (!r.ok) { return r.json().catch(function () { return null; }).then(function (e) { attente.textContent = (e && e.detail) ? e.detail : "Le service a répondu HTTP " + r.status + " — réessaie dans un instant."; return null; }); } return r.json(); }) .then(function (d) { if (!d) return; convId = d.conversation_id; convKind = d.kind || convKind; if (d.title) convTitre = d.title; stockerConv(); majTitre(); if (d.html) { attente.className = "draco-msg agent md"; attente.innerHTML = d.html; } else { attente.textContent = d.reply; } msgsEl.scrollTop = msgsEl.scrollHeight; if (d.tools_used && d.tools_used.length) { ajouterMsg("sys", "Outils utilisés : " + d.tools_used.join(", ") + " · modèle : " + (d.model || "?")); } majEtat("ok", "Connecté au service — domaine " + d.domain); }) .catch(function () { 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(); }); } function ouvrir() { if (!root) { construire(); demarrerDiscussions(); } root.classList.add("open"); appliquerTheme(); verifierIdentite(); setTimeout(function () { inputEl.focus(); }, 300); } function fermer() { if (root) root.classList.remove("open"); } window.DRACO_PANNEAU = { open: ouvrir, close: fermer, toggle: function () { if (root && root.classList.contains("open")) fermer(); else ouvrir(); } }; })();