From 2efb5d9cc6e26f6ff66255c0c556d8d0c2ef3035 Mon Sep 17 00:00:00 2001 From: mikbry Date: Thu, 29 Feb 2024 12:26:46 +0100 Subject: [PATCH] feat: add CommandManager --- webapp/components/views/Threads/Prompt.tsx | 8 +-- .../views/Threads/PromptCommandInput.tsx | 13 ++-- webapp/components/views/Threads/Thread.tsx | 14 ++--- webapp/utils/commands/index.ts | 60 +++++++++++++------ .../utils/commands/{Command.ts => types.ts} | 9 +++ webapp/utils/parsers/index.ts | 1 + webapp/utils/parsers/validator.ts | 26 ++++---- 7 files changed, 79 insertions(+), 52 deletions(-) rename webapp/utils/commands/{Command.ts => types.ts} (71%) diff --git a/webapp/components/views/Threads/Prompt.tsx b/webapp/components/views/Threads/Prompt.tsx index 675e767d..deaa8e83 100644 --- a/webapp/components/views/Threads/Prompt.tsx +++ b/webapp/components/views/Threads/Prompt.tsx @@ -21,7 +21,7 @@ import { KeyBinding, ShortcutIds, defaultShortcuts } from '@/hooks/useShortcuts' import logger from '@/utils/logger'; import { ParsedPrompt, TokenValidator, parsePrompt } from '@/utils/parsers'; import { getCaretPosition } from '@/utils/caretposition'; -import { Command } from '@/utils/commands/Command'; +import { CommandManager } from '@/utils/commands/types'; import { Button } from '../../ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'; import { ShortcutBadge } from '../../common/ShortCut'; @@ -30,7 +30,7 @@ import PromptCommandInput from './PromptCommandInput'; export type PromptProps = { conversationId: string; prompt: ParsedPrompt; - commands: Command[]; + commandManager: CommandManager; isLoading: boolean; errorMessage: string; disabled: boolean; @@ -43,7 +43,7 @@ export type PromptProps = { export default function Prompt({ conversationId, prompt, - commands, + commandManager, errorMessage, disabled, onUpdatePrompt, @@ -119,7 +119,7 @@ export default function Prompt({ void; onKeyDown: (event: KeyboardEvent) => void; className?: string; @@ -42,7 +42,7 @@ type PromptCommandProps = { function PromptCommandInput({ value, placeholder, - commands, + commandManager, className, onChange, onFocus, @@ -194,11 +194,8 @@ function PromptCommandInput({ }, [handleBlur, handleKeyDown, handleMouseDown, handleSelectionChange]); const filteredCommands = useMemo( - () => - commands.filter( - (c) => !(!c.value || c.value?.toLowerCase().indexOf(commandValue.toLowerCase()) === -1), - ), - [commands, commandValue], + () => commandManager.filterCommands(commandValue), + [commandManager, commandValue], ); const commandType = getCommandType(commandValue); const notFound = commandType ? `No ${commandType}s found.` : ''; diff --git a/webapp/components/views/Threads/Thread.tsx b/webapp/components/views/Threads/Thread.tsx index c23ec6bd..a29604ac 100644 --- a/webapp/components/views/Threads/Thread.tsx +++ b/webapp/components/views/Threads/Thread.tsx @@ -64,7 +64,7 @@ import { import { getConversationTitle } from '@/utils/conversations'; import validator from '@/utils/parsers/validator'; import { createMessage, changeMessageContent, mergeMessages } from '@/utils/data/messages'; -import { getCommands } from '@/utils/commands'; +import { getCommandManager } from '@/utils/commands'; import PromptArea from './Prompt'; import PromptsGrid from './PromptsGrid'; import ThreadMenu from './ThreadMenu'; @@ -164,10 +164,10 @@ function Thread({ const showEmptyChat = !conversationId; const selectedModel = selectedConversation?.model || activeModel; - const { modelItems, commands } = useMemo(() => { + const { modelItems, commandManager } = useMemo(() => { const items = getModelsAsItems(providers, backendContext, selectedModel); - const cmds = getCommands(items); - return { modelItems: items, commands: cmds }; + const manager = getCommandManager(items); + return { modelItems: items, commandManager: manager }; }, [backendContext, providers, selectedModel]); useEffect(() => { @@ -191,8 +191,8 @@ function Thread({ parsedPrompt: ParsedPrompt, _previousToken: PromptToken | undefined, ): [PromptToken, PromptToken | undefined] => - validator(commands, token, parsedPrompt, _previousToken), - [commands], + validator(commandManager, token, parsedPrompt, _previousToken), + [commandManager], ); const currentPrompt = useMemo( @@ -700,7 +700,7 @@ function Thread({ false, + }, ]; const parameterItems: Command[] = [ { @@ -38,7 +44,30 @@ export const getHashtagCommands = (): Command[] => { return parameters; }; -export const getCommands = (mentionItems: Partial[]): Command[] => { +export const getCommandType = (value: string | undefined): CommandType | undefined => { + if (value?.startsWith('/')) { + return CommandType.Action; + } + if (value?.startsWith('#')) { + return CommandType.Parameter; + } + if (value?.startsWith('@')) { + return CommandType.Mention; + } + return undefined; +}; + +export const compareCommands = ( + command1: string | undefined, + command2: string | string, + type?: CommandType, +): boolean => { + const type1 = getCommandType(command1); + const type2 = getCommandType(command2); + return (!type || (type === type1 && type === type2)) && command1 === command2; +}; + +export const getCommandManager = (mentionItems: Partial[]): CommandManager => { const commands = [ ...mentionItems .filter((item) => !item.selected) @@ -54,18 +83,15 @@ export const getCommands = (mentionItems: Partial[]): Command[] => { ...getHashtagCommands(), ...getActionCommands(), ]; - return commands; -}; - -export const getCommandType = (value: string): CommandType | undefined => { - if (value.startsWith('/')) { - return CommandType.Action; - } - if (value.startsWith('#')) { - return CommandType.Parameter; - } - if (value.startsWith('@')) { - return CommandType.Mention; - } - return undefined; + return { + commands, + getCommand: (value: string, type: string) => { + const command = commands.find((m) => compareCommands(m.value, value, type as CommandType)); + return command; + }, + filterCommands: (commandValue: string): Command[] => + commands.filter( + (c) => !(!c.value || c.value?.toLowerCase().indexOf(commandValue.toLowerCase()) === -1), + ), + }; }; diff --git a/webapp/utils/commands/Command.ts b/webapp/utils/commands/types.ts similarity index 71% rename from webapp/utils/commands/Command.ts rename to webapp/utils/commands/types.ts index b4c3b458..c401db81 100644 --- a/webapp/utils/commands/Command.ts +++ b/webapp/utils/commands/types.ts @@ -22,4 +22,13 @@ export enum CommandType { export type Command = Ui.MenuItem & { type: CommandType; + execute?: (value: string) => void; + postValidate?: (value?: string) => boolean; + validate?: (value?: string) => boolean; +}; + +export type CommandManager = { + commands: Command[]; + getCommand: (value: string, type: string) => Command | undefined; + filterCommands: (commandValue: string) => Command[]; }; diff --git a/webapp/utils/parsers/index.ts b/webapp/utils/parsers/index.ts index dd39a37b..8748a453 100644 --- a/webapp/utils/parsers/index.ts +++ b/webapp/utils/parsers/index.ts @@ -37,6 +37,7 @@ export type PromptToken = { value: string; index: number; state?: PromptTokenState; + blockOtherCommands?: boolean; }; export type ParsedPrompt = { diff --git a/webapp/utils/parsers/validator.ts b/webapp/utils/parsers/validator.ts index 192e80b3..ec0fd53e 100644 --- a/webapp/utils/parsers/validator.ts +++ b/webapp/utils/parsers/validator.ts @@ -13,19 +13,11 @@ // limitations under the License. import logger from '../logger'; -import { - ParsedPrompt, - PromptToken, - PromptTokenState, - PromptTokenType, - compareActions, - compareHashtags, - compareMentions, -} from '.'; -import { Command } from '../commands/Command'; +import { ParsedPrompt, PromptToken, PromptTokenState, PromptTokenType } from '.'; +import { CommandManager } from '../commands/types'; const validator = ( - commands: Command[], + commandManager: CommandManager, token: PromptToken, parsedPrompt: ParsedPrompt, _previousToken: PromptToken | undefined, @@ -42,10 +34,12 @@ const validator = ( let { type } = token; const isAtCaret = token.value.length + token.index === parsedPrompt.caretPosition; const isEditing = type !== PromptTokenType.Text && isAtCaret; + let blockOtherCommands: boolean | undefined; + const command = commandManager.getCommand(token.value, type); if (type === PromptTokenType.Mention) { if (isEditing) { state = PromptTokenState.Editing; - } else if (!commands.find((m) => compareMentions(m.value, token.value))) { + } else if (!command) { // this model is not available state = PromptTokenState.Error; } else if (parsedPrompt.tokens.find((to) => to.value === token.value && to.type === type)) { @@ -56,7 +50,6 @@ const validator = ( state = PromptTokenState.Disabled; } } else if (type === PromptTokenType.Hashtag) { - const command = commands.find((m) => compareHashtags(m.value, token.value)); if (isEditing) { state = PromptTokenState.Editing; } else if (!command) { @@ -70,15 +63,16 @@ const validator = ( state = PromptTokenState.Disabled; } } else if (type === PromptTokenType.Action) { - const command = commands.find((m) => compareActions(m.value, token.value)); if (isEditing) { state = PromptTokenState.Editing; } else if (!command) { // this command is not available state = PromptTokenState.Error; + } else { + blockOtherCommands = command.validate?.(); } } else if (type === PromptTokenType.Text && _previousToken?.type === PromptTokenType.Hashtag) { - const previousCommand = commands.find((m) => compareHashtags(m.value, _previousToken.value)); + const previousCommand = commandManager.getCommand(_previousToken.value, _previousToken.type); if (previousCommand && previousCommand.group !== 'parameters-boolean') { type = PromptTokenType.ParameterValue; previousToken = { ..._previousToken, state: PromptTokenState.Ok }; @@ -93,7 +87,7 @@ const validator = ( state = PromptTokenState.Editing; type = PromptTokenType.Hashtag; } - return [{ ...token, type, state }, previousToken]; + return [{ ...token, type, state, blockOtherCommands }, previousToken]; }; export default validator;