Skip to content

Commit

Permalink
refactoring messages object to use messageToElements to allow element…
Browse files Browse the repository at this point in the history
…s to be manipulated
  • Loading branch information
OvidijusParsiunas committed Nov 9, 2024
1 parent 1e0e917 commit bb70740
Show file tree
Hide file tree
Showing 11 changed files with 76 additions and 52 deletions.
14 changes: 7 additions & 7 deletions component/src/services/openAI/assistant/openAIAssistantIOI.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {AssistantFunctionHandler, OpenAI, OpenAIAssistant, OpenAINewAssistant} from '../../../types/openAI';
import {MessageContentI, MessageToElements} from '../../../types/messagesInternal';
import {OpenAIAssistantUtils, UploadedFile} from './utils/openAIAssistantUtils';
import {MessageStream} from '../../../views/chat/messages/stream/messageStream';
import {FileMessageUtils} from '../../../views/chat/messages/fileMessageUtils';
import {KeyVerificationDetails} from '../../../types/keyVerificationDetails';
import {OpenAIConverseBodyInternal} from '../../../types/openAIInternal';
import {History} from '../../../views/chat/messages/history/history';
import {MessageLimitUtils} from '../../utils/messageLimitUtils';
import {MessageContentI} from '../../../types/messagesInternal';
import {Messages} from '../../../views/chat/messages/messages';
import {Response as ResponseI} from '../../../types/response';
import {HTTPRequest} from '../../../utils/HTTP/HTTPRequest';
Expand Down Expand Up @@ -202,7 +202,7 @@ export class OpenAIAssistantIOI extends DirectServiceIO {
if (!this.connectSettings) throw new Error('Request settings have not been set up');
this.rawBody.assistant_id ??= this.config.assistant_id || (await this.createNewAssistant());
// here instead of constructor as messages may be loaded later
if (!this.searchedForThreadId) this.searchPreviousMessagesForThreadId(messages.messages);
if (!this.searchedForThreadId) this.searchPreviousMessagesForThreadId(messages.messageToElements);
const uploadedFiles = files
? await OpenAIAssistantUtils.storeFiles(this, messages, files, this.urlSegments.storeFiles)
: undefined;
Expand All @@ -223,9 +223,9 @@ export class OpenAIAssistantIOI extends DirectServiceIO {
return undefined;
}

private searchPreviousMessagesForThreadId(messages: MessageContentI[]) {
const messageWithSession = messages.find((message) => message._sessionId);
if (messageWithSession) this.sessionId = messageWithSession._sessionId;
private searchPreviousMessagesForThreadId(messageToElements: MessageToElements) {
const messageWithSession = messageToElements.find(([msgToEls]) => msgToEls._sessionId);
if (messageWithSession) this.sessionId = messageWithSession[0]._sessionId;
this.searchedForThreadId = true;
}

Expand Down Expand Up @@ -260,8 +260,8 @@ export class OpenAIAssistantIOI extends DirectServiceIO {
this.run_id = result.id;
// updates the user sent message with the session id (the message event sent did not have this id)
// user can clear the messages when they make a request, hence checking if messages length > 0
if (this.messages && this.messages.messages.length > 0) {
this.messages.messages[this.messages.messages.length - 1]._sessionId = this.sessionId;
if (this.messages && this.messages.messageToElements.length > 0) {
this.messages.messageToElements[this.messages.messageToElements.length - 1][0]._sessionId = this.sessionId;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion component/src/services/utils/baseServiceIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class BaseServiceIO implements ServiceIO {
async callAPI(requestContents: RequestContents, messages: Messages) {
if (!this.connectSettings) throw new Error('Request settings have not been set up');
const processedMessages = MessageLimitUtils.processMessages(
messages.messages, this.maxMessages, this.totalMessagesMaxCharLength);
messages.messageToElements.map(([msg]) => msg), this.maxMessages, this.totalMessagesMaxCharLength);
if (this.connectSettings.websocket) {
const body = {messages: processedMessages, ...this.rawBody};
Websocket.sendWebsocket(this, body, messages, false);
Expand Down
1 change: 0 additions & 1 deletion component/src/services/utils/messageLimitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export class MessageLimitUtils {
return messages.slice(Math.max(messages.length - maxMessages, 0));
}

// prettier-ignore
// if maxMessages is not defined we send all messages
// if maxMessages above 0 we send that number
// if maxMessages 0 or below we send only what is in the request
Expand Down
3 changes: 3 additions & 0 deletions component/src/types/messagesInternal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {MessageElements} from '../views/chat/messages/messages';
import {MessageFileType} from './messageFile';
import {PropsRequired} from './utilityTypes';
import {MessageContent} from './messages';

export type MessageToElements = [MessageContentI, MessageElements[]][];

export type MessageContentI = PropsRequired<MessageContent, 'role'>;

export type UserContentI = {text?: string; files?: {file: File; type: MessageFileType}[]};
Expand Down
14 changes: 7 additions & 7 deletions component/src/utils/demo/demo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {MessageContentI, MessageToElements} from '../../types/messagesInternal';
import {ServiceIO, StreamHandlers} from '../../services/serviceIO';
import {MessageContentI} from '../../types/messagesInternal';
import {Messages} from '../../views/chat/messages/messages';
import {DemoResponse} from '../../types/demo';
import {Response} from '../../types/response';
Expand All @@ -8,8 +8,8 @@ import {Stream} from '../HTTP/stream';
export class Demo {
public static readonly URL = 'deep-chat-demo';

private static generateResponse(messages: Messages) {
const requestMessage = messages.messages[messages.messages.length - 1];
private static generateResponse(messageToElements: MessageToElements) {
const requestMessage = messageToElements[messageToElements.length - 1][0];
if (requestMessage.files && requestMessage.files.length > 0) {
if (requestMessage.files.length > 1) {
return 'These are interesting files!';
Expand Down Expand Up @@ -42,10 +42,10 @@ export class Demo {
return customResponse;
}

private static getResponse(messages: Messages): Response {
return messages.customDemoResponse
? Demo.getCustomResponse(messages.customDemoResponse, messages.messages[messages.messages.length - 1])
: {text: Demo.generateResponse(messages)};
private static getResponse({customDemoResponse, messageToElements}: Messages): Response {
return customDemoResponse
? Demo.getCustomResponse(customDemoResponse, messageToElements[messageToElements.length - 1][0])
: {text: Demo.generateResponse(messageToElements)};
}

// timeout is used to simulate a timeout for a response to come back
Expand Down
19 changes: 12 additions & 7 deletions component/src/views/chat/messages/history/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {ServiceIO} from '../../../../services/serviceIO';
import {Legacy} from '../../../../utils/legacy/legacy';
import {LoadingHistory} from './loadingHistory';
import {DeepChat} from '../../../../deepChat';
import {MessageUtils} from '../messageUtils';
import {Messages} from '../messages';

export class History {
Expand All @@ -32,23 +33,27 @@ export class History {
}

private processLoadedHistory(historyMessages: HistoryMessage[]) {
const firstMessageEl = this._messages.messageElementRefs[0]?.outerContainer;
const currentScrollTop = this._messages.elementRef.scrollTop;
const {messageElementRefs, messageToElements, elementRef, addAnyMessage, sendClientUpdate} = this._messages;
const firstMessageEl = messageElementRefs[0]?.outerContainer;
const currentScrollTop = elementRef.scrollTop;
historyMessages
?.reverse()
.map((message) => {
if (message) {
const messageContent = this._messages.addAnyMessage({...message, sendUpdate: true}, true, true);
if (messageContent) this._messages.messages.unshift(messageContent);
const messageContent = addAnyMessage({...message, sendUpdate: true}, true, true);
if (messageContent) {
const num = MessageUtils.getNumberOfElements(messageContent);
messageToElements.unshift([messageContent, messageElementRefs.slice(0, num)]);
}
return messageContent;
} else {
this._isPaginationComplete = true;
}
})
.filter((message) => !!message)
.reverse()
.forEach((message) => this._messages.sendClientUpdate(message as MessageContentI, true));
if (firstMessageEl) this._messages.elementRef.scrollTop = currentScrollTop + firstMessageEl.offsetTop;
.forEach((message) => sendClientUpdate(message as MessageContentI, true));
if (firstMessageEl) elementRef.scrollTop = currentScrollTop + firstMessageEl.offsetTop;
}

private async setupLoadHistoryOnScroll(loadHistory: LoadHistory) {
Expand Down Expand Up @@ -87,7 +92,7 @@ export class History {
this._messages.removeMessage(loadingElements);
this._isPaginationComplete = !!messages.find((message) => !message);
const messageContent = messages.filter((message) => !!message);
this.processLoadedHistory(messageContent as MessageContent[]);
this.processLoadedHistory(messageContent);
// force scroll to bottom if user has not scrolled anywhere themselves, otherwise keep at current location
if (scrollTop === 0) {
// https://github.com/OvidijusParsiunas/deep-chat/issues/84
Expand Down
4 changes: 2 additions & 2 deletions component/src/views/chat/messages/html/htmlMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export class HTMLMessages {
}

private static overwriteLast(messages: MessagesBase, html: string, role: string, messagesEls: MessageElements[]) {
const {messages: aMessages} = messages;
const overwrittenElements = MessageUtils.overwriteMessage(aMessages, messagesEls, html, role, 'html', 'html-message');
const {messageToElements: msgToEls} = messages;
const overwrittenElements = MessageUtils.overwriteMessage(msgToEls, messagesEls, html, role, 'html', 'html-message');
if (overwrittenElements) HTMLMessages.overwrite(messages, html, overwrittenElements);
return overwrittenElements;
}
Expand Down
24 changes: 16 additions & 8 deletions component/src/views/chat/messages/messageUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {MessageContentI, MessageToElements} from '../../../types/messagesInternal';
import {LoadingStyle} from '../../../utils/loading/loadingStyle';
import {MessageContentI} from '../../../types/messagesInternal';
import {MessageContent} from '../../../types/messages';
import {Avatars} from '../../../types/avatars';
import {MessageElements} from './messages';
Expand Down Expand Up @@ -33,13 +33,13 @@ export class MessageUtils {
return undefined;
}

public static getLastMessage(messages: MessageContentI[], role: string, content?: keyof Omit<MessageContent, 'role'>) {
for (let i = messages.length - 1; i >= 0; i -= 1) {
if (messages[i].role === role) {
public static getLastMessage(msgToEls: MessageToElements, role: string, content?: keyof Omit<MessageContent, 'role'>) {
for (let i = msgToEls.length - 1; i >= 0; i -= 1) {
if (msgToEls[i][0].role === role) {
if (content) {
if (messages[i][content]) return messages[i];
if (msgToEls[i][0][content]) return msgToEls[i][0];
} else {
return messages[i];
return msgToEls[i][0];
}
}
}
Expand All @@ -58,12 +58,12 @@ export class MessageUtils {
// IMPORTANT: If the overwrite message does not contain a role property it will look for the last 'ai' role message
// and if messages have custom roles, it will still look to update the last 'ai' role message
// prettier-ignore
public static overwriteMessage(messages: MessageContentI[], messagesElements: MessageElements[],
public static overwriteMessage(messageToElements: MessageToElements, messagesElements: MessageElements[],
content: string, role: string, contentType: 'text' | 'html', className: string) {
// not sure if LoadingStyle.LOADING_MESSAGE_TEXT_CLASS is needed
const elements = MessageUtils.getLastElementsByClass(
messagesElements, [MessageUtils.getRoleClass(role), className], [LoadingStyle.BUBBLE_CLASS]);
const lastMessage = MessageUtils.getLastMessage(messages, role, contentType);
const lastMessage = MessageUtils.getLastMessage(messageToElements, role, contentType);
if (lastMessage) lastMessage[contentType] = content;
return elements;
}
Expand Down Expand Up @@ -133,6 +133,14 @@ export class MessageUtils {
messageEls.outerContainer.classList.add(...classes);
}

public static getNumberOfElements(messageContent: MessageContentI) {
let length = 0;
if (messageContent.text !== undefined) length += 1;
if (messageContent.html !== undefined) length += 1;
if (messageContent.files) length += messageContent.files.length;
return length;
}

public static classifyMessages(role: string, messageElementRefs: MessageElements[]) {
const currentRole = MessageUtils.buildRoleContainerClass(role);
messageElementRefs.forEach((messageEls, index) => {
Expand Down
27 changes: 18 additions & 9 deletions component/src/views/chat/messages/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class Messages extends MessagesBase {
this.addIntroductoryMessages(deepChat, serviceIO);
new History(deepChat, this, serviceIO);
this._displayServiceErrorMessages = deepChat.errorMessages?.displayServiceErrorMessages;
deepChat.getMessages = () => JSON.parse(JSON.stringify(this.messages));
deepChat.getMessages = () => JSON.parse(JSON.stringify(this.messageToElements.map(([msg]) => msg)));
deepChat.clearMessages = this.clearMessages.bind(this, serviceIO);
deepChat.refreshMessages = this.refreshTextMessages.bind(this);
deepChat.scrollToBottom = ElementUtils.scrollToBottom.bind(this, this.elementRef);
Expand Down Expand Up @@ -183,7 +183,10 @@ export class Messages extends MessagesBase {
}

private updateStateOnMessage(messageContent: MessageContentI, overwritten?: boolean, update = true, isHistory = false) {
if (!overwritten) this.messages.push(messageContent);
if (!overwritten) {
const num = MessageUtils.getNumberOfElements(messageContent);
this.messageToElements.push([messageContent, this.messageElementRefs.slice(this.messageElementRefs.length - num)]);
}
if (update) this.sendClientUpdate(messageContent, isHistory);
}

Expand Down Expand Up @@ -323,18 +326,17 @@ export class Messages extends MessagesBase {
// WORK - update all message classes to use deep-chat prefix
private clearMessages(serviceIO: ServiceIO, isReset?: boolean) {
const retainedElements: MessageElements[] = [];
const retainedTextElemenets: [MessageElements, string][] = [];
const retainedTextElements: [MessageElements, string][] = [];
this.messageElementRefs.forEach((message) => {
const bubbleClasslist = message.bubbleElement.classList;
if (Messages.isActiveElement(bubbleClasslist)) {
if (Messages.isActiveElement(message.bubbleElement.classList)) {
retainedElements.push(message);
} else {
message.outerContainer.remove();
}
});
this.textElementsToText.forEach((textElementToText) => {
const bubbleClasslist = textElementToText[0].bubbleElement.classList;
if (Messages.isActiveElement(bubbleClasslist)) retainedTextElemenets.push(textElementToText);
if (Messages.isActiveElement(bubbleClasslist)) retainedTextElements.push(textElementToText);
});
// this is a form of cleanup as this.messageElementRefs does not contain error messages
// and can only be deleted by direct search
Expand All @@ -345,12 +347,19 @@ export class Messages extends MessagesBase {
}
});
this.messageElementRefs = retainedElements;
this.messages.splice(0, this.messages.length);
const retainedMessageToElements = this.messageToElements.filter((elToMessage) => {
if (elToMessage[0].text !== undefined || elToMessage[0].html !== undefined) {
// safe because streamed messages can't contain multiple props (text, html)
return elToMessage[1].find((els) => Messages.isActiveElement(els.bubbleElement.classList));
}
return false;
});
this.messageToElements.splice(0, this.messageToElements.length, ...retainedMessageToElements);
if (isReset !== false) {
if (this._introPanel?._elementRef) this._introPanel.display();
this.addIntroductoryMessages();
}
this.textElementsToText = retainedTextElemenets;
this.textElementsToText = retainedTextElements;
this._onClearMessages?.();
delete serviceIO.sessionId;
}
Expand All @@ -363,7 +372,7 @@ export class Messages extends MessagesBase {
return console.error('The second argument of updateHTMLMessage must be of type Number');
}
const processedIndex = Math.floor(index);
const message = this.messages[processedIndex];
const message = this.messageToElements[processedIndex][0];
if (!message?.html) {
return console.error(`The message at index ${processedIndex} does not contain a 'html' message`);
}
Expand Down
18 changes: 9 additions & 9 deletions component/src/views/chat/messages/messagesBase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {MessageElementsStyles, MessageRoleStyles, MessageStyles, UserContent} from '../../../types/messages';
import {MessageContentI, Overwrite} from '../../../types/messagesInternal';
import {MessageContentI, MessageToElements, Overwrite} from '../../../types/messagesInternal';
import {ProcessedTextToSpeechConfig} from './textToSpeech/textToSpeech';
import {ElementUtils} from '../../../utils/element/elementUtils';
import {HTMLDeepChatElements} from './html/htmlDeepChatElements';
Expand All @@ -24,8 +24,8 @@ export class MessagesBase {
submitUserMessage?: (content: UserContent) => void;
readonly elementRef: HTMLElement;
readonly messageStyles?: MessageStyles;
readonly messages: MessageContentI[] = [];
readonly htmlClassUtilities: HTMLClassUtilities = {};
readonly messageToElements: MessageToElements = [];
textElementsToText: [MessageElements, string][] = [];
htmlElementsToMessage: [MessageElements, MessageContentI][] = [];
protected _introPanel?: IntroPanel;
Expand Down Expand Up @@ -71,13 +71,13 @@ export class MessagesBase {
}

private overwriteText(role: string, text: string, elementRefs: MessageElements[]) {
const elements = MessageUtils.overwriteMessage(this.messages, elementRefs, text, role, 'text', 'text-message');
if (elements) {
this.renderText(elements.bubbleElement, text);
const elementToText = MessageUtils.getLastTextToElement(this.textElementsToText, elements);
const elems = MessageUtils.overwriteMessage(this.messageToElements, elementRefs, text, role, 'text', 'text-message');
if (elems) {
this.renderText(elems.bubbleElement, text);
const elementToText = MessageUtils.getLastTextToElement(this.textElementsToText, elems);
if (elementToText) elementToText[1] = text;
}
return elements;
return elems;
}

protected createAndAppendNewMessageElement(text: string, role: string) {
Expand Down Expand Up @@ -124,7 +124,7 @@ export class MessagesBase {
if ((this._avatars || this._names) && HTMLDeepChatElements.isElementTemporary(tempElements)) {
// if prev message before temp has a different role to the new one, make sure its avatar is revealed
const prevMessageElements = this.messageElementRefs[this.messageElementRefs.length - 2];
if (prevMessageElements && this.messages[this.messages.length - 1]
if (prevMessageElements && this.messageToElements.length > 0
&& !tempElements.bubbleElement.classList.contains(MessageUtils.getRoleClass(newRole))) {
MessageUtils.revealRoleElements(prevMessageElements.innerContainer, this._avatars, this._names);
}
Expand Down Expand Up @@ -160,7 +160,7 @@ export class MessagesBase {

// prettier-ignore
private addInnerContainerElements(bubbleElement: HTMLElement, text: string, role: string) {
if (this.messages[this.messages.length - 1]?.role === role && !this.isLastMessageError()) {
if (this.messageToElements[this.messageToElements.length - 1]?.[0].role === role && !this.isLastMessageError()) {
MessageUtils.hideRoleElements(this.messageElementRefs, !!this._avatars, !!this._names);
}
bubbleElement.classList.add('message-bubble', MessageUtils.getRoleClass(role),
Expand Down
2 changes: 1 addition & 1 deletion component/src/views/chat/messages/stream/messageStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class MessageStream {
this._streamedContent = content;
this._activeMessageRole = role;
this._message = {role: this._activeMessageRole, [streamType]: this._streamedContent};
this._messages.messages.push(this._message);
this._messages.messageToElements.push([this._message, [this._elements]]);
}

private updateBasedOnType(content: string, expectedType: string, bubbleElement: HTMLElement, isOverwrite = false) {
Expand Down

0 comments on commit bb70740

Please sign in to comment.