Download apps/admin/src/components/AdminChat.tsx from safetrack/edtech: direct link, hf CLI and curl.
- Browser
- Download file 9.78 kB
-
https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/admin/src/components/AdminChat.tsx
- Command line
-
hf download hf://spaces/safetrack/edtech/apps/admin/src/components/AdminChat.tsx
-
curl -L -o AdminChat.tsx https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/admin/src/components/AdminChat.tsx
9.78 kB
| 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> | |
| )} | |
| </> | |
| ); | |
| } | |