diff --git a/packages/chat-component/src/components/chat-component.ts b/packages/chat-component/src/components/chat-component.ts deleted file mode 100644 index 8673538d..00000000 --- a/packages/chat-component/src/components/chat-component.ts +++ /dev/null @@ -1,565 +0,0 @@ -/* eslint-disable unicorn/template-indent */ -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, requestOptions } from '../config/global-config.js'; -import { getAPIResponse } from '../core/http/index.js'; -import { parseStreamedMessages } from '../core/parser/index.js'; -import { chatStyle } from '../styles/chat-component.js'; -import { getTimestamp, processText } from '../utils/index.js'; -import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; -// TODO: allow host applications to customize these icons - -import iconLightBulb from '../../public/svg/lightbulb-icon.svg?raw'; -import iconDelete from '../../public/svg/delete-icon.svg?raw'; -import iconSuccess from '../../public/svg/success-icon.svg?raw'; -import iconCopyToClipboard from '../../public/svg/copy-icon.svg?raw'; -import iconSend from '../../public/svg/send-icon.svg?raw'; -import iconClose from '../../public/svg/close-icon.svg?raw'; -import iconQuestion from '../../public/svg/question-icon.svg?raw'; - -/** - * A chat component that allows the user to ask questions and get answers from an API. - * The component also displays default prompts that the user can click on to ask a question. - * The component is built as a custom element that extends LitElement. - * - * Labels and other aspects are configurable via properties that get their values from the global config file. - * @element chat-component - * @fires chat-component#questionSubmitted - Fired when the user submits a question - * @fires chat-component#defaultQuestionClicked - Fired when the user clicks on a default question - * */ - -@customElement('chat-component') -export class ChatComponent extends LitElement { - //-- - // Public attributes - // -- - - @property({ type: String, attribute: 'data-input-position' }) - inputPosition = 'sticky'; - - @property({ type: String, attribute: 'data-interaction-model' }) - interactionModel: 'ask' | 'chat' = 'chat'; - - @property({ type: String, attribute: 'data-api-url' }) - apiUrl = chatHttpOptions.url; - - @property({ type: String, attribute: 'data-use-stream', converter: (value) => value?.toLowerCase() === 'true' }) - useStream: boolean = chatHttpOptions.stream; - - @property({ type: String, attribute: 'data-overrides', converter: (value) => JSON.parse(value || '{}') }) - overrides: RequestOverrides = {}; - - //-- - - @property({ type: String }) - currentQuestion = ''; - - @query('#question-input') - questionInput!: HTMLInputElement; - - // Default prompts to display in the chat - @state() - isDisabled = false; - - @state() - isChatStarted = false; - - @state() - isResetInput = false; - - // The program is awaiting response from API - @state() - isAwaitingResponse = false; - - // Show error message to the end-user, if API call fails - @property({ type: Boolean }) - hasAPIError = false; - - // Has the response been copied to the clipboard - @state() - isResponseCopied = false; - - // Is showing thought process panel - @state() - isShowingThoughtProcess = false; - - @state() - canShowThoughtProcess = false; - - @state() - isDefaultPromptsEnabled: boolean = globalConfig.IS_DEFAULT_PROMPTS_ENABLED && !this.isChatStarted; - - // api response - apiResponse = {} as BotResponse | Response; - // These are the chat bubbles that will be displayed in the chat - chatThread: ChatThreadEntry[] = []; - defaultPrompts: string[] = globalConfig.DEFAULT_PROMPTS; - defaultPromptsHeading: string = globalConfig.DEFAULT_PROMPTS_HEADING; - chatButtonLabelText: string = globalConfig.CHAT_BUTTON_LABEL_TEXT; - chatThoughts: string | null = ''; - chatDataPoints: string[] = []; - - chatRequestOptions: ChatRequestOptions = requestOptions; - chatHttpOptions: ChatHttpOptions = chatHttpOptions; - - static override styles = [chatStyle]; - - // Send the question to the Open AI API and render the answer in the chat - - // Add a message to the chat, when the user or the API sends a message - async processApiResponse({ message, isUserMessage }: { message: string; isUserMessage: boolean }) { - const citations: Citation[] = []; - const followingSteps: string[] = []; - const followupQuestions: string[] = []; - // Get the timestamp for the message - const timestamp = getTimestamp(); - const updateChatWithMessageOrChunk = async (part: string, isChunk: boolean) => { - if (isChunk) { - // we need to prepare an empty instance of the chat message so that we can start populating it - this.chatThread = [ - ...this.chatThread, - { - text: [{ value: '', followingSteps: [] }], - followupQuestions: [], - citations: [], - timestamp: timestamp, - isUserMessage, - }, - ]; - - const result = await parseStreamedMessages({ - chatThread: this.chatThread, - apiResponseBody: (this.apiResponse as Response).body, - visit: () => { - // NOTE: this function is called whenever we mutate sub-properties of the array - this.requestUpdate('chatThread'); - }, - // this will be processing thought process only with streaming enabled - }); - this.chatThoughts = result.thoughts; - this.chatDataPoints = result.data_points; - this.canShowThoughtProcess = true; - return true; - } - - this.chatThread = [ - ...this.chatThread, - { - text: [ - { - value: part, - followingSteps, - }, - ], - followupQuestions, - citations: [...new Set(citations)], - timestamp: timestamp, - isUserMessage, - }, - ]; - return true; - }; - - // Check if message is a bot message to process citations and follow-up questions - if (isUserMessage) { - updateChatWithMessageOrChunk(message, false); - } else { - if (this.useStream) { - await updateChatWithMessageOrChunk(message, true); - } else { - // non-streamed response - const processedText = processText(message, [citations, followingSteps, followupQuestions]); - message = processedText.replacedText; - // Push all lists coming from processText to the corresponding arrays - citations.push(...(processedText.arrays[0] as unknown as Citation[])); - followingSteps.push(...(processedText.arrays[1] as string[])); - followupQuestions.push(...(processedText.arrays[2] as string[])); - updateChatWithMessageOrChunk(message, false); - } - } - } - - // This function is only necessary when default prompts are enabled - // and we're rendering a teaser list component - // TODO: move to utils - handleOnTeaserClick(event): void { - this.questionInput.value = DOMPurify.sanitize(event?.detail.question || ''); - this.currentQuestion = this.questionInput.value; - } - - // Handle the click on the chat button and send the question to the API - async handleUserChatSubmit(event: Event): Promise { - event.preventDefault(); - const question = DOMPurify.sanitize(this.questionInput.value); - if (question) { - this.currentQuestion = question; - try { - const type = this.interactionModel; - // Remove default prompts - this.isChatStarted = true; - this.isDefaultPromptsEnabled = false; - // Disable the input field and submit button while waiting for the API response - this.isDisabled = true; - // Show loading indicator while waiting for the API response - - this.isAwaitingResponse = true; - if (type === 'chat') { - this.processApiResponse({ message: question, isUserMessage: true }); - } - - this.apiResponse = await getAPIResponse( - { - ...this.chatRequestOptions, - overrides: { - ...this.chatRequestOptions.overrides, - ...this.overrides, - }, - question, - type, - }, - { - // use defaults - ...this.chatHttpOptions, - - // override if the user has provided different values - url: this.apiUrl, - stream: this.useStream, - }, - ); - - this.questionInput.value = ''; - this.isAwaitingResponse = false; - this.isDisabled = false; - this.isResetInput = false; - const response = this.apiResponse as BotResponse; - // adds thought process support when streaming is disabled - if (!this.useStream) { - this.chatThoughts = response.choices[0].message.context?.thoughts ?? ''; - this.chatDataPoints = response.choices[0].message.context?.data_points ?? []; - this.canShowThoughtProcess = true; - } - await this.processApiResponse({ - message: this.useStream ? '' : response.choices[0].message.content, - isUserMessage: false, - }); - } catch (error) { - console.error(error); - this.handleAPIError(); - } - } - } - - // Reset the input field and the current question - resetInputField(event: Event): void { - event.preventDefault(); - this.questionInput.value = ''; - this.currentQuestion = ''; - this.isResetInput = false; - } - - // Reset the chat and show the default prompts - resetCurrentChat(event: Event): void { - this.isChatStarted = false; - this.chatThread = []; - this.isDisabled = false; - this.isDefaultPromptsEnabled = true; - this.isResponseCopied = false; - this.hideThoughtProcess(event); - } - - // Show the default prompts when enabled - showDefaultPrompts(event: Event): void { - if (!this.isDefaultPromptsEnabled) { - this.resetCurrentChat(event); - } - } - - // Handle the change event on the input field - handleOnInputChange(): void { - this.isResetInput = !!this.questionInput.value; - } - - // Handle API error - handleAPIError(): void { - this.hasAPIError = true; - this.isDisabled = false; - } - - // Copy response to clipboard - copyResponseToClipboard(): void { - const response = this.chatThread.at(-1)?.text.at(-1)?.value as string; - navigator.clipboard.writeText(response); - this.isResponseCopied = true; - } - - // show thought process aside - expandAside(event: Event): void { - event.preventDefault(); - this.isShowingThoughtProcess = true; - this.shadowRoot?.querySelector('#overlay')?.classList.add('active'); - this.shadowRoot?.querySelector('#chat__containerWrapper')?.classList.add('aside-open'); - } - // hide thought process aside - hideThoughtProcess(event: Event): void { - event.preventDefault(); - this.isShowingThoughtProcess = false; - this.shadowRoot?.querySelector('#chat__containerWrapper')?.classList.remove('aside-open'); - this.shadowRoot?.querySelector('#overlay')?.classList.remove('active'); - } - - // Render text entries in bubbles - renderTextEntry(textEntry: ChatMessageText) { - const entries = [html`

