TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1"use client";2import { useEffect, useRef, useState } from "react";3import Link from "next/link";4import type { ListingCard } from "@/lib/types";5import { ListingCardView } from "@/components/listing-card";6import { IconSpark } from "@/components/icons";7import { Notice } from "@/components/ui";8type Msg = { role: "user" | "assistant"; content: string; citations?: string[]; cards?: ListingCard[] };9const STARTERS = ["Quels sont les meilleurs quartiers familiaux à Québec ?", "Montre-moi des condos à louer à Montréal sous 1 800 $", "Combien vaut le 1234 rue des Érables à Québec ?", "Quel est le prix médian des maisons en Montérégie ?", "Versement mensuel pour 500 000 $ avec 10 % de mise de fonds à 4,9 %"];10export function AssistantView({ uid, compact = false }: { uid?: string; compact?: boolean }) {11 const [msgs, setMsgs] = useState<Msg[]>([]); const [q, setQ] = useState(""); const [busy, setBusy] = useState(false); const end = useRef<HTMLDivElement>(null);12 useEffect(() => { end.current?.scrollIntoView({ behavior: "smooth", block: "nearest" }); }, [msgs, busy]);13 const send = async (text: string) => {14 if (!text.trim() || busy) return;15 const next: Msg[] = [...msgs, { role: "user", content: text.trim() }]; setMsgs(next); setQ(""); setBusy(true);16 const r = await fetch("/api/assistant", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: next.map(({ role, content }) => ({ role, content })), context: uid ? { uid } : undefined }) });17 const d = await r.json(); let cards: ListingCard[] = [];18 if (r.ok && d.citations?.length) { const c = await fetch(`/api/listings?uids=${encodeURIComponent(d.citations.slice(0, 6).join(","))}`).then((r) => r.json()).catch(() => null); cards = c?.items ?? []; }19 setMsgs([...next, { role: "assistant", content: r.ok ? d.text : (d.error ?? "Erreur"), citations: d.citations, cards }]); setBusy(false);20 };21 return (22 <div className={`flex flex-col ${compact ? "h-[520px]" : "min-h-[70dvh]"}`}>23 {!compact && <div className="mb-4"><h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-2"><span className="p-2 rounded-full bg-brand-soft text-brand-dark"><IconSpark /></span> Assistant Toveli</h1><p className="text-ink-2 mt-1">Il interroge les données Toveli (annonces, marché, rôle, ventes) et cite les fiches qu'il utilise. Il ne devine pas de prix.</p></div>}24 <div className="flex-1 overflow-auto space-y-4 pr-1">25 {msgs.length === 0 && <div className="flex flex-wrap gap-2">{STARTERS.map((s) => <button key={s} type="button" onClick={() => send(s)} className="tv-chip text-left whitespace-normal">{s}</button>)}</div>}26 {msgs.map((m, i) => (27 <div key={i} className={`flex ${m.role === "user" ? "justify-end" : "justify-start"}`}>28 <div className={`max-w-[88%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap ${m.role === "user" ? "bg-brand text-white rounded-br-sm" : "bg-surface-2 text-ink rounded-bl-sm"}`}>29 {m.content}30 {m.cards && m.cards.length > 0 && <div className="mt-3 grid gap-2">{m.cards.map((c) => <ListingCardView key={c.uid} c={c} compact />)}</div>}31 </div>32 </div>33 ))}34 {busy && <div className="flex"><div className="bg-surface-2 rounded-2xl px-4 py-2.5 text-sm text-ink-3">Je consulte les données…</div></div>}35 <div ref={end} />36 </div>37 <form onSubmit={(e) => { e.preventDefault(); send(q); }} className="mt-3 flex gap-2">38 <input value={q} onChange={(e) => setQ(e.target.value)} className="tv-input !pl-4 flex-1" placeholder="Posez votre question…" aria-label="Votre question" disabled={busy} />39 <button type="submit" className="tv-btn tv-btn-primary" disabled={busy || !q.trim()}>Envoyer</button>40 </form>41 <div className="mt-2"><Notice>Réponses générées par un modèle de langage à partir des données Toveli ; vérifiez sur la fiche d'origine. Pas de conseil financier, juridique ou d'évaluation agréée. <Link href="/confidentialite" className="underline">Vos messages ne sont pas conservés</Link>.</Notice></div>42 </div>43 );44}45