1
1
import React , { useState , FormEvent , useRef , useEffect } from 'react' ;
2
2
import { Conversation , Message } from '../../types/chat' ;
3
- import { Send , Square , Copy , RotateCcw , Pencil , Loader2 , Globe } from 'lucide-react' ;
3
+ import { Send , Square , Copy , Pencil , Loader2 , Globe , RefreshCw , Check , X } from 'lucide-react' ;
4
4
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
8
import { SettingsService } from '../../services/settings-service' ;
9
9
import { ChatService } from '../../services/chat-service' ;
10
10
import { AIServiceCapability } from '../../types/capabilities' ;
11
+ import ProviderIcon from '../ui/ProviderIcon' ;
11
12
12
13
interface ChatMessageAreaProps {
13
14
activeConversation : Conversation | null ;
@@ -34,8 +35,10 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
34
35
selectedProvider,
35
36
selectedModel,
36
37
} ) => {
37
- const [ input , setInput ] = useState ( '' ) ;
38
+ const [ inputValue , setInput ] = useState ( '' ) ;
39
+ const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
38
40
const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
41
+ const editingContentRef = useRef < HTMLTextAreaElement > ( null ) ;
39
42
const [ hoveredMessageId , setHoveredMessageId ] = useState < string | null > ( null ) ;
40
43
const [ editingMessageId , setEditingMessageId ] = useState < string | null > ( null ) ;
41
44
const [ editingContent , setEditingContent ] = useState ( '' ) ;
@@ -50,9 +53,11 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
50
53
} , 50 ) ;
51
54
52
55
if ( activeConversation ) {
53
- const messagesList = MessageHelper . mapMessagesTreeToList ( activeConversation ) ;
56
+ const messagesList = MessageHelper . mapMessagesTreeToList ( activeConversation , false ) ;
54
57
setMessagesList ( messagesList ) ;
55
58
}
59
+
60
+ handleCancelEdit ( ) ;
56
61
} , [ activeConversation , activeConversation ?. messages ] ) ;
57
62
58
63
// Load web search status on component mount
@@ -77,9 +82,9 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
77
82
const handleSubmit = ( e : FormEvent ) => {
78
83
e . preventDefault ( ) ;
79
84
80
- if ( ! input . trim ( ) || isLoading ) return ;
85
+ if ( ! inputValue . trim ( ) || isLoading ) return ;
81
86
82
- onSendMessage ( input ) ;
87
+ onSendMessage ( inputValue ) ;
83
88
84
89
setInput ( '' ) ;
85
90
} ;
@@ -172,7 +177,7 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
172
177
updatedFatherMessage . conversationId
173
178
) ;
174
179
175
- setMessagesList ( MessageHelper . mapMessagesTreeToList ( activeConversation ) ) ;
180
+ setMessagesList ( MessageHelper . mapMessagesTreeToList ( activeConversation , false ) ) ;
176
181
} ;
177
182
178
183
const getCurrentMessageIndex = ( messageId : string ) => {
@@ -211,8 +216,8 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
211
216
}
212
217
} ;
213
218
214
- const getModelName = ( modelID : string ) => {
215
- const providerSettings = SettingsService . getInstance ( ) . getProviderSettings ( selectedProvider ) ;
219
+ const getModelName = ( modelID : string , provider : string ) => {
220
+ const providerSettings = SettingsService . getInstance ( ) . getProviderSettings ( provider ) ;
216
221
if ( ! providerSettings . models ) return modelID ;
217
222
218
223
const model = providerSettings . models ?. find ( m => m . modelId === modelID ) ;
@@ -272,12 +277,14 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
272
277
// },
273
278
{
274
279
id : 'regenerate' ,
275
- icon : RotateCcw ,
280
+ icon : RefreshCw ,
276
281
label : 'Regenerate' ,
277
282
onClick : ( ) => handleRegenerateResponse ( message . messageId ) ,
278
283
}
279
284
] ;
280
-
285
+
286
+ if ( message . role === 'system' ) return null ;
287
+
281
288
return (
282
289
< div
283
290
key = { message . messageId }
@@ -286,44 +293,61 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
286
293
onMouseLeave = { ( ) => setHoveredMessageId ( null ) }
287
294
>
288
295
{ isEditing && isUserMessage ? (
289
- < div className = "w-[80%]" >
290
- < textarea
296
+ < form
297
+ onSubmit = { handleSendEditedMessage }
298
+ onClick = { ( ) => {
299
+ editingContentRef . current ?. focus ( ) ;
300
+ } }
301
+ onFocus = { ( ) => {
302
+ editingContentRef . current ?. focus ( ) ;
303
+ } }
304
+ onKeyDown = { ( e ) => {
305
+ if ( e . key === 'Escape' ) {
306
+ handleCancelEdit ( ) ;
307
+ }
308
+ } }
309
+ className = "w-[80%] px-3 py-2 input-border rounded-lg cursor-text transition-all duration-200"
310
+ >
311
+ < textarea
312
+ ref = { editingContentRef }
291
313
value = { editingContent }
292
314
onChange = { ( e ) => setEditingContent ( e . target . value ) }
293
- className = "w-full px-3 py-2 border border-blue-400 rounded-lg focus:outline -none focus:ring-2 focus:ring-blue-500 "
315
+ className = "w-full rounded-lg resize -none focus:outline-none "
294
316
rows = { 3 }
295
- autoFocus
317
+ autoFocus = { true }
296
318
/>
297
319
< div className = "flex justify-end mt-2 space-x-2" >
298
320
< button
321
+ type = "button"
299
322
onClick = { handleCancelEdit }
300
- className = "px-3 py-1 text-sm text-gray-600 bg-gray-100 rounded hover:bg-gray-200 "
323
+ className = "p-2 text-sm text-red-400 transition-all duration-200 rounded-md hover:text-red-500 message-icon "
301
324
>
302
- Cancel
325
+ < X size = { 18 } />
303
326
</ button >
304
- < button
305
- onClick = { handleSendEditedMessage }
306
- className = "px-3 py-1 text-sm text-white bg-blue-500 rounded hover:bg-blue-600 "
327
+ < button
328
+ type = "submit"
329
+ className = "p-2 text-sm transition-all duration-200 rounded-md message-icon "
307
330
disabled = { ! editingContent . trim ( ) }
308
331
>
309
- Send
332
+ < Check size = { 18 } />
310
333
</ button >
311
334
</ div >
312
- </ div >
335
+ </ form >
313
336
) : (
314
337
< >
315
338
{ ! isUserMessage &&
316
339
< div className = "flex items-center flex-1 gap-2 px-2 mb-4 justify-left" >
317
- < span className = "text-sm text-gray-500" > { getModelName ( message . model ) } </ span >
318
- < span className = "text-sm text-gray-500 bg-gray-200 rounded-full px-2 py-0.5" > { message . provider } </ span >
340
+ < ProviderIcon providerName = { message . provider } className = "w-4 h-4 mr-2" />
341
+ < span className = "text-sm message-model-tag" > { getModelName ( message . model , message . provider ) } </ span >
342
+ < span className = "px-3 py-1 text-xs font-medium message-provider-tag" > { message . provider } </ span >
319
343
</ div >
320
344
}
321
345
322
346
< div
323
347
className = { `max-w-[80%] rounded-lg p-3 ${
324
348
isUserMessage
325
- ? 'bg-blue-500 text-white rounded-tr-none'
326
- : 'bg-gray-200 text-gray-800 rounded-tl-none'
349
+ ? 'message-user rounded-tr-none'
350
+ : 'message-assistant rounded-tl-none'
327
351
} `}
328
352
>
329
353
{ isUserMessage ? (
@@ -377,19 +401,42 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
377
401
</ div >
378
402
379
403
{ /* Input form */ }
380
- < form onSubmit = { handleSubmit } className = "flex flex-col gap-2 px-4 pt-4 pb-2 m-2 mb-4 border border-gray-200 rounded-lg" >
404
+ < form onSubmit = { handleSubmit }
405
+ onClick = { ( ) => {
406
+ inputRef . current ?. focus ( ) ;
407
+ } }
408
+ onFocus = { ( ) => {
409
+ inputRef . current ?. focus ( ) ;
410
+ } }
411
+ className = "flex flex-col gap-2 px-4 pt-3 pb-2 m-2 mb-4 transition-all duration-200 rounded-lg input-border cursor-text"
412
+ >
381
413
< div className = "flex space-x-2" >
382
- < input
383
- type = "text"
384
- value = { input }
385
- onChange = { ( e ) => setInput ( e . target . value ) }
414
+ < textarea
415
+ ref = { inputRef }
416
+ value = { inputValue }
417
+ onChange = { ( e ) => {
418
+ setInput ( e . target . value ) ;
419
+ // Adjust height based on content
420
+ const textarea = e . target ;
421
+ textarea . style . height = 'auto' ; // Reset height
422
+
423
+ // Calculate new height based on scrollHeight, with min and max constraints
424
+ const minHeight = 38 ; // Approx height for 1 row
425
+ const maxHeight = 38 * 3 ; // Approx height for 3 rows
426
+
427
+ const newHeight = Math . min ( Math . max ( textarea . scrollHeight , minHeight ) , maxHeight ) ;
428
+ textarea . style . height = `${ newHeight } px` ;
429
+ } }
386
430
placeholder = "Type your message..."
387
- className = "flex-1 px-4 py-2 border border-gray-300 rounded-full focus:outline -none focus:ring-2 focus:ring-blue-500 "
431
+ className = "flex-1 px-2 pt-1 pb-2 resize -none focus:outline-none "
388
432
disabled = { isLoading }
389
- />
433
+ inputMode = 'text'
434
+ rows = { 1 }
435
+ style = { { minHeight : '38px' , maxHeight : '114px' , height : '38px' , overflow : 'auto' } }
436
+ > </ textarea >
390
437
</ div >
391
438
392
- < div className = "flex flex-row items-center justify-between px-2 " >
439
+ < div className = "flex flex-row items-center justify-between px-1 " >
393
440
{
394
441
isWebSearchAllowed ? (
395
442
< button
@@ -422,18 +469,19 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
422
469
< button
423
470
type = "button"
424
471
onClick = { handleStopStreaming }
425
- className = "flex items-center justify-center w-10 h-10 text-white bg-red-500 rounded-full hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 "
472
+ className = "flex items-center justify-center w-10 h-10 transition-all duration-200 rounded-full conversation-stop-button focus:outline-none"
426
473
aria-label = "Stop response"
427
474
title = "Stop response"
428
475
>
429
- < Square size = { 20 } />
476
+ < Square size = { 20 } fill = "currentColor" />
430
477
</ button >
431
478
) : (
432
479
< button
433
480
type = "submit"
434
- disabled = { isLoading || ! input . trim ( ) }
435
- className = "flex items-center justify-center w-10 h-10 text-white bg-blue-500 rounded-full hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
436
- aria-label = "Send message"
481
+ disabled = { isLoading || ! inputValue . trim ( ) }
482
+ className = "flex items-center justify-center w-10 h-10 transition-all duration-200 rounded-full conversation-send-button focus:outline-none disabled:cursor-not-allowed"
483
+ aria-label = { isLoading || ! inputValue . trim ( ) ? 'Cannot send message' : 'Send message' }
484
+ title = { isLoading || ! inputValue . trim ( ) ? 'Cannot send message' : 'Send message' }
437
485
>
438
486
< Send size = { 20 } />
439
487
</ button >
0 commit comments