From 21f673f061411d787a4355c132fff3a37d262d44 Mon Sep 17 00:00:00 2001 From: simonmilord Date: Fri, 17 Jan 2025 14:02:43 -0500 Subject: [PATCH] initial commit --- .../exampleQuanticGeneratedAnswer.html | 1 + .../exampleQuanticGeneratedAnswer.js | 7 ++ .../__tests__/quanticGeneratedAnswer.test.js | 89 +++++++++++++++++-- .../quanticGeneratedAnswer.js | 57 ++++++++++-- .../templates/generatedAnswer.html | 1 + .../quanticGeneratedAnswerContent.js | 2 + 6 files changed, 142 insertions(+), 15 deletions(-) diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticGeneratedAnswer/exampleQuanticGeneratedAnswer.html b/packages/quantic/force-app/examples/main/lwc/exampleQuanticGeneratedAnswer/exampleQuanticGeneratedAnswer.html index 693f889149e..8083f641da6 100644 --- a/packages/quantic/force-app/examples/main/lwc/exampleQuanticGeneratedAnswer/exampleQuanticGeneratedAnswer.html +++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticGeneratedAnswer/exampleQuanticGeneratedAnswer.html @@ -23,6 +23,7 @@ engine-id={engineId} fields-to-include-in-citations={config.fieldsToIncludeInCitations} collapsible={config.collapsible} + max-collapsed-height={config.maxCollapsedHeight} with-toggle={config.withToggle} > diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticGeneratedAnswer/exampleQuanticGeneratedAnswer.js b/packages/quantic/force-app/examples/main/lwc/exampleQuanticGeneratedAnswer/exampleQuanticGeneratedAnswer.js index 6e483002701..993f801ae6f 100644 --- a/packages/quantic/force-app/examples/main/lwc/exampleQuanticGeneratedAnswer/exampleQuanticGeneratedAnswer.js +++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticGeneratedAnswer/exampleQuanticGeneratedAnswer.js @@ -22,6 +22,13 @@ export default class ExampleQuanticGeneratedAnswer extends LightningElement { description: 'Indicates whether the answer should be collapsible.', defaultValue: false, }, + { + attribute: 'maxCollapsedHeight', + label: 'Max Collapsed Height', + description: + 'The maximum height of the answer when it is collapsed, in pixels.', + defaultValue: 250, + }, { attribute: 'withToggle', label: 'With Toggle', diff --git a/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js b/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js index 42333a3940f..9e61d44537c 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +++ b/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js @@ -4,7 +4,7 @@ import {createElement} from 'lwc'; import QuanticGeneratedAnswer from 'c/quanticGeneratedAnswer'; import * as mockHeadlessLoader from 'c/quanticHeadlessLoader'; -let mockAnswerHeight = 300; +let mockAnswerHeight = 250; jest.mock('c/quanticHeadlessLoader'); jest.mock('c/quanticUtils', () => ({ @@ -31,6 +31,7 @@ const defaultOptions = { answerConfigurationId: undefined, withToggle: false, collapsible: false, + maxCollapsedHeight: 250, }; function createTestComponent(options = defaultOptions) { @@ -101,7 +102,7 @@ const exampleEngine = { id: 'dummy engine', }; let isInitialized = false; -const maximumAnswerHeight = 250; +const defaultAnswerHeight = 250; function prepareHeadlessState() { // @ts-ignore @@ -342,7 +343,7 @@ describe('c-quantic-generated-answer', () => { describe('when the property collapsible is set to true', () => { describe('when the answer is shorter than the maximum answer height', () => { beforeEach(() => { - mockAnswerHeight = maximumAnswerHeight - 100; + mockAnswerHeight = defaultAnswerHeight - 100; }); it('should not display the generating answer message', async () => { @@ -363,7 +364,7 @@ describe('c-quantic-generated-answer', () => { describe('when the answer is longer than the maximum answer height', () => { beforeEach(() => { - mockAnswerHeight = maximumAnswerHeight + 100; + mockAnswerHeight = defaultAnswerHeight + 100; }); it('should display the generating answer message', async () => { @@ -489,7 +490,7 @@ describe('c-quantic-generated-answer', () => { describe('when the property collapsible is set to true', () => { describe('when the answer is shorter than the maximum answer height', () => { beforeEach(() => { - mockAnswerHeight = maximumAnswerHeight - 100; + mockAnswerHeight = defaultAnswerHeight - 100; }); it('should not display the generating answer message', async () => { @@ -525,7 +526,7 @@ describe('c-quantic-generated-answer', () => { describe('when the answer is longer than the maximum answer height', () => { beforeEach(() => { - mockAnswerHeight = maximumAnswerHeight + 100; + mockAnswerHeight = defaultAnswerHeight + 100; }); it('should not display the generating answer message', async () => { @@ -558,6 +559,82 @@ describe('c-quantic-generated-answer', () => { expect(generatedAnswerCollapseToggle).not.toBeNull(); }); }); + + describe('when the property maxCollapsedHeight is set to a custom value', () => { + // The valid range is between 150 and 500 pixels. + describe('when the value is within the valid range', () => { + beforeEach(() => { + mockAnswerHeight = defaultAnswerHeight - 25; + }); + + it('should set the answer height with the custom value', async () => { + const expectedAnswerHeightValue = 300; + const element = createTestComponent({ + ...defaultOptions, + maxCollapsedHeight: expectedAnswerHeightValue, + }); + await flushPromises(); + + const generatedAnswer = element.shadowRoot.querySelector( + '.generated-answer__answer' + ); + expect(generatedAnswer).not.toBeNull(); + const computedStyle = getComputedStyle(generatedAnswer); + console.log( + 'computedStyle: ' + JSON.stringify(computedStyle.maxHeight) + ); + expect(computedStyle.maxHeight).toEqual( + `${expectedAnswerHeightValue}px` + ); + }); + }); + + describe('when the value is greater than the valid range', () => { + beforeEach(() => { + mockAnswerHeight = defaultAnswerHeight + 100; + }); + + it('should set the answer height with the fallback default value', async () => { + const element = createTestComponent({ + ...defaultOptions, + maxCollapsedHeight: 550, + }); + await flushPromises(); + + const generatedAnswer = element.shadowRoot.querySelector( + '.generated-answer__answer' + ); + expect(generatedAnswer).not.toBeNull(); + const computedStyle = getComputedStyle(generatedAnswer); + expect(computedStyle.maxHeight).toEqual( + `${defaultAnswerHeight}px` + ); + }); + }); + + describe('when the value is smaller than the valid range', () => { + beforeEach(() => { + mockAnswerHeight = defaultAnswerHeight; + }); + + it('should set the answer height with the fallback default value', async () => { + const element = createTestComponent({ + ...defaultOptions, + maxCollapsedHeight: 100, + }); + await flushPromises(); + + const generatedAnswer = element.shadowRoot.querySelector( + '.generated-answer__answer' + ); + expect(generatedAnswer).not.toBeNull(); + const computedStyle = getComputedStyle(generatedAnswer); + expect(computedStyle.maxHeight).toEqual( + `${defaultAnswerHeight}px` + ); + }); + }); + }); }); it('should display the generated answer content', async () => { diff --git a/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js b/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js index 0f07971777e..6f3ba0b3aa1 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +++ b/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js @@ -44,6 +44,10 @@ const FEEDBACK_LIKED_STATE = 'liked'; const FEEDBACK_DISLIKED_STATE = 'disliked'; const FEEDBACK_NEUTRAL_STATE = 'neutral'; +const DEFAULT_COLLAPSED_HEIGHT = 250; +const MAX_COLLAPSED_HEIGHT = 500; +const MIN_COLLAPSED_HEIGHT = 150; + const GENERATED_ANSWER_DATA_KEY = 'coveo-generated-answer-data'; /** @@ -86,6 +90,13 @@ export default class QuanticGeneratedAnswer extends LightningElement { * @type {string} */ @api answerConfigurationId; + /** + * The maximum height (in px units) of the generated answer when it is collapsed. + * @api + * @type {number} + * @default {250} + */ + @api maxCollapsedHeight = DEFAULT_COLLAPSED_HEIGHT; labels = { generatedAnswerForYou, @@ -125,8 +136,6 @@ export default class QuanticGeneratedAnswer extends LightningElement { ariaLiveMessage; /** @type {boolean} */ hasInitializationError = false; - /** @type {number} */ - _maximumAnswerHeight = 250; /** @type {boolean} */ _exceedsMaximumHeight = false; /** @type {boolean} */ @@ -153,11 +162,7 @@ export default class QuanticGeneratedAnswer extends LightningElement { if (this.collapsible) { // If we are still streaming add a little extra height to the answer element to account for the next answer chunk. // This helps a lot with the jankyness of the answer fading out when the chunk is close but not yet over the max height. - const answerElementHeight = this.isStreaming - ? this.generatedAnswerElementHeight + 50 - : this.generatedAnswerElementHeight; - this._exceedsMaximumHeight = - answerElementHeight > this._maximumAnswerHeight; + this._exceedsMaximumHeight = this.isMaximumHeightExceeded(); } } @@ -259,6 +264,15 @@ export default class QuanticGeneratedAnswer extends LightningElement { } } + isMaximumHeightExceeded() { + const maximumAnswerHeight = this.validateMaxCollapsedHeight(); + const answerElementHeight = this.isStreaming + ? this.generatedAnswerElementHeight + 50 + : this.generatedAnswerElementHeight; + + return answerElementHeight > maximumAnswerHeight; + } + /** * handles hovering over a citation. * @param {string} id @@ -348,6 +362,14 @@ export default class QuanticGeneratedAnswer extends LightningElement { } }; + handleAnswerDoneGenerating = (event) => { + event.stopPropagation(); + if (this.collapsible) { + this._exceedsMaximumHeight = this.isMaximumHeightExceeded(); + } + this.updateGeneratedAnswerCSSVariables(); + }; + handleToggleCollapseAnswer() { this.state?.expanded ? this.generatedAnswer.collapse() @@ -385,13 +407,30 @@ export default class QuanticGeneratedAnswer extends LightningElement { } /** - * Sets the the value of the CSS variable "--maxHeight" the value of the _maximumAnswerHeight property. + * Sets the the value of the CSS variable "--maxHeight" the value of the maxCollapsedHeight property. */ updateGeneratedAnswerCSSVariables() { if (this._exceedsMaximumHeight) { const styles = this.generatedAnswerElement?.style; - styles.setProperty('--maxHeight', `${this._maximumAnswerHeight}px`); + styles.setProperty('--maxHeight', `${this.maxCollapsedHeight}px`); + } + } + + /** + * Validates that the value of the maxCollapsedHeight property is within acceptable bounds. + */ + validateMaxCollapsedHeight() { + const isValid = + this.maxCollapsedHeight >= MIN_COLLAPSED_HEIGHT && + this.maxCollapsedHeight <= MAX_COLLAPSED_HEIGHT; + + if (!isValid) { + console.warn( + `max-collapsed-height (${this.maxCollapsedHeight}px) must be between ${MIN_COLLAPSED_HEIGHT} and ${MAX_COLLAPSED_HEIGHT}. Falling back to default value: ${DEFAULT_COLLAPSED_HEIGHT}px.` + ); } + + return isValid ? this.maxCollapsedHeight : DEFAULT_COLLAPSED_HEIGHT; } get answer() { diff --git a/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html b/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html index c66a1726caa..207bbca84b9 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +++ b/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html @@ -53,6 +53,7 @@ answer-content-format={answerContentFormat} answer={answer} is-streaming={isStreaming} + onquantic__answergenerated={handleAnswerDoneGenerating} data-cy="generated-answer__content" > diff --git a/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js b/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js index a5e10c46698..fb1b5041331 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js +++ b/packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js @@ -11,6 +11,7 @@ import generatedTextContentTemplate from './templates/generatedTextContent.html' /** * The `QuanticGeneratedAnswerContent` component displays the generated answer content. * @category Internal + * @fires CustomEvent#quantic__answergenerated * @example * */ @@ -110,6 +111,7 @@ export default class QuanticGeneratedAnswerContent extends LightningElement { else { answerContainer.textContent = this.answer; } + this.dispatchEvent(new CustomEvent('quantic__answergenerated')); } get generatedAnswerContentClass() {