@@ -22,6 +22,8 @@ import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportCh
2222import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons' ;
2323import { ExamplePrompts } from '~/components/chat/ExamplePrompts' ;
2424
25+ import FilePreview from './FilePreview' ;
26+
2527// @ts -ignore TODO: Introduce proper types
2628// eslint-disable-next-line @typescript-eslint/no-unused-vars
2729const ModelSelector = ( { model, setModel, provider, setProvider, modelList, providerList, apiKeys } ) => {
@@ -85,8 +87,11 @@ interface BaseChatProps {
8587 enhancePrompt ?: ( ) => void ;
8688 importChat ?: ( description : string , messages : Message [ ] ) => Promise < void > ;
8789 exportChat ?: ( ) => void ;
90+ uploadedFiles ?: File [ ] ;
91+ setUploadedFiles ?: ( files : File [ ] ) => void ;
92+ imageDataList ?: string [ ] ;
93+ setImageDataList ?: ( dataList : string [ ] ) => void ;
8894}
89-
9095export const BaseChat = React . forwardRef < HTMLDivElement , BaseChatProps > (
9196 (
9297 {
@@ -96,20 +101,24 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
96101 showChat = true ,
97102 chatStarted = false ,
98103 isStreaming = false ,
99- enhancingPrompt = false ,
100- promptEnhanced = false ,
101- messages,
102- input = '' ,
103104 model,
104105 setModel,
105106 provider,
106107 setProvider,
107- sendMessage,
108+ input = '' ,
109+ enhancingPrompt,
108110 handleInputChange,
111+ promptEnhanced,
109112 enhancePrompt,
113+ sendMessage,
110114 handleStop,
111115 importChat,
112116 exportChat,
117+ uploadedFiles = [ ] ,
118+ setUploadedFiles,
119+ imageDataList = [ ] ,
120+ setImageDataList,
121+ messages,
113122 } ,
114123 ref ,
115124 ) => {
@@ -159,6 +168,58 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
159168 }
160169 } ;
161170
171+ const handleFileUpload = ( ) => {
172+ const input = document . createElement ( 'input' ) ;
173+ input . type = 'file' ;
174+ input . accept = 'image/*' ;
175+
176+ input . onchange = async ( e ) => {
177+ const file = ( e . target as HTMLInputElement ) . files ?. [ 0 ] ;
178+
179+ if ( file ) {
180+ const reader = new FileReader ( ) ;
181+
182+ reader . onload = ( e ) => {
183+ const base64Image = e . target ?. result as string ;
184+ setUploadedFiles ?.( [ ...uploadedFiles , file ] ) ;
185+ setImageDataList ?.( [ ...imageDataList , base64Image ] ) ;
186+ } ;
187+ reader . readAsDataURL ( file ) ;
188+ }
189+ } ;
190+
191+ input . click ( ) ;
192+ } ;
193+
194+ const handlePaste = async ( e : React . ClipboardEvent ) => {
195+ const items = e . clipboardData ?. items ;
196+
197+ if ( ! items ) {
198+ return ;
199+ }
200+
201+ for ( const item of items ) {
202+ if ( item . type . startsWith ( 'image/' ) ) {
203+ e . preventDefault ( ) ;
204+
205+ const file = item . getAsFile ( ) ;
206+
207+ if ( file ) {
208+ const reader = new FileReader ( ) ;
209+
210+ reader . onload = ( e ) => {
211+ const base64Image = e . target ?. result as string ;
212+ setUploadedFiles ?.( [ ...uploadedFiles , file ] ) ;
213+ setImageDataList ?.( [ ...imageDataList , base64Image ] ) ;
214+ } ;
215+ reader . readAsDataURL ( file ) ;
216+ }
217+
218+ break ;
219+ }
220+ }
221+ } ;
222+
162223 const baseChat = (
163224 < div
164225 ref = { ref }
@@ -276,17 +337,56 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
276337 ) }
277338 </ div >
278339 </ div >
279-
340+ < FilePreview
341+ files = { uploadedFiles }
342+ imageDataList = { imageDataList }
343+ onRemove = { ( index ) => {
344+ setUploadedFiles ?.( uploadedFiles . filter ( ( _ , i ) => i !== index ) ) ;
345+ setImageDataList ?.( imageDataList . filter ( ( _ , i ) => i !== index ) ) ;
346+ } }
347+ />
280348 < div
281349 className = { classNames (
282350 'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg' ,
283351 ) }
284352 >
285353 < textarea
286354 ref = { textareaRef }
287- className = {
288- 'w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm'
289- }
355+ className = { classNames (
356+ 'w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm' ,
357+ 'transition-all duration-200' ,
358+ 'hover:border-bolt-elements-focus' ,
359+ ) }
360+ onDragEnter = { ( e ) => {
361+ e . preventDefault ( ) ;
362+ e . currentTarget . style . border = '2px solid #1488fc' ;
363+ } }
364+ onDragOver = { ( e ) => {
365+ e . preventDefault ( ) ;
366+ e . currentTarget . style . border = '2px solid #1488fc' ;
367+ } }
368+ onDragLeave = { ( e ) => {
369+ e . preventDefault ( ) ;
370+ e . currentTarget . style . border = '1px solid var(--bolt-elements-borderColor)' ;
371+ } }
372+ onDrop = { ( e ) => {
373+ e . preventDefault ( ) ;
374+ e . currentTarget . style . border = '1px solid var(--bolt-elements-borderColor)' ;
375+
376+ const files = Array . from ( e . dataTransfer . files ) ;
377+ files . forEach ( ( file ) => {
378+ if ( file . type . startsWith ( 'image/' ) ) {
379+ const reader = new FileReader ( ) ;
380+
381+ reader . onload = ( e ) => {
382+ const base64Image = e . target ?. result as string ;
383+ setUploadedFiles ?.( [ ...uploadedFiles , file ] ) ;
384+ setImageDataList ?.( [ ...imageDataList , base64Image ] ) ;
385+ } ;
386+ reader . readAsDataURL ( file ) ;
387+ }
388+ } ) ;
389+ } }
290390 onKeyDown = { ( event ) => {
291391 if ( event . key === 'Enter' ) {
292392 if ( event . shiftKey ) {
@@ -302,6 +402,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
302402 onChange = { ( event ) => {
303403 handleInputChange ?.( event ) ;
304404 } }
405+ onPaste = { handlePaste }
305406 style = { {
306407 minHeight : TEXTAREA_MIN_HEIGHT ,
307408 maxHeight : TEXTAREA_MAX_HEIGHT ,
@@ -312,29 +413,36 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
312413 < ClientOnly >
313414 { ( ) => (
314415 < SendButton
315- show = { input . length > 0 || isStreaming }
416+ show = { input . length > 0 || isStreaming || uploadedFiles . length > 0 }
316417 isStreaming = { isStreaming }
317418 onClick = { ( event ) => {
318419 if ( isStreaming ) {
319420 handleStop ?.( ) ;
320421 return ;
321422 }
322423
323- sendMessage ?.( event ) ;
424+ if ( input . length > 0 || uploadedFiles . length > 0 ) {
425+ sendMessage ?.( event ) ;
426+ }
324427 } }
325428 />
326429 ) }
327430 </ ClientOnly >
328431 < div className = "flex justify-between items-center text-sm p-4 pt-2" >
329432 < div className = "flex gap-1 items-center" >
433+ < IconButton title = "Upload file" className = "transition-all" onClick = { ( ) => handleFileUpload ( ) } >
434+ < div className = "i-ph:paperclip text-xl" > </ div >
435+ </ IconButton >
330436 < IconButton
331437 title = "Enhance prompt"
332438 disabled = { input . length === 0 || enhancingPrompt }
333- className = { classNames ( 'transition-all' , {
334- 'opacity-100!' : enhancingPrompt ,
335- 'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!' :
336- promptEnhanced ,
337- } ) }
439+ className = { classNames (
440+ 'transition-all' ,
441+ enhancingPrompt ? 'opacity-100' : '' ,
442+ promptEnhanced ? 'text-bolt-elements-item-contentAccent' : '' ,
443+ promptEnhanced ? 'pr-1.5' : '' ,
444+ promptEnhanced ? 'enabled:hover:bg-bolt-elements-item-backgroundAccent' : '' ,
445+ ) }
338446 onClick = { ( ) => enhancePrompt ?.( ) }
339447 >
340448 { enhancingPrompt ? (
0 commit comments