src/main.ts (879 lines of code) (raw):
/*!
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { Notification } from './components/notification';
import { DomBuilder, ExtendedHTMLElement } from './helper/dom';
import {
MynahPortalNames,
RelevancyVoteType,
FeedbackPayload,
MynahUIDataModel,
MynahEventNames,
NotificationType,
ChatItem,
ChatItemAction,
ChatPrompt,
MynahUITabStoreModel,
MynahUITabStoreTab,
ConfigModel,
ReferenceTrackerInformation,
CodeSelectionType,
Engagement,
ChatItemFormItem,
ChatItemButton,
ChatItemType,
CardRenderDetails,
PromptAttachmentType,
QuickActionCommand,
DetailedList,
TreeNodeDetails,
} from './static';
import { MynahUIGlobalEvents } from './helper/events';
import { Tabs } from './components/navigation-tabs';
import { ChatWrapper } from './components/chat-item/chat-wrapper';
import { FeedbackForm } from './components/feedback-form/feedback-form';
import { MynahUITabsStore } from './helper/tabs-store';
import { Config } from './helper/config';
import { generateUID } from './helper/guid';
import { NoTabs } from './components/no-tabs';
import { copyToClipboard } from './helper/chat-item';
import { Spinner } from './components/spinner/spinner';
import { serializeHtml, serializeMarkdown } from './helper/serialize-chat';
import { Sheet } from './components/sheet';
import { DetailedListSheet, DetailedListSheetProps } from './components/detailed-list/detailed-list-sheet';
import { configureMarked, parseMarkdown } from './helper/marked';
import { MynahUIDataStore } from './helper/store';
import { StyleLoader } from './helper/style-loader';
export { generateUID } from './helper/guid';
export {
ChatItemBodyRenderer,
} from './helper/dom';
export {
AllowedAttributesInCustomRenderer,
AllowedTagsInCustomRenderer
} from './helper/sanitize';
export * from './static';
export {
ToggleOption
} from './components/tabs';
export {
MynahIcons,
MynahIconsType
} from './components/icon';
export {
DomBuilder,
DomBuilderObject,
ExtendedHTMLElement,
} from './helper/dom';
export {
ButtonProps,
ButtonAbstract
} from './components/button';
export {
RadioGroupProps,
RadioGroupAbstract
} from './components/form-items/radio-group';
export {
SelectProps,
SelectAbstract
} from './components/form-items/select';
export {
TextInputProps,
TextInputAbstract
} from './components/form-items/text-input';
export {
TextAreaProps,
TextAreaAbstract
} from './components/form-items/text-area';
export {
ChatItemCardContent,
ChatItemCardContentProps
} from './components/chat-item/chat-item-card-content';
export { default as MynahUITestIds } from './helper/test-ids';
export interface MynahUIProps {
rootSelector?: string;
loadStyles?: boolean;
defaults?: MynahUITabStoreTab;
splashScreenInitialStatus?: {
visible: boolean;
text: string;
};
tabs?: MynahUITabStoreModel;
config?: Partial<ConfigModel>;
onShowMoreWebResultsClick?: (
tabId: string,
messageId: string,
eventId?: string) => void;
onReady?: () => void;
onFocusStateChanged?: (focusState: boolean) => void;
onVote?: (
tabId: string,
messageId: string,
vote: RelevancyVoteType,
eventId?: string) => void;
onStopChatResponse?: (
tabId: string,
eventId?: string) => void;
onResetStore?: (tabId: string) => void;
onChatPrompt?: (
tabId: string,
prompt: ChatPrompt,
eventId?: string) => void;
onChatPromptProgressActionButtonClicked?: (
tabId: string,
action: {
id: string;
text?: string;
},
eventId?: string) => void;
onFollowUpClicked?: (
tabId: string,
messageId: string,
followUp: ChatItemAction,
eventId?: string) => void;
onInBodyButtonClicked?: (
tabId: string,
messageId: string,
action: {
id: string;
text?: string;
formItemValues?: Record<string, string>;
},
eventId?: string) => void;
onTabbedContentTabChange?: (
tabId: string,
messageId: string,
contentTabId: string,
eventId?: string) => void;
onTabChange?: (
tabId: string,
eventId?: string) => void;
onTabAdd?: (
tabId: string,
eventId?: string) => void;
onContextSelected?: (
contextItem: QuickActionCommand,
tabId: string,
eventId?: string
) => boolean;
onTabRemove?: (
tabId: string,
eventId?: string) => void;
/**
* @param tabId tabId which the close button triggered
* @returns boolean -> If you want to close the tab immediately send true
*/
onBeforeTabRemove?: (
tabId: string,
eventId?: string) => boolean;
onChatItemEngagement?: (
tabId: string,
messageId: string,
engagement: Engagement) => void;
onCodeBlockActionClicked?: (
tabId: string,
messageId: string,
actionId: string,
data?: string,
code?: string,
type?: CodeSelectionType,
referenceTrackerInformation?: ReferenceTrackerInformation[],
eventId?: string,
codeBlockIndex?: number,
totalCodeBlocks?: number,) => void;
/**
* @deprecated since version 4.14.0. It will be only used for keyboard, context menu copy actions, not for button actions after version 5.x.x. Use {@link onCodeBlockActionClicked} instead
*/
onCopyCodeToClipboard?: (
tabId: string,
messageId: string,
code?: string,
type?: CodeSelectionType,
referenceTrackerInformation?: ReferenceTrackerInformation[],
eventId?: string,
codeBlockIndex?: number,
totalCodeBlocks?: number,
data?: any) => void;
/**
* @deprecated since version 4.14.0. Will be dropped after version 5.x.x. Use {@link onCodeBlockActionClicked} instead
*/
onCodeInsertToCursorPosition?: (
tabId: string,
messageId: string,
code?: string,
type?: CodeSelectionType,
referenceTrackerInformation?: ReferenceTrackerInformation[],
eventId?: string,
codeBlockIndex?: number,
totalCodeBlocks?: number,
data?: any) => void;
onSourceLinkClick?: (
tabId: string,
messageId: string,
link: string,
mouseEvent?: MouseEvent,
eventId?: string) => void;
onLinkClick?: (
tabId: string,
messageId: string,
link: string,
mouseEvent?: MouseEvent,
eventId?: string) => void;
onInfoLinkClick?: (
tabId: string,
link: string,
mouseEvent?: MouseEvent,
eventId?: string) => void;
onFormLinkClick?: (
link: string,
mouseEvent?: MouseEvent,
eventId?: string) => void;
onSendFeedback?: (
tabId: string,
feedbackPayload: FeedbackPayload,
eventId?: string) => void;
onFormModifierEnterPress?: (
formData: Record<string, string>,
tabId: string,
eventId?: string) => void;
onFormTextualItemKeyPress?: (
event: KeyboardEvent,
formData: Record<string, string>,
itemId: string,
tabId: string,
eventId?: string) => boolean;
onFormChange?: (
formData: Record<string, any>,
isValid: boolean,
tabId: string) => void;
onCustomFormAction?: (
tabId: string,
action: {
id: string;
text?: string;
formItemValues?: Record<string, string>;
},
eventId?: string) => void;
onPromptInputOptionChange?: (
tabId: string,
optionsValues: Record<string, string>,
eventId?: string) => void;
onPromptInputButtonClick?: (
tabId: string,
buttonId: string,
eventId?: string) => void;
/**
* @deprecated since version 4.6.3. Will be dropped after version 5.x.x. Use {@link onFileClick} instead
*/
onOpenDiff?: (
tabId: string,
filePath: string,
deleted: boolean,
messageId?: string,
eventId?: string) => void;
onFileClick?: (
tabId: string,
filePath: string,
deleted: boolean,
messageId?: string,
eventId?: string,
fileDetails?: TreeNodeDetails
) => void;
onMessageDismiss?: (
tabId: string,
messageId: string,
eventId?: string) => void;
onFileActionClick?: (
tabId: string,
messageId: string,
filePath: string,
actionName: string,
eventId?: string) => void;
onTabBarButtonClick?: (
tabId: string,
buttonId: string,
eventId?: string) => void;
onQuickCommandGroupActionClick?: (
tabId: string,
action: {
id: string;
},
eventId?: string) => void;
}
export class MynahUI {
private readonly render: ExtendedHTMLElement;
private lastEventId: string = '';
private readonly props: MynahUIProps;
private readonly splashLoader: ExtendedHTMLElement;
private readonly splashLoaderText: ExtendedHTMLElement;
private readonly tabsWrapper: ExtendedHTMLElement;
private readonly tabContentsWrapper: ExtendedHTMLElement;
private readonly feedbackForm?: FeedbackForm;
private readonly sheet?: Sheet;
private readonly chatWrappers: Record<string, ChatWrapper> = {};
constructor (props: MynahUIProps) {
StyleLoader.getInstance(props.loadStyles !== false).load('styles.scss');
configureMarked();
this.props = props;
Config.getInstance(props.config);
DomBuilder.getInstance(props.rootSelector);
MynahUITabsStore.getInstance(this.props.tabs, this.props.defaults);
MynahUIGlobalEvents.getInstance();
this.splashLoaderText = DomBuilder.getInstance().build({
type: 'div',
classNames: [ 'mynah-ui-splash-loader-text' ],
innerHTML: parseMarkdown(this.props.splashScreenInitialStatus?.text ?? '', { includeLineBreaks: true }),
});
this.splashLoader = DomBuilder.getInstance().build({
type: 'div',
classNames: [ 'mynah-ui-splash-loader-wrapper' ],
children: [
new Spinner().render,
this.splashLoaderText
]
});
if (this.props.splashScreenInitialStatus?.visible === true) {
this.splashLoader.addClass('visible');
}
const initTabs = MynahUITabsStore.getInstance().getAllTabs();
this.tabContentsWrapper = DomBuilder.getInstance().build({
type: 'div',
classNames: [ 'mynah-ui-tab-contents-wrapper' ],
children: Object.keys(initTabs).slice(0, Config.getInstance().config.maxTabs).map((tabId: string) => {
this.chatWrappers[tabId] = new ChatWrapper({
tabId,
onStopChatResponse: props.onStopChatResponse != null
? (tabId) => {
if (props.onStopChatResponse != null) {
props.onStopChatResponse(tabId, this.getUserEventId());
}
}
: undefined,
});
return this.chatWrappers[tabId].render;
})
});
if (props.onSendFeedback !== undefined) {
this.feedbackForm = new FeedbackForm();
}
this.sheet = new Sheet();
if (Config.getInstance().config.maxTabs > 1) {
this.tabsWrapper = new Tabs({
onChange: (selectedTabId: string) => {
this.focusToInput(selectedTabId);
if (this.props.onTabChange !== undefined) {
this.props.onTabChange(selectedTabId, this.getUserEventId());
}
},
maxTabsTooltipDuration: Config.getInstance().config.maxTabsTooltipDuration,
noMoreTabsTooltip: Config.getInstance().config.noMoreTabsTooltip,
onBeforeTabRemove: (tabId): boolean => {
if (props.onBeforeTabRemove !== undefined) {
return props.onBeforeTabRemove(tabId, this.getUserEventId());
}
return true;
}
}).render;
this.tabsWrapper.setAttribute('selected-tab', MynahUITabsStore.getInstance().getSelectedTabId());
}
this.render = DomBuilder.getInstance().createPortal(
MynahPortalNames.WRAPPER,
{
type: 'div',
attributes: {
id: 'mynah-wrapper'
},
children: [
this.tabsWrapper ?? '',
...(Config.getInstance().config.maxTabs > 1 ? [ new NoTabs().render ] : []),
this.tabContentsWrapper,
this.splashLoader
]
},
'afterbegin'
);
MynahUITabsStore.getInstance().addListener('add', (tabId: string) => {
this.chatWrappers[tabId] = new ChatWrapper({
tabId,
onStopChatResponse: props.onStopChatResponse != null
? (tabId) => {
if (props.onStopChatResponse != null) {
props.onStopChatResponse(tabId, this.getUserEventId());
}
}
: undefined,
});
this.tabContentsWrapper.appendChild(this.chatWrappers[tabId].render);
this.focusToInput(tabId);
if (this.props.onTabAdd !== undefined) {
this.props.onTabAdd(tabId, this.getUserEventId());
}
});
MynahUITabsStore.getInstance().addListener('remove', (tabId: string) => {
this.chatWrappers[tabId].render.remove();
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.chatWrappers[tabId];
if (this.props.onTabRemove !== undefined) {
this.props.onTabRemove(tabId, this.getUserEventId());
}
});
this.addGlobalListeners();
const tabId = MynahUITabsStore.getInstance().getSelectedTabId() ?? '';
window.addEventListener('focus', () => {
this.focusToInput(tabId);
}, false);
this.focusToInput(tabId);
if (this.props.onReady !== undefined) {
this.props.onReady();
}
}
private readonly getUserEventId = (): string => {
this.lastEventId = generateUID();
return this.lastEventId;
};
private readonly focusToInput = (tabId: string): void => {
if (Config.getInstance().config.autoFocus) {
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.TAB_FOCUS, { tabId });
}
};
private readonly addGlobalListeners = (): void => {
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CHAT_PROMPT, (data: { tabId: string; prompt: ChatPrompt }) => {
if (this.props.onChatPrompt !== undefined) {
this.props.onChatPrompt(data.tabId, data.prompt, this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FOLLOW_UP_CLICKED, (data: {
tabId: string;
messageId: string;
followUpOption: ChatItemAction;
}) => {
if (this.props.onFollowUpClicked !== undefined) {
this.props.onFollowUpClicked(
data.tabId,
data.messageId,
data.followUpOption,
this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CONTEXT_SELECTED, (data: {
contextItem: QuickActionCommand;
tabId: string;
promptInputCallback: (insert: boolean) => void;
}) => {
data.promptInputCallback(this.props.onContextSelected === undefined || this.props.onContextSelected(data.contextItem, data.tabId, this.getUserEventId()));
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FORM_MODIFIER_ENTER_PRESS, (data: {
formData: Record<string, string>;
tabId: string;
}) => {
if (this.props.onFormModifierEnterPress !== undefined) {
this.props.onFormModifierEnterPress(data.formData, data.tabId, this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FORM_TEXTUAL_ITEM_KEYPRESS, (data: {
event: KeyboardEvent;
formData: Record<string, string>;
itemId: string;
tabId: string;
callback: (disableAll?: boolean) => void;
}) => {
if (this.props.onFormTextualItemKeyPress !== undefined) {
data.callback(this.props.onFormTextualItemKeyPress(data.event, data.formData, data.itemId, data.tabId, this.getUserEventId()));
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FORM_CHANGE, (data: {
formData: Record<string, string>;
isValid: boolean;
tabId: string;
}) => {
if (this.props.onFormChange !== undefined) {
this.props.onFormChange(data.formData, data.isValid, data.tabId);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.BODY_ACTION_CLICKED, (data: {
tabId: string;
messageId: string;
actionId: string;
actionText?: string;
formItemValues?: Record<string, string>;
}) => {
if (this.props.onInBodyButtonClicked !== undefined) {
this.props.onInBodyButtonClicked(data.tabId, data.messageId, {
id: data.actionId,
text: data.actionText,
formItemValues: data.formItemValues
}, this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.QUICK_COMMAND_GROUP_ACTION_CLICK, (data: {
tabId: string;
actionId: string;
}) => {
if (this.props.onQuickCommandGroupActionClick !== undefined) {
this.props.onQuickCommandGroupActionClick(data.tabId, {
id: data.actionId,
}, this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.TABBED_CONTENT_SWITCH, (data: {
tabId: string;
messageId: string;
contentTabId: string;
}) => {
if (this.props.onTabbedContentTabChange != null) {
this.props.onTabbedContentTabChange(data.tabId, data.messageId, data.contentTabId);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.PROMPT_PROGRESS_ACTION_CLICK, (data: {
tabId: string;
actionId: string;
actionText?: string;
}) => {
if (this.props.onChatPromptProgressActionButtonClicked !== undefined) {
this.props.onChatPromptProgressActionButtonClicked(data.tabId, {
id: data.actionId,
text: data.actionText
}, this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.SHOW_MORE_WEB_RESULTS_CLICK, (data: { messageId: string }) => {
if (this.props.onShowMoreWebResultsClick !== undefined) {
this.props.onShowMoreWebResultsClick(
MynahUITabsStore.getInstance().getSelectedTabId(),
data.messageId,
this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FEEDBACK_SET, (feedbackData) => {
if (this.props.onSendFeedback !== undefined) {
this.props.onSendFeedback(
MynahUITabsStore.getInstance().getSelectedTabId(),
feedbackData,
this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CHAT_ITEM_ENGAGEMENT, (data: { engagement: Engagement; messageId: string }) => {
if (this.props.onChatItemEngagement !== undefined) {
this.props.onChatItemEngagement(MynahUITabsStore.getInstance().getSelectedTabId(), data.messageId, data.engagement);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CODE_BLOCK_ACTION, (data) => {
// TODO needs to be deprecated and followed through onCodeBlockActionClicked
if (data.actionId === 'insert-to-cursor') {
this.props.onCodeInsertToCursorPosition?.(
MynahUITabsStore.getInstance().getSelectedTabId(),
data.messageId,
data.text,
data.type,
data.referenceTrackerInformation,
this.getUserEventId(),
data.codeBlockIndex,
data.totalCodeBlocks,
);
}
// TODO needs to be deprecated and followed through onCodeBlockActionClicked
if (data.actionId === 'copy') {
copyToClipboard(data.text, (): void => {
this.props.onCopyCodeToClipboard?.(
MynahUITabsStore.getInstance().getSelectedTabId(),
data.messageId,
data.text,
data.type,
data.referenceTrackerInformation,
this.getUserEventId(),
data.codeBlockIndex,
data.totalCodeBlocks,
);
});
}
this.props.onCodeBlockActionClicked?.(
MynahUITabsStore.getInstance().getSelectedTabId(),
data.messageId,
data.actionId,
data.data,
data.text,
data.type,
data.referenceTrackerInformation,
this.getUserEventId(),
data.codeBlockIndex,
data.totalCodeBlocks,
);
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.COPY_CODE_TO_CLIPBOARD, (data) => {
if (this.props.onCopyCodeToClipboard !== undefined) {
this.props.onCopyCodeToClipboard(
MynahUITabsStore.getInstance().getSelectedTabId(),
data.messageId,
data.text,
data.type,
data.referenceTrackerInformation,
this.getUserEventId(),
data.codeBlockIndex,
data.totalCodeBlocks,
);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.SOURCE_LINK_CLICK, (data) => {
if (this.props.onSourceLinkClick !== undefined) {
this.props.onSourceLinkClick(
MynahUITabsStore.getInstance().getSelectedTabId(),
data.messageId,
data.link,
data.event,
this.getUserEventId()
);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.LINK_CLICK, (data) => {
if (this.props.onLinkClick !== undefined) {
this.props.onLinkClick(
MynahUITabsStore.getInstance().getSelectedTabId(),
data.messageId,
data.link,
data.event,
this.getUserEventId()
);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FORM_LINK_CLICK, (data) => {
if (this.props.onFormLinkClick !== undefined) {
this.props.onFormLinkClick(
data.link,
data.event,
this.getUserEventId()
);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.INFO_LINK_CLICK, (data) => {
if (this.props.onInfoLinkClick !== undefined) {
this.props.onInfoLinkClick(
MynahUITabsStore.getInstance().getSelectedTabId(),
data.link,
data.event,
this.getUserEventId()
);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CARD_VOTE, (data) => {
if (this.props.onVote !== undefined) {
this.props.onVote(
data.tabId,
data.messageId,
data.vote,
this.getUserEventId()
);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.RESET_STORE, (data: { tabId: string }) => {
if (this.props.onResetStore !== undefined) {
this.props.onResetStore(data.tabId);
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.ROOT_FOCUS, (data: { focusState: boolean }) => {
this.props.onFocusStateChanged?.(data.focusState);
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FILE_CLICK, (data) => {
if (this.props.onFileClick !== undefined) {
this.props.onFileClick(
data.tabId,
data.filePath,
data.deleted,
data.messageId,
this.getUserEventId(),
data.fileDetails);
}
if (this.props.onOpenDiff !== undefined) {
console.warn('onOpenDiff will be deprecated after v5.x.x. Please use MynahUIProps.onFileClick instead');
this.props.onOpenDiff(
data.tabId,
data.filePath,
data.deleted,
data.messageId,
this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CARD_DISMISS, (data) => {
this.props.onMessageDismiss?.(
data.tabId,
data.messageId,
this.getUserEventId());
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FILE_ACTION_CLICK, (data) => {
if (this.props.onFileActionClick !== undefined) {
this.props.onFileActionClick(
data.tabId,
data.messageId,
data.filePath,
data.actionName,
this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CUSTOM_FORM_ACTION_CLICK, (data) => {
if (this.props.onCustomFormAction !== undefined) {
this.props.onCustomFormAction(data.tabId, data, this.getUserEventId());
}
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.PROMPT_INPUT_OPTIONS_CHANGE, (data) => {
this.props.onPromptInputOptionChange?.(data.tabId, data.optionsValues, this.getUserEventId());
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.PROMPT_INPUT_BUTTON_CLICK, (data) => {
this.props.onPromptInputButtonClick?.(data.tabId, data.buttonId, this.getUserEventId());
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.TAB_BAR_BUTTON_CLICK, (data) => {
if (this.props.onTabBarButtonClick !== undefined) {
this.props.onTabBarButtonClick(data.tabId, data.buttonId, this.getUserEventId());
}
});
};
public addToUserPrompt = (tabId: string, attachmentContent: string, type?: PromptAttachmentType): void => {
if (Config.getInstance().config.showPromptField && MynahUITabsStore.getInstance().getTab(tabId) !== null) {
this.chatWrappers[tabId].addAttachmentToPrompt(attachmentContent, type);
}
};
/**
* Adds a new item to the chat window
* @param tabId Corresponding tab ID.
* @param answer ChatItem object.
*/
public addChatItem = (tabId: string, chatItem: ChatItem): void => {
if (MynahUITabsStore.getInstance().getTab(tabId) !== null) {
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.CHAT_ITEM_ADD, { tabId, chatItem });
MynahUITabsStore.getInstance().getTabDataStore(tabId).updateStore({
chatItems: [
...MynahUITabsStore.getInstance().getTabDataStore(tabId).getValue('chatItems'),
chatItem
]
});
}
};
/**
* Updates the last ChatItemType.ANSWER_STREAM chat item
* @param tabId Corresponding tab ID.
* @param updateWith ChatItem object to update with.
*/
public updateLastChatAnswer = (tabId: string, updateWith: Partial<ChatItem>): void => {
if (MynahUITabsStore.getInstance().getTab(tabId) != null) {
if (this.chatWrappers[tabId].getLastStreamingMessageId() != null) {
this.chatWrappers[tabId].updateLastChatAnswer(updateWith);
} else {
// We're assuming consumer shouldn't try to update last chat item if it is not a streaming one
// However, to be on the safe side, if there is no streaming card available, we're adding one.
this.addChatItem(tabId, {
type: ChatItemType.ANSWER_STREAM,
body: '',
messageId: generateUID(),
});
this.chatWrappers[tabId].updateLastChatAnswer(updateWith);
}
}
};
/**
* Updates the chat item with the given messageId
* @param tabId Corresponding tab ID.
* @param messageId Corresponding tab ID.
* @param updateWith ChatItem object to update with.
*/
public updateChatAnswerWithMessageId = (tabId: string, messageId: string, updateWith: Partial<ChatItem>): void => {
if (MynahUITabsStore.getInstance().getTab(tabId) !== null) {
this.chatWrappers[tabId].updateChatAnswerWithMessageId(messageId, updateWith);
}
};
/**
* Serialize all (non-empty) chat messages in a tab into a string
* @param tabId Corresponding tab ID.
* @param format Whether to serialize to markdown or HTML format
*/
public serializeChat = (tabId: string, format: 'markdown' | 'html'): string => {
if (format === 'markdown') {
return serializeMarkdown(tabId);
}
return serializeHtml(tabId);
};
/**
* Converts a card to an ANSWER if it is an ANSWER_STREAM
* @param tabId Corresponding tab ID.
* @param messageId Corresponding tab ID.
* @param updateWith Optional, if you like update the card while converting it to
* a normal ANSWER instead of a stream one, you can send a ChatItem object to update with.
*/
public endMessageStream = (tabId: string, messageId: string, updateWith?: Partial<ChatItem>): CardRenderDetails => {
if (MynahUITabsStore.getInstance().getTab(tabId) !== null) {
const chatMessage = this.chatWrappers[tabId].getChatItem(messageId);
if (chatMessage != null && ![ ChatItemType.AI_PROMPT, ChatItemType.PROMPT, ChatItemType.SYSTEM_PROMPT ].includes(chatMessage.chatItem.type)) {
this.chatWrappers[tabId].endStreamWithMessageId(messageId, {
type: ChatItemType.ANSWER,
...updateWith
});
return chatMessage.renderDetails;
}
}
return {
totalNumberOfCodeBlocks: 0
};
};
/**
* If exists, switch to a different tab
* @param tabId Tab ID to switch to
* @param eventId last action's user event ID passed from an event binded to mynahUI.
* Without user intent you cannot switch to a different tab
*/
public selectTab = (tabId: string, eventId?: string): void => {
// TODO: until we find a way to confirm the events from the consumer as events,
// eventId === this.lastEventId: This check will be removed
if (MynahUITabsStore.getInstance().getTab(tabId) !== null) {
MynahUITabsStore.getInstance().selectTab(tabId);
}
};
/**
* If exists, close the given tab
* @param tabId Tab ID to switch to
* @param eventId last action's user event ID passed from an event binded to mynahUI.
* Without user intent you cannot switch to a different tab
*/
public removeTab = (tabId: string, eventId: string): void => {
if (eventId === this.lastEventId && MynahUITabsStore.getInstance().getTab(tabId) !== null) {
MynahUITabsStore.getInstance().removeTab(tabId);
}
};
/**
* Updates only the UI with the given data for the given tab
* Send tab id as an empty string to open a new tab!
* If max tabs reached, will not return tabId
* @param data A full or partial set of data with values.
*/
public updateStore = (tabId: string | '', data: MynahUIDataModel): string | undefined => {
if (tabId === '') {
return MynahUITabsStore.getInstance().addTab({ store: { ...data } });
} else if (MynahUITabsStore.getInstance().getTab(tabId) !== null) {
MynahUITabsStore.getInstance().updateTab(tabId, { store: { ...data } });
}
return tabId;
};
/**
* Updates defaults of the tab store
* @param defaults MynahUITabStoreTab
*/
public updateTabDefaults = (defaults: MynahUITabStoreTab): void => {
MynahUITabsStore.getInstance().updateTabDefaults(defaults);
};
/**
* Updates defaults of the tab store
* @param defaults MynahUITabStoreTab
*/
public getTabDefaults = (): MynahUITabStoreTab => {
return MynahUITabsStore.getInstance().getTabDefaults();
};
/**
* This function returns the selected tab id if there is any, otherwise returns undefined
* @returns string selectedTabId or undefined
*/
public getSelectedTabId = (): string | undefined => {
const selectedTabId = MynahUITabsStore.getInstance().getSelectedTabId();
return selectedTabId === '' ? undefined : selectedTabId;
};
/**
* Returns all tabs with their store information
* @returns string selectedTabId or undefined
*/
public getAllTabs = (): MynahUITabStoreModel => MynahUITabsStore.getInstance().getAllTabs();
public getTabData = (tabId: string): MynahUIDataStore => MynahUITabsStore.getInstance().getTabDataStore(tabId);
/**
* Toggles the visibility of the splash loader screen
*/
public toggleSplashLoader = (visible: boolean, text?: string): void => {
if (visible) {
this.splashLoader.addClass('visible');
} else {
this.splashLoader.removeClass('visible');
}
if (text != null) {
this.splashLoaderText.update({
innerHTML: parseMarkdown(text, { includeLineBreaks: true })
});
}
};
/**
* Simply creates and shows a notification
* @param props NotificationProps
*/
public notify = (props: {
/**
* -1 for infinite
*/
duration?: number;
type?: NotificationType;
title?: string;
content: string;
onNotificationClick?: (eventId: string) => void;
onNotificationHide?: (eventId: string) => void;
}): void => {
new Notification({
...props,
onNotificationClick: (props.onNotificationClick != null)
? () => {
if (props.onNotificationClick != null) {
props.onNotificationClick(this.getUserEventId());
}
}
: undefined,
onNotificationHide: (props.onNotificationHide != null)
? () => {
if (props.onNotificationHide != null) {
props.onNotificationHide(this.getUserEventId());
}
}
: undefined,
}).notify();
};
/**
* Simply creates and shows a custom form
*/
public showCustomForm = (
tabId: string,
formItems?: ChatItemFormItem[],
buttons?: ChatItemButton[],
title?: string,
description?: string): void => {
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.SHOW_FEEDBACK_FORM, {
tabId,
customFormData: {
title,
description,
buttons,
formItems
},
});
};
public openDetailedList = (
data: DetailedListSheetProps,
): {
update: (data: DetailedList) => void;
close: () => void;
changeTarget: (direction: 'up' | 'down', snapOnLastAndFirst?: boolean) => void;
getTargetElementId: () => string | undefined;
} => {
const detailedListSheet = new DetailedListSheet({
detailedList: data.detailedList,
events: data.events
});
detailedListSheet.open();
const getTargetElementId = (): string | undefined => {
const targetElement = detailedListSheet.detailedListWrapper.getTargetElement();
return targetElement?.id ?? undefined;
};
return {
update: detailedListSheet.update,
close: detailedListSheet.close,
changeTarget: detailedListSheet.detailedListWrapper.changeTarget,
getTargetElementId
};
};
public destroy = (): void => {
Config.getInstance().destroy();
MynahUITabsStore.getInstance().destroy();
MynahUIGlobalEvents.getInstance().destroy();
DomBuilder.getInstance().destroy();
};
}