@@ -28,6 +28,7 @@ type PendingMessage = {
2828 url : string ;
2929 status : 'pending' | 'uploaded' | 'error' ;
3030 error ?: string ;
31+ base64 ?: string ;
3132 } > ;
3233 model : string | null ;
3334 messages : { role : string ; content : string } [ ] ;
@@ -45,7 +46,7 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
4546 } )
4647 const createMessage = useCreateMessage ( )
4748 const [ error , setError ] = useState < string | null > ( null )
48- const [ uploadError , setUploadError ] = useState < string | null > ( null )
49+ const [ uploadError ] = useState < string | null > ( null )
4950 const fileInputRef = useRef < HTMLInputElement > ( null )
5051 const chatInputRef = useRef < HTMLInputElement > ( null )
5152 const [ inputValue , setInputValue ] = useState ( '' )
@@ -57,8 +58,8 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
5758 url : string ;
5859 status : 'pending' | 'uploaded' | 'error' ;
5960 error ?: string ;
61+ base64 ?: string ;
6062 } > > ( [ ] )
61- const [ , forceRerender ] = useState ( 0 )
6263 const { data : messages } = useMessages ( activeConversationId ) ;
6364 const queryClient = useQueryClient ( ) ;
6465 const selectedModelRaw = useConversationModelStore ( s => activeConversationId ? s . getModel ( activeConversationId ) : undefined )
@@ -68,76 +69,6 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
6869 const [ showLimitModal , setShowLimitModal ] = useState ( false )
6970 const decrementPremiumCount = usePremiumQueryCountStore ( s => s . decrement )
7071
71- const handleFileChange = async ( e : React . ChangeEvent < HTMLInputElement > ) => {
72- console . log ( 'handleFileChange' , e . target . files )
73- setUploadError ( null )
74- const files = e . target . files
75- if ( ! files || files . length === 0 ) return
76- const allowed = [
77- 'image/png' , 'image/jpeg' , 'image/webp' , 'application/pdf' ,
78- 'text/plain' , 'application/zip' , 'application/json' ,
79- ]
80- for ( const file of Array . from ( files ) ) {
81- if ( ! allowed . includes ( file . type ) ) {
82- setUploadError ( 'Unsupported file type' )
83- continue
84- }
85- if ( file . size > 10 * 1024 * 1024 ) {
86- setUploadError ( 'File too large (max 10MB)' )
87- continue
88- }
89- // Optimistically add preview
90- let localUrl = ''
91- if ( typeof window !== 'undefined' && typeof window . URL !== 'undefined' && typeof window . URL . createObjectURL === 'function' ) {
92- localUrl = window . URL . createObjectURL ( file )
93- } else {
94- // fallback for test/SSR
95- localUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...'
96- }
97- const ext = file . name . split ( '.' ) . pop ( )
98- const filePath = `uploads/${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } .${ ext } `
99- setPendingAttachments ( prev => {
100- const next : typeof prev = [
101- ...prev ,
102- {
103- name : file . name ,
104- type : file . type ,
105- size : file . size ,
106- filePath,
107- url : localUrl ,
108- status : 'pending' as const ,
109- } ,
110- ]
111- return next
112- } )
113- forceRerender ( n => n + 1 )
114- // Start upload in background
115- void ( async ( ) => {
116- try {
117- const { error : uploadError } = await supabase . storage . from ( 'attachments' ) . upload ( filePath , file , {
118- cacheControl : '3600' ,
119- upsert : false ,
120- } )
121- if ( uploadError ) {
122- setPendingAttachments ( prev => prev . map ( a =>
123- a . filePath === filePath ? { ...a , status : 'error' , error : uploadError . message } : a
124- ) )
125- return
126- }
127- const { data } = supabase . storage . from ( 'attachments' ) . getPublicUrl ( filePath )
128- setPendingAttachments ( prev => prev . map ( a =>
129- a . filePath === filePath ? { ...a , url : data . publicUrl , status : 'uploaded' } : a
130- ) )
131- } catch {
132- setPendingAttachments ( prev => prev . map ( a =>
133- a . filePath === filePath ? { ...a , status : 'error' , error : 'Upload failed' } : a
134- ) )
135- }
136- } ) ( )
137- }
138- if ( fileInputRef . current ) fileInputRef . current . value = ''
139- }
140-
14172 const handleRemoveAttachment = ( filePath : string ) => {
14273 setPendingAttachments ( prev => prev . filter ( a => a . filePath !== filePath ) )
14374 }
@@ -306,13 +237,14 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
306237 console . log ( 'Assistant message created:' , assistantMsg ) ;
307238 let streamedContent = '' ;
308239 let res ;
309- const imageUrls = pendingAttachments
310- . filter ( ( att : { type : string } ) => att . type . startsWith ( 'image/' ) )
311- . map ( ( att : { url : string } ) => att . url ) ;
240+ // Only use base64 for Together AI attachments
241+ const togetherAttachments = pendingAttachments
242+ . filter ( att => att . type . startsWith ( 'image/' ) && att . base64 )
243+ . map ( att => att . base64 )
312244 try {
313245 res = await fetch ( '/api/chat' , {
314246 method : 'POST' ,
315- body : JSON . stringify ( { messages : currentMessages , conversation_id : conversationId , model : selectedModel , attachments : imageUrls } ) ,
247+ body : JSON . stringify ( { messages : currentMessages , conversation_id : conversationId , model : selectedModel , attachments : togetherAttachments } ) ,
316248 } ) ;
317249 console . log ( 'API response:' , res ) ;
318250 } catch ( err ) {
@@ -360,23 +292,13 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
360292 < div className = "text-sm text-red-500 mb-2" > { error } </ div >
361293 ) }
362294 { uploadError && < div className = "text-xs text-red-500 mb-2" > { uploadError } </ div > }
363- { /* Hidden file input for upload icon */ }
364- < input
365- ref = { fileInputRef }
366- type = "file"
367- className = "hidden"
368- accept = "image/png,image/jpeg,image/webp,application/pdf,text/plain,application/zip,application/json"
369- onChange = { handleFileChange }
370- disabled = { createConversation . isPending || createMessage . isPending }
371- multiple
372- />
373295 { /* File preview row inside the input box, at the top */ }
374296 < div className = "flex items-center gap-4 mb-2 overflow-x-auto scrollbar-thin scrollbar-thumb-[#353740] scrollbar-track-transparent" >
375297 { pendingAttachments . map ( att => {
376298 console . log ( 'preview row map' , att )
377299 return (
378300 < div
379- key = { att . filePath }
301+ key = { att . filePath || att . name || att . base64 || Math . random ( ) }
380302 className = { `relative flex-shrink-0 w-16 h-16 rounded-2xl overflow-hidden bg-[#23272f] border border-[#353740] group` }
381303 >
382304 { /* Status indicator (spinner or error) */ }
@@ -402,7 +324,7 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
402324 ) : (
403325 < span className = "flex items-center justify-center w-full h-full text-3xl text-[#b4bcd0]" > 📄</ span >
404326 ) }
405- { /* Filename overlay at bottom */ }
327+ { /* Filename overlay at bottom (always show, even on error) */ }
406328 < div data-testid = "attachment-filename" className = "absolute bottom-0 left-0 w-full px-2 py-1 bg-gradient-to-t from-black/80 to-black/0 text-[11px] text-[#ececf1] truncate pointer-events-none font-medium" >
407329 { att . name }
408330 </ div >
@@ -534,6 +456,67 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }:
534456 </ Tooltip . Portal >
535457 </ Tooltip . Root >
536458 </ div >
459+ { /* Hidden file input for upload icon */ }
460+ < input
461+ ref = { fileInputRef }
462+ type = "file"
463+ className = "hidden"
464+ accept = "image/png,image/jpeg,image/webp"
465+ onChange = { async e => {
466+ const files = e . target . files ;
467+ if ( ! files || files . length === 0 ) return ;
468+ const file = files [ 0 ] ;
469+ if ( ! [ 'image/png' , 'image/jpeg' , 'image/webp' ] . includes ( file . type ) ) {
470+ setPendingAttachments ( prev => [
471+ ...prev ,
472+ {
473+ name : file . name ,
474+ type : file . type ,
475+ size : file . size ,
476+ filePath : '' ,
477+ url : '' ,
478+ status : 'error' ,
479+ error : 'Unsupported file type' ,
480+ }
481+ ] ) ;
482+ return ;
483+ }
484+ if ( file . size > 10 * 1024 * 1024 ) {
485+ setPendingAttachments ( prev => [
486+ ...prev ,
487+ {
488+ name : file . name ,
489+ type : file . type ,
490+ size : file . size ,
491+ filePath : '' ,
492+ url : '' ,
493+ status : 'error' ,
494+ error : 'File too large' ,
495+ }
496+ ] ) ;
497+ return ;
498+ }
499+ const base64 = await new Promise < string > ( ( resolve , reject ) => {
500+ const reader = new FileReader ( ) ;
501+ reader . onload = ( ) => resolve ( reader . result as string ) ;
502+ reader . onerror = reject ;
503+ reader . readAsDataURL ( file ) ;
504+ } ) ;
505+ setPendingAttachments ( prev => [
506+ ...prev ,
507+ {
508+ name : file . name ,
509+ type : file . type ,
510+ size : file . size ,
511+ filePath : '' ,
512+ url : base64 ,
513+ status : 'uploaded' ,
514+ base64,
515+ }
516+ ] ) ;
517+ } }
518+ disabled = { createConversation . isPending || createMessage . isPending }
519+ />
537520 < style jsx global > { `
538521 input::placeholder {
539522 color: #b4bcd0 !important;
0 commit comments