@coveo/quantic 3.41.0 → 3.42.1

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 (37) hide show
  1. package/docs/out/quantic-docs.json +9 -0
  2. package/force-app/main/default/labels/CustomLabels.labels-meta.xml +14 -0
  3. package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +271 -22
  4. package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +92 -31
  5. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/conversation.css +7 -0
  6. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/conversation.html +63 -0
  7. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +0 -11
  8. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +2 -2
  9. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/__tests__/quanticGeneratedAnswerBody.test.js +39 -1
  10. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js +8 -4
  11. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.html +30 -24
  12. package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/__tests__/quanticGeneratedAnswerCopyToClipboard.test.js +5 -1
  13. package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/quanticGeneratedAnswerCopyToClipboard.js +9 -2
  14. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/__tests__/quanticGeneratedAnswerFollowUpInput.test.js +109 -55
  15. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.css +59 -11
  16. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.html +14 -13
  17. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.js +40 -2
  18. package/force-app/main/default/lwc/quanticSourceCitations/__tests__/quanticSourceCitations.test.js +22 -2
  19. package/force-app/main/default/lwc/quanticSourceCitations/quanticSourceCitations.js +7 -0
  20. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +15 -15
  21. package/force-app/main/default/staticresources/coveoheadless/definitions/commerce.index.d.ts +1 -1
  22. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/core/sub-controller/headless-sub-controller.d.ts +3 -1
  23. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/recent-queries-list/headless-recent-queries-list.d.ts +13 -2
  24. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/search/did-you-mean/headless-did-you-mean.d.ts +10 -1
  25. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/generated-answer/headless-generated-answer.d.ts +2 -2
  26. package/force-app/main/default/staticresources/coveoheadless/definitions/features/follow-up-answers/follow-up-answers-actions.d.ts +32 -1
  27. package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-actions.d.ts +26 -1
  28. package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-state.d.ts +19 -0
  29. package/force-app/main/default/staticresources/coveoheadless/definitions/index.d.ts +1 -1
  30. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr/commerce/controllers/did-you-mean/headless-did-you-mean.ssr.d.ts +4 -3
  31. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr-commerce-next.index.d.ts +1 -1
  32. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr-commerce.index.d.ts +1 -1
  33. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr-next/commerce/controllers/did-you-mean/headless-did-you-mean.ssr.d.ts +4 -3
  34. package/force-app/main/default/staticresources/coveoheadless/headless.js +18 -18
  35. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +17 -17
  36. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +15 -15
  37. package/package.json +2 -2
@@ -1031,6 +1031,15 @@
1031
1031
  "defaultValue": "{undefined}",
1032
1032
  "type": "string"
1033
1033
  },
