diff --git a/docs/DATAMODEL.md b/docs/DATAMODEL.md index 415586ca..51cee0c5 100644 --- a/docs/DATAMODEL.md +++ b/docs/DATAMODEL.md @@ -28,9 +28,12 @@ interface MynahUIDataModel { * Chat screen loading animation state (mainly use during the stream or getting the initial answer) */ loadingChat?: boolean; + /** + * Show chat avatars or not + * */ + showChatAvatars?: boolean; /** * Show cancel button while loading the chat - * If you want to disable it globally, leave the onStopChatResponse on mynah ui constructor as undefined * */ cancelButtonWhenLoading?: boolean; /** @@ -38,13 +41,17 @@ interface MynahUIDataModel { */ quickActionCommands?: QuickActionCommandGroup[]; /** + * Context commands to show when user hits @ to the input any point + */ + contextCommands?: QuickActionCommandGroup[]; + /** * Placeholder to be shown on prompt input */ promptInputPlaceholder?: string; /** * Info block to be shown under prompt input */ - promptInputInfo?: string; // supports MARKDOWN string + promptInputInfo?: string; /** * A sticky chat item card on top of the prompt input */ @@ -218,7 +225,7 @@ mynahUI.updateStore('tab-1', { ```

- mainTitle + quickActionCommands

To handle the incoming command (if there is) check it with the prompt object in the `onChatPrompt` event. @@ -246,6 +253,72 @@ const mynahUI = new MynahUI({ --- +### `contextCommands` (default: `[]`) +Context commands are the predefined context items which user can pick between but unlike quick action commands, they can be picked several times at any point in the prompt text. When users hit `@` from their keyboard in the input, if there is an available list of context items provided through store it will show up as an overlay menu. + + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + contextCommands: [ + { + groupName: 'Metion code', + commands:[ + { + command: '@ws', + description: '(BETA) Reference all code in workspace.' + }, + { + command: '@folder', + placeholder: 'mention a specific folder', + description: 'All files within a specific folder' + }, + { + command: '@file', + placeholder: 'mention a specific file', + description: 'Reference a specific file' + }, + { + command: '@code', + placeholder: 'mention a specific file/folder, or leave blank for full project', + description: 'After that mention a specific file/folder, or leave blank for full project' + }, + { + command: '@gitlab', + description: 'Ask about data in gitlab account' + } + ] + } + ] +}) +``` + +

+ contextCommands +

+ +To see which context is used, check the incoming array in the prompt object comes with the `onChatPrompt` event. + +```typescript +const mynahUI = new MynahUI({ + ... + onChatPrompt: (prompt)=>{ + if(prompt.context != null && prompt.context.indexOf('@ws') { + // Use whole workspace! + } + } +}); +``` + +--- + ### `promptInputPlaceholder` (default: `''`) This is the placeholder text for the prompt input @@ -1988,3 +2061,18 @@ mynahUI.notify({

mainTitle

+ + +--- + +## ChatPrompt +This is the object model which will be send along with the `onChatPrompt` event. + +```typescript +export interface ChatPrompt { + prompt?: string; + escapedPrompt?: string; // Generally being used to send it back to mynah-ui for end user prompt rendering + command?: string; + context?: string[]; +} +``` \ No newline at end of file diff --git a/docs/PROPERTIES.md b/docs/PROPERTIES.md index 788c8273..77dc2ce0 100644 --- a/docs/PROPERTIES.md +++ b/docs/PROPERTIES.md @@ -364,6 +364,7 @@ onChatPrompt?: ( console.log(`Prompt text (as written): ${prompt.prompt}`); console.log(`Prompt text (HTML escaped): ${prompt.escapedPrompt}`); console.log(`Command (if selected from quick actions): ${prompt.command}`); + console.log(`Context (if selected from context selector): ${(prompt.context??[]).join(', ')}`); console.log(`Attachment (feature not available yet): ${prompt.attachment}`); }; ... diff --git a/docs/img/data-model/tabStore/contextCommands.png b/docs/img/data-model/tabStore/contextCommands.png new file mode 100644 index 00000000..af5831c0 Binary files /dev/null and b/docs/img/data-model/tabStore/contextCommands.png differ diff --git a/example/src/config.ts b/example/src/config.ts index e04b5623..7c94ea16 100644 --- a/example/src/config.ts +++ b/example/src/config.ts @@ -101,7 +101,7 @@ export const QuickActionCommands:QuickActionCommandGroup[] = [ }, ], }, -]; +] as QuickActionCommandGroup[]; export const mynahUIDefaults = { store: { @@ -117,6 +117,36 @@ export const mynahUIDefaults = { defaultFollowUps ], quickActionCommands: QuickActionCommands, - promptInputPlaceholder: 'Type something or "/" for quick action commands', + contextCommands: [ + { + groupName: 'Metion code', + commands:[ + { + command: '@ws', + description: '(BETA) Reference all code in workspace.' + }, + { + command: '@folder', + placeholder: 'mention a specific folder', + description: 'All files within a specific folder' + }, + { + command: '@file', + placeholder: 'mention a specific file', + description: 'Reference a specific file' + }, + { + command: '@code', + placeholder: 'mention a specific file/folder, or leave blank for full project', + description: 'After that mention a specific file/folder, or leave blank for full project' + }, + { + command: '@gitlab', + description: 'Ask about data in gitlab account' + } + ] + } + ] as QuickActionCommandGroup[], + promptInputPlaceholder: 'Type something or "/" for quick action commands or @ for choosing context', } }; diff --git a/example/src/main.ts b/example/src/main.ts index 495ab047..76503a46 100644 --- a/example/src/main.ts +++ b/example/src/main.ts @@ -143,7 +143,8 @@ export const createMynahUI = (initialData?: MynahUIDataModel): MynahUI => { onChatPrompt: (tabId: string, prompt: ChatPrompt) => { Log(`New prompt on tab: ${tabId}
prompt: ${prompt.prompt !== undefined && prompt.prompt !== '' ? prompt.prompt : '{command only}'}
- command: ${prompt.command ?? '{none}'}`); + command: ${prompt.command ?? '{none}'}
+ context: ${(prompt.context??[]).join(', ')}`); if (tabId === 'tab-1') { mynahUI.updateStore(tabId, { tabCloseConfirmationMessage: `Working on "${prompt.prompt}"`, diff --git a/package-lock.json b/package-lock.json index f7c26160..db3774fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aws/mynah-ui", - "version": "4.11.2", + "version": "4.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aws/mynah-ui", - "version": "4.11.2", + "version": "4.12.0", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { diff --git a/package.json b/package.json index d664974b..072f1d64 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@aws/mynah-ui", "displayName": "AWS Mynah UI", - "version": "4.11.2", + "version": "4.12.0", "description": "AWS Toolkit VSCode and Intellij IDE Extension Mynah UI", "publisher": "Amazon Web Services", "license": "Apache License 2.0", diff --git a/src/components/chat-item/chat-prompt-input.ts b/src/components/chat-item/chat-prompt-input.ts index 7c0cfa59..b3e74058 100644 --- a/src/components/chat-item/chat-prompt-input.ts +++ b/src/components/chat-item/chat-prompt-input.ts @@ -4,7 +4,7 @@ */ import { DomBuilder, ExtendedHTMLElement } from '../../helper/dom'; -import { KeyMap, MynahEventNames, PromptAttachmentType, QuickActionCommand, QuickActionCommandGroup } from '../../static'; +import { ChatPrompt, KeyMap, MynahEventNames, PromptAttachmentType, QuickActionCommand, QuickActionCommandGroup } from '../../static'; import { MynahUIGlobalEvents, cancelEvent } from '../../helper/events'; import { Overlay, OverlayHorizontalDirection, OverlayVerticalDirection } from '../overlay'; import { MynahUITabsStore } from '../../helper/tabs-store'; @@ -31,14 +31,16 @@ export class ChatPromptInput { private readonly remainingCharsIndicator: ExtendedHTMLElement; private readonly sendButton: SendButton; private readonly promptAttachment: PromptAttachment; - private quickActionCommands: QuickActionCommandGroup[]; - private commandSelector: Overlay; - private commandSelectorOpen: boolean = false; + private quickPickTriggerIndex: number; + private quickPickType: 'quick-action' | 'context'; + private textAfter: string; + private quickPickItemGroups: QuickActionCommandGroup[]; + private filteredQuickPickItemGroups: QuickActionCommandGroup[]; + private quickPick: Overlay; + private quickPickOpen: boolean = false; private selectedCommand: string = ''; - private filteredCommandsList: QuickActionCommandGroup[]; constructor (props: ChatPromptInputProps) { this.props = props; - this.quickActionCommands = MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId).getValue('quickActionCommands') as QuickActionCommandGroup[]; this.promptTextInputCommand = new ChatPromptInputCommand({ command: '', onRemoveClick: () => { @@ -142,70 +144,94 @@ export class ChatPromptInput { }; private readonly handleInputKeydown = (e: KeyboardEvent): void => { - if (!this.commandSelectorOpen) { - this.quickActionCommands = MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId).getValue('quickActionCommands') as QuickActionCommandGroup[]; - if (e.key === KeyMap.BACKSPACE && this.selectedCommand !== '' && this.promptTextInput.getTextInputValue() === '') { - cancelEvent(e); - this.clearTextArea(true); + if (!this.quickPickOpen) { + if (e.key === KeyMap.BACKSPACE || e.key === KeyMap.DELETE) { + if (this.selectedCommand !== '' && this.promptTextInput.getTextInputValue() === '') { + cancelEvent(e); + this.clearTextArea(true); + } else { + // If we're trying to delete a context item, we should do it as a word, not just some letter inside the context. + // Since those context are defined, it should match the whole term or it shouldn't be there at all. + const targetWord = this.promptTextInput.getWordAndIndexOnCursorPos(); + if (targetWord.word.charAt(0) === KeyMap.AT) { + cancelEvent(e); + const currValue = this.promptTextInput.getTextInputValue(); + this.promptTextInput.updateTextInputValue(currValue.substring(0, targetWord.wordStartIndex) + currValue.substring(targetWord.wordStartIndex + targetWord.word.length)); + this.promptTextInput.focus(targetWord.wordStartIndex); + } + } } else if (e.key === KeyMap.ENTER && ((!e.isComposing && !e.shiftKey && !e.ctrlKey) || (e.isComposing && (e.shiftKey)))) { cancelEvent(e); this.sendPrompt(); - } else if (this.selectedCommand === '' && this.quickActionCommands.length > 0 && e.key === KeyMap.SLASH && this.promptTextInput.getTextInputValue() === '') { - // Show available quick actions - if (this.commandSelector !== undefined) { - this.commandSelector.close(); - } - this.filteredCommandsList = [ ...this.quickActionCommands ]; - this.commandSelector = new Overlay({ - closeOnOutsideClick: true, - referenceElement: this.render.querySelector('.mynah-chat-prompt') as ExtendedHTMLElement, - dimOutside: false, - stretchWidth: true, - verticalDirection: OverlayVerticalDirection.TO_TOP, - horizontalDirection: OverlayHorizontalDirection.START_TO_RIGHT, - onClose: () => { - this.commandSelectorOpen = false; - }, - children: [ - this.getQuickCommandActions(this.quickActionCommands) - ], - }); + } else if ( + (this.selectedCommand === '' && e.key === KeyMap.SLASH && this.promptTextInput.getTextInputValue() === '') || + (e.key === KeyMap.AT) + ) { + this.quickPickType = e.key === KeyMap.AT ? 'context' : 'quick-action'; + this.quickPickItemGroups = MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId).getValue(this.quickPickType === 'context' ? 'contextCommands' : 'quickActionCommands') as QuickActionCommandGroup[]; + this.quickPickTriggerIndex = this.quickPickType === 'context' ? this.promptTextInput.getCursorPos() : 1; + this.textAfter = this.promptTextInput.getTextInputValue().substring(this.quickPickTriggerIndex); + + if (this.quickPickItemGroups.length > 0) { + this.filteredQuickPickItemGroups = [ ...this.quickPickItemGroups ]; + this.quickPick = new Overlay({ + closeOnOutsideClick: true, + referenceElement: this.render.querySelector('.mynah-chat-prompt') as ExtendedHTMLElement, + dimOutside: false, + stretchWidth: true, + verticalDirection: OverlayVerticalDirection.TO_TOP, + horizontalDirection: OverlayHorizontalDirection.START_TO_RIGHT, + onClose: () => { + this.quickPickOpen = false; + }, + children: [ + this.getQuickPickItemGroups(this.filteredQuickPickItemGroups) + ], + }); - this.commandSelectorOpen = true; + this.quickPickOpen = true; + } } } else { - const blockedKeys = [ KeyMap.ENTER, KeyMap.ESCAPE, KeyMap.SPACE, KeyMap.TAB, KeyMap.BACK_SLASH, KeyMap.SLASH ] as string[]; + const blockedKeys = [ KeyMap.ENTER, KeyMap.ESCAPE, KeyMap.SPACE, KeyMap.TAB, KeyMap.AT, KeyMap.BACK_SLASH, KeyMap.SLASH ] as string[]; const navigationalKeys = [ KeyMap.ARROW_UP, KeyMap.ARROW_DOWN ] as string[]; if (blockedKeys.includes(e.key)) { e.preventDefault(); - if (e.key === KeyMap.ENTER || e.key === KeyMap.TAB || e.key === KeyMap.SPACE) { + if (e.key === KeyMap.ESCAPE) { + if (this.quickPickType === 'quick-action') { + this.clearTextArea(true); + } else { + this.promptTextInput.updateTextInputValue(`${this.promptTextInput.getTextInputValue().substring(0, this.quickPickTriggerIndex)}${this.textAfter}`); + this.promptTextInput.focus(this.quickPickTriggerIndex); + } + this.quickPick?.close(); + } else if (e.key === KeyMap.ENTER || e.key === KeyMap.TAB || e.key === KeyMap.SPACE) { let targetElement; - if (this.filteredCommandsList.length > 0) { - // If list is empty, it means there's no match, so we need to clear the selection - if (this.commandSelector.render.querySelector('.target-command') != null) { - targetElement = this.commandSelector.render.querySelector('.target-command'); - } else if (this.commandSelector.render.querySelector('.mynah-chat-command-selector-command')?.getAttribute('disabled') !== 'true') { - targetElement = this.commandSelector.render.querySelector('.mynah-chat-command-selector-command'); + // If list is empty, it means there's no match, so we need to clear the selection + if (this.filteredQuickPickItemGroups.length > 0) { + if (this.quickPick.render.querySelector('.target-command') != null) { + targetElement = this.quickPick.render.querySelector('.target-command'); + } else if (this.quickPick.render.querySelector('.mynah-chat-command-selector-command')?.getAttribute('disabled') !== 'true') { + targetElement = this.quickPick.render.querySelector('.mynah-chat-command-selector-command'); } } - this.handleCommandSelection({ + const commandToSend = { command: targetElement?.getAttribute('command') ?? '', placeholder: targetElement?.getAttribute('placeholder') ?? undefined, - }); - } - if (this.commandSelector !== undefined) { - if (e.key === KeyMap.ESCAPE) { - this.clearTextArea(true); + }; + if (this.quickPickType === 'context') { + this.handleContextCommandSelection(commandToSend); + } else { + this.handleQuickActionCommandSelection(commandToSend); } - this.commandSelector.close(); } } else if (navigationalKeys.includes(e.key)) { e.preventDefault(); - const commandsWrapper = this.commandSelector.render.querySelector('.mynah-chat-command-selector'); + const commandsWrapper = this.quickPick.render.querySelector('.mynah-chat-command-selector'); (commandsWrapper as ExtendedHTMLElement).addClass('has-target-item'); - const commandElements = Array.from(this.commandSelector.render.querySelectorAll('.mynah-chat-command-selector-command')); + const commandElements = Array.from(this.quickPick.render.querySelectorAll('.mynah-chat-command-selector-command')); let lastActiveElement = commandElements.findIndex(commandElement => commandElement.classList.contains('target-command')); lastActiveElement = lastActiveElement === -1 ? commandElements.length : lastActiveElement; let nextElementIndex: number = lastActiveElement; @@ -242,32 +268,33 @@ export class ChatPromptInput { } } } else { - if (this.commandSelector !== undefined) { + if (this.quickPick != null) { setTimeout(() => { if (this.promptTextInput.getTextInputValue() === '') { - this.commandSelector.close(); + this.quickPick.close(); } else { - this.filteredCommandsList = []; - [ ...this.quickActionCommands ].forEach((quickActionGroup: QuickActionCommandGroup) => { - const newQuickActionCommandGroup = { ...quickActionGroup }; + this.filteredQuickPickItemGroups = []; + [ ...this.quickPickItemGroups ].forEach((quickPickGroup: QuickActionCommandGroup) => { + const newQuickPickCommandGroup = { ...quickPickGroup }; try { - const promptRegex = new RegExp(`${this.promptTextInput.getTextInputValue().substring(1)}` ?? '', 'gi'); - newQuickActionCommandGroup.commands = newQuickActionCommandGroup.commands.filter(command => + const searchTerm = this.promptTextInput.getTextInputValue().substring(this.quickPickTriggerIndex).match(/\S*/gi)?.[0]; + const promptRegex = new RegExp(searchTerm ?? '', 'gi'); + newQuickPickCommandGroup.commands = newQuickPickCommandGroup.commands.filter(command => command.command.match(promptRegex) ); - if (newQuickActionCommandGroup.commands.length > 0) { - this.filteredCommandsList.push(newQuickActionCommandGroup); + if (newQuickPickCommandGroup.commands.length > 0) { + this.filteredQuickPickItemGroups.push(newQuickPickCommandGroup); } } catch (e) { // In case the prompt is an incomplete regex } }); - if (this.filteredCommandsList.length > 0) { - this.commandSelector.toggleHidden(false); - this.commandSelector.updateContent([ this.getQuickCommandActions(this.filteredCommandsList) ]); + if (this.filteredQuickPickItemGroups.length > 0) { + this.quickPick.toggleHidden(false); + this.quickPick.updateContent([ this.getQuickPickItemGroups(this.filteredQuickPickItemGroups) ]); } else { // If there's no matching action, hide the command selector overlay - this.commandSelector.toggleHidden(true); + this.quickPick.toggleHidden(true); } } }, 1); @@ -276,33 +303,37 @@ export class ChatPromptInput { } }; - private readonly getQuickCommandActions = (quickCommandList: QuickActionCommandGroup[]): ExtendedHTMLElement => { + private readonly getQuickPickItemGroups = (quickPickGroupList: QuickActionCommandGroup[]): ExtendedHTMLElement => { return DomBuilder.getInstance().build({ type: 'div', classNames: [ 'mynah-chat-command-selector' ], - children: quickCommandList.map((quickActionCommandGroup) => { + children: quickPickGroupList.map((quickPickGroup) => { return DomBuilder.getInstance().build({ type: 'div', classNames: [ 'mynah-chat-command-selector-group' ], children: [ - ...(quickActionCommandGroup.groupName !== undefined + ...(quickPickGroup.groupName !== undefined ? [ DomBuilder.getInstance().build({ type: 'h4', classNames: [ 'mynah-chat-command-selector-group-title' ], - children: [ quickActionCommandGroup.groupName ] + children: [ quickPickGroup.groupName ] }) ] : []), - ...(quickActionCommandGroup.commands.map(quickActionCommand => { + ...(quickPickGroup.commands.map(quickPickCommand => { return DomBuilder.getInstance().build({ type: 'div', classNames: [ 'mynah-chat-command-selector-command' ], attributes: { - ...quickActionCommand + ...quickPickCommand }, events: { click: () => { - if (quickActionCommand.disabled !== true) { - this.handleCommandSelection(quickActionCommand); + if (quickPickCommand.disabled !== true) { + if (this.quickPickType === 'context') { + this.handleContextCommandSelection(quickPickCommand); + } else { + this.handleQuickActionCommandSelection(quickPickCommand); + } } } }, @@ -310,13 +341,13 @@ export class ChatPromptInput { { type: 'div', classNames: [ 'mynah-chat-command-selector-command-name' ], - children: [ quickActionCommand.command ] + children: [ quickPickCommand.command ] }, - ...(quickActionCommand.description !== undefined + ...(quickPickCommand.description !== undefined ? [ { type: 'div', classNames: [ 'mynah-chat-command-selector-command-description' ], - children: [ quickActionCommand.description ] + children: [ quickPickCommand.description ] } ] : []) ] @@ -328,7 +359,7 @@ export class ChatPromptInput { }); }; - private readonly handleCommandSelection = (quickActionCommand: QuickActionCommand): void => { + private readonly handleQuickActionCommandSelection = (quickActionCommand: QuickActionCommand): void => { this.selectedCommand = quickActionCommand.command; this.promptTextInput.updateTextInputValue(''); if (quickActionCommand.placeholder !== undefined) { @@ -337,12 +368,22 @@ export class ChatPromptInput { } else { this.sendPrompt(); } - this.commandSelector.close(); + this.quickPick.close(); if (Config.getInstance().config.autoFocus) { this.promptTextInput.focus(); } }; + private readonly handleContextCommandSelection = (contextCommand: QuickActionCommand): void => { + const previousText = this.promptTextInput.getTextInputValue().substring(0, this.quickPickTriggerIndex); + this.promptTextInput.updateTextInputValue(`${previousText}${contextCommand.command} ${this.textAfter}`, { + index: this.quickPickTriggerIndex + contextCommand.command.length, + text: contextCommand.placeholder + }); + this.quickPick.close(); + this.promptTextInput.focus(this.quickPickTriggerIndex + contextCommand.command.length + 1); + }; + public readonly clearTextArea = (keepAttachment?: boolean): void => { this.selectedCommand = ''; this.promptTextInput.clear(); @@ -368,11 +409,19 @@ export class ChatPromptInput { if (currentInputValue.trim() !== '' || this.selectedCommand.trim() !== '') { const attachmentContent: string | undefined = this.promptAttachment?.lastAttachmentContent; const promptText = currentInputValue + (attachmentContent ?? ''); - const promptData = { + const context: string[] = []; + const escapedPrompt = escapeHTML(promptText.replace(/@\S*/gi, (match) => { + if (!context.includes(match)) { + context.push(match); + } + return `**${match}**`; + })); + const promptData: {tabId: string; prompt: ChatPrompt} = { tabId: this.props.tabId, prompt: { prompt: promptText, - escapedPrompt: escapeHTML(promptText), + escapedPrompt, + context, ...(this.selectedCommand !== '' ? { command: this.selectedCommand } : {}), } }; diff --git a/src/components/chat-item/prompt-input/prompt-text-input.ts b/src/components/chat-item/prompt-input/prompt-text-input.ts index ce84b6ad..868c941f 100644 --- a/src/components/chat-item/prompt-input/prompt-text-input.ts +++ b/src/components/chat-item/prompt-input/prompt-text-input.ts @@ -112,13 +112,53 @@ export class PromptTextInput { }); } - private readonly updatePromptTextInputSizer = (): void => { + private readonly updatePromptTextInputSizer = (placeHolder?: { + index?: number; + text?: string; + }): void => { if (this.promptTextInput.value.trim() !== '') { this.render.removeClass('no-text'); } else { this.render.addClass('no-text'); } - this.promptTextInputSizer.innerHTML = this.promptTextInput.value.replace(/\n/g, '
 '); + let initProcessedValue = this.promptTextInput.value; + if (placeHolder?.text != null) { + initProcessedValue = `${initProcessedValue.substring(0, placeHolder.index ?? initProcessedValue.length)} ${placeHolder.text} ${initProcessedValue.substring((placeHolder.index ?? initProcessedValue.length) + 1)}`; + } + this.promptTextInputSizer.innerHTML = `${initProcessedValue.replace(/\n/g, '
').replace(/@\S*/gi, (match) => `${match}`)} `; + }; + + public readonly getCursorPos = (): number => { + return this.promptTextInput.selectionStart ?? this.promptTextInput.value.length; + }; + + public readonly getWordAndIndexOnCursorPos = (): { wordStartIndex: number; word: string } => { + const currentValue = this.promptTextInput.value; + const cursorPos = this.getCursorPos(); + let prevSpaceIndex = -1; + let nextSpaceIndex = currentValue.indexOf(' ', cursorPos); + + // We're not splitting the text value by spaces to get the words and check all of them + // Reason behind that is performance concerns. + // We know that we're looking for a word, and we only need the word for the given index if it is inside a word + + // Find previous space chararacter + for (let i = cursorPos - 1; i >= 0; i--) { + if (currentValue[i] === ' ') { + prevSpaceIndex = i; + break; + } + } + + // Find next space character + if (nextSpaceIndex === -1) { + nextSpaceIndex = currentValue.length; + } + + return { + wordStartIndex: prevSpaceIndex + 1, + word: currentValue.substring(prevSpaceIndex + 1, nextSpaceIndex) + }; }; public readonly clear = (): void => { @@ -129,19 +169,27 @@ export class PromptTextInput { this.render.addClass('no-text'); }; - public readonly focus = (): void => { + public readonly focus = (cursorIndex?: number): void => { if (Config.getInstance().config.autoFocus) { this.promptTextInput.focus(); } - this.updateTextInputValue(''); + if (cursorIndex != null) { + this.promptTextInput.setSelectionRange(cursorIndex, cursorIndex); + } else { + this.updateTextInputValue(''); + } }; public readonly getTextInputValue = (): string => { return this.promptTextInput.value; }; - public readonly updateTextInputValue = (value: string): void => { + public readonly updateTextInputValue = (value: string, placeHolder?: { + index?: number; + text?: string; + }): void => { this.promptTextInput.value = value; + this.updatePromptTextInputSizer(placeHolder); }; public readonly updateTextInputMaxLength = (maxLength: number): void => { diff --git a/src/helper/store.ts b/src/helper/store.ts index 315c91ed..bc2a5525 100644 --- a/src/helper/store.ts +++ b/src/helper/store.ts @@ -22,6 +22,7 @@ export class EmptyMynahUIDataModel { cancelButtonWhenLoading: true, showChatAvatars: false, quickActionCommands: [], + contextCommands: [], promptInputPlaceholder: '', promptInputInfo: '', promptInputStickyCard: null, diff --git a/src/static.ts b/src/static.ts index 429e7321..91c1cecd 100644 --- a/src/static.ts +++ b/src/static.ts @@ -53,6 +53,10 @@ export interface MynahUIDataModel { */ quickActionCommands?: QuickActionCommandGroup[]; /** + * Context commands to show when user hits @ to the input any point + */ + contextCommands?: QuickActionCommandGroup[]; + /** * Placeholder to be shown on prompt input */ promptInputPlaceholder?: string; @@ -217,6 +221,7 @@ export interface ChatPrompt { prompt?: string; escapedPrompt?: string; command?: string; + context?: string[]; } export interface ChatItemAction extends ChatPrompt { @@ -279,6 +284,7 @@ export enum KeyMap { SHIFT = 'Shift', CONTROL = 'Control', ALT = 'Alt', + AT = '@', SLASH = '/', BACK_SLASH = '\\' } diff --git a/src/styles/components/chat/_chat-prompt-wrapper.scss b/src/styles/components/chat/_chat-prompt-wrapper.scss index a8406d4d..265bdd44 100644 --- a/src/styles/components/chat/_chat-prompt-wrapper.scss +++ b/src/styles/components/chat/_chat-prompt-wrapper.scss @@ -60,7 +60,8 @@ resize: none; background-color: rgba(0, 0, 0, 0); font-size: var(--mynah-font-size-large); - color: var(--mynah-color-text-input); + color: rgba(0, 0, 0, 0); + caret-color: var(--mynah-color-text-input); outline: none; width: 100%; max-height: 20vh; @@ -86,18 +87,42 @@ bottom: 0; left: 0; padding: 0; + z-index: 100; } } >.mynah-chat-prompt-input-sizer { display: block; width: 100%; - opacity: 0; - visibility: hidden; + opacity: 1; pointer-events: none; overflow: hidden; white-space: pre-wrap; word-break: break-word; + color: var(--mynah-color-text-input); + position: relative; + z-index: 50; + > span.context { + position: relative; + color: var(--mynah-color-button-reverse); + border-radius: calc(var(--mynah-input-radius)/2); + display: inline-block; + &:before{ + content: ""; + position: absolute; + left: -1px; + right: -1px; + width: auto; + height: 100%; + background-color: var(--mynah-color-button); + border-radius: inherit; + z-index: -1; + } + } + > span.placeholder { + position: relative; + color: var(--mynah-color-text-weak); + } } &~.mynah-chat-prompt-button {