Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 47.5%
HTML 27.9%
TypeScript 15.5%
CSS 7.2%
JavaScript 2%
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/KaAssistant.tsx : « ✦ Demander à Ka » — bouton discret (bas droite,5// au-dessus du CTA) qui remplace la grosse bulle KA Agent sur la fiche, et6// bottom sheet de démarrage : contexte de la propriété, suggestions7// prédéfinies, champ libre. L'envoi passe par le widget KA Agent existant8// (ka-agent.js, backend api-ka inchangé) : on ouvre son panneau et on lui9// transmet la question, enrichie du contexte de la fiche (adresse, ville,10// prix, type, lien).11// -----------------------------------------------------------------------------12import { useEffect, useState } from "react";13import { Listing, fmtPrice } from "../api";14import { Ico } from "../components/Icons";15import BottomSheet from "./BottomSheet";1617const SUGGESTIONS = [18 "Est-ce un bon prix pour ce secteur ?",19 "Compare cette propriété au quartier",20 "Quels frais dois-je prévoir en plus du prix ?",21 "Quels sont les points de vigilance ?",22 "Explique l'estimation Vrai-Prix",23];2425/** Ouvre le panneau KA Agent (widget partagé) et envoie la question. */26function envoyerAKa(question: string): boolean {27 const btn = document.querySelector<HTMLButtonElement>(".kaa-btn");28 const ta = document.querySelector<HTMLTextAreaElement>(".kaa-panel textarea");29 const send = document.querySelector<HTMLButtonElement>(".kaa-in button");30 if (!btn || !ta || !send) return false;31 btn.click();32 ta.value = question;33 setTimeout(() => send.click(), 60);34 return true;35}3637export default function KaAssistant({ l, hidden }: { l: Listing; hidden?: boolean }) {38 const [open, setOpen] = useState(false);39 const [q, setQ] = useState("");40 const [err, setErr] = useState(false);41 useEffect(() => {42 const on = () => setOpen(true);43 window.addEventListener("ik:askka", on);44 return () => window.removeEventListener("ik:askka", on);45 }, []);4647 const contexte = () =>48 `(Propriété consultée sur Immo-Ka : ${l.address || l.title}${l.city ? `, ${l.city}` : ""}` +49 `${l.price != null ? ` — ${fmtPrice(l.price)}` : ""}${l.property_type ? ` — ${l.property_type}` : ""}` +50 ` — https://www.immo-ka.com/propriete/${encodeURIComponent(l.uid)})`;5152 const poser = (question: string) => {53 const ok = envoyerAKa(`${question}\n\n${contexte()}`);54 if (ok) { setOpen(false); setQ(""); setErr(false); } else setErr(true);55 };5657 return (58 <>59 <button type="button" className={`ik-ka-btn ${hidden ? "hide" : ""}`} onClick={() => setOpen(true)}60 aria-label="Demander à Ka, l'assistant Groupe KA">61 <Ico name="sparkles" size={16} /> Demander à Ka62 </button>63 <BottomSheet open={open} onClose={() => setOpen(false)} title="Demander à Ka" sub="Assistant Groupe KA · connaît cette fiche"64 footer={65 <form className="ik-ka-in" onSubmit={(e) => { e.preventDefault(); if (q.trim()) poser(q.trim()); }}>66 <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Posez votre question sur cette propriété…" aria-label="Votre question" />67 <button type="submit" aria-label="Envoyer" disabled={!q.trim()}><Ico name="chevright" size={20} /></button>68 </form>69 }>70 <div className="ik-ka-intro">71 <span className="ik-ka-avatar" aria-hidden="true"><Ico name="sparkles" size={18} /></span>72 <p>Je peux situer ce prix dans le quartier, expliquer les données de la fiche, estimer les frais et chercher dans les autres plateformes Groupe KA.</p>73 </div>74 <div className="ik-ka-ctx">75 {l.images?.[0] && <img src={l.images[0]} alt="" />}76 <span style={{ minWidth: 0 }}>77 <b>{l.address || l.title}</b>78 {[l.city, l.price != null ? fmtPrice(l.price) : null, l.property_type].filter(Boolean).join(" · ")}79 </span>80 </div>81 <div className="ik-ka-sugs">82 {SUGGESTIONS.map((s) => (83 <button type="button" className="ik-ka-sug" key={s} onClick={() => poser(s)}>84 {s} <Ico name="chevright" size={16} />85 </button>86 ))}87 </div>88 {err && <p className="ik-note warn">L'assistant n'est pas encore chargé — réessayez dans un instant.</p>}89 </BottomSheet>90 </>91 );92}93