Skip to content

Commit

Permalink
feat: make chatHistoryController use DI
Browse files Browse the repository at this point in the history
  • Loading branch information
Shibani Basava authored and Shibani Basava committed Feb 16, 2024
1 parent 7a5354e commit d958d3f
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 67 deletions.
68 changes: 39 additions & 29 deletions packages/chat-component/src/components/chat-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { LitElement, html } from 'lit';
import DOMPurify from 'dompurify';
import { customElement, property, query, state } from 'lit/decorators.js';
import { chatHttpOptions, globalConfig, requestOptions, MAX_CHAT_HISTORY } from '../config/global-config.js';
import { chatHttpOptions, globalConfig, requestOptions } from '../config/global-config.js';
import { chatStyle } from '../styles/chat-component.js';
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
import { chatEntryToString, newListWithEntryAtIndex } from '../utils/index.js';
Expand All @@ -13,16 +13,16 @@ import iconDelete from '../../public/svg/delete-icon.svg?raw';
import iconCancel from '../../public/svg/cancel-icon.svg?raw';
import iconSend from '../../public/svg/send-icon.svg?raw';
import iconLogo from '../../public/branding/brand-logo.svg?raw';
import iconUp from '../../public/svg/chevron-up-icon.svg?raw';

import { ChatController } from './chat-controller.js';
import { ChatHistoryController } from './chat-history-controller.js';
import {
lazyMultiInject,
ControllerType,
type ChatInputController,
type ChatInputFooterController,
type ChatSectionController,
type ChatActionController,
type ChatThreadController,
} from './composable.js';
import { ChatContextController } from './chat-context.js';

Expand Down Expand Up @@ -99,7 +99,6 @@ export class ChatComponent extends LitElement {
isResetInput = false;

private chatController = new ChatController(this);
private chatHistoryController = new ChatHistoryController(this);
private chatContext = new ChatContextController(this);

// These are the chat bubbles that will be displayed in the chat
Expand All @@ -116,9 +115,16 @@ export class ChatComponent extends LitElement {
@lazyMultiInject(ControllerType.ChatSection)
chatSectionControllers: ChatSectionController[] | undefined;

@lazyMultiInject(ControllerType.ChatAction)
chatActionControllers: ChatActionController[] | undefined;

@lazyMultiInject(ControllerType.ChatThread)
chatThreadControllers: ChatThreadController[] | undefined;

public constructor() {
super();
this.setQuestionInputValue = this.setQuestionInputValue.bind(this);
this.renderChatThread = this.renderChatThread.bind(this);
}

// Lifecycle method that runs when the component is first connected to the DOM
Expand All @@ -139,6 +145,16 @@ export class ChatComponent extends LitElement {
component.attach(this, this.chatContext);
}
}
if (this.chatActionControllers) {
for (const component of this.chatActionControllers) {
component.attach(this, this.chatContext);
}
}
if (this.chatThreadControllers) {
for (const component of this.chatThreadControllers) {
component.attach(this, this.chatContext);
}
}
}

