@@ -31,7 +31,7 @@ export default function CommandViewer() {
3131 const { actions, runAction, setKeywordFilter } = useScopedActions ( ) ;
3232
3333 const [ inputPlaceholder , setInputPlaceholder ] = useState ( "" ) ;
34- const [ selectCommandIndex , setSelectCommandIndex ] = useState ( - 1 ) ;
34+ const [ selectActionIndex , setSelectActionIndex ] = useState ( - 1 ) ;
3535 const [ inputTextValue , setInputTextValue ] = useState ( "" ) ;
3636 const [ inputAudioValue , setInputAudioValue ] = useState <
3737 ReadableStream < ArrayBuffer > | undefined
@@ -40,7 +40,9 @@ export default function CommandViewer() {
4040 const [ isOutputVoice , setIsOutputVoice ] = useState ( false ) ;
4141 const [ isWaitingAssistant , setIsWaitingAssistant ] = useState ( false ) ;
4242
43+ const [ isArgsInputOpen , setIsArgsInputOpen ] = useState ( false ) ;
4344 const [ args , setArgs ] = useState < any > ( { } ) ;
45+ const [ actionReadyToRun , setActionReadyToRun ] = useState < boolean > ( false ) ;
4446
4547 const historyRef = useRef < HTMLDivElement > ( null ) ;
4648
@@ -61,9 +63,10 @@ export default function CommandViewer() {
6163 title : "Command Execution Failed" ,
6264 description : `Failed to execute command: ${ action . action . name } . Error: ${ error . message } ` ,
6365 } ) ;
66+ console . error ( "Failed to run action:" , error ) ;
6467 }
6568 } ,
66- [ runAction ] ,
69+ [ runAction , args ] ,
6770 ) ;
6871
6972 useEffect ( ( ) => {
@@ -83,13 +86,14 @@ export default function CommandViewer() {
8386 clearInterval ( interval ) ;
8487 } ;
8588 } , [ ] ) ;
89+
8690 useEffect ( ( ) => {
8791 if ( inputTextValue !== "" ) {
8892 // Assume commands are ordered by relevance.
8993 // Filter commands based on the input value
9094 setKeywordFilter ( inputTextValue ) ;
9195 // Choose the first command suggestion.
92- setSelectCommandIndex ( 0 ) ;
96+ setSelectActionIndex ( 0 ) ;
9397 } else {
9498 setKeywordFilter ( undefined ) ;
9599 }
@@ -116,6 +120,42 @@ export default function CommandViewer() {
116120 }
117121 } , [ history ] ) ;
118122
123+ // Reset input if selected index changes
124+ useEffect ( ( ) => {
125+ setArgs ( { } ) ;
126+ setIsArgsInputOpen ( false ) ;
127+ } , [ selectActionIndex ] ) ;
128+
129+ // Check action validity
130+ useEffect ( ( ) => {
131+ async function checkAndRunAction ( ) {
132+ if ( ! actionReadyToRun ) {
133+ return ;
134+ }
135+
136+ console . log ( "Action ready to run:" , actionReadyToRun ) ;
137+
138+ const queuedAction = actions [ selectActionIndex ] ;
139+
140+ if ( queuedAction ) {
141+ const isValid = validateActionArgs ( queuedAction , args ) ;
142+ if ( isValid ) {
143+ // Run action directly if args are valid
144+ await runActionCallback ( queuedAction ) ;
145+ } else {
146+ // Otherwise, open args input and wait for user
147+ // to fill in required args.
148+ setIsArgsInputOpen ( true ) ;
149+ }
150+ }
151+
152+ // Reset ready state if this pass ran or did not run.
153+ setActionReadyToRun ( false ) ;
154+ }
155+
156+ checkAndRunAction ( ) ;
157+ } , [ actionReadyToRun ] ) ;
158+
119159 function handleKeyDown ( e : KeyboardEvent ) {
120160 // Prevent default behavior for certain keys
121161 const key = e . key ;
@@ -136,7 +176,7 @@ export default function CommandViewer() {
136176 if ( keysToPrevent . includes ( key ) ) {
137177 e . preventDefault ( ) ;
138178 // @ts -expect-error continuePropagation is not in the type definition
139- e . continuePropagation ( ) ;
179+ if ( e . continuePropagation ) e . continuePropagation ( ) ;
140180 }
141181
142182 const keys = [ ...( editorContext ?. editorStates . pressedKeys ?? [ ] ) , key ] ;
@@ -167,7 +207,7 @@ export default function CommandViewer() {
167207 if ( keysToPrevent . includes ( key ) ) {
168208 e . preventDefault ( ) ;
169209 // @ts -expect-error continuePropagation is not in the type definition
170- e . continuePropagation ( ) ;
210+ if ( e . continuePropagation ) e . continuePropagation ( ) ;
171211 }
172212
173213 const keys =
@@ -185,10 +225,10 @@ export default function CommandViewer() {
185225 const isArrowUpPressed = pressedKeys . includes ( "ArrowUp" ) ;
186226 const isArrowDownPressed = pressedKeys . includes ( "ArrowDown" ) ;
187227 const isControlPressed = pressedKeys . includes ( "Control" ) ;
188- if ( isEnterPressed && isControlPressed && selectCommandIndex !== - 1 ) {
228+ if ( isEnterPressed && isControlPressed && selectActionIndex !== - 1 ) {
189229 // Run command if ctrl is pressed
190230 console . log ( "Running command" ) ;
191- runActionCallback ( actions [ selectCommandIndex ] ) ;
231+ setActionReadyToRun ( true ) ;
192232 } else if ( isEnterPressed && ! isControlPressed ) {
193233 // Chat with assistant if ctrl is not pressed
194234 console . log ( "Chatting with assistant" ) ;
@@ -197,21 +237,55 @@ export default function CommandViewer() {
197237 setIsWaitingAssistant ( true ) ;
198238 } ) ;
199239 } else {
240+ if ( inputTextValue === "" ) {
241+ if ( selectActionIndex !== - 1 ) {
242+ addToast ( {
243+ color : "warning" ,
244+ title : "Chat input is empty" ,
245+ description : `Did you mean to run the command: ${ actions [ selectActionIndex ] . action . name } ? Use Ctrl + Enter to run the selected command.` ,
246+ } ) ;
247+ } else {
248+ addToast ( {
249+ color : "warning" ,
250+ title : "Chat input is empty" ,
251+ description : "Please enter a message or use voice input." ,
252+ } ) ;
253+ }
254+ return ;
255+ }
200256 chatWithAssistant ( inputTextValue , isOutputVoice ) . then ( ( ) => {
201257 setIsWaitingAssistant ( true ) ;
202258 } ) ;
203259 }
204260 } else if ( isArrowUpPressed ) {
205- setSelectCommandIndex ( ( prev ) =>
261+ setSelectActionIndex ( ( prev ) =>
206262 prev === 0 ? actions . length - 1 : prev - 1 ,
207263 ) ;
208264 } else if ( isArrowDownPressed ) {
209- setSelectCommandIndex ( ( prev ) =>
265+ setSelectActionIndex ( ( prev ) =>
210266 prev === actions . length - 1 ? 0 : prev + 1 ,
211267 ) ;
212268 }
213269 }
214270
271+ function validateActionArgs ( action : ScopedAction , args : any ) {
272+ const paramsEntries = Object . entries ( action . action . parameters ) ;
273+
274+ // Check if all required arguments are provided
275+ if ( paramsEntries . length > 0 ) {
276+ const missingParams = paramsEntries . filter (
277+ ( [ key , value ] ) =>
278+ ! action . action . parameters [ key ] . optional && args [ key ] === undefined ,
279+ ) ;
280+ if ( missingParams . length > 0 ) {
281+ return false ;
282+ }
283+ return true ;
284+ }
285+
286+ return true ;
287+ }
288+
215289 return (
216290 < div className = "absolute top-20 left-1/2 z-50 -translate-x-1/2" >
217291 < div className = "flex max-h-[calc(100vh-100px)] flex-col items-center gap-y-1" >
@@ -319,38 +393,79 @@ export default function CommandViewer() {
319393 < Listbox
320394 selectionMode = "single"
321395 selectedKeys = {
322- selectCommandIndex === - 1 ? [ ] : [ selectCommandIndex . toString ( ) ]
396+ selectActionIndex === - 1 ? [ ] : [ selectActionIndex . toString ( ) ]
323397 }
324- onSelectionChange = { ( selection ) => {
325- const key = selection as any ;
326- const index = key . currentKey
327- ? parseInt ( key . currentKey as string )
328- : selectCommandIndex ;
329-
330- setSelectCommandIndex ( index ) ;
331- runActionCallback ( actions [ index ] ) ;
332- } }
333398 label = "Command Suggestions"
399+ shouldSelectOnPressUp
334400 >
335401 { actions . map ( ( command , index ) => (
336402 < ListboxItem
337403 key = { index . toString ( ) }
338404 className = "data-[is-selected=true]:bg-primary/20"
339- data-is-selected = { selectCommandIndex === index }
405+ data-is-selected = { selectActionIndex === index }
340406 endContent = {
341- selectCommandIndex === index && (
407+ selectActionIndex === index && (
342408 < div className = "absolute right-7" >
343409 < Kbd > Ctrl + Enter</ Kbd >
344410 </ div >
345411 )
346412 }
413+ onPress = { ( e ) => {
414+ // Prevent triggering when pressing Enter to run command.
415+ if ( e . pointerType === "keyboard" ) {
416+ return ;
417+ }
418+
419+ setSelectActionIndex ( index ) ;
420+ setActionReadyToRun ( true ) ;
421+ } }
422+ onKeyDown = { ( e ) => handleKeyDown ( e as any ) }
423+ onKeyUp = { ( e ) => handleKeyUp ( e as any ) }
347424 >
348425 { command . action . name }
349426 </ ListboxItem >
350427 ) ) }
351428 </ Listbox >
352429 </ div >
353430 ) }
431+ { isArgsInputOpen && actions [ selectActionIndex ] && (
432+ < div className = "bg-content1 w-80 rounded-2xl shadow-md p-4" >
433+ < p className = "mb-2 font-bold" > Command Action Arguments</ p >
434+ { Object . entries ( actions [ selectActionIndex ] . action . parameters ) . map (
435+ ( [ paramName , param ] , index ) => (
436+ < div key = { paramName } className = "mb-2" >
437+ < Input
438+ className = "w-full"
439+ value = { args [ paramName ] || "" }
440+ onValueChange = { ( value ) =>
441+ setArgs ( ( prev : any ) => ( {
442+ ...prev ,
443+ [ paramName ] : value ,
444+ } ) )
445+ }
446+ placeholder = { param . description || "" }
447+ label = { `${ paramName } ${ param . optional ? " (optional)" : "" } ` }
448+ autoFocus = { index === 0 }
449+ isRequired = { ! param . optional }
450+ size = "sm"
451+ />
452+ </ div >
453+ ) ,
454+ ) }
455+ < Button
456+ className = "w-full"
457+ onPress = { ( ) => {
458+ setIsArgsInputOpen ( false ) ;
459+ setActionReadyToRun ( true ) ;
460+ } }
461+ isDisabled = { ! validateActionArgs ( actions [ selectActionIndex ] , args ) }
462+ color = "primary"
463+ >
464+ Run Command
465+ < Kbd > Ctrl + Enter</ Kbd >
466+ </ Button >
467+ </ div >
468+ ) }
354469 </ div >
355470 </ div >
356471 ) ;
0 commit comments