diff --git a/package-lock.json b/package-lock.json index d5a76cb1..3311a597 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9236,6 +9236,17 @@ "tmpl": "1.0.5" } }, + "node_modules/marked": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.5.tgz", + "integrity": "sha512-14QG3shv8Kg/xc0Yh6TNkMj90wXH9mmldi5941I2OevfJ/FQAFLEwtwU2/FfgSAOMlWHrEukWSGQf8MiVYNG2A==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/md5": { "version": "2.3.0", "license": "BSD-3-Clause", @@ -13540,7 +13551,8 @@ "license": "MIT", "dependencies": { "dompurify": "^3.0.6", - "lit": "^2.8.0" + "lit": "^2.8.0", + "marked": "^9.1.5" }, "devDependencies": { "@types/dompurify": "^3.0.3", diff --git a/packages/chat-component/package.json b/packages/chat-component/package.json index 01649ea8..c91e3c53 100644 --- a/packages/chat-component/package.json +++ b/packages/chat-component/package.json @@ -23,7 +23,8 @@ "license": "MIT", "dependencies": { "dompurify": "^3.0.6", - "lit": "^2.8.0" + "lit": "^2.8.0", + "marked": "^9.1.5" }, "devDependencies": { "@types/dompurify": "^3.0.3", diff --git a/packages/chat-component/src/main.ts b/packages/chat-component/src/main.ts index d620d5c1..57310d27 100644 --- a/packages/chat-component/src/main.ts +++ b/packages/chat-component/src/main.ts @@ -19,6 +19,7 @@ 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'; +import { marked } from 'marked'; /** * A chat component that allows the user to ask questions and get answers from an API. @@ -106,6 +107,8 @@ export class ChatComponent extends LitElement { chatRequestOptions: ChatRequestOptions = requestOptions; chatHttpOptions: ChatHttpOptions = chatHttpOptions; + selectedAsideTab: 'tab-thought-process' | 'tab-support-context' | 'tab-citations' = 'tab-thought-process'; + static override styles = [mainStyle]; // debounce dispatching must-scroll event @@ -199,6 +202,49 @@ export class ChatComponent extends LitElement { this.currentQuestion = this.questionInput.value; } + async handleCitationClick(sourceUrl: string, event: Event): Promise { + if (sourceUrl?.endsWith('.md')) { + event?.preventDefault(); + + if (!this.isShowingThoughtProcess) { + this.selectedAsideTab = 'tab-citations'; + this.showThoughtProcess(); + } + + const response = await fetch(sourceUrl); + if (response.ok) { + // highlight the clicked citation to make it clear which is being previewed + const citationsList = this.shadowRoot?.querySelectorAll( + '.aside .items__list.citations .items__listItem--citation', + ); + + if (citationsList) { + const citationsArray = [...citationsList]; + const clickedIndex = citationsArray.findIndex((citation) => { + const link = citation.querySelector('a'); + return link?.href === sourceUrl; + }); + + for (const citation of citationsList) { + const index = citationsArray.indexOf(citation); + if (index === clickedIndex) { + citation.classList.add('active'); + } else { + citation.classList.remove('active'); + } + } + } + + // update the markdown previewer with the content of the clicked citation + const previewer = this.shadowRoot?.querySelector('#citation-previewer'); + if (previewer) { + const markdownContent = await response.text(); + previewer.innerHTML = DOMPurify.sanitize(marked.parse(markdownContent)); + } + } + } + } + // Handle the click on the chat button and send the question to the API async handleUserChatSubmit(event: Event): Promise { event.preventDefault(); @@ -304,13 +350,19 @@ export class ChatComponent extends LitElement { this.isResponseCopied = true; } + handleShowThoughtProcess(event: Event): void { + event?.preventDefault(); + this.selectedAsideTab = 'tab-thought-process'; + this.showThoughtProcess(); + } + // show thought process aside - showThoughtProcess(event: Event): void { - event.preventDefault(); + showThoughtProcess(): void { 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(); @@ -386,6 +438,8 @@ export class ChatComponent extends LitElement { data-testid="citation" target="_blank" rel="noopener noreferrer" + @click="${(event: Event) => + this.handleCitationClick(`${this.apiUrl}/content/${citation.text}`, event)}" >${citation.ref}. ${citation.text} @@ -454,7 +508,7 @@ export class ChatComponent extends LitElement { title="${globalConfig.SHOW_THOUGH_PROCESS_BUTTON_LABEL_TEXT}" class="button chat__header--button" data-testid="chat-show-thought-process" - @click="${this.showThoughtProcess}" + @click="${this.handleShowThoughtProcess}" ?disabled="${this.isShowingThoughtProcess || !this.canShowThoughtProcess}" >