import React, { useState, useCallback, useRef, useEffect, lazy, Suspense } from 'react'; import { Send, Smile, Paperclip, X, Image as ImageIcon, File } from 'lucide-react'; import { useChat } from '../hooks/useChat'; import { useChatStore } from '../store/chatStore'; // PERF: Lazy load EmojiPicker (composant volumineux ~200KB) const EmojiPicker = lazy(() => import('emoji-picker-react').then(module => ({ default: module.default }))); import { Theme } from 'emoji-picker-react'; import { useDropzone } from 'react-dropzone'; import { apiClient } from '@/services/api/client'; import { MessageAttachment } from '../types'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; import { logger } from '@/utils/logger'; export const ChatInput: React.FC = () => { const [message, setMessage] = useState(''); const [attachments, setAttachments] = useState([]); const [isUploading, setIsUploading] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const { sendMessage, setTyping } = useChat(); const { currentConversationId } = useChatStore(); const typingTimeoutRef = useRef(null); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if ((message.trim() || attachments.length > 0) && currentConversationId) { sendMessage(message, attachments.length > 0 ? attachments : undefined); setMessage(''); setAttachments([]); // Stop typing indicator immediately if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } setTyping(false); } }; const fileInputRef = useRef(null); const onDrop = useCallback(async (acceptedFiles: File[]) => { setIsUploading(true); try { const uploadPromises = acceptedFiles.map(async (file) => { const formData = new FormData(); formData.append('file', file); // Use existing upload endpoint const response = await apiClient.post('/uploads', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); const data = response.data; return { file_name: file.name, file_type: file.type, file_url: data.url, // Assuming backend returns { url: "..." } file_size: file.size, } as MessageAttachment; }); const newAttachments = await Promise.all(uploadPromises); setAttachments((prev) => [...prev, ...newAttachments]); } catch (error) { logger.error('Failed to upload files', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); } finally { setIsUploading(false); } }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, noClick: true, // We want custom click on button }); const handleEmojiClick = (emojiData: { emoji: string }) => { setMessage((prev) => prev + emojiData.emoji); setShowEmojiPicker(false); }; const handleFileButtonClick = () => { fileInputRef.current?.click(); }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { onDrop(Array.from(e.target.files)); } }; const removeAttachment = (index: number) => { setAttachments((prev) => prev.filter((_, i) => i !== index)); }; // Typing indicator logic useEffect(() => { if (message.length > 0) { setTyping(true); if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } typingTimeoutRef.current = setTimeout(() => { setTyping(false); }, 3000); // Stop typing after 3 seconds of inactivity } else { setTyping(false); } }, [message, setTyping]); return (
{/* File Previews */} {attachments.length > 0 && (
{attachments.map((att, i) => (
{att.file_type.startsWith('image') ? ( ) : ( )} {att.file_name}
))}
)} {isDragActive && (

Déposez vos fichiers ici

)}
{showEmojiPicker && (
setShowEmojiPicker(false)} />
}>
)}
setMessage(e.target.value)} placeholder="Écrire un message..." className="flex-1 p-2 bg-white border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all" disabled={!currentConversationId || isUploading} />
); }; // Helper for class names since Lucide and ShadUI might be used function cn(...classes: (string | boolean | undefined)[]) { return classes.filter(Boolean).join(' '); }