1
1
import React , { useState , FormEvent , useRef , useEffect } from 'react' ;
2
2
import { Conversation , Message } from '../../types/chat' ;
3
- import { Send , Square , Copy , Pencil , Loader2 , Globe , RefreshCw , Check , X } from 'lucide-react' ;
3
+ import { MCPServerSettings } from '../../types/settings' ;
4
+ import { Send , Square , Copy , Pencil , Loader2 , Globe , RefreshCw , Check , X , ServerCog } from 'lucide-react' ;
4
5
import MarkdownContent from './MarkdownContent' ;
5
6
import MessageToolboxMenu , { ToolboxAction } from '../ui/MessageToolboxMenu' ;
6
7
import { MessageHelper } from '../../services/message-helper' ;
@@ -26,6 +27,9 @@ interface ChatMessageAreaProps {
26
27
isCurrentlyStreaming ?: boolean ;
27
28
selectedProvider : string ;
28
29
selectedModel : string ;
30
+ mcpServers ?: Record < string , MCPServerSettings > ;
31
+ selectedMcpServers ?: string [ ] ;
32
+ onToggleMcpServer ?: ( serverId : string ) => void ;
29
33
}
30
34
31
35
export const ChatMessageArea : React . FC < ChatMessageAreaProps > = ( {
@@ -40,6 +44,9 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
40
44
isCurrentlyStreaming = false ,
41
45
selectedProvider,
42
46
selectedModel,
47
+ mcpServers,
48
+ selectedMcpServers,
49
+ onToggleMcpServer,
43
50
} ) => {
44
51
const { t } = useTranslation ( ) ;
45
52
const [ inputValue , setInput ] = useState ( '' ) ;
@@ -54,6 +61,9 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
54
61
const [ webSearchActive , setWebSearchActive ] = useState ( false ) ;
55
62
const [ isWebSearchPreviewEnabled , setIsWebSearchPreviewEnabled ] = useState ( false ) ;
56
63
const [ selectedFiles , setSelectedFiles ] = useState < File [ ] > ( [ ] ) ;
64
+ const [ mcpPopupOpen , setMcpPopupOpen ] = useState ( false ) ;
65
+ const mcpButtonRef = useRef < HTMLButtonElement > ( null ) ;
66
+ const mcpPopupRef = useRef < HTMLDivElement > ( null ) ;
57
67
58
68
// Scroll to bottom when messages change
59
69
useEffect ( ( ) => {
@@ -278,6 +288,25 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
278
288
textarea . style . height = `${ newHeight } px` ;
279
289
}
280
290
291
+ // Add useEffect to handle click outside for MCP popup
292
+ useEffect ( ( ) => {
293
+ const handleClickOutside = ( event : MouseEvent ) => {
294
+ if (
295
+ mcpPopupRef . current &&
296
+ ! mcpPopupRef . current . contains ( event . target as Node ) &&
297
+ mcpButtonRef . current &&
298
+ ! mcpButtonRef . current . contains ( event . target as Node )
299
+ ) {
300
+ setMcpPopupOpen ( false ) ;
301
+ }
302
+ } ;
303
+
304
+ document . addEventListener ( 'mousedown' , handleClickOutside ) ;
305
+ return ( ) => {
306
+ document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
307
+ } ;
308
+ } , [ ] ) ;
309
+
281
310
// If no active conversation is selected
282
311
if ( ! activeConversation ) {
283
312
return (
@@ -552,6 +581,70 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
552
581
webSearchElement
553
582
}
554
583
584
+ { /* MCP Servers dropdown */ }
585
+ { mcpServers && Object . keys ( mcpServers ) . length > 0 && (
586
+ < div className = "relative" >
587
+ < button
588
+ type = "button"
589
+ onClick = { ( ) => setMcpPopupOpen ( ! mcpPopupOpen ) }
590
+ ref = { mcpButtonRef }
591
+ className = { `flex items-center justify-center w-fit h-8 p-2 transition-all duration-200 rounded-full outline outline-2
592
+ ${ selectedMcpServers && selectedMcpServers . length > 0 ? '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' } ` }
593
+ aria-label = "MCP Servers"
594
+ title = "MCP Servers"
595
+ >
596
+ < ServerCog className = { `mr-1 ${ selectedMcpServers && selectedMcpServers . length > 0 ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } size = { 20 } />
597
+ < span className = { `text-sm font-light ${ selectedMcpServers && selectedMcpServers . length > 0 ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } >
598
+ MCP Tools { selectedMcpServers && selectedMcpServers . length > 0 ? `(${ selectedMcpServers . length } )` : '' }
599
+ </ span >
600
+ </ button >
601
+
602
+ { mcpPopupOpen && (
603
+ < div
604
+ ref = { mcpPopupRef }
605
+ className = "absolute z-10 mt-2 image-generation-popup"
606
+ style = { { bottom : '100%' , left : 0 , minWidth : '220px' } }
607
+ >
608
+ < div className = "p-2" >
609
+ < div className = "mb-2 text-sm font-medium text-gray-700" >
610
+ { t ( 'chat.availableMcpServers' ) }
611
+ </ div >
612
+ < div className = "overflow-y-auto max-h-60" >
613
+ { Object . values ( mcpServers ) . map ( ( server ) => (
614
+ < div
615
+ key = { server . id }
616
+ className = { `flex items-center px-3 py-2 cursor-pointer rounded-md ${
617
+ selectedMcpServers ?. includes ( server . id )
618
+ ? 'image-generation-provider-selected'
619
+ : 'image-generation-provider-item'
620
+ } `}
621
+ onClick = { ( ) => onToggleMcpServer ?.( server . id ) }
622
+ >
623
+ < ServerCog className = "w-5 h-5 mr-2" />
624
+ < div className = "flex flex-col" >
625
+ < span className = "text-sm font-medium" > { server . name } </ span >
626
+ { server . isDefault && (
627
+ < span className = "text-xs text-gray-500" > { t ( 'mcpServer.default' ) } </ span >
628
+ ) }
629
+ { server . isImageGeneration && (
630
+ < span className = "text-xs text-gray-500" > { t ( 'mcpServer.imageGeneration' ) } </ span >
631
+ ) }
632
+ </ div >
633
+ </ div >
634
+ ) ) }
635
+
636
+ { Object . keys ( mcpServers ) . length === 0 && (
637
+ < div className = "px-3 py-2 text-sm text-gray-500" >
638
+ { t ( 'chat.noMcpServersAvailable' ) }
639
+ </ div >
640
+ ) }
641
+ </ div >
642
+ </ div >
643
+ </ div >
644
+ ) }
645
+ </ div >
646
+ ) }
647
+
555
648
< span className = { `flex-1 hidden text-xs text-center pt-4 text-gray-300 md:block truncate pr-6 lg:pr-12` } >
556
649
{ t ( 'chat.pressShiftEnterToChangeLines' ) }
557
650
</ span >
0 commit comments