From 703d27850d20c9f2fa10c03fbcad7cc84313740e Mon Sep 17 00:00:00 2001 From: Natalia Venditto Date: Tue, 12 Dec 2023 14:45:32 +0100 Subject: [PATCH] feat: add custom styling and branding v2 (#160) * 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) --- .../public/branding/brand-logo.svg | 95 +++++++++ .../src/components/chat-component.ts | 43 +++- .../src/components/chat-history-controller.ts | 2 +- .../src/components/chat-stage.ts | 32 +++ .../src/components/chat-thread-component.ts | 48 +++-- .../src/components/link-icon.ts | 33 +++ .../src/config/global-config.js | 7 + .../src/styles/chat-action-button.ts | 30 +-- .../src/styles/chat-component.ts | 196 +++++++++--------- .../chat-component/src/styles/chat-stage.ts | 47 +++++ .../src/styles/chat-thread-component.ts | 90 ++++---- .../src/styles/citation-list.ts | 16 +- .../chat-component/src/styles/link-icon.ts | 36 ++++ .../src/styles/loading-indicator.ts | 4 +- .../src/styles/tab-component.ts | 18 +- .../src/styles/teaser-list-component.ts | 28 +-- .../src/styles/voice-input-button.ts | 6 +- packages/webapp/src/api/models.ts | 13 ++ .../SettingsStyles/SettingsStyles.css | 25 +++ .../SettingsStyles/SettingsStyles.tsx | 132 ++++++++++++ .../src/components/SettingsStyles/index.tsx | 1 + .../components/ThemeSwitch/ThemeSwitch.css | 5 + .../components/ThemeSwitch/ThemeSwitch.tsx | 35 ++++ .../src/components/ThemeSwitch/index.tsx | 1 + packages/webapp/src/index.css | 5 + packages/webapp/src/pages/chat/Chat.tsx | 137 +++++++++++- tests/e2e/webapp.spec.ts | 40 ++++ 27 files changed, 909 insertions(+), 216 deletions(-) create mode 100644 packages/chat-component/public/branding/brand-logo.svg create mode 100644 packages/chat-component/src/components/chat-stage.ts create mode 100644 packages/chat-component/src/components/link-icon.ts create mode 100644 packages/chat-component/src/styles/chat-stage.ts create mode 100644 packages/chat-component/src/styles/link-icon.ts create mode 100644 packages/webapp/src/components/SettingsStyles/SettingsStyles.css create mode 100644 packages/webapp/src/components/SettingsStyles/SettingsStyles.tsx create mode 100644 packages/webapp/src/components/SettingsStyles/index.tsx create mode 100644 packages/webapp/src/components/ThemeSwitch/ThemeSwitch.css create mode 100644 packages/webapp/src/components/ThemeSwitch/ThemeSwitch.tsx create mode 100644 packages/webapp/src/components/ThemeSwitch/index.tsx diff --git a/packages/chat-component/public/branding/brand-logo.svg b/packages/chat-component/public/branding/brand-logo.svg new file mode 100644 index 00000000..0dccfb3f --- /dev/null +++ b/packages/chat-component/public/branding/brand-logo.svg @@ -0,0 +1,95 @@ + + + + + + + + image/svg+xml + + + + + + + YOURBRAND + + + diff --git a/packages/chat-component/src/components/chat-component.ts b/packages/chat-component/src/components/chat-component.ts index 9ed03fc2..515cb87f 100644 --- a/packages/chat-component/src/components/chat-component.ts +++ b/packages/chat-component/src/components/chat-component.ts @@ -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'; @@ -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'; @@ -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 }) @@ -108,6 +119,26 @@ export class ChatComponent extends LitElement { static override styles = [chatStyle]; + override updated(changedProperties: Map) { + 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; @@ -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}" @@ -372,10 +405,18 @@ export class ChatComponent extends LitElement { return html`
+ ${this.isCustomBranding && !this.isChatStarted + ? html` + ` + : ''}
${this.isChatStarted ? html` -
+
${this.interactionModel === 'chat' ? this.chatHistoryController.renderHistoryButton({ disabled: this.isDisabled }) : ''} diff --git a/packages/chat-component/src/components/chat-history-controller.ts b/packages/chat-component/src/components/chat-history-controller.ts index 6d0ce32a..e0684503 100644 --- a/packages/chat-component/src/components/chat-history-controller.ts +++ b/packages/chat-component/src/components/chat-history-controller.ts @@ -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[] = []; diff --git a/packages/chat-component/src/components/chat-stage.ts b/packages/chat-component/src/components/chat-stage.ts new file mode 100644 index 00000000..4cd7da4f --- /dev/null +++ b/packages/chat-component/src/components/chat-stage.ts @@ -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` +
+ +

${this.pagetitle}

+
+ `; + } +} diff --git a/packages/chat-component/src/components/chat-thread-component.ts b/packages/chat-component/src/components/chat-thread-component.ts index 49f286a2..dd973a06 100644 --- a/packages/chat-component/src/components/chat-thread-component.ts +++ b/packages/chat-component/src/components/chat-thread-component.ts @@ -103,29 +103,31 @@ export class ChatThreadComponent extends LitElement { renderResponseActions(entry: ChatThreadEntry) { return html` -
- ${this.actionButtons.map( - (actionButton) => html` - - `, - )} - -
+
+
+ ${this.actionButtons.map( + (actionButton) => html` + + `, + )} + +
+
`; } diff --git a/packages/chat-component/src/components/link-icon.ts b/packages/chat-component/src/components/link-icon.ts new file mode 100644 index 00000000..89a65c04 --- /dev/null +++ b/packages/chat-component/src/components/link-icon.ts @@ -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` + + ${unsafeSVG(this.svgIcon)} + + `; + } +} diff --git a/packages/chat-component/src/config/global-config.js b/packages/chat-component/src/config/global-config.js index 017aa5ce..580c1779 100644 --- a/packages/chat-component/src/config/global-config.js +++ b/packages/chat-component/src/config/global-config.js @@ -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}', diff --git a/packages/chat-component/src/styles/chat-action-button.ts b/packages/chat-component/src/styles/chat-action-button.ts index 12c883fe..2d8ee200 100644 --- a/packages/chat-component/src/styles/chat-action-button.ts +++ b/packages/chat-component/src/styles/chat-action-button.ts @@ -4,15 +4,15 @@ export const styles = css` button { color: var(--text-color); text-decoration: underline; - border: 1px solid var(--accent-dark); + border: var(--border-thin) solid var(--c-accent-dark); text-decoration: none; - border-radius: 5px; - background: var(--white); + border-radius: var(--radius-small); + background: var(--c-white); display: flex; align-items: center; margin-left: 5px; opacity: 1; - padding: 5px; + padding: var(--d-xsmall); transition: all 0.3s ease-in-out; position: relative; cursor: pointer; @@ -27,12 +27,12 @@ export const styles = css` position: absolute; text-align: right; top: -80%; - background: var(--accent-dark); - color: white; + background: var(--c-accent-dark); + color: var(--c-white); opacity: 0; - right: 0px; - padding: 5px 10px; - border-radius: 5px; + right: 0; + padding: var(--d-xsmall) var(--d-small); + border-radius: var(--radius-small); font-weight: bold; word-wrap: nowrap; } @@ -43,15 +43,15 @@ export const styles = css` height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; - border-top: 8px solid var(--accent-dark); + border-top: var(--border-thick) solid var(--c-accent-dark); bottom: -8px; right: 5px; } svg { fill: currentColor; - width: 20px; - height: 20px; - padding: 3px; + padding: var(--d-xsmall); + width: var(--d-base); + height: var(--d-base); } button:hover > span, button:focus > span { @@ -62,8 +62,8 @@ export const styles = css` button:focus, button:hover > svg, button:focus > svg { - background-color: var(--light-gray); - border-radius: 5px; + background-color: var(--c-light-gray); + border-radius: var(--radius-small); transition: background 0.3s ease-in-out; } `; diff --git a/packages/chat-component/src/styles/chat-component.ts b/packages/chat-component/src/styles/chat-component.ts index c360b22e..a24c2258 100644 --- a/packages/chat-component/src/styles/chat-component.ts +++ b/packages/chat-component/src/styles/chat-component.ts @@ -2,41 +2,66 @@ import { css } from 'lit'; export const chatStyle = css` :host { + --c-primary: #123f58; + --c-secondary: #f5f5f5; + --c-text: var(--c-primary); + --c-white: #fff; + --c-black: #111111; + --c-red: #ff0000; + --c-light-gray: #e3e3e3; + --c-base-gray: var(--c-secondary); + --c-dark-gray: #4e5288; + --c-accent-high: #692b61; + --c-accent-dark: #5e3c7d; + --c-accent-light: #f6d5f2; + --c-error: #8a0000; + --c-error-background: rgb(253, 231, 233); + --c-success: #26b32b; + --font-r-small: 1vw; + --font-r-base: 3vw; + --font-r-large: 5vw; + --font-base: 14px; + --font-rel-base: 1.2rem; + --font-small: small; + --font-large: large; + --font-larger: x-large; + --border-base: 3px; + --border-thin: 1px; + --border-thicker: 8px; + --radius-small: 5px; + --radius-base: 10px; + --radius-large: 25px; + --radius-none: 0; + --width-wide: 90%; + --width-base: 80%; + --width-narrow: 50%; + --d-base: 20px; + --d-small: 10px; + --d-xsmall: 5px; + --d-large: 30px; + --d-xlarge: 50px; + --shadow: 0 0 10px rgba(0, 0, 0, 0.1); width: 100vw; display: block; - padding: 16px; - --secondary-color: #f5f5f5; - --text-color: #123f58; - --primary-color: rgba(241, 255, 165, 0.6); - --white: #fff; - --black: #111111; - --red: #ff0000; - --light-gray: #e3e3e3; - --dark-gray: #4e5288; - --accent-high: #692b61; - --accent-dark: #002b23; - --accent-light: #e6fbf7; - --accent-lighter: #f6d0d0; - --accent-contrast: #7d3c71; - --error-color: #8a0000; - --error-color-background: rgb(253, 231, 233); + padding: var(--d-base); + color: var(--c-text); } :host([data-theme='dark']) { - display: block; - padding: 16px; - --secondary-color: #1f2e32; - --text-color: #ffffff; - --primary-color: rgba(241, 255, 165, 0.6); - --white: #000000; - --light-gray: #e3e3e3; - --dark-gray: #4e5288; - --accent-high: #005164; - --accent-dark: #b4e2ee; - --accent-light: #e6fbf7; - --accent-lighter: #f6d0d0; - --accent-contrast: #7d3c71; - --error-color: rgb(243, 242, 241); - --error-color-background: rgb(68, 39, 38); + --c-primary: #fdfeff; + --c-secondary: #32343e; + --c-text: var(--c-primary); + --c-white: var(--c-secondary); + --c-black: var(--c-primary); + --c-red: #ff0000; + --c-light-gray: #636d9c; + --c-dark-gray: #e3e3e3; + --c-base-gray: var(--c-secondary); + --c-accent-high: #dcdef8; + --c-accent-dark: var(--c-primary); + --c-accent-light: #032219; + --c-error: #8a0000; + --c-error-background: rgb(253, 231, 233); + --c-success: #26b32b; } html { scroll-behavior: smooth; @@ -46,7 +71,7 @@ export const chatStyle = css` margin-block-end: 0; } .button { - color: var(--text-color); + color: var(--c-text); border: 0; background: none; cursor: pointer; @@ -61,7 +86,7 @@ export const chatStyle = css` display: flex; width: 100%; height: 0; - background: var(--black); + background: var(--c-black); z-index: 2; opacity: 0.8; transition: all 0.3s ease-in-out; @@ -81,27 +106,15 @@ export const chatStyle = css` .container-col { display: flex; flex-direction: column; - gap: 8px; + gap: var(--d-small); } .container-row { flex-direction: row; } - .headline { - color: var(--text-color); - font-size: 5vw; - padding: 0; - margin: 10px 0 30px; - - @media (min-width: 1024px) { - font-size: 3vw; - text-align: center; - } - } - .subheadline { - color: var(--text-color); - font-size: 1.2rem; - padding: 0; - margin: 0; + .chat__header--thread { + display: flex; + align-items: center; + justify-content: flex-end; } .chat__container { min-width: 100%; @@ -110,20 +123,19 @@ export const chatStyle = css` } .chat__containerWrapper.aside-open { .chat__listItem { - max-width: 90%; - min-width: 80%; + max-width: var(--width-wide); } } .chat__containerWrapper { display: grid; grid-template-columns: 1fr; - gutter: 20px; + gutter: var(--d-base); } .chat__containerWrapper.aside-open { display: grid; grid-template-columns: 1fr; - grid-column-gap: 20px; - grid-row-gap: 20px; + grid-column-gap: var(--d-base); + grid-row-gap: var(--d-base); @media (min-width: 1024px) { grid-template-columns: 1fr 1fr; @@ -131,52 +143,42 @@ export const chatStyle = css` } .chat__containerWrapper.aside-open .aside { width: 100%; - border-left: 1px solid #d2d2d2; + border-left: var(--border-thin) solid var(--c-light-gray); @media (max-width: 1024px) { - width: 80%; + width: var(--width-base); } } @media (max-width: 1024px) { .aside { - top: 30px; + top: var(-d-large); left: auto; z-index: 3; - background: var(--white); + background: var(--c-white); display: block; - padding: 20px; + padding: var(--d-base); position: absolute; - width: 80%; - border-radius: 10px; + width: var(--width-base); + border-radius: var(--radius-base); } } .form__container { - margin-top: 30px; - padding: 10px; + margin-top: var(--d-large); + padding: var(--d-small); } .form__container-sticky { position: sticky; bottom: 0; z-index: 1; - border-radius: 10px; - background: linear-gradient( - 0deg, - rgba(245, 245, 245, 1) 0%, - rgba(245, 245, 245, 0.8) 75%, - rgba(245, 245, 245, 0.5) 100% - ); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - padding: 15px 10px 50px; + border-radius: var(--radius-base); + background: linear-gradient(0deg, var(--c-base-gray) 0%, var(--c-base-gray) 75%, var(--c-base-gray) 100%); + box-shadow: var(--shadow); + padding: var(--d-small) var(--d-small) var(--d-large); } .form__label { display: block; - padding: 5px 0; - font-size: small; - } - .chat__header { - display: flex; - justify-content: flex-end; - padding: 20px; + padding: var(-d-xsmall) 0; + font-size: var(--font-small); } .chatbox__button:disabled, .chatbox__input:disabled { @@ -184,28 +186,28 @@ export const chatStyle = css` cursor: not-allowed; } .chatbox__button svg { - fill: var(--accent-high); - width: 25px; + fill: var(--c-accent-high); + width: calc(var(--d-base) + var(--d-xsmall)); } .chatbox__container { position: relative; height: 50px; } .chatbox__button { - background: var(--white); + background: var(--c-white); border: none; color: var(--text-color); font-weight: bold; cursor: pointer; border-radius: 4px; margin-left: 8px; - width: 80px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + width: calc(var(--d-large) + var(--d-xlarge)); + box-shadow: var(--shadow); transition: background 0.3s ease-in-out; } .chatbox__button:hover, .chatbox__button:focus { - background: var(--secondary-color); + background: var(--c-secondary); } .chatbox__button:hover svg, .chatbox__button:focus svg { @@ -218,18 +220,18 @@ export const chatStyle = css` background: transparent; border: none; color: gray; - background: var(--accent-dark); + background: var(--c-accent-dark); border-radius: 50%; - color: var(--white); + color: var(--c-white); font-weight: bold; height: 20px; - width: 20px; + width: var(--d-base); cursor: pointer; } .chatbox__input-container { display: flex; - border: 1px solid var(--black); - background: var(--white); + border: var(--border-thin) solid var(--c-black); + background: var(--c-white); border-radius: 4px; } .chatbox__input-container:focus-within { @@ -239,7 +241,7 @@ export const chatStyle = css` background: transparent; color: var(--text-color); border: none; - padding: 8px; + padding: var(--d-small); flex: 1 1 auto; font-size: 1rem; } @@ -251,14 +253,14 @@ export const chatStyle = css` justify-content: end; } .tab-component__content { - padding: 20px 20px 20px 0px; + padding: var(--d-base) var(--d-base) var(--d-base) 0; } .tab-component__paragraph { font-family: monospace; - font-size: large; - border: 1px solid var(--light-gray); - border-radius: 25px; - padding: 20px; + font-size: var(--font-large); + border: var(--border-thin) solid var(--c-light-gray); + border-radius: var(--radius-large); + padding: var(--d-base); } .chat-history__footer { display: flex; diff --git a/packages/chat-component/src/styles/chat-stage.ts b/packages/chat-component/src/styles/chat-stage.ts new file mode 100644 index 00000000..20d6e9e5 --- /dev/null +++ b/packages/chat-component/src/styles/chat-stage.ts @@ -0,0 +1,47 @@ +import { css } from 'lit'; + +export const styles = css` + .chat-stage__header { + display: flex; + width: var(--width-base); + margin: 0 auto var(--d-large); + justify-content: center; + align-items: center; + + @media (min-width: 1024px) { + width: var(--width-narrow); + } + } + .chat-stage__link svg { + width: calc(var(--width-base) - var(--d-small)); + height: calc(var(--width-base) - var(--d-small)); + position: relative; + z-index: 1; + } + .chat-stage__link { + flex-shrink: 0; + border-radius: calc(var(--radius-large) * 3); + border: var(--border-thicker) solid transparent; + background-origin: border-box; + background-clip: content-box, border-box; + background-size: cover; + background-image: linear-gradient(to right, var(--c-accent-light), var(--c-accent-high)); + width: calc(var(--d-xlarge) * 2); + height: calc(var(--d-xlarge) * 2); + display: flex; + align-items: center; + justify-content: center; + margin-right: var(--d-large); + overflow: hidden; + padding: var(--d-small); + position: relative; + } + .chat-stage__link::after { + content: ''; + border-radius: calc(var(--radius-large) * 3); + width: calc(var(--width-base) - var(--d-small)); + height: calc(var(--width-base) - var(--d-small)); + position: absolute; + background-color: var(--c-secondary); + } +`; diff --git a/packages/chat-component/src/styles/chat-thread-component.ts b/packages/chat-component/src/styles/chat-thread-component.ts index 7efa2f4d..d8a8ff12 100644 --- a/packages/chat-component/src/styles/chat-thread-component.ts +++ b/packages/chat-component/src/styles/chat-thread-component.ts @@ -12,13 +12,21 @@ export const styles = css` } 100% { opacity: 1; - top: 0px; + top: 0; } } + .chat__header--button { + display: flex; + align-items: center; + } .chat__header { display: flex; + align-items: top; justify-content: flex-end; - padding: 20px; + padding: var(--d-base); + } + .chat__header--button { + margin-right: var(--d-base); } .chat__list { color: var(--text-color); @@ -29,68 +37,68 @@ export const styles = css` } .chat__footer { width: 100%; - height: 70px; + height: calc(var(--d-large) + var(--d-base)); } .chat__listItem { - max-width: 90%; - min-width: 80%; + max-width: var(--width-wide); + min-width: var(--width-base); display: flex; flex-direction: column; height: auto; @media (min-width: 768px) { max-width: 55%; - min-width: 50%; + min-width: var(--width-narrow); } } .chat__txt { animation: chatmessageanimation 0.5s ease-in-out; - background-color: var(--secondary-color); + background-color: var(--c-secondary); color: var(--text-color); - border-radius: 10px; + border-radius: var(--radius-base); margin-top: 8px; word-wrap: break-word; margin-block-end: 0; position: relative; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - border: 1px solid var(--light-gray); + box-shadow: var(--shadow); + border: var(--border-thin) solid var(--c-light-gray); } .chat__txt.error { - border: 3px solid var(--error-color); + border: var(--border-base) solid var(--error-color); color: var(--error-color); - padding: 20px; - background: var(--error-color-background); + padding: var(--d-base); + background: var(--c-error-background); } .chat__txt.user-message { - background: linear-gradient(to left, var(--accent-contrast), var(--accent-high)); - color: var(--white); - border: 1px solid var(--accent-lighter); + background: linear-gradient(to left, var(--c-accent-dark), var(--c-accent-high)); + color: var(--c-white); + border: var(--border-thin) solid var(--c-accent-light); } .chat__listItem.user-message { align-self: flex-end; } .chat__txt--entry { - padding: 0 20px; + padding: 0 var(--d-base); } .chat__txt--info { font-size: smaller; font-style: italic; margin: 0; - margin-top: 1px; + margin-top: var(--border-thin); } .user-message .chat__txt--info { text-align: right; } .items__listWrapper { - border-top: 1px solid var(--light-gray); + border-top: var(--border-thin) solid var(--c-light-gray); display: grid; - padding: 0 20px; + padding: 0 var(--d-base); grid-template-columns: 1fr 18fr; } .items__listWrapper svg { - fill: var(--accent-high); - width: 30px; - margin: 32px auto; + fill: var(--c-accent-high); + width: var(--d-large); + margin: var(--d-large) auto; } svg { height: auto; @@ -99,33 +107,33 @@ export const styles = css` .items__list.followup { display: flex; flex-direction: row; - padding: 20px; + padding: var(--d-base); list-style-type: none; flex-wrap: wrap; } .items__list.steps { - padding: 0 20px 0 40px; + padding: 0 var(--d-base) 0 var(--d-xlarge); list-style-type: disc; } .chat__citations { - border-top: 1px solid var(--light-gray); + border-top: var(--border-thin) solid var(--c-light-gray); } .items__list { - margin: 10px 0; + margin: var(--d-small) 0; display: block; - padding: 0 20px; + padding: 0 var(--d-base); } .items__listItem--followup { cursor: pointer; - padding: 0 5px; - border-radius: 10px; - border: 1px solid var(--accent-high); - margin: 5px; + padding: 0 var(--d-xsmall); + border-radius: var(--radius-base); + border: var(--border-thin) solid var(--c-accent-high); + margin: var(--d-xsmall); transition: background-color 0.3s ease-in-out; } .items__listItem--followup:hover, .items__listItem--followup:focus { - background-color: var(--accent-lighter); + background-color: var(--c-accent-light); cursor: pointer; } .items__link { @@ -133,21 +141,21 @@ export const styles = css` color: var(--text-color); } .steps .items__listItem--step { - padding: 5px 0; - font-size: 14px; + padding: var(--d-xsmall) 0; + font-size: var(--font-base); line-height: 1; } .followup .items__link { - color: var(--accent-high); + color: var(--c-accent-high); display: block; - padding: 5px 0; - border-bottom: 1px solid var(--light-gray); - font-size: small; + padding: var(--d-xsmall) 0; + border-bottom: var(--border-thin) solid var(--c-light-gray); + font-size: var(--font-small); } .citation { - background-color: var(--accent-lighter); + background-color: var(--c-accent-light); border-radius: 3px; - padding: 2px; + padding: calc(var(--d-small) / 5); margin-left: 3px; } `; diff --git a/packages/chat-component/src/styles/citation-list.ts b/packages/chat-component/src/styles/citation-list.ts index c8ccbc24..89357a6a 100644 --- a/packages/chat-component/src/styles/citation-list.ts +++ b/packages/chat-component/src/styles/citation-list.ts @@ -7,21 +7,21 @@ export const styles = css` } .items__list { border-top: none; - padding: 0px 20px; - margin: 10px 0; + padding: 0 var(--d-base); + margin: var(--d-small) 0; display: block; } .items__listItem { display: inline-block; - background-color: var(--accent-lighter); - border-radius: 5px; + background-color: var(--c-accent-light); + border-radius: var(--radius-small); text-decoration: none; - padding: 5px; + padding: var(--d-xsmall); margin-top: 5px; - font-size: small; + font-size: var(--font-small); } .items__listItem.active { - background-color: var(--accent-high); + background-color: var(--c-accent-high); } .items__listItem:not(first-child) { margin-left: 5px; @@ -31,6 +31,6 @@ export const styles = css` color: var(--text-color); } .items__listItem.active .items__link { - color: var(--white); + color: var(--c-white); } `; diff --git a/packages/chat-component/src/styles/link-icon.ts b/packages/chat-component/src/styles/link-icon.ts new file mode 100644 index 00000000..8c2357c0 --- /dev/null +++ b/packages/chat-component/src/styles/link-icon.ts @@ -0,0 +1,36 @@ +import { css } from 'lit'; + +export const styles = css` + a svg { + width: calc(var(--width-base) - var(--d-small)); + height: calc(var(--width-base) - var(--d-small)); + position: relative; + z-index: 1; + } + a { + flex-shrink: 0; + border-radius: calc(var(--radius-large) * 3); + border: var(--border-thicker) solid transparent; + background-origin: border-box; + background-clip: content-box, border-box; + background-size: cover; + background-image: linear-gradient(to right, var(--c-accent-light), var(--c-accent-high)); + width: calc(var(--d-xlarge) * 2); + height: calc(var(--d-xlarge) * 2); + display: flex; + align-items: center; + justify-content: center; + margin-right: var(--d-large); + overflow: hidden; + padding: var(--d-small); + position: relative; + } + a::after { + content: ''; + border-radius: calc(var(--radius-large) * 3); + width: calc(var(--width-base) - var(--d-small)); + height: calc(var(--width-base) - var(--d-small)); + position: absolute; + background-color: var(--c-secondary); + } +`; diff --git a/packages/chat-component/src/styles/loading-indicator.ts b/packages/chat-component/src/styles/loading-indicator.ts index c0d0a62d..eab7a0c2 100644 --- a/packages/chat-component/src/styles/loading-indicator.ts +++ b/packages/chat-component/src/styles/loading-indicator.ts @@ -14,9 +14,9 @@ export const styles = css` align-items: center; } svg { - width: 30px; + width: var(--d-large); height: 30px; - fill: var(--accent-lighter); + fill: var(--c-accent-light); animation: spinneranimation 1s linear infinite; margin-right: 10px; } diff --git a/packages/chat-component/src/styles/tab-component.ts b/packages/chat-component/src/styles/tab-component.ts index 13f22396..03547c03 100644 --- a/packages/chat-component/src/styles/tab-component.ts +++ b/packages/chat-component/src/styles/tab-component.ts @@ -4,9 +4,9 @@ export const styles = css` .tab-component__list { list-style-type: none; display: flex; - box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 10px; - border-radius: 10px; - padding: 3px; + box-shadow: var(--shadow); + border-radius: var(--radius-base); + padding: var(--d-xsmall); width: 450px; margin: 0 auto; justify-content: space-evenly; @@ -16,23 +16,23 @@ export const styles = css` text-align: center; } .tab-component__link.active { - background: linear-gradient(to left, var(--accent-contrast), var(--accent-high)); - color: var(--white); + background: linear-gradient(to left, var(--c-accent-light), var(--c-accent-high)); + color: var(--c-white); } .tab-component__link:not(.active):hover { - background: var(--light-gray); + background: var(--c-light-gray); cursor: pointer; } .tab-component__link { border-bottom: 4px solid transparent; - border-radius: 5px; + border-radius: var(--radius-small); text-decoration: none; color: var(--text-color); font-weight: bold; - font-size: small; + font-size: var(--font-small); cursor: pointer; display: block; - padding: 10px; + padding: var(--d-small); } .tab-component__content { position: relative; diff --git a/packages/chat-component/src/styles/teaser-list-component.ts b/packages/chat-component/src/styles/teaser-list-component.ts index 4a2be620..209eaf84 100644 --- a/packages/chat-component/src/styles/teaser-list-component.ts +++ b/packages/chat-component/src/styles/teaser-list-component.ts @@ -3,12 +3,12 @@ import { css } from 'lit'; export const styles = css` .headline { color: var(--text-color); - font-size: 5vw; + font-size: var(--font-r-large); padding: 0; - margin: 10px 0 30px; + margin: var(--d-small) 0 var(--d-large); @media (min-width: 1024px) { - font-size: 3vw; + font-size: var(--font-r-base); text-align: center; } } @@ -16,7 +16,7 @@ export const styles = css` text-decoration: none; color: var(--text-color); display: block; - font-size: 1.2rem; + font-size: var(--font-rel-base); } .teaser-list { list-style-type: none; @@ -35,14 +35,14 @@ export const styles = css` } } .teaser-list-item { - padding: 10px; - border-radius: 10px; - background: var(--white); - margin: 4px; + padding: var(--d-small); + border-radius: var(--radius-base); + background: var(--c-white); + margin: var(--d-xsmall); color: var(--text-color); justify-content: space-evenly; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - border: 3px solid transparent; + box-shadow: var(--shadow); + border: var(--border-base) solid transparent; @media (min-width: 768px) { min-height: 100px; @@ -50,13 +50,13 @@ export const styles = css` } .teaser-list-item:hover, .teaser-list-item:focus { - color: var(--accent-dark); - background: var(--secondary-color); + color: var(--c-accent-dark); + background: var(--c-secondary); transition: all 0.3s ease-in-out; - border-color: var(--accent-high); + border-color: var(--c-accent-high); } .teaser-list-item .teaser-click-label { - color: var(--accent-high); + color: var(--c-accent-high); font-weight: bold; display: block; margin-top: 20px; diff --git a/packages/chat-component/src/styles/voice-input-button.ts b/packages/chat-component/src/styles/voice-input-button.ts index 6b992cbd..b34b1920 100644 --- a/packages/chat-component/src/styles/voice-input-button.ts +++ b/packages/chat-component/src/styles/voice-input-button.ts @@ -10,19 +10,19 @@ export const styles = css` box-shadow: none; border: none; cursor: pointer; - width: 50px; + width: var(--d-xlarge); height: 100%; } button:hover, button:focus { - background: var(--secondary-color); + background: var(--c-secondary); } button:hover svg, button:focus svg { opacity: 0.8; } .not-recording svg { - fill: var(--black); + fill: var(--c-black); } .recording svg { fill: var(--red); diff --git a/packages/webapp/src/api/models.ts b/packages/webapp/src/api/models.ts index 8bfe0503..a15dd119 100644 --- a/packages/webapp/src/api/models.ts +++ b/packages/webapp/src/api/models.ts @@ -10,6 +10,19 @@ export const enum RetrievalMode { Text = 'text', } +export const enum CustomStyles { + AccentHigh = 'AccentHigh', + AccentLight = 'AccentLighter', + AccentDark = 'AccentContrast', + TextColor = 'TextColor', + BackgroundColor = 'BackgroundColor', + FormBackgroundColor = 'FormBackgroundColor', + ForegroundColor = 'ForegroundColor', + BorderRadius = 'BorderRadius', + BorderWidth = 'BorderWidth', + FontBaseSize = 'FontBaseSize', +} + export type RequestOverrides = { retrieval_mode?: RetrievalMode; semantic_ranker?: boolean; diff --git a/packages/webapp/src/components/SettingsStyles/SettingsStyles.css b/packages/webapp/src/components/SettingsStyles/SettingsStyles.css new file mode 100644 index 00000000..00e3a07f --- /dev/null +++ b/packages/webapp/src/components/SettingsStyles/SettingsStyles.css @@ -0,0 +1,25 @@ +.ms-style-picker.colors, +.ms-style-picker.sliders { + display: grid; + margin-top: 10px; + padding-bottom: 20px; +} + +.ms-style-picker.colors { + grid-template-columns: repeat(4, 1fr); +} + +.ms-style-picker > input, +.ms-style-picker > label { + margin-bottom: 3px; + margin-right: 10px; + font-size: small; + font-weight: bold; +} + +.ms-settings-input-slider { + display: flex; + align-items: center; + justify-content: space-around; + margin-bottom: 10px; +} diff --git a/packages/webapp/src/components/SettingsStyles/SettingsStyles.tsx b/packages/webapp/src/components/SettingsStyles/SettingsStyles.tsx new file mode 100644 index 00000000..b1f1b748 --- /dev/null +++ b/packages/webapp/src/components/SettingsStyles/SettingsStyles.tsx @@ -0,0 +1,132 @@ +import React, { useState, useEffect } from 'react'; +import './SettingsStyles.css'; + +export type CustomStylesState = { + AccentHigh: string; + AccentLight: string; + AccentDark: string; + TextColor: string; + BackgroundColor: string; + FormBackgroundColor: string; + BorderRadius: string; + BorderWidth: string; + FontBaseSize: string; +}; + +interface Props { + onChange: (newStyles: CustomStylesState) => void; +} + +export const SettingsStyles = ({ onChange }: Props) => { + // this needs to come from an API call to some config persisted in the DB + const styleDefaultsLight = { + AccentHigh: '#692b61', + AccentLight: '#f6d5f2', + AccentDark: '#5e3c7d', + TextColor: '#123f58', + BackgroundColor: '#e3e3e3', + ForegroundColor: '#4e5288', + FormBackgroundColor: '#f5f5f5', + }; + + const styleDefaultsDark = { + AccentHigh: '#dcdef8', + AccentLight: '#032219', + AccentDark: '#fdfeff', + TextColor: '#fdfeff', + BackgroundColor: '#e3e3e3', + ForegroundColor: '#4e5288', + FormBackgroundColor: '#32343e', + }; + + const getInitialStyles = (): CustomStylesState => { + const storedStyles = localStorage.getItem('ms-azoaicc:customStyles'); + const themeStore = localStorage.getItem('ms-azoaicc:isDarkTheme'); + const styleDefaults = themeStore === 'true' ? styleDefaultsDark : styleDefaultsLight; + if (storedStyles === '') { + localStorage.setItem('ms-azoaicc:customStyles', JSON.stringify(styleDefaults)); + } + return storedStyles + ? JSON.parse(storedStyles) + : { + AccentHigh: styleDefaults.AccentHigh, + AccentLight: styleDefaults.AccentLight, + AccentDark: styleDefaults.AccentDark, + TextColor: styleDefaults.TextColor, + BackgroundColor: styleDefaults.BackgroundColor, + ForegroundColor: styleDefaults.ForegroundColor, + FormBackgroundColor: styleDefaults.FormBackgroundColor, + BorderRadius: '10px', + BorderWidth: '3px', + FontBaseSize: '14px', + }; + }; + + const [customStyles, setStyles] = useState(getInitialStyles); + + useEffect(() => { + // Update the parent component when the state changes + onChange(customStyles); + }, [customStyles, onChange]); + + const handleInputChange = (key: keyof CustomStylesState, value: string | number) => { + setStyles((previousStyles) => ({ + ...previousStyles, + [key]: value, + })); + }; + + return ( + <> +

Modify Styles

+
+ {[ + { label: 'Accent High', name: 'AccentHigh', placeholder: 'Accent high' }, + { label: 'Accent Light', name: 'AccentLight', placeholder: 'Accent light' }, + { label: 'Accent Dark', name: 'AccentDark', placeholder: 'Accent dark' }, + { label: 'Text Color', name: 'TextColor', placeholder: 'Text color' }, + { label: 'Background Color', name: 'BackgroundColor', placeholder: 'Background color' }, + { label: 'Foreground Color', name: 'ForegroundColor', placeholder: 'Foreground color' }, + { label: 'Form background', name: 'FormBackgroundColor', placeholder: 'Form Background color' }, + ].map((input) => ( + + + handleInputChange(input.name as keyof CustomStylesState, event.target.value)} + /> + + ))} +
+
+ {/* Sliders */} + {[ + { label: 'Border Radius', name: 'BorderRadius', min: 0, max: 25 }, + { label: 'Border Width', name: 'BorderWidth', min: 1, max: 5 }, + { label: 'Font Base Size', name: 'FontBaseSize', min: 12, max: 20 }, + ].map((slider) => ( + +
+ + + handleInputChange(slider.name as keyof CustomStylesState, `${event.target.value}px`) + } + /> + {customStyles[slider.name as keyof CustomStylesState]} +
+
+ ))} +
+ + ); +}; diff --git a/packages/webapp/src/components/SettingsStyles/index.tsx b/packages/webapp/src/components/SettingsStyles/index.tsx new file mode 100644 index 00000000..086817fe --- /dev/null +++ b/packages/webapp/src/components/SettingsStyles/index.tsx @@ -0,0 +1 @@ +export * from './SettingsStyles.jsx'; diff --git a/packages/webapp/src/components/ThemeSwitch/ThemeSwitch.css b/packages/webapp/src/components/ThemeSwitch/ThemeSwitch.css new file mode 100644 index 00000000..d6ab5492 --- /dev/null +++ b/packages/webapp/src/components/ThemeSwitch/ThemeSwitch.css @@ -0,0 +1,5 @@ +.ms-toggle-wrapper { + display: flex; + align-items: flex-start; + margin-top: 20px; +} diff --git a/packages/webapp/src/components/ThemeSwitch/ThemeSwitch.tsx b/packages/webapp/src/components/ThemeSwitch/ThemeSwitch.tsx new file mode 100644 index 00000000..3b8230af --- /dev/null +++ b/packages/webapp/src/components/ThemeSwitch/ThemeSwitch.tsx @@ -0,0 +1,35 @@ +// ThemeSwitch.tsx +import React, { useEffect } from 'react'; +import { Toggle } from '@fluentui/react'; +import './ThemeSwitch.css'; + +interface ThemeSwitchProps { + onToggle: (isDarkTheme: boolean) => void; + isDarkTheme: boolean; + isConfigPanelOpen: boolean; +} + +export const ThemeSwitch: React.FC = ({ onToggle, isDarkTheme, isConfigPanelOpen }) => { + const handleToggleChange = () => { + onToggle(!isDarkTheme); // Pass the new theme state to the parent component + }; + + useEffect(() => { + // Toggle 'dark' class on the shell app body element based on the isDarkTheme prop and isConfigPanelOpen + document.body.classList.toggle('dark', isDarkTheme); + document.documentElement.dataset.theme = isDarkTheme && isConfigPanelOpen ? 'dark' : ''; + localStorage.removeItem('ms-azoaicc:isDarkTheme'); + }, [isDarkTheme, isConfigPanelOpen]); + + return ( +
+ +
+ ); +}; diff --git a/packages/webapp/src/components/ThemeSwitch/index.tsx b/packages/webapp/src/components/ThemeSwitch/index.tsx new file mode 100644 index 00000000..83894a98 --- /dev/null +++ b/packages/webapp/src/components/ThemeSwitch/index.tsx @@ -0,0 +1 @@ +export * from './ThemeSwitch.jsx'; diff --git a/packages/webapp/src/index.css b/packages/webapp/src/index.css index 73abda28..cbd8bd73 100644 --- a/packages/webapp/src/index.css +++ b/packages/webapp/src/index.css @@ -9,6 +9,11 @@ body { padding: 0; } +body.dark { + background: #1e1e1e; + color: #fff; +} + html { background: #f2f2f2; diff --git a/packages/webapp/src/pages/chat/Chat.tsx b/packages/webapp/src/pages/chat/Chat.tsx index 0cc8bde8..0ade9c49 100644 --- a/packages/webapp/src/pages/chat/Chat.tsx +++ b/packages/webapp/src/pages/chat/Chat.tsx @@ -2,10 +2,13 @@ import { useEffect, useRef, useState } from 'react'; import styles from './Chat.module.css'; import { RetrievalMode, apiBaseUrl, type RequestOverrides } from '../../api/index.js'; import { SettingsButton } from '../../components/SettingsButton/index.js'; -import { Checkbox, DefaultButton, Dropdown, Panel, SpinButton, TextField, TooltipHost } from '@fluentui/react'; +import { Checkbox, DefaultButton, Dropdown, Panel, SpinButton, TextField, TooltipHost, Toggle } from '@fluentui/react'; import type { IDropdownOption } from '@fluentui/react/lib-commonjs/Dropdown'; import 'chat-component'; import { toolTipText, toolTipTextCalloutProps } from '../../i18n/tooltips.js'; +import { SettingsStyles } from '../../components/SettingsStyles/SettingsStyles.js'; +import type { CustomStylesState } from '../../components/SettingsStyles/SettingsStyles.js'; +import { ThemeSwitch } from '../../components/ThemeSwitch/ThemeSwitch.js'; const Chat = () => { const [isConfigPanelOpen, setIsConfigPanelOpen] = useState(false); @@ -22,7 +25,10 @@ const Chat = () => { const [isLoading] = useState(false); - useEffect(() => chatMessageStreamEnd.current?.scrollIntoView({ behavior: 'smooth' }), [isLoading]); + const [isBrandingEnabled, setEnableBranding] = useState(() => { + const storedBranding = localStorage.getItem('ms-azoaicc:isBrandingEnabled'); + return storedBranding ? JSON.parse(storedBranding) : false; + }); const onPromptTemplateChange = ( _event?: React.FormEvent, @@ -59,6 +65,10 @@ const Chat = () => { setExcludeCategory(newValue || ''); }; + const onEnableBrandingChange = (_event?: React.FormEvent, checked?: boolean) => { + setEnableBranding(!!checked); + }; + const onUseSuggestFollowupQuestionsChange = ( _event?: React.FormEvent, checked?: boolean, @@ -66,6 +76,106 @@ const Chat = () => { setUseSuggestFollowupQuestions(!!checked); }; + const [isDarkTheme, setIsDarkTheme] = useState(() => { + const storedTheme = localStorage.getItem('ms-azoaicc:isDarkTheme'); + return storedTheme ? JSON.parse(storedTheme) : false; + }); + + const [customStyles, setCustomStyles] = useState(() => { + const styleDefaultsLight = { + AccentHigh: '#692b61', + AccentLight: '#f6d5f2', + AccentDark: '#5e3c7d', + TextColor: '#123f58', + BackgroundColor: '#e3e3e3', + ForegroundColor: '#4e5288', + FormBackgroundColor: '#f5f5f5', + BorderRadius: '10px', + BorderWidth: '3px', + FontBaseSize: '14px', + }; + + const styleDefaultsDark = { + AccentHigh: '#dcdef8', + AccentLight: '#032219', + AccentDark: '#fdfeff', + TextColor: '#fdfeff', + BackgroundColor: '#32343e', + ForegroundColor: '#4e5288', + FormBackgroundColor: '#32343e', + BorderRadius: '10px', + BorderWidth: '3px', + FontBaseSize: '14px', + }; + const defaultStyles = isDarkTheme ? styleDefaultsDark : styleDefaultsLight; + const storedStyles = localStorage.getItem('ms-azoaicc:customStyles'); + return storedStyles ? JSON.parse(storedStyles) : defaultStyles; + }); + + const handleCustomStylesChange = (newStyles: CustomStylesState) => { + setCustomStyles(newStyles); + }; + + const handleThemeToggle = (newIsDarkTheme: boolean) => { + // Get the ChatComponent instance (modify this according to how you manage your components) + const chatComponent = document.querySelector('chat-component'); + if (chatComponent) { + // Remove existing style attributes + chatComponent.removeAttribute('style'); + // eslint-disable-next-line unicorn/prefer-dom-node-dataset + chatComponent.setAttribute('data-theme', newIsDarkTheme ? 'dark' : ''); + } + // Update the body class and html data-theme + localStorage.removeItem('ms-azoaicc:customStyles'); + + // Update the state + setIsDarkTheme(newIsDarkTheme); + }; + + useEffect(() => { + // Update the state when local storage changes + const handleStorageChange = () => { + const storedStyles = localStorage.getItem('ms-azoaicc:customStyles'); + if (storedStyles) { + setCustomStyles(JSON.parse(storedStyles)); + } + + const storedBranding = localStorage.getItem('ms-azoaicc:isBrandingEnabled'); + if (storedBranding) { + setEnableBranding(JSON.parse(storedBranding)); + } + + const storedTheme = localStorage.getItem('ms-azoaicc:isDarkTheme'); + if (storedTheme) { + setIsDarkTheme(JSON.parse(storedTheme)); + } + }; + + // Attach the event listener + window.addEventListener('storage', handleStorageChange); + + // Store customStyles in local storage whenever it changes + localStorage.setItem('ms-azoaicc:customStyles', JSON.stringify(customStyles)); + + // Store isBrandingEnabled in local storage whenever it changes + localStorage.setItem('ms-azoaicc:isBrandingEnabled', JSON.stringify(isBrandingEnabled)); + + // Store isDarkTheme in local storage whenever it changes + localStorage.setItem('ms-azoaicc:isDarkTheme', JSON.stringify(isDarkTheme)); + + // Scroll into view when isLoading changes + chatMessageStreamEnd.current?.scrollIntoView({ behavior: 'smooth' }); + // Toggle 'dark' class on the shell app body element based on the isDarkTheme prop and isConfigPanelOpen + document.body.classList.toggle('dark', isDarkTheme); + document.documentElement.dataset.theme = isDarkTheme ? 'dark' : ''; + // Clean up the event listener when the component is unmounted + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, [customStyles, isBrandingEnabled, isDarkTheme, isLoading]); + + const [isChatStylesAccordionOpen, setIsChatStylesAccordionOpen] = useState(false); + const overrides: RequestOverrides = { retrieval_mode: retrievalMode, top: retrieveCount, @@ -93,6 +203,9 @@ const Chat = () => { data-use-stream={useStream} data-approach="rrr" data-overrides={JSON.stringify(overrides)} + data-custom-styles={JSON.stringify(customStyles)} + data-custom-branding={JSON.stringify(isBrandingEnabled)} + data-theme={isDarkTheme ? 'dark' : ''} >
@@ -106,6 +219,9 @@ const Chat = () => { onRenderFooterContent={() => setIsConfigPanelOpen(false)}>Close} isFooterAtBottom={true} > + + + { onChange={onUseStreamChange} /> +
+ setIsChatStylesAccordionOpen(!isChatStylesAccordionOpen)} + /> + {isChatStylesAccordionOpen && ( + <> + + + + + )} + + + +
); diff --git a/tests/e2e/webapp.spec.ts b/tests/e2e/webapp.spec.ts index ee1acf8a..54a0042a 100644 --- a/tests/e2e/webapp.spec.ts +++ b/tests/e2e/webapp.spec.ts @@ -438,6 +438,8 @@ test.describe('generate answer', () => { test('follow up questions', async ({ page }) => { const followupQuestions = page.getByTestId('followUpQuestion'); + await followupQuestions.waitFor(); + await expect(followupQuestions).toHaveCount(3); const chatInput = page.getByTestId('question-input'); @@ -471,6 +473,44 @@ test.describe('developer settings', () => { await expect(page.getByLabel('Retrieval mode')).toContainText('Vectors + Text (Hybrid)'); }); + test('enable branding toggled', async ({ page }) => { + await page.goto('/'); + await page.getByTestId('button__developer-settings').click(); + // toggle enable branding + await page.locator('label').filter({ hasText: 'Enable branding' }).click(); + await page.waitForTimeout(1000); + // await for brading to be visible + await expect(page.getByTestId('chat-branding')).toBeVisible(); + }); + + test('select dark theme', async ({ page }) => { + await page.goto('/'); + expect(await page.getAttribute('html', 'data-theme')).toBe(''); + await page.getByTestId('button__developer-settings').click(); + await page.locator('label').filter({ hasText: 'Select theme' }).click(); + // Wait for the state to update + await page.waitForFunction(() => { + return document.querySelector('html')?.dataset.theme === 'dark'; + }); + // Check the updated state + expect(await page.getAttribute('html', 'data-theme')).toBe('dark'); + }); + + test('customize chat styles toggled and check localStorage', async ({ page }) => { + await page.goto('/'); + await page.getByTestId('button__developer-settings').click(); + await page.locator('label').filter({ hasText: 'Customize chat styles' }).click(); + + await page.waitForTimeout(1000); + // check if localStorage has an item called 'customStyles' and it's not empty + const hasCustomStyles = await page.evaluate(() => { + const customStyles = localStorage.getItem('ms-azoaicc:customStyles'); + return customStyles !== null && customStyles.trim() !== ''; + }); + + await expect(hasCustomStyles).toBe(true); + }); + test('handle no stream parsing', async ({ page }) => { await page.goto('/'); await page.getByTestId('default-question').nth(0).click();