/* Panneau Spark — branchement au service d'agents (Fusion, jalons J1-J2). * * 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 * GET /api/v1/conversations/… — historique de la conversation * États honnêtes : service injoignable / session non reconnue / LLM non * configuré sont affichés tels quels, jamais masqués. */ (function () { "use strict"; var API = "https://agent.dracodev.net"; var convId = null; try { convId = sessionStorage.getItem("draco_panneau_conv") || null; } catch (e) { /* stockage indispo */ } var etatConnu = false; var historiqueCharge = false; var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null; function domaine() { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } } 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…' + '
' + '
' + ' ' + ' ' + '
' + '
' + '
' + ' ' + '
' + '
Jalon J2 : conversation réelle — Spark peut lire tes tâches et tes cartes via les outils des apps ; toute écriture 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]"); root.querySelector(".fermer").addEventListener("click", fermer); root.querySelector('[data-outil="aujourdhui"]').addEventListener("click", chargerAujourdhui); root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations); root.querySelector("form").addEventListener("submit", envoyer); document.addEventListener("keydown", function (ev) { if (ev.key === "Escape" && root.classList.contains("open")) fermer(); }); ajouterMsg("sys", "Conversation avec le service d'agents — domaine : " + domaine() + "."); } 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; } function majEtat(cls, texte) { etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls; etatEl.querySelector(".txt").textContent = texte; } 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]; }); } 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 ?)."); }); } function chargerHistorique() { if (historiqueCharge || !convId) return; historiqueCharge = true; fetch(API + "/api/v1/conversations/" + convId, { credentials: "include" }) .then(function (r) { if (r.status === 404) { convId = null; try { sessionStorage.removeItem("draco_panneau_conv"); } catch (e) { /* ok */ } return null; } if (!r.ok) return null; return r.json(); }) .then(function (d) { if (!d || !d.messages || !d.messages.length) return; var sys = msgsEl.querySelector(".draco-msg.sys"); d.messages.forEach(function (m) { var el = ajouterMsg(m.role === "user" ? "user" : "agent", m.content); if (sys) msgsEl.insertBefore(el, sys.nextSibling), sys = el; }); msgsEl.scrollTop = msgsEl.scrollHeight; }) .catch(function () { /* historique indisponible : la conversation repart à neuf */ }); } 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; try { sessionStorage.setItem("draco_panneau_conv", convId); } catch (e) { /* ok */ } attente.textContent = d.reply; 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(); root.classList.add("open"); verifierIdentite(); chargerHistorique(); 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(); } }; })();