|
1 |
+/* Auteur : Simon-Pierre Boucher — contact@spboucher.ai |
|
2 |
+ * ka-ui/agent/ka-agent.js — KA AGENT : widget de chat commun de l'écosystème |
|
3 |
+ * 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 |
+ * Bulle flottante en bas à droite → panneau de chat (bulle) → plein écran (⛶). |
|
7 |
+ * Flux SSE en direct depuis le backend central (texte token par token + |
|
8 |
+ * indicateur d'outil). Historique conservé en localStorage (par site). |
|
9 |
+ */ |
|
10 |
+(function () { |
|
11 |
+ "use strict"; |
|
12 |
+ var CFG = window.KA_AGENT || {}; |
|
13 |
+ var SITE = CFG.site || "groupe-ka"; |
|
14 |
+ var ENDPOINT = CFG.endpoint || "https://www.api-ka.com/api/agent/chat"; |
|
15 |
+ var KEY = "ka-agent-" + SITE; |
|
16 |
+ var MAXLOG = 20; |
|
17 |
+ |
|
18 |
+ /* ---------- styles ---------- */ |
|
19 |
+ var css = ` |
|
20 |
+ .kaa-btn{position:fixed;right:18px;bottom:18px;z-index:2147483000;width:58px;height:58px; |
|
21 |
+ 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; |
|
23 |
+ cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center; |
|
24 |
+ justify-content:center;transition:transform .15s;padding-bottom:env(safe-area-inset-bottom,0)} |
|
25 |
+ .kaa-btn:hover{transform:translate(-2px,-2px)} |
|
26 |
+ .kaa-panel{position:fixed;right:16px;bottom:86px;z-index:2147483001;width:min(392px,calc(100vw - 24px)); |
|
27 |
+ height:min(580px,calc(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee); |
|
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} |
|
31 |
+ .kaa-panel.kaa-full{right:0;bottom:0;width:100vw;height:100dvh;border-radius:0;border:0;box-shadow:none} |
|
32 |
+ .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814); |
|
33 |
+ color:var(--paper,#f5f3ee);flex:none} |
|
34 |
+ .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em} |
|
35 |
+ .kaa-head .kaa-ka{display:inline-block;background:var(--accent,#d9f26b);color:var(--on-accent,#141814); |
|
36 |
+ border-radius:6px;padding:1px 7px 2px;transform:rotate(-2deg);font-weight:700} |
|
37 |
+ .kaa-head small{font:700 9.5px "JetBrains Mono",monospace;letter-spacing:.09em;text-transform:uppercase;opacity:.65} |
|
38 |
+ .kaa-head .kaa-sp{flex:1} |
|
39 |
+ .kaa-hbtn{width:34px;height:34px;border:1.5px solid rgba(245,243,238,.4);border-radius:8px;background:none; |
|
40 |
+ color:var(--paper,#f5f3ee);font-size:15px;cursor:pointer;line-height:1} |
|
41 |
+ .kaa-hbtn:hover{border-color:var(--accent,#d9f26b);color:var(--accent,#d9f26b)} |
|
42 |
+ .kaa-log{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px; |
|
43 |
+ overscroll-behavior:contain;-webkit-overflow-scrolling:touch} |
|
44 |
+ .kaa-msg{max-width:86%;padding:9px 13px;border:1.5px solid var(--ink,#141814);border-radius:12px; |
|
45 |
+ font-size:13.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word} |
|
46 |
+ .kaa-msg.kaa-u{align-self:flex-end;background:var(--accent,#d9f26b);color:var(--on-accent,#141814); |
|
47 |
+ border-bottom-right-radius:4px} |
|
48 |
+ .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)} |
|
49 |
+ .kaa-msg.kaa-a a{color:inherit;text-decoration:underline;text-underline-offset:3px} |
|
50 |
+ .kaa-msg.kaa-a b{font-weight:700} |
|
51 |
+ .kaa-tool{align-self:flex-start;font:700 10px "JetBrains Mono",monospace;letter-spacing:.07em; |
|
52 |
+ text-transform:uppercase;color:var(--ink-2,#4d5551);padding:3px 10px;border:1.5px dashed var(--ink-2,#4d5551); |
|
53 |
+ border-radius:999px;background:var(--surface-2,#faf9f5)} |
|
54 |
+ .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none; |
|
55 |
+ padding-bottom:calc(11px + env(safe-area-inset-bottom,0))} |
|
56 |
+ .kaa-in textarea{flex:1;resize:none;min-height:44px;max-height:110px;padding:11px 12px;font:13.5px/1.4 Inter,system-ui,sans-serif; |
|
57 |
+ border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none} |
|
58 |
+ .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px; |
|
59 |
+ background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer} |
|
60 |
+ .kaa-in button:disabled{opacity:.45} |
|
61 |
+ .kaa-hello{font-size:12.5px;color:var(--ink-2,#4d5551);padding:4px 2px} |
|
62 |
+ .kaa-dots::after{content:"…";animation:kaa-b 1.2s infinite} |
|
63 |
+ @keyframes kaa-b{0%{opacity:.2}50%{opacity:1}100%{opacity:.2}} |
|
64 |
+ @media (max-width:560px){.kaa-panel{right:8px;left:8px;width:auto}} |
|
65 |
+ `; |
|
66 |
+ |
|
67 |
+ /* ---------- markdown minimal (gras, liens, puces) — texte échappé d'abord ---------- */ |
|
68 |
+ function esc(s) { |
|
69 |
+ return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
|
70 |
+ } |
|
71 |
+ function md(s) { |
|
72 |
+ return esc(s) |
|
73 |
+ .replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>") |
|
74 |
+ .replace(/(https?:\/\/[^\s<)"']+)/g, function (u) { |
|
75 |
+ return '<a href="' + u + '" target="_blank" rel="noopener noreferrer">' + u.replace(/^https?:\/\/(www\.)?/, "").slice(0, 40) + "</a>"; |
|
76 |
+ }); |
|
77 |
+ } |
|
78 |
+ |
|
79 |
+ /* ---------- état ---------- */ |
|
80 |
+ var log = []; |
|
81 |
+ try { log = JSON.parse(localStorage.getItem(KEY) || "[]"); } catch (e) {} |
|
82 |
+ function save() { try { localStorage.setItem(KEY, JSON.stringify(log.slice(-MAXLOG))); } catch (e) {} } |
|
83 |
+ |
|
84 |
+ /* ---------- DOM ---------- */ |
|
85 |
+ var style = document.createElement("style"); |
|
86 |
+ style.textContent = css; |
|
87 |
+ document.head.appendChild(style); |
|
88 |
+ |
|
89 |
+ var btn = document.createElement("button"); |
|
90 |
+ btn.className = "kaa-btn"; |
|
91 |
+ btn.setAttribute("aria-label", "Ouvrir KA Agent — assistant de l'écosystème Groupe KA"); |
|
92 |
+ btn.innerHTML = "Ka"; |
|
93 |
+ |
|
94 |
+ var panel = document.createElement("div"); |
|
95 |
+ panel.className = "kaa-panel"; |
|
96 |
+ panel.setAttribute("role", "dialog"); |
|
97 |
+ panel.setAttribute("aria-label", "KA Agent"); |
|
98 |
+ panel.innerHTML = |
|
99 |
+ '<div class="kaa-head"><b>KA<span class="kaa-ka">Agent</span></b><small>Groupe KA · IA</small><span class="kaa-sp"></span>' + |
|
100 |
+ '<button class="kaa-hbtn kaa-x1" title="Plein écran" aria-label="Plein écran">⛶</button>' + |
|
101 |
+ '<button class="kaa-hbtn kaa-x2" title="Fermer" aria-label="Fermer">✕</button></div>' + |
|
102 |
+ '<div class="kaa-log"></div>' + |
|
103 |
+ '<div class="kaa-in"><textarea rows="1" placeholder="Posez votre question…" aria-label="Votre question"></textarea>' + |
|
104 |
+ "<button aria-label=\"Envoyer\">➤</button></div>"; |
|
105 |
+ document.body.appendChild(btn); |
|
106 |
+ document.body.appendChild(panel); |
|
107 |
+ |
|
108 |
+ var logEl = panel.querySelector(".kaa-log"); |
|
109 |
+ var ta = panel.querySelector("textarea"); |
|
110 |
+ var send = panel.querySelector(".kaa-in button"); |
|
111 |
+ |
|
112 |
+ function scroll() { logEl.scrollTop = logEl.scrollHeight; } |
|
113 |
+ function bubble(cls, html) { |
|
114 |
+ var d = document.createElement("div"); |
|
115 |
+ d.className = "kaa-msg " + cls; |
|
116 |
+ d.innerHTML = html; |
|
117 |
+ logEl.appendChild(d); |
|
118 |
+ scroll(); |
|
119 |
+ return d; |
|
120 |
+ } |
|
121 |
+ function toolChip(name) { |
|
122 |
+ var d = document.createElement("div"); |
|
123 |
+ d.className = "kaa-tool kaa-dots"; |
|
124 |
+ d.textContent = "🔎 " + name.replace(/_/g, " "); |
|
125 |
+ logEl.appendChild(d); |
|
126 |
+ scroll(); |
|
127 |
+ return d; |
|
128 |
+ } |
|
129 |
+ |
|
130 |
+ function hello() { |
|
131 |
+ if (log.length) return; |
|
132 |
+ var d = document.createElement("div"); |
|
133 |
+ d.className = "kaa-hello"; |
|
134 |
+ 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."; |
|
135 |
+ logEl.appendChild(d); |
|
136 |
+ } |
|
137 |
+ function render() { |
|
138 |
+ logEl.innerHTML = ""; |
|
139 |
+ hello(); |
|
140 |
+ log.forEach(function (m) { bubble(m.role === "user" ? "kaa-u" : "kaa-a", md(m.content)); }); |
|
141 |
+ scroll(); |
|
142 |
+ } |
|
143 |
+ |
|
144 |
+ /* ---------- ouverture / plein écran ---------- */ |
|
145 |
+ btn.addEventListener("click", function () { |
|
146 |
+ panel.classList.toggle("kaa-open"); |
|
147 |
+ if (panel.classList.contains("kaa-open")) { render(); ta.focus(); } |
|
148 |
+ }); |
|
149 |
+ panel.querySelector(".kaa-x2").addEventListener("click", function () { |
|
150 |
+ panel.classList.remove("kaa-open", "kaa-full"); |
|
151 |
+ }); |
|
152 |
+ panel.querySelector(".kaa-x1").addEventListener("click", function () { |
|
153 |
+ panel.classList.toggle("kaa-full"); |
|
154 |
+ scroll(); |
|
155 |
+ }); |
|
156 |
+ |
|
157 |
+ /* ---------- envoi + flux SSE ---------- */ |
|
158 |
+ var busy = false; |
|
159 |
+ function ask() { |
|
160 |
+ var q = ta.value.trim(); |
|
161 |
+ if (!q || busy) return; |
|
162 |
+ busy = true; |
|
163 |
+ send.disabled = true; |
|
164 |
+ ta.value = ""; |
|
165 |
+ log.push({ role: "user", content: q }); |
|
166 |
+ save(); |
|
167 |
+ bubble("kaa-u", md(q)); |
|
168 |
+ var out = bubble("kaa-a", '<span class="kaa-dots"></span>'); |
|
169 |
+ var acc = ""; |
|
170 |
+ var chips = []; |
|
171 |
+ |
|
172 |
+ fetch(ENDPOINT, { |
|
173 |
+ method: "POST", |
|
174 |
+ headers: { "Content-Type": "application/json" }, |
|
175 |
+ body: JSON.stringify({ site: SITE, messages: log.slice(-MAXLOG) }), |
|
176 |
+ }) |
|
177 |
+ .then(function (res) { |
|
178 |
+ if (!res.ok || !res.body) throw new Error("HTTP " + res.status); |
|
179 |
+ var reader = res.body.getReader(); |
|
180 |
+ var dec = new TextDecoder(); |
|
181 |
+ var buf = ""; |
|
182 |
+ function pump() { |
|
183 |
+ return reader.read().then(function (r) { |
|
184 |
+ if (r.done) return finish(); |
|
185 |
+ buf += dec.decode(r.value, { stream: true }); |
|
186 |
+ var blocks = buf.split("\n\n"); |
|
187 |
+ buf = blocks.pop(); |
|
188 |
+ blocks.forEach(function (b) { |
|
189 |
+ var ev = (b.match(/^event: (.+)$/m) || [])[1]; |
|
190 |
+ var dm = (b.match(/^data: (.+)$/m) || [])[1]; |
|
191 |
+ if (!ev || !dm) return; |
|
192 |
+ var data = {}; |
|
193 |
+ try { data = JSON.parse(dm); } catch (e) {} |
|
194 |
+ if (ev === "delta" && data.text) { |
|
195 |
+ acc += data.text; |
|
196 |
+ out.innerHTML = md(acc); |
|
197 |
+ scroll(); |
|
198 |
+ } else if (ev === "tool") { |
|
199 |
+ chips.push(toolChip(data.name || "recherche")); |
|
200 |
+ logEl.appendChild(out); // la bulle reste après le chip |
|
201 |
+ scroll(); |
|
202 |
+ } else if (ev === "error") { |
|
203 |
+ acc = acc || "Désolé, une erreur est survenue (" + (data.message || "inconnue") + "). Réessayez."; |
|
204 |
+ out.innerHTML = md(acc); |
|
205 |
+ } |
|
206 |
+ }); |
|
207 |
+ return pump(); |
|
208 |
+ }); |
|
209 |
+ } |
|
210 |
+ return pump(); |
|
211 |
+ }) |
|
212 |
+ .catch(function (e) { |
|
213 |
+ acc = acc || "Impossible de joindre KA Agent (" + e.message + "). Réessayez dans un instant."; |
|
214 |
+ out.innerHTML = md(acc); |
|
215 |
+ }) |
|
216 |
+ .then(function () { finish(); }); |
|
217 |
+ |
|
218 |
+ function finish() { |
|
219 |
+ if (busy === false) return; |
|
220 |
+ busy = false; |
|
221 |
+ send.disabled = false; |
|
222 |
+ chips.forEach(function (c) { c.classList.remove("kaa-dots"); }); |
|
223 |
+ if (!acc) { acc = "Je n'ai pas pu formuler de réponse — réessayez."; out.innerHTML = md(acc); } |
|
224 |
+ log.push({ role: "assistant", content: acc }); |
|
225 |
+ save(); |
|
226 |
+ scroll(); |
|
227 |
+ } |
|
228 |
+ } |
|
229 |
+ send.addEventListener("click", ask); |
|
230 |
+ ta.addEventListener("keydown", function (e) { |
|
231 |
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); ask(); } |
|
232 |
+ }); |
|
233 |
+})(); |