Skip to content

Commit

Permalink
adding load history functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
OvidijusParsiunas committed Jul 20, 2024
1 parent 6e893bc commit 1f57797
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 63 deletions.
4 changes: 3 additions & 1 deletion component/src/types/history.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {MessageContent} from './messages';

export type LoadHistory = (index: number) => MessageContent[] | Promise<MessageContent[]>;
export type HistoryMessage = MessageContent | false;

export type LoadHistory = (index: number) => HistoryMessage[] | Promise<HistoryMessage[]>;
10 changes: 7 additions & 3 deletions component/src/views/chat/messages/fileMessageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import {MessageElements} from './messages';
export class FileMessageUtils {
public static readonly DEFAULT_FILE_NAME = 'file';

public static addMessage(messages: MessagesBase, elements: MessageElements, styles: keyof MessageStyles, role: string) {
messages.elementRef.appendChild(elements.outerContainer);
// prettier-ignore
public static addMessage(
messages: MessagesBase, elements: MessageElements, styles: keyof MessageStyles, role: string, isTop: boolean) {
messages.applyCustomStyles(elements, role, true, messages.messageStyles?.[styles]);
messages.elementRef.scrollTop = messages.elementRef.scrollHeight;
if (!isTop) {
messages.elementRef.appendChild(elements.outerContainer);
messages.elementRef.scrollTop = messages.elementRef.scrollHeight;
}
}

private static wrapInLink(element: HTMLElement, url: string, name?: string) {
Expand Down
30 changes: 15 additions & 15 deletions component/src/views/chat/messages/fileMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import {MessageUtils} from './messageUtils';
import {Messages} from './messages';

export class FileMessages {
private static createImage(imageData: MessageFile, messagesContainerEl: HTMLElement) {
private static createImage(imageData: MessageFile, messagesContainerEl: HTMLElement, isTop: boolean) {
const imageElement = new Image();
imageElement.src = imageData.src as string;
FileMessageUtils.scrollDownOnImageLoad(imageElement.src, messagesContainerEl);
if (!isTop) FileMessageUtils.scrollDownOnImageLoad(imageElement.src, messagesContainerEl);
return FileMessageUtils.processContent('image', imageElement, imageElement.src, imageData.name);
}

// WORK - image still does not scroll down when loaded
private static async addNewImageMessage(messages: Messages, imageData: MessageFile, role: string) {
const image = FileMessages.createImage(imageData, messages.elementRef);
private static async addNewImageMessage(messages: Messages, imageData: MessageFile, role: string, isTop: boolean) {
const image = FileMessages.createImage(imageData, messages.elementRef, isTop);
const elements = messages.createNewMessageElement('', role);
elements.bubbleElement.appendChild(image);
elements.bubbleElement.classList.add('image-message');
FileMessageUtils.addMessage(messages, elements, 'image', role);
FileMessageUtils.addMessage(messages, elements, 'image', role, isTop);
}

private static createAudioElement(audioData: MessageFile, role: string) {
Expand All @@ -37,12 +37,12 @@ export class FileMessages {
return audioElement;
}

private static addNewAudioMessage(messages: Messages, audioData: MessageFile, role: string) {
private static addNewAudioMessage(messages: Messages, audioData: MessageFile, role: string, isTop: boolean) {
const audioElement = FileMessages.createAudioElement(audioData, role);
const elements = messages.createNewMessageElement('', role);
const elements = messages.createMessageElementsOnOrientation('', role, isTop);
elements.bubbleElement.appendChild(audioElement);
elements.bubbleElement.classList.add('audio-message');
FileMessageUtils.addMessage(messages, elements, 'audio', role);
FileMessageUtils.addMessage(messages, elements, 'audio', role, isTop);
}

private static createAnyFile(imageData: MessageFile) {
Expand All @@ -61,24 +61,24 @@ export class FileMessages {
return FileMessageUtils.processContent('any', contents, imageData.src, fileNameElement.textContent);
}

private static addNewAnyFileMessage(messages: Messages, data: MessageFile, role: string) {
const elements = messages.createNewMessageElement('', role);
private static addNewAnyFileMessage(messages: Messages, data: MessageFile, role: string, isTop: boolean) {
const elements = messages.createMessageElementsOnOrientation('', role, isTop);
const anyFile = FileMessages.createAnyFile(data);
elements.bubbleElement.classList.add('any-file-message-bubble');
elements.bubbleElement.appendChild(anyFile);
FileMessageUtils.addMessage(messages, elements, 'file', role);
FileMessageUtils.addMessage(messages, elements, 'file', role, isTop);
}

// no overwrite previous message logic as it is complex to track which files are to be overwritten
public static addMessages(messages: Messages, files: MessageFiles, role: string) {
public static addMessages(messages: Messages, files: MessageFiles, role: string, isTop: boolean) {
files.forEach((fileData) => {
if (fileData.ref) fileData = FileMessageUtils.removeFileRef(fileData);
if (FileMessageUtils.isAudioFile(fileData)) {
FileMessages.addNewAudioMessage(messages, fileData, role);
FileMessages.addNewAudioMessage(messages, fileData, role, isTop);
} else if (FileMessageUtils.isImageFile(fileData)) {
FileMessages.addNewImageMessage(messages, fileData, role);
FileMessages.addNewImageMessage(messages, fileData, role, isTop);
} else {
FileMessages.addNewAnyFileMessage(messages, fileData, role);
FileMessages.addNewAnyFileMessage(messages, fileData, role, isTop);
}
});
}
Expand Down
11 changes: 6 additions & 5 deletions component/src/views/chat/messages/html/htmlMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class HTMLMessages {
messages.elementRef.scrollTop = messages.elementRef.scrollHeight;
}

private static createElements(messages: MessagesBase, html: string, role: string) {
const messageElements = messages.createNewMessageElement('', role);
private static createElements(messages: MessagesBase, html: string, role: string, isTop: boolean) {
const messageElements = messages.createMessageElementsOnOrientation('', role, isTop);
messageElements.bubbleElement.classList.add('html-message');
messageElements.bubbleElement.innerHTML = html;
return messageElements;
Expand All @@ -31,18 +31,19 @@ export class HTMLMessages {

// prettier-ignore
public static add(
messages: MessagesBase, html: string, role: string, messagesEls: MessageElements[], overwrite?: Overwrite) {
messages: MessagesBase, html: string, role: string,
messagesEls: MessageElements[], overwrite?: Overwrite, isTop = false) {
if (overwrite?.status) {
const overwrittenElements = this.overwrite(messages, html, role, messagesEls);
if (overwrittenElements) return overwrittenElements;
overwrite.status = false;
}
const messageElements = HTMLMessages.createElements(messages, html, role);
const messageElements = HTMLMessages.createElements(messages, html, role, isTop);
MessageUtils.fillEmptyMessageElement(messageElements.bubbleElement, html);
HTMLUtils.apply(messages, messageElements.outerContainer);
Legacy.flagHTMLUpdateClass(messageElements.bubbleElement);
messages.applyCustomStyles(messageElements, role, false, messages.messageStyles?.html);
HTMLMessages.addElement(messages, messageElements.outerContainer);
if (!isTop) HTMLMessages.addElement(messages, messageElements.outerContainer);
return messageElements;
}
}
8 changes: 8 additions & 0 deletions component/src/views/chat/messages/messageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,12 @@ export class MessageUtils {
if (avatars) Avatar.hide(innerContainer);
if (names) Name.hide(innerContainer);
}

public static updateRefArr<T>(arr: Array<T>, item: T, isTop: boolean) {
if (isTop) {
arr.unshift(item);
} else {
arr.push(item);
}
}
}
47 changes: 15 additions & 32 deletions component/src/views/chat/messages/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import {Demo, DemoResponse} from '../../../types/demo';
import {MessageStyleUtils} from './messageStyleUtils';
import {IntroMessage} from '../../../types/messages';
import {MessageStream} from './stream/messageStream';
import {Legacy} from '../../../utils/legacy/legacy';
import {IntroPanel} from '../introPanel/introPanel';
import {FileMessageUtils} from './fileMessageUtils';
import {WebModel} from '../../../webModel/webModel';
import {CustomStyle} from '../../../types/styles';
import {MessagesHistory} from './messagesHistory';
import {HTMLMessages} from './html/htmlMessages';
import {SetupMessages} from './setupMessages';
import {FileMessages} from './fileMessages';
Expand All @@ -32,6 +32,7 @@ export interface MessageElements {
bubbleElement: HTMLElement;
}

// WORK - change setUp to setup
export class Messages extends MessagesBase {
private readonly _errorMessageOverrides?: ErrorMessageOverrides;
private readonly _onClearMessages?: () => void;
Expand All @@ -54,7 +55,7 @@ export class Messages extends MessagesBase {
this.populateIntroPanel(panel, introPanelMarkUp, deepChat.introPanelStyle);
}
this.addIntroductoryMessage(deepChat, serviceIO);
this.populateHistory(deepChat);
new MessagesHistory(deepChat, this, serviceIO);
this._displayServiceErrorMessages = deepChat.errorMessages?.displayServiceErrorMessages;
deepChat.getMessages = () => JSON.parse(JSON.stringify(this.messages));
deepChat.clearMessages = this.clearMessages.bind(this, serviceIO);
Expand All @@ -63,14 +64,14 @@ export class Messages extends MessagesBase {
deepChat.addMessage = (message: ResponseI, isUpdate?: boolean) => {
this.addAnyMessage({...message, sendUpdate: !!isUpdate}, !isUpdate);
};
// interface - setUpMessagesForService
if (serviceIO.isWebModel()) (serviceIO as WebModel).setUpMessages(this);
if (demo) this.prepareDemo(demo);
if (deepChat.textToSpeech) {
TextToSpeech.processConfig(deepChat.textToSpeech, (processedConfig) => {
this.textToSpeech = processedConfig;
});
}
if (serviceIO.fetchHistory) this.fetchHistory(serviceIO.fetchHistory);
}

private static getDisplayLoadingMessage(deepChat: DeepChat, serviceIO: ServiceIO) {
Expand All @@ -93,6 +94,7 @@ export class Messages extends MessagesBase {
}

private addSetupMessageIfNeeded(deepChat: DeepChat, serviceIO: ServiceIO) {
// interface - getSetUpMessage
const text = SetupMessages.getText(deepChat, serviceIO);
if (text) {
const elements = this.createAndAppendNewMessageElement(text, MessageUtils.AI_ROLE);
Expand All @@ -105,6 +107,7 @@ export class Messages extends MessagesBase {
private addIntroductoryMessage(deepChat?: DeepChat, serviceIO?: ServiceIO) {
if (deepChat?.shadowRoot) this._introMessage = deepChat.introMessage;
let introMessage = this._introMessage;
// interface - introMessage
if (serviceIO?.isWebModel()) introMessage ??= (serviceIO as WebModel).getIntroMessage(introMessage);
if (introMessage) {
let elements;
Expand All @@ -128,54 +131,34 @@ export class Messages extends MessagesBase {
}
}

private populateHistory(deepChat: DeepChat) {
const history = deepChat.history || Legacy.processHistory(deepChat);
if (!history) return;
history.forEach((message) => {
Legacy.processHistoryFile(message);
this.addNewMessage(message, true);
});
// attempt to wait for the font file to be downloaded as otherwise text dimensions change after scroll
// the timeout is sometimes not long enough - see the following on how user's can fix it:
// https://github.com/OvidijusParsiunas/deep-chat/issues/84
setTimeout(() => ElementUtils.scrollToBottom(this.elementRef), 0);
}

private async fetchHistory(ioFetchHistory: Required<ServiceIO>['fetchHistory']) {
const history = await ioFetchHistory();
history.forEach((message) => this.addAnyMessage(message, true));
// https://github.com/OvidijusParsiunas/deep-chat/issues/84
setTimeout(() => ElementUtils.scrollToBottom(this.elementRef), 0);
}

private addAnyMessage(message: ResponseI, isHistory = false) {
public addAnyMessage(message: ResponseI, isHistory = false, isTop = false) {
if (message.error) {
this.addNewErrorMessage('service', message.error);
} else {
this.addNewMessage(message, isHistory);
return this.addNewErrorMessage('service', message.error);
}
return this.addNewMessage(message, isHistory, isTop);
}

// this should not be activated by streamed messages
public addNewMessage(data: ResponseI, isHistory = false) {
public addNewMessage(data: ResponseI, isHistory = false, isTop = false) {
const message = Messages.createMessageContent(data);
const overwrite: Overwrite = {status: data.overwrite}; // if did not overwrite, create a new message
if (!data.ignoreText && message.text !== undefined && data.text !== null) {
this.addNewTextMessage(message.text, message.role, overwrite);
this.addNewTextMessage(message.text, message.role, overwrite, isTop);
if (!isHistory && this.textToSpeech && message.role !== MessageUtils.USER_ROLE) {
TextToSpeech.speak(message.text, this.textToSpeech);
}
}
if (message.files && Array.isArray(message.files)) {
FileMessages.addMessages(this, message.files, message.role);
FileMessages.addMessages(this, message.files, message.role, isTop);
}
if (message.html !== undefined && message.html !== null) {
const elements = HTMLMessages.add(this, message.html, message.role, this.messageElementRefs, overwrite);
const elements = HTMLMessages.add(this, message.html, message.role, this.messageElementRefs, overwrite, isTop);
if (HTMLDeepChatElements.isElementTemporary(elements)) delete message.html;
}
if (this.isValidMessageContent(message)) {
if (this.isValidMessageContent(message) && !isTop) {
this.updateStateOnMessage(message, data.overwrite, data.sendUpdate, isHistory);
}
return message;
}

private isValidMessageContent(messageContent: MessageContentI) {
Expand Down
29 changes: 22 additions & 7 deletions component/src/views/chat/messages/messagesBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,20 @@ export class MessagesBase {
return container;
}

public addNewTextMessage(text: string, role: string, overwrite?: Overwrite) {
public addNewTextMessage(text: string, role: string, overwrite?: Overwrite, isTop = false) {
if (overwrite?.status) {
const overwrittenElements = this.overwriteText(role, text, this.messageElementRefs);
if (overwrittenElements) return overwrittenElements;
overwrite.status = false;
}
const messageElements = this.createAndAppendNewMessageElement(text, role);
const messageElements = isTop
? this.createAndPrependNewMessageElement(text, role, isTop)
: this.createAndAppendNewMessageElement(text, role);
messageElements.bubbleElement.classList.add('text-message');
this.applyCustomStyles(messageElements, role, false);
MessageUtils.fillEmptyMessageElement(messageElements.bubbleElement, text);
this.textElementsToText.push([messageElements, text]);
const textElements: [MessageElements, string] = [messageElements, text];
MessageUtils.updateRefArr(this.textElementsToText, textElements, isTop);
return messageElements;
}

Expand All @@ -81,14 +84,26 @@ export class MessagesBase {
return messageElements;
}

public createNewMessageElement(text: string, role: string) {
private createAndPrependNewMessageElement(text: string, role: string, isTop: boolean) {
const messageElements = this.createNewMessageElement(text, role, isTop);
this.elementRef.insertBefore(messageElements.outerContainer, this.elementRef.firstChild);
return messageElements;
}

public createMessageElementsOnOrientation(text: string, role: string, isTop: boolean) {
return isTop
? this.createAndPrependNewMessageElement(text, role, isTop)
: this.createNewMessageElement(text, role, isTop);
}

public createNewMessageElement(text: string, role: string, isTop = false) {
this._introPanel?.hide();
const lastMessageElements = this.messageElementRefs[this.messageElementRefs.length - 1];
if (MessagesBase.isTemporaryElement(lastMessageElements)) {
lastMessageElements.outerContainer.remove();
this.messageElementRefs.pop();
}
return this.createMessageElements(text, role);
return this.createMessageElements(text, role, isTop);
}

protected static isTemporaryElement(elements: MessageElements) {
Expand All @@ -98,12 +113,12 @@ export class MessagesBase {
);
}

protected createMessageElements(text: string, role: string) {
protected createMessageElements(text: string, role: string, isTop = false) {
const messageElements = MessagesBase.createBaseElements();
const {outerContainer, innerContainer, bubbleElement} = messageElements;
outerContainer.appendChild(innerContainer);
this.addInnerContainerElements(bubbleElement, text, role);
this.messageElementRefs.push(messageElements);
MessageUtils.updateRefArr(this.messageElementRefs, messageElements, isTop);
return messageElements;
}

Expand Down
Loading

0 comments on commit 1f57797

Please sign in to comment.