@coveo/quantic 3.39.1 → 3.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/force-app/main/default/classes/HeadlessControllerTest.cls +1 -0
  2. package/force-app/main/default/classes/InsightControllerTest.cls +1 -0
  3. package/force-app/main/default/classes/RecommendationsControllerTest.cls +1 -1
  4. package/force-app/main/default/classes/SampleTokenProvider.cls +1 -0
  5. package/force-app/main/default/classes/SampleTokenProviderTest.cls +1 -0
  6. package/force-app/main/default/labels/CustomLabels.labels-meta.xml +77 -0
  7. package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +54 -0
  8. package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +26 -3
  9. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +5 -0
  10. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +3 -1
  11. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/__tests__/quanticGeneratedAnswerBody.test.js +341 -0
  12. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js +148 -0
  13. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js-meta.xml +5 -0
  14. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.css +3 -0
  15. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.html +53 -0
  16. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/cannotAnswer.html +7 -0
  17. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/error.html +7 -0
  18. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/__tests__/quanticGeneratedAnswerContent.test.js +269 -0
  19. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js +136 -0
  20. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/templates/generatedMarkdownContent.css +10 -0
  21. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/quanticGeneratedAnswerStreamOfThought.test.js +348 -0
  22. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.css +17 -0
  23. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js +163 -0
  24. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js-meta.xml +5 -0
  25. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.css +1 -0
  26. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.html +32 -0
  27. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.css +1 -0
  28. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.html +65 -0
  29. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/__tests__/quanticGeneratedAnswerThread.test.js +285 -0
  30. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.css +47 -0
  31. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.html +67 -0
  32. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js +93 -0
  33. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js-meta.xml +5 -0
  34. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.css +0 -4
  35. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.html +1 -1
  36. package/force-app/main/default/lwc/quanticUtils/__tests__/markdownUtils.test.js +38 -0
  37. package/force-app/main/default/lwc/quanticUtils/markdownUtils.js +18 -0
  38. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +6 -6
  39. package/force-app/main/default/staticresources/coveoheadless/definitions/api/commerce/common/pagination.d.ts +1 -1
  40. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-generated-answer.d.ts +1 -0
  41. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-interactive-citation.d.ts +3 -3
  42. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/generated-answer/interactive-citation-analytics-client.d.ts +3 -0
  43. package/force-app/main/default/staticresources/coveoheadless/definitions/features/analytics/analytics-utils.d.ts +3 -2
  44. package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-analytics-actions.d.ts +2 -0
  45. package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
  46. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +7 -7
  47. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
  48. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  @isTest
