@@ -12,6 +12,36 @@ import { useStore } from '@tomic/react';
12
12
import { useSettings } from '../../../helpers/AppSettings' ;
13
13
import type { Node } from '@tiptap/pm/model' ;
14
14
import Placeholder from '@tiptap/extension-placeholder' ;
15
+ import { useMcpServers } from '../../../components/AI/MCP/useMcpServers' ;
16
+ import type {
17
+ AtomicResourceSuggestion ,
18
+ MCPResourceSuggestion ,
19
+ MentionItem ,
20
+ } from './types' ;
21
+ import { Row } from '../../../components/Row' ;
22
+ import {
23
+ IconButton ,
24
+ IconButtonVariant ,
25
+ } from '../../../components/IconButton/IconButton' ;
26
+ import { FaArrowRight } from 'react-icons/fa6' ;
27
+
28
+ const createAttribute = ( propName : string , dataName : string ) => {
29
+ return {
30
+ [ propName ] : {
31
+ default : null ,
32
+ parseHTML : ( element : HTMLElement ) => element . getAttribute ( dataName ) ,
33
+ renderHTML : ( attributes : Record < string , unknown > ) => {
34
+ if ( ! attributes [ propName ] ) {
35
+ return { } ;
36
+ }
37
+
38
+ return {
39
+ [ dataName ] : attributes [ propName ] ,
40
+ } ;
41
+ } ,
42
+ } ,
43
+ } ;
44
+ } ;
15
45
16
46
// Modify the Mention extension to allow serializing to markdown.
17
47
const SerializableMention = Mention . extend ( {
@@ -27,76 +57,95 @@ const SerializableMention = Mention.extend({
27
57
} ,
28
58
} ;
29
59
} ,
60
+ addAttributes ( ) {
61
+ return {
62
+ ...createAttribute ( 'type' , 'data-type' ) ,
63
+ ...createAttribute ( 'serverId' , 'data-server-id' ) ,
64
+ ...createAttribute ( 'mimeType' , 'data-mime-type' ) ,
65
+ ...createAttribute ( 'id' , 'data-id' ) ,
66
+ ...createAttribute ( 'label' , 'data-label' ) ,
67
+ ...createAttribute ( 'isA' , 'data-is-a' ) ,
68
+ } ;
69
+ } ,
30
70
} ) ;
31
71
32
72
interface AsyncAIChatInputProps {
33
- onMentionUpdate : ( mentions : string [ ] ) => void ;
73
+ onMentionUpdate : ( mentions : MentionItem [ ] ) => void ;
34
74
onChange : ( markdown : string ) => void ;
35
75
onSubmit : ( ) => void ;
76
+ hasFiles : boolean ;
36
77
}
37
78
38
- const AsyncAIChatInput : React . FC < AsyncAIChatInputProps > = ( {
39
- onMentionUpdate,
40
- onChange,
41
- onSubmit,
42
- } ) => {
79
+ const AsyncAIChatInput : React . FC <
80
+ React . PropsWithChildren < AsyncAIChatInputProps >
81
+ > = ( { onMentionUpdate, onChange, onSubmit, children, hasFiles } ) => {
43
82
const store = useStore ( ) ;
44
- const { drive } = useSettings ( ) ;
83
+ const { drive, mcpServers } = useSettings ( ) ;
45
84
const [ markdown , setMarkdown ] = useState ( '' ) ;
46
85
const markdownRef = useRef ( markdown ) ;
47
86
const onSubmitRef = useRef ( onSubmit ) ;
48
-
49
- const editor = useEditor ( {
50
- extensions : [
51
- Markdown . configure ( {
52
- html : true ,
53
- } ) ,
54
- StarterKit . extend ( {
55
- addKeyboardShortcuts ( ) {
56
- return {
57
- Enter : ( ) => {
58
- // Check if the cursor is in a code block, if so allow the user to press enter.
59
- // Pressing shift + enter will exit the code block.
60
- if ( 'language' in this . editor . getAttributes ( 'codeBlock' ) ) {
61
- return false ;
62
- }
63
-
64
- // The content has to be read from a ref because this callback is not updated often leading to stale content.
65
- onSubmitRef . current ( ) ;
66
- setMarkdown ( '' ) ;
67
- this . editor . commands . clearContent ( ) ;
68
-
69
- return true ;
70
- } ,
71
- } ;
72
- } ,
73
- } ) . configure ( {
74
- blockquote : false ,
75
- bulletList : false ,
76
- orderedList : false ,
77
- // paragraph: false,
78
- heading : false ,
79
- listItem : false ,
80
- horizontalRule : false ,
81
- bold : false ,
82
- strike : false ,
83
- italic : false ,
84
- } ) ,
85
- SerializableMention . configure ( {
86
- HTMLAttributes : {
87
- class : 'ai-chat-mention' ,
88
- } ,
89
- suggestion : searchSuggestionBuilder ( store , drive ) ,
90
- renderText ( { options, node } ) {
91
- return `${ options . suggestion . char } ${ node . attrs . title } ` ;
92
- } ,
93
- } ) ,
94
- Placeholder . configure ( {
95
- placeholder : 'Ask me anything...' ,
96
- } ) ,
97
- ] ,
98
- autofocus : true ,
99
- } ) ;
87
+ const { serversWithResources, searchResourcesOfServer } = useMcpServers ( ) ;
88
+
89
+ const editor = useEditor (
90
+ {
91
+ extensions : [
92
+ Markdown . configure ( {
93
+ html : true ,
94
+ } ) ,
95
+ StarterKit . extend ( {
96
+ addKeyboardShortcuts ( ) {
97
+ return {
98
+ Enter : ( ) => {
99
+ // Check if the cursor is in a code block, if so allow the user to press enter.
100
+ // Pressing shift + enter will exit the code block.
101
+ if ( 'language' in this . editor . getAttributes ( 'codeBlock' ) ) {
102
+ return false ;
103
+ }
104
+
105
+ // The content has to be read from a ref because this callback is not updated often leading to stale content.
106
+ onSubmitRef . current ( ) ;
107
+ setMarkdown ( '' ) ;
108
+ this . editor . commands . clearContent ( ) ;
109
+
110
+ return true ;
111
+ } ,
112
+ } ;
113
+ } ,
114
+ } ) . configure ( {
115
+ blockquote : false ,
116
+ bulletList : false ,
117
+ orderedList : false ,
118
+ heading : false ,
119
+ listItem : false ,
120
+ horizontalRule : false ,
121
+ bold : false ,
122
+ strike : false ,
123
+ italic : false ,
124
+ } ) ,
125
+ SerializableMention . configure ( {
126
+ HTMLAttributes : {
127
+ class : 'ai-chat-mention' ,
128
+ } ,
129
+ suggestion : searchSuggestionBuilder (
130
+ store ,
131
+ drive ,
132
+ mcpServers . filter ( server =>
133
+ serversWithResources . includes ( server . id ) ,
134
+ ) ,
135
+ searchResourcesOfServer ,
136
+ ) ,
137
+ renderText ( { options, node } ) {
138
+ return `${ options . suggestion . char } bla${ node . attrs . title } ` ;
139
+ } ,
140
+ } ) ,
141
+ Placeholder . configure ( {
142
+ placeholder : 'Ask me anything...' ,
143
+ } ) ,
144
+ ] ,
145
+ autofocus : true ,
146
+ } ,
147
+ [ serversWithResources , searchResourcesOfServer ] ,
148
+ ) ;
100
149
101
150
const handleChange = ( value : string ) => {
102
151
setMarkdown ( value ) ;
@@ -108,7 +157,7 @@ const AsyncAIChatInput: React.FC<AsyncAIChatInputProps> = ({
108
157
}
109
158
110
159
const mentions = digForMentions ( editor . getJSON ( ) ) ;
111
- onMentionUpdate ( Array . from ( new Set ( mentions ) ) ) ;
160
+ onMentionUpdate ( mentions ) ;
112
161
} ;
113
162
114
163
useEffect ( ( ) => {
@@ -117,12 +166,29 @@ const AsyncAIChatInput: React.FC<AsyncAIChatInputProps> = ({
117
166
} , [ markdown , onSubmit ] ) ;
118
167
119
168
return (
120
- < EditorWrapper hideEditor = { false } >
121
- < TiptapContextProvider editor = { editor } >
122
- < EditorContent editor = { editor } />
123
- < EditorEvents onChange = { handleChange } />
124
- </ TiptapContextProvider >
125
- </ EditorWrapper >
169
+ < >
170
+ < EditorWrapper hideEditor = { false } >
171
+ < TiptapContextProvider editor = { editor } >
172
+ < EditorContent editor = { editor } />
173
+ < EditorEvents onChange = { handleChange } />
174
+ </ TiptapContextProvider >
175
+ </ EditorWrapper >
176
+ < Row justify = 'space-between' >
177
+ { children }
178
+ < IconButton
179
+ disabled = { markdown . length === 0 && ! hasFiles }
180
+ onClick = { ( ) => {
181
+ onSubmit ( ) ;
182
+ setMarkdown ( '' ) ;
183
+ editor ?. commands . clearContent ( ) ;
184
+ } }
185
+ title = 'Send'
186
+ variant = { IconButtonVariant . Fill }
187
+ >
188
+ < FaArrowRight />
189
+ </ IconButton >
190
+ </ Row >
191
+ </ >
126
192
) ;
127
193
} ;
128
194
@@ -141,9 +207,11 @@ const EditorWrapper = styled(EditorWrapperBase)`
141
207
}
142
208
` ;
143
209
144
- function digForMentions ( data : JSONContent ) : string [ ] {
210
+ function digForMentions (
211
+ data : JSONContent ,
212
+ ) : Array < MCPResourceSuggestion | AtomicResourceSuggestion > {
145
213
if ( data . type === 'mention' ) {
146
- return [ data . attrs ! . id ] ;
214
+ return [ data . attrs as MCPResourceSuggestion | AtomicResourceSuggestion ] ;
147
215
}
148
216
149
217
if ( data . content ) {
0 commit comments