SPB Git forge

spb/api-ka

Public

API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com

48commits 1branches 0releases
5.9 MBsize
maindefault branch
19 days agolast push
Python 60.9% HTML 21% TypeScript 7.3% JavaScript 5.2% CSS 4.8% Shell 0.8%

Câblage du widget KA Agent (bulle de chat IA Groupe KA)

- Route GET /ka-agent.js (FileResponse, comme /og.png) dans src/api/main.py.
- Balises window.KA_AGENT={site:"api-ka"} + /ka-agent.js (defer) dans
  index.html, contact.html et stats.html.
- pytest : 33 passés.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 18, 2026) parent 75a5049

5 changed files +245 −0

modified src/api/main.py +6 −0
@@ -165,3 +165,9 @@ def apple_touch_icon() -> FileResponse:
165 165 @app.get("/og.png", include_in_schema=False)
166 166 def og_image() -> FileResponse:
167 167 return FileResponse(WEB_DIR / "og.png", media_type="image/png")
168 +
169 +
170 +@app.get("/ka-agent.js", include_in_schema=False)
171 +def ka_agent_js() -> FileResponse:
172 + """Widget KA Agent — bulle de chat IA commune a l ecosysteme Groupe KA."""
173 + return FileResponse(WEB_DIR / "ka-agent.js", media_type="application/javascript")
modified src/api/web/contact.html +2 −0
@@ -109,5 +109,7 @@ section{padding:64px 0}
109 109 <footer class="ka-footer"><div class="container" id="ka-footer-root"></div></footer>
110 110
111 111 <script src="/ka/ka-shell.js" defer></script>
112 +<script>window.KA_AGENT={site:"api-ka"}</script>
113 +<script src="/ka-agent.js" defer></script>
112 114 </body>
113 115 </html>
modified src/api/web/index.html +2 −0
@@ -561,5 +561,7 @@ function observeReveals(){
561 561 }
562 562 observeReveals();
563 563 </script>
564 +<script>window.KA_AGENT={site:"api-ka"}</script>
565 +<script src="/ka-agent.js" defer></script>
564 566 </body>
565 567 </html>
added src/api/web/ka-agent.js +233 −0
@@ -0,0 +1,233 @@
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 +})();
modified src/api/web/stats.html +2 −0
@@ -568,5 +568,7 @@ async function load(){
568 568 }
569 569 load();
570 570 </script>
571 +<script>window.KA_AGENT={site:"api-ka"}</script>
572 +<script src="/ka-agent.js" defer></script>
571 573 </body>
572 574 </html>
573 575