| import { useState, useRef, useEffect } from 'react'; |
| import { MessageCircle, X, Send, Flame } from 'lucide-react'; |
| import { useTranslation } from 'react-i18next'; |
| import { api } from '@/lib/api'; |
| import { useAuth } from '@/lib/auth'; |
| import { useTenant } from '@/lib/tenant'; |
|
|
| export type AdminChatPage = 'billing' | 'settings' | 'templates' | 'agent' | 'onboarding' | 'general'; |
|
|
| interface ChatMessage { |
| role: 'user' | 'assistant'; |
| text: string; |
| } |
|
|
| interface AdminChatProps { |
| page: AdminChatPage; |
| } |
|
|
| export default function AdminChat({ page }: AdminChatProps) { |
| const { t } = useTranslation(); |
| const { token, user } = useAuth(); |
| const { selectedOrgId } = useTenant(); |
| const [open, setOpen] = useState(false); |
| const [messages, setMessages] = useState<ChatMessage[]>([]); |
| const [input, setInput] = useState(''); |
| const [loading, setLoading] = useState(false); |
| const chatEndRef = useRef<HTMLDivElement>(null); |
|
|
| const PAGE_CONFIG: Record<AdminChatPage, { title: string; subtitle: string; questions: string[] }> = { |
| billing: { |
| title: t('admin_chat.billing_title'), |
| subtitle: t('admin_chat.billing_subtitle'), |
| questions: [ |
| t('admin_chat.billing_q1'), |
| t('admin_chat.billing_q2'), |
| t('admin_chat.billing_q3'), |
| t('admin_chat.billing_q4'), |
| ], |
| }, |
| settings: { |
| title: t('admin_chat.settings_title'), |
| subtitle: t('admin_chat.settings_subtitle'), |
| questions: [ |
| t('admin_chat.settings_q1'), |
| t('admin_chat.settings_q2'), |
| t('admin_chat.settings_q3'), |
| t('admin_chat.settings_q4'), |
| ], |
| }, |
| templates: { |
| title: t('admin_chat.templates_title'), |
| subtitle: t('admin_chat.templates_subtitle'), |
| questions: [ |
| t('admin_chat.templates_q1'), |
| t('admin_chat.templates_q2'), |
| t('admin_chat.templates_q3'), |
| t('admin_chat.templates_q4'), |
| ], |
| }, |
| agent: { |
| title: t('admin_chat.agent_title'), |
| subtitle: t('admin_chat.agent_subtitle'), |
| questions: [ |
| t('admin_chat.agent_q1'), |
| t('admin_chat.agent_q2'), |
| t('admin_chat.agent_q3'), |
| t('admin_chat.agent_q4'), |
| ], |
| }, |
| onboarding: { |
| title: t('admin_chat.onboarding_title'), |
| subtitle: t('admin_chat.onboarding_subtitle'), |
| questions: [ |
| t('admin_chat.onboarding_q1'), |
| t('admin_chat.onboarding_q2'), |
| t('admin_chat.onboarding_q3'), |
| t('admin_chat.onboarding_q4'), |
| ], |
| }, |
| general: { |
| title: t('admin_chat.general_title'), |
| subtitle: t('admin_chat.general_subtitle'), |
| questions: [ |
| t('admin_chat.general_q1'), |
| t('admin_chat.general_q2'), |
| t('admin_chat.general_q3'), |
| t('admin_chat.general_q4'), |
| ], |
| }, |
| }; |
|
|
| const config = PAGE_CONFIG[page]; |
|
|
| useEffect(() => { |
| setMessages([]); |
| setInput(''); |
| }, [page]); |
|
|
| useEffect(() => { |
| if (open) chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| }, [messages, open]); |
|
|
| const send = async (question?: string) => { |
| const q = (question ?? input).trim(); |
| if (!q || loading || !selectedOrgId || !token) return; |
| setInput(''); |
| setMessages(prev => [...prev, { role: 'user', text: q }]); |
| setLoading(true); |
| try { |
| const res = await api.post('/v1/billing/chat', { |
| question: q, |
| language: (user as any)?.language ?? 'FR', |
| page, |
| }, token, selectedOrgId); |
| setMessages(prev => [...prev, { role: 'assistant', text: res.answer }]); |
| } catch { |
| setMessages(prev => [...prev, { role: 'assistant', text: t('admin_chat.error') }]); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| return ( |
| <> |
| {/* Floating button */} |
| <button |
| onClick={() => setOpen(v => !v)} |
| className={`fixed bottom-6 right-6 z-40 w-14 h-14 rounded-full shadow-xl flex items-center justify-center transition-all duration-200 ${ |
| open ? 'bg-slate-800 scale-90' : 'bg-indigo-600 hover:bg-indigo-700 hover:scale-105' |
| }`} |
| aria-label={t('admin_chat.aria_label')} |
| > |
| {open |
| ? <X className="w-5 h-5 text-white" /> |
| : <MessageCircle className="w-6 h-6 text-white" /> |
| } |
| </button> |
| |
| {/* Chat panel */} |
| {open && ( |
| <div |
| className="fixed bottom-24 right-6 z-40 w-80 sm:w-96 bg-white rounded-3xl shadow-2xl border border-slate-100 flex flex-col overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-200" |
| style={{ maxHeight: '480px' }} |
| > |
| {/* Header */} |
| <div className="px-5 py-4 bg-gradient-to-r from-indigo-600 to-indigo-500 text-white shrink-0"> |
| <p className="font-bold text-sm">{config.title}</p> |
| <p className="text-xs text-indigo-200 mt-0.5">{config.subtitle}</p> |
| </div> |
| |
| {/* Quick questions (only when no messages yet) */} |
| {messages.length === 0 && ( |
| <div className="px-4 pt-3 pb-2 flex flex-col gap-1.5 shrink-0"> |
| {config.questions.map(q => ( |
| <button |
| key={q} |
| onClick={() => send(q)} |
| disabled={loading} |
| className="text-left text-xs px-3 py-2 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-700 text-slate-600 rounded-xl transition-colors border border-transparent hover:border-indigo-200 disabled:opacity-50" |
| > |
| {q} |
| </button> |
| ))} |
| </div> |
| )} |
| |
| {/* Messages */} |
| <div className="flex-1 overflow-y-auto px-4 py-3 space-y-3 min-h-0"> |
| {messages.length === 0 && ( |
| <div className="text-center pt-2"> |
| <Flame className="w-6 h-6 mx-auto mb-1 text-slate-200" /> |
| <p className="text-slate-300 text-xs">{t('admin_chat.choose_or_write')}</p> |
| </div> |
| )} |
| {messages.map((msg, i) => ( |
| <div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}> |
| <div className={`max-w-[80%] px-3 py-2 rounded-2xl text-xs leading-relaxed whitespace-pre-wrap ${ |
| msg.role === 'user' |
| ? 'bg-indigo-600 text-white rounded-br-sm' |
| : 'bg-slate-100 text-slate-800 rounded-bl-sm' |
| }`}> |
| {msg.text} |
| </div> |
| </div> |
| ))} |
| {loading && ( |
| <div className="flex justify-start"> |
| <div className="bg-slate-100 px-3 py-2.5 rounded-2xl rounded-bl-sm flex items-center gap-1"> |
| <span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} /> |
| <span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} /> |
| <span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} /> |
| </div> |
| </div> |
| )} |
| <div ref={chatEndRef} /> |
| </div> |
| |
| {/* Input */} |
| <div className="px-4 py-3 border-t border-slate-100 flex gap-2 shrink-0"> |
| <input |
| type="text" |
| value={input} |
| onChange={e => setInput(e.target.value)} |
| onKeyDown={e => e.key === 'Enter' && send()} |
| placeholder={t('admin_chat.input_placeholder')} |
| className="flex-1 text-xs border border-slate-200 rounded-xl px-3 py-2 outline-none focus:ring-2 focus:ring-indigo-300 focus:border-indigo-300" |
| disabled={loading} |
| /> |
| <button |
| onClick={() => send()} |
| disabled={!input.trim() || loading} |
| className="bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white rounded-xl px-3 py-2 flex items-center transition-colors" |
| > |
| <Send className="w-3.5 h-3.5" /> |
| </button> |
| </div> |
| </div> |
| )} |
| </> |
| ); |
| } |
|
|