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)
1 changed file +177 −48
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, "&").replace(/</g, "<").replace(/>/g, ">"); | 100 | return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); | ||
| 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*>|^\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 | ||||