Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show / hide past conversations before the current session #153

Merged
merged 8 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/chat-component/public/svg/chevron-up-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/chat-component/public/svg/history-dismiss-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/chat-component/public/svg/history-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 80 additions & 20 deletions packages/chat-component/src/components/chat-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import { LitElement, html } from 'lit';
import DOMPurify from 'dompurify';
import { customElement, property, query, state } from 'lit/decorators.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { chatHttpOptions, globalConfig, teaserListTexts, requestOptions } from '../config/global-config.js';
import {
chatHttpOptions,
globalConfig,
teaserListTexts,
requestOptions,
MAX_CHAT_HISTORY,
} from '../config/global-config.js';
import { chatStyle } from '../styles/chat-component.js';
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
import { produce } from 'immer';
import { chatEntryToString } from '../utils/index.js';

// TODO: allow host applications to customize these icons

Expand All @@ -15,6 +22,7 @@ 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 iconClose from '../../public/svg/close-icon.svg?raw';
import iconUp from '../../public/svg/chevron-up-icon.svg?raw';

import './loading-indicator.js';
import './voice-input-button.js';
Expand All @@ -26,6 +34,7 @@ import './chat-thread-component.js';
import './chat-action-button.js';
import { type TabContent } from './tab-component.js';
import { ChatController } from './chat-controller.js';
import { ChatHistoryController } from './chat-history-controller.js';