${unsafeHTML(textEntry.value)}

`]; - - // render steps - if (textEntry.followingSteps && textEntry.followingSteps.length > 0) { - entries.push( - html`
    - ${textEntry.followingSteps.map( - (followingStep) => html`
  1. ${unsafeHTML(followingStep)}
  2. `, - )} -
`, - ); - } - - return entries; - } - - renderCitation(citations: Citation[] | undefined) { - // render citations - if (citations && citations.length > 0) { - return html` -
    - ${citations.map( - (citation) => html` -
  1. - ${citation.ref}. ${citation.text} -
  2. - `, - )} -
- `; - } - - return ''; - } - - renderFollowupQuestions(followupQuestions: string[] | undefined) { - // render followup questions - // need to fix first after decoupling of teaserlist - if (followupQuestions && followupQuestions.length > 0) { - return html` -
- ${unsafeSVG(iconQuestion)} - -
- `; - } - - return ''; - } - - // Render the chat component as a web component - override render() { - return html` -
-
-
- ${ - this.isChatStarted - ? html` -
- -
-
    - ${this.chatThread.map( - (message) => html` -
  • -
    - ${message.isUserMessage - ? '' - : html`
    - - -
    `} - ${message.text.map((textEntry) => this.renderTextEntry(textEntry))} - ${this.renderCitation(message.citations)} - ${this.renderFollowupQuestions(message.followupQuestions)} -
    -

    - ${message.timestamp}, - ${message.isUserMessage ? 'You' : globalConfig.USER_IS_BOT} -

    -
  • - `, - )} - ${this.hasAPIError - ? html` -
  • -

    ${globalConfig.API_ERROR_MESSAGE}

    -
  • - ` - : ''} -
- ` - : '' - } - ${ - this.isAwaitingResponse && !this.hasAPIError - ? html` -
-
-
-
-
- ` - : '' - } - -
- - ${ - this.isDefaultPromptsEnabled - ? html` - - ` - : '' - } -
-
- - - -
- - ${ - this.isDefaultPromptsEnabled - ? '' - : html`
- -
` - } -
-
- ${ - this.isShowingThoughtProcess - ? html` - - ` - : '' - } -
- `; - } -}