/* ┌──────────────────────────────────────────────────────────────────────────────┐ │ @author: Davidson Gomes │ │ @file: /app/chat/components/ChatInput.tsx │ │ Developed by: Davidson Gomes │ │ Creation date: May 14, 2025 │ │ Contact: contato@evolution-api.com │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @copyright © Evolution API 2025. All rights reserved. │ │ Licensed under the Apache License, Version 2.0 │ │ │ │ You may not use this file except in compliance with the License. │ │ You may obtain a copy of the License at │ │ │ │ http://www.apache.org/licenses/LICENSE-2.0 │ │ │ │ Unless required by applicable law or agreed to in writing, software │ │ distributed under the License is distributed on an "AS IS" BASIS, │ │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ │ See the License for the specific language governing permissions and │ │ limitations under the License. │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @important │ │ For any future changes to the code in this file, it is recommended to │ │ include, together with the modification, the information of the developer │ │ who changed it and the date of modification. │ └──────────────────────────────────────────────────────────────────────────────┘ */ "use client"; import React, { useState, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Loader2, Send, Paperclip, X, Image, FileText, File } from "lucide-react"; import { FileData, formatFileSize, isImageFile } from "@/lib/file-utils"; import { toast } from "@/hooks/use-toast"; interface ChatInputProps { onSendMessage: (message: string, files?: FileData[]) => void; isLoading?: boolean; placeholder?: string; className?: string; buttonClassName?: string; containerClassName?: string; autoFocus?: boolean; } export function ChatInput({ onSendMessage, isLoading = false, placeholder = "Type your message...", className = "", buttonClassName = "", containerClassName = "", autoFocus = true, }: ChatInputProps) { const [messageInput, setMessageInput] = useState(""); const [selectedFiles, setSelectedFiles] = useState([]); const [resetFileUpload, setResetFileUpload] = useState(false); const fileInputRef = useRef(null); const textareaRef = useRef(null); // Autofocus the textarea when the component is mounted React.useEffect(() => { // Small timeout to ensure focus is applied after the complete rendering if (autoFocus) { const timer = setTimeout(() => { if (textareaRef.current && !isLoading) { textareaRef.current.focus(); } }, 100); return () => clearTimeout(timer); } }, [isLoading, autoFocus]); const handleSendMessage = (e: React.FormEvent) => { e.preventDefault(); if (!messageInput.trim() && selectedFiles.length === 0) return; onSendMessage(messageInput, selectedFiles.length > 0 ? selectedFiles : undefined); setMessageInput(""); setSelectedFiles([]); setResetFileUpload(true); setTimeout(() => { setResetFileUpload(false); // Keep the focus on the textarea after sending the message if (autoFocus && textareaRef.current) { textareaRef.current.focus(); } }, 100); const textarea = document.querySelector("textarea"); if (textarea) textarea.style.height = "auto"; }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(e as unknown as React.FormEvent); } }; const autoResizeTextarea = (e: React.ChangeEvent) => { const textarea = e.target; textarea.style.height = "auto"; const maxHeight = 10 * 24; const newHeight = Math.min(textarea.scrollHeight, maxHeight); textarea.style.height = `${newHeight}px`; setMessageInput(textarea.value); }; const handleFilesSelected = async (e: React.ChangeEvent) => { if (!e.target.files || e.target.files.length === 0) return; const newFiles = Array.from(e.target.files); const maxFileSize = 10 * 1024 * 1024; // 10MB if (selectedFiles.length + newFiles.length > 5) { toast({ title: `You can only attach up to 5 files.`, variant: "destructive", }); return; } const validFiles: FileData[] = []; for (const file of newFiles) { if (file.size > maxFileSize) { toast({ title: `File ${file.name} exceeds the maximum size of ${formatFileSize(maxFileSize)}.`, variant: "destructive", }); continue; } try { const reader = new FileReader(); const readFile = new Promise((resolve, reject) => { reader.onload = () => { const base64 = reader.result as string; const base64Data = base64.split(',')[1]; resolve(base64Data); }; reader.onerror = reject; }); reader.readAsDataURL(file); const base64Data = await readFile; const previewUrl = URL.createObjectURL(file); validFiles.push({ filename: file.name, content_type: file.type, data: base64Data, size: file.size, preview_url: previewUrl }); } catch (error) { console.error("Error processing file:", error); toast({ title: `Error processing file ${file.name}`, variant: "destructive", }); } } if (validFiles.length > 0) { const updatedFiles = [...selectedFiles, ...validFiles]; setSelectedFiles(updatedFiles); } if (fileInputRef.current) { fileInputRef.current.value = ""; } }; const openFileSelector = () => { fileInputRef.current?.click(); }; return (
{selectedFiles.length > 0 && (
{selectedFiles.map((file, index) => (
{isImageFile(file.content_type) ? ( ) : file.content_type === 'application/pdf' ? ( ) : ( )} {file.filename} ({formatFileSize(file.size)})
))}
)}
{selectedFiles.length < 5 && ( )}