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 * v3 (2026-08-22) : bulle DÉPLAÇABLE au doigt/à la souris (pointer events,14 * seuil 6 px pour distinguer clic et glisser, bornée aux marges de l'écran,15 * position mémorisée par site en localStorage, re-bornée au resize) ; en mode16 * fenêtré le panneau s'ancre du côté où se trouve la bulle.17 */18(function () {19 "use strict";20 var CFG = window.KA_AGENT || {};21 var SITE = CFG.site || "groupe-ka";22 var ENDPOINT = CFG.endpoint || "https://www.api-ka.com/api/agent/chat";23 var KEY = "ka-agent-" + SITE;24 var MAXLOG = 20;2526 /* ---------- styles ---------- */27 var css = `28 .kaa-btn{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;width:58px;height:58px;29 border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b);30 color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif;31 cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center;32 justify-content:center;transition:transform .15s;touch-action:none}33 .kaa-btn:hover{transform:translate(-2px,-2px)}34 .kaa-btn.kaa-drag{transition:none;cursor:grabbing}35 .kaa-btn.kaa-drag:hover{transform:none}36 .kaa-panel{position:fixed;z-index:2147483001;display:none;flex-direction:column;background:var(--paper,#f5f3ee);37 overflow:hidden;font-family:Inter,system-ui,sans-serif;touch-action:manipulation}38 .kaa-panel.kaa-open{display:flex}39 .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}40 .kaa-panel:not(.kaa-full){right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));width:min(392px,calc(100vw - 24px));41 height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px));42 border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85)}43 .kaa-panel.kaa-left:not(.kaa-full){right:auto;left:16px}44 html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none;touch-action:none}45 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);46 color:var(--paper,#f5f3ee);flex:none;padding-top:max(11px,env(safe-area-inset-top,0px))}47 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}48 .kaa-head .kaa-ka{display:inline-block;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);49 border-radius:6px;padding:1px 7px 2px;transform:rotate(-2deg);font-weight:700}50 .kaa-head small{font:700 9.5px "JetBrains Mono",monospace;letter-spacing:.09em;text-transform:uppercase;opacity:.65}51 .kaa-head .kaa-sp{flex:1}52 .kaa-hbtn{width:34px;height:34px;border:1.5px solid rgba(245,243,238,.4);border-radius:8px;background:none;53 color:var(--paper,#f5f3ee);font-size:15px;cursor:pointer;line-height:1;flex:none}54 .kaa-hbtn:hover{border-color:var(--accent,#d9f26b);color:var(--accent,#d9f26b)}55 .kaa-log{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px;56 overscroll-behavior:contain;-webkit-overflow-scrolling:touch}57 .kaa-msg{max-width:86%;padding:9px 13px;border:1.5px solid var(--ink,#141814);border-radius:12px;58 font-size:13.5px;line-height:1.55;word-break:break-word}59 .kaa-full .kaa-msg{max-width:min(86%,760px)}60 .kaa-full .kaa-log{align-items:stretch}61 .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);62 border-bottom-right-radius:4px;white-space:pre-wrap}63 .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)}64 .kaa-msg.kaa-a a{color:inherit;text-decoration:underline;text-underline-offset:3px;font-weight:600}65 .kaa-msg.kaa-a p{margin:.35em 0}66 .kaa-msg.kaa-a p:first-child{margin-top:0}67 .kaa-msg.kaa-a p:last-child{margin-bottom:0}68 .kaa-msg.kaa-a h1,.kaa-msg.kaa-a h2,.kaa-msg.kaa-a h3,.kaa-msg.kaa-a h4{69 font:700 14px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.01em;margin:.7em 0 .3em}70 .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}71 .kaa-msg.kaa-a h1{font-size:15.5px}.kaa-msg.kaa-a h2{font-size:14.5px}72 .kaa-msg.kaa-a ul,.kaa-msg.kaa-a ol{margin:.35em 0;padding-left:1.35em}73 .kaa-msg.kaa-a li{margin:.18em 0}74 .kaa-msg.kaa-a code{font:600 12px "JetBrains Mono",monospace;background:var(--surface-2,#f0eee7);75 border:1px solid rgba(20,24,20,.15);border-radius:5px;padding:.5px 4px}76 .kaa-msg.kaa-a pre{margin:.45em 0;padding:9px 11px;background:var(--ink,#141814);color:var(--paper,#f5f3ee);77 border-radius:9px;overflow-x:auto}78 .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}79 .kaa-msg.kaa-a blockquote{margin:.45em 0;padding:.15em .8em;border-left:3px solid var(--accent,#d9f26b);80 background:var(--surface-2,#faf9f5);color:var(--ink-2,#4d5551)}81 .kaa-msg.kaa-a hr{border:0;border-top:1.5px dashed rgba(20,24,20,.3);margin:.6em 0}82 .kaa-msg.kaa-a table{border-collapse:collapse;margin:.45em 0;font-size:12.5px;display:block;overflow-x:auto;max-width:100%}83 .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}84 .kaa-msg.kaa-a th{background:var(--accent,#d9f26b);color:var(--on-accent,#141814);85 font:700 11px "Space Grotesk",system-ui,sans-serif;letter-spacing:.02em}86 .kaa-msg.kaa-a td{background:#fff}87 .kaa-tool{align-self:flex-start;font:700 10px "JetBrains Mono",monospace;letter-spacing:.07em;88 text-transform:uppercase;color:var(--ink-2,#4d5551);padding:3px 10px;border:1.5px dashed var(--ink-2,#4d5551);89 border-radius:999px;background:var(--surface-2,#faf9f5)}90 .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none;91 padding-bottom:calc(11px + env(safe-area-inset-bottom,0))}92 .kaa-full .kaa-in{justify-content:center}93 .kaa-full .kaa-in textarea{max-width:700px}94 .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;95 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none}96 .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px;97 background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer;touch-action:manipulation}98 .kaa-in button:disabled{opacity:.45}99 .kaa-hello{font-size:12.5px;color:var(--ink-2,#4d5551);padding:4px 2px;max-width:760px}100 .kaa-dots::after{content:"…";animation:kaa-b 1.2s infinite}101 @keyframes kaa-b{0%{opacity:.2}50%{opacity:1}100%{opacity:.2}}102 @media (max-width:640px){.kaa-x1{display:none}}103 `;104105 /* ---------- markdown complet (rendu en streaming, texte échappé d'abord) ---------- */106 function esc(s) {107 return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");108 }109 function inline(s) {110 s = esc(s);111 s = s.replace(/`([^`\n]+)`/g, "<code>$1</code>");112 s = s.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g,113 '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');114 s = s.replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>");115 s = s.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\*)/g, "$1<i>$2</i>");116 s = s.replace(/(^|[^"'>=\w])(https?:\/\/[^\s<)"']+)/g, function (m, pre, u) {117 return pre + '<a href="' + u + '" target="_blank" rel="noopener noreferrer">' +118 u.replace(/^https?:\/\/(www\.)?/, "").slice(0, 42) + "</a>";119 });120 return s;121 }122 function md(src) {123 var lines = String(src).split("\n"), out = [], i = 0, m;124 function para(buf) { if (buf.length) out.push("<p>" + buf.map(inline).join("<br>") + "</p>"); }125 while (i < lines.length) {126 var l = lines[i];127 if (/^\s*```/.test(l)) { /* bloc de code */128 var code = []; i++;129 while (i < lines.length && !/^\s*```/.test(lines[i])) code.push(lines[i++]);130 i++;131 out.push("<pre><code>" + esc(code.join("\n")) + "</code></pre>");132 } else if ((m = l.match(/^\s*(#{1,4})\s+(.*)$/))) { /* titres */133 out.push("<h" + m[1].length + ">" + inline(m[2]) + "</h" + m[1].length + ">"); i++;134 } else if (/^\s*(---+|\*\*\*+)\s*$/.test(l)) { /* filet */135 out.push("<hr>"); i++;136 } else if (/^\s*>|^\s*>/.test(l)) { /* citation */137 var q = [];138 while (i < lines.length && /^\s*>/.test(lines[i])) q.push(lines[i++].replace(/^\s*>\s?/, ""));139 out.push("<blockquote>" + q.map(inline).join("<br>") + "</blockquote>");140 } else if (/^\s*\|.*\|\s*$/.test(l)) { /* tableau */141 var rows = [];142 while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) {143 var cells = lines[i].trim().replace(/^\||\|$/g, "").split("|").map(function (c) { return c.trim(); });144 if (!/^[:\-\s|]+$/.test(lines[i].replace(/\|/g, ""))) rows.push(cells);145 i++;146 }147 if (rows.length) {148 var t = "<table>";149 rows.forEach(function (r, ri) {150 var tag = ri === 0 ? "th" : "td";151 t += "<tr>" + r.map(function (c) { return "<" + tag + ">" + inline(c) + "</" + tag + ">"; }).join("") + "</tr>";152 });153 out.push(t + "</table>");154 }155 } else if (/^\s*[-*+]\s+/.test(l)) { /* liste à puces */156 var ul = [];157 while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i]))158 ul.push("<li>" + inline(lines[i++].replace(/^\s*[-*+]\s+/, "")) + "</li>");159 out.push("<ul>" + ul.join("") + "</ul>");160 } else if (/^\s*\d+[.)]\s+/.test(l)) { /* liste numérotée */161 var ol = [];162 while (i < lines.length && /^\s*\d+[.)]\s+/.test(lines[i]))163 ol.push("<li>" + inline(lines[i++].replace(/^\s*\d+[.)]\s+/, "")) + "</li>");164 out.push("<ol>" + ol.join("") + "</ol>");165 } else if (!l.trim()) { i++; } /* ligne vide */166 else { /* paragraphe */167 var buf = [];168 while (i < lines.length && lines[i].trim() &&169 !/^\s*(```|#{1,4}\s|[-*+]\s|\d+[.)]\s|>|\||---+\s*$)/.test(lines[i]))170 buf.push(lines[i++]);171 if (!buf.length) buf.push(lines[i++] || "");172 para(buf);173 }174 }175 return out.join("");176 }177178 /* ---------- état ---------- */179 var log = [];180 try { log = JSON.parse(localStorage.getItem(KEY) || "[]"); } catch (e) {}181 function save() { try { localStorage.setItem(KEY, JSON.stringify(log.slice(-MAXLOG))); } catch (e) {} }182183 /* ---------- DOM ---------- */184 var style = document.createElement("style");185 style.textContent = css;186 document.head.appendChild(style);187188 var btn = document.createElement("button");189 btn.className = "kaa-btn";190 btn.setAttribute("aria-label", "Ouvrir KA Agent — assistant de l'écosystème Groupe KA");191 btn.innerHTML = "Ka";192193 var panel = document.createElement("div");194 panel.className = "kaa-panel";195 panel.setAttribute("role", "dialog");196 panel.setAttribute("aria-label", "KA Agent");197 panel.innerHTML =198 '<div class="kaa-head"><b>KA<span class="kaa-ka">Agent</span></b><small>Groupe KA · IA</small><span class="kaa-sp"></span>' +199 '<button class="kaa-hbtn kaa-x1" title="Réduire / agrandir" aria-label="Réduire ou agrandir">▭</button>' +200 '<button class="kaa-hbtn kaa-x2" title="Fermer" aria-label="Fermer">✕</button></div>' +201 '<div class="kaa-log"></div>' +202 '<div class="kaa-in"><textarea rows="1" enterkeyhint="send" autocapitalize="sentences" autocomplete="off" placeholder="Posez votre question…" aria-label="Votre question"></textarea>' +203 "<button aria-label=\"Envoyer\">➤</button></div>";204 document.body.appendChild(btn);205 document.body.appendChild(panel);206207 var logEl = panel.querySelector(".kaa-log");208 var ta = panel.querySelector("textarea");209 var send = panel.querySelector(".kaa-in button");210211 function scroll() { logEl.scrollTop = logEl.scrollHeight; }212 function bubble(cls, html) {213 var d = document.createElement("div");214 d.className = "kaa-msg " + cls;215 d.innerHTML = html;216 logEl.appendChild(d);217 scroll();218 return d;219 }220 function toolChip(name) {221 var d = document.createElement("div");222 d.className = "kaa-tool kaa-dots";223 d.textContent = "🔎 " + name.replace(/_/g, " ");224 logEl.appendChild(d);225 scroll();226 return d;227 }228229 function hello() {230 if (log.length) return;231 var d = document.createElement("div");232 d.className = "kaa-hello";233 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.";234 logEl.appendChild(d);235 }236 function render() {237 logEl.innerHTML = "";238 hello();239 log.forEach(function (m) {240 m.role === "user" ? bubble("kaa-u", esc(m.content)) : bubble("kaa-a", md(m.content));241 });242 scroll();243 }244245 /* ---------- plein écran + clavier mobile ----------246 Ouverture = PLEIN ÉCRAN direct (desktop et mobile). ▭ réduit en fenêtre247 (desktop seulement — caché sur mobile). Tant que le plein écran est ouvert,248 l'arrière-plan est GELÉ (body position:fixed, scroll restauré à la249 fermeture) et le panneau se cale sur window.visualViewport : quand le250 clavier s'ouvre, le panneau rétrécit à la place de laisser iOS251 zoomer/pousser la page. Pas de focus clavier forcé au tap sur mobile. */252 var fineInput = window.matchMedia("(hover: hover) and (pointer: fine)");253 var vv = window.visualViewport;254 var savedY = 0;255256 function isOpen() { return panel.classList.contains("kaa-open"); }257 function isFull() { return panel.classList.contains("kaa-full"); }258259 function fitVV() {260 if (!vv || !isOpen() || !isFull()) { panel.style.height = ""; panel.style.top = ""; return; }261 panel.style.top = Math.round(vv.offsetTop) + "px";262 panel.style.height = Math.round(vv.height) + "px";263 scroll();264 }265 if (vv) {266 vv.addEventListener("resize", fitVV);267 vv.addEventListener("scroll", fitVV);268 }269270 function lockBg(on) {271 var b = document.body, h = document.documentElement;272 if (on && !h.classList.contains("kaa-lock")) {273 savedY = window.scrollY || 0;274 h.classList.add("kaa-lock");275 b.style.position = "fixed";276 b.style.top = -savedY + "px";277 b.style.left = "0"; b.style.right = "0"; b.style.width = "100%";278 } else if (!on && h.classList.contains("kaa-lock")) {279 h.classList.remove("kaa-lock");280 b.style.position = ""; b.style.top = ""; b.style.left = ""; b.style.right = ""; b.style.width = "";281 window.scrollTo(0, savedY);282 }283 }284 function sync() {285 lockBg(isOpen() && isFull());286 fitVV();287 }288289 /* ---------- bulle déplaçable (v3) ----------290 pointerdown sur la bulle → si le doigt/curseur bouge de plus de 6 px c'est291 un GLISSER (la bulle suit, bornée à 8 px des bords) ; sinon c'est un CLIC292 (ouverture normale). Position mémorisée par site (localStorage) et293 re-bornée quand le viewport change. En mode fenêtré, le panneau s'ancre à294 gauche si la bulle est dans la moitié gauche de l'écran. */295 var POSKEY = "ka-agent-pos-" + SITE;296 var drag = { on: false, moved: false, id: null, sx: 0, sy: 0, bx: 0, by: 0 };297298 function clampPos(l, t) {299 var m = 8, w = btn.offsetWidth || 58, h = btn.offsetHeight || 58;300 return {301 l: Math.min(Math.max(l, m), Math.max(m, window.innerWidth - w - m)),302 t: Math.min(Math.max(t, m), Math.max(m, window.innerHeight - h - m)),303 };304 }305 function applyPos(l, t) {306 var p = clampPos(l, t);307 btn.style.left = p.l + "px";308 btn.style.top = p.t + "px";309 btn.style.right = "auto";310 btn.style.bottom = "auto";311 panel.classList.toggle("kaa-left", p.l + (btn.offsetWidth || 58) / 2 < window.innerWidth / 2);312 }313 function savePos() {314 var r = btn.getBoundingClientRect();315 try { localStorage.setItem(POSKEY, JSON.stringify({ l: r.left, t: r.top })); } catch (e) {}316 }317 btn.addEventListener("pointerdown", function (e) {318 if (!e.isPrimary) return;319 drag.on = true; drag.moved = false; drag.id = e.pointerId;320 drag.sx = e.clientX; drag.sy = e.clientY;321 var r = btn.getBoundingClientRect();322 drag.bx = r.left; drag.by = r.top;323 try { btn.setPointerCapture(e.pointerId); } catch (err) {} // capture immédiate : un glisser rapide sort de la bulle avant le 1er move324 });325 btn.addEventListener("pointermove", function (e) {326 if (!drag.on || e.pointerId !== drag.id) return;327 var dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;328 if (!drag.moved && dx * dx + dy * dy < 36) return; // < 6 px : encore un clic329 if (!drag.moved) {330 drag.moved = true;331 btn.classList.add("kaa-drag");332 }333 applyPos(drag.bx + dx, drag.by + dy);334 });335 function dragEnd(e) {336 if (!drag.on || e.pointerId !== drag.id) return;337 drag.on = false;338 if (drag.moved) {339 btn.classList.remove("kaa-drag");340 savePos();341 setTimeout(function () { drag.moved = false; }, 0); // filet si le click ne suit pas342 }343 }344 btn.addEventListener("pointerup", dragEnd);345 btn.addEventListener("pointercancel", dragEnd);346 window.addEventListener("resize", function () {347 if (!btn.style.left) return;348 var r = btn.getBoundingClientRect();349 applyPos(r.left, r.top);350 });351 (function restorePos() {352 var p = null;353 try { p = JSON.parse(localStorage.getItem(POSKEY) || "null"); } catch (e) {}354 if (p && typeof p.l === "number" && typeof p.t === "number") applyPos(p.l, p.t);355 })();356357 btn.addEventListener("click", function (e) {358 if (drag.moved) { drag.moved = false; e.preventDefault(); return; } // fin de glisser, pas un clic359 panel.classList.add("kaa-open", "kaa-full"); // plein écran d'office360 render();361 if (fineInput.matches) ta.focus();362 sync();363 });364 panel.querySelector(".kaa-x2").addEventListener("click", function () {365 panel.classList.remove("kaa-open", "kaa-full");366 sync();367 });368 panel.querySelector(".kaa-x1").addEventListener("click", function () {369 panel.classList.toggle("kaa-full");370 sync();371 scroll();372 });373 document.addEventListener("keydown", function (e) {374 if (e.key === "Escape" && isOpen()) {375 panel.classList.remove("kaa-open", "kaa-full");376 sync();377 }378 });379380 /* ---------- envoi + flux SSE ---------- */381 var busy = false;382 function ask() {383 var q = ta.value.trim();384 if (!q || busy) return;385 busy = true;386 send.disabled = true;387 ta.value = "";388 log.push({ role: "user", content: q });389 save();390 bubble("kaa-u", esc(q));391 var out = bubble("kaa-a", '<span class="kaa-dots"></span>');392 var acc = "";393 var chips = [];394395 fetch(ENDPOINT, {396 method: "POST",397 headers: { "Content-Type": "application/json" },398 body: JSON.stringify({ site: SITE, messages: log.slice(-MAXLOG) }),399 })400 .then(function (res) {401 if (!res.ok || !res.body) throw new Error("HTTP " + res.status);402 var reader = res.body.getReader();403 var dec = new TextDecoder();404 var buf = "";405 function pump() {406 return reader.read().then(function (r) {407 if (r.done) return finish();408 buf += dec.decode(r.value, { stream: true });409 var blocks = buf.split("\n\n");410 buf = blocks.pop();411 blocks.forEach(function (b) {412 var ev = (b.match(/^event: (.+)$/m) || [])[1];413 var dm = (b.match(/^data: (.+)$/m) || [])[1];414 if (!ev || !dm) return;415 var data = {};416 try { data = JSON.parse(dm); } catch (e) {}417 if (ev === "delta" && data.text) {418 acc += data.text;419 out.innerHTML = md(acc); // markdown re-rendu à chaque delta420 scroll();421 } else if (ev === "tool") {422 chips.push(toolChip(data.name || "recherche"));423 logEl.appendChild(out); // la bulle reste après le chip424 scroll();425 } else if (ev === "error") {426 acc = acc || "Désolé, une erreur est survenue (" + (data.message || "inconnue") + "). Réessayez.";427 out.innerHTML = md(acc);428 }429 });430 return pump();431 });432 }433 return pump();434 })435 .catch(function (e) {436 acc = acc || "Impossible de joindre KA Agent (" + e.message + "). Réessayez dans un instant.";437 out.innerHTML = md(acc);438 })439 .then(function () { finish(); });440441 function finish() {442 if (busy === false) return;443 busy = false;444 send.disabled = false;445 chips.forEach(function (c) { c.classList.remove("kaa-dots"); });446 if (!acc) { acc = "Je n'ai pas pu formuler de réponse — réessayez."; out.innerHTML = md(acc); }447 log.push({ role: "assistant", content: acc });448 save();449 scroll();450 }451 }452 send.addEventListener("click", ask);453 ta.addEventListener("keydown", function (e) {454 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); ask(); }455 });456})();457