/**
* A chat component that allows the user to ask questions and get answers from an API.
Expand Down Expand Up @@ -78,6 +87,7 @@ export class ChatComponent extends LitElement {
isResetInput = false;

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

// Is showing thought process panel
@state()
Expand Down Expand Up @@ -127,14 +137,34 @@ export class ChatComponent extends LitElement {
}
}

getMessageContext(): Message[] {
if (this.interactionModel === 'ask') {
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 : []),
];

const messages: Message[] = history.map((entry) => {
return {
content: chatEntryToString(entry),
role: entry.isUserMessage ? 'user' : 'assistant',
};
});

return messages;
}

// Handle the click on the chat button and send the question to the API
async handleUserChatSubmit(event: Event): Promise<void> {
event.preventDefault();
this.collapseAside(event);
const question = DOMPurify.sanitize(this.questionInput.value);
this.isChatStarted = true;
this.isDefaultPromptsEnabled = false;
this.questionInput.value = '';

await this.chatController.generateAnswer(
{
Expand All @@ -145,6 +175,7 @@ export class ChatComponent extends LitElement {
},
question,
type: this.interactionModel,
messages: this.getMessageContext(),
},
{
// use defaults
Expand All @@ -155,6 +186,13 @@ export class ChatComponent extends LitElement {
stream: this.useStream,
},
);

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

this.questionInput.value = '';
this.isResetInput = false;
}

// Reset the input field and the current question
Expand All @@ -173,6 +211,8 @@ export class ChatComponent extends LitElement {
this.isDefaultPromptsEnabled = true;
this.selectedCitation = undefined;
this.chatController.reset();
// clean up the current session content from the history too
this.chatHistoryController.saveChatHistory(this.chatThread);
this.collapseAside(event);
this.handleUserChatCancel(event);
}
Expand Down Expand Up @@ -309,6 +349,27 @@ export class ChatComponent extends LitElement {
}
}

renderChatThread(chatThread: ChatThreadEntry[]) {
return html`<chat-thread-component
.chatThread="${chatThread}"
.actionButtons="${[
{
id: 'chat-show-thought-process',
label: globalConfig.SHOW_THOUGH_PROCESS_BUTTON_LABEL_TEXT,
svgIcon: iconLightBulb,
isDisabled: this.isShowingThoughtProcess,
},
] as any}"
.isDisabled="${this.isDisabled}"
.isProcessingResponse="${this.chatController.isProcessingResponse}"
.selectedCitation="${this.selectedCitation}"
@on-action-button-click="${this.handleChatEntryActionButtonClick}"
@on-citation-click="${this.handleCitationClick}"
@on-followup-click="${this.handleQuestionInputClick}"
>
</chat-thread-component>`;
}

// Render the chat component as a web component
override render() {
return html`
Expand All @@ -318,6 +379,9 @@ export class ChatComponent extends LitElement {
${this.isChatStarted
? html`
<div class="chat__header">
${this.interactionModel === 'chat'
? this.chatHistoryController.renderHistoryButton({ disabled: this.isDisabled })
: ''}
<chat-action-button
.label="${globalConfig.RESET_CHAT_BUTTON_TITLE}"
actionId="chat-reset-button"
Expand All @@ -326,24 +390,20 @@ export class ChatComponent extends LitElement {
>
</chat-action-button>
</div>
<chat-thread-component
.chatThread="${this.chatThread}"
.actionButtons="${[
{
id: 'chat-show-thought-process',
label: globalConfig.SHOW_THOUGH_PROCESS_BUTTON_LABEL_TEXT,
svgIcon: iconLightBulb,
isDisabled: this.isShowingThoughtProcess,
},
] as any}"
.isDisabled="${this.isDisabled}"
.isProcessingResponse="${this.chatController.isProcessingResponse}"
.selectedCitation="${this.selectedCitation}"
@on-action-button-click="${this.handleChatEntryActionButtonClick}"
@on-citation-click="${this.handleCitationClick}"
@on-followup-click="${this.handleQuestionInputClick}"
>
</chat-thread-component>
${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.renderChatThread(this.chatThread)}
`
: ''}
${this.chatController.isAwaitingResponse
Expand Down
81 changes: 81 additions & 0 deletions packages/chat-component/src/components/chat-history-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { type ReactiveController, type ReactiveControllerHost } from 'lit';
import { html } from 'lit';
import { globalConfig, MAX_CHAT_HISTORY } from '../config/global-config.js';

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

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

export class ChatHistoryController implements ReactiveController {
host: ReactiveControllerHost;
static CHATHISTORY_ID = 'component:chat-history';

chatHistory: ChatThreadEntry[] = [];

private _showChatHistory: boolean = false;

get showChatHistory() {
return this._showChatHistory;
}

set showChatHistory(value: boolean) {
this._showChatHistory = value;
this.host.requestUpdate();
}

constructor(host: ReactiveControllerHost) {
(this.host = host).addController(this);
}

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

// find last 5 user messages indexes
const lastUserMessagesIndexes = history
.map((entry, index) => {
if (entry.isUserMessage) {
return index;
}
})
.filter((index) => index !== undefined)
.slice(-MAX_CHAT_HISTORY);

// trim everything before the first user message
const trimmedHistory = lastUserMessagesIndexes.length === 0 ? history : history.slice(lastUserMessagesIndexes[0]);

this.chatHistory = trimmedHistory;
}
}

hostDisconnected() {
// no-op
}

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)));
}

handleChatHistoryButtonClick(event: Event) {
event.preventDefault();
this.showChatHistory = !this.showChatHistory;
}

renderHistoryButton(options: { disabled: boolean } | undefined) {
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>
`;
}
}
33 changes: 17 additions & 16 deletions packages/chat-component/src/components/chat-thread-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { styles } from '../styles/chat-thread-component.js';
import { globalConfig } from '../config/global-config.js';
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { chatEntryToString } from '../utils/index.js';

import iconSuccess from '../../public/svg/success-icon.svg?raw';
import iconCopyToClipboard from '../../public/svg/copy-icon.svg?raw';
Expand Down Expand Up @@ -42,9 +43,7 @@ export class ChatThreadComponent extends LitElement {

// Copy response to clipboard
copyResponseToClipboard(entry: ChatThreadEntry): void {
const response = entry.text
.map((textEntry) => textEntry.value + '\n\n' + textEntry.followingSteps?.map((s) => ' - ' + s).join('\n'))
.join('\n\n');
const response = chatEntryToString(entry);

navigator.clipboard.writeText(response);
this.isResponseCopied = true;
Expand Down Expand Up @@ -177,19 +176,21 @@ export class ChatThreadComponent extends LitElement {
<div class="items__listWrapper">
${unsafeSVG(iconQuestion)}
<ul class="items__list followup">
${followupQuestions.map(
(followupQuestion) => html`
<li class="items__listItem--followup">
<a
class="items__link"
href="#"
data-testid="followUpQuestion"
@click="${(event) => this.handleFollowupQuestionClick(followupQuestion, entry, event)}"
>${followupQuestion}</a
>
</li>
`,
)}
${followupQuestions
.filter((q) => q !== null)
.map(
(followupQuestion) => html`
<li class="items__listItem--followup">
<a
class="items__link"
href="#"
data-testid="followUpQuestion"
@click="${(event) => this.handleFollowupQuestionClick(followupQuestion, entry, event)}"
>${followupQuestion}</a
>
</li>
`,
)}
</ul>
</div>
`;
Expand Down
16 changes: 15 additions & 1 deletion packages/chat-component/src/config/global-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const globalConfig = {
SUPPORT_CONTEXT_LABEL: 'Support Context',
CITATIONS_LABEL: 'Learn More:',
CITATIONS_TAB_LABEL: 'Citations',
SHOW_CHAT_HISTORY_LABEL: 'Show Chat History',
HIDE_CHAT_HISTORY_LABEL: 'Hide Chat History',
CHAT_MAX_COUNT_TAG: '{MAX_CHAT_HISTORY}',
CHAT_HISTORY_FOOTER_TEXT: 'Showing past {MAX_CHAT_HISTORY} conversations',
};

const teaserListTexts = {
Expand Down Expand Up @@ -72,6 +76,16 @@ const chatHttpOptions = {
stream: true,
};

const MAX_CHAT_HISTORY = 5;

const APPROACH_MODEL = ['rrr', 'rtr'];

export { globalConfig, requestOptions, chatHttpOptions, NEXT_QUESTION_INDICATOR, APPROACH_MODEL, teaserListTexts };
export {
globalConfig,
requestOptions,
chatHttpOptions,
NEXT_QUESTION_INDICATOR,
APPROACH_MODEL,
teaserListTexts,
MAX_CHAT_HISTORY,
};
3 changes: 2 additions & 1 deletion packages/chat-component/src/core/http/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChatResponseError } from '../../utils/index.js';

export async function callHttpApi(
{ question, type, approach, overrides }: ChatRequestOptions,
{ question, type, approach, overrides, messages }: ChatRequestOptions,
{ method, url, stream, signal }: ChatHttpOptions,
) {
return await fetch(`${url}/${type}`, {
Expand All @@ -12,6 +12,7 @@ export async function callHttpApi(
signal,
body: JSON.stringify({
messages: [
...(messages ?? []),
{
content: question,
role: 'user',
Expand Down
1 change: 1 addition & 0 deletions packages/chat-component/src/styles/chat-action-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const styles = css`
svg {
fill: currentColor;
width: 20px;
height: 20px;
padding: 3px;
}
button:hover > span,
Expand Down
Loading