Skip to content

Commit ccab043

Browse files
Shibani Basavashibbas
Shibani Basava
authored andcommitted
feat: make chatHistoryController use DI
1 parent bc04da9 commit ccab043

File tree

5 files changed

+159
-67
lines changed

5 files changed

+159
-67
lines changed

Diff for: packages/chat-component/src/components/chat-component.ts

+39-29
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { LitElement, html } from 'lit';
33
import DOMPurify from 'dompurify';
44
import { customElement, property, query, state } from 'lit/decorators.js';
5-
import { chatHttpOptions, globalConfig, requestOptions, MAX_CHAT_HISTORY } from '../config/global-config.js';
5+
import { chatHttpOptions, globalConfig, requestOptions } from '../config/global-config.js';
66
import { chatStyle } from '../styles/chat-component.js';
77
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
88
import { chatEntryToString, newListWithEntryAtIndex } from '../utils/index.js';
@@ -13,16 +13,16 @@ import iconDelete from '../../public/svg/delete-icon.svg?raw';
1313
import iconCancel from '../../public/svg/cancel-icon.svg?raw';
1414
import iconSend from '../../public/svg/send-icon.svg?raw';
1515
import iconLogo from '../../public/branding/brand-logo.svg?raw';
16-
import iconUp from '../../public/svg/chevron-up-icon.svg?raw';
1716

1817
import { ChatController } from './chat-controller.js';
19-
import { ChatHistoryController } from './chat-history-controller.js';
2018
import {
2119
lazyMultiInject,
2220
ControllerType,
2321
type ChatInputController,
2422
type ChatInputFooterController,
2523
type ChatSectionController,
24+
type ChatActionController,
25+
type ChatThreadController,
2626
} from './composable.js';
2727
import { ChatContextController } from './chat-context.js';
2828

@@ -99,7 +99,6 @@ export class ChatComponent extends LitElement {
9999
isResetInput = false;
100100

101101
private chatController = new ChatController(this);
102-
private chatHistoryController = new ChatHistoryController(this);
103102
private chatContext = new ChatContextController(this);
104103

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

118+
@lazyMultiInject(ControllerType.ChatAction)
119+
chatActionControllers: ChatActionController[] | undefined;
120+
121+
@lazyMultiInject(ControllerType.ChatThread)
122+
chatThreadControllers: ChatThreadController[] | undefined;
123+
119124
public constructor() {
120125
super();
121126
this.setQuestionInputValue = this.setQuestionInputValue.bind(this);
127+
this.renderChatThread = this.renderChatThread.bind(this);
122128
}
123129

124130
// Lifecycle method that runs when the component is first connected to the DOM
@@ -139,6 +145,16 @@ export class ChatComponent extends LitElement {
139145
component.attach(this, this.chatContext);
140146
}
141147
}
148+
if (this.chatActionControllers) {
149+
for (const component of this.chatActionControllers) {
150+
component.attach(this, this.chatContext);
151+
}
152+
}
153+
if (this.chatThreadControllers) {
154+
for (const component of this.chatThreadControllers) {
155+
component.attach(this, this.chatContext);
156+
}
157+
}
142158
}
143159

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

192-
const history = [
193-
...this.chatThread,
194-
// include the history from the previous session if the user has enabled the chat history
195-
...(this.chatHistoryController.showChatHistory ? this.chatHistoryController.chatHistory : []),
196-
];
208+
let thread: ChatThreadEntry[] = [...this.chatThread];
209+
if (this.chatThreadControllers) {
210+
for (const controller of this.chatThreadControllers) {
211+
thread = controller.merge(thread);
212+
}
213+
}
197214

