Skip to content

Commit

Permalink
feat: add custom styling and branding v2 (#160)
Browse files Browse the repository at this point in the history
* feat: add chat stage and avatar components

* feat: add branding

* feat: implement design system

* chore: change brand logo file

* feat: add settings styles component

* fix: improve initialization

* fix: fix minor bugs

* feat: add additional values

* fix: move branding toggle out of the styling toggle

* fix: refine dark style theme

* fix: make branding dynamic

* fix: remove direct classlist add and leave only reactive use effect

* fix: color initialization on theme

* test: add tests

* tests: move test out of init clause

* fix: merge issues causing test failure

* fix: fix layout cosmetics and remove avatar from bubbles

* fix: make link icon generic and remove BEM from styles

* fix: fix dark theme toggle

* fix: remove boolean and icon specific bindings for chat avatar

* fix: remove styles from chat-thread

* fix: remove duplicated styles in wrong context

* fix: update color name

* chore: optimize default names

* chore: update init object for colors styling

* fix: fix chat stage rendering conditional

* test: fix enable branding test

* test: fix theme switch test

* test: fix broken follow up questions test

* feat: change items to compliant name. Closes #163

* test: fix tests to match new names

* fix: remove unnecessary empty container in return

* fix: remove unnecessary css rule

---------

Co-authored-by: Shibani Basava (from Dev Box) <shibanib@microsoft.com>
  • Loading branch information
anfibiacreativa and shibbas authored Dec 12, 2023
1 parent 081659a commit 703d278
Show file tree
Hide file tree
Showing 27 changed files with 909 additions and 216 deletions.
95 changes: 95 additions & 0 deletions packages/chat-component/public/branding/brand-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 42 additions & 1 deletion packages/chat-component/src/components/chat-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ 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 iconLogo from '../../public/branding/brand-logo.svg?raw';
import iconUp from '../../public/svg/chevron-up-icon.svg?raw';

// import only necessary components to reduce bundle size
import './link-icon.js';
import './chat-stage.js';
import './loading-indicator.js';
import './voice-input-button.js';
import './teaser-list-component.js';
Expand All @@ -31,6 +35,7 @@ import './tab-component.js';
import './citation-list.js';
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';
Expand Down Expand Up @@ -61,12 +66,18 @@ export class ChatComponent extends LitElement {
@property({ type: String, attribute: 'data-api-url' })
apiUrl = chatHttpOptions.url;

@property({ type: String, attribute: 'data-custom-branding', converter: (value) => value?.toLowerCase() === 'true' })
isCustomBranding: boolean = globalConfig.IS_CUSTOM_BRANDING;

@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, attribute: 'data-custom-styles', converter: (value) => JSON.parse(value || '{}') })
customStyles: any = {};

//--

