/* Panneau Spark — branchement au service d'agents (Fusion, J1 → J5). * * 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 * GET /api/v1/executors — registre (noyau, sous-agents J3, * externes J4 : état de configuration) * POST /api/v1/missions — missions longues (J5) : lancement, * suivi, rapport, annulation * É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; 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…' + '
' + '
' + ' ' + ' ' + ' ' + '
' + '
' + '
' + ' ' + '
' + '
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]"); root.querySelector(".fermer").addEventListener("click", fermer); 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); 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 ?)."); }); } /* ── 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 rapport = m.result ? "
" + esc(String(m.result).slice(0, 700)) + "
" : ""; ligne.innerHTML = "
" + esc(m.title) + "" + "" + esc(STATUTS_MISSION[m.status] || m.status) + "
" + "
" + esc(m.executor) + (m.meta && m.meta.duree_s ? " · " + esc(m.meta.duree_s) + " s" : "") + (m.meta && m.meta.cout ? " · coût : " + esc(m.meta.cout) : "") + "
" + 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 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(); } }; })();