@@ -12,6 +12,36 @@ import { useStore } from '@tomic/react';
1212import { useSettings } from '../../../helpers/AppSettings' ;
1313import type { Node } from '@tiptap/pm/model' ;
1414import 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+ } ;
1545
1646// Modify the Mention extension to allow serializing to markdown.
1747const SerializableMention = Mention . extend ( {
@@ -27,76 +57,95 @@ const SerializableMention = Mention.extend({
2757 } ,
2858 } ;
2959 } ,
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+ } ,
3070} ) ;
3171
3272interface AsyncAIChatInputProps {
33- onMentionUpdate : ( mentions : string [ ] ) => void ;
73+ onMentionUpdate : ( mentions : MentionItem [ ] ) => void ;
3474 onChange : ( markdown : string ) => void ;
3575 onSubmit : ( ) => void ;
76+ hasFiles : boolean ;
3677}
3778
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 } ) => {
4382 const store = useStore ( ) ;
44- const { drive } = useSettings ( ) ;
83+ const { drive, mcpServers } = useSettings ( ) ;
4584 const [ markdown , setMarkdown ] = useState ( '' ) ;
4685 const markdownRef = useRef ( markdown ) ;
4786 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+ ) ;
100149
101150 const handleChange = ( value : string ) => {
102151 setMarkdown ( value ) ;
@@ -108,7 +157,7 @@ const AsyncAIChatInput: React.FC<AsyncAIChatInputProps> = ({
108157 }
109158
110159 const mentions = digForMentions ( editor . getJSON ( ) ) ;
111- onMentionUpdate ( Array . from ( new Set ( mentions ) ) ) ;
160+ onMentionUpdate ( mentions ) ;
112161 } ;
113162
114163 useEffect ( ( ) => {
@@ -117,12 +166,29 @@ const AsyncAIChatInput: React.FC<AsyncAIChatInputProps> = ({
117166 } , [ markdown , onSubmit ] ) ;
118167
119168 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+ </ >
126192 ) ;
127193} ;
128194
@@ -141,9 +207,11 @@ const EditorWrapper = styled(EditorWrapperBase)`
141207 }
142208` ;
143209
144- function digForMentions ( data : JSONContent ) : string [ ] {
210+ function digForMentions (
211+ data : JSONContent ,
212+ ) : Array < MCPResourceSuggestion | AtomicResourceSuggestion > {
145213 if ( data . type === 'mention' ) {
146- return [ data . attrs ! . id ] ;
214+ return [ data . attrs as MCPResourceSuggestion | AtomicResourceSuggestion ] ;
147215 }
148216
149217 if ( data . content ) {
0 commit comments