import React, { useState, useCallback, useRef, useEffect, lazy, Suspense, } from 'react'; import { Send, Smile, Paperclip, X, Image as ImageIcon, File, Mic, } from 'lucide-react'; import { useChat } from '../hooks/useChat'; import { useChatStore } from '../store/chatStore'; 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'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { useIsRateLimited } from '@/hooks/useIsRateLimited'; // Lazy load const EmojiPicker = lazy(() => import('emoji-picker-react').then((module) => ({ default: module.default })), ); 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 isRateLimited = useIsRateLimited(); 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([]); 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); 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, 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), }); } finally { setIsUploading(false); } }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, noClick: true, }); const handleEmojiClick = (emojiData: { emoji: string }) => { setMessage((prev) => prev + emojiData.emoji); setShowEmojiPicker(false); }; const removeAttachment = (index: number) => { setAttachments((prev) => prev.filter((_, i) => i !== index)); }; useEffect(() => { if (message.length > 0) { setTyping(true); if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); typingTimeoutRef.current = setTimeout(() => { setTyping(false); }, 3000); } else { setTyping(false); } }, [message, setTyping]); return (
{/* Upload Overlay */} {isDragActive && (

Initiate Data Transfer

)} {/* Attachments Preview */} {attachments.length > 0 && (
{attachments.map((att, i) => (
{att.file_type.startsWith('image') ? ( ) : ( )} {att.file_name}
))}
)}
{showEmojiPicker && (
setShowEmojiPicker(false)} />
} >
)}
setMessage(e.target.value)} placeholder="Broadcast message..." className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-white placeholder:text-kodo-secondary/50 focus:outline-none focus:border-border/50 focus:ring-1 focus:ring-kodo-steel/50 transition-all font-mono text-sm" disabled={!currentConversationId || isUploading} /> {message.length === 0 && !isUploading && ( )}
); };