198-
const messages: Message[] = history.map((entry) => {
215+
const messages: Message[] = thread.map((entry) => {
199216
return {
200217
content: chatEntryToString(entry),
201218
role: entry.isUserMessage ? 'user' : 'assistant',
@@ -234,7 +251,7 @@ export class ChatComponent extends LitElement {
234251
);
235252

236253
if (this.interactionModel === 'chat') {
237-
this.chatHistoryController.saveChatHistory(this.chatThread);
254+
this.saveChatThreads(this.chatThread);
238255
}
239256

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

269+
saveChatThreads(chatThread: ChatThreadEntry[]): void {
270+
if (this.chatThreadControllers) {
271+
for (const component of this.chatThreadControllers) {
272+
component.save(chatThread);
273+
}
274+
}
275+
}
276+
252277
// Reset the chat and show the default prompts
253278
resetCurrentChat(event: Event): void {
254279
this.isChatStarted = false;
255280
this.chatThread = [];
256281
this.isDisabled = false;
257282
this.chatContext.selectedCitation = undefined;
258283
this.chatController.reset();
259-
// clean up the current session content from the history too
260-
this.chatHistoryController.saveChatHistory(this.chatThread);
284+
this.saveChatThreads(this.chatThread);
261285
this.collapseAside(event);
262286
this.handleUserChatCancel(event);
263287
}
@@ -360,9 +384,7 @@ export class ChatComponent extends LitElement {
360384
${this.isChatStarted
361385
? html`
362386
<div class="chat__header--thread">
363-
${this.interactionModel === 'chat'
364-
? this.chatHistoryController.renderHistoryButton({ disabled: this.isDisabled })
365-
: ''}
387+
${this.chatActionControllers?.map((component) => component.render(this.isDisabled))}
366388
<chat-action-button
367389
.label="${globalConfig.RESET_CHAT_BUTTON_TITLE}"
368390
actionId="chat-reset-button"
@@ -371,19 +393,7 @@ export class ChatComponent extends LitElement {
371393
>
372394
</chat-action-button>
373395
</div>
374-
${this.chatHistoryController.showChatHistory
375-
? html`<div class="chat-history__container">
376-
${this.renderChatThread(this.chatHistoryController.chatHistory)}
377-
<div class="chat-history__footer">
378-
${unsafeSVG(iconUp)}
379-
${globalConfig.CHAT_HISTORY_FOOTER_TEXT.replace(
380-
globalConfig.CHAT_MAX_COUNT_TAG,
381-
MAX_CHAT_HISTORY,
382-
)}
383-
${unsafeSVG(iconUp)}
384-
</div>
385-
</div>`
386-
: ''}
396+
${this.chatThreadControllers?.map((component) => component.render(this.renderChatThread))}
387397
${this.renderChatThread(this.chatThread)}
388398
`
389399
: ''}
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,79 @@
1-
import { type ReactiveController, type ReactiveControllerHost } from 'lit';
2-
import { html } from 'lit';
1+
import { html, type TemplateResult } from 'lit';
32
import { globalConfig, MAX_CHAT_HISTORY } from '../config/global-config.js';
3+
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
44

55
import iconHistory from '../../public/svg/history-icon.svg?raw';
66
import iconHistoryDismiss from '../../public/svg/history-dismiss-icon.svg?raw';
7+
import iconUp from '../../public/svg/chevron-up-icon.svg?raw';
78

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

10-
export class ChatHistoryController implements ReactiveController {
11-
host: ReactiveControllerHost;
12-
static CHATHISTORY_ID = 'ms-azoaicc:history';
11+
import { injectable } from 'inversify';
12+
import {
13+
container,
14+
type ChatActionController,
15+
type ChatThreadController,
16+
ControllerType,
17+
ComposableReactiveControllerBase,
18+
} from './composable.js';
19+
20+
const CHATHISTORY_FEATURE_FLAG = 'showChatHistory';
21+
22+
@injectable()
23+
export class ChatHistoryActionButton extends ComposableReactiveControllerBase implements ChatActionController {
24+
constructor() {
25+
super();
26+
this.getShowChatHistory = this.getShowChatHistory.bind(this);
27+
this.setShowChatHistory = this.setShowChatHistory.bind(this);
28+
}
1329

14-
chatHistory: ChatThreadEntry[] = [];
30+
getShowChatHistory() {
31+
return this.context.getState(CHATHISTORY_FEATURE_FLAG);
32+
}
1533

16-
private _showChatHistory: boolean = false;
34+
setShowChatHistory(value: boolean) {
35+
this.context.setState(CHATHISTORY_FEATURE_FLAG, value);
36+
}
1737

18-
get showChatHistory() {
19-
return this._showChatHistory;
38+
render(isDisabled: boolean) {
39+
if (this.context.interactionModel === 'ask') {
40+
return html``;
41+
}
42+
43+
const showChatHistory = this.getShowChatHistory();
44+
return html`
45+
<chat-action-button
46+
.label="${showChatHistory ? globalConfig.HIDE_CHAT_HISTORY_LABEL : globalConfig.SHOW_CHAT_HISTORY_LABEL}"
47+
actionId="chat-history-button"
48+
@click="${() => this.setShowChatHistory(!showChatHistory)}"
49+
.isDisabled="${isDisabled}"
50+
.svgIcon="${showChatHistory ? iconHistoryDismiss : iconHistory}"
51+
>
52+
</chat-action-button>
53+
`;
2054
}
55+
}
2156

22-
set showChatHistory(value: boolean) {
23-
this._showChatHistory = value;
24-
this.host.requestUpdate();
57+
@injectable()
58+
export class ChatHistoryController extends ComposableReactiveControllerBase implements ChatThreadController {
59+
static CHATHISTORY_ID = 'ms-azoaicc:history';
60+
61+
private _chatHistory: ChatThreadEntry[] = [];
62+
63+
constructor() {
64+
super();
65+
this.getShowChatHistory = this.getShowChatHistory.bind(this);
2566
}
2667

27-
constructor(host: ReactiveControllerHost) {
28-
(this.host = host).addController(this);
68+
getShowChatHistory() {
69+
return this.context.getState(CHATHISTORY_FEATURE_FLAG);
2970
}
3071

31-
hostConnected() {
72+
override hostConnected() {
3273
const chatHistory = localStorage.getItem(ChatHistoryController.CHATHISTORY_ID);
3374
if (chatHistory) {
3475
// decode base64 string and then parse it
35-
const history = JSON.parse(atob(chatHistory));
76+
const history = JSON.parse(decodeURIComponent(atob(chatHistory)));
3677

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

50-
this.chatHistory = trimmedHistory;
91+
this._chatHistory = trimmedHistory;
5192
}
5293
}
5394

54-
hostDisconnected() {
55-
// no-op
95+
save(currentChat: ChatThreadEntry[]): void {
96+
const newChatHistory = [...this._chatHistory, ...currentChat];
97+
// encode to base64 string and then save it
98+
localStorage.setItem(
99+
ChatHistoryController.CHATHISTORY_ID,
100+
btoa(encodeURIComponent(JSON.stringify(newChatHistory))),
101+
);
56102
}
57103

58-
saveChatHistory(currentChat: ChatThreadEntry[]): void {
59-
const newChatHistory = [...this.chatHistory, ...currentChat];
60-
// encode to base64 string and then save it
61-
localStorage.setItem(ChatHistoryController.CHATHISTORY_ID, btoa(JSON.stringify(newChatHistory)));
104+
reset(): void {
105+
this._chatHistory = [];
62106
}
63107

64-
handleChatHistoryButtonClick(event: Event) {
65-
event.preventDefault();
66-
this.showChatHistory = !this.showChatHistory;
108+
merge(thread: ChatThreadEntry[]): ChatThreadEntry[] {
109+
// include the history from the previous session if the user has enabled the chat history
110+
return [...this._chatHistory, ...thread];
67111
}
68112

69-
renderHistoryButton(options: { disabled: boolean } | undefined) {
113+
render(threadRenderer: (thread: ChatThreadEntry[]) => TemplateResult) {
114+
const showChatHistory = this.getShowChatHistory();
115+
if (!showChatHistory) {
116+
return html``;
117+
}
118+
70119
return html`
71-
<chat-action-button
72-
.label="${this.showChatHistory ? globalConfig.HIDE_CHAT_HISTORY_LABEL : globalConfig.SHOW_CHAT_HISTORY_LABEL}"
73-
actionId="chat-history-button"
74-
@click="${(event) => this.handleChatHistoryButtonClick(event)}"
75-
.isDisabled="${options?.disabled}"
76-
.svgIcon="${this.showChatHistory ? iconHistoryDismiss : iconHistory}"
77-
>
78-
</chat-action-button>
120+
<div class="chat-history__container">
121+
${threadRenderer(this._chatHistory)}
122+
<div class="chat-history__footer">
123+
${unsafeSVG(iconUp)}
124+
${globalConfig.CHAT_HISTORY_FOOTER_TEXT.replace(globalConfig.CHAT_MAX_COUNT_TAG, MAX_CHAT_HISTORY)}
125+
${unsafeSVG(iconUp)}
126+
</div>
127+
</div>
79128
`;
80129
}
81130
}
131+
132+
container.bind<ChatActionController>(ControllerType.ChatAction).to(ChatHistoryActionButton);
133+
container.bind<ChatThreadController>(ControllerType.ChatThread).to(ChatHistoryController);

Diff for: packages/chat-component/src/components/composable.ts

+28
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const ControllerType = {
1313
ChatEntryAction: Symbol.for('ChatEntryActionController'),
1414
Citation: Symbol.for('CitationController'),
1515
ChatEntryInlineInput: Symbol.for('ChatEntryInlineInputController'),
16+
ChatAction: Symbol.for('ChatActionController'),
17+
ChatThread: Symbol.for('ChatThreadController'),
1618
};
1719

1820
export interface ComposableReactiveController extends ReactiveController {
@@ -48,6 +50,10 @@ export interface ChatSectionController extends ComposableReactiveController {
4850
render: () => TemplateResult;
4951
}
5052

53+
export interface ChatActionController extends ComposableReactiveController {
54+
render: (isDisabled: boolean) => TemplateResult;
55+
}
56+
5157
export interface ChatEntryActionController extends ComposableReactiveController {
5258
render: (entry: ChatThreadEntry, isDisabled: boolean) => TemplateResult;
5359
}
@@ -60,6 +66,14 @@ export interface CitationController extends ComposableReactiveController {
6066
render: (citation: Citation, url: string) => TemplateResult;
6167
}
6268

69+
export interface ChatThreadController extends ComposableReactiveController {
70+
save(thread: ChatThreadEntry[]): void;
71+
reset(): void;
72+
merge: (thread: ChatThreadEntry[]) => ChatThreadEntry[];
73+
// wrap the way the chat thread is rendered with additional components
74+
render: (threadRenderer: (thread: ChatThreadEntry[]) => TemplateResult) => TemplateResult;
75+
}
76+
6377
// Add a default component since inversify currently doesn't seem to support optional bindings
6478
// and bindings fail if no component is provided
6579
@injectable()
@@ -80,9 +94,23 @@ export class DefaultChatSectionController extends DefaultController implements C
8094
close() {}
8195
}
8296

97+
@injectable()
98+
export class DefaultChatThreadController extends ComposableReactiveControllerBase implements ChatThreadController {
99+
save() {}
100+
reset() {}
101+
merge(thread: ChatThreadEntry[]) {
102+
return thread;
103+
}
104+
render() {
105+
return html``;
106+
}
107+
}
108+
83109
container.bind<ChatInputController>(ControllerType.ChatInput).to(DefaultInputController);
84110
container.bind<ChatInputFooterController>(ControllerType.ChatInputFooter).to(DefaultController);
85111
container.bind<ChatSectionController>(ControllerType.ChatSection).to(DefaultChatSectionController);
86112
container.bind<ChatEntryActionController>(ControllerType.ChatEntryAction).to(DefaultController);
87113
container.bind<CitationController>(ControllerType.Citation).to(DefaultController);
88114
container.bind<ChatEntryInlineInputController>(ControllerType.ChatEntryInlineInput).to(DefaultController);
115+
container.bind<ChatActionController>(ControllerType.ChatAction).to(DefaultController);
116+
container.bind<ChatThreadController>(ControllerType.ChatThread).to(DefaultChatThreadController);

Diff for: packages/chat-component/src/components/features.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ import './document-previewer.js';
1414
import './citation-previewer.js';
1515

1616
import './follow-up-questions.js';
17+
18+
import './chat-history-controller.js';
1719
// [COMPOSE COMPONENTS END]

0 commit comments

Comments
 (0)