@@ -5,7 +5,7 @@ import MarkdownContent from './MarkdownContent';
5
5
import MessageToolboxMenu , { ToolboxAction } from '../ui/MessageToolboxMenu' ;
6
6
import { MessageHelper } from '../../services/message-helper' ;
7
7
import { DatabaseIntegrationService } from '../../services/database-integration' ;
8
- import { SettingsService } from '../../services/settings-service' ;
8
+ import { SETTINGS_CHANGE_EVENT , SettingsService } from '../../services/settings-service' ;
9
9
import { ChatService } from '../../services/chat-service' ;
10
10
import { AIServiceCapability } from '../../types/capabilities' ;
11
11
import ProviderIcon from '../ui/ProviderIcon' ;
@@ -43,8 +43,9 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
43
43
const [ editingMessageId , setEditingMessageId ] = useState < string | null > ( null ) ;
44
44
const [ editingContent , setEditingContent ] = useState ( '' ) ;
45
45
const [ messagesList , setMessagesList ] = useState < Message [ ] > ( [ ] ) ;
46
+ const [ ableToWebSearch , setAbleToWebSearch ] = useState ( false ) ;
46
47
const [ webSearchActive , setWebSearchActive ] = useState ( false ) ;
47
- const [ isWebSearchAllowed , setIsWebSearchAllowed ] = useState ( false ) ;
48
+ const [ isWebSearchPreviewEnabled , setIsWebSearchPreviewEnabled ] = useState ( false ) ;
48
49
49
50
// Scroll to bottom when messages change
50
51
useEffect ( ( ) => {
@@ -65,24 +66,35 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
65
66
const loadWebSearchStatus = async ( ) => {
66
67
try {
67
68
const settingsService = SettingsService . getInstance ( ) ;
68
- setWebSearchActive ( settingsService . getWebSearchEnabled ( ) ) ;
69
+ setWebSearchActive ( settingsService . getWebSearchActive ( ) ) ;
70
+ setIsWebSearchPreviewEnabled ( settingsService . getWebSearchPreviewEnabled ( ) ) ;
69
71
} catch ( error ) {
70
72
console . error ( 'Failed to load web search status:' , error ) ;
71
73
}
72
74
} ;
73
75
74
76
loadWebSearchStatus ( ) ;
77
+
78
+ window . addEventListener ( SETTINGS_CHANGE_EVENT , ( ) => {
79
+ loadWebSearchStatus ( ) ;
80
+ } ) ;
75
81
} , [ ] ) ;
76
82
77
83
useEffect ( ( ) => {
78
84
const result = ChatService . getInstance ( ) . getCurrentProviderModelCapabilities ( ) . includes ( AIServiceCapability . WebSearch ) ;
79
- setIsWebSearchAllowed ( result ) ;
85
+ setAbleToWebSearch ( result ) ;
80
86
} , [ selectedProvider , selectedModel ] ) ;
81
87
88
+ useEffect ( ( ) => {
89
+ if ( isCurrentlyStreaming ) {
90
+ inputRef . current ?. focus ( ) ;
91
+ }
92
+ } , [ isCurrentlyStreaming ] ) ;
93
+
82
94
const handleSubmit = ( e : FormEvent ) => {
83
95
e . preventDefault ( ) ;
84
96
85
- if ( ! inputValue . trim ( ) || isLoading ) return ;
97
+ if ( ! inputValue . trim ( ) || isLoading || isCurrentlyStreaming ) return ;
86
98
87
99
onSendMessage ( inputValue ) ;
88
100
@@ -113,6 +125,8 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
113
125
114
126
// Handle send edited message
115
127
const handleSendEditedMessage = ( ) => {
128
+ if ( isCurrentlyStreaming ) return ;
129
+
116
130
if ( editingMessageId && onEditMessage && editingContent . trim ( ) ) {
117
131
const newContent = editingContent ;
118
132
@@ -253,21 +267,21 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
253
267
id : 'edit' ,
254
268
icon : Pencil ,
255
269
label : 'Edit' ,
256
- onClick : ( ) => handleEditMessage ( message . messageId , message . content ) ,
270
+ onClick : ( ) => handleEditMessage ( message . messageId , MessageHelper . MessageContentToText ( message . content ) ) ,
257
271
} ,
258
272
{
259
273
id : 'copy' ,
260
274
icon : Copy ,
261
275
label : 'Copy' ,
262
- onClick : ( ) => handleCopyMessage ( message . content ) ,
276
+ onClick : ( ) => handleCopyMessage ( MessageHelper . MessageContentToText ( message . content ) ) ,
263
277
}
264
278
]
265
279
: [
266
280
{
267
281
id : 'copy' ,
268
282
icon : Copy ,
269
283
label : 'Copy' ,
270
- onClick : ( ) => handleCopyMessage ( message . content ) ,
284
+ onClick : ( ) => handleCopyMessage ( MessageHelper . MessageContentToText ( message . content ) ) ,
271
285
} ,
272
286
// {
273
287
// id: 'share',
@@ -327,7 +341,7 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
327
341
< button
328
342
type = "submit"
329
343
className = "p-2 text-sm transition-all duration-200 rounded-md message-icon-btn"
330
- disabled = { ! editingContent . trim ( ) }
344
+ disabled = { ! editingContent . trim ( ) || isCurrentlyStreaming }
331
345
>
332
346
< Check size = { 18 } />
333
347
</ button >
@@ -351,9 +365,9 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
351
365
} `}
352
366
>
353
367
{ isUserMessage ? (
354
- < p className = "whitespace-pre-wrap" > { message . content } </ p >
368
+ < p className = "whitespace-pre-wrap" > { MessageHelper . MessageContentToText ( message . content ) } </ p >
355
369
) : (
356
- message . content === '' ? (
370
+ ( message . content . length === 0 || MessageHelper . MessageContentToText ( message . content ) . length === 0 ) ? (
357
371
< div className = "w-4 h-4 bg-blue-600 rounded-full animate-bounce" > </ div >
358
372
) : (
359
373
< MarkdownContent content = { message . content } />
@@ -427,6 +441,12 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
427
441
const newHeight = Math . min ( Math . max ( textarea . scrollHeight , minHeight ) , maxHeight ) ;
428
442
textarea . style . height = `${ newHeight } px` ;
429
443
} }
444
+ onKeyDown = { ( e ) => {
445
+ if ( e . key === 'Enter' && ! e . shiftKey ) {
446
+ e . preventDefault ( ) ;
447
+ handleSubmit ( e ) ;
448
+ }
449
+ } }
430
450
placeholder = "Type your message..."
431
451
className = "flex-1 px-2 pt-1 pb-2 resize-none focus:outline-none"
432
452
disabled = { isLoading }
@@ -438,33 +458,40 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
438
458
439
459
< div className = "flex flex-row items-center justify-between px-1" >
440
460
{
441
- isWebSearchAllowed ? (
442
- < button
443
- type = "button"
444
- onClick = { handleToggleWebSearch }
445
- className = { `flex items-center justify-center w-fit h-8 p-2 transition-all duration-200 rounded-full outline outline-2 hover:outline
446
- ${ 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' } ` }
447
- aria-label = "Toggle Web Search"
448
- title = "Toggle Web Search"
449
- >
450
- < Globe className = { `mr-1 ${ webSearchActive ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } size = { 20 } />
451
- < span className = { `text-sm font-light ${ webSearchActive ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } > Web Search</ span >
452
- </ button >
453
- )
454
- :
455
- (
456
- < button
457
- type = "button"
458
- 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` }
459
- aria-label = "Toggle Web Search"
460
- title = "Toggle Web Search"
461
- >
462
- < Globe className = { `mr-1 text-gray-400 transition-all duration-200` } size = { 20 } />
463
- < span className = { `text-sm font-light text-gray-400 transition-all duration-200` } > Web Search (Not available)</ span >
464
- </ button >
461
+ isWebSearchPreviewEnabled ? (
462
+ ableToWebSearch ? (
463
+ < button
464
+ type = "button"
465
+ onClick = { handleToggleWebSearch }
466
+ className = { `flex items-center justify-center w-fit h-8 p-2 transition-all duration-200 rounded-full outline outline-2 hover:outline
467
+ ${ 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' } ` }
468
+ aria-label = "Toggle Web Search"
469
+ title = "Toggle Web Search"
470
+ >
471
+ < Globe className = { `mr-1 ${ webSearchActive ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } size = { 20 } />
472
+ < span className = { `text-sm font-light ${ webSearchActive ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } > Web Search</ span >
473
+ </ button >
474
+ )
475
+ :
476
+ (
477
+ < button
478
+ type = "button"
479
+ 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` }
480
+ aria-label = "Toggle Web Search"
481
+ title = "Toggle Web Search"
482
+ >
483
+ < Globe className = { `mr-1 text-gray-400 transition-all duration-200` } size = { 20 } />
484
+ < span className = { `text-sm font-light text-gray-400 transition-all duration-200` } > Web Search (Not available)</ span >
485
+ </ button >
486
+ )
465
487
)
488
+ :< > </ >
466
489
}
467
490
491
+ < span className = { `flex-1 hidden text-xs text-center pt-4 text-gray-300 md:block truncate ${ isWebSearchPreviewEnabled ? 'pr-6 lg:pr-12' : '' } ` } >
492
+ { isWebSearchPreviewEnabled ? 'Press Shift+Enter to change lines' : '' }
493
+ </ span >
494
+
468
495
{ isCurrentlyStreaming || hasStreamingMessage ? (
469
496
< button
470
497
type = "button"
0 commit comments