SPB Git

spb/job-ka

Public

KA Agent v2 (widget) : plein écran à l'ouverture (desktop+mobile), fond gelé + calage visualViewport (clavier mobile stable), rendu Markdown complet en streaming (titres, listes, tableaux, code, liens nommés)

Simon-Pierre Boucher committed (Aug 19, 2026) parent 7ebf05f Browse files

1 changed file +177 −48

Unified Split
modified frontend/public/ka-agent.js +177 −48
@@ -3,7 +3,11 @@
3 * Groupe KA. Vanilla JS, zéro dépendance, injecté sur les 13 sites. 3 * Groupe KA. Vanilla JS, zéro dépendance, injecté sur les 13 sites.
4 * <script>window.KA_AGENT={site:"lou-ka"}</script> 4 * <script>window.KA_AGENT={site:"lou-ka"}</script>
5 * <script src="/ka-agent.js" defer></script> 5 * <script src="/ka-agent.js" defer></script>
6− * Bulle flottante en bas à droite → panneau de chat (bulle) → plein écran (⛶). 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 se
8+ * cale sur window.visualViewport (le clavier ne fait plus grossir/défiler la
9+ * page — fond verrouillé par body position:fixed) ; rendu MARKDOWN complet en
10+ * streaming (titres, listes, tableaux, code, liens nommés, citations).
7 * Flux SSE en direct depuis le backend central (texte token par token + 11 * Flux SSE en direct depuis le backend central (texte token par token +
8 * indicateur d'outil). Historique conservé en localStorage (par site). 12 * indicateur d'outil). Historique conservé en localStorage (par site).
9 */ 13 */
@@ -21,60 +25,147 @@
21 border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b); 25 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; 26 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; 27 cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center;
24− justify-content:center;transition:transform .15s} 28+ justify-content:center;transition:transform .15s;touch-action:manipulation}
25 .kaa-btn:hover{transform:translate(-2px,-2px)} 29 .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)); 30+ .kaa-panel{position:fixed;z-index:2147483001;display:none;flex-direction:column;background:var(--paper,#f5f3ee);
27− height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee); 31+ overflow:hidden;font-family:Inter,system-ui,sans-serif;touch-action:manipulation}
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} 32 .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} 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}
32− html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior: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}
33 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814); 38 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);
34− color:var(--paper,#f5f3ee);flex:none} 39+ color:var(--paper,#f5f3ee);flex:none;padding-top:max(11px,env(safe-area-inset-top,0px))}
35 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em} 40 .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); 41 .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} 42 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} 43 .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} 44 .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; 45 .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} 46+ color:var(--paper,#f5f3ee);font-size:15px;cursor:pointer;line-height:1;flex:none}
42 .kaa-hbtn:hover{border-color:var(--accent,#d9f26b);color:var(--accent,#d9f26b)} 47 .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; 48 .kaa-log{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px;
44 overscroll-behavior:contain;-webkit-overflow-scrolling:touch} 49 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; 50 .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} 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}
47 .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#d9f26b);color:var(--on-accent,#141814); 54 .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#d9f26b);color:var(--on-accent,#141814);
48− border-bottom-right-radius:4px} 55+ border-bottom-right-radius:4px;white-space:pre-wrap}
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)} 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)}
50− .kaa-msg.kaa-a a{color:inherit;text-decoration:underline;text-underline-offset:3px} 57+ .kaa-msg.kaa-a a{color:inherit;text-decoration:underline;text-underline-offset:3px;font-weight:600}
51− .kaa-msg.kaa-a b{font-weight:700} 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}
52 .kaa-tool{align-self:flex-start;font:700 10px "JetBrains Mono",monospace;letter-spacing:.07em; 80 .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); 81 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)} 82 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; 83 .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))} 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}
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; 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;
58 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none} 88 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; 89 .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} 90+ background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer;touch-action:manipulation}
61 .kaa-in button:disabled{opacity:.45} 91 .kaa-in button:disabled{opacity:.45}
62− .kaa-hello{font-size:12.5px;color:var(--ink-2,#4d5551);padding:4px 2px} 92+ .kaa-hello{font-size:12.5px;color:var(--ink-2,#4d5551);padding:4px 2px;max-width:760px}
63 .kaa-dots::after{content:"…";animation:kaa-b 1.2s infinite} 93 .kaa-dots::after{content:"…";animation:kaa-b 1.2s infinite}
64 @keyframes kaa-b{0%{opacity:.2}50%{opacity:1}100%{opacity:.2}} 94 @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}} 95+ @media (max-width:640px){.kaa-x1{display:none}}
66 `; 96 `;
67 97
68− /* ---------- markdown minimal (gras, liens, puces) — texte échappé d'abord ---------- */ 98+ /* ---------- markdown complet (rendu en streaming, texte échappé d'abord) ---------- */
69 function esc(s) { 99 function esc(s) {
70 return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); 100 return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
71 } 101 }
72− function md(s) { 102+ function inline(s) {
73− return esc(s) 103+ s = esc(s);
74− .replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>") 104+ s = s.replace(/`([^`\n]+)`/g, "<code>$1</code>");
75− .replace(/(https?:\/\/[^\s<)"']+)/g, function (u) { 105+ s = s.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g,
76− return '<a href="' + u + '" target="_blank" rel="noopener noreferrer">' + u.replace(/^https?:\/\/(www\.)?/, "").slice(0, 40) + "</a>"; 106+ '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
77− }); 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("");
78 } 169 }
79 170
80 /* ---------- état ---------- */ 171 /* ---------- état ---------- */
@@ -98,10 +189,10 @@
98 panel.setAttribute("aria-label", "KA Agent"); 189 panel.setAttribute("aria-label", "KA Agent");
99 panel.innerHTML = 190 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>' + 191 '<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>' + 192+ '<button class="kaa-hbtn kaa-x1" title="Réduire / agrandir" aria-label="Réduire ou agrandir">▭</button>' +
102 '<button class="kaa-hbtn kaa-x2" title="Fermer" aria-label="Fermer">✕</button></div>' + 193 '<button class="kaa-hbtn kaa-x2" title="Fermer" aria-label="Fermer">✕</button></div>' +
103 '<div class="kaa-log"></div>' + 194 '<div class="kaa-log"></div>' +
104− '<div class="kaa-in"><textarea rows="1" placeholder="Posez votre question…" aria-label="Votre question"></textarea>' + 195+ '<div class="kaa-in"><textarea rows="1" enterkeyhint="send" autocapitalize="sentences" autocomplete="off" placeholder="Posez votre question…" aria-label="Votre question"></textarea>' +
105 "<button aria-label=\"Envoyer\">➤</button></div>"; 196 "<button aria-label=\"Envoyer\">➤</button></div>";
106 document.body.appendChild(btn); 197 document.body.appendChild(btn);
107 document.body.appendChild(panel); 198 document.body.appendChild(panel);
@@ -132,45 +223,83 @@
132 if (log.length) return; 223 if (log.length) return;
133 var d = document.createElement("div"); 224 var d = document.createElement("div");
134 d.className = "kaa-hello"; 225 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."; 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.";
136 logEl.appendChild(d); 227 logEl.appendChild(d);
137 } 228 }
138 function render() { 229 function render() {
139 logEl.innerHTML = ""; 230 logEl.innerHTML = "";
140 hello(); 231 hello();
141− log.forEach(function (m) { bubble(m.role === "user" ? "kaa-u" : "kaa-a", md(m.content)); }); 232+ log.forEach(function (m) {
233+ m.role === "user" ? bubble("kaa-u", esc(m.content)) : bubble("kaa-a", md(m.content));
234+ });
142 scroll(); 235 scroll();
143 } 236 }
144 237
145− /* ---------- ouverture / plein écran ---------- */ 238+ /* ---------- plein écran + clavier mobile ----------
146− /* Sur téléphone : panneau plein écran d'office + scroll d'arrière-plan 239+ Ouverture = PLEIN ÉCRAN direct (desktop et mobile). ▭ réduit en fenêtre
147− verrouillé tant qu'il est ouvert (html.kaa-lock). Pas de focus clavier 240+ (desktop seulement — caché sur mobile). Tant que le plein écran est ouvert,
148− forcé au tap (le clavier ne s'ouvre que si l'utilisateur touche le champ). */ 241+ l'arrière-plan est GELÉ (body position:fixed, scroll restauré à la
149− var mobileMq = window.matchMedia("(max-width: 640px)"); 242+ fermeture) et le panneau se cale sur window.visualViewport : quand le
243+ clavier s'ouvre, le panneau rétrécit à la place de laisser iOS
244+ zoomer/pousser la page. Pas de focus clavier forcé au tap sur mobile. */
150 var fineInput = window.matchMedia("(hover: hover) and (pointer: fine)"); 245 var fineInput = window.matchMedia("(hover: hover) and (pointer: fine)");
151− function syncLock() { 246+ var vv = window.visualViewport;
152− var open = panel.classList.contains("kaa-open"); 247+ var savedY = 0;
153− if (open && mobileMq.matches) panel.classList.add("kaa-full"); 248+
154− document.documentElement.classList.toggle( 249+ function isOpen() { return panel.classList.contains("kaa-open"); }
155− "kaa-lock", open && panel.classList.contains("kaa-full")); 250+ function isFull() { return panel.classList.contains("kaa-full"); }
251+
252+ 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();
156 } 257 }
157− btn.addEventListener("click", function () { 258+ if (vv) {
158− panel.classList.toggle("kaa-open"); 259+ vv.addEventListener("resize", fitVV);
159− if (panel.classList.contains("kaa-open")) { 260+ vv.addEventListener("scroll", fitVV);
160− render(); 261+ }
161− if (fineInput.matches) ta.focus(); 262+
263+ 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);
162 } 275 }
163− syncLock(); 276+ }
277+ function sync() {
278+ lockBg(isOpen() && isFull());
279+ fitVV();
280+ }
281+
282+ btn.addEventListener("click", function () {
283+ panel.classList.add("kaa-open", "kaa-full"); // plein écran d'office
284+ render();
285+ if (fineInput.matches) ta.focus();
286+ sync();
164 }); 287 });
165 panel.querySelector(".kaa-x2").addEventListener("click", function () { 288 panel.querySelector(".kaa-x2").addEventListener("click", function () {
166 panel.classList.remove("kaa-open", "kaa-full"); 289 panel.classList.remove("kaa-open", "kaa-full");
167− syncLock(); 290+ sync();
168 }); 291 });
169 panel.querySelector(".kaa-x1").addEventListener("click", function () { 292 panel.querySelector(".kaa-x1").addEventListener("click", function () {
170 panel.classList.toggle("kaa-full"); 293 panel.classList.toggle("kaa-full");
171− syncLock(); 294+ sync();
172 scroll(); 295 scroll();
173 }); 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+ });
174 303
175 /* ---------- envoi + flux SSE ---------- */ 304 /* ---------- envoi + flux SSE ---------- */
176 var busy = false; 305 var busy = false;
@@ -182,7 +311,7 @@
182 ta.value = ""; 311 ta.value = "";
183 log.push({ role: "user", content: q }); 312 log.push({ role: "user", content: q });
184 save(); 313 save();
185− bubble("kaa-u", md(q)); 314+ bubble("kaa-u", esc(q));
186 var out = bubble("kaa-a", '<span class="kaa-dots"></span>'); 315 var out = bubble("kaa-a", '<span class="kaa-dots"></span>');
187 var acc = ""; 316 var acc = "";
188 var chips = []; 317 var chips = [];
@@ -211,7 +340,7 @@
211 try { data = JSON.parse(dm); } catch (e) {} 340 try { data = JSON.parse(dm); } catch (e) {}
212 if (ev === "delta" && data.text) { 341 if (ev === "delta" && data.text) {
213 acc += data.text; 342 acc += data.text;
214− out.innerHTML = md(acc); 343+ out.innerHTML = md(acc); // markdown re-rendu à chaque delta
215 scroll(); 344 scroll();
216 } else if (ev === "tool") { 345 } else if (ev === "tool") {
217 chips.push(toolChip(data.name || "recherche")); 346 chips.push(toolChip(data.name || "recherche"));
218 347
Menu

Theme