SPB Git forge

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

52commits 1branches 0releases
11.6 MBsize
maindefault branch
19 days agolast push
Python 69.3% TypeScript 16.7% CSS 7.9% JavaScript 4.7% HTML 1.4%

feat(ka-id v2.1): CF SIMILAR_USERS + langue transversale (kaid.py), SDK pilules « Sauvegarder cette recherche »/« Pas pour moi » (ka-id.js), alignés sur ka-ui.git/kaid cbe9c9f

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 29 days ago (Aug 26, 2026) parent 7f3446f

2 changed files +116 −3

modified frontend/public/ka-id.js +102 −3
@@ -135,9 +135,11 @@
135 135
136 136 function checkRoute() {
137 137 var uid = detailUid();
138 if (uid === dwell.uid) return;
139 endDwell(false);
140 if (uid) dwell = { uid: uid, t0: Date.now() };
138 + if (uid !== dwell.uid) {
139 + endDwell(false);
140 + if (uid) dwell = { uid: uid, t0: Date.now() };
141 + }
142 + updateUi();
141 143 }
142 144
143 145 function watchRoutes() {
@@ -158,6 +160,103 @@
158 160 checkRoute();
159 161 }
160 162
163 + /* ---------- interface : sauvegarder la recherche / pas pour moi ---------- */
164 +
165 + var INK = "#141814";
166 + var PAPER = "#f5f3ee";
167 +
168 + function injectCss() {
169 + if (document.getElementById("kaid-ui-css")) return;
170 + var st = document.createElement("style");
171 + st.id = "kaid-ui-css";
172 + st.textContent =
173 + ".kaid-pill{position:fixed;left:14px;bottom:14px;z-index:560;display:inline-flex;" +
174 + "align-items:center;gap:8px;padding:9px 14px;border-radius:999px;border:1.5px solid " + INK + ";" +
175 + "background:" + PAPER + ";color:" + INK + ";font:600 12.5px/1 system-ui,sans-serif;" +
176 + "box-shadow:0 2px 10px rgba(20,24,20,.18);cursor:pointer;max-width:78vw}" +
177 + ".kaid-pill b{font-weight:700}" +
178 + ".kaid-pill .kaid-x{margin-left:2px;opacity:.55;font-weight:700;cursor:pointer}" +
179 + ".kaid-pill a{color:inherit;text-decoration:underline}" +
180 + "@media (max-width:768px){.kaid-pill{bottom:calc(84px + env(safe-area-inset-bottom))}}";
181 + document.head.appendChild(st);
182 + }
183 +
184 + function removePill() {
185 + var el = document.getElementById("kaid-pill");
186 + if (el) el.remove();
187 + }
188 +
189 + function pillDismissed() {
190 + try {
191 + return Date.now() - Number(localStorage.getItem("kaid_pill_off") || 0) < 6048e5;
192 + } catch (err) { return false; }
193 + }
194 +
195 + function showSavePill() {
196 + if (pillDismissed() || document.getElementById("kaid-pill")) return;
197 + injectCss();
198 + var el = document.createElement("div");
199 + el.id = "kaid-pill";
200 + el.className = "kaid-pill";
201 + el.innerHTML = "<span>☆ <b>Sauvegarder cette recherche</b></span>" +
202 + "<span class='kaid-x' role='button' aria-label='Ne plus proposer'>✕</span>";
203 + el.querySelector(".kaid-x").addEventListener("click", function (ev) {
204 + ev.stopPropagation();
205 + try { localStorage.setItem("kaid_pill_off", String(Date.now())); } catch (err) { /* privé */ }
206 + removePill();
207 + });
208 + el.addEventListener("click", function () {
209 + el.innerHTML = "<span>…</span>";
210 + var filters = {};
211 + new URLSearchParams(location.search).forEach(function (v, k) {
212 + if (v) filters[k] = v;
213 + });
214 + window.KAID.saveSearch({ alert: true, filters: filters,
215 + query: filters.q || null })
216 + .then(function (r) {
217 + el.innerHTML = r && r.ok !== false && !r.error
218 + ? "<span>✓ Sauvegardée · alerte activée — <a href='https://www.groupe-ka.com/mon-ka'>Mon KA</a></span>"
219 + : "<span>Connexion KA ID requise</span>";
220 + setTimeout(removePill, 6000);
221 + })
222 + .catch(function () { removePill(); });
223 + });
224 + document.body.appendChild(el);
225 + }
226 +
227 + function showHideLink(uid) {
228 + if (document.getElementById("kaid-pill")) return;
229 + injectCss();
230 + var el = document.createElement("div");
231 + el.id = "kaid-pill";
232 + el.className = "kaid-pill";
233 + el.innerHTML = "<span>✕ Pas pour moi</span>" +
234 + "<span class='kaid-x' role='button' aria-label='Fermer'>✕</span>";
235 + el.querySelector(".kaid-x").addEventListener("click", function (ev) {
236 + ev.stopPropagation();
237 + try { localStorage.setItem("kaid_pill_off", String(Date.now())); } catch (err) { /* privé */ }
238 + removePill();
239 + });
240 + el.addEventListener("click", function () {
241 + el.innerHTML = "<span>…</span>";
242 + window.KAID.hide(uid).then(function () {
243 + el.innerHTML = "<span>Masquée — elle ne réapparaîtra plus dans vos résultats.</span>";
244 + setTimeout(removePill, 5000);
245 + }).catch(function () { removePill(); });
246 + });
247 + document.body.appendChild(el);
248 + }
249 +
250 + function updateUi() {
251 + if (!connected || CONF.ui === false) return;
252 + removePill();
253 + if (pillDismissed()) return;
254 + var uid = detailUid();
255 + if (uid) showHideLink(uid);
256 + else if (location.search.length > 1 && CONF.savePill !== false)
257 + showSavePill();
258 + }
259 +
161 260 /* ---------- visite de retour ---------- */
162 261
163 262 function returnVisit() {
modified restoka/kaid.py +14 −0
@@ -41,6 +41,7 @@ PREFS_TTL = 90 # secondes de cache du profil
41 41 TIMEOUT = 5 # secondes par appel hub
42 42 LOCATION_DIMS = {"city", "region", "sector", "quartier", "ville",
43 43 "location", "neighborhood"}
44 +LANGUAGE_DIMS = {"language", "langue"}
44 45 PRICE_DIMS = {"price", "rent", "salary", "salary_year", "price_min"}
45 46
46 47 # Événements acceptés depuis le navigateur (le reste vient du serveur).
@@ -243,6 +244,14 @@ def personal_score(feats: dict, app_profile: dict, global_profile: dict,
243 244 den += w
244 245 if max(affs) >= 0.6 and "MATCH_LOCATION" not in reasons:
245 246 reasons.append("MATCH_LOCATION")
247 + if dim in LANGUAGE_DIMS:
248 + glang = (global_profile or {}).get("language") or {}
249 + gv = glang.get("values") or {}
250 + affs = [gv[v] for v in vals if v in gv]
251 + if affs and max(affs) > 0:
252 + w = 0.4 * float(glang.get("conf") or 0.3)
253 + num += w * max(affs)
254 + den += w
246 255 if den < 0.8:
247 256 return None, []
248 257 score = (num / den + 1.0) / 2.0
@@ -283,6 +292,8 @@ def rerank(items: list, user, *, features_of, uid_of=None,
283 292 tail = items[max_considered:]
284 293 n = len(head)
285 294 active = active_dims or set()
295 + # co-favoris des membres semblables (filtrage collaboratif du hub)
296 + similar = {str(s) for s in (app_p.get("similar") or [])}
286 297 scored = []
287 298 badged = 0
288 299 for i, it in enumerate(head):
@@ -292,6 +303,9 @@ def rerank(items: list, user, *, features_of, uid_of=None,
292 303 profile.get("global") or {}, active)
293 304 except Exception:
294 305 p, reasons = None, []
306 + if similar and str(uid_of(it)) in similar:
307 + p = min(1.0, (p if p is not None else 0.55) + 0.25)
308 + reasons = (["SIMILAR_USERS"] + reasons)[:4]
295 309 if p is None:
296 310 final = (1.0 - blend) * base + blend * 0.5
297 311 else:
298 312