override updated(changedProperties: Map<string | number | symbol, unknown>) {
Expand Down Expand Up @@ -189,13 +205,14 @@ export class ChatComponent extends LitElement {
return [];
}

const history = [
...this.chatThread,
// include the history from the previous session if the user has enabled the chat history
...(this.chatHistoryController.showChatHistory ? this.chatHistoryController.chatHistory : []),
];
let thread: ChatThreadEntry[] = [...this.chatThread];
if (this.chatThreadControllers) {
for (const controller of this.chatThreadControllers) {
thread = controller.merge(thread);
}
}

const messages: Message[] = history.map((entry) => {
const messages: Message[] = thread.map((entry) => {
return {
content: chatEntryToString(entry),
role: entry.isUserMessage ? 'user' : 'assistant',
Expand Down Expand Up @@ -234,7 +251,7 @@ export class ChatComponent extends LitElement {
);

if (this.interactionModel === 'chat') {
this.chatHistoryController.saveChatHistory(this.chatThread);
this.saveChatThreads(this.chatThread);
}

this.questionInput.value = '';
Expand All @@ -249,15 +266,22 @@ export class ChatComponent extends LitElement {
this.isResetInput = false;
}

saveChatThreads(chatThread: ChatThreadEntry[]): void {
if (this.chatThreadControllers) {
for (const component of this.chatThreadControllers) {
component.save(chatThread);
}
}
}

// Reset the chat and show the default prompts
resetCurrentChat(event: Event): void {
this.isChatStarted = false;
this.chatThread = [];
this.isDisabled = false;
this.chatContext.selectedCitation = undefined;
this.chatController.reset();
// clean up the current session content from the history too
this.chatHistoryController.saveChatHistory(this.chatThread);
this.saveChatThreads(this.chatThread);
this.collapseAside(event);
this.handleUserChatCancel(event);
}
Expand Down Expand Up @@ -360,9 +384,7 @@ export class ChatComponent extends LitElement {
${this.isChatStarted
? html`
<div class="chat__header--thread">
${this.interactionModel === 'chat'
? this.chatHistoryController.renderHistoryButton({ disabled: this.isDisabled })
: ''}
${this.chatActionControllers?.map((component) => component.render(this.isDisabled))}
<chat-action-button
.label="${globalConfig.RESET_CHAT_BUTTON_TITLE}"
actionId="chat-reset-button"
Expand All @@ -371,19 +393,7 @@ export class ChatComponent extends LitElement {
>
</chat-action-button>
</div>
${this.chatHistoryController.showChatHistory
? html`<div class="chat-history__container">
${this.renderChatThread(this.chatHistoryController.chatHistory)}
<div class="chat-history__footer">
${unsafeSVG(iconUp)}
${globalConfig.CHAT_HISTORY_FOOTER_TEXT.replace(
globalConfig.CHAT_MAX_COUNT_TAG,
MAX_CHAT_HISTORY,
)}
${unsafeSVG(iconUp)}
</div>
</div>`
: ''}
${this.chatThreadControllers?.map((component) => component.render(this.renderChatThread))}
${this.renderChatThread(this.chatThread)}
`
: ''}
Expand Down
122 changes: 87 additions & 35 deletions packages/chat-component/src/components/chat-history-controller.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,79 @@
import { type ReactiveController, type ReactiveControllerHost } from 'lit';
import { html } from 'lit';
import { html, type TemplateResult } from 'lit';
import { globalConfig, MAX_CHAT_HISTORY } from '../config/global-config.js';
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';

import iconHistory from '../../public/svg/history-icon.svg?raw';
import iconHistoryDismiss from '../../public/svg/history-dismiss-icon.svg?raw';
import iconUp from '../../public/svg/chevron-up-icon.svg?raw';

import './chat-action-button.js';

export class ChatHistoryController implements ReactiveController {
host: ReactiveControllerHost;
static CHATHISTORY_ID = 'ms-azoaicc:history';
import { injectable } from 'inversify';
import {
container,
type ChatActionController,
type ChatThreadController,
ControllerType,
ComposableReactiveControllerBase,
} from './composable.js';

const CHATHISTORY_FEATURE_FLAG = 'showChatHistory';

@injectable()
export class ChatHistoryActionButton extends ComposableReactiveControllerBase implements ChatActionController {
constructor() {
super();
this.getShowChatHistory = this.getShowChatHistory.bind(this);
this.setShowChatHistory = this.setShowChatHistory.bind(this);
}

chatHistory: ChatThreadEntry[] = [];
getShowChatHistory() {
return this.context.getState(CHATHISTORY_FEATURE_FLAG);
}

private _showChatHistory: boolean = false;
setShowChatHistory(value: boolean) {
this.context.setState(CHATHISTORY_FEATURE_FLAG, value);
}

get showChatHistory() {
return this._showChatHistory;
render(isDisabled: boolean) {
if (this.context.interactionModel === 'ask') {
return html``;
}

const showChatHistory = this.getShowChatHistory();
return html`
<chat-action-button
.label="${showChatHistory ? globalConfig.HIDE_CHAT_HISTORY_LABEL : globalConfig.SHOW_CHAT_HISTORY_LABEL}"
actionId="chat-history-button"
@click="${() => this.setShowChatHistory(!showChatHistory)}"
.isDisabled="${isDisabled}"
.svgIcon="${showChatHistory ? iconHistoryDismiss : iconHistory}"
>
</chat-action-button>
`;
}
}

set showChatHistory(value: boolean) {
this._showChatHistory = value;
this.host.requestUpdate();
@injectable()
export class ChatHistoryController extends ComposableReactiveControllerBase implements ChatThreadController {
static CHATHISTORY_ID = 'ms-azoaicc:history';

private _chatHistory: ChatThreadEntry[] = [];

constructor() {
super();
this.getShowChatHistory = this.getShowChatHistory.bind(this);
}

constructor(host: ReactiveControllerHost) {
(this.host = host).addController(this);
getShowChatHistory() {
return this.context.getState(CHATHISTORY_FEATURE_FLAG);
}

hostConnected() {
override hostConnected() {
const chatHistory = localStorage.getItem(ChatHistoryController.CHATHISTORY_ID);
if (chatHistory) {
// decode base64 string and then parse it
const history = JSON.parse(atob(chatHistory));
const history = JSON.parse(decodeURIComponent(atob(chatHistory)));

// find last 5 user messages indexes
const lastUserMessagesIndexes = history
Expand All @@ -47,35 +88,46 @@ export class ChatHistoryController implements ReactiveController {
// trim everything before the first user message
const trimmedHistory = lastUserMessagesIndexes.length === 0 ? history : history.slice(lastUserMessagesIndexes[0]);

this.chatHistory = trimmedHistory;
this._chatHistory = trimmedHistory;
}
}

hostDisconnected() {
// no-op
save(currentChat: ChatThreadEntry[]): void {
const newChatHistory = [...this._chatHistory, ...currentChat];
// encode to base64 string and then save it
localStorage.setItem(
ChatHistoryController.CHATHISTORY_ID,
btoa(encodeURIComponent(JSON.stringify(newChatHistory))),
);
}

saveChatHistory(currentChat: ChatThreadEntry[]): void {
const newChatHistory = [...this.chatHistory, ...currentChat];
// encode to base64 string and then save it
localStorage.setItem(ChatHistoryController.CHATHISTORY_ID, btoa(JSON.stringify(newChatHistory)));
reset(): void {
this._chatHistory = [];
}

handleChatHistoryButtonClick(event: Event) {
event.preventDefault();
this.showChatHistory = !this.showChatHistory;
merge(thread: ChatThreadEntry[]): ChatThreadEntry[] {
// include the history from the previous session if the user has enabled the chat history
return [...this._chatHistory, ...thread];
}

renderHistoryButton(options: { disabled: boolean } | undefined) {
render(threadRenderer: (thread: ChatThreadEntry[]) => TemplateResult) {
const showChatHistory = this.getShowChatHistory();
if (!showChatHistory) {
return html``;
}

return html`
<chat-action-button
.label="${this.showChatHistory ? globalConfig.HIDE_CHAT_HISTORY_LABEL : globalConfig.SHOW_CHAT_HISTORY_LABEL}"
actionId="chat-history-button"
@click="${(event) => this.handleChatHistoryButtonClick(event)}"
.isDisabled="${options?.disabled}"
.svgIcon="${this.showChatHistory ? iconHistoryDismiss : iconHistory}"
>
</chat-action-button>
<div class="chat-history__container">
${threadRenderer(this._chatHistory)}
<div class="chat-history__footer">
${unsafeSVG(iconUp)}
${globalConfig.CHAT_HISTORY_FOOTER_TEXT.replace(globalConfig.CHAT_MAX_COUNT_TAG, MAX_CHAT_HISTORY)}
${unsafeSVG(iconUp)}
</div>
</div>
`;
}
}

container.bind<ChatActionController>(ControllerType.ChatAction).to(ChatHistoryActionButton);
container.bind<ChatThreadController>(ControllerType.ChatThread).to(ChatHistoryController);
28 changes: 28 additions & 0 deletions packages/chat-component/src/components/composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const ControllerType = {
ChatEntryAction: Symbol.for('ChatEntryActionController'),
Citation: Symbol.for('CitationController'),
ChatEntryInlineInput: Symbol.for('ChatEntryInlineInputController'),
ChatAction: Symbol.for('ChatActionController'),
ChatThread: Symbol.for('ChatThreadController'),
};

export interface ComposableReactiveController extends ReactiveController {
Expand Down Expand Up @@ -48,6 +50,10 @@ export interface ChatSectionController extends ComposableReactiveController {
render: () => TemplateResult;
}

export interface ChatActionController extends ComposableReactiveController {
render: (isDisabled: boolean) => TemplateResult;
}

export interface ChatEntryActionController extends ComposableReactiveController {
render: (entry: ChatThreadEntry, isDisabled: boolean) => TemplateResult;
}
Expand All @@ -60,6 +66,14 @@ export interface CitationController extends ComposableReactiveController {
render: (citation: Citation, url: string) => TemplateResult;
}

export interface ChatThreadController extends ComposableReactiveController {
save(thread: ChatThreadEntry[]): void;
reset(): void;
merge: (thread: ChatThreadEntry[]) => ChatThreadEntry[];
// wrap the way the chat thread is rendered with additional components
render: (threadRenderer: (thread: ChatThreadEntry[]) => TemplateResult) => TemplateResult;
}

// Add a default component since inversify currently doesn't seem to support optional bindings
// and bindings fail if no component is provided
@injectable()
Expand All @@ -80,9 +94,23 @@ export class DefaultChatSectionController extends DefaultController implements C
close() {}
}

@injectable()
export class DefaultChatThreadController extends ComposableReactiveControllerBase implements ChatThreadController {
save() {}
reset() {}
merge(thread: ChatThreadEntry[]) {
return thread;
}
render() {
return html``;
}
}

container.bind<ChatInputController>(ControllerType.ChatInput).to(DefaultInputController);
container.bind<ChatInputFooterController>(ControllerType.ChatInputFooter).to(DefaultController);
container.bind<ChatSectionController>(ControllerType.ChatSection).to(DefaultChatSectionController);
container.bind<ChatEntryActionController>(ControllerType.ChatEntryAction).to(DefaultController);
container.bind<CitationController>(ControllerType.Citation).to(DefaultController);
container.bind<ChatEntryInlineInputController>(ControllerType.ChatEntryInlineInput).to(DefaultController);
container.bind<ChatActionController>(ControllerType.ChatAction).to(DefaultController);
container.bind<ChatThreadController>(ControllerType.ChatThread).to(DefaultChatThreadController);
2 changes: 2 additions & 0 deletions packages/chat-component/src/components/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ import './document-previewer.js';
import './citation-previewer.js';

import './follow-up-questions.js';

import './chat-history-controller.js';
// [COMPOSE COMPONENTS END]
Loading

0 comments on commit d958d3f

Please sign in to comment.