@@ -117,8 +117,7 @@ export function Thread() {
117
117
parseAsBoolean . withDefault ( false ) ,
118
118
) ;
119
119
const [ input , setInput ] = useState ( "" ) ;
120
- const [ imageUrlList , setImageUrlList ] = useState < Base64ContentBlock [ ] > ( [ ] ) ;
121
- const [ pdfUrlList , setPdfUrlList ] = useState < Base64ContentBlock [ ] > ( [ ] ) ;
120
+ const [ contentBlocks , setContentBlocks ] = useState < Base64ContentBlock [ ] > ( [ ] ) ;
122
121
const [ firstTokenReceived , setFirstTokenReceived ] = useState ( false ) ;
123
122
const isLargeScreen = useMediaQuery ( "(min-width: 1024px)" ) ;
124
123
@@ -174,12 +173,7 @@ export function Thread() {
174
173
175
174
const handleSubmit = ( e : FormEvent ) => {
176
175
e . preventDefault ( ) ;
177
- if (
178
- ( input . trim ( ) . length === 0 &&
179
- imageUrlList . length === 0 &&
180
- pdfUrlList . length === 0 ) ||
181
- isLoading
182
- )
176
+ if ( ( input . trim ( ) . length === 0 && contentBlocks . length === 0 ) || isLoading )
183
177
return ;
184
178
setFirstTokenReceived ( false ) ;
185
179
@@ -188,12 +182,10 @@ export function Thread() {
188
182
type : "human" ,
189
183
content : [
190
184
...( input . trim ( ) . length > 0 ? [ { type : "text" , text : input } ] : [ ] ) ,
191
- ...pdfUrlList ,
192
- ...imageUrlList ,
185
+ ...contentBlocks ,
193
186
] as Message [ "content" ] ,
194
187
} ;
195
188
196
-
197
189
const toolMessages = ensureToolCallsHaveResponses ( stream . messages ) ;
198
190
stream . submit (
199
191
{ messages : [ ...toolMessages , newHumanMessage ] } ,
@@ -211,19 +203,33 @@ export function Thread() {
211
203
) ;
212
204
213
205
setInput ( "" ) ;
214
- setImageUrlList ( [ ] ) ;
215
- setPdfUrlList ( [ ] ) ;
206
+ setContentBlocks ( [ ] ) ;
216
207
} ;
217
208
218
- const SUPPORTED_IMAGE_TYPES = [ "image/jpeg" , "image/png" , "image/gif" , "image/webp" ] ;
209
+ const SUPPORTED_IMAGE_TYPES = [
210
+ "image/jpeg" ,
211
+ "image/png" ,
212
+ "image/gif" ,
213
+ "image/webp" ,
214
+ ] ;
219
215
const SUPPORTED_FILE_TYPES = [ ...SUPPORTED_IMAGE_TYPES , "application/pdf" ] ;
220
216
221
- const isDuplicate = ( file : File , images : Base64ContentBlock [ ] , pdfs : Base64ContentBlock [ ] ) => {
217
+ const isDuplicate = ( file : File , blocks : Base64ContentBlock [ ] ) => {
222
218
if ( SUPPORTED_IMAGE_TYPES . includes ( file . type ) ) {
223
- return images . some ( img => img . metadata ?. name === file . name && img . mime_type === file . type ) ;
219
+ return blocks . some (
220
+ ( b ) =>
221
+ b . type === "image" &&
222
+ b . metadata ?. name === file . name &&
223
+ b . mime_type === file . type ,
224
+ ) ;
224
225
}
225
226
if ( file . type === "application/pdf" ) {
226
- return pdfs . some ( pdf => pdf . metadata ?. filename === file . name ) ;
227
+ return blocks . some (
228
+ ( b ) =>
229
+ b . type === "file" &&
230
+ b . mime_type === "application/pdf" &&
231
+ b . metadata ?. filename === file . name ,
232
+ ) ;
227
233
}
228
234
return false ;
229
235
} ;
@@ -232,10 +238,18 @@ export function Thread() {
232
238
const files = e . target . files ;
233
239
if ( ! files ) return ;
234
240
const fileArray = Array . from ( files ) ;
235
- const validFiles = fileArray . filter ( ( file ) => SUPPORTED_FILE_TYPES . includes ( file . type ) ) ;
236
- const invalidFiles = fileArray . filter ( ( file ) => ! SUPPORTED_FILE_TYPES . includes ( file . type ) ) ;
237
- const duplicateFiles = validFiles . filter ( ( file ) => isDuplicate ( file , imageUrlList , pdfUrlList ) ) ;
238
- const uniqueFiles = validFiles . filter ( ( file ) => ! isDuplicate ( file , imageUrlList , pdfUrlList ) ) ;
241
+ const validFiles = fileArray . filter ( ( file ) =>
242
+ SUPPORTED_FILE_TYPES . includes ( file . type ) ,
243
+ ) ;
244
+ const invalidFiles = fileArray . filter (
245
+ ( file ) => ! SUPPORTED_FILE_TYPES . includes ( file . type ) ,
246
+ ) ;
247
+ const duplicateFiles = validFiles . filter ( ( file ) =>
248
+ isDuplicate ( file , contentBlocks ) ,
249
+ ) ;
250
+ const uniqueFiles = validFiles . filter (
251
+ ( file ) => ! isDuplicate ( file , contentBlocks ) ,
252
+ ) ;
239
253
240
254
if ( invalidFiles . length > 0 ) {
241
255
toast . error (
@@ -244,22 +258,24 @@ export function Thread() {
244
258
}
245
259
if ( duplicateFiles . length > 0 ) {
246
260
toast . error (
247
- `Duplicate file(s) detected: ${ duplicateFiles . map ( f => f . name ) . join ( ", " ) } . Each file can only be uploaded once per message.` ,
261
+ `Duplicate file(s) detected: ${ duplicateFiles . map ( ( f ) => f . name ) . join ( ", " ) } . Each file can only be uploaded once per message.` ,
248
262
) ;
249
263
}
250
264
251
- const imageFiles = uniqueFiles . filter ( ( file ) => SUPPORTED_IMAGE_TYPES . includes ( file . type ) ) ;
252
- const pdfFiles = uniqueFiles . filter ( ( file ) => file . type === "application/pdf" ) ;
253
-
254
- if ( imageFiles . length ) {
255
- const imageBlocks = await Promise . all ( imageFiles . map ( fileToImageBlock ) ) ;
256
- setImageUrlList ( ( prev ) => [ ...prev , ...imageBlocks ] ) ;
257
- }
265
+ const imageFiles = uniqueFiles . filter ( ( file ) =>
266
+ SUPPORTED_IMAGE_TYPES . includes ( file . type ) ,
267
+ ) ;
268
+ const pdfFiles = uniqueFiles . filter (
269
+ ( file ) => file . type === "application/pdf" ,
270
+ ) ;
258
271
259
- if ( pdfFiles . length ) {
260
- const pdfBlocks = await Promise . all ( pdfFiles . map ( fileToPDFBlock ) ) ;
261
- setPdfUrlList ( ( prev ) => [ ...prev , ...pdfBlocks ] ) ;
262
- }
272
+ const imageBlocks = imageFiles . length
273
+ ? await Promise . all ( imageFiles . map ( fileToImageBlock ) )
274
+ : [ ] ;
275
+ const pdfBlocks = pdfFiles . length
276
+ ? await Promise . all ( pdfFiles . map ( fileToPDFBlock ) )
277
+ : [ ] ;
278
+ setContentBlocks ( ( prev ) => [ ...prev , ...imageBlocks , ...pdfBlocks ] ) ;
263
279
e . target . value = "" ;
264
280
} ;
265
281
@@ -295,10 +311,18 @@ export function Thread() {
295
311
if ( ! e . dataTransfer ) return ;
296
312
297
313
const files = Array . from ( e . dataTransfer . files ) ;
298
- const validFiles = files . filter ( ( file ) => SUPPORTED_FILE_TYPES . includes ( file . type ) ) ;
299
- const invalidFiles = files . filter ( ( file ) => ! SUPPORTED_FILE_TYPES . includes ( file . type ) ) ;
300
- const duplicateFiles = validFiles . filter ( ( file ) => isDuplicate ( file , imageUrlList , pdfUrlList ) ) ;
301
- const uniqueFiles = validFiles . filter ( ( file ) => ! isDuplicate ( file , imageUrlList , pdfUrlList ) ) ;
314
+ const validFiles = files . filter ( ( file ) =>
315
+ SUPPORTED_FILE_TYPES . includes ( file . type ) ,
316
+ ) ;
317
+ const invalidFiles = files . filter (
318
+ ( file ) => ! SUPPORTED_FILE_TYPES . includes ( file . type ) ,
319
+ ) ;
320
+ const duplicateFiles = validFiles . filter ( ( file ) =>
321
+ isDuplicate ( file , contentBlocks ) ,
322
+ ) ;
323
+ const uniqueFiles = validFiles . filter (
324
+ ( file ) => ! isDuplicate ( file , contentBlocks ) ,
325
+ ) ;
302
326
303
327
if ( invalidFiles . length > 0 ) {
304
328
toast . error (
@@ -307,26 +331,24 @@ export function Thread() {
307
331
}
308
332
if ( duplicateFiles . length > 0 ) {
309
333
toast . error (
310
- `Duplicate file(s) detected: ${ duplicateFiles . map ( f => f . name ) . join ( ", " ) } . Each file can only be uploaded once per message.` ,
334
+ `Duplicate file(s) detected: ${ duplicateFiles . map ( ( f ) => f . name ) . join ( ", " ) } . Each file can only be uploaded once per message.` ,
311
335
) ;
312
336
}
313
337
314
- const imageFiles = uniqueFiles . filter ( ( file ) => SUPPORTED_IMAGE_TYPES . includes ( file . type ) ) ;
315
- const pdfFiles = uniqueFiles . filter ( ( file ) => file . type === "application/pdf" ) ;
316
-
317
- if ( imageFiles . length ) {
318
- const imageBlocks : Base64ContentBlock [ ] = await Promise . all (
319
- imageFiles . map ( fileToImageBlock ) ,
320
- ) ;
321
- setImageUrlList ( ( prev ) => [ ...prev , ...imageBlocks ] ) ;
322
- }
338
+ const imageFiles = uniqueFiles . filter ( ( file ) =>
339
+ SUPPORTED_IMAGE_TYPES . includes ( file . type ) ,
340
+ ) ;
341
+ const pdfFiles = uniqueFiles . filter (
342
+ ( file ) => file . type === "application/pdf" ,
343
+ ) ;
323
344
324
- if ( pdfFiles . length ) {
325
- const pdfBlocks : Base64ContentBlock [ ] = await Promise . all (
326
- pdfFiles . map ( fileToPDFBlock ) ,
327
- ) ;
328
- setPdfUrlList ( ( prev ) => [ ...prev , ...pdfBlocks ] ) ;
329
- }
345
+ const imageBlocks : Base64ContentBlock [ ] = imageFiles . length
346
+ ? await Promise . all ( imageFiles . map ( fileToImageBlock ) )
347
+ : [ ] ;
348
+ const pdfBlocks : Base64ContentBlock [ ] = pdfFiles . length
349
+ ? await Promise . all ( pdfFiles . map ( fileToPDFBlock ) )
350
+ : [ ] ;
351
+ setContentBlocks ( ( prev ) => [ ...prev , ...imageBlocks , ...pdfBlocks ] ) ;
330
352
} ;
331
353
332
354
const handleDragEnter = ( e : DragEvent ) => {
@@ -544,30 +566,50 @@ export function Thread() {
544
566
onSubmit = { handleSubmit }
545
567
className = "mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
546
568
>
547
- { imageUrlList . length > 0 && (
569
+ { contentBlocks . filter ( ( b ) => b . type === "image" ) . length >
570
+ 0 && (
548
571
< div className = "flex flex-wrap gap-2 p-3.5 pb-0" >
549
- { imageUrlList . map ( ( imageBlock , idx ) => (
550
- < MultimodalPreview
551
- key = { idx }
552
- block = { imageBlock }
553
- removable
554
- onRemove = { ( ) => setImageUrlList ( imageUrlList . filter ( ( _ , i ) => i !== idx ) ) }
555
- size = "md"
556
- />
557
- ) ) }
572
+ { contentBlocks
573
+ . filter ( ( b ) => b . type === "image" )
574
+ . map ( ( imageBlock , idx ) => (
575
+ < MultimodalPreview
576
+ key = { idx }
577
+ block = { imageBlock }
578
+ removable
579
+ onRemove = { ( ) =>
580
+ setContentBlocks (
581
+ contentBlocks . filter ( ( _ , i ) => i !== idx ) ,
582
+ )
583
+ }
584
+ size = "md"
585
+ />
586
+ ) ) }
558
587
</ div >
559
588
) }
560
- { pdfUrlList . length > 0 && (
589
+ { contentBlocks . filter (
590
+ ( b ) =>
591
+ b . type === "file" && b . mime_type === "application/pdf" ,
592
+ ) . length > 0 && (
561
593
< div className = "flex flex-wrap gap-2 p-3.5 pb-0" >
562
- { pdfUrlList . map ( ( pdfBlock , idx ) => (
563
- < MultimodalPreview
564
- key = { idx }
565
- block = { pdfBlock }
566
- removable
567
- onRemove = { ( ) => setPdfUrlList ( pdfUrlList . filter ( ( _ , i ) => i !== idx ) ) }
568
- size = "md"
569
- />
570
- ) ) }
594
+ { contentBlocks
595
+ . filter (
596
+ ( b ) =>
597
+ b . type === "file" &&
598
+ b . mime_type === "application/pdf" ,
599
+ )
600
+ . map ( ( pdfBlock , idx ) => (
601
+ < MultimodalPreview
602
+ key = { idx }
603
+ block = { pdfBlock }
604
+ removable
605
+ onRemove = { ( ) =>
606
+ setContentBlocks (
607
+ contentBlocks . filter ( ( _ , i ) => i !== idx ) ,
608
+ )
609
+ }
610
+ size = "md"
611
+ />
612
+ ) ) }
571
613
</ div >
572
614
) }
573
615
< textarea
@@ -637,9 +679,7 @@ export function Thread() {
637
679
className = "shadow-md transition-all"
638
680
disabled = {
639
681
isLoading ||
640
- ( ! input . trim ( ) &&
641
- imageUrlList . length === 0 &&
642
- pdfUrlList . length === 0 )
682
+ ( ! input . trim ( ) && contentBlocks . length === 0 )
643
683
}
644
684
>
645
685
Send
0 commit comments