@@ -127,6 +127,10 @@ export function Thread() {
127
127
128
128
const lastError = useRef < string | undefined > ( undefined ) ;
129
129
130
+ const dropRef = useRef < HTMLDivElement > ( null ) ;
131
+ const [ dragging , setDragging ] = useState ( false ) ;
132
+ const formats = [ "jpg" , "png" , "gif" , "webp" , "bmp" , "jpeg" , "tiff" ] ;
133
+
130
134
useEffect ( ( ) => {
131
135
if ( ! stream . error ) {
132
136
lastError . current = undefined ;
@@ -247,6 +251,81 @@ export function Thread() {
247
251
( m ) => m . type === "ai" || m . type === "tool" ,
248
252
) ;
249
253
254
+ useEffect ( ( ) => {
255
+ if ( ! dropRef . current ) return ;
256
+
257
+ const handleDragOver = ( e : DragEvent ) => {
258
+ e . preventDefault ( ) ;
259
+ e . stopPropagation ( ) ;
260
+ } ;
261
+
262
+ const handleDrop = async ( e : DragEvent ) => {
263
+ e . preventDefault ( ) ;
264
+ e . stopPropagation ( ) ;
265
+ setDragging ( false ) ;
266
+
267
+ if ( ! e . dataTransfer ) return ;
268
+
269
+ const files = Array . from ( e . dataTransfer . files ) ;
270
+
271
+ if ( formats && files . some ( file =>
272
+ ! formats . some ( format =>
273
+ file . name . toLowerCase ( ) . endsWith ( format . toLowerCase ( ) )
274
+ )
275
+ ) ) {
276
+ toast . error ( "Invalid file type. Please upload an image. supported formats: " + formats . join ( ", " ) ) ;
277
+ return ;
278
+ }
279
+
280
+
281
+ if ( files . length ) {
282
+ const imageUrls = await Promise . all (
283
+ Array . from ( files ) . map ( ( file ) => {
284
+ return new Promise < MessageContentImageUrl > ( ( resolve ) => {
285
+ const reader = new FileReader ( ) ;
286
+ reader . onloadend = ( ) => {
287
+ resolve ( {
288
+ type : "image_url" ,
289
+ image_url : {
290
+ url : reader . result as string ,
291
+ } ,
292
+ } ) ;
293
+ } ;
294
+ reader . readAsDataURL ( file ) ;
295
+ } ) ;
296
+ } ) ,
297
+ ) ;
298
+ setImageUrlList ( [ ...imageUrlList , ...imageUrls ] ) ;
299
+ }
300
+ } ;
301
+
302
+ const handleDragEnter = ( e : DragEvent ) => {
303
+ e . preventDefault ( ) ;
304
+ e . stopPropagation ( ) ;
305
+ setDragging ( true ) ;
306
+ } ;
307
+
308
+ const handleDragLeave = ( e : DragEvent ) => {
309
+ e . preventDefault ( ) ;
310
+ e . stopPropagation ( ) ;
311
+ setDragging ( false ) ;
312
+ } ;
313
+
314
+ const element = dropRef . current ;
315
+ element . addEventListener ( "dragover" , handleDragOver ) ;
316
+ element . addEventListener ( "drop" , handleDrop ) ;
317
+ element . addEventListener ( "dragenter" , handleDragEnter ) ;
318
+ element . addEventListener ( "dragleave" , handleDragLeave ) ;
319
+
320
+ return ( ) => {
321
+ element . removeEventListener ( "dragover" , handleDragOver ) ;
322
+ element . removeEventListener ( "drop" , handleDrop ) ;
323
+ element . removeEventListener ( "dragenter" , handleDragEnter ) ;
324
+ element . removeEventListener ( "dragleave" , handleDragLeave ) ;
325
+ } ;
326
+ } ) ;
327
+
328
+
250
329
return (
251
330
< div className = "flex h-screen w-full overflow-hidden" >
252
331
< div className = "relative hidden lg:flex" >
@@ -430,7 +509,10 @@ export function Thread() {
430
509
431
510
< ScrollToBottom className = "animate-in fade-in-0 zoom-in-95 absolute bottom-full left-1/2 mb-4 -translate-x-1/2" />
432
511
433
- < div className = "bg-muted relative z-10 mx-auto mb-8 w-full max-w-3xl rounded-2xl border shadow-xs" >
512
+ < div
513
+ ref = { dropRef }
514
+ className = "bg-muted relative z-10 mx-auto mb-8 w-full max-w-3xl rounded-2xl border shadow-xs"
515
+ >
434
516
< form
435
517
onSubmit = { handleSubmit }
436
518
className = "mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
0 commit comments