Skip to content

Commit 7094aea

Browse files
committed
Allow entering action args in command viewer
1 parent e20c10e commit 7094aea

File tree

1 file changed

+136
-21
lines changed

1 file changed

+136
-21
lines changed

web/components/interface/command-viewer.tsx

Lines changed: 136 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)