2
2
  private class HeadlessControllerTest {
3
+ // This API key is intentionally public — it belongs to a sample organization used for samples/docs.
3
4
  static final String sampleHeadlessConfiguration = '{"accessToken":"xx564559b1-0045-48e1-953c-3addd1ee4457","organizationId":"searchuisamples"}';
4
5
  @IsTest
5
6
  static void shouldReturnStringifiedConfiguration() {
@@ -1,5 +1,6 @@
1
1
  @isTest
2
2
  private class InsightControllerTest {
3
+ // This API key is intentionally public — it belongs to a sample organization used for samples/docs.
3
4
  static final String sampleHeadlessConfiguration = '{"accessToken":"xx564559b1-0045-48e1-953c-3addd1ee4457","organizationId":"searchuisamples"}';
4
5
  @IsTest
5
6
  static void shouldReturnStringifiedConfiguration() {
@@ -1,6 +1,6 @@
1
1
  @isTest
2
2
  private class RecommendationsControllerTest {
3
- // This is a demo token and it's okay to be public.
3
+ // This API key is intentionally public it belongs to a sample organization used for samples/docs.
4
4
  static final String sampleHeadlessConfiguration = '{"accessToken":"xx564559b1-0045-48e1-953c-3addd1ee4457","organizationId":"searchuisamples"}';
5
5
  @IsTest
6
6
  static void shouldReturnStringifiedConfiguration() {
@@ -10,6 +10,7 @@ global with sharing class SampleTokenProvider implements ITokenProvider {
10
10
  headlessConfiguration.put('organizationId', 'searchuisamples');
11
11
  headlessConfiguration.put(
12
12
  'accessToken',
13
+ // This API key is intentionally public — it belongs to a sample organization used for samples/docs.
13
14
  'xx564559b1-0045-48e1-953c-3addd1ee4457'
14
15
  );
15
16
  return JSON.serialize(headlessConfiguration);
@@ -1,5 +1,6 @@
1
1
  @isTest
2
2
  private class SampleTokenProviderTest {
3
+ // This API key is intentionally public — it belongs to a sample organization used for samples/docs.
3
4
  static final String sampleHeadlessConfiguration = '{"accessToken":"xx564559b1-0045-48e1-953c-3addd1ee4457","organizationId":"searchuisamples"}';
4
5
  @IsTest
5
6
  static void shouldReturnStringifiedConfiguration() {
@@ -455,6 +455,13 @@
455
455
  <protected>false</protected>
456
456
  <shortDescription>Collapse [component label]</shortDescription>
457
457
  </labels>
458
+ <labels>
459
+ <fullName>quantic_CollapseButton</fullName>
460
+ <value>Collapse</value>
461
+ <language>en_US</language>
462
+ <protected>false</protected>
463
+ <shortDescription>Collapse</shortDescription>
464
+ </labels>
458
465
  <labels>
459
466
  <fullName>quantic_Expand</fullName>
460
467
  <value>Expand {{0}}</value>
@@ -1288,6 +1295,13 @@
1288
1295
  <protected>false</protected>
1289
1296
  <shortDescription>Something went wrong and we couldn't generate an answer.</shortDescription>
1290
1297
  </labels>
1298
+ <labels>
1299
+ <fullName>quantic_GeneratedAnswerErrorTurnLimitReached</fullName>
1300
+ <value>Conversation turn limit reached. Please start a new conversation.</value>
1301
+ <language>en_US</language>
1302
+ <protected>false</protected>
1303
+ <shortDescription>Generated answer conversation turn limit reached error.</shortDescription>
1304
+ </labels>
1291
1305
  <labels>
1292
1306
  <fullName>quantic_ShowingResultsFor</fullName>
1293
1307
  <value>Showing results for {{0}}</value>
@@ -1638,4 +1652,67 @@
1638
1652
  <protected>false</protected>
1639
1653
  <shortDescription>Submit follow-up</shortDescription>
1640
1654
  </labels>
1655
+ <labels>
1656
+ <fullName>quantic_ShowPreviousQuestions_plural</fullName>
1657
+ <value>Show {{0}} previous questions</value>
1658
+ <language>en_US</language>
1659
+ <protected>false</protected>
1660
+ <shortDescription>Show previous questions (plural)</shortDescription>
1661
+ </labels>
1662
+ <labels>
1663
+ <fullName>quantic_AgentGenerationStepAnalyzingQuestion</fullName>
1664
+ <value>Analyzing your question…</value>
1665
+ <language>en_US</language>
1666
+ <protected>false</protected>
1667
+ <shortDescription>Analyzing your question</shortDescription>
1668
+ </labels>
1669
+ <labels>
1670
+ <fullName>quantic_AgentGenerationStepAnalyzingQuestionCompleted</fullName>
1671
+ <value>Question analyzed</value>
1672
+ <language>en_US</language>
1673
+ <protected>false</protected>
1674
+ <shortDescription>Question analyzed</shortDescription>
1675
+ </labels>
1676
+ <labels>
1677
+ <fullName>quantic_AgentGenerationStepSearch</fullName>
1678
+ <value>Searching knowledge base…</value>
1679
+ <language>en_US</language>
1680
+ <protected>false</protected>
1681
+ <shortDescription>Searching knowledge base…</shortDescription>
1682
+ </labels>
1683
+ <labels>
1684
+ <fullName>quantic_AgentGenerationStepSearchCompleted</fullName>
1685
+ <value>Knowledge base searched</value>
1686
+ <language>en_US</language>
1687
+ <protected>false</protected>
1688
+ <shortDescription>Knowledge base searched</shortDescription>
1689
+ </labels>
1690
+ <labels>
1691
+ <fullName>quantic_AgentGenerationStepAnalyzingResults</fullName>
1692
+ <value>Analyzing results…</value>
1693
+ <language>en_US</language>
1694
+ <protected>false</protected>
1695
+ <shortDescription>Analyzing results…</shortDescription>
1696
+ </labels>
1697
+ <labels>
1698
+ <fullName>quantic_AgentGenerationStepAnalyzingResultsCompleted</fullName>
1699
+ <value>Results analyzed</value>
1700
+ <language>en_US</language>
1701
+ <protected>false</protected>
1702
+ <shortDescription>Results analyzed</shortDescription>
1703
+ </labels>
1704
+ <labels>
1705
+ <fullName>quantic_AgentGenerationStepAnswering</fullName>
1706
+ <value>Generating answer…</value>
1707
+ <language>en_US</language>
1708
+ <protected>false</protected>
1709
+ <shortDescription>Generating answer…</shortDescription>
1710
+ </labels>
1711
+ <labels>
1712
+ <fullName>quantic_AgentGenerationStepAnsweringCompleted</fullName>
1713
+ <value>Answer generated</value>
1714
+ <language>en_US</language>
1715
+ <protected>false</protected>
1716
+ <shortDescription>Answer generated</shortDescription>
1717
+ </labels>
1641
1718
  </CustomLabels>
@@ -22,6 +22,8 @@ const exampleCitations = [
22
22
  uri: 'https://example.com/2',
23
23
  },
24
24
  ];
25
+ const exampleEngineId = 'example engine id';
26
+ const exampleAnswerId = 'example answer id';
25
27
  jest.mock('c/quanticHeadlessLoader');
26
28
  jest.mock('c/quanticUtils', () => ({
27
29
  AriaLiveRegion: jest.fn(() => ({
@@ -50,6 +52,7 @@ jest.mock(
50
52
 
51
53
  /** @type {Object} */
52
54
  const defaultOptions = {
55
+ engineId: exampleEngineId,
53
56
  fieldsToIncludeInCitations: 'sfid,sfkbid,sfkavid,filetype',
54
57
  answerConfigurationId: undefined,
55
58
  withToggle: false,
@@ -348,6 +351,7 @@ describe('c-quantic-generated-answer', () => {
348
351
  isStreaming: true,
349
352
  answer: exampleAnswer,
350
353
  answerContentFormat: exampleAnswerContentFormat,
354
+ answerId: exampleAnswerId,
351
355
  };
352
356
  mockSuccessfulHeadlessInitialization();
353
357
  prepareHeadlessState();
@@ -461,6 +465,8 @@ describe('c-quantic-generated-answer', () => {
461
465
  expect(generatedAnswerContent.answerContentFormat).toBe(
462
466
  exampleAnswerContentFormat
463
467
  );
468
+ expect(generatedAnswerContent.engineId).toBe(exampleEngineId);
469
+ expect(generatedAnswerContent.answerId).toBe(exampleAnswerId);
464
470
  });
465
471
 
466
472
  it('should not display the generated answer actions', async () => {
@@ -784,6 +790,54 @@ describe('c-quantic-generated-answer', () => {
784
790
  expect(generatedAnswerCitations).not.toBeNull();
785
791
  expect(generatedAnswerCitations.disableCitationAnchoring).toBe(false);
786
792
  });
793
+
794
+ describe('when follow-ups are enabled', () => {
795
+ // TODO SFINT-6786: Add test cases to cover the behavior of the component when follow-ups are enabled based on the actual implementation of the follow-up feature in the state.
796
+ it.skip('should render the content section with the scrollable class and ignore the collapsible feature', async () => {
797
+ mockAnswerHeight = defaultAnswerHeight + 100;
798
+ const element = createTestComponent({
799
+ ...defaultOptions,
800
+ collapsible: true,
801
+ });
802
+ await flushPromises();
803
+
804
+ const generatedAnswerBody = element.shadowRoot.querySelector(
805
+ selectors.generatedAnswerBody
806
+ );
807
+
808
+ expect(generatedAnswerBody).not.toBeNull();
809
+ expect(
810
+ generatedAnswerBody.classList.contains(
811
+ 'generated-answer__content--scrollable'
812
+ )
813
+ ).toBe(true);
814
+
815
+ const generatedAnswerCollapseToggle =
816
+ element.shadowRoot.querySelector(
817
+ selectors.generatedAnswerCollapseToggle
818
+ );
819
+
820
+ expect(generatedAnswerCollapseToggle).toBeNull();
821
+ });
822
+ });
823
+
824
+ describe('when follow-ups are not enabled', () => {
825
+ it('should not render the content section with the scrollable class', async () => {
826
+ const element = createTestComponent();
827
+ await flushPromises();
828
+
829
+ const generatedAnswerBody = element.shadowRoot.querySelector(
830
+ selectors.generatedAnswerBody
831
+ );
832
+
833
+ expect(generatedAnswerBody).not.toBeNull();
834
+ expect(
835
+ generatedAnswerBody.classList.contains(
836
+ 'generated-answer__content--scrollable'
837
+ )
838
+ ).toBe(false);
839
+ });
840
+ });
787
841
  });
788
842
 
789
843
  describe('when the answer cannot be generated after a query is executed', () => {
@@ -172,6 +172,8 @@ export default class QuanticGeneratedAnswer extends LightningElement {
172
172
  /** @type {boolean} */
173
173
  hasInitializationError = false;
174
174
  /** @type {boolean} */
175
+ _areFollowUpsEnabled = false;
176
+ /** @type {boolean} */
175
177
  _exceedsMaximumHeight = false;
176
178
  /** @type {boolean} */
177
179
  _liked = false;
@@ -196,7 +198,7 @@ export default class QuanticGeneratedAnswer extends LightningElement {
196
198
 
197
199
  renderedCallback() {
198
200
  initializeWithHeadless(this, this.engineId, this.initialize);
199
- if (this.collapsible) {
201
+ if (this.isCollapsibleEnabled) {
200
202
  this._exceedsMaximumHeight = this.isMaximumHeightExceeded();
201
203
  }
202
204
  }
@@ -259,7 +261,7 @@ export default class QuanticGeneratedAnswer extends LightningElement {
259
261
  this.updateFeedbackState();
260
262
  this.ariaLiveMessage.dispatchMessage(this.getGeneratedAnswerStatus());
261
263
 
262
- if (this.collapsible) {
264
+ if (this.isCollapsibleEnabled) {
263
265
  this.updateGeneratedAnswerCSSVariables();
264
266
  }
265
267
  }
@@ -400,7 +402,7 @@ export default class QuanticGeneratedAnswer extends LightningElement {
400
402
 
401
403
  handleAnswerContentUpdated = (event) => {
402
404
  event.stopPropagation();
403
- if (this.collapsible) {
405
+ if (this.isCollapsibleEnabled) {
404
406
  this._exceedsMaximumHeight = this.isMaximumHeightExceeded();
405
407
  }
406
408
  this.updateGeneratedAnswerCSSVariables();
@@ -456,6 +458,10 @@ export default class QuanticGeneratedAnswer extends LightningElement {
456
458
  return this?.state?.answer;
457
459
  }
458
460
 
461
+ get answerId() {
462
+ return this?.state?.answerId;
463
+ }
464
+
459
465
  get citations() {
460
466
  return this?.state?.citations;
461
467
  }
@@ -481,6 +487,15 @@ export default class QuanticGeneratedAnswer extends LightningElement {
481
487
  return this.state.isVisible;
482
488
  }
483
489
 
490
+ get areFollowUpsEnabled() {
491
+ // TODO SFINT-6786: Modify this getter to return the actual value from the state for follow-up enabled/agentId.
492
+ return this._areFollowUpsEnabled;
493
+ }
494
+
495
+ get isCollapsibleEnabled() {
496
+ return this.collapsible && !this.areFollowUpsEnabled;
497
+ }
498
+
484
499
  get isAnswerCollapsed() {
485
500
  // Answer is considered collapsed only if it exceeds the maximum height and was not expanded.
486
501
  return this._exceedsMaximumHeight && !this.isExpanded;
@@ -501,6 +516,14 @@ export default class QuanticGeneratedAnswer extends LightningElement {
501
516
  return `generated-answer__answer ${collapsedStateClass}`;
502
517
  }
503
518
 
519
+ get contentSectionClass() {
520
+ const baseClass =
521
+ 'generated-answer__content slds-p-top_medium slds-p-horizontal_large';
522
+ return this.areFollowUpsEnabled
523
+ ? `${baseClass} generated-answer__content--scrollable`
524
+ : baseClass;
525
+ }
526
+
504
527
  get hasRetryableError() {
505
528
  return !this?.searchStatusState?.hasError && this.state?.error?.isRetryable;
506
529
  }
@@ -9,6 +9,11 @@
9
9
  word-wrap: break-word;
10
10
  }
11
11
 
12
+ .generated-answer__content--scrollable {
13
+ overflow-y: auto;
14
+ height: var(--quantic-generated-answer-content-fixed-height, 50vh);
15
+ }
16
+
12
17
  .generated-answer__footer {
13
18
  gap: 1rem;
14
19
  }
@@ -27,13 +27,15 @@
27
27
  <template lwc:if={isVisible}>
28
28
  <section
29
29
  data-testid="generated-answer__body"
30
- class="generated-answer__content slds-p-top_medium slds-p-horizontal_large"
30
+ class={contentSectionClass}
31
31
  >
32
32
  <div
33
33
  data-testid="generated-answer__answer"
34
34
  class={generatedAnswerClass}
35
35
  >
36
36
  <c-quantic-generated-answer-content
37
+ engine-id={engineId}
38
+ answer-id={answerId}
37
39
  answer-content-format={answerContentFormat}
38
40
  answer={answer}
39
41
  is-streaming={isStreaming}
@@ -0,0 +1,341 @@
1
+ // @ts-ignore
2
+ import QuanticGeneratedAnswerBody from 'c/quanticGeneratedAnswerBody';
3
+ import {buildCreateTestComponent, cleanup, flushPromises} from 'c/testUtils';
4
+
5
+ jest.mock('c/quanticHeadlessLoader');
6
+ jest.mock('c/quanticUtils', () => ({
7
+ loadMarkdownDependencies: jest.fn(() => Promise.resolve()),
8
+ transformMarkdownToHtml: jest.fn((answer) => answer),
9
+ LinkUtils: {
10
+ bindAnalyticsToLink: jest.fn(() => jest.fn()),
11
+ },
12
+ generateTextFragmentUrl: jest.fn((uri) => uri),
13
+ }));
14
+ jest.mock(
15
+ '@salesforce/label/c.quantic_CouldNotGenerateAnAnswer',
16
+ () => ({default: 'Could not generate an answer.'}),
17
+ {virtual: true}
18
+ );
19
+ jest.mock(
20
+ '@salesforce/label/c.quantic_GenericErrorTitle',
21
+ () => ({
22
+ default:
23
+ 'Something went wrong while generating the answer. Please try again later.',
24
+ }),
25
+ {virtual: true}
26
+ );
27
+ jest.mock(
28
+ '@salesforce/label/c.quantic_GeneratedAnswerErrorTurnLimitReached',
29
+ () => ({
30
+ default:
31
+ 'Conversation turn limit reached. Please start a new conversation.',
32
+ }),
33
+ {virtual: true}
34
+ );
35
+ jest.mock(
36
+ '@salesforce/label/c.quantic_ThisAnswerWasHelpful',
37
+ () => ({default: 'This answer was helpful'}),
38
+ {virtual: true}
39
+ );
40
+ jest.mock(
41
+ '@salesforce/label/c.quantic_ThisAnswerWasNotHelpful',
42
+ () => ({default: 'This answer was not helpful'}),
43
+ {virtual: true}
44
+ );
45
+
46
+ const defaultOptions = {
47
+ engineId: 'example-engine',
48
+ generatedAnswer: {
49
+ answerId: 'answer-1',
50
+ question: 'What is the meaning of life?',
51
+ answer: 'Example generated answer',
52
+ answerContentFormat: 'text/plain',
53
+ citations: [],
54
+ isStreaming: false,
55
+ liked: false,
56
+ disliked: false,
57
+ cannotAnswer: false,
58
+ },
59
+ };
60
+
61
+ const selectors = {
62
+ body: '[data-testid="generated-answer-body"]',
63
+ actions: '[data-testid="generated-answer-body__actions"]',
64
+ citations: 'c-quantic-source-citations',
65
+ feedback: 'c-quantic-feedback',
66
+ copy: 'c-quantic-generated-answer-copy-to-clipboard',
67
+ error: '[data-testid="generated-answer-body__error"]',
68
+ noAnswer: '[data-testid="generated-answer-body__no-answer-message"]',
69
+ content: 'c-quantic-generated-answer-content',
70
+ };
71
+
72
+ const createTestComponent = buildCreateTestComponent(
73
+ QuanticGeneratedAnswerBody,
74
+ 'c-quantic-generated-answer-body',
75
+ defaultOptions
76
+ );
77
+
78
+ describe('c-quantic-generated-answer-body', () => {
79
+ afterEach(() => {
80
+ cleanup();
81
+ });
82
+
83
+ it('should pass answer and answerContentFormat to the QuanticGeneratedAnswerContent component', async () => {
84
+ const element = createTestComponent();
85
+ await flushPromises();
86
+
87
+ const content = element.shadowRoot.querySelector(selectors.content);
88
+
89
+ expect(content.answer).toBe(defaultOptions.generatedAnswer.answer);
90
+ expect(content.answerContentFormat).toBe(
91
+ defaultOptions.generatedAnswer.answerContentFormat
92
+ );
93
+ });
94
+
95
+ it('should send the answerId within event details when dispatching the #quantic__generatedanswerlike event', async () => {
96
+ const element = createTestComponent();
97
+ const handler = jest.fn();
98
+ element.addEventListener('quantic__generatedanswerlike', handler);
99
+ await flushPromises();
100
+
101
+ const feedback = element.shadowRoot.querySelector(selectors.feedback);
102
+ feedback.dispatchEvent(new CustomEvent('quantic__like'));
103
+
104
+ expect(handler).toHaveBeenCalledTimes(1);
105
+ expect(handler.mock.calls[0][0].detail).toEqual({answerId: 'answer-1'});
106
+ });
107
+
108
+ it('should send the answerId within event details when dispatching the #quantic__generatedanswerdislike event', async () => {
109
+ const element = createTestComponent();
110
+ const handler = jest.fn();
111
+ element.addEventListener('quantic__generatedanswerdislike', handler);
112
+ await flushPromises();
113
+
114
+ const feedback = element.shadowRoot.querySelector(selectors.feedback);
115
+ feedback.dispatchEvent(new CustomEvent('quantic__dislike'));
116
+
117
+ expect(handler).toHaveBeenCalledTimes(1);
118
+ expect(handler.mock.calls[0][0].detail).toEqual({answerId: 'answer-1'});
119
+ });
120
+
121
+ it('should pass the answerId to the copy-to-clipboard component', async () => {
122
+ const element = createTestComponent();
123
+ await flushPromises();
124
+
125
+ const copy = element.shadowRoot.querySelector(selectors.copy);
126
+
127
+ expect(copy.answerId).toBe('answer-1');
128
+ });
129
+
130
+ it('should send the answerId within event details when dispatching the #quantic__citationhover event', async () => {
131
+ const element = createTestComponent({
132
+ ...defaultOptions,
133
+ generatedAnswer: {
134
+ ...defaultOptions.generatedAnswer,
135
+ // @ts-ignore
136
+ citations: [{id: 'citation-1', title: 'Citation'}],
137
+ },
138
+ });
139
+ const handler = jest.fn();
140
+ element.addEventListener('quantic__citationhover', handler);
141
+ await flushPromises();
142
+
143
+ const citations = element.shadowRoot.querySelector(
144
+ 'c-quantic-source-citations'
145
+ );
146
+ citations.citationHoverHandler('citation-1', 1200);
147
+
148
+ expect(handler).toHaveBeenCalledTimes(1);
149
+ expect(handler.mock.calls[0][0].detail).toEqual({
150
+ answerId: 'answer-1',
151
+ citationId: 'citation-1',
152
+ citationHoverTimeMs: 1200,
153
+ });
154
+ });
155
+
156
+ describe('when an answer has not been generated', () => {
157
+ it('should render the no-answer message when the answer cannot be generated', async () => {
158
+ const element = createTestComponent({
159
+ ...defaultOptions,
160
+ generatedAnswer: {
161
+ ...defaultOptions.generatedAnswer,
162
+ cannotAnswer: true,
163
+ },
164
+ });
165
+ await flushPromises();
166
+
167
+ const noAnswer = element.shadowRoot.querySelector(selectors.noAnswer);
168
+
169
+ expect(noAnswer).not.toBeNull();
170
+ });
171
+ });
172
+
173
+ describe('when an error occurs', () => {
174
+ it('should render the generic error message when a non-retryable error occurs', async () => {
175
+ const element = createTestComponent({
176
+ ...defaultOptions,
177
+ generatedAnswer: {
178
+ ...defaultOptions.generatedAnswer,
179
+ answer: '',
180
+ // @ts-ignore
181
+ error: {code: 500},
182
+ },
183
+ });
184
+ await flushPromises();
185
+
186
+ const error = element.shadowRoot.querySelector(selectors.error);
187
+
188
+ expect(error).not.toBeNull();
189
+ expect(error.textContent).toContain(
190
+ 'Something went wrong while generating the answer. Please try again later.'
191
+ );
192
+ });
193
+
194
+ it('should render the turn limit reached error message when the SSE turn limit is exceeded', async () => {
195
+ const element = createTestComponent({
196
+ ...defaultOptions,
197
+ generatedAnswer: {
198
+ ...defaultOptions.generatedAnswer,
199
+ answer: '',
200
+ // @ts-ignore
201
+ error: {
202
+ code: 429,
203
+ isSseTurnLimitReachedError: () => true,
204
+ },
205
+ },
206
+ });
207
+ await flushPromises();
208
+
209
+ const error = element.shadowRoot.querySelector(selectors.error);
210
+
211
+ expect(error).not.toBeNull();
212
+ expect(error.textContent).toContain(
213
+ 'Conversation turn limit reached. Please start a new conversation.'
214
+ );
215
+ });
216
+
217
+ it('should render the error message even when the answer is not empty', async () => {
218
+ const element = createTestComponent({
219
+ ...defaultOptions,
220
+ generatedAnswer: {
221
+ ...defaultOptions.generatedAnswer,
222
+ answer: 'Partial answer content',
223
+ // @ts-ignore
224
+ error: {code: 500},
225
+ },
226
+ });
227
+ await flushPromises();
228
+
229
+ const error = element.shadowRoot.querySelector(selectors.error);
230
+ const content = element.shadowRoot.querySelector(selectors.content);
231
+
232
+ expect(error).not.toBeNull();
233
+ expect(content).toBeNull();
234
+ });
235
+ });
236
+
237
+ describe('rendering of actions', () => {
238
+ it('should display actions when the answer is not empty and done streaming', async () => {
239
+ const element = createTestComponent();
240
+ await flushPromises();
241
+
242
+ const actions = element.shadowRoot.querySelector(selectors.actions);
243
+
244
+ expect(actions).not.toBeNull();
245
+ });
246
+
247
+ it('should not display actions while the answer is streaming', async () => {
248
+ const element = createTestComponent({
249
+ ...defaultOptions,
250
+ generatedAnswer: {
251
+ ...defaultOptions.generatedAnswer,
252
+ isStreaming: true,
253
+ },
254
+ });
255
+ await flushPromises();
256
+
257
+ const actions = element.shadowRoot.querySelector(selectors.actions);
258
+
259
+ expect(actions).toBeNull();
260
+ });
261
+
262
+ it('should not display actions when there is no answer', async () => {
263
+ const element = createTestComponent({
264
+ ...defaultOptions,
265
+ generatedAnswer: {
266
+ ...defaultOptions.generatedAnswer,
267
+ answer: '',
268
+ },
269
+ });
270
+ await flushPromises();
271
+
272
+ const actions = element.shadowRoot.querySelector(selectors.actions);
273
+
274
+ expect(actions).toBeNull();
275
+ });
276
+ });
277
+
278
+ describe('rendering of citations', () => {
279
+ it('should display citations when citations are not empty', async () => {
280
+ const element = createTestComponent({
281
+ ...defaultOptions,
282
+ generatedAnswer: {
283
+ ...defaultOptions.generatedAnswer,
284
+ // @ts-ignore
285
+ citations: [{id: 'citation-1', title: 'Citation'}],
286
+ },
287
+ });
288
+ await flushPromises();
289
+
290
+ const citations = element.shadowRoot.querySelector(selectors.citations);
291
+
292
+ expect(citations).not.toBeNull();
293
+ });
294
+
295
+ it('should not display citations when citations are empty', async () => {
296
+ const element = createTestComponent();
297
+ await flushPromises();
298
+
299
+ const citations = element.shadowRoot.querySelector(selectors.citations);
300
+
301
+ expect(citations).toBeNull();
302
+ });
303
+
304
+ it('should not display citations while the answer is streaming', async () => {
305
+ const element = createTestComponent({
306
+ ...defaultOptions,
307
+ generatedAnswer: {
308
+ ...defaultOptions.generatedAnswer,
309
+ // @ts-ignore
310
+ citations: [{id: 'citation-1', title: 'Citation'}],
311
+ isStreaming: true,
312
+ },
313
+ });
314
+ await flushPromises();
315
+
316
+ const citations = element.shadowRoot.querySelector(selectors.citations);
317
+
318
+ expect(citations).toBeNull();
319
+ });
320
+ });
321
+
322
+ describe('when generatedAnswer is null', () => {
323
+ it('should render without errors', async () => {
324
+ const element = createTestComponent({
325
+ ...defaultOptions,
326
+ generatedAnswer: null,
327
+ });
328
+ await flushPromises();
329
+
330
+ const error = element.shadowRoot.querySelector(selectors.error);
331
+ const noAnswer = element.shadowRoot.querySelector(selectors.noAnswer);
332
+ const actions = element.shadowRoot.querySelector(selectors.actions);
333
+ const citations = element.shadowRoot.querySelector(selectors.citations);
334
+
335
+ expect(error).toBeNull();
336
+ expect(noAnswer).toBeNull();
337
+ expect(actions).toBeNull();
338
+ expect(citations).toBeNull();
339
+ });
340
+ });
341
+ });