@@ -10,12 +10,15 @@ import { ChatService } from '../../services/chat-service';
10
10
import { AIServiceCapability } from '../../types/capabilities' ;
11
11
import ProviderIcon from '../ui/ProviderIcon' ;
12
12
import { useTranslation } from '../../hooks/useTranslation' ;
13
+ import FileUploadButton from './FileUploadButton' ;
14
+ import FileAttachmentDisplay from './FileAttachmentDisplay' ;
13
15
14
16
interface ChatMessageAreaProps {
15
17
activeConversation : Conversation | null ;
16
18
isLoading : boolean ;
17
19
error : string | null ;
18
20
onSendMessage : ( content : string ) => void ;
21
+ onSendMessageWithFiles ?: ( content : string , files : File [ ] ) => void ;
19
22
onStopStreaming ?: ( ) => void ;
20
23
onRegenerateResponse ?: ( messageId : string ) => void ;
21
24
onEditMessage ?: ( messageId : string , newContent : string ) => void ;
@@ -29,6 +32,7 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
29
32
isLoading,
30
33
error,
31
34
onSendMessage,
35
+ onSendMessageWithFiles,
32
36
onStopStreaming,
33
37
onRegenerateResponse,
34
38
onEditMessage,
@@ -48,6 +52,7 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
48
52
const [ ableToWebSearch , setAbleToWebSearch ] = useState ( false ) ;
49
53
const [ webSearchActive , setWebSearchActive ] = useState ( false ) ;
50
54
const [ isWebSearchPreviewEnabled , setIsWebSearchPreviewEnabled ] = useState ( false ) ;
55
+ const [ selectedFiles , setSelectedFiles ] = useState < File [ ] > ( [ ] ) ;
51
56
52
57
// Scroll to bottom when messages change
53
58
useEffect ( ( ) => {
@@ -93,28 +98,31 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
93
98
}
94
99
} , [ isCurrentlyStreaming ] ) ;
95
100
96
- const handleSubmit = ( e : FormEvent ) => {
97
- e . preventDefault ( ) ;
98
-
99
- if ( ! inputValue . trim ( ) || isLoading || isCurrentlyStreaming ) return ;
100
-
101
- onSendMessage ( inputValue ) ;
102
-
103
- setInput ( '' ) ;
104
-
105
- const textarea = inputRef . current ;
106
- if ( ! textarea ) return ;
107
- // Calculate new height based on scrollHeight, with min and max constraints
108
- const minHeight = 36 ; // Approx height for 1 row
101
+ // Handle file selection
102
+ const handleFilesSelected = ( files : File [ ] ) => {
103
+ setSelectedFiles ( [ ...selectedFiles , ...files ] ) ;
104
+ } ;
109
105
110
- textarea . style . height = `${ minHeight } px` ;
106
+ // Remove a selected file
107
+ const handleRemoveFile = ( index : number ) => {
108
+ const newFiles = [ ...selectedFiles ] ;
109
+ newFiles . splice ( index , 1 ) ;
110
+ setSelectedFiles ( newFiles ) ;
111
111
} ;
112
112
113
- const handleStopStreaming = ( ) => {
114
- if ( onStopStreaming ) {
115
- onStopStreaming ( ) ;
116
- isCurrentlyStreaming = false ;
113
+ // Handle form submission with files
114
+ const handleFormSubmit = ( e : FormEvent ) => {
115
+ e . preventDefault ( ) ;
116
+ if ( isLoading || isCurrentlyStreaming || ! inputValue . trim ( ) ) return ;
117
+
118
+ if ( selectedFiles . length > 0 && onSendMessageWithFiles ) {
119
+ onSendMessageWithFiles ( inputValue , selectedFiles ) ;
120
+ setSelectedFiles ( [ ] ) ;
121
+ } else {
122
+ onSendMessage ( inputValue ) ;
117
123
}
124
+
125
+ setInput ( '' ) ;
118
126
} ;
119
127
120
128
// Handle regenerate response
@@ -161,6 +169,12 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
161
169
} ) ;
162
170
} ;
163
171
172
+ const handleStopStreaming = ( ) => {
173
+ if ( onStopStreaming ) {
174
+ onStopStreaming ( ) ;
175
+ }
176
+ } ;
177
+
164
178
// Placeholder error handler for other actions
165
179
// const handleActionError = (action: string) => {
166
180
// console.error(`Function not implemented yet: ${action}`);
@@ -275,6 +289,35 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
275
289
// Check if there's a streaming message
276
290
const hasStreamingMessage = Array . from ( activeConversation . messages . values ( ) ) . some ( m => m . messageId . startsWith ( 'streaming-' ) ) ;
277
291
292
+ const webSearchElement = isWebSearchPreviewEnabled ? (
293
+ ableToWebSearch ? (
294
+ < button
295
+ type = "button"
296
+ onClick = { handleToggleWebSearch }
297
+ className = { `flex items-center justify-center w-fit h-8 p-2 transition-all duration-200 rounded-full outline outline-2 hover:outline
298
+ ${ webSearchActive ? 'bg-blue-50 outline-blue-300 hover:bg-blue-200 hover:outline hover:outline-blue-500' : 'bg-white outline-gray-100 hover:bg-blue-50 hover:outline hover:outline-blue-300' } ` }
299
+ aria-label = "Toggle Web Search"
300
+ title = "Toggle Web Search"
301
+ >
302
+ < Globe className = { `mr-1 ${ webSearchActive ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } size = { 20 } />
303
+ < span className = { `text-sm font-light ${ webSearchActive ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } > Web Search</ span >
304
+ </ button >
305
+ )
306
+ :
307
+ (
308
+ < button
309
+ type = "button"
310
+ className = { `flex items-center justify-center bg-gray-100 w-fit h-8 p-2 ml-2 transition-all duration-200 rounded-full cursor-not-allowed` }
311
+ aria-label = "Toggle Web Search"
312
+ title = "Toggle Web Search"
313
+ >
314
+ < Globe className = { `mr-1 text-gray-400 transition-all duration-200` } size = { 20 } />
315
+ < span className = { `text-sm font-light text-gray-400 transition-all duration-200` } > Web Search (Not available)</ span >
316
+ </ button >
317
+ )
318
+ )
319
+ :< > </ > ;
320
+
278
321
return (
279
322
< div className = "flex flex-col w-full h-full max-w-full" >
280
323
{ /* Messages area */ }
@@ -325,7 +368,7 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
325
368
return (
326
369
< div
327
370
key = { message . messageId }
328
- className = { `flex flex-col ${ isUserMessage ? 'items-end' : 'items-start' } ` }
371
+ className = { `flex flex-col h-fit ${ isUserMessage ? 'items-end' : 'items-start' } ` }
329
372
onMouseEnter = { ( ) => setHoveredMessageId ( message . messageId ) }
330
373
onMouseLeave = { ( ) => setHoveredMessageId ( null ) }
331
374
>
@@ -388,12 +431,12 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
388
431
} `}
389
432
>
390
433
{ isUserMessage ? (
391
- < MarkdownContent content = { message . content } />
434
+ < MarkdownContent content = { message . content } isUserMessage = { true } />
392
435
) : (
393
436
( message . content . length === 0 || MessageHelper . MessageContentToText ( message . content ) . length === 0 ) ? (
394
437
< div className = "w-4 h-4 bg-blue-600 rounded-full animate-bounce" > </ div >
395
438
) : (
396
- < MarkdownContent content = { message . content } />
439
+ < MarkdownContent content = { message . content } isUserMessage = { false } />
397
440
)
398
441
) }
399
442
</ div >
@@ -438,67 +481,66 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
438
481
</ div >
439
482
440
483
{ /* Input form */ }
441
- < form onSubmit = { handleSubmit }
484
+ < form onSubmit = { handleFormSubmit }
442
485
onClick = { ( ) => {
443
486
inputRef . current ?. focus ( ) ;
444
487
} }
445
488
onFocus = { ( ) => {
446
489
inputRef . current ?. focus ( ) ;
447
490
} }
448
- className = { `relative flex ${ isWebSearchPreviewEnabled ? ' flex-col' : 'flex-row justify-stretch items-center' } gap-2 px-4 pt-3 pb-2 m-2 mb-4 transition-all duration-200 rounded-lg form-textarea-border cursor-text` }
491
+ className = { `relative flex flex-col gap-2 h-fit px-4 pt-3 pb-2 m-2 mb-4 transition-all duration-200 rounded-lg form-textarea-border cursor-text` }
449
492
>
493
+ { /* Selected Files Display */ }
494
+ { selectedFiles . length > 0 && (
495
+ < div className = "flex flex-row w-full gap-1 mb-2" >
496
+ { selectedFiles . map ( ( file , index ) => (
497
+ < FileAttachmentDisplay
498
+ key = { `${ file . name } -${ index } ` }
499
+ file = { file }
500
+ isUser = { true }
501
+ showRemoveButton = { true }
502
+ onRemove = { ( ) => handleRemoveFile ( index ) }
503
+ />
504
+ ) ) }
505
+ </ div >
506
+ ) }
507
+
450
508
< textarea
451
509
ref = { inputRef }
452
510
value = { inputValue }
453
511
onChange = { ( e ) => { handleInputChanged ( e ) ; } }
454
512
onKeyDown = { ( e ) => {
455
513
if ( e . key === 'Enter' && ! e . shiftKey ) {
456
514
e . preventDefault ( ) ;
457
- handleSubmit ( e ) ;
515
+ handleFormSubmit ( e ) ;
458
516
}
459
517
} }
460
518
placeholder = { t ( 'chat.typeMessage' ) }
461
- className = "flex-1 w-[100%] px-2 pt-1 pb-2 resize-none focus:outline-none"
519
+ className = "w-[100%] resize-none focus:outline-none"
462
520
disabled = { isLoading }
463
521
inputMode = 'text'
464
522
rows = { 1 }
465
523
style = { { minHeight : '36px' , maxHeight : '108px' , height : '36px' , overflow : 'auto' } }
466
524
> </ textarea >
467
525
468
- < div className = "flex flex-row items-end justify-between h-full px-1" >
526
+ < div className = "flex flex-row items-center justify-between flex-1 h-full gap-2 px-1" >
527
+ < div className = 'flex flex-row items-center h-full gap-2' >
528
+ { /* File upload button */ }
529
+ { onSendMessageWithFiles && (
530
+ < FileUploadButton
531
+ onFilesSelected = { handleFilesSelected }
532
+ disabled = { isLoading || isCurrentlyStreaming }
533
+ />
534
+ ) }
535
+ </ div >
536
+
537
+ { /* Web search element */ }
469
538
{
470
- isWebSearchPreviewEnabled ? (
471
- ableToWebSearch ? (
472
- < button
473
- type = "button"
474
- onClick = { handleToggleWebSearch }
475
- className = { `flex items-center justify-center w-fit h-8 p-2 transition-all duration-200 rounded-full outline outline-2 hover:outline
476
- ${ webSearchActive ? 'bg-blue-50 outline-blue-300 hover:bg-blue-200 hover:outline hover:outline-blue-500' : 'bg-white outline-gray-100 hover:bg-blue-50 hover:outline hover:outline-blue-300' } ` }
477
- aria-label = "Toggle Web Search"
478
- title = "Toggle Web Search"
479
- >
480
- < Globe className = { `mr-1 ${ webSearchActive ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } size = { 20 } />
481
- < span className = { `text-sm font-light ${ webSearchActive ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } > Web Search</ span >
482
- </ button >
483
- )
484
- :
485
- (
486
- < button
487
- type = "button"
488
- className = { `flex items-center justify-center bg-gray-100 w-fit h-8 p-2 ml-2 transition-all duration-200 rounded-full cursor-not-allowed` }
489
- aria-label = "Toggle Web Search"
490
- title = "Toggle Web Search"
491
- >
492
- < Globe className = { `mr-1 text-gray-400 transition-all duration-200` } size = { 20 } />
493
- < span className = { `text-sm font-light text-gray-400 transition-all duration-200` } > Web Search (Not available)</ span >
494
- </ button >
495
- )
496
- )
497
- :< > </ >
539
+ webSearchElement
498
540
}
499
541
500
- < span className = { `flex-1 hidden text-xs text-center pt-4 text-gray-300 md:block truncate ${ isWebSearchPreviewEnabled ? ' pr-6 lg:pr-12' : '' } ` } >
501
- { isWebSearchPreviewEnabled ? t ( 'chat.pressShiftEnterToChangeLines' ) : '' }
542
+ < span className = { `flex-1 hidden text-xs text-center pt-4 text-gray-300 md:block truncate pr-6 lg:pr-12` } >
543
+ { t ( 'chat.pressShiftEnterToChangeLines' ) }
502
544
</ span >
503
545
504
546
{ isCurrentlyStreaming || hasStreamingMessage ? (
0 commit comments