saas-veille / components /Chatbot.tsx
PrestaGhis's picture
Update components/Chatbot.tsx
2aac07d verified
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { X, Send, Sparkles, Minus, RotateCcw } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { supabase } from '@/lib/supabase';
interface Message {
role: 'user' | 'assistant';
content: string;
}
export default function Chatbot({ clientId }: { clientId?: string }) {
const [isOpen, setIsOpen] = useState(false);
const [msg, setMsg] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [chatbotMascotUrl, setChatbotMascotUrl] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const router = useRouter();
useEffect(() => {
const fetchMascot = async () => {
try {
// 1. Si on est chez un client, on cherche SA mascotte
if (clientId) {
const { data } = await supabase.from('client_settings').select('chatbot_mascot_url').eq('client_id', clientId).maybeSingle();
if (data?.chatbot_mascot_url) {
setChatbotMascotUrl(data.chatbot_mascot_url);
return;
}
}
// 2. Sinon (ou si client n'a pas de mascotte), on prend la mascotte Admin (GLOBALE)
const { data: globalData } = await supabase.from('settings').select('chatbot_mascot_url').eq('id', 1).maybeSingle();
if (globalData?.chatbot_mascot_url) {
setChatbotMascotUrl(globalData.chatbot_mascot_url);
}
} catch (e) {
console.error("Error fetching chatbot mascot:", e);
}
};
fetchMascot();
}, [clientId]);
useEffect(() => {
if (msg.length === 0) {
setMsg([{ role: 'assistant', content: 'Bonjour ! 👋 CoachArgy au rapport ! Comment puis-je vous aider ?' }]);
}
}, [msg.length]);
useEffect(() => {
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [msg, loading]);
const resetChat = () => {
if (confirm("Réinitialiser ?")) setMsg([{ role: 'assistant', content: 'Je vous écoute.' }]);
};
const renderContent = (content: string) => {
if (!content) return null;
const parts = content.split(/(\[.*?\]\(.*?\))|(\*\*.*?\*\*)/g);
return parts.map((part, index) => {
if (!part) return null;
const linkMatch = part.match(/\[(.*?)\]\((.*?)\)/);
if (linkMatch) return <span key={index} onClick={() => { router.push(linkMatch[2]); }} className="text-amber-400 font-bold underline cursor-pointer hover:text-amber-300 mx-0.5">{linkMatch[1]}</span>;
const boldMatch = part.match(/\*\*(.*?)\*\*/);
if (boldMatch) return <strong key={index} className="text-white font-bold">{boldMatch[1]}</strong>;
return <span key={index}>{part}</span>;
});
};
async function send() {
if (!input.trim() || loading) return;
const userMsg: Message = { role: 'user', content: input };
setMsg(prev => [...prev, userMsg]);
setInput('');
setLoading(true);
try {
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token;
const res = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ messages: [...msg, userMsg], clientId })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
setMsg(prev => [...prev, { role: 'assistant', content: data.message }]);
} catch (e: any) {
setMsg(prev => [...prev, { role: 'assistant', content: '❌ Erreur : ' + (e instanceof Error ? e.message : 'Une erreur est survenue') }]);
} finally { setLoading(false); }
}
return (
<div className="fixed bottom-4 right-4 z-[9999] font-sans">
{isOpen && (
<div className="absolute bottom-16 right-0 w-[320px] md:w-[400px] h-[550px] bg-slate-900 border border-slate-700 shadow-2xl rounded-2xl flex flex-col overflow-hidden text-white">
<div className="p-4 bg-slate-800 border-b border-slate-700 flex justify-between items-center px-5">
<div className="flex items-center gap-2">
{chatbotMascotUrl ? (
<img src={chatbotMascotUrl} alt="Mascot" className="w-6 h-6 rounded-full object-cover border border-slate-600 shadow-sm" />
) : (
<Sparkles size={16} className="text-indigo-400" />
)}
<span className="font-bold text-sm tracking-tight text-white/90">CoachArgy</span>
</div>
<div className="flex items-center gap-1">
<button onClick={resetChat} className="p-1 px-2 hover:bg-white/5 rounded text-slate-400"><RotateCcw size={14} /></button>
<button onClick={() => setIsOpen(false)} className="p-1 px-2 hover:bg-white/5 rounded text-slate-400"><Minus size={16} /></button>
</div>
</div>
<div ref={scrollRef} className="flex-1 overflow-y-auto p-5 space-y-5 bg-slate-950/50">
{msg.map((m, i) => (
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] p-3.5 rounded-xl text-sm ${m.role === 'user' ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-200 border border-slate-700'}`}>
<div className="whitespace-pre-wrap">{renderContent(m.content)}</div>
</div>
</div>
))}
{loading && <div className="text-[10px] animate-pulse text-indigo-400 font-bold px-2 tracking-widest uppercase text-center">Analyse...</div>}
</div>
<div className="p-4 bg-slate-900 border-t border-slate-800 flex gap-2">
<input className="flex-1 bg-slate-800 border border-slate-700 rounded-xl px-4 py-2 text-white text-sm outline-none focus:ring-1 focus:ring-indigo-500 transition-all" value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} placeholder="Ma question..." />
<button onClick={send} className="bg-indigo-600 p-2.5 rounded-xl"><Send size={18} /></button>
</div>
</div>
)}
<button onClick={() => setIsOpen(!isOpen)} className={`relative w-14 h-14 rounded-full shadow-[0_0_20px_rgba(79,70,229,0.5)] flex items-center justify-center text-white border-2 transition-all duration-300 z-50 overflow-hidden ${isOpen ? 'bg-slate-800 border-slate-600 scale-100 rotate-90' : 'bg-gradient-to-br from-indigo-500 to-purple-600 border-indigo-400 hover:scale-110 active:scale-95 animate-[pulse_3s_ease-in-out_infinite]'}`}>
{/* Petit point de notification si fermé */}
{!isOpen && <span className="absolute top-0 right-0 w-3.5 h-3.5 bg-red-500 border-2 border-slate-900 rounded-full animate-bounce z-10"></span>}
{isOpen ? (
<X size={24} />
) : chatbotMascotUrl ? (
<img src={chatbotMascotUrl} alt="Coach" className="w-full h-full object-cover" />
) : (
<Sparkles size={24} className="animate-[wiggle_1s_ease-in-out_infinite]" />
)}
</button>
</div>
);
}