@property({ type: String })
Expand Down Expand Up @@ -108,6 +119,26 @@ export class ChatComponent extends LitElement {

static override styles = [chatStyle];

override updated(changedProperties: Map<string | number | symbol, unknown>) {
super.updated(changedProperties);
// The following block is only necessary when you want to override the component from settings in the outside.
// Remove this block when not needed, considering that updated() is a LitElement lifecycle method
// that may be used by other components if you update this code.
if (changedProperties.has('customStyles')) {
this.style.setProperty('--c-accent-high', this.customStyles.AccentHigh);
this.style.setProperty('--c-accent-lighter', this.customStyles.AccentLight);
this.style.setProperty('--c-accent-dark', this.customStyles.AccentDark);
this.style.setProperty('--c-text-color', this.customStyles.TextColor);
this.style.setProperty('--c-light-gray', this.customStyles.BackgroundColor);
this.style.setProperty('--c-dark-gray', this.customStyles.ForegroundColor);
this.style.setProperty('--c-base-gray', this.customStyles.FormBackgroundColor);
this.style.setProperty('--radius-base', this.customStyles.BorderRadius);
this.style.setProperty('--border-base', this.customStyles.BorderWidth);
this.style.setProperty('--font-base', this.customStyles.FontBaseSize);
}
}
// Send the question to the Open AI API and render the answer in the chat

setQuestionInputValue(value: string): void {
this.questionInput.value = DOMPurify.sanitize(value || '');
this.currentQuestion = this.questionInput.value;
Expand Down Expand Up @@ -360,6 +391,8 @@ export class ChatComponent extends LitElement {
.isDisabled="${this.isDisabled}"
.isProcessingResponse="${this.chatController.isProcessingResponse}"
.selectedCitation="${this.selectedCitation}"
.isCustomBranding="${this.isCustomBranding}"
.svgIcon="${iconLogo}"
@on-action-button-click="${this.handleChatEntryActionButtonClick}"
@on-citation-click="${this.handleCitationClick}"
@on-followup-click="${this.handleQuestionInputClick}"
Expand All @@ -372,10 +405,18 @@ export class ChatComponent extends LitElement {
return html`
<div id="overlay" class="overlay"></div>
<section id="chat__containerWrapper" class="chat__containerWrapper">
${this.isCustomBranding && !this.isChatStarted
? html` <chat-stage
svgIcon="${iconLogo}"
pagetitle="${globalConfig.BRANDING_HEADLINE}"
url="${globalConfig.BRANDING_URL}"
>
</chat-stage>`
: ''}
<section class="chat__container" id="chat-container">
${this.isChatStarted
? html`
<div class="chat__header">
<div class="chat__header--thread">
${this.interactionModel === 'chat'
? this.chatHistoryController.renderHistoryButton({ disabled: this.isDisabled })
: ''}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import './chat-action-button.js';

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

chatHistory: ChatThreadEntry[] = [];

Expand Down
32 changes: 32 additions & 0 deletions packages/chat-component/src/components/chat-stage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { styles } from '../styles/chat-stage.js';
import './link-icon.js';
export interface ChatStage {
pagetitle: string;
url: string;
svgIcon: string;
}

@customElement('chat-stage')
export class ChatStageComponent extends LitElement {
static override styles = [styles];

@property({ type: String })
pagetitle = '';

@property({ type: String })
url = '';

@property({ type: String })
svgIcon = '';

override render() {
return html`
<header class="chat-stage__header" data-testid="chat-branding">
<link-icon url="${this.url}" svgIcon="${this.svgIcon}"></link-icon>
<h1 class="chat-stage__hl">${this.pagetitle}</h1>
</header>
`;
}
}
48 changes: 25 additions & 23 deletions packages/chat-component/src/components/chat-thread-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,29 +103,31 @@ export class ChatThreadComponent extends LitElement {

renderResponseActions(entry: ChatThreadEntry) {
return html`
<div class="chat__header">
${this.actionButtons.map(
(actionButton) => html`
<chat-action-button
.label="${actionButton.label}"
.svgIcon="${actionButton.svgIcon}"
.isDisabled="${actionButton.isDisabled || this.isDisabled}"
.actionId="${actionButton.id}"
@click="${(event) => this.actionButtonClicked(actionButton, entry, event)}"
></chat-action-button>
`,
)}
<chat-action-button
.label="${globalConfig.COPY_RESPONSE_BUTTON_LABEL_TEXT}"
.svgIcon="${this.isResponseCopied ? iconSuccess : iconCopyToClipboard}"
.isDisabled="${this.isDisabled}"
actionId="copy-to-clipboard"
.tooltip="${this.isResponseCopied
? globalConfig.COPIED_SUCCESSFULLY_MESSAGE
: globalConfig.COPY_RESPONSE_BUTTON_LABEL_TEXT}"
@click="${() => this.copyResponseToClipboard(entry)}"
></chat-action-button>
</div>
<header class="chat__header">
<div class="chat__header--button">
${this.actionButtons.map(
(actionButton) => html`
<chat-action-button
.label="${actionButton.label}"
.svgIcon="${actionButton.svgIcon}"
.isDisabled="${actionButton.isDisabled}"
.actionId="${actionButton.id}"
@click="${(event) => this.actionButtonClicked(actionButton, entry, event)}"
></chat-action-button>
`,
)}
<chat-action-button
.label="${globalConfig.COPY_RESPONSE_BUTTON_LABEL_TEXT}"
.svgIcon="${this.isResponseCopied ? iconSuccess : iconCopyToClipboard}"
.isDisabled="${this.isDisabled}"
actionId="copy-to-clipboard"
.tooltip="${this.isResponseCopied
? globalConfig.COPIED_SUCCESSFULLY_MESSAGE
: globalConfig.COPY_RESPONSE_BUTTON_LABEL_TEXT}"
@click="${this.copyResponseToClipboard}"
></chat-action-button>
</div>
</header>
`;
}

Expand Down
33 changes: 33 additions & 0 deletions packages/chat-component/src/components/link-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

import { styles } from '../styles/link-icon.js';
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';

export interface LinkIcon {
label: string;
svgIcon: string;
url: string;
}

@customElement('link-icon')
export class LinkIconComponent extends LitElement {
static override styles = [styles];

@property({ type: String })
label = '';

@property({ type: String })
svgIcon = '';

@property({ type: String })
url = '';

override render() {
return html`
<a title="${this.label}" href="${this.url}" target="_blank" rel="noopener noreferrer">
${unsafeSVG(this.svgIcon)}
</a>
`;
}
}
7 changes: 7 additions & 0 deletions packages/chat-component/src/config/global-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ const globalConfig = {
SUPPORT_CONTEXT_LABEL: 'Support Context',
CITATIONS_LABEL: 'Learn More:',
CITATIONS_TAB_LABEL: 'Citations',
// Custom Branding
IS_CUSTOM_BRANDING: true,
// Custom Branding details
// All these should come from persistence config
BRANDING_URL: '#',
BRANDING_LOGO_ALT: 'Brand Logo',
BRANDING_HEADLINE: 'Welcome to the Support Assistant of our Brand',
SHOW_CHAT_HISTORY_LABEL: 'Show Chat History',
HIDE_CHAT_HISTORY_LABEL: 'Hide Chat History',
CHAT_MAX_COUNT_TAG: '{MAX_CHAT_HISTORY}',
Expand Down
Loading

0 comments on commit 703d278

Please sign in to comment.