import React, { useRef, useState, useCallback, useEffect } from 'react' import { useDropzone } from 'react-dropzone' import { Send, Plus, X, ImageIcon, Paperclip, UploadCloud } from 'lucide-react' const MAX_IMAGES = 5 const ChatInput = ({ onSendMessage, isLoading, onImageClick }) => { const [input, setInput] = useState(() => sessionStorage.getItem('chat_draft') || '') const [uploadedImages, setUploadedImages] = useState([]) const [showAttachMenu, setShowAttachMenu] = useState(false) const [uploadError, setUploadError] = useState('') const textareaRef = useRef(null) // Draft persistence useEffect(() => { sessionStorage.setItem('chat_draft', input) }, [input]) // Initial image restoration useEffect(() => { const savedImages = sessionStorage.getItem('chat_images') if (savedImages) { try { const parsed = JSON.parse(savedImages) const restored = parsed.map(img => { // Convert base64 back to Blob/File if needed, or just use data URL as preview return { file: null, // Original file is lost on refresh, but we have the data preview: img.data, name: img.name, type: img.type } }) setUploadedImages(restored) } catch (e) { console.error('Failed to restore images:', e) } } }, []) // Image persistence (Base64) useEffect(() => { if (uploadedImages.length === 0) { sessionStorage.removeItem('chat_images') return } // Only save images that have a preview (base64 or blob URL) // If it's a blob URL, it won't survive refresh, so we need to ensure they are base64 when adding const persistImages = async () => { const dataToSave = await Promise.all(uploadedImages.map(async (img) => { if (img.preview.startsWith('data:')) { return { name: img.name, type: img.type, data: img.preview } } // If it's a blob URL, we should have converted it already on drop return { name: img.name, type: img.type, data: img.preview } })) sessionStorage.setItem('chat_images', JSON.stringify(dataToSave)) } persistImages() }, [uploadedImages]) // Auto-resize textarea useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto' textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 200) + 'px' } }, [input]) // Clear error after 3 seconds useEffect(() => { if (uploadError) { const timer = setTimeout(() => setUploadError(''), 3000) return () => clearTimeout(timer) } }, [uploadError]) // Close menu when clicking outside useEffect(() => { const handleClickOutside = (e) => { if (showAttachMenu && !e.target.closest('.attach-btn-wrapper')) { setShowAttachMenu(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [showAttachMenu]) const handleKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSubmit() } } const onDrop = useCallback((acceptedFiles) => { const processFiles = async () => { const newImagesPromises = acceptedFiles.map(file => { return new Promise((resolve) => { const reader = new FileReader() reader.onloadend = () => { resolve({ file, preview: reader.result, name: file.name, type: file.type }) } reader.readAsDataURL(file) }) }) const newImages = await Promise.all(newImagesPromises) setUploadedImages(prev => { const remaining = MAX_IMAGES - prev.length if (remaining <= 0) { setUploadError(`Bạn chỉ được tải tối đa ${MAX_IMAGES} ảnh`) return prev } if (newImages.length > remaining) { setUploadError(`Chỉ nhận thêm ${remaining} ảnh cuối cùng`) } return [...prev, ...newImages.slice(0, remaining)] }) } processFiles() setShowAttachMenu(false) }, []) const { getRootProps, getInputProps, isDragActive, open: openFilePicker } = useDropzone({ onDrop, accept: { 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'] }, maxFiles: MAX_IMAGES, noClick: true, noKeyboard: true, disabled: uploadedImages.length >= MAX_IMAGES, }) const removeImage = (index) => { setUploadedImages(prev => prev.filter((_, i) => i !== index)) } const clearAllImages = () => { setUploadedImages([]) } const handleSubmit = () => { if ((!input.trim() && uploadedImages.length === 0) || isLoading) return onSendMessage(input, uploadedImages) // Clear persistence setInput('') setUploadedImages([]) sessionStorage.removeItem('chat_draft') sessionStorage.removeItem('chat_images') if (textareaRef.current) textareaRef.current.style.height = 'auto' } return (
{/* Image Previews & Counter */} {uploadedImages.length > 0 && (
Đã đính kèm: {uploadedImages.length}/{MAX_IMAGES} ảnh
{uploadedImages.map((img, idx) => (
onImageClick?.(uploadedImages.map(img => img.preview), idx)} title="Xem ảnh lớn" > {`Preview
))}
)} {/* Upload Error Message */} {uploadError && (
{uploadError}
)}
{/* Drag Overlay - Compact & Premium */} {isDragActive && (
Thả ảnh vào đây
)}
{showAttachMenu && (
)}