1- import { useRef , useState } from "react" ;
1+ import { useRef , useState , useEffect } from "react" ;
22import { chatStore } from "../store/chatStore" ;
3- import { X , Image , Send , Loader } from "lucide-react" ;
3+ import { X , Image , Send , Loader , FileText , Paperclip } from "lucide-react" ;
44import { toast } from "react-toastify" ;
55
66const chatBotId = "67a5af796174659ba813c735" ;
77
8+ // File size limits (in MB)
9+ const FILE_SIZE_LIMITS = {
10+ image : 3 ,
11+ document : 5 ,
12+ } ;
13+
814export const MessageInput = ( ) => {
9- const { sendMessage, isSendingMessage } = chatStore ( ) ;
10- const { selectedUser } = chatStore ( ) ;
15+ const { sendMessage, isSendingMessage, selectedUser } = chatStore ( ) ;
1116 const [ text , setText ] = useState ( "" ) ;
17+ const [ imageFile , setImageFile ] = useState ( null ) ;
1218 const [ imagePreview , setImagePreview ] = useState ( null ) ;
13- const fileInputRef = useRef ( null ) ;
19+ const [ documentFile , setDocumentFile ] = useState ( null ) ;
20+ const [ documentPreview , setDocumentPreview ] = useState ( null ) ;
21+ const [ showAttachMenu , setShowAttachMenu ] = useState ( false ) ;
22+ const imageInputRef = useRef ( null ) ;
23+ const documentInputRef = useRef ( null ) ;
24+ const attachMenuRef = useRef ( null ) ;
25+
26+ useEffect ( ( ) => {
27+ const handleClickOutside = ( event ) => {
28+ if ( attachMenuRef . current && ! attachMenuRef . current . contains ( event . target ) ) {
29+ setShowAttachMenu ( false ) ;
30+ }
31+ } ;
32+
33+ document . addEventListener ( "mousedown" , handleClickOutside ) ;
34+ return ( ) => document . removeEventListener ( "mousedown" , handleClickOutside ) ;
35+ } , [ ] ) ;
1436
1537 const handleImageChange = ( event ) => {
1638 const file = event . target . files [ 0 ] ;
39+ if ( ! file ) return ;
40+
1741 if ( ! file . type . startsWith ( "image/" ) ) {
1842 toast . error ( "Please select an image file." ) ;
1943 return ;
2044 }
45+
46+ if ( file . size > FILE_SIZE_LIMITS . image * 1024 * 1024 ) {
47+ toast . error ( `Image size must be less than ${ FILE_SIZE_LIMITS . image } MB` ) ;
48+ return ;
49+ }
50+
51+ setImageFile ( file ) ;
2152 const reader = new FileReader ( ) ;
2253 reader . onloadend = ( ) => {
2354 setImagePreview ( reader . result ) ;
2455 } ;
2556 reader . readAsDataURL ( file ) ;
57+ setShowAttachMenu ( false ) ;
58+ } ;
59+
60+ const handleDocumentChange = ( event ) => {
61+ const file = event . target . files [ 0 ] ;
62+ if ( ! file ) return ;
63+
64+ if ( file . type !== "application/pdf" ) {
65+ toast . error ( "Only PDF files are allowed." ) ;
66+ return ;
67+ }
68+
69+ if ( file . size > FILE_SIZE_LIMITS . document * 1024 * 1024 ) {
70+ toast . error (
71+ `Document size must be less than ${ FILE_SIZE_LIMITS . document } MB`
72+ ) ;
73+ return ;
74+ }
75+
76+ setDocumentFile ( file ) ;
77+ setDocumentPreview ( {
78+ name : file . name ,
79+ size : file . size ,
80+ type : file . type ,
81+ } ) ;
82+ setShowAttachMenu ( false ) ;
2683 } ;
2784
2885 const removeImage = ( ) => {
86+ setImageFile ( null ) ;
2987 setImagePreview ( null ) ;
30- if ( fileInputRef . current ) fileInputRef . current . value = null ;
88+ if ( imageInputRef . current ) imageInputRef . current . value = null ;
89+ } ;
90+
91+ const removeDocument = ( ) => {
92+ setDocumentFile ( null ) ;
93+ setDocumentPreview ( null ) ;
94+ if ( documentInputRef . current ) documentInputRef . current . value = null ;
95+ } ;
96+
97+ const handleAttachClick = ( ) => {
98+ setShowAttachMenu ( ! showAttachMenu ) ;
99+ } ;
100+
101+ const handleImageSelect = ( ) => {
102+ imageInputRef . current ?. click ( ) ;
103+ } ;
104+
105+ const handleDocumentSelect = ( ) => {
106+ documentInputRef . current ?. click ( ) ;
31107 } ;
32108
33109 const handleSendMessage = async ( event ) => {
34110 event . preventDefault ( ) ;
35- if ( ! text . trim ( ) && ! imagePreview ) return ;
111+ if ( ! text . trim ( ) && ! imageFile && ! documentFile ) return ;
112+
36113 try {
37- await sendMessage ( {
38- text : text . trim ( ) ,
39- image : imagePreview ,
40- } ) ;
114+ const formData = new FormData ( ) ;
115+ if ( text . trim ( ) ) formData . append ( "text" , text . trim ( ) ) ;
116+ if ( imageFile ) formData . append ( "image" , imageFile ) ;
117+ if ( documentFile ) formData . append ( "document" , documentFile ) ;
118+
119+ await sendMessage ( formData ) ;
120+
41121 setText ( "" ) ;
122+ setImageFile ( null ) ;
42123 setImagePreview ( null ) ;
43- if ( fileInputRef . current ) fileInputRef . current . value = null ;
124+ setDocumentFile ( null ) ;
125+ setDocumentPreview ( null ) ;
126+ if ( imageInputRef . current ) imageInputRef . current . value = null ;
127+ if ( documentInputRef . current ) documentInputRef . current . value = null ;
44128 } catch ( error ) {
45129 console . error ( "Message not sent." , error ) ;
130+ toast . error ( "Failed to send message" ) ;
46131 }
47132 } ;
133+
48134 return (
49135 < div className = "p-4 w-full" >
136+ { /* Image Preview */ }
50137 { imagePreview && (
51138 < div className = "mb-3 flex items-center gap-2" >
52139 < div className = "relative" >
@@ -66,6 +153,30 @@ export const MessageInput = () => {
66153 </ div >
67154 ) }
68155
156+ { /* Document Preview */ }
157+ { documentPreview && (
158+ < div className = "mb-3 flex items-center gap-2" >
159+ < div className = "relative flex items-center gap-2 p-2 bg-base-200 rounded-lg border border-zinc-700" >
160+ < FileText className = "size-8 text-primary" />
161+ < div className = "flex-1" >
162+ < p className = "text-sm font-medium truncate max-w-[200px]" >
163+ { documentPreview . name }
164+ </ p >
165+ < p className = "text-xs text-zinc-500" >
166+ { ( documentPreview . size / 1024 ) . toFixed ( 2 ) } KB
167+ </ p >
168+ </ div >
169+ < button
170+ onClick = { removeDocument }
171+ className = "size-5 rounded-full bg-base-300 flex items-center justify-center hover:bg-error hover:text-error-content"
172+ type = "button"
173+ >
174+ < X className = "size-3" />
175+ </ button >
176+ </ div >
177+ </ div >
178+ ) }
179+
69180 < form onSubmit = { handleSendMessage } className = "flex items-center gap-2" >
70181 < div className = "flex-1 flex gap-2" >
71182 < input
@@ -75,31 +186,71 @@ export const MessageInput = () => {
75186 value = { text }
76187 onChange = { ( event ) => setText ( event . target . value ) }
77188 />
189+
78190 < input
79191 type = "file"
80192 className = "hidden"
81193 accept = "image/*"
82- ref = { fileInputRef }
194+ ref = { imageInputRef }
83195 onChange = { handleImageChange }
84196 />
85- { selectedUser . _id !== chatBotId ? (
86- < button
87- type = "button"
88- className = { `hidden sm:flex btn btn-circle ${
89- imagePreview ? "text-emerald-500" : "text-zinc-400"
90- } `}
91- onClick = { ( ) => fileInputRef . current ?. click ( ) }
92- >
93- < Image size = { 20 } />
94- </ button >
95- ) : null }
197+ < input
198+ type = "file"
199+ className = "hidden"
200+ accept = ".pdf"
201+ ref = { documentInputRef }
202+ onChange = { handleDocumentChange }
203+ />
204+
205+ { selectedUser . _id !== chatBotId && (
206+ < div className = "relative" ref = { attachMenuRef } >
207+ < button
208+ type = "button"
209+ className = { `btn btn-circle ${
210+ imagePreview || documentPreview ? "text-emerald-500" : "text-zinc-400"
211+ } `}
212+ onClick = { handleAttachClick }
213+ title = "Attach File"
214+ >
215+ < Paperclip size = { 20 } />
216+ </ button >
217+
218+ { /* Attachment Menu */ }
219+ { showAttachMenu && (
220+ < div className = "absolute bottom-full mb-2 right-0 bg-base-200 rounded-lg shadow-lg border border-zinc-700 py-1 min-w-[160px] z-10" >
221+ < button
222+ type = "button"
223+ className = "w-full px-4 py-2 flex items-center gap-3 hover:bg-base-300 transition-colors text-left"
224+ onClick = { handleImageSelect }
225+ >
226+ < Image size = { 18 } className = "text-emerald-500" />
227+ < span className = "text-sm" > Image</ span >
228+ </ button >
229+ < button
230+ type = "button"
231+ className = "w-full px-4 py-2 flex items-center gap-3 hover:bg-base-300 transition-colors text-left"
232+ onClick = { handleDocumentSelect }
233+ >
234+ < FileText size = { 18 } className = "text-blue-500" />
235+ < span className = "text-sm" > PDF Document</ span >
236+ </ button >
237+ </ div >
238+ ) }
239+ </ div >
240+ ) }
96241 </ div >
97242 < button
98243 type = "submit"
99244 className = "btn btn-circle btn-sm btn-primary"
100- disabled = { ( ! text . trim ( ) && ! imagePreview ) || isSendingMessage }
245+ disabled = {
246+ ( ! text . trim ( ) && ! imageFile && ! documentFile ) || isSendingMessage
247+ }
101248 >
102- { isSendingMessage ? < Loader size = { 22 } /> : < Send size = { 22 } /> }
249+ { isSendingMessage ? (
250+ < Loader size = { 22 } className = "animate-spin" />
251+ ) : (
252+ < Send size = { 22 } />
253+ ) }
103254 </ button >
104255 </ form >
105256 </ div >
0 commit comments