diff --git a/component/src/deepChat.ts b/component/src/deepChat.ts index 4b6d393d5..17ef92d5d 100644 --- a/component/src/deepChat.ts +++ b/component/src/deepChat.ts @@ -31,6 +31,7 @@ import {ServiceIO} from './services/serviceIO'; import {Legacy} from './utils/legacy/legacy'; import {TextInput} from './types/textInput'; import {LoadHistory} from './types/history'; +import {FocusMode} from './types/focusMode'; import {CustomStyle} from './types/styles'; import {Response} from './types/response'; import style from './deepChat.css?inline'; @@ -145,7 +146,7 @@ export class DeepChat extends InternalHTML { remarkable?: RemarkableOptions; @Property('boolean') - focusMode?: boolean; + focusMode?: FocusMode; getMessages: () => MessageContent[] = () => []; diff --git a/component/src/types/focusMode.ts b/component/src/types/focusMode.ts new file mode 100644 index 000000000..dea6b89a1 --- /dev/null +++ b/component/src/types/focusMode.ts @@ -0,0 +1,3 @@ +export type FocusModeFade = boolean | number; + +export type FocusMode = boolean | {isScroll?: boolean; fade?: FocusModeFade}; diff --git a/component/src/utils/element/elementUtils.ts b/component/src/utils/element/elementUtils.ts index b97e2c9d9..a260d6705 100644 --- a/component/src/utils/element/elementUtils.ts +++ b/component/src/utils/element/elementUtils.ts @@ -24,8 +24,12 @@ export class ElementUtils { return newElement; } - public static scrollToBottom(element: HTMLElement) { - element.scrollTop = element.scrollHeight; + public static scrollToBottom(element: HTMLElement, isAnimation = false) { + if (isAnimation) { + element.scrollTo({left: 0, top: element.scrollHeight, behavior: 'smooth'}); + } else { + element.scrollTop = element.scrollHeight; + } } public static scrollToTop(element: HTMLElement) { diff --git a/component/src/views/chat/input/buttons/submit/submitButton.ts b/component/src/views/chat/input/buttons/submit/submitButton.ts index 3e5dde645..26aab6cfa 100644 --- a/component/src/views/chat/input/buttons/submit/submitButton.ts +++ b/component/src/views/chat/input/buttons/submit/submitButton.ts @@ -2,6 +2,7 @@ import {FileAttachmentsType} from '../../fileAttachments/fileAttachmentTypes/fil import {ValidationHandler} from '../../../../../types/validationHandler'; import {CustomButtonInnerElements} from '../customButtonInnerElements'; import {FileAttachments} from '../../fileAttachments/fileAttachments'; +import {FocusModeUtils} from '../../../messages/utils/focusModeUtils'; import {SubmitButtonStyles} from '../../../../../types/submitButton'; import {SpeechToText} from '../microphone/speechToText/speechToText'; import {SUBMIT_ICON_STRING} from '../../../../../icons/submitIcon'; @@ -195,9 +196,12 @@ export class SubmitButton extends InputButton { public async attemptSubmit(content: UserContentI, isProgrammatic = false) { if ((await this._validationHandler?.(isProgrammatic ? content : undefined)) === false) return; this.changeToLoadingIcon(); + this._textInput.clear(); + if (typeof this._messages.focusMode !== 'boolean' && this._messages.focusMode?.fade) { + await FocusModeUtils.fadeAnimation(this._messages.elementRef, this._messages.focusMode.fade); + } await this.addNewMessage(content); if (!this._serviceIO.isWebModel()) this._messages.addLoadingMessage(); - this._textInput.clear(); const filesData = content.files?.map((fileData) => fileData.file); const requestContents = {text: content.text === '' ? undefined : content.text, files: filesData}; await this._serviceIO.callAPI(requestContents, this._messages); diff --git a/component/src/views/chat/messages/messagesBase.ts b/component/src/views/chat/messages/messagesBase.ts index 122dc0fc4..60dc1c819 100644 --- a/component/src/views/chat/messages/messagesBase.ts +++ b/component/src/views/chat/messages/messagesBase.ts @@ -10,8 +10,10 @@ import {FireEvents} from '../../../utils/events/fireEvents'; import {RemarkableOptions} from '../../../types/remarkable'; import {LoadingHistory} from './history/loadingHistory'; import {HTMLClassUtilities} from '../../../types/html'; +import {FocusModeUtils} from './utils/focusModeUtils'; import {IntroPanel} from '../introPanel/introPanel'; import {Legacy} from '../../../utils/legacy/legacy'; +import {FocusMode} from '../../../types/focusMode'; import {MessageUtils} from './utils/messageUtils'; import {Response} from '../../../types/response'; import {Avatars} from '../../../types/avatars'; @@ -25,7 +27,7 @@ export class MessagesBase { textToSpeech?: ProcessedTextToSpeechConfig; submitUserMessage?: (content: UserContent) => void; readonly elementRef: HTMLElement; - readonly focusMode: boolean; + readonly focusMode?: FocusMode; readonly messageStyles?: MessageStyles; readonly htmlClassUtilities: HTMLClassUtilities = {}; readonly messageToElements: MessageToElements = []; @@ -48,7 +50,10 @@ export class MessagesBase { this._names = deepChat.names; this._onMessage = FireEvents.onMessage.bind(this, deepChat); if (deepChat.htmlClassUtilities) this.htmlClassUtilities = deepChat.htmlClassUtilities; - this.focusMode = !!deepChat.focusMode; + this.focusMode = deepChat.focusMode; + if (typeof this.focusMode !== 'boolean' && this.focusMode?.fade) { + FocusModeUtils.setFade(this.elementRef, this.focusMode.fade); + } setTimeout(() => { this.submitUserMessage = deepChat.submitUserMessage; // wait for it to be available in input.ts }); @@ -99,7 +104,9 @@ export class MessagesBase { const messageElements = this.createNewMessageElement(text, role); this.appendOuterContainerElemet(messageElements.outerContainer); if (role === 'user') { - setTimeout(() => ElementUtils.scrollToBottom(this.elementRef)); // timeout neeed when bubble font is large + const isAnimation = typeof this.focusMode !== 'boolean' && this.focusMode?.isScroll; + // timeout neeed when bubble font is large + setTimeout(() => ElementUtils.scrollToBottom(this.elementRef, isAnimation)); } else { // prevents a browser bug where a long response from AI would sometimes scroll down this.messageElementRefs[this.messageElementRefs.length - 2]?.outerContainer.scrollIntoView(); diff --git a/component/src/views/chat/messages/utils/focusModeUtils.ts b/component/src/views/chat/messages/utils/focusModeUtils.ts new file mode 100644 index 000000000..89160d96a --- /dev/null +++ b/component/src/views/chat/messages/utils/focusModeUtils.ts @@ -0,0 +1,18 @@ +import {FocusModeFade} from '../../../../types/focusMode'; + +export class FocusModeUtils { + private static readonly DEFAULT_FADE_MS = 500; + + public static setFade(elementRef: HTMLElement, fade: FocusModeFade) { + elementRef.style.transitionDuration = typeof fade === 'number' ? `${fade}ms` : `${FocusModeUtils.DEFAULT_FADE_MS}ms`; + } + + public static async fadeAnimation(elementRef: HTMLElement, fade: FocusModeFade) { + elementRef.style.opacity = '0'; + const timeoutMS = typeof fade === 'number' ? fade : FocusModeUtils.DEFAULT_FADE_MS; + await new Promise((resolve) => { + setTimeout(() => resolve(), timeoutMS); + }); + elementRef.style.opacity = '1'; + } +}