/* Auteur : Simon-Pierre Boucher — contact@spboucher.ai * ka-ui/agent/ka-agent.js — KA AGENT : widget de chat commun de l'écosystème * Groupe KA. Vanilla JS, zéro dépendance, injecté sur les 13 sites. * * * v2 (2026-08-19) : ouverture directement PLEIN ÉCRAN (desktop et mobile, * bouton ▭ pour réduire en fenêtre sur desktop) ; sur téléphone le panneau se * cale sur window.visualViewport (le clavier ne fait plus grossir/défiler la * page — fond verrouillé par body position:fixed) ; rendu MARKDOWN complet en * streaming (titres, listes, tableaux, code, liens nommés, citations). * Flux SSE en direct depuis le backend central (texte token par token + * indicateur d'outil). Historique conservé en localStorage (par site). */ (function () { "use strict"; var CFG = window.KA_AGENT || {}; var SITE = CFG.site || "groupe-ka"; var ENDPOINT = CFG.endpoint || "https://www.api-ka.com/api/agent/chat"; var KEY = "ka-agent-" + SITE; var MAXLOG = 20; /* ---------- styles ---------- */ var css = ` .kaa-btn{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;width:58px;height:58px; border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b); color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif; cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center; justify-content:center;transition:transform .15s;touch-action:manipulation} .kaa-btn:hover{transform:translate(-2px,-2px)} .kaa-panel{position:fixed;z-index:2147483001;display:none;flex-direction:column;background:var(--paper,#f5f3ee); overflow:hidden;font-family:Inter,system-ui,sans-serif;touch-action:manipulation} .kaa-panel.kaa-open{display:flex} .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} .kaa-panel:not(.kaa-full){right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));width:min(392px,calc(100vw - 24px)); height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px)); border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85)} html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none;touch-action:none} .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814); color:var(--paper,#f5f3ee);flex:none;padding-top:max(11px,env(safe-area-inset-top,0px))} .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em} .kaa-head .kaa-ka{display:inline-block;background:var(--accent,#d9f26b);color:var(--on-accent,#141814); border-radius:6px;padding:1px 7px 2px;transform:rotate(-2deg);font-weight:700} .kaa-head small{font:700 9.5px "JetBrains Mono",monospace;letter-spacing:.09em;text-transform:uppercase;opacity:.65} .kaa-head .kaa-sp{flex:1} .kaa-hbtn{width:34px;height:34px;border:1.5px solid rgba(245,243,238,.4);border-radius:8px;background:none; color:var(--paper,#f5f3ee);font-size:15px;cursor:pointer;line-height:1;flex:none} .kaa-hbtn:hover{border-color:var(--accent,#d9f26b);color:var(--accent,#d9f26b)} .kaa-log{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px; overscroll-behavior:contain;-webkit-overflow-scrolling:touch} .kaa-msg{max-width:86%;padding:9px 13px;border:1.5px solid var(--ink,#141814);border-radius:12px; font-size:13.5px;line-height:1.55;word-break:break-word} .kaa-full .kaa-msg{max-width:min(86%,760px)} .kaa-full .kaa-log{align-items:stretch} .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#d9f26b);color:var(--on-accent,#141814); border-bottom-right-radius:4px;white-space:pre-wrap} .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)} .kaa-msg.kaa-a a{color:inherit;text-decoration:underline;text-underline-offset:3px;font-weight:600} .kaa-msg.kaa-a p{margin:.35em 0} .kaa-msg.kaa-a p:first-child{margin-top:0} .kaa-msg.kaa-a p:last-child{margin-bottom:0} .kaa-msg.kaa-a h1,.kaa-msg.kaa-a h2,.kaa-msg.kaa-a h3,.kaa-msg.kaa-a h4{ font:700 14px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.01em;margin:.7em 0 .3em} .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} .kaa-msg.kaa-a h1{font-size:15.5px}.kaa-msg.kaa-a h2{font-size:14.5px} .kaa-msg.kaa-a ul,.kaa-msg.kaa-a ol{margin:.35em 0;padding-left:1.35em} .kaa-msg.kaa-a li{margin:.18em 0} .kaa-msg.kaa-a code{font:600 12px "JetBrains Mono",monospace;background:var(--surface-2,#f0eee7); border:1px solid rgba(20,24,20,.15);border-radius:5px;padding:.5px 4px} .kaa-msg.kaa-a pre{margin:.45em 0;padding:9px 11px;background:var(--ink,#141814);color:var(--paper,#f5f3ee); border-radius:9px;overflow-x:auto} .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} .kaa-msg.kaa-a blockquote{margin:.45em 0;padding:.15em .8em;border-left:3px solid var(--accent,#d9f26b); background:var(--surface-2,#faf9f5);color:var(--ink-2,#4d5551)} .kaa-msg.kaa-a hr{border:0;border-top:1.5px dashed rgba(20,24,20,.3);margin:.6em 0} .kaa-msg.kaa-a table{border-collapse:collapse;margin:.45em 0;font-size:12.5px;display:block;overflow-x:auto;max-width:100%} .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} .kaa-msg.kaa-a th{background:var(--accent,#d9f26b);color:var(--on-accent,#141814); font:700 11px "Space Grotesk",system-ui,sans-serif;letter-spacing:.02em} .kaa-msg.kaa-a td{background:#fff} .kaa-tool{align-self:flex-start;font:700 10px "JetBrains Mono",monospace;letter-spacing:.07em; text-transform:uppercase;color:var(--ink-2,#4d5551);padding:3px 10px;border:1.5px dashed var(--ink-2,#4d5551); border-radius:999px;background:var(--surface-2,#faf9f5)} .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none; padding-bottom:calc(11px + env(safe-area-inset-bottom,0))} .kaa-full .kaa-in{justify-content:center} .kaa-full .kaa-in textarea{max-width:700px} .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; border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none} .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px; background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer;touch-action:manipulation} .kaa-in button:disabled{opacity:.45} .kaa-hello{font-size:12.5px;color:var(--ink-2,#4d5551);padding:4px 2px;max-width:760px} .kaa-dots::after{content:"…";animation:kaa-b 1.2s infinite} @keyframes kaa-b{0%{opacity:.2}50%{opacity:1}100%{opacity:.2}} @media (max-width:640px){.kaa-x1{display:none}} `; /* ---------- markdown complet (rendu en streaming, texte échappé d'abord) ---------- */ function esc(s) { return s.replace(/&/g, "&").replace(//g, ">"); } function inline(s) { s = esc(s); s = s.replace(/`([^`\n]+)`/g, "$1"); s = s.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1'); s = s.replace(/\*\*([^*\n]+)\*\*/g, "$1"); s = s.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\*)/g, "$1$2"); s = s.replace(/(^|[^"'>=\w])(https?:\/\/[^\s<)"']+)/g, function (m, pre, u) { return pre + '' + u.replace(/^https?:\/\/(www\.)?/, "").slice(0, 42) + ""; }); return s; } function md(src) { var lines = String(src).split("\n"), out = [], i = 0, m; function para(buf) { if (buf.length) out.push("

" + buf.map(inline).join("
") + "

"); } while (i < lines.length) { var l = lines[i]; if (/^\s*```/.test(l)) { /* bloc de code */ var code = []; i++; while (i < lines.length && !/^\s*```/.test(lines[i])) code.push(lines[i++]); i++; out.push("
" + esc(code.join("\n")) + "
"); } else if ((m = l.match(/^\s*(#{1,4})\s+(.*)$/))) { /* titres */ out.push("" + inline(m[2]) + ""); i++; } else if (/^\s*(---+|\*\*\*+)\s*$/.test(l)) { /* filet */ out.push("
"); i++; } else if (/^\s*>|^\s*>/.test(l)) { /* citation */ var q = []; while (i < lines.length && /^\s*>/.test(lines[i])) q.push(lines[i++].replace(/^\s*>\s?/, "")); out.push("
" + q.map(inline).join("
") + "
"); } else if (/^\s*\|.*\|\s*$/.test(l)) { /* tableau */ var rows = []; while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) { var cells = lines[i].trim().replace(/^\||\|$/g, "").split("|").map(function (c) { return c.trim(); }); if (!/^[:\-\s|]+$/.test(lines[i].replace(/\|/g, ""))) rows.push(cells); i++; } if (rows.length) { var t = ""; rows.forEach(function (r, ri) { var tag = ri === 0 ? "th" : "td"; t += "" + r.map(function (c) { return "<" + tag + ">" + inline(c) + ""; }).join("") + ""; }); out.push(t + "
"); } } else if (/^\s*[-*+]\s+/.test(l)) { /* liste à puces */ var ul = []; while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i])) ul.push("
  • " + inline(lines[i++].replace(/^\s*[-*+]\s+/, "")) + "
  • "); out.push(""); } else if (/^\s*\d+[.)]\s+/.test(l)) { /* liste numérotée */ var ol = []; while (i < lines.length && /^\s*\d+[.)]\s+/.test(lines[i])) ol.push("
  • " + inline(lines[i++].replace(/^\s*\d+[.)]\s+/, "")) + "
  • "); out.push("
      " + ol.join("") + "
    "); } else if (!l.trim()) { i++; } /* ligne vide */ else { /* paragraphe */ var buf = []; while (i < lines.length && lines[i].trim() && !/^\s*(```|#{1,4}\s|[-*+]\s|\d+[.)]\s|>|\||---+\s*$)/.test(lines[i])) buf.push(lines[i++]); if (!buf.length) buf.push(lines[i++] || ""); para(buf); } } return out.join(""); } /* ---------- état ---------- */ var log = []; try { log = JSON.parse(localStorage.getItem(KEY) || "[]"); } catch (e) {} function save() { try { localStorage.setItem(KEY, JSON.stringify(log.slice(-MAXLOG))); } catch (e) {} } /* ---------- DOM ---------- */ var style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); var btn = document.createElement("button"); btn.className = "kaa-btn"; btn.setAttribute("aria-label", "Ouvrir KA Agent — assistant de l'écosystème Groupe KA"); btn.innerHTML = "Ka"; var panel = document.createElement("div"); panel.className = "kaa-panel"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-label", "KA Agent"); panel.innerHTML = '
    KAAgentGroupe KA · IA' + '' + '
    ' + '
    ' + '
    ' + "
    "; document.body.appendChild(btn); document.body.appendChild(panel); var logEl = panel.querySelector(".kaa-log"); var ta = panel.querySelector("textarea"); var send = panel.querySelector(".kaa-in button"); function scroll() { logEl.scrollTop = logEl.scrollHeight; } function bubble(cls, html) { var d = document.createElement("div"); d.className = "kaa-msg " + cls; d.innerHTML = html; logEl.appendChild(d); scroll(); return d; } function toolChip(name) { var d = document.createElement("div"); d.className = "kaa-tool kaa-dots"; d.textContent = "🔎 " + name.replace(/_/g, " "); logEl.appendChild(d); scroll(); return d; } function hello() { if (log.length) return; var d = document.createElement("div"); d.className = "kaa-hello"; 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."; logEl.appendChild(d); } function render() { logEl.innerHTML = ""; hello(); log.forEach(function (m) { m.role === "user" ? bubble("kaa-u", esc(m.content)) : bubble("kaa-a", md(m.content)); }); scroll(); } /* ---------- plein écran + clavier mobile ---------- Ouverture = PLEIN ÉCRAN direct (desktop et mobile). ▭ réduit en fenêtre (desktop seulement — caché sur mobile). Tant que le plein écran est ouvert, l'arrière-plan est GELÉ (body position:fixed, scroll restauré à la fermeture) et le panneau se cale sur window.visualViewport : quand le clavier s'ouvre, le panneau rétrécit à la place de laisser iOS zoomer/pousser la page. Pas de focus clavier forcé au tap sur mobile. */ var fineInput = window.matchMedia("(hover: hover) and (pointer: fine)"); var vv = window.visualViewport; var savedY = 0; function isOpen() { return panel.classList.contains("kaa-open"); } function isFull() { return panel.classList.contains("kaa-full"); } function fitVV() { if (!vv || !isOpen() || !isFull()) { panel.style.height = ""; panel.style.top = ""; return; } panel.style.top = Math.round(vv.offsetTop) + "px"; panel.style.height = Math.round(vv.height) + "px"; scroll(); } if (vv) { vv.addEventListener("resize", fitVV); vv.addEventListener("scroll", fitVV); } function lockBg(on) { var b = document.body, h = document.documentElement; if (on && !h.classList.contains("kaa-lock")) { savedY = window.scrollY || 0; h.classList.add("kaa-lock"); b.style.position = "fixed"; b.style.top = -savedY + "px"; b.style.left = "0"; b.style.right = "0"; b.style.width = "100%"; } else if (!on && h.classList.contains("kaa-lock")) { h.classList.remove("kaa-lock"); b.style.position = ""; b.style.top = ""; b.style.left = ""; b.style.right = ""; b.style.width = ""; window.scrollTo(0, savedY); } } function sync() { lockBg(isOpen() && isFull()); fitVV(); } btn.addEventListener("click", function () { panel.classList.add("kaa-open", "kaa-full"); // plein écran d'office render(); if (fineInput.matches) ta.focus(); sync(); }); panel.querySelector(".kaa-x2").addEventListener("click", function () { panel.classList.remove("kaa-open", "kaa-full"); sync(); }); panel.querySelector(".kaa-x1").addEventListener("click", function () { panel.classList.toggle("kaa-full"); sync(); scroll(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && isOpen()) { panel.classList.remove("kaa-open", "kaa-full"); sync(); } }); /* ---------- envoi + flux SSE ---------- */ var busy = false; function ask() { var q = ta.value.trim(); if (!q || busy) return; busy = true; send.disabled = true; ta.value = ""; log.push({ role: "user", content: q }); save(); bubble("kaa-u", esc(q)); var out = bubble("kaa-a", ''); var acc = ""; var chips = []; fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ site: SITE, messages: log.slice(-MAXLOG) }), }) .then(function (res) { if (!res.ok || !res.body) throw new Error("HTTP " + res.status); var reader = res.body.getReader(); var dec = new TextDecoder(); var buf = ""; function pump() { return reader.read().then(function (r) { if (r.done) return finish(); buf += dec.decode(r.value, { stream: true }); var blocks = buf.split("\n\n"); buf = blocks.pop(); blocks.forEach(function (b) { var ev = (b.match(/^event: (.+)$/m) || [])[1]; var dm = (b.match(/^data: (.+)$/m) || [])[1]; if (!ev || !dm) return; var data = {}; try { data = JSON.parse(dm); } catch (e) {} if (ev === "delta" && data.text) { acc += data.text; out.innerHTML = md(acc); // markdown re-rendu à chaque delta scroll(); } else if (ev === "tool") { chips.push(toolChip(data.name || "recherche")); logEl.appendChild(out); // la bulle reste après le chip scroll(); } else if (ev === "error") { acc = acc || "Désolé, une erreur est survenue (" + (data.message || "inconnue") + "). Réessayez."; out.innerHTML = md(acc); } }); return pump(); }); } return pump(); }) .catch(function (e) { acc = acc || "Impossible de joindre KA Agent (" + e.message + "). Réessayez dans un instant."; out.innerHTML = md(acc); }) .then(function () { finish(); }); function finish() { if (busy === false) return; busy = false; send.disabled = false; chips.forEach(function (c) { c.classList.remove("kaa-dots"); }); if (!acc) { acc = "Je n'ai pas pu formuler de réponse — réessayez."; out.innerHTML = md(acc); } log.push({ role: "assistant", content: acc }); save(); scroll(); } } send.addEventListener("click", ask); ta.addEventListener("keydown", function (e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); ask(); } }); })();