SPB Git

spb/ka-ui

Public
18.4 KB · 381 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 * v2 (2026-08-19) : ouverture directement PLEIN ÉCRAN (desktop et mobile,7 * bouton ▭ pour réduire en fenêtre sur desktop) ; sur téléphone le panneau se8 * cale sur window.visualViewport (le clavier ne fait plus grossir/défiler la9 * page — fond verrouillé par body position:fixed) ; rendu MARKDOWN complet en10 * streaming (titres, listes, tableaux, code, liens nommés, citations).11 * Flux SSE en direct depuis le backend central (texte token par token +12 * indicateur d'outil). Historique conservé en localStorage (par site).13 */14(function () {15  "use strict";16  var CFG = window.KA_AGENT || {};17  var SITE = CFG.site || "groupe-ka";18  var ENDPOINT = CFG.endpoint || "https://www.api-ka.com/api/agent/chat";19  var KEY = "ka-agent-" + SITE;20  var MAXLOG = 20;2122  /* ---------- styles ---------- */23  var css = `24  .kaa-btn{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;width:58px;height:58px;25    border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b);26    color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif;27    cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center;28    justify-content:center;transition:transform .15s;touch-action:manipulation}29  .kaa-btn:hover{transform:translate(-2px,-2px)}30  .kaa-panel{position:fixed;z-index:2147483001;display:none;flex-direction:column;background:var(--paper,#f5f3ee);31    overflow:hidden;font-family:Inter,system-ui,sans-serif;touch-action:manipulation}32  .kaa-panel.kaa-open{display:flex}33  .kaa-panel.kaa-full{left:0;top:0;right:0;bottom:0;width:100vw;height:100vh;height:100dvh;border:0;border-radius:0;box-shadow:none}34  .kaa-panel:not(.kaa-full){right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));width:min(392px,calc(100vw - 24px));35    height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px));36    border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85)}37  html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none;touch-action:none}38  .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);39    color:var(--paper,#f5f3ee);flex:none;padding-top:max(11px,env(safe-area-inset-top,0px))}40  .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}41  .kaa-head .kaa-ka{display:inline-block;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);42    border-radius:6px;padding:1px 7px 2px;transform:rotate(-2deg);font-weight:700}43  .kaa-head small{font:700 9.5px "JetBrains Mono",monospace;letter-spacing:.09em;text-transform:uppercase;opacity:.65}44  .kaa-head .kaa-sp{flex:1}45  .kaa-hbtn{width:34px;height:34px;border:1.5px solid rgba(245,243,238,.4);border-radius:8px;background:none;46    color:var(--paper,#f5f3ee);font-size:15px;cursor:pointer;line-height:1;flex:none}47  .kaa-hbtn:hover{border-color:var(--accent,#d9f26b);color:var(--accent,#d9f26b)}48  .kaa-log{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px;49    overscroll-behavior:contain;-webkit-overflow-scrolling:touch}50  .kaa-msg{max-width:86%;padding:9px 13px;border:1.5px solid var(--ink,#141814);border-radius:12px;51    font-size:13.5px;line-height:1.55;word-break:break-word}52  .kaa-full .kaa-msg{max-width:min(86%,760px)}53  .kaa-full .kaa-log{align-items:stretch}54  .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);55    border-bottom-right-radius:4px;white-space:pre-wrap}56  .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)}57  .kaa-msg.kaa-a a{color:inherit;text-decoration:underline;text-underline-offset:3px;font-weight:600}58  .kaa-msg.kaa-a p{margin:.35em 0}59  .kaa-msg.kaa-a p:first-child{margin-top:0}60  .kaa-msg.kaa-a p:last-child{margin-bottom:0}61  .kaa-msg.kaa-a h1,.kaa-msg.kaa-a h2,.kaa-msg.kaa-a h3,.kaa-msg.kaa-a h4{62    font:700 14px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.01em;margin:.7em 0 .3em}63  .kaa-msg.kaa-a h1:first-child,.kaa-msg.kaa-a h2:first-child,.kaa-msg.kaa-a h3:first-child,.kaa-msg.kaa-a h4:first-child{margin-top:.1em}64  .kaa-msg.kaa-a h1{font-size:15.5px}.kaa-msg.kaa-a h2{font-size:14.5px}65  .kaa-msg.kaa-a ul,.kaa-msg.kaa-a ol{margin:.35em 0;padding-left:1.35em}66  .kaa-msg.kaa-a li{margin:.18em 0}67  .kaa-msg.kaa-a code{font:600 12px "JetBrains Mono",monospace;background:var(--surface-2,#f0eee7);68    border:1px solid rgba(20,24,20,.15);border-radius:5px;padding:.5px 4px}69  .kaa-msg.kaa-a pre{margin:.45em 0;padding:9px 11px;background:var(--ink,#141814);color:var(--paper,#f5f3ee);70    border-radius:9px;overflow-x:auto}71  .kaa-msg.kaa-a pre code{background:none;border:0;color:inherit;padding:0;font-weight:400;font-size:11.5px;line-height:1.5}72  .kaa-msg.kaa-a blockquote{margin:.45em 0;padding:.15em .8em;border-left:3px solid var(--accent,#d9f26b);73    background:var(--surface-2,#faf9f5);color:var(--ink-2,#4d5551)}74  .kaa-msg.kaa-a hr{border:0;border-top:1.5px dashed rgba(20,24,20,.3);margin:.6em 0}75  .kaa-msg.kaa-a table{border-collapse:collapse;margin:.45em 0;font-size:12.5px;display:block;overflow-x:auto;max-width:100%}76  .kaa-msg.kaa-a th,.kaa-msg.kaa-a td{border:1.5px solid var(--ink,#141814);padding:4px 9px;text-align:left;white-space:nowrap}77  .kaa-msg.kaa-a th{background:var(--accent,#d9f26b);color:var(--on-accent,#141814);78    font:700 11px "Space Grotesk",system-ui,sans-serif;letter-spacing:.02em}79  .kaa-msg.kaa-a td{background:#fff}80  .kaa-tool{align-self:flex-start;font:700 10px "JetBrains Mono",monospace;letter-spacing:.07em;81    text-transform:uppercase;color:var(--ink-2,#4d5551);padding:3px 10px;border:1.5px dashed var(--ink-2,#4d5551);82    border-radius:999px;background:var(--surface-2,#faf9f5)}83  .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none;84    padding-bottom:calc(11px + env(safe-area-inset-bottom,0))}85  .kaa-full .kaa-in{justify-content:center}86  .kaa-full .kaa-in textarea{max-width:700px}87  .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;88    border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none}89  .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px;90    background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer;touch-action:manipulation}91  .kaa-in button:disabled{opacity:.45}92  .kaa-hello{font-size:12.5px;color:var(--ink-2,#4d5551);padding:4px 2px;max-width:760px}93  .kaa-dots::after{content:"…";animation:kaa-b 1.2s infinite}94  @keyframes kaa-b{0%{opacity:.2}50%{opacity:1}100%{opacity:.2}}95  @media (max-width:640px){.kaa-x1{display:none}}96  `;9798  /* ---------- markdown complet (rendu en streaming, texte échappé d'abord) ---------- */99  function esc(s) {100    return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");101  }102  function inline(s) {103    s = esc(s);104    s = s.replace(/`([^`\n]+)`/g, "<code>$1</code>");105    s = s.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g,106      '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');107    s = s.replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>");108    s = s.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\*)/g, "$1<i>$2</i>");109    s = s.replace(/(^|[^"'>=\w])(https?:\/\/[^\s<)"']+)/g, function (m, pre, u) {110      return pre + '<a href="' + u + '" target="_blank" rel="noopener noreferrer">' +111        u.replace(/^https?:\/\/(www\.)?/, "").slice(0, 42) + "</a>";112    });113    return s;114  }115  function md(src) {116    var lines = String(src).split("\n"), out = [], i = 0, m;117    function para(buf) { if (buf.length) out.push("<p>" + buf.map(inline).join("<br>") + "</p>"); }118    while (i < lines.length) {119      var l = lines[i];120      if (/^\s*```/.test(l)) {                                    /* bloc de code */121        var code = []; i++;122        while (i < lines.length && !/^\s*```/.test(lines[i])) code.push(lines[i++]);123        i++;124        out.push("<pre><code>" + esc(code.join("\n")) + "</code></pre>");125      } else if ((m = l.match(/^\s*(#{1,4})\s+(.*)$/))) {         /* titres */126        out.push("<h" + m[1].length + ">" + inline(m[2]) + "</h" + m[1].length + ">"); i++;127      } else if (/^\s*(---+|\*\*\*+)\s*$/.test(l)) {              /* filet */128        out.push("<hr>"); i++;129      } else if (/^\s*&gt;|^\s*>/.test(l)) {                      /* citation */130        var q = [];131        while (i < lines.length && /^\s*>/.test(lines[i])) q.push(lines[i++].replace(/^\s*>\s?/, ""));132        out.push("<blockquote>" + q.map(inline).join("<br>") + "</blockquote>");133      } else if (/^\s*\|.*\|\s*$/.test(l)) {                      /* tableau */134        var rows = [];135        while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) {136          var cells = lines[i].trim().replace(/^\||\|$/g, "").split("|").map(function (c) { return c.trim(); });137          if (!/^[:\-\s|]+$/.test(lines[i].replace(/\|/g, ""))) rows.push(cells);138          i++;139        }140        if (rows.length) {141          var t = "<table>";142          rows.forEach(function (r, ri) {143            var tag = ri === 0 ? "th" : "td";144            t += "<tr>" + r.map(function (c) { return "<" + tag + ">" + inline(c) + "</" + tag + ">"; }).join("") + "</tr>";145          });146          out.push(t + "</table>");147        }148      } else if (/^\s*[-*+]\s+/.test(l)) {                        /* liste à puces */149        var ul = [];150        while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i]))151          ul.push("<li>" + inline(lines[i++].replace(/^\s*[-*+]\s+/, "")) + "</li>");152        out.push("<ul>" + ul.join("") + "</ul>");153      } else if (/^\s*\d+[.)]\s+/.test(l)) {                      /* liste numérotée */154        var ol = [];155        while (i < lines.length && /^\s*\d+[.)]\s+/.test(lines[i]))156          ol.push("<li>" + inline(lines[i++].replace(/^\s*\d+[.)]\s+/, "")) + "</li>");157        out.push("<ol>" + ol.join("") + "</ol>");158      } else if (!l.trim()) { i++; }                              /* ligne vide */159      else {                                                      /* paragraphe */160        var buf = [];161        while (i < lines.length && lines[i].trim() &&162          !/^\s*(```|#{1,4}\s|[-*+]\s|\d+[.)]\s|>|\||---+\s*$)/.test(lines[i]))163          buf.push(lines[i++]);164        if (!buf.length) buf.push(lines[i++] || "");165        para(buf);166      }167    }168    return out.join("");169  }170171  /* ---------- état ---------- */172  var log = [];173  try { log = JSON.parse(localStorage.getItem(KEY) || "[]"); } catch (e) {}174  function save() { try { localStorage.setItem(KEY, JSON.stringify(log.slice(-MAXLOG))); } catch (e) {} }175176  /* ---------- DOM ---------- */177  var style = document.createElement("style");178  style.textContent = css;179  document.head.appendChild(style);180181  var btn = document.createElement("button");182  btn.className = "kaa-btn";183  btn.setAttribute("aria-label", "Ouvrir KA Agent — assistant de l'écosystème Groupe KA");184  btn.innerHTML = "Ka";185186  var panel = document.createElement("div");187  panel.className = "kaa-panel";188  panel.setAttribute("role", "dialog");189  panel.setAttribute("aria-label", "KA Agent");190  panel.innerHTML =191    '<div class="kaa-head"><b>KA<span class="kaa-ka">Agent</span></b><small>Groupe KA · IA</small><span class="kaa-sp"></span>' +192    '<button class="kaa-hbtn kaa-x1" title="Réduire / agrandir" aria-label="Réduire ou agrandir">▭</button>' +193    '<button class="kaa-hbtn kaa-x2" title="Fermer" aria-label="Fermer">✕</button></div>' +194    '<div class="kaa-log"></div>' +195    '<div class="kaa-in"><textarea rows="1" enterkeyhint="send" autocapitalize="sentences" autocomplete="off" placeholder="Posez votre question…" aria-label="Votre question"></textarea>' +196    "<button aria-label=\"Envoyer\">➤</button></div>";197  document.body.appendChild(btn);198  document.body.appendChild(panel);199200  var logEl = panel.querySelector(".kaa-log");201  var ta = panel.querySelector("textarea");202  var send = panel.querySelector(".kaa-in button");203204  function scroll() { logEl.scrollTop = logEl.scrollHeight; }205  function bubble(cls, html) {206    var d = document.createElement("div");207    d.className = "kaa-msg " + cls;208    d.innerHTML = html;209    logEl.appendChild(d);210    scroll();211    return d;212  }213  function toolChip(name) {214    var d = document.createElement("div");215    d.className = "kaa-tool kaa-dots";216    d.textContent = "🔎 " + name.replace(/_/g, " ");217    logEl.appendChild(d);218    scroll();219    return d;220  }221222  function hello() {223    if (log.length) return;224    var d = document.createElement("div");225    d.className = "kaa-hello";226    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, fiches, stats…) ou sur le groupe.";227    logEl.appendChild(d);228  }229  function render() {230    logEl.innerHTML = "";231    hello();232    log.forEach(function (m) {233      m.role === "user" ? bubble("kaa-u", esc(m.content)) : bubble("kaa-a", md(m.content));234    });235    scroll();236  }237238  /* ---------- plein écran + clavier mobile ----------239     Ouverture = PLEIN ÉCRAN direct (desktop et mobile). ▭ réduit en fenêtre240     (desktop seulement — caché sur mobile). Tant que le plein écran est ouvert,241     l'arrière-plan est GELÉ (body position:fixed, scroll restauré à la242     fermeture) et le panneau se cale sur window.visualViewport : quand le243     clavier s'ouvre, le panneau rétrécit à la place de laisser iOS244     zoomer/pousser la page. Pas de focus clavier forcé au tap sur mobile. */245  var fineInput = window.matchMedia("(hover: hover) and (pointer: fine)");246  var vv = window.visualViewport;247  var savedY = 0;248249  function isOpen() { return panel.classList.contains("kaa-open"); }250  function isFull() { return panel.classList.contains("kaa-full"); }251252  function fitVV() {253    if (!vv || !isOpen() || !isFull()) { panel.style.height = ""; panel.style.top = ""; return; }254    panel.style.top = Math.round(vv.offsetTop) + "px";255    panel.style.height = Math.round(vv.height) + "px";256    scroll();257  }258  if (vv) {259    vv.addEventListener("resize", fitVV);260    vv.addEventListener("scroll", fitVV);261  }262263  function lockBg(on) {264    var b = document.body, h = document.documentElement;265    if (on && !h.classList.contains("kaa-lock")) {266      savedY = window.scrollY || 0;267      h.classList.add("kaa-lock");268      b.style.position = "fixed";269      b.style.top = -savedY + "px";270      b.style.left = "0"; b.style.right = "0"; b.style.width = "100%";271    } else if (!on && h.classList.contains("kaa-lock")) {272      h.classList.remove("kaa-lock");273      b.style.position = ""; b.style.top = ""; b.style.left = ""; b.style.right = ""; b.style.width = "";274      window.scrollTo(0, savedY);275    }276  }277  function sync() {278    lockBg(isOpen() && isFull());279    fitVV();280  }281282  btn.addEventListener("click", function () {283    panel.classList.add("kaa-open", "kaa-full"); // plein écran d'office284    render();285    if (fineInput.matches) ta.focus();286    sync();287  });288  panel.querySelector(".kaa-x2").addEventListener("click", function () {289    panel.classList.remove("kaa-open", "kaa-full");290    sync();291  });292  panel.querySelector(".kaa-x1").addEventListener("click", function () {293    panel.classList.toggle("kaa-full");294    sync();295    scroll();296  });297  document.addEventListener("keydown", function (e) {298    if (e.key === "Escape" && isOpen()) {299      panel.classList.remove("kaa-open", "kaa-full");300      sync();301    }302  });303304  /* ---------- envoi + flux SSE ---------- */305  var busy = false;306  function ask() {307    var q = ta.value.trim();308    if (!q || busy) return;309    busy = true;310    send.disabled = true;311    ta.value = "";312    log.push({ role: "user", content: q });313    save();314    bubble("kaa-u", esc(q));315    var out = bubble("kaa-a", '<span class="kaa-dots"></span>');316    var acc = "";317    var chips = [];318319    fetch(ENDPOINT, {320      method: "POST",321      headers: { "Content-Type": "application/json" },322      body: JSON.stringify({ site: SITE, messages: log.slice(-MAXLOG) }),323    })324      .then(function (res) {325        if (!res.ok || !res.body) throw new Error("HTTP " + res.status);326        var reader = res.body.getReader();327        var dec = new TextDecoder();328        var buf = "";329        function pump() {330          return reader.read().then(function (r) {331            if (r.done) return finish();332            buf += dec.decode(r.value, { stream: true });333            var blocks = buf.split("\n\n");334            buf = blocks.pop();335            blocks.forEach(function (b) {336              var ev = (b.match(/^event: (.+)$/m) || [])[1];337              var dm = (b.match(/^data: (.+)$/m) || [])[1];338              if (!ev || !dm) return;339              var data = {};340              try { data = JSON.parse(dm); } catch (e) {}341              if (ev === "delta" && data.text) {342                acc += data.text;343                out.innerHTML = md(acc);   // markdown re-rendu à chaque delta344                scroll();345              } else if (ev === "tool") {346                chips.push(toolChip(data.name || "recherche"));347                logEl.appendChild(out); // la bulle reste après le chip348                scroll();349              } else if (ev === "error") {350                acc = acc || "Désolé, une erreur est survenue (" + (data.message || "inconnue") + "). Réessayez.";351                out.innerHTML = md(acc);352              }353            });354            return pump();355          });356        }357        return pump();358      })359      .catch(function (e) {360        acc = acc || "Impossible de joindre KA Agent (" + e.message + "). Réessayez dans un instant.";361        out.innerHTML = md(acc);362      })363      .then(function () { finish(); });364365    function finish() {366      if (busy === false) return;367      busy = false;368      send.disabled = false;369      chips.forEach(function (c) { c.classList.remove("kaa-dots"); });370      if (!acc) { acc = "Je n'ai pas pu formuler de réponse — réessayez."; out.innerHTML = md(acc); }371      log.push({ role: "assistant", content: acc });372      save();373      scroll();374    }375  }376  send.addEventListener("click", ask);377  ta.addEventListener("keydown", function (e) {378    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); ask(); }379  });380})();381
Files · laptop-wip-20261002
Menu

Theme