src/components/chat-item/chat-item-card.ts (756 lines of code) (raw):

/*! * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { DomBuilder, DomBuilderObject, ExtendedHTMLElement } from '../../helper/dom'; import { cancelEvent, MynahUIGlobalEvents } from '../../helper/events'; import { MynahUITabsStore } from '../../helper/tabs-store'; import { CardRenderDetails, ChatItem, ChatItemType, MynahEventNames } from '../../static'; import { CardBody, CardBodyProps } from '../card/card-body'; import { Icon, MynahIcons } from '../icon'; import { ChatItemFollowUpContainer } from './chat-item-followup'; import { ChatItemSourceLinksContainer } from './chat-item-source-links'; import { ChatItemRelevanceVote } from './chat-item-relevance-vote'; import { ChatItemTreeViewWrapper } from './chat-item-tree-view-wrapper'; import { Config } from '../../helper/config'; import { ChatItemFormItemsWrapper } from './chat-item-form-items'; import { ChatItemButtonsWrapper, ChatItemButtonsWrapperProps } from './chat-item-buttons'; import { cleanHtml } from '../../helper/sanitize'; import { chatItemHasContent } from '../../helper/chat-item'; import { Card } from '../card/card'; import { ChatItemCardContent, ChatItemCardContentProps } from './chat-item-card-content'; import testIds from '../../helper/test-ids'; import { ChatItemInformationCard } from './chat-item-information-card'; import { ChatItemTabbedCard } from './chat-item-tabbed-card'; import { MoreContentIndicator } from '../more-content-indicator'; import { Button } from '../button'; import { Overlay, OverlayHorizontalDirection, OverlayVerticalDirection } from '../overlay'; import { marked } from 'marked'; const TOOLTIP_DELAY = 350; export interface ChatItemCardProps { tabId: string; initVisibility?: boolean; chatItem: ChatItem; inline?: boolean; small?: boolean; onAnimationStateChange?: (isAnimating: boolean) => void; } export class ChatItemCard { readonly props: ChatItemCardProps; render: ExtendedHTMLElement; private tooltipOverlay: Overlay | null; private tooltipTimeout: ReturnType<typeof setTimeout>; private readonly card: Card | null = null; private readonly updateStack: Array<Partial<ChatItem>> = []; private readonly initialSpinner: ExtendedHTMLElement[] | null = null; private cardFooter: ExtendedHTMLElement | null = null; private cardHeader: ExtendedHTMLElement | null = null; private cardTitle: ExtendedHTMLElement | null = null; private informationCard: ChatItemInformationCard | null = null; private tabbedCard: ChatItemTabbedCard | null = null; private cardIcon: Icon | null = null; private contentBody: ChatItemCardContent | null = null; private chatAvatar: ExtendedHTMLElement; private chatFormItems: ChatItemFormItemsWrapper | null = null; private customRendererWrapper: CardBody | null = null; private chatButtonsInside: ChatItemButtonsWrapper | null = null; private chatButtonsOutside: ChatItemButtonsWrapper | null = null; private fileTreeWrapper: ChatItemTreeViewWrapper | null = null; private fileTreeWrapperCollapsedState: boolean | null = null; private followUps: ChatItemFollowUpContainer | null = null; private readonly moreContentIndicator: MoreContentIndicator | null = null; private isMoreContentExpanded: boolean = false; private votes: ChatItemRelevanceVote | null = null; private footer: ChatItemCard | null = null; private header: ChatItemCard | null = null; constructor (props: ChatItemCardProps) { this.props = { ...props, chatItem: { ...props.chatItem, fullWidth: props.chatItem.fullWidth, padding: props.chatItem.padding != null ? props.chatItem.padding : (props.chatItem.type !== ChatItemType.DIRECTIVE), } }; this.chatAvatar = this.getChatAvatar(); MynahUITabsStore.getInstance() .getTabDataStore(this.props.tabId) .subscribe('showChatAvatars', (value: boolean) => { if (value && this.canShowAvatar()) { this.chatAvatar = this.getChatAvatar(); this.render.insertChild('afterbegin', this.chatAvatar); } else { this.chatAvatar.remove(); } }); if (this.props.chatItem.type === ChatItemType.ANSWER_STREAM) { this.initialSpinner = [ DomBuilder.getInstance().build({ type: 'div', persistent: true, classNames: [ 'mynah-chat-items-spinner', 'text-shimmer' ], children: [ { type: 'div', children: [ Config.getInstance().config.texts.spinnerText ] } ], }), ]; } this.cardTitle = this.getCardTitle(); this.cardHeader = this.getCardHeader(); this.cardFooter = this.getCardFooter(); this.card = new Card({ testId: testIds.chatItem.card, children: this.initialSpinner ?? [], background: this.props.inline !== true && this.props.chatItem.type !== ChatItemType.DIRECTIVE && !(this.props.chatItem.fullWidth !== true && (this.props.chatItem.type === ChatItemType.ANSWER || this.props.chatItem.type === ChatItemType.ANSWER_STREAM)), border: this.props.inline !== true && this.props.chatItem.type !== ChatItemType.DIRECTIVE && !(this.props.chatItem.fullWidth !== true && (this.props.chatItem.type === ChatItemType.ANSWER || this.props.chatItem.type === ChatItemType.ANSWER_STREAM)), padding: this.props.inline === true || this.props.chatItem.padding === false || (this.props.chatItem.fullWidth !== true && (this.props.chatItem.type === ChatItemType.ANSWER || this.props.chatItem.type === ChatItemType.ANSWER_STREAM)) ? 'none' : undefined, }); this.updateCardContent(); this.render = this.generateCard(); /** * Generate/update more content indicator if available */ this.moreContentIndicator = new MoreContentIndicator({ icon: MynahIcons.DOWN_OPEN, border: false, onClick: () => { if (this.isMoreContentExpanded) { this.isMoreContentExpanded = false; this.render.addClass('mynah-chat-item-collapsed'); this.moreContentIndicator?.update({ icon: MynahIcons.DOWN_OPEN }); } else { this.isMoreContentExpanded = true; this.render.removeClass('mynah-chat-item-collapsed'); this.moreContentIndicator?.update({ icon: MynahIcons.UP_OPEN }); } }, testId: testIds.chatItem.moreContentIndicator }); this.render.insertChild('beforeend', this.moreContentIndicator.render); if (this.props.chatItem.autoCollapse === true) { this.render.addClass('mynah-chat-item-collapsed'); } if (this.props.chatItem.type === ChatItemType.ANSWER_STREAM && (this.props.chatItem.body ?? '').trim() !== '') { this.updateCardStack({}); } } private readonly getCardFooter = (): ExtendedHTMLElement => { return DomBuilder.getInstance().build({ type: 'div', classNames: [ 'mynah-chat-item-card-footer', 'mynah-card-inner-order-70' ] }); }; private readonly getCardHeader = (): ExtendedHTMLElement => { return DomBuilder.getInstance().build({ type: 'div', classNames: [ 'mynah-chat-item-card-header', 'mynah-card-inner-order-5' ] }); }; private readonly getCardTitle = (): ExtendedHTMLElement => { return DomBuilder.getInstance().build({ type: 'div', classNames: [ 'mynah-chat-item-card-title', 'mynah-card-inner-order-3' ] }); }; private readonly generateCard = (): ExtendedHTMLElement => { const generatedCard = DomBuilder.getInstance().build({ type: 'div', testId: `${testIds.chatItem.type.any}-${this.props.chatItem.type ?? ChatItemType.ANSWER}`, classNames: this.getCardClasses(), attributes: { messageId: this.props.chatItem.messageId ?? 'unknown', }, children: [ ...(this.canShowAvatar() && MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId).getValue('showChatAvatars') === true ? [ this.chatAvatar ] : []), ...(this.card != null ? [ this.card?.render ] : []), ...(this.chatButtonsOutside != null ? [ this.chatButtonsOutside?.render ] : []), ...(this.props.chatItem.followUp?.text !== undefined ? [ new ChatItemFollowUpContainer({ tabId: this.props.tabId, chatItem: this.props.chatItem }).render ] : []) ], }); setTimeout( () => { this.setMaxHeightClass(this.card?.render); generatedCard.addClass('reveal'); }, 50 ); return generatedCard; }; private readonly setMaxHeightClass = (elm?: ExtendedHTMLElement): void => { if (elm != null) { if (this.props.chatItem.autoCollapse === true && elm.scrollHeight > window.innerHeight / 4) { this.render?.addClass('mynah-chat-item-auto-collapse'); } else { this.render?.removeClass('mynah-chat-item-auto-collapse'); } } }; private readonly getCardClasses = (): string[] => { return [ ...(this.props.chatItem.hoverEffect !== undefined ? [ 'mynah-chat-item-hover-effect' ] : []), ...(this.props.chatItem.shimmer === true ? [ 'text-shimmer' ] : []), ...(this.props.chatItem.icon !== undefined ? [ 'mynah-chat-item-card-has-icon' ] : []), ...(this.props.chatItem.fullWidth === true || this.props.chatItem.type === ChatItemType.ANSWER || this.props.chatItem.type === ChatItemType.ANSWER_STREAM ? [ 'full-width' ] : []), ...(this.props.chatItem.padding === false ? [ 'no-padding' ] : []), ...(this.props.inline === true ? [ 'mynah-ui-chat-item-inline-card' ] : []), ...(this.props.chatItem.muted === true ? [ 'muted' ] : []), ...(this.props.small === true ? [ 'mynah-ui-chat-item-small-card' ] : []), ...(this.props.initVisibility === true ? [ 'reveal' ] : []), `mynah-chat-item-card-status-${this.props.chatItem.status ?? 'default'}`, `mynah-chat-item-card-content-horizontal-align-${this.props.chatItem.contentHorizontalAlignment ?? 'default'}`, 'mynah-chat-item-card', `mynah-chat-item-${this.props.chatItem.type ?? ChatItemType.ANSWER}`, ...(!chatItemHasContent(this.props.chatItem) ? [ 'mynah-chat-item-empty' ] : []), ]; }; private readonly updateCardContent = (): void => { if (MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId) === undefined) { return; } const bodyEvents: Partial<CardBodyProps> = { onLinkClick: (url: string, e: MouseEvent) => { MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.LINK_CLICK, { messageId: this.props.chatItem.messageId, link: url, event: e, }); }, onCopiedToClipboard: (type, text, referenceTrackerInformation, codeBlockIndex) => { MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.COPY_CODE_TO_CLIPBOARD, { messageId: this.props.chatItem.messageId, type, text, referenceTrackerInformation, codeBlockIndex, totalCodeBlocks: (this.contentBody?.getRenderDetails().totalNumberOfCodeBlocks ?? 0) + (this.customRendererWrapper?.nextCodeBlockIndex ?? 0), }); }, ...(Object.keys(Config.getInstance().config.codeBlockActions ?? {}).length > 0 || Object.keys(this.props.chatItem.codeBlockActions ?? {}).length > 0 ? { codeBlockActions: { ...Config.getInstance().config.codeBlockActions, ...this.props.chatItem.codeBlockActions }, onCodeBlockAction: (actionId, data, type, text, referenceTrackerInformation, codeBlockIndex) => { MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.CODE_BLOCK_ACTION, { actionId, data, messageId: this.props.chatItem.messageId, type, text, referenceTrackerInformation, codeBlockIndex, totalCodeBlocks: (this.contentBody?.getRenderDetails().totalNumberOfCodeBlocks ?? 0) + (this.customRendererWrapper?.nextCodeBlockIndex ?? 0), }); } } : {}) }; if (chatItemHasContent(this.props.chatItem)) { this.initialSpinner?.[0]?.remove(); } // If no data is provided for the header // skip removing and checking it if (this.props.chatItem.canBeDismissed === true || this.props.chatItem.title != null) { if (this.cardTitle != null) { this.cardTitle.remove(); this.cardTitle = null; } this.cardTitle = this.getCardTitle(); if (this.props.chatItem.title != null) { this.cardTitle?.insertChild('beforeend', DomBuilder.getInstance().build({ type: 'div', classNames: [ 'mynah-chat-item-card-title-text' ], children: [ this.props.chatItem.title ] })); } if (this.props.chatItem.canBeDismissed === true) { this.cardTitle?.insertChild('beforeend', new Button({ icon: new Icon({ icon: 'cancel' }).render, onClick: () => { this.render.remove(); MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.CARD_DISMISS, { tabId: this.props.tabId, messageId: this.props.chatItem.messageId }); if (this.props.chatItem.messageId !== undefined) { const currentChatItems: ChatItem[] = MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId).getValue('chatItems'); MynahUITabsStore.getInstance() .getTabDataStore(this.props.tabId) .updateStore( { chatItems: [ ...currentChatItems.map(chatItem => this.props.chatItem.messageId !== chatItem.messageId ? chatItem : { type: ChatItemType.ANSWER, messageId: chatItem.messageId }) ], }, true ); } }, primary: false, status: 'clear', testId: testIds.chatItem.dismissButton }).render); } this.card?.render.insertChild('afterbegin', this.cardTitle); } // Handle data updates with the update structure of the chat item itself if (this.props.chatItem.header === null) { this.cardHeader?.remove(); this.cardHeader = null; this.header?.render.remove(); this.header = null; } else if (this.props.chatItem.header != null) { if (this.cardHeader != null && this.header != null) { this.header.updateCardStack({ ...this.props.chatItem.header, status: undefined, type: ChatItemType.ANSWER, messageId: this.props.chatItem.messageId, } satisfies ChatItem); } else { this.cardHeader?.remove(); this.cardHeader = this.getCardHeader(); this.card?.render.insertChild('beforeend', this.cardHeader); this.header = new ChatItemCard({ tabId: this.props.tabId, small: true, initVisibility: true, inline: true, chatItem: { ...this.props.chatItem.header, status: undefined, type: ChatItemType.ANSWER, messageId: this.props.chatItem.messageId, }, }); this.cardHeader.insertChild('beforeend', this.header.render); } if (this.props.chatItem.header.status != null) { this.cardHeader.insertAdjacentElement(this.props.chatItem.header.status.position === 'left' ? 'afterbegin' : 'beforeend', DomBuilder.getInstance().build({ type: 'span', classNames: [ 'mynah-chat-item-card-header-status', `status-${this.props.chatItem.header.status.status ?? 'default'}` ], children: [ ...(this.props.chatItem.header.status.icon != null ? [ new Icon({ icon: this.props.chatItem.header.status.icon }).render ] : []), ...(this.props.chatItem.header.status.text != null ? [ { type: 'span', classNames: [ 'mynah-chat-item-card-header-status-text' ], children: [ this.props.chatItem.header.status.text ] } ] : []), ], ...(this.props.chatItem.header.status?.description != null ? { events: { mouseover: (e) => { cancelEvent(e); const tooltipText = marked(this.props.chatItem?.header?.status?.description ?? '', { breaks: true }) as string; this.showTooltip(tooltipText, e.target ?? e.currentTarget); }, mouseleave: this.hideTooltip } } : {}) })); } } /** * Generate card icon if available */ if (this.props.chatItem.icon != null) { if (this.cardIcon != null) { this.cardIcon.render.remove(); this.cardIcon = null; } this.cardIcon = new Icon({ icon: this.props.chatItem.icon, status: this.props.chatItem.iconForegroundStatus, subtract: this.props.chatItem.iconStatus != null, classNames: [ 'mynah-chat-item-card-icon', 'mynah-card-inner-order-10', `icon-status-${this.props.chatItem.iconStatus ?? 'none'}` ] }); this.card?.render.insertChild('beforeend', this.cardIcon.render); } /** * Generate contentBody if available */ if (this.props.chatItem.body != null && this.props.chatItem.body !== '') { const updatedCardContentBodyProps: ChatItemCardContentProps = { body: this.props.chatItem.body ?? '', hideCodeBlockLanguage: this.props.chatItem.padding === false, wrapCode: this.props.chatItem.wrapCodes, unlimitedCodeBlockHeight: this.props.chatItem.autoCollapse, classNames: [ 'mynah-card-inner-order-20' ], renderAsStream: this.props.chatItem.type === ChatItemType.ANSWER_STREAM || this.props.chatItem.type === ChatItemType.DIRECTIVE, codeReference: this.props.chatItem.codeReference ?? undefined, onAnimationStateChange: (isAnimating) => { if (isAnimating) { this.render?.addClass('typewriter-animating'); } else { this.render?.removeClass('typewriter-animating'); this.props.onAnimationStateChange?.(isAnimating); } }, children: this.props.chatItem.relatedContent !== undefined ? [ new ChatItemSourceLinksContainer({ messageId: this.props.chatItem.messageId ?? 'unknown', tabId: this.props.tabId, relatedContent: this.props.chatItem.relatedContent?.content, title: this.props.chatItem.relatedContent?.title, }).render, ] : [], contentProperties: bodyEvents, }; if (this.contentBody != null) { this.contentBody.updateCardStack(updatedCardContentBodyProps); } else { this.contentBody = new ChatItemCardContent(updatedCardContentBodyProps); this.card?.render.insertChild('beforeend', this.contentBody.render); } } else if (this.props.chatItem.body === null) { this.contentBody?.render.remove(); this.contentBody = null; } /** * Generate customRenderer if available */ if (this.customRendererWrapper != null) { this.customRendererWrapper.render.remove(); this.customRendererWrapper = null; } if (this.props.chatItem.customRenderer != null) { const customRendererContent: Partial<DomBuilderObject> = {}; if (typeof this.props.chatItem.customRenderer === 'object') { customRendererContent.children = Array.isArray(this.props.chatItem.customRenderer) ? this.props.chatItem.customRenderer : [ this.props.chatItem.customRenderer ]; } else if (typeof this.props.chatItem.customRenderer === 'string') { customRendererContent.innerHTML = cleanHtml(this.props.chatItem.customRenderer); } this.customRendererWrapper = new CardBody({ body: customRendererContent.innerHTML, children: customRendererContent.children, classNames: [ 'mynah-card-inner-order-30' ], processChildren: true, useParts: true, codeBlockStartIndex: (this.contentBody?.getRenderDetails().totalNumberOfCodeBlocks ?? 0), ...bodyEvents, }); this.card?.render.insertChild('beforeend', this.customRendererWrapper.render); } /** * Generate form items if available */ if (this.chatFormItems != null) { this.chatFormItems.render.remove(); this.chatFormItems = null; } if (this.props.chatItem.formItems != null) { this.chatFormItems = new ChatItemFormItemsWrapper({ classNames: [ 'mynah-card-inner-order-40' ], tabId: this.props.tabId, chatItem: this.props.chatItem, onModifierEnterPress (formData, tabId) { MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FORM_MODIFIER_ENTER_PRESS, { formData, tabId }); }, onTextualItemKeyPress (event, itemId, formData, tabId, disableAllCallback) { MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FORM_TEXTUAL_ITEM_KEYPRESS, { event, formData, itemId, tabId, callback: (disableAll?: boolean) => { if (disableAll === true) { disableAllCallback(); } } }); }, onFormChange (formData, isValid, tabId) { MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FORM_CHANGE, { formData, isValid, tabId }); }, }); this.card?.render.insertChild('beforeend', this.chatFormItems.render); } /** * Generate file tree if available */ if (this.fileTreeWrapper != null) { this.fileTreeWrapper.render.remove(); this.fileTreeWrapper = null; } if (this.props.chatItem.fileList != null) { const { filePaths = [], deletedFiles = [], actions, details, flatList } = this.props.chatItem.fileList; const referenceSuggestionLabel = this.props.chatItem.body ?? ''; this.fileTreeWrapper = new ChatItemTreeViewWrapper({ tabId: this.props.tabId, classNames: [ 'mynah-card-inner-order-50' ], messageId: this.props.chatItem.messageId ?? '', cardTitle: this.props.chatItem.fileList.fileTreeTitle, rootTitle: this.props.chatItem.fileList.rootFolderTitle, rootStatusIcon: this.props.chatItem.fileList.rootFolderStatusIcon, rootIconForegroundStatus: this.props.chatItem.fileList.rootFolderStatusIconForegroundStatus, rootLabel: this.props.chatItem.fileList.rootFolderLabel, folderIcon: this.props.chatItem.fileList.folderIcon, hideFileCount: this.props.chatItem.fileList.hideFileCount ?? false, collapsed: this.fileTreeWrapperCollapsedState != null ? this.fileTreeWrapperCollapsedState : this.props.chatItem.fileList.collapsed != null ? this.props.chatItem.fileList.collapsed : false, files: filePaths, deletedFiles, flatList, actions, details, references: this.props.chatItem.codeReference ?? [], referenceSuggestionLabel, onRootCollapsedStateChange: (isRootCollapsed) => { this.fileTreeWrapperCollapsedState = isRootCollapsed; } }); this.card?.render.insertChild('beforeend', this.fileTreeWrapper.render); } else { this.fileTreeWrapperCollapsedState = null; } /** * Generate information card if available */ if (this.informationCard != null) { this.informationCard.render.remove(); this.informationCard = null; } if (this.props.chatItem.informationCard != null) { this.informationCard = new ChatItemInformationCard({ tabId: this.props.tabId, messageId: this.props.chatItem.messageId, classNames: [ 'mynah-card-inner-order-55' ], informationCard: this.props.chatItem.informationCard ?? {} }); this.card?.render.insertChild('beforeend', this.informationCard.render); } /** * Generate buttons if available */ if (this.chatButtonsInside != null) { this.chatButtonsInside.render.remove(); this.chatButtonsInside = null; } if (this.chatButtonsOutside != null) { this.chatButtonsOutside.render.remove(); this.chatButtonsOutside = null; } if (this.props.chatItem.buttons != null) { const insideButtons = this.props.chatItem.buttons.filter((button) => button.position == null || button.position === 'inside'); const outsideButtons = this.props.chatItem.buttons.filter((button) => button.position === 'outside'); const chatButtonProps: ChatItemButtonsWrapperProps = { tabId: this.props.tabId, classNames: [ 'mynah-card-inner-order-60' ], formItems: this.chatFormItems, buttons: [], onActionClick: action => { MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.BODY_ACTION_CLICKED, { tabId: this.props.tabId, messageId: this.props.chatItem.messageId, actionId: action.id, actionText: action.text, ...(this.chatFormItems !== null ? { formItemValues: this.chatFormItems.getAllValues() } : {}), }); if (action.keepCardAfterClick === false) { this.render.remove(); if (this.props.chatItem.messageId !== undefined) { const currentChatItems: ChatItem[] = MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId).getValue('chatItems'); MynahUITabsStore.getInstance() .getTabDataStore(this.props.tabId) .updateStore( { chatItems: [ ...currentChatItems.map(chatItem => this.props.chatItem.messageId !== chatItem.messageId ? chatItem : { type: ChatItemType.ANSWER, messageId: chatItem.messageId }) ], }, true ); } } }, }; if (insideButtons.length > 0) { this.chatButtonsInside = new ChatItemButtonsWrapper({ ...chatButtonProps, buttons: insideButtons }); this.card?.render.insertChild('beforeend', this.chatButtonsInside.render); } if (outsideButtons.length > 0) { this.chatButtonsOutside = new ChatItemButtonsWrapper({ ...chatButtonProps, buttons: outsideButtons }); this.render?.insertChild('beforeend', this.chatButtonsOutside.render); } } /** * Generate tabbed card if available */ if (this.tabbedCard != null) { this.tabbedCard.render.remove(); this.tabbedCard = null; } if (this.props.chatItem.tabbedContent != null) { this.tabbedCard = new ChatItemTabbedCard({ tabId: this.props.tabId, messageId: this.props.chatItem.messageId, tabbedCard: this.props.chatItem.tabbedContent, classNames: [ 'mynah-card-inner-order-55' ] }); this.card?.render.insertChild('beforeend', this.tabbedCard.render); } /** * Clear footer block */ if (this.cardFooter != null) { this.cardFooter.remove(); this.cardFooter = null; } if (this.props.chatItem.footer != null || this.props.chatItem.canBeVoted === true) { this.cardFooter = this.getCardFooter(); this.card?.render.insertChild('beforeend', this.cardFooter); /** * Generate footer if available */ if (this.footer != null) { this.footer.render.remove(); this.footer = null; } if (this.props.chatItem.footer != null) { this.footer = new ChatItemCard({ tabId: this.props.tabId, small: true, inline: true, chatItem: { ...this.props.chatItem.footer, type: ChatItemType.ANSWER, messageId: this.props.chatItem.messageId } }); this.cardFooter.insertChild('beforeend', this.footer.render); } /** * Generate votes if available */ if (this.votes !== null) { this.votes.render.remove(); this.votes = null; } if (this.props.chatItem.canBeVoted === true && this.props.chatItem.messageId !== undefined) { this.votes = new ChatItemRelevanceVote({ tabId: this.props.tabId, messageId: this.props.chatItem.messageId }); this.cardFooter.insertChild('beforeend', this.votes.render); } } /** * Generate/update followups if available */ if (this.followUps !== null) { this.followUps.render.remove(); this.followUps = null; } if (this.props.chatItem.followUp != null) { this.followUps = new ChatItemFollowUpContainer({ tabId: this.props.tabId, chatItem: this.props.chatItem }); this.render?.insertChild('beforeend', this.followUps.render); } }; private readonly getChatAvatar = (): ExtendedHTMLElement => DomBuilder.getInstance().build({ type: 'div', classNames: [ 'mynah-chat-item-card-icon-wrapper' ], children: [ new Icon({ icon: this.props.chatItem.type === ChatItemType.PROMPT ? MynahIcons.USER : MynahIcons.Q }).render ], }); private readonly canShowAvatar = (): boolean => (this.props.chatItem.type === ChatItemType.ANSWER_STREAM || (this.props.inline !== true && chatItemHasContent({ ...this.props.chatItem, followUp: undefined }))); private readonly showTooltip = (content: string, elm: HTMLElement): void => { if (content.trim() !== undefined) { clearTimeout(this.tooltipTimeout); this.tooltipTimeout = setTimeout(() => { this.tooltipOverlay = new Overlay({ background: true, closeOnOutsideClick: false, referenceElement: elm, dimOutside: false, removeOtherOverlays: true, verticalDirection: OverlayVerticalDirection.TO_TOP, horizontalDirection: OverlayHorizontalDirection.CENTER, children: [ new Card({ border: false, children: [ new CardBody({ body: content }).render ] }).render ], }); }, TOOLTIP_DELAY); } }; public readonly hideTooltip = (): void => { clearTimeout(this.tooltipTimeout); if (this.tooltipOverlay !== null) { this.tooltipOverlay?.close(); this.tooltipOverlay = null; } }; public readonly updateCard = (): void => { if (this.updateStack.length > 0) { const updateWith: Partial<ChatItem> | undefined = this.updateStack.shift(); if (updateWith !== undefined) { // Update item inside the store if (this.props.chatItem.messageId != null) { const currentTabChatItems = MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId)?.getStore()?.chatItems; MynahUITabsStore.getInstance() .getTabDataStore(this.props.tabId) .updateStore( { chatItems: currentTabChatItems?.map((chatItem: ChatItem) => { if (chatItem.messageId === this.props.chatItem.messageId) { return { ...this.props.chatItem, ...updateWith }; } return chatItem; }), }, true ); } this.props.chatItem = { ...this.props.chatItem, ...updateWith, }; this.render?.update({ ...(this.props.chatItem.messageId != null ? { attributes: { messageid: this.props.chatItem.messageId } } : {}), classNames: [ ...this.getCardClasses(), 'reveal', ...(this.isMoreContentExpanded ? [ ] : [ 'mynah-chat-item-collapsed' ]) ], }); this.updateCardContent(); this.updateCard(); this.setMaxHeightClass(this.card?.render); } } }; public readonly updateCardStack = (updateWith: Partial<ChatItem>): void => { this.updateStack.push(updateWith); this.updateCard(); }; public readonly clearContent = (): void => { this.cardHeader?.remove(); this.cardHeader = null; this.contentBody?.render.remove(); this.contentBody = null; this.chatButtonsInside?.render.remove(); this.chatButtonsInside = null; this.customRendererWrapper?.render.remove(); this.customRendererWrapper = null; this.fileTreeWrapper?.render.remove(); this.fileTreeWrapper = null; this.followUps?.render.remove(); this.followUps = null; this.cardFooter?.remove(); this.cardFooter = null; this.chatFormItems?.render.remove(); this.chatFormItems = null; this.informationCard?.render.remove(); this.informationCard = null; this.tabbedCard?.render.remove(); this.tabbedCard = null; }; public readonly getRenderDetails = (): CardRenderDetails => { return { totalNumberOfCodeBlocks: (this.contentBody?.getRenderDetails().totalNumberOfCodeBlocks ?? 0) + (this.customRendererWrapper?.nextCodeBlockIndex ?? 0) }; }; public readonly cleanFollowupsAndRemoveIfEmpty = (): boolean => { this.followUps?.render?.remove(); this.followUps = null; if (!chatItemHasContent({ ...this.props.chatItem, followUp: undefined })) { this.render.remove(); return true; } return false; }; }