SPB Git

spb/ka-ui

Public

kaid v2.1 : filtrage collaboratif SIMILAR_USERS (co-favoris du hub, boost +0.25), affinité de langue transversale ; ka-id.js : pilule « Sauvegarder cette recherche » (pages liste filtrées, alerte activée) et « Pas pour moi » (pages fiche), rejetables 7 jours

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 26, 2026) parent a0df701 Browse files

2 changed files +116 −3

modified kaid/ka-id.js +102 −3
@@ -135,9 +135,11 @@
135 135
136 function checkRoute() { 136 function checkRoute() {
137 var uid = detailUid(); 137 var uid = detailUid();
138− if (uid === dwell.uid) return; 138+ if (uid !== dwell.uid) {
139− endDwell(false); 139+ endDwell(false);
140− if (uid) dwell = { uid: uid, t0: Date.now() }; 140+ if (uid) dwell = { uid: uid, t0: Date.now() };
141+ }
142+ updateUi();
141 } 143 }
142 144
143 function watchRoutes() { 145 function watchRoutes() {
@@ -158,6 +160,103 @@
158 checkRoute(); 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 /* ---------- visite de retour ---------- */ 260 /* ---------- visite de retour ---------- */
162 261
163 function returnVisit() { 262 function returnVisit() {
modified kaid/kaid.py +14 −0
@@ -41,6 +41,7 @@ PREFS_TTL = 90 # secondes de cache du profil
41 TIMEOUT = 5 # secondes par appel hub 41 TIMEOUT = 5 # secondes par appel hub
42 LOCATION_DIMS = {"city", "region", "sector", "quartier", "ville", 42 LOCATION_DIMS = {"city", "region", "sector", "quartier", "ville",
43 "location", "neighborhood"} 43 "location", "neighborhood"}
44+LANGUAGE_DIMS = {"language", "langue"}
44 PRICE_DIMS = {"price", "rent", "salary", "salary_year", "price_min"} 45 PRICE_DIMS = {"price", "rent", "salary", "salary_year", "price_min"}
45 46
46 # Événements acceptés depuis le navigateur (le reste vient du serveur). 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 den += w 244 den += w
244 if max(affs) >= 0.6 and "MATCH_LOCATION" not in reasons: 245 if max(affs) >= 0.6 and "MATCH_LOCATION" not in reasons:
245 reasons.append("MATCH_LOCATION") 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 if den < 0.8: 255 if den < 0.8:
247 return None, [] 256 return None, []
248 score = (num / den + 1.0) / 2.0 257 score = (num / den + 1.0) / 2.0
@@ -283,6 +292,8 @@ def rerank(items: list, user, *, features_of, uid_of=None,
283 tail = items[max_considered:] 292 tail = items[max_considered:]
284 n = len(head) 293 n = len(head)
285 active = active_dims or set() 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 scored = [] 297 scored = []
287 badged = 0 298 badged = 0
288 for i, it in enumerate(head): 299 for i, it in enumerate(head):
@@ -292,6 +303,9 @@ def rerank(items: list, user, *, features_of, uid_of=None,
292 profile.get("global") or {}, active) 303 profile.get("global") or {}, active)
293 except Exception: 304 except Exception:
294 p, reasons = None, [] 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 if p is None: 309 if p is None:
296 final = (1.0 - blend) * base + blend * 0.5 310 final = (1.0 - blend) * base + blend * 0.5
297 else: 311 else:
298 312
Menu

Theme