edtech / apps /admin /src /components /AdminChat.tsx
CognxSafeTrack
feat(admin): UX non-tech complète + i18n 4 langues
66ff7a1
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>
)}
</>
);
}