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: add markdown preview for citation #103

Merged
merged 4 commits into from
Nov 7, 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
14 changes: 13 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/chat-component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
103 changes: 85 additions & 18 deletions packages/chat-component/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -103,6 +104,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];

// Send the question to the Open AI API and render the answer in the chat
Expand Down Expand Up @@ -187,6 +190,49 @@ export class ChatComponent extends LitElement {
this.currentQuestion = this.questionInput.value;
}

async handleCitationClick(sourceUrl: string, event: Event): Promise<void> {
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');
sinedied marked this conversation as resolved.
Show resolved Hide resolved
} 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<void> {
event.preventDefault();
Expand Down Expand Up @@ -292,13 +338,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();
Expand Down Expand Up @@ -373,6 +425,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}</a
>
</li>
Expand Down Expand Up @@ -443,7 +497,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}"
>
<span class="chat__header--span"
Expand Down Expand Up @@ -601,12 +655,12 @@ export class ChatComponent extends LitElement {
<ul class="aside__list" role="tablist">
<li class="aside__listItem">
<a
id="tab-1"
class="aside__link active"
id="tab-thought-process"
class="aside__link${this.selectedAsideTab === 'tab-thought-process' ? ' active' : ''}"
role="tab"
href="#"
aria-selected="true"
aria-hidden="false"
aria-selected="${this.selectedAsideTab === 'tab-thought-process'}"
aria-hidden="${this.selectedAsideTab !== 'tab-thought-process'}"
aria-controls="tabpanel-1"
@click="${(event: Event) => this.activateTab(event)}"
title="${globalConfig.THOUGHT_PROCESS_LABEL}"
Expand All @@ -616,12 +670,12 @@ export class ChatComponent extends LitElement {
</li>
<li class="aside__listItem">
<a
id="tab-2"
class="aside__link"
id="tab-support-context"
class="aside__link${this.selectedAsideTab === 'tab-support-context' ? ' active' : ''}"
role="tab"
href="#"
aria-selected="false"
aria-hidden="true"
aria-selected="${this.selectedAsideTab === 'tab-support-context'}"
aria-hidden="${this.selectedAsideTab !== 'tab-support-context'}"
aria-controls="tabpanel-2"
@click="${(event: Event) => this.activateTab(event)}"
title="${globalConfig.SUPPORT_CONTEXT_LABEL}"
Expand All @@ -631,12 +685,12 @@ export class ChatComponent extends LitElement {
</li>
<li class="aside__listItem">
<a
id="tab-3"
class="aside__link"
id="tab-citations"
class="aside__link${this.selectedAsideTab === 'tab-citations' ? ' active' : ''}"
role="tab"
href="#"
aria-selected="false"
aria-hidden="true"
aria-selected="${this.selectedAsideTab === 'tab-citations'}"
aria-hidden="${this.selectedAsideTab !== 'tab-citations'}"
aria-controls="tabpanel-3"
@click="${(event: Event) => this.activateTab(event)}"
title="${globalConfig.CITATIONS_LABEL}"
Expand All @@ -647,22 +701,35 @@ export class ChatComponent extends LitElement {
</ul>
</nav>
<div class="aside__content">
<div id="tabpanel-1" class="aside__tab active" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
<div id="tabpanel-1" class="aside__tab${
this.selectedAsideTab === 'tab-thought-process' ? ' active' : ''
}" role="tabpanel" tabindex="${
this.selectedAsideTab === 'tab-thought-process' ? 0 : -1
}" aria-labelledby="tab-thought-process">
<h3 class="subheadline--small">${globalConfig.THOUGHT_PROCESS_LABEL}</h3>
<div class="aside__innerContainer">
${this.chatThoughts ? html` <p class="aside__paragraph">${unsafeHTML(this.chatThoughts)}</p> ` : ''}
</div>
</div>
<div id="tabpanel-2" class="aside__tab" role="tabpanel" aria-labelledby="tab-2" tabindex="-1">
<div id="tabpanel-2" class="aside__tab${
this.selectedAsideTab === 'tab-support-context' ? ' active' : ''
}" role="tabpanel" aria-labelledby="tab-support-context" tabindex="${
sinedied marked this conversation as resolved.
Show resolved Hide resolved
this.selectedAsideTab === 'tab-support-context' ? 0 : -1
}">
<h3 class="subheadline--small">${globalConfig.SUPPORT_CONTEXT_LABEL}</h3>
<ul class="defaults__list always-row">
${this.chatDataPoints.map(
(dataPoint) => html` <li class="defaults__listItem">${dataPoint}</li> `,
)}
</ul>
</div>
<div id="tabpanel-3" class="aside__tab" role="tabpanel" tabindex="-1" aria-labelledby="tab-3">
<div id="tabpanel-3" class="aside__tab${
this.selectedAsideTab === 'tab-citations' ? ' active' : ''
}" role="tabpanel" tabindex="${
this.selectedAsideTab === 'tab-citations' ? 0 : -1
}" aria-labelledby="tab-citations">
${this.renderCitation(this.chatThread.at(-1)?.citations)}
<div id="citation-previewer"></div>
</div>
</div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions packages/chat-component/src/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,13 +403,19 @@ export const mainStyle = css`
margin-top: 5px;
font-size: small;
}
.items__listItem--citation.active {
background-color: var(--accent-high);
}
.items__listItem--citation:not(first-child) {
margin-left: 5px;
}
.items__link {
text-decoration: none;
color: var(--text-color);
}
.items__listItem--citation.active .items__link {
color: var(--white);
}
.steps .items__listItem--step {
padding: 5px 0;
border-bottom: 1px solid var(--light-gray);
Expand Down
Loading