1034
+ {
1035
+ "name": "agentId",
1036
+ "attribute": "agent-id",
1037
+ "access": "@api",
1038
+ "description": "<p>The unique identifier of the agent to use to generate the answer.</p>",
1039
+ "required": false,
1040
+ "defaultValue": "{undefined}",
1041
+ "type": "string"
1042
+ },
1034
1043
  {
1035
1044
  "name": "maxCollapsedHeight",
1036
1045
  "attribute": "max-collapsed-height",
@@ -1715,4 +1715,18 @@
1715
1715
  <protected>false</protected>
1716
1716
  <shortDescription>Answer generated</shortDescription>
1717
1717
  </labels>
1718
+ <labels>
1719
+ <fullName>quantic_Like</fullName>
1720
+ <value>Like</value>
1721
+ <language>en_US</language>
1722
+ <protected>false</protected>
1723
+ <shortDescription>Like</shortDescription>
1724
+ </labels>
1725
+ <labels>
1726
+ <fullName>quantic_Dislike</fullName>
1727
+ <value>Dislike</value>
1728
+ <language>en_US</language>
1729
+ <protected>false</protected>
1730
+ <shortDescription>Dislike</shortDescription>
1731
+ </labels>
1718
1732
  </CustomLabels>
@@ -2,6 +2,7 @@
2
2
  // @ts-ignore
3
3
  import {createElement} from 'lwc';
4
4
  import QuanticGeneratedAnswer from 'c/quanticGeneratedAnswer';
5
+ import FeedbackModalQna from 'c/quanticFeedbackModalQna';
5
6
  import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
6
7
 
7
8
  let mockAnswerHeight = 250;
@@ -25,6 +26,12 @@ const exampleCitations = [
25
26
  const exampleEngineId = 'example engine id';
26
27
  const exampleAnswerId = 'example answer id';
27
28
  jest.mock('c/quanticHeadlessLoader');
29
+ jest.mock('c/quanticFeedbackModalQna', () => ({
30
+ __esModule: true,
31
+ default: {
32
+ open: jest.fn(() => Promise.resolve()),
33
+ },
34
+ }));
28
35
  jest.mock('c/quanticUtils', () => ({
29
36
  AriaLiveRegion: jest.fn(() => ({
30
37
  dispatchMessage: jest.fn(),
@@ -95,6 +102,9 @@ const selectors = {
95
102
  generatedAnswerNoAnswerMessage:
96
103
  '[data-testid="generated-answer__no-answer-message"]',
97
104
  generatedAnswerCitations: 'c-quantic-source-citations',
105
+ generatedAnswerThread: 'c-quantic-generated-answer-thread',
106
+ generatedAnswerFollowUpInput: 'c-quantic-generated-answer-follow-up-input',
107
+ generatedAnswerFeedback: 'c-quantic-feedback',
98
108
  loadingSpinner: 'lightning-spinner',
99
109
  };
100
110
 
@@ -103,7 +113,10 @@ const initialSearchStatusState = {
103
113
  };
104
114
  let searchStatusState = initialSearchStatusState;
105
115
 
106
- const initialGeneratedAnswerState = {isVisible: true, cannotAnswer: false};
116
+ const initialGeneratedAnswerState = {
117
+ isVisible: true,
118
+ cannotAnswer: false,
119
+ };
107
120
  let generatedAnswerState = initialGeneratedAnswerState;
108
121
 
109
122
  const functionsMocks = {
@@ -111,6 +124,10 @@ const functionsMocks = {
111
124
  state: generatedAnswerState,
112
125
  subscribe: functionsMocks.generatedAnswerStateSubscriber,
113
126
  retry: functionsMocks.retry,
127
+ askFollowUp: functionsMocks.askFollowUp,
128
+ like: functionsMocks.like,
129
+ dislike: functionsMocks.dislike,
130
+ closeFeedbackModal: functionsMocks.closeFeedbackModal,
114
131
  })),
115
132
  buildSearchStatus: jest.fn(() => ({
116
133
  state: searchStatusState,
@@ -127,6 +144,10 @@ const functionsMocks = {
127
144
  generatedAnswerStateUnsubscriber: jest.fn(),
128
145
  searchStatusStateUnsubscriber: jest.fn(),
129
146
  retry: jest.fn(),
147
+ askFollowUp: jest.fn(),
148
+ like: jest.fn(),
149
+ dislike: jest.fn(),
150
+ closeFeedbackModal: jest.fn(),
130
151
  };
131
152
 
132
153
  /**
@@ -145,8 +166,10 @@ function flushPromises() {
145
166
  return new Promise((resolve) => setTimeout(resolve, 0));
146
167
  }
147
168
 
169
+ const exampleQuery = 'example query';
148
170
  const exampleEngine = {
149
171
  id: 'dummy engine',
172
+ state: {query: {q: exampleQuery}},
150
173
  };
151
174
  let isInitialized = false;
152
175
  const defaultAnswerHeight = 250;
@@ -339,6 +362,36 @@ describe('c-quantic-generated-answer', () => {
339
362
  );
340
363
  });
341
364
  });
365
+
366
+ describe('when the agent id property is passed to the component', () => {
367
+ it('should initialize the controller with the correct agent id value', async () => {
368
+ const exampleAgentId = 'example agent id';
369
+ createTestComponent({
370
+ ...defaultOptions,
371
+ agentId: exampleAgentId,
372
+ });
373
+ await flushPromises();
374
+
375
+ expect(functionsMocks.buildGeneratedAnswer).toHaveBeenCalledTimes(1);
376
+ expect(functionsMocks.buildGeneratedAnswer).toHaveBeenCalledWith(
377
+ exampleEngine,
378
+ expect.objectContaining({agentId: exampleAgentId})
379
+ );
380
+ });
381
+ });
382
+
383
+ describe('when the agent id property is not passed to the component', () => {
384
+ it('should initialize the controller without the agent id value', async () => {
385
+ createTestComponent();
386
+ await flushPromises();
387
+
388
+ expect(functionsMocks.buildGeneratedAnswer).toHaveBeenCalledTimes(1);
389
+ expect(functionsMocks.buildGeneratedAnswer).toHaveBeenCalledWith(
390
+ exampleEngine,
391
+ expect.not.objectContaining({agentId: expect.any(String)})
392
+ );
393
+ });
394
+ });
342
395
  });
343
396
 
344
397
  describe('the rendering of the generated answer', () => {
@@ -792,25 +845,119 @@ describe('c-quantic-generated-answer', () => {
792
845
  });
793
846
 
794
847
  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;
848
+ const exampleAgentId = 'example agent id';
849
+ const exampleFollowUpAnswers = [
850
+ {
851
+ answer: 'follow-up answer',
852
+ answerId: 'answer-id-2',
853
+ question: 'follow-up question',
854
+ },
855
+ ];
856
+ const followUpOptions = {...defaultOptions, agentId: exampleAgentId};
857
+
858
+ beforeEach(() => {
859
+ generatedAnswerState = {
860
+ ...initialGeneratedAnswerState,
861
+ answer: exampleAnswer,
862
+ answerId: 'answer-id-1',
863
+ followUpAnswers: {
864
+ isEnabled: true,
865
+ followUpAnswers: exampleFollowUpAnswers,
866
+ },
867
+ };
868
+ });
869
+
870
+ it('should render the conversation thread and follow-up input instead of the single answer body', async () => {
871
+ const element = createTestComponent(followUpOptions);
872
+ await flushPromises();
873
+
874
+ const thread = element.shadowRoot.querySelector(
875
+ selectors.generatedAnswerThread
876
+ );
877
+ const followUpInput = element.shadowRoot.querySelector(
878
+ selectors.generatedAnswerFollowUpInput
879
+ );
880
+ const singleAnswerBody = element.shadowRoot.querySelector(
881
+ selectors.generatedAnswerBody
882
+ );
883
+
884
+ expect(thread).not.toBeNull();
885
+ expect(followUpInput).not.toBeNull();
886
+ expect(singleAnswerBody).toBeNull();
887
+ });
888
+
889
+ it('should display the scrollable container', async () => {
890
+ const element = createTestComponent(followUpOptions);
891
+ await flushPromises();
892
+
893
+ const scrollableContainer = element.shadowRoot.querySelector(
894
+ '.generated-answer__content--scrollable'
895
+ );
896
+
897
+ expect(scrollableContainer).not.toBeNull();
898
+ });
899
+
900
+ it('should pass the engine, citation anchoring and the combined initial and follow-up answers to the thread', async () => {
798
901
  const element = createTestComponent({
799
- ...defaultOptions,
800
- collapsible: true,
902
+ ...followUpOptions,
903
+ disableCitationAnchoring: true,
801
904
  });
802
905
  await flushPromises();
803
906
 
804
- const generatedAnswerBody = element.shadowRoot.querySelector(
805
- selectors.generatedAnswerBody
907
+ const thread = element.shadowRoot.querySelector(
908
+ selectors.generatedAnswerThread
806
909
  );
807
910
 
808
- expect(generatedAnswerBody).not.toBeNull();
809
- expect(
810
- generatedAnswerBody.classList.contains(
811
- 'generated-answer__content--scrollable'
812
- )
813
- ).toBe(true);
911
+ expect(thread.engineId).toBe(exampleEngineId);
912
+ expect(thread.disableCitationAnchoring).toBe(true);
913
+ expect(thread.generatedAnswers).toEqual([
914
+ expect.objectContaining({
915
+ answer: exampleAnswer,
916
+ question: exampleQuery,
917
+ }),
918
+ ...exampleFollowUpAnswers,
919
+ ]);
920
+ });
921
+
922
+ it('should ask a follow-up question when the follow-up input emits a submit event', async () => {
923
+ const element = createTestComponent(followUpOptions);
924
+ await flushPromises();
925
+
926
+ const followUpInput = element.shadowRoot.querySelector(
927
+ selectors.generatedAnswerFollowUpInput
928
+ );
929
+ const exampleFollowUpQuestion = 'example follow-up question';
930
+ followUpInput.dispatchEvent(
931
+ new CustomEvent('quantic__submitfollowup', {
932
+ detail: {value: exampleFollowUpQuestion},
933
+ })
934
+ );
935
+
936
+ expect(functionsMocks.askFollowUp).toHaveBeenCalledTimes(1);
937
+ expect(functionsMocks.askFollowUp).toHaveBeenCalledWith(
938
+ exampleFollowUpQuestion
939
+ );
940
+ });
941
+
942
+ it('should disable the follow-up submit button while an answer is being generated', async () => {
943
+ generatedAnswerState = {...generatedAnswerState, isStreaming: true};
944
+ const element = createTestComponent(followUpOptions);
945
+ await flushPromises();
946
+
947
+ const followUpInput = element.shadowRoot.querySelector(
948
+ selectors.generatedAnswerFollowUpInput
949
+ );
950
+
951
+ expect(followUpInput.submitButtonDisabled).toBe(true);
952
+ });
953
+
954
+ it('should ignore the collapsible feature and not render the single answer collapse toggle', async () => {
955
+ mockAnswerHeight = defaultAnswerHeight + 100;
956
+ const element = createTestComponent({
957
+ ...followUpOptions,
958
+ collapsible: true,
959
+ });
960
+ await flushPromises();
814
961
 
815
962
  const generatedAnswerCollapseToggle =
816
963
  element.shadowRoot.querySelector(
@@ -822,20 +969,19 @@ describe('c-quantic-generated-answer', () => {
822
969
  });
823
970
 
824
971
  describe('when follow-ups are not enabled', () => {
825
- it('should not render the content section with the scrollable class', async () => {
972
+ it('should render the single answer body instead of the conversation thread', async () => {
826
973
  const element = createTestComponent();
827
974
  await flushPromises();
828
975
 
829
- const generatedAnswerBody = element.shadowRoot.querySelector(
976
+ const singleAnswerBody = element.shadowRoot.querySelector(
830
977
  selectors.generatedAnswerBody
831
978
  );
979
+ const thread = element.shadowRoot.querySelector(
980
+ selectors.generatedAnswerThread
981
+ );
832
982
 
833
- expect(generatedAnswerBody).not.toBeNull();
834
- expect(
835
- generatedAnswerBody.classList.contains(
836
- 'generated-answer__content--scrollable'
837
- )
838
- ).toBe(false);
983
+ expect(singleAnswerBody).not.toBeNull();
984
+ expect(thread).toBeNull();
839
985
  });
840
986
  });
841
987
  });
@@ -1238,4 +1384,107 @@ describe('c-quantic-generated-answer', () => {
1238
1384
  });
1239
1385
  });
1240
1386
  });
1387
+
1388
+ describe('the generated answer feedback', () => {
1389
+ const exampleAnswer = 'answer generated successfully';
1390
+ const exampleAgentId = 'example agent id';
1391
+
1392
+ describe.each([
1393
+ {
1394
+ action: 'like',
1395
+ feedbackEvent: 'quantic__like',
1396
+ threadEvent: 'quantic__generatedanswerlike',
1397
+ controllerMethod: 'like',
1398
+ },
1399
+ {
1400
+ action: 'dislike',
1401
+ feedbackEvent: 'quantic__dislike',
1402
+ threadEvent: 'quantic__generatedanswerdislike',
1403
+ controllerMethod: 'dislike',
1404
+ },
1405
+ ])(
1406
+ 'when the user clicks the $action button',
1407
+ ({feedbackEvent, threadEvent, controllerMethod}) => {
1408
+ describe('when follow-ups are not enabled', () => {
1409
+ beforeEach(() => {
1410
+ generatedAnswerState = {
1411
+ ...initialGeneratedAnswerState,
1412
+ isStreaming: false,
1413
+ answer: exampleAnswer,
1414
+ answerContentFormat: 'text/markdown',
1415
+ answerId: exampleAnswerId,
1416
+ citations: exampleCitations,
1417
+ };
1418
+ mockSuccessfulHeadlessInitialization();
1419
+ prepareHeadlessState();
1420
+ });
1421
+
1422
+ afterAll(() => {
1423
+ generatedAnswerState = initialGeneratedAnswerState;
1424
+ });
1425
+
1426
+ it('should call the controller and open the feedback modal', async () => {
1427
+ const element = createTestComponent();
1428
+ await flushPromises();
1429
+
1430
+ const feedback = element.shadowRoot.querySelector(
1431
+ selectors.generatedAnswerFeedback
1432
+ );
1433
+ feedback.dispatchEvent(
1434
+ new CustomEvent(feedbackEvent, {
1435
+ detail: {answerId: exampleAnswerId},
1436
+ })
1437
+ );
1438
+ await flushPromises();
1439
+
1440
+ expect(functionsMocks[controllerMethod]).toHaveBeenCalledTimes(1);
1441
+ expect(functionsMocks[controllerMethod]).toHaveBeenCalledWith(
1442
+ exampleAnswerId
1443
+ );
1444
+ expect(FeedbackModalQna.open).toHaveBeenCalledTimes(1);
1445
+ });
1446
+ });
1447
+
1448
+ describe('when follow-ups are enabled', () => {
1449
+ beforeEach(() => {
1450
+ generatedAnswerState = {
1451
+ ...initialGeneratedAnswerState,
1452
+ answer: exampleAnswer,
1453
+ followUpAnswers: {isEnabled: true, followUpAnswers: []},
1454
+ };
1455
+ mockSuccessfulHeadlessInitialization();
1456
+ prepareHeadlessState();
1457
+ });
1458
+
1459
+ afterAll(() => {
1460
+ generatedAnswerState = initialGeneratedAnswerState;
1461
+ });
1462
+
1463
+ it('should call the controller without opening the feedback modal', async () => {
1464
+ const element = createTestComponent({
1465
+ ...defaultOptions,
1466
+ agentId: exampleAgentId,
1467
+ });
1468
+ await flushPromises();
1469
+
1470
+ const thread = element.shadowRoot.querySelector(
1471
+ selectors.generatedAnswerThread
1472
+ );
1473
+ thread.dispatchEvent(
1474
+ new CustomEvent(threadEvent, {
1475
+ detail: {answerId: exampleAnswerId},
1476
+ })
1477
+ );
1478
+ await flushPromises();
1479
+
1480
+ expect(functionsMocks[controllerMethod]).toHaveBeenCalledTimes(1);
1481
+ expect(functionsMocks[controllerMethod]).toHaveBeenCalledWith(
1482
+ exampleAnswerId
1483
+ );
1484
+ expect(FeedbackModalQna.open).not.toHaveBeenCalled();
1485
+ });
1486
+ });
1487
+ }
1488
+ );
1489
+ });
1241
1490
  });
@@ -35,9 +35,12 @@ import generatedAnswerTemplate from './templates/generatedAnswer.html';
35
35
  import loadingTemplate from './templates/loading.html';
36
36
  // @ts-ignore
37
37
  import retryPromptTemplate from './templates/retryPrompt.html';
38
+ // @ts-ignore
39
+ import conversationTemplate from './templates/conversation.html';
38
40
 
39
41
  /** @typedef {import("coveo").SearchEngine} SearchEngine */
40
42
  /** @typedef {import("coveo").GeneratedAnswer} GeneratedAnswer */
43
+ /** @typedef {import("coveo").GeneratedAnswerWithFollowUps} GeneratedAnswerWithFollowUps */
41
44
  /** @typedef {import("coveo").GeneratedAnswerState} GeneratedAnswerState */
42
45
  /** @typedef {import("coveo").GeneratedAnswerCitation} GeneratedAnswerCitation */
43
46
  /** @typedef { 'neutral' | 'liked' | 'disliked'} FeedbackState */
@@ -100,6 +103,13 @@ export default class QuanticGeneratedAnswer extends LightningElement {
100
103
  * @default {undefined}
101
104
  */
102
105
  @api answerConfigurationId;
106
+ /**
107
+ * The unique identifier of the agent to use to generate the answer.
108
+ * @api
109
+ * @type {string}
110
+ * @default {undefined}
111
+ */
112
+ @api agentId;
103
113
  /**
104
114
  * The maximum height (in px units) of the generated answer when it is collapsed.
105
115
  * @api
@@ -155,7 +165,7 @@ export default class QuanticGeneratedAnswer extends LightningElement {
155
165
  noGeneratedAnswer,
156
166
  };
157
167
 
158
- /** @type {GeneratedAnswer} */
168
+ /** @type { GeneratedAnswer | GeneratedAnswerWithFollowUps} */
159
169
  generatedAnswer;
160
170
  /** @type {GeneratedAnswerState} */
161
171
  state;
@@ -172,13 +182,7 @@ export default class QuanticGeneratedAnswer extends LightningElement {
172
182
  /** @type {boolean} */
173
183
  hasInitializationError = false;
174
184
  /** @type {boolean} */
175
- _areFollowUpsEnabled = false;
176
- /** @type {boolean} */
177
185
  _exceedsMaximumHeight = false;
178
- /** @type {boolean} */
179
- _liked = false;
180
- /** @type {boolean} */
181
- _disliked = false;
182
186
  /** @type {number} */
183
187
  _maxCollapsedHeight = DEFAULT_COLLAPSED_HEIGHT;
184
188
 
@@ -207,6 +211,7 @@ export default class QuanticGeneratedAnswer extends LightningElement {
207
211
  * @param {SearchEngine} engine
208
212
  */
209
213
  initialize = (engine) => {
214
+ this.engine = engine;
210
215
  this.ariaLiveMessage = AriaLiveRegion('GeneratedAnswer', this);
211
216
  this.headless = getHeadlessBundle(this.engineId);
212
217
  this.generatedAnswer = this.buildHeadlessGeneratedAnswerController(engine);
@@ -238,6 +243,9 @@ export default class QuanticGeneratedAnswer extends LightningElement {
238
243
  ...(this.answerConfigurationId && {
239
244
  answerConfigurationId: this.answerConfigurationId,
240
245
  }),
246
+ ...(this.agentId?.trim() && {
247
+ agentId: this.agentId.trim(),
248
+ }),
241
249
  fieldsToIncludeInCitations: this.citationFields,
242
250
  });
243
251
  }
@@ -316,7 +324,7 @@ export default class QuanticGeneratedAnswer extends LightningElement {
316
324
  * @param {string} id
317
325
  * @param {number} citationHoverTimeMs
318
326
  */
319
- handleCitationHover = (id, citationHoverTimeMs) => {
327
+ logCitationHover = (id, citationHoverTimeMs) => {
320
328
  this.generatedAnswer.logCitationHover(id, citationHoverTimeMs);
321
329
  };
322
330
 
@@ -326,12 +334,8 @@ export default class QuanticGeneratedAnswer extends LightningElement {
326
334
  */
327
335
  async handleLike(event) {
328
336
  event.stopPropagation();
329
- if (!this._liked) {
330
- this._liked = true;
331
- this._disliked = false;
332
- this.generatedAnswer.like?.();
333
- }
334
- if (!this.feedbackSubmitted) {
337
+ this.generatedAnswer.like?.(event.detail?.answerId);
338
+ if (!this.feedbackSubmitted && !this.areFollowUpsEnabled) {
335
339
  // @ts-ignore
336
340
  await FeedbackModalQna.open({
337
341
  size: 'small',
@@ -344,18 +348,28 @@ export default class QuanticGeneratedAnswer extends LightningElement {
344
348
  }
345
349
  }
346
350
 
351
+ /**
352
+ * handles hovering over a citation.
353
+ * @param {CustomEvent} event
354
+ */
355
+ handleCitationHover(event) {
356
+ event.stopPropagation();
357
+ const {citationId, citationHoverTimeMs, answerId} = event.detail;
358
+ this.generatedAnswer.logCitationHover(
359
+ citationId,
360
+ citationHoverTimeMs,
361
+ answerId
362
+ );
363
+ }
364
+
347
365
  /**
348
366
  * handles disliking the generated answer.
349
367
  * @param {CustomEvent} event
350
368
  */
351
369
  async handleDislike(event) {
352
370
  event.stopPropagation();
353
- if (!this._disliked) {
354
- this._disliked = true;
355
- this._liked = false;
356
- this.generatedAnswer.dislike?.();
357
- }
358
- if (!this.feedbackSubmitted) {
371
+ this.generatedAnswer.dislike?.(event.detail?.answerId);
372
+ if (!this.feedbackSubmitted && !this.areFollowUpsEnabled) {
359
373
  // @ts-ignore
360
374
  await FeedbackModalQna.open({
361
375
  size: 'small',
@@ -381,9 +395,13 @@ export default class QuanticGeneratedAnswer extends LightningElement {
381
395
  this.generatedAnswer.retry();
382
396
  }
383
397
 
398
+ /**
399
+ * handles copying the generated answer to the clipboard.
400
+ * @param {CustomEvent} event
401
+ */
384
402
  handleGeneratedAnswerCopyToClipboard = (event) => {
385
403
  event.stopPropagation();
386
- this.generatedAnswer.logCopyToClipboard();
404
+ this.generatedAnswer.logCopyToClipboard(event.detail?.answerId);
387
405
  };
388
406
 
389
407
  handleGeneratedAnswerToggle = (event) => {
@@ -487,9 +505,37 @@ export default class QuanticGeneratedAnswer extends LightningElement {
487
505
  return this.state.isVisible;
488
506
  }
489
507
 
508
+ /**
509
+ * Returns the generated answer controller narrowed to `GeneratedAnswerWithFollowUps`, or `null` if follow-ups are not available.
510
+ * @returns {GeneratedAnswerWithFollowUps | null}
511
+ */
512
+ get generateAnswerWithFollowUps() {
513
+ if (
514
+ !this.agentId ||
515
+ !this.generatedAnswer ||
516
+ !('askFollowUp' in this.generatedAnswer)
517
+ ) {
518
+ return null;
519
+ }
520
+ return /** @type {GeneratedAnswerWithFollowUps} */ (this.generatedAnswer);
521
+ }
522
+
490
523
  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;
524
+ return (
525
+ this.generateAnswerWithFollowUps?.state.followUpAnswers?.isEnabled ===
526
+ true
527
+ );
528
+ }
529
+
530
+ get allGeneratedAnswers() {
531
+ const initialAnswer = {
532
+ ...this.state,
533
+ question: this.engine?.state.query?.q ?? '',
534
+ };
535
+ const followUpAnswers =
536
+ this.generateAnswerWithFollowUps?.state.followUpAnswers
537
+ ?.followUpAnswers ?? [];
538
+ return [initialAnswer, ...followUpAnswers];
493
539
  }
494
540
 
495
541
  get isCollapsibleEnabled() {
@@ -516,14 +562,6 @@ export default class QuanticGeneratedAnswer extends LightningElement {
516
562
  return `generated-answer__answer ${collapsedStateClass}`;
517
563
  }
518
564
 
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
-
527
565
  get hasRetryableError() {
528
566
  return !this?.searchStatusState?.hasError && this.state?.error?.isRetryable;
529
567
  }
@@ -618,6 +656,17 @@ export default class QuanticGeneratedAnswer extends LightningElement {
618
656
  return this.state?.isLoading;
619
657
  }
620
658
 
659
+ get isAnswerGenerationOngoing() {
660
+ const initialAnswerPending = this.isStreaming || this.isLoading;
661
+ const followUpAnswers =
662
+ this.generateAnswerWithFollowUps?.state?.followUpAnswers
663
+ ?.followUpAnswers ?? [];
664
+ return (
665
+ initialAnswerPending ||
666
+ followUpAnswers.some((answer) => answer.isStreaming || answer.isLoading)
667
+ );
668
+ }
669
+
621
670
  get isManualAnswerGeneration() {
622
671
  return this.state?.answerGenerationMode === 'manual';
623
672
  }
@@ -629,6 +678,15 @@ export default class QuanticGeneratedAnswer extends LightningElement {
629
678
  this.hasInitializationError = true;
630
679
  }
631
680
 
681
+ /**
682
+ * Handles the submission of a follow-up question from the `QuanticGeneratedAnswerFollowUpInput` component.
683
+ * @param {CustomEvent} event - The custom event containing the follow-up question details.
684
+ * @returns {void}
685
+ */
686
+ handleFollowUpSubmit = (event) => {
687
+ this.generateAnswerWithFollowUps?.askFollowUp(event.detail.value);
688
+ };
689
+
632
690
  render() {
633
691
  if (this.hasInitializationError) {
634
692
  return errorTemplate;
@@ -643,6 +701,9 @@ export default class QuanticGeneratedAnswer extends LightningElement {
643
701
  if (this.hasRetryableError) {
644
702
  return retryPromptTemplate;
645
703
  }
704
+ if (this.areFollowUpsEnabled) {
705
+ return conversationTemplate;
706
+ }
646
707
  return generatedAnswerTemplate;
647
708
  }
648
709
  }
@@ -0,0 +1,7 @@
1
+ @import '../quanticGeneratedAnswer.css';
2
+
3
+ .generated-answer__content--scrollable {
4
+ overflow-y: auto;
5
+ height: var(--quantic-generated-answer-content-fixed-height, 50vh);
6
+ overflow-x: hidden;
7
+ }