@@ -19,19 +19,24 @@ import { CancelButton } from './input/cancel-button';
19
19
import { SendButton } from './input/send-button' ;
20
20
import { IChatModel } from '../model' ;
21
21
import { IAutocompletionRegistry } from '../registry' ;
22
- import {
23
- AutocompleteCommand ,
24
- IAutocompletionCommandsProps ,
25
- IConfig ,
26
- Selection
27
- } from '../types' ;
22
+ import { IConfig , Selection } from '../types' ;
23
+ import { useChatCommands } from './input/use-chat-commands' ;
24
+ import { IChatCommandRegistry } from '../chat-commands' ;
28
25
29
26
const INPUT_BOX_CLASS = 'jp-chat-input-container' ;
30
27
31
28
export function ChatInput ( props : ChatInput . IProps ) : JSX . Element {
32
- const { autocompletionName, autocompletionRegistry, model } = props ;
33
- const autocompletion = useRef < IAutocompletionCommandsProps > ( ) ;
29
+ const { model } = props ;
34
30
const [ input , setInput ] = useState < string > ( props . value || '' ) ;
31
+ const inputRef = useRef < HTMLInputElement > ( ) ;
32
+
33
+ const chatCommands = useChatCommands (
34
+ input ,
35
+ setInput ,
36
+ inputRef ,
37
+ props . chatCommandRegistry
38
+ ) ;
39
+
35
40
const [ sendWithShiftEnter , setSendWithShiftEnter ] = useState < boolean > (
36
41
model . config . sendWithShiftEnter ?? false
37
42
) ;
@@ -46,9 +51,6 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
46
51
hideIncludeSelection = true ;
47
52
}
48
53
49
- // store reference to the input element to enable focusing it easily
50
- const inputRef = useRef < HTMLInputElement > ( ) ;
51
-
52
54
useEffect ( ( ) => {
53
55
const configChanged = ( _ : IChatModel , config : IConfig ) => {
54
56
setSendWithShiftEnter ( config . sendWithShiftEnter ?? false ) ;
@@ -69,87 +71,62 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
69
71
} ;
70
72
} , [ model ] ) ;
71
73
72
- // The autocomplete commands options.
73
- const [ commandOptions , setCommandOptions ] = useState < AutocompleteCommand [ ] > (
74
- [ ]
75
- ) ;
76
- // whether any option is highlighted in the slash command autocomplete
77
- const [ highlighted , setHighlighted ] = useState < boolean > ( false ) ;
78
- // controls whether the slash command autocomplete is open
79
- const [ open , setOpen ] = useState < boolean > ( false ) ;
80
-
81
74
const inputExists = ! ! input . trim ( ) ;
82
75
83
76
/**
84
- * Effect: fetch the list of available autocomplete commands.
85
- */
86
- useEffect ( ( ) => {
87
- if ( autocompletionRegistry === undefined ) {
88
- return ;
89
- }
90
- autocompletion . current = autocompletionName
91
- ? autocompletionRegistry . get ( autocompletionName )
92
- : autocompletionRegistry . getDefaultCompletion ( ) ;
93
-
94
- if ( autocompletion . current === undefined ) {
95
- return ;
96
- }
97
-
98
- if ( Array . isArray ( autocompletion . current . commands ) ) {
99
- setCommandOptions ( autocompletion . current . commands ) ;
100
- } else if ( typeof autocompletion . current . commands === 'function' ) {
101
- autocompletion . current
102
- . commands ( )
103
- . then ( ( commands : AutocompleteCommand [ ] ) => {
104
- setCommandOptions ( commands ) ;
105
- } ) ;
106
- }
107
- } , [ ] ) ;
108
-
109
- /**
110
- * Effect: Open the autocomplete when the user types the 'opener' string into an
111
- * empty chat input. Close the autocomplete and reset the last selected value when
112
- * the user clears the chat input.
77
+ * `handleKeyDown()`: callback invoked when the user presses any key in the
78
+ * `TextField` component. This is used to send the message when a user presses
79
+ * "Enter". This also handles many of the edge cases in the MUI Autocomplete
80
+ * component.
113
81
*/
114
- useEffect ( ( ) => {
115
- if ( ! autocompletion . current ?. opener ) {
116
- return ;
117
- }
118
-
119
- if ( input === autocompletion . current ?. opener ) {
120
- setOpen ( true ) ;
121
- return ;
122
- }
123
-
124
- if ( input === '' ) {
125
- setOpen ( false ) ;
126
- return ;
127
- }
128
- } , [ input ] ) ;
129
-
130
82
function handleKeyDown ( event : React . KeyboardEvent < HTMLInputElement > ) {
131
- if ( [ 'ArrowDown' , 'ArrowUp' ] . includes ( event . key ) && ! open ) {
83
+ /**
84
+ * IMPORTANT: This statement ensures that arrow keys can be used to navigate
85
+ * the multiline input when the chat commands menu is closed.
86
+ */
87
+ if (
88
+ [ 'ArrowDown' , 'ArrowUp' ] . includes ( event . key ) &&
89
+ ! chatCommands . menu . open
90
+ ) {
132
91
event . stopPropagation ( ) ;
133
92
return ;
134
93
}
135
94
95
+ // remainder of this function only handles the "Enter" key.
136
96
if ( event . key !== 'Enter' ) {
137
97
return ;
138
98
}
139
99
140
- // Do not send the message if the user was selecting a suggested command from the
141
- // Autocomplete component.
142
- if ( highlighted ) {
100
+ /**
101
+ * IMPORTANT: This statement ensures that when the chat commands menu is
102
+ * open with a highlighted command, the "Enter" key should run that command
103
+ * instead of sending the message.
104
+ *
105
+ * This is done by returning early and letting the event propagate to the
106
+ * `Autocomplete` component.
107
+ */
108
+ if ( chatCommands . menu . highlighted ) {
143
109
return ;
144
110
}
145
111
112
+ // remainder of this function only handles the "Enter" key pressed while the
113
+ // commands menu is closed.
114
+ /**
115
+ * IMPORTANT: This ensures that when the "Enter" key is pressed with the
116
+ * commands menu closed, the event is not propagated up to the
117
+ * `Autocomplete` component. Without this, `Autocomplete.onChange()` gets
118
+ * called with an invalid `string` instead of a `ChatCommand`.
119
+ */
120
+ event . stopPropagation ( ) ;
121
+
146
122
// Do not send empty messages, and avoid adding new line in empty message.
147
123
if ( ! inputExists ) {
148
124
event . stopPropagation ( ) ;
149
125
event . preventDefault ( ) ;
150
126
return ;
151
127
}
152
128
129
+ // Finally, send the message when all other conditions are met.
153
130
if (
154
131
( sendWithShiftEnter && event . shiftKey ) ||
155
132
( ! sendWithShiftEnter && ! event . shiftKey )
@@ -201,11 +178,7 @@ ${selection.source}
201
178
return (
202
179
< Box sx = { props . sx } className = { clsx ( INPUT_BOX_CLASS ) } >
203
180
< Autocomplete
204
- options = { commandOptions }
205
- value = { props . value }
206
- open = { open }
207
- autoHighlight
208
- freeSolo
181
+ { ...chatCommands . autocompleteProps }
209
182
// ensure the autocomplete popup always renders on top
210
183
componentsProps = { {
211
184
popper : {
@@ -255,38 +228,13 @@ ${selection.source}
255
228
helperText = { input . length > 2 ? helperText : ' ' }
256
229
/>
257
230
) }
258
- { ...autocompletion . current ?. props }
259
231
inputValue = { input }
260
232
onInputChange = { ( _ , newValue : string ) => {
261
233
setInput ( newValue ) ;
262
234
if ( typingNotification && model . inputChanged ) {
263
235
model . inputChanged ( newValue ) ;
264
236
}
265
237
} }
266
- onHighlightChange = {
267
- /**
268
- * On highlight change: set `highlighted` to whether an option is
269
- * highlighted by the user.
270
- *
271
- * This isn't called when an option is selected for some reason, so we
272
- * need to call `setHighlighted(false)` in `onClose()`.
273
- */
274
- ( _ , highlightedOption ) => {
275
- setHighlighted ( ! ! highlightedOption ) ;
276
- }
277
- }
278
- onClose = {
279
- /**
280
- * On close: set `highlighted` to `false` and close the popup by
281
- * setting `open` to `false`.
282
- */
283
- ( ) => {
284
- setHighlighted ( false ) ;
285
- setOpen ( false ) ;
286
- }
287
- }
288
- // hide default extra right padding in the text field
289
- disableClearable
290
238
/>
291
239
</ Box >
292
240
) ;
@@ -332,5 +280,9 @@ export namespace ChatInput {
332
280
* Autocompletion name.
333
281
*/
334
282
autocompletionName ?: string ;
283
+ /**
284
+ * Chat command registry.
285
+ */
286
+ chatCommandRegistry ?: IChatCommandRegistry ;
335
287
}
336
288
}
0 commit comments