Spaces:
Sleeping
Sleeping
| '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> | |
| ); | |
| } | |