SPB Git

spb/ka-ui

Public
11.5 KB · 252 lines javascript
1/* Auteur : Simon-Pierre Boucher — contact@spboucher.ai2 * ka-ui/agent/ka-agent.js — KA AGENT : widget de chat commun de l'écosystème3 * Groupe KA. Vanilla JS, zéro dépendance, injecté sur les 13 sites.4 *   <script>window.KA_AGENT={site:"lou-ka"}</script>5 *   <script src="/ka-agent.js" defer></script>6 * Bulle flottante en bas à droite → panneau de chat (bulle) → plein écran (⛶).7 * Flux SSE en direct depuis le backend central (texte token par token +8 * indicateur d'outil). Historique conservé en localStorage (par site).9 */10(function () {11  "use strict";12  var CFG = window.KA_AGENT || {};13  var SITE = CFG.site || "groupe-ka";14  var ENDPOINT = CFG.endpoint || "https://www.api-ka.com/api/agent/chat";15  var KEY = "ka-agent-" + SITE;16  var MAXLOG = 20;1718  /* ---------- styles ---------- */19  var css = `20  .kaa-btn{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;width:58px;height:58px;21    border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b);22    color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif;23    cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center;24    justify-content:center;transition:transform .15s}25  .kaa-btn:hover{transform:translate(-2px,-2px)}26  .kaa-panel{position:fixed;right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));z-index:2147483001;width:min(392px,calc(100vw - 24px));27    height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee);28    border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85);29    overflow:hidden;font-family:Inter,system-ui,sans-serif}30  .kaa-panel.kaa-open{display:flex}31  .kaa-panel.kaa-full{right:0;bottom:0;width:100vw;height:100vh;height:100dvh;border-radius:0;border:0;box-shadow:none}32  html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none}33  .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);34    color:var(--paper,#f5f3ee);flex:none}35  .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}36  .kaa-head .kaa-ka{display:inline-block;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);37    border-radius:6px;padding:1px 7px 2px;transform:rotate(-2deg);font-weight:700}38  .kaa-head small{font:700 9.5px "JetBrains Mono",monospace;letter-spacing:.09em;text-transform:uppercase;opacity:.65}39  .kaa-head .kaa-sp{flex:1}40  .kaa-hbtn{width:34px;height:34px;border:1.5px solid rgba(245,243,238,.4);border-radius:8px;background:none;41    color:var(--paper,#f5f3ee);font-size:15px;cursor:pointer;line-height:1}42  .kaa-hbtn:hover{border-color:var(--accent,#d9f26b);color:var(--accent,#d9f26b)}43  .kaa-log{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px;44    overscroll-behavior:contain;-webkit-overflow-scrolling:touch}45  .kaa-msg{max-width:86%;padding:9px 13px;border:1.5px solid var(--ink,#141814);border-radius:12px;46    font-size:13.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}47  .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);48    border-bottom-right-radius:4px}49  .kaa-msg.kaa-a{align-self:flex-start;background:#fff;border-bottom-left-radius:4px;box-shadow:3px 3px 0 rgba(20,24,20,.12)}50  .kaa-msg.kaa-a a{color:inherit;text-decoration:underline;text-underline-offset:3px}51  .kaa-msg.kaa-a b{font-weight:700}52  .kaa-tool{align-self:flex-start;font:700 10px "JetBrains Mono",monospace;letter-spacing:.07em;53    text-transform:uppercase;color:var(--ink-2,#4d5551);padding:3px 10px;border:1.5px dashed var(--ink-2,#4d5551);54    border-radius:999px;background:var(--surface-2,#faf9f5)}55  .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none;56    padding-bottom:calc(11px + env(safe-area-inset-bottom,0))}57  .kaa-in textarea{flex:1;resize:none;min-height:44px;max-height:110px;padding:10px 12px;font:16px/1.4 Inter,system-ui,sans-serif;58    border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none}59  .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px;60    background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer}61  .kaa-in button:disabled{opacity:.45}62  .kaa-hello{font-size:12.5px;color:var(--ink-2,#4d5551);padding:4px 2px}63  .kaa-dots::after{content:"…";animation:kaa-b 1.2s infinite}64  @keyframes kaa-b{0%{opacity:.2}50%{opacity:1}100%{opacity:.2}}65  @media (max-width:560px){.kaa-panel{right:8px;left:8px;width:auto}}66  `;6768  /* ---------- markdown minimal (gras, liens, puces) — texte échappé d'abord ---------- */69  function esc(s) {70    return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");71  }72  function md(s) {73    return esc(s)74      .replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>")75      .replace(/(https?:\/\/[^\s<)"']+)/g, function (u) {76        return '<a href="' + u + '" target="_blank" rel="noopener noreferrer">' + u.replace(/^https?:\/\/(www\.)?/, "").slice(0, 40) + "</a>";77      });78  }7980  /* ---------- état ---------- */81  var log = [];82  try { log = JSON.parse(localStorage.getItem(KEY) || "[]"); } catch (e) {}83  function save() { try { localStorage.setItem(KEY, JSON.stringify(log.slice(-MAXLOG))); } catch (e) {} }8485  /* ---------- DOM ---------- */86  var style = document.createElement("style");87  style.textContent = css;88  document.head.appendChild(style);8990  var btn = document.createElement("button");91  btn.className = "kaa-btn";92  btn.setAttribute("aria-label", "Ouvrir KA Agent — assistant de l'écosystème Groupe KA");93  btn.innerHTML = "Ka";9495  var panel = document.createElement("div");96  panel.className = "kaa-panel";97  panel.setAttribute("role", "dialog");98  panel.setAttribute("aria-label", "KA Agent");99  panel.innerHTML =100    '<div class="kaa-head"><b>KA<span class="kaa-ka">Agent</span></b><small>Groupe KA · IA</small><span class="kaa-sp"></span>' +101    '<button class="kaa-hbtn kaa-x1" title="Plein écran" aria-label="Plein écran">⛶</button>' +102    '<button class="kaa-hbtn kaa-x2" title="Fermer" aria-label="Fermer">✕</button></div>' +103    '<div class="kaa-log"></div>' +104    '<div class="kaa-in"><textarea rows="1" placeholder="Posez votre question…" aria-label="Votre question"></textarea>' +105    "<button aria-label=\"Envoyer\">➤</button></div>";106  document.body.appendChild(btn);107  document.body.appendChild(panel);108109  var logEl = panel.querySelector(".kaa-log");110  var ta = panel.querySelector("textarea");111  var send = panel.querySelector(".kaa-in button");112113  function scroll() { logEl.scrollTop = logEl.scrollHeight; }114  function bubble(cls, html) {115    var d = document.createElement("div");116    d.className = "kaa-msg " + cls;117    d.innerHTML = html;118    logEl.appendChild(d);119    scroll();120    return d;121  }122  function toolChip(name) {123    var d = document.createElement("div");124    d.className = "kaa-tool kaa-dots";125    d.textContent = "🔎 " + name.replace(/_/g, " ");126    logEl.appendChild(d);127    scroll();128    return d;129  }130131  function hello() {132    if (log.length) return;133    var d = document.createElement("div");134    d.className = "kaa-hello";135    d.textContent = "👋 Je suis KA Agent, l'assistant de l'écosystème Groupe KA. Posez-moi une question sur ce site, ses données (prix, annonces, stats…) ou sur le groupe.";136    logEl.appendChild(d);137  }138  function render() {139    logEl.innerHTML = "";140    hello();141    log.forEach(function (m) { bubble(m.role === "user" ? "kaa-u" : "kaa-a", md(m.content)); });142    scroll();143  }144145  /* ---------- ouverture / plein écran ---------- */146  /* Sur téléphone : panneau plein écran d'office + scroll d'arrière-plan147     verrouillé tant qu'il est ouvert (html.kaa-lock). Pas de focus clavier148     forcé au tap (le clavier ne s'ouvre que si l'utilisateur touche le champ). */149  var mobileMq = window.matchMedia("(max-width: 640px)");150  var fineInput = window.matchMedia("(hover: hover) and (pointer: fine)");151  function syncLock() {152    var open = panel.classList.contains("kaa-open");153    if (open && mobileMq.matches) panel.classList.add("kaa-full");154    document.documentElement.classList.toggle(155      "kaa-lock", open && panel.classList.contains("kaa-full"));156  }157  btn.addEventListener("click", function () {158    panel.classList.toggle("kaa-open");159    if (panel.classList.contains("kaa-open")) {160      render();161      if (fineInput.matches) ta.focus();162    }163    syncLock();164  });165  panel.querySelector(".kaa-x2").addEventListener("click", function () {166    panel.classList.remove("kaa-open", "kaa-full");167    syncLock();168  });169  panel.querySelector(".kaa-x1").addEventListener("click", function () {170    panel.classList.toggle("kaa-full");171    syncLock();172    scroll();173  });174175  /* ---------- envoi + flux SSE ---------- */176  var busy = false;177  function ask() {178    var q = ta.value.trim();179    if (!q || busy) return;180    busy = true;181    send.disabled = true;182    ta.value = "";183    log.push({ role: "user", content: q });184    save();185    bubble("kaa-u", md(q));186    var out = bubble("kaa-a", '<span class="kaa-dots"></span>');187    var acc = "";188    var chips = [];189190    fetch(ENDPOINT, {191      method: "POST",192      headers: { "Content-Type": "application/json" },193      body: JSON.stringify({ site: SITE, messages: log.slice(-MAXLOG) }),194    })195      .then(function (res) {196        if (!res.ok || !res.body) throw new Error("HTTP " + res.status);197        var reader = res.body.getReader();198        var dec = new TextDecoder();199        var buf = "";200        function pump() {201          return reader.read().then(function (r) {202            if (r.done) return finish();203            buf += dec.decode(r.value, { stream: true });204            var blocks = buf.split("\n\n");205            buf = blocks.pop();206            blocks.forEach(function (b) {207              var ev = (b.match(/^event: (.+)$/m) || [])[1];208              var dm = (b.match(/^data: (.+)$/m) || [])[1];209              if (!ev || !dm) return;210              var data = {};211              try { data = JSON.parse(dm); } catch (e) {}212              if (ev === "delta" && data.text) {213                acc += data.text;214                out.innerHTML = md(acc);215                scroll();216              } else if (ev === "tool") {217                chips.push(toolChip(data.name || "recherche"));218                logEl.appendChild(out); // la bulle reste après le chip219                scroll();220              } else if (ev === "error") {221                acc = acc || "Désolé, une erreur est survenue (" + (data.message || "inconnue") + "). Réessayez.";222                out.innerHTML = md(acc);223              }224            });225            return pump();226          });227        }228        return pump();229      })230      .catch(function (e) {231        acc = acc || "Impossible de joindre KA Agent (" + e.message + "). Réessayez dans un instant.";232        out.innerHTML = md(acc);233      })234      .then(function () { finish(); });235236    function finish() {237      if (busy === false) return;238      busy = false;239      send.disabled = false;240      chips.forEach(function (c) { c.classList.remove("kaa-dots"); });241      if (!acc) { acc = "Je n'ai pas pu formuler de réponse — réessayez."; out.innerHTML = md(acc); }242      log.push({ role: "assistant", content: acc });243      save();244      scroll();245    }246  }247  send.addEventListener("click", ask);248  ta.addEventListener("keydown", function (e) {249    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); ask(); }250  });251})();252
Files · aaba5c95c621ed3a38b9ccea2a376f1eaeac9e16
Menu

Theme