Skip to main content
Glama
CommandCreator.tsx9.38 kB
import { ReactNode, useEffect, useRef, useState } from "react"; import _ from "lodash"; import { useAuth } from "../../context/AuthContext"; import { useDeviceContext } from "../../context/DeviceContext"; import AuthModal from "../common/AuthModal"; import { Button } from "../design-system/button"; import { Input, InputHint, InputWrapper, TextArea } from "../design-system/input"; import { AiSparkles, EnterKey } from "../design-system/utils/images"; import CommandInput from "./CommandInput"; import { API } from "../../api/api"; import { Spinner } from "../design-system/spinner"; import ChatGptApiKeyModal from "../common/ChatGptApiKeyModal"; type CommandCreatorProps = { onSubmit: () => void; error: string | null; setError: (val: string | null) => void; }; /************************************************ * Main Component ************************************************/ export default function CommandCreator({ onSubmit, error, setError, }: CommandCreatorProps) { const { authToken } = useAuth(); const { currentCommandValue, setCurrentCommandValue } = useDeviceContext(); const [showAuthModal, setShowAuthModal] = useState<boolean>(false); const showAiInput = currentCommandValue[0] === " "; useEffect(() => { const enableAI = currentCommandValue[0] === " "; if (enableAI && !authToken) { setShowAuthModal(true); } }, [authToken, currentCommandValue]); const handleSetValue = (value: string) => { setError(null); setCurrentCommandValue(value); }; return ( <div> <AuthModal open={showAuthModal} onOpenChange={(val: boolean) => { setShowAuthModal(val); setCurrentCommandValue(""); }} /> {currentCommandValue.length > 0 ? ( <> {showAiInput ? ( <AiInput /> ) : ( <CommandForm onSubmit={onSubmit} error={error} setValue={handleSetValue} /> )} </> ) : ( <DefaultInput /> )} </div> ); } /************************************************ * Default Placed Input ************************************************/ const DefaultInput = () => { const inputRef = useRef<HTMLTextAreaElement>(null); const { currentCommandValue, setCurrentCommandValue } = useDeviceContext(); useEffect(() => { inputRef.current?.focus(); }, []); return ( <TextArea ref={inputRef} placeholder="Press ‘space’ for AI, or type commands…" value={currentCommandValue} onChange={(e) => setCurrentCommandValue(e.target.value)} rows={1} resize="none" /> ); }; /************************************************ * AI Input Form ************************************************/ const AiInput = () => { const aiCommandFormRef = useRef<HTMLFormElement>(null) const abortControllerRef = useRef<any>(null); const { authToken, openAiToken, deleteOpenAiToken } = useAuth(); const { setCurrentCommandValue } = useDeviceContext(); const aiInputRef = useRef<HTMLInputElement>(null); const [userInput, setUserInput] = useState<string>(""); const [formStates, setFormStates] = useState<{ isLoading: boolean; error: string | ReactNode | null; }>({ isLoading: false, error: null, }); const [showApiKeyModal, setShowApiKeyModal] = useState<boolean>(false); useEffect(() => { aiInputRef.current?.focus(); aiCommandFormRef.current?.scrollIntoView({ block: "end", inline: "nearest" }); }, []); const handleFormSubmit = async (e: React.FormEvent) => { abortControllerRef.current = new AbortController(); e.preventDefault(); setFormStates({ isLoading: true, error: null }); try { const viewHeir = await API.lastViewHierarchy(); const response = await API.generateCommandWithAI({ screen: viewHeir, userInput, token: authToken, signal: abortControllerRef.current.signal, openAiToken: openAiToken, }); if (_.get(response, "command")) { setFormStates({ isLoading: false, error: null }); setCurrentCommandValue(_.get(response, "command", "")); } else { setFormStates({ isLoading: false, error: "AI was not able to generate a command.", }); } } catch (error) { let errorMessage; if (_.get(error, "name") === "AbortError") { errorMessage = "Request was aborted!"; } else if (_.get(error, "status") === "429" && !openAiToken) { errorMessage = ( <> Exceeded the rate limit.{" "} <span className="underline cursor-pointer" onClick={() => setShowApiKeyModal(true)} > Add your own Key </span> </> ); } else { errorMessage = _.get(error, "message") || "An unexpected error occurred!"; } setFormStates({ isLoading: false, error: errorMessage, }); } }; // Function to handle backspace when the input is empty or escape key const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.key === "Backspace" && userInput === "") || e.key === "Escape") { setUserInput(""); setCurrentCommandValue(""); } }; if (formStates.isLoading) { return ( <div className="ai-loader flex px-3 h-10 items-center gap-2 relative rounded-xl"> <div className="absolute top-0.5 left-0.5 right-0.5 bottom-0.5 rounded-[10px] bg-white dark:bg-gray-900 z-0" /> <Spinner size="18" className="relative z-10" /> <p className="flex-grow text-sm font-semibold relative z-10"> {userInput} </p> <Button onClick={() => { abortControllerRef.current.abort(); setFormStates({ error: null, isLoading: false }); }} leftIcon="RiStopLine" variant="quaternary" className="relative z-10" > Stop Request </Button> </div> ); } return ( <> <ChatGptApiKeyModal open={showApiKeyModal} onOpenChange={(val) => setShowApiKeyModal(val)} /> <form ref={aiCommandFormRef} className="relative pb-10" onSubmit={handleFormSubmit}> <InputWrapper error={formStates.error}> <Input ref={aiInputRef} value={userInput} onKeyDown={handleKeyDown} onChange={(e) => { setUserInput(e.target.value); }} leftElement={<AiSparkles className="w-[18px] text-orange-500" />} placeholder="Ask AI to generate command" /> <InputHint /> </InputWrapper> <Button disabled={userInput === ""} type="submit" size="sm" className="absolute top-1.5 right-2 p-0 w-[28px] h-[28px]" > <EnterKey className="w-4" /> </Button> {openAiToken && ( <div className="mt-2 bg-blue-100 px-4 py-2 rounded-lg flex flex-col md:flex-row gap-2 md:items-center"> <p className="text-sm font-medium flex-grow"> Your openai API key: {openAiToken} </p> <div className="flex gap-2"> <Button type="button" onClick={() => setShowApiKeyModal(true)} variant="secondary" className="min-w-[85px]" > Update API </Button> <Button type="button" onClick={deleteOpenAiToken} variant="secondary-red" leftIcon="RiDeleteBin2Line" className="min-w-[88px]" > Remove API </Button> </div> </div> )} </form> </> ); }; /************************************************ * Command Input Form ************************************************/ const CommandForm = ({ onSubmit, error, setValue, }: { onSubmit: () => void; error: string | null; setValue: (val: string) => void; }) => { const commandForm = useRef<HTMLFormElement>(null); const commandInputRef = useRef<HTMLTextAreaElement>(null); const { currentCommandValue } = useDeviceContext(); useEffect(() => { const input = commandInputRef.current; if (input) { input.focus(); commandForm.current?.scrollIntoView({ block: "end", inline: "nearest" }); const len = input.value.length; input.selectionStart = len; input.selectionEnd = len; } }, []); const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(); }; return ( <form ref={commandForm} className="gap-2 flex flex-col relative pb-10" onSubmit={handleFormSubmit}> <CommandInput ref={commandInputRef} setValue={setValue} value={currentCommandValue} error={error} placeholder="Enter a command" onSubmit={onSubmit} /> <Button disabled={!currentCommandValue || !!error} type="submit" leftIcon="RiCommandLine" size="sm" className="absolute top-2 right-2 text-lg font-medium" > + <EnterKey className="w-4" /> </Button> </form> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mobile-dev-inc/Maestro'

If you have feedback or need assistance with the MCP directory API, please join our Discord server