@@ -21,6 +21,8 @@ export function useFileUpload({
21
21
const [ contentBlocks , setContentBlocks ] =
22
22
useState < Base64ContentBlock [ ] > ( initialBlocks ) ;
23
23
const dropRef = useRef < HTMLDivElement > ( null ) ;
24
+ const [ dragOver , setDragOver ] = useState ( false ) ;
25
+ const dragCounter = useRef ( 0 ) ;
24
26
25
27
const isDuplicate = ( file : File , blocks : Base64ContentBlock [ ] ) => {
26
28
if ( file . type === "application/pdf" ) {
@@ -81,14 +83,27 @@ export function useFileUpload({
81
83
useEffect ( ( ) => {
82
84
if ( ! dropRef . current ) return ;
83
85
84
- const handleDragOver = ( e : DragEvent ) => {
85
- e . preventDefault ( ) ;
86
- e . stopPropagation ( ) ;
86
+ // Global drag events with counter for robust dragOver state
87
+ const handleWindowDragEnter = ( e : DragEvent ) => {
88
+ if ( e . dataTransfer ?. types ?. includes ( "Files" ) ) {
89
+ dragCounter . current += 1 ;
90
+ setDragOver ( true ) ;
91
+ }
87
92
} ;
88
-
89
- const handleDrop = async ( e : DragEvent ) => {
93
+ const handleWindowDragLeave = ( e : DragEvent ) => {
94
+ if ( e . dataTransfer ?. types ?. includes ( "Files" ) ) {
95
+ dragCounter . current -= 1 ;
96
+ if ( dragCounter . current <= 0 ) {
97
+ setDragOver ( false ) ;
98
+ dragCounter . current = 0 ;
99
+ }
100
+ }
101
+ } ;
102
+ const handleWindowDrop = async ( e : DragEvent ) => {
90
103
e . preventDefault ( ) ;
91
104
e . stopPropagation ( ) ;
105
+ dragCounter . current = 0 ;
106
+ setDragOver ( false ) ;
92
107
93
108
if ( ! e . dataTransfer ) return ;
94
109
@@ -122,28 +137,53 @@ export function useFileUpload({
122
137
: [ ] ;
123
138
setContentBlocks ( ( prev ) => [ ...prev , ...newBlocks ] ) ;
124
139
} ;
140
+ const handleWindowDragEnd = ( e : DragEvent ) => {
141
+ dragCounter . current = 0 ;
142
+ setDragOver ( false ) ;
143
+ } ;
144
+ window . addEventListener ( "dragenter" , handleWindowDragEnter ) ;
145
+ window . addEventListener ( "dragleave" , handleWindowDragLeave ) ;
146
+ window . addEventListener ( "drop" , handleWindowDrop ) ;
147
+ window . addEventListener ( "dragend" , handleWindowDragEnd ) ;
125
148
126
- const handleDragEnter = ( e : DragEvent ) => {
149
+ // Prevent default browser behavior for dragover globally
150
+ const handleWindowDragOver = ( e : DragEvent ) => {
127
151
e . preventDefault ( ) ;
128
152
e . stopPropagation ( ) ;
129
153
} ;
154
+ window . addEventListener ( "dragover" , handleWindowDragOver ) ;
130
155
156
+ // Remove element-specific drop event (handled globally)
157
+ const handleDragOver = ( e : DragEvent ) => {
158
+ e . preventDefault ( ) ;
159
+ e . stopPropagation ( ) ;
160
+ setDragOver ( true ) ;
161
+ } ;
162
+ const handleDragEnter = ( e : DragEvent ) => {
163
+ e . preventDefault ( ) ;
164
+ e . stopPropagation ( ) ;
165
+ setDragOver ( true ) ;
166
+ } ;
131
167
const handleDragLeave = ( e : DragEvent ) => {
132
168
e . preventDefault ( ) ;
133
169
e . stopPropagation ( ) ;
170
+ setDragOver ( false ) ;
134
171
} ;
135
-
136
172
const element = dropRef . current ;
137
173
element . addEventListener ( "dragover" , handleDragOver ) ;
138
- element . addEventListener ( "drop" , handleDrop ) ;
139
174
element . addEventListener ( "dragenter" , handleDragEnter ) ;
140
175
element . addEventListener ( "dragleave" , handleDragLeave ) ;
141
176
142
177
return ( ) => {
143
178
element . removeEventListener ( "dragover" , handleDragOver ) ;
144
- element . removeEventListener ( "drop" , handleDrop ) ;
145
179
element . removeEventListener ( "dragenter" , handleDragEnter ) ;
146
180
element . removeEventListener ( "dragleave" , handleDragLeave ) ;
181
+ window . removeEventListener ( "dragenter" , handleWindowDragEnter ) ;
182
+ window . removeEventListener ( "dragleave" , handleWindowDragLeave ) ;
183
+ window . removeEventListener ( "drop" , handleWindowDrop ) ;
184
+ window . removeEventListener ( "dragend" , handleWindowDragEnd ) ;
185
+ window . removeEventListener ( "dragover" , handleWindowDragOver ) ;
186
+ dragCounter . current = 0 ;
147
187
} ;
148
188
} , [ contentBlocks ] ) ;
149
189
@@ -153,12 +193,76 @@ export function useFileUpload({
153
193
154
194
const resetBlocks = ( ) => setContentBlocks ( [ ] ) ;
155
195
196
+ /**
197
+ * Handle paste event for files (images, PDFs)
198
+ * Can be used as onPaste={handlePaste} on a textarea or input
199
+ */
200
+ const handlePaste = async (
201
+ e : React . ClipboardEvent < HTMLTextAreaElement | HTMLInputElement > ,
202
+ ) => {
203
+ e . preventDefault ( ) ;
204
+ const items = e . clipboardData . items ;
205
+ if ( ! items ) return ;
206
+ const files : File [ ] = [ ] ;
207
+ for ( let i = 0 ; i < items . length ; i += 1 ) {
208
+ const item = items [ i ] ;
209
+ if ( item . kind === "file" ) {
210
+ const file = item . getAsFile ( ) ;
211
+ if ( file ) files . push ( file ) ;
212
+ }
213
+ }
214
+ if ( files . length === 0 ) return ;
215
+ const validFiles = files . filter ( ( file ) =>
216
+ SUPPORTED_FILE_TYPES . includes ( file . type ) ,
217
+ ) ;
218
+ const invalidFiles = files . filter (
219
+ ( file ) => ! SUPPORTED_FILE_TYPES . includes ( file . type ) ,
220
+ ) ;
221
+ const isDuplicate = ( file : File ) => {
222
+ if ( file . type === "application/pdf" ) {
223
+ return contentBlocks . some (
224
+ ( b ) =>
225
+ b . type === "file" &&
226
+ b . mime_type === "application/pdf" &&
227
+ b . metadata ?. filename === file . name ,
228
+ ) ;
229
+ }
230
+ if ( SUPPORTED_FILE_TYPES . includes ( file . type ) ) {
231
+ return contentBlocks . some (
232
+ ( b ) =>
233
+ b . type === "image" &&
234
+ b . metadata ?. name === file . name &&
235
+ b . mime_type === file . type ,
236
+ ) ;
237
+ }
238
+ return false ;
239
+ } ;
240
+ const duplicateFiles = validFiles . filter ( isDuplicate ) ;
241
+ const uniqueFiles = validFiles . filter ( ( file ) => ! isDuplicate ( file ) ) ;
242
+ if ( invalidFiles . length > 0 ) {
243
+ toast . error (
244
+ "You have pasted an invalid file type. Please paste a JPEG, PNG, GIF, WEBP image or a PDF." ,
245
+ ) ;
246
+ }
247
+ if ( duplicateFiles . length > 0 ) {
248
+ toast . error (
249
+ `Duplicate file(s) detected: ${ duplicateFiles . map ( ( f ) => f . name ) . join ( ", " ) } . Each file can only be uploaded once per message.` ,
250
+ ) ;
251
+ }
252
+ if ( uniqueFiles . length > 0 ) {
253
+ const newBlocks = await Promise . all ( uniqueFiles . map ( fileToContentBlock ) ) ;
254
+ setContentBlocks ( ( prev ) => [ ...prev , ...newBlocks ] ) ;
255
+ }
256
+ } ;
257
+
156
258
return {
157
259
contentBlocks,
158
260
setContentBlocks,
159
261
handleFileUpload,
160
262
dropRef,
161
263
removeBlock,
162
264
resetBlocks,
265
+ dragOver,
266
+ handlePaste,
163
267
} ;
164
268
}
0 commit comments