/* agent-chat.tsx spboucher.ai Web Author: Simon-Pierre Boucher Mail: contact@spboucher.ai */ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; import { ArrowUp, Minus, Sparkles, X } from "lucide-react"; interface ChatMessage { role: "user" | "assistant"; content: string; } const SUGGESTIONS = [ "Who is Simon-Pierre Boucher?", "What does his research focus on?", "Tell me about the Zyquo apps", "Qu'est-ce que HF Market Data ?", ]; const GREETING = "Hi — I'm SPB Agent, Simon-Pierre Boucher's AI assistant. Ask me anything about his research, teaching, or the software he builds. Je réponds aussi en français."; /** Floating chat bubble + panel, streaming answers from /api/agent. */ export function AgentChat() { const [open, setOpen] = useState(false); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [streaming, setStreaming] = useState(false); const scrollRef = useRef(null); const inputRef = useRef(null); const abortRef = useRef(null); const reducedMotion = useReducedMotion(); useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages, open]); useEffect(() => { if (open) inputRef.current?.focus(); }, [open]); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const send = useCallback( async (text: string) => { const question = text.trim(); if (!question || streaming) return; const history: ChatMessage[] = [ ...messages, { role: "user", content: question }, ]; setMessages([...history, { role: "assistant", content: "" }]); setInput(""); setStreaming(true); const controller = new AbortController(); abortRef.current = controller; try { const res = await fetch("/api/agent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: history }), signal: controller.signal, }); if (!res.ok || !res.body) { throw new Error(`HTTP ${res.status}`); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let answer = ""; for (;;) { const { done, value } = await reader.read(); if (done) break; answer += decoder.decode(value, { stream: true }); const snapshot = answer; setMessages([ ...history, { role: "assistant", content: snapshot }, ]); } } catch (err) { if (!(err instanceof DOMException && err.name === "AbortError")) { setMessages([ ...history, { role: "assistant", content: "Sorry — I couldn't reach the agent right now. Please try again in a moment, or email contact@spboucher.ai.", }, ]); } } finally { setStreaming(false); abortRef.current = null; } }, [messages, streaming], ); const onSubmit = (e: React.FormEvent) => { e.preventDefault(); void send(input); }; return ( <> {/* Panel */} {open && ( {/* Header */}

SPB Agent

{/* Messages */}
{messages.map((m, i) => ( ))} {messages.length === 0 && (

Try asking

{SUGGESTIONS.map((s) => ( ))}
)}
{/* Input */}