@coveo/quantic 3.42.1 → 3.43.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 (32) hide show
  1. package/force-app/main/default/labels/CustomLabels.labels-meta.xml +21 -0
  2. package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +145 -60
  3. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/agentData.ts +248 -0
  4. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/fixture-agent.ts +77 -0
  5. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/fixture.ts +74 -65
  6. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/pageObject.ts +157 -15
  7. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/quanticGeneratedAnswer.e2e.ts +1 -5
  8. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/quanticGeneratedAnswerAgent.e2e.ts +233 -0
  9. package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +12 -108
  10. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/conversation.html +1 -1
  11. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +8 -47
  12. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/__tests__/quanticGeneratedAnswerContent.test.js +27 -0
  13. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js +8 -1
  14. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/__tests__/quanticGeneratedAnswerFollowUpInput.test.js +242 -10
  15. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.css +2 -2
  16. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.html +16 -1
  17. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.js +47 -4
  18. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/quanticGeneratedAnswerStreamOfThought.test.js +95 -41
  19. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/stepsUtils.test.js +118 -0
  20. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js +21 -35
  21. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/stepsUtils.js +66 -0
  22. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +15 -15
  23. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/product-listing/facets/headless-product-listing-facet-options.d.ts +4 -39
  24. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/search/facets/headless-search-facet-options.d.ts +4 -39
  25. package/force-app/main/default/staticresources/coveoheadless/definitions/features/commerce/facets/facet-set/facet-set-selector.d.ts +3 -9
  26. package/force-app/main/default/staticresources/coveoheadless/definitions/features/facets/facet-search-set/category/category-facet-search-state-selector.d.ts +1 -27
  27. package/force-app/main/default/staticresources/coveoheadless/definitions/features/facets/facet-search-set/specific/specific-facet-search-state-selector.d.ts +1 -27
  28. package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
  29. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +16 -16
  30. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
  31. package/force-app/main/default/staticresources/dompurify/purify.min.js +2 -2
  32. package/package.json +7 -6
@@ -44,70 +44,77 @@ type QuanticGeneratedAnswerE2EInsightFixtures =
44
44
 
45
45
  export const exampleQuery = 'test';
46
46
 
47
- export const testSearch = quanticBase.extend<QuanticGeneratedAnswerE2ESearchFixtures>({
48
- pageUrl,
49
- genQaData,
50
- options: {},
51
- analyticsMode: AnalyticsModeEnum.legacy,
52
- withFacets: false,
53
- search: async ({page}, use) => {
54
- await use(new SearchObject(page, searchRequestRegex));
55
- },
56
- baseFacet: async ({page}, use) => {
57
- await use(new BaseFacetObject(page, searchRequestRegex));
58
- },
59
- generatedAnswer: async (
60
- {
61
- page,
62
- options,
63
- configuration,
64
- search,
65
- genQaData: data,
66
- analytics,
67
- withFacets,
47
+ export const testSearch =
48
+ quanticBase.extend<QuanticGeneratedAnswerE2ESearchFixtures>({
49
+ pageUrl,
50
+ genQaData,
51
+ options: {},
52
+ analyticsMode: AnalyticsModeEnum.legacy,
53
+ withFacets: false,
54
+ search: async ({page}, use) => {
55
+ await use(new SearchObject(page, searchRequestRegex));
68
56
  },
69
- use
70
- ) => {
71
- const generatedAnswerObject = new GeneratedAnswerObject(
72
- page,
73
- data.streamId,
74
- analytics,
75
- !!options.answerConfigurationId,
76
- rgaGenerateRequestRegex,
77
- withFacets,
78
- );
79
-
80
- const streamingUrl = options.answerConfigurationId
81
- ? ANSWER_API_URL
82
- : `${MACHINE_LEARNING_API_URL}/${data.streamId}`;
83
- await generatedAnswerObject.mockStreamResponse(
84
- streamingUrl,
85
- data.streams,
86
- data.streamId
87
- );
88
-
89
- await page.goto(pageUrl);
90
- if (withFacets) {
91
- await generatedAnswerObject.clickAddFacetsButton();
92
- }
93
- await configuration.configure(options);
94
- await search.waitForSearchResponse();
57
+ baseFacet: async ({page}, use) => {
58
+ await use(new BaseFacetObject(page, searchRequestRegex));
59
+ },
60
+ generatedAnswer: async (
61
+ {
62
+ page,
63
+ options,
64
+ configuration,
65
+ search,
66
+ genQaData: data,
67
+ analytics,
68
+ withFacets,
69
+ },
70
+ use
71
+ ) => {
72
+ const generatedAnswerObject = new GeneratedAnswerObject(
73
+ page,
74
+ data.streamId,
75
+ analytics,
76
+ {
77
+ answerApiEnabled: !!options.answerConfigurationId,
78
+ headAnswerRequestRegex: rgaGenerateRequestRegex,
79
+ withFacets,
80
+ }
81
+ );
82
+
83
+ const streamingUrl = options.answerConfigurationId
84
+ ? ANSWER_API_URL
85
+ : `${MACHINE_LEARNING_API_URL}/${data.streamId}`;
86
+ await generatedAnswerObject.mockStreamResponse(
87
+ streamingUrl,
88
+ data.streams,
89
+ data.streamId
90
+ );
91
+
92
+ await search.mockSearchWithBaseResponse();
93
+ await page.goto(pageUrl);
94
+ if (withFacets) {
95
+ await generatedAnswerObject.clickAddFacetsButton();
96
+ }
97
+ await configuration.configure(options);
98
+ await search.waitForSearchResponse();
95
99
 
96
100
  generatedAnswerObject.streamEndAnalyticRequestPromise =
97
101
  generatedAnswerObject.waitForStreamEndAnalytics();
102
+
98
103
  if (options.answerConfigurationId) {
99
104
  generatedAnswerObject.generateRequestPromise =
100
- generatedAnswerObject.waitForGenerateRequest();
105
+ generatedAnswerObject.waitForHeadAnswerRequest();
101
106
  }
102
107
 
103
- await search.fillSearchInput(exampleQuery);
104
- await search.mockSearchWithGenerativeQuestionAnsweringId(data.streamId);
105
- await search.performSearch();
106
- await search.waitForSearchResponse();
108
+ await search.mockSearchWithGenerativeQuestionAnsweringId(data.streamId);
109
+ await search.fillSearchInput(exampleQuery);
110
+ await Promise.all([
111
+ search.waitForSearchResponse(),
112
+ search.performSearch(),
113
+ ]);
107
114
 
108
- await use(generatedAnswerObject);
109
- },
110
- });
115
+ await use(generatedAnswerObject);
116
+ },
117
+ });
111
118
 
112
119
  export const testInsight =
113
120
  quanticBase.extend<QuanticGeneratedAnswerE2EInsightFixtures>({
@@ -142,9 +149,11 @@ export const testInsight =
142
149
  page,
143
150
  data.streamId,
144
151
  analytics,
145
- !!options.answerConfigurationId,
146
- insightRgaGenerateRequestRegex,
147
- withFacets
152
+ {
153
+ answerApiEnabled: !!options.answerConfigurationId,
154
+ headAnswerRequestRegex: insightRgaGenerateRequestRegex,
155
+ withFacets,
156
+ }
148
157
  );
149
158
 
150
159
  const streamingUrl = options.answerConfigurationId
@@ -156,27 +165,27 @@ export const testInsight =
156
165
  data.streamId
157
166
  );
158
167
 
168
+ await search.mockSearchWithBaseResponse();
159
169
  await page.goto(pageUrl);
160
170
  if (withFacets) {
161
171
  await generatedAnswerObject.clickAddFacetsButton();
162
172
  }
163
173
  configuration.configure({...options, useCase: useCaseEnum.insight});
164
-
165
174
  await insightSetup.waitForInsightInterfaceInitialization();
166
- await search.performSearch();
167
- await search.waitForSearchResponse();
168
175
 
169
- await search.mockSearchWithGenerativeQuestionAnsweringId(data.streamId);
170
176
  generatedAnswerObject.streamEndAnalyticRequestPromise =
171
177
  generatedAnswerObject.waitForStreamEndAnalytics();
172
178
  if (options.answerConfigurationId) {
173
179
  generatedAnswerObject.generateRequestPromise =
174
- generatedAnswerObject.waitForGenerateRequest();
180
+ generatedAnswerObject.waitForHeadAnswerRequest();
175
181
  }
176
182
 
183
+ await search.mockSearchWithGenerativeQuestionAnsweringId(data.streamId);
177
184
  await search.fillSearchInput(exampleQuery);
178
- await search.performSearch();
179
- await search.waitForSearchResponse();
185
+ await Promise.all([
186
+ search.waitForSearchResponse(),
187
+ search.performSearch(),
188
+ ]);
180
189
 
181
190
  await use(generatedAnswerObject);
182
191
  },
@@ -5,6 +5,7 @@ import {
5
5
  } from '../../../../../../playwright/utils/analyticsMode';
6
6
  import {AnalyticsObject} from '../../../../../../playwright/page-object/analytics';
7
7
  import {isRgaEvaluationRequest} from '../../../../../../playwright/utils/requests';
8
+ import type {AgentMessage} from './agentData';
8
9
 
9
10
  const minimumCitationTooltipDisplayDurationMs = 1500;
10
11
  const removeUnknownFields = (object: Record<string, unknown>) => {
@@ -24,32 +25,42 @@ const facetElementsSelectors = {
24
25
  },
25
26
  };
26
27
 
28
+ export interface GeneratedAnswerObjectConfig {
29
+ answerApiEnabled?: boolean;
30
+ headAnswerRequestRegex?: RegExp;
31
+ agentFollowUpRequestRegex?: RegExp;
32
+ withFacets?: boolean;
33
+ }
34
+
27
35
  export class GeneratedAnswerObject {
28
36
  private analyticsMode: AnalyticsMode;
37
+ private answerApiEnabled: boolean;
38
+ private headAnswerRequestRegex?: RegExp;
39
+ private agentFollowUpRequestRegex?: RegExp;
40
+ private withFacets: boolean;
29
41
 
30
42
  constructor(
31
43
  private page: Page,
32
44
  private streamId: string,
33
45
  private analytics: AnalyticsObject,
34
- private answerApiEnabled: boolean,
35
- private generateRequestRegex: RegExp,
36
- private withFacets: boolean
46
+ config: GeneratedAnswerObjectConfig = {}
37
47
  ) {
38
48
  this.page = page;
39
49
  this.streamId = streamId;
40
50
  this.analytics = analytics;
41
51
  this.analyticsMode = this.analytics.analyticsMode;
42
- this.answerApiEnabled = answerApiEnabled;
43
- this.generateRequestRegex = generateRequestRegex;
44
- this.withFacets = withFacets;
52
+ this.answerApiEnabled = config.answerApiEnabled ?? false;
53
+ this.headAnswerRequestRegex = config.headAnswerRequestRegex;
54
+ this.agentFollowUpRequestRegex = config.agentFollowUpRequestRegex;
55
+ this.withFacets = config.withFacets ?? false;
45
56
  }
46
57
 
47
58
  get likeButton(): Locator {
48
- return this.page.getByRole('button', {name: 'This answer was helpful'});
59
+ return this.page.getByRole('button', {name: 'Like', exact: true});
49
60
  }
50
61
 
51
62
  get dislikeButton(): Locator {
52
- return this.page.getByRole('button', {name: 'This answer was not helpful'});
63
+ return this.page.getByRole('button', {name: 'Dislike', exact: true});
53
64
  }
54
65
 
55
66
  get copyToClipboardButton(): Locator {
@@ -118,7 +129,7 @@ export class GeneratedAnswerObject {
118
129
 
119
130
  get citationLink(): Locator {
120
131
  return this.page
121
- .getByTestId('generated-answer__citations')
132
+ .getByTestId('generated-answer-body__citations')
122
133
  .locator('.citation__link');
123
134
  }
124
135
 
@@ -126,9 +137,25 @@ export class GeneratedAnswerObject {
126
137
  return this.page.getByTestId('generated-answer__answer-toggle');
127
138
  }
128
139
 
140
+ get showPreviousAnswersButton(): Locator {
141
+ return this.page.getByTestId('show-previous-button');
142
+ }
143
+
144
+ get threadItems(): Locator {
145
+ return this.page.locator('c-quantic-thread-item');
146
+ }
147
+
148
+ get threadItemTitleButton(): Locator {
149
+ return this.threadItems.getByTestId('thread-item-title-button');
150
+ }
151
+
152
+ get generatedAnswerBody(): Locator {
153
+ return this.page.locator('c-quantic-generated-answer-body');
154
+ }
155
+
129
156
  async hoverOverCitation(index: number): Promise<void> {
130
- // waiting 500ms to allow the component to render completely, cause any re-rendering abort the hover action.
131
- await this.page.waitForTimeout(500);
157
+ // waiting 1000ms to allow the component to render completely, cause any re-rendering abort the hover action.
158
+ await this.page.waitForTimeout(1000);
132
159
  await this.citationLink.nth(index).hover();
133
160
  await this.page.waitForTimeout(minimumCitationTooltipDisplayDurationMs);
134
161
  await this.page.mouse.move(0, 0);
@@ -166,10 +193,18 @@ export class GeneratedAnswerObject {
166
193
  await this.likeButton.click();
167
194
  }
168
195
 
196
+ async clickNthLikeButton(answerIndex: number): Promise<void> {
197
+ await this.likeButton.nth(answerIndex).click();
198
+ }
199
+
169
200
  async clickDislikeButton(): Promise<void> {
170
201
  await this.dislikeButton.click();
171
202
  }
172
203
 
204
+ async clickNthDislikeButton(answerIndex: number): Promise<void> {
205
+ await this.dislikeButton.nth(answerIndex).click();
206
+ }
207
+
173
208
  async clickCopyToClipboardButton(): Promise<void> {
174
209
  await this.copyToClipboardButton.click();
175
210
  }
@@ -178,8 +213,26 @@ export class GeneratedAnswerObject {
178
213
  await this.toggleButton.click();
179
214
  }
180
215
 
181
- async waitForGenerateRequest(): Promise<Request> {
182
- return this.page.waitForRequest(this.generateRequestRegex);
216
+ async clickShowPreviousAnswersButton(): Promise<void> {
217
+ await this.showPreviousAnswersButton.click();
218
+ }
219
+
220
+ async clickNthThreadItem(index: number): Promise<void> {
221
+ await this.threadItemTitleButton.nth(index).click();
222
+ }
223
+
224
+ async waitForHeadAnswerRequest(): Promise<Request> {
225
+ if (!this.headAnswerRequestRegex) {
226
+ throw new Error('headAnswerRequestRegex is not configured');
227
+ }
228
+ return this.page.waitForRequest(this.headAnswerRequestRegex);
229
+ }
230
+
231
+ async waitForAgentFollowUpRequest(): Promise<Request> {
232
+ if (!this.agentFollowUpRequestRegex) {
233
+ throw new Error('agentFollowUpRequestRegex is not configured');
234
+ }
235
+ return this.page.waitForRequest(this.agentFollowUpRequestRegex);
183
236
  }
184
237
 
185
238
  async waitForStreamEndAnalytics(): Promise<Request | boolean> {
@@ -200,6 +253,12 @@ export class GeneratedAnswerObject {
200
253
  }
201
254
 
202
255
  async waitForLikeGeneratedAnswerAnalytics(): Promise<Request> {
256
+ return this.waitForLikeGeneratedAnswerAnalyticsForId(this.streamId);
257
+ }
258
+
259
+ async waitForLikeGeneratedAnswerAnalyticsForId(
260
+ answerId: string
261
+ ): Promise<Request> {
203
262
  if (this.analyticsMode === AnalyticsModeEnum.legacy) {
204
263
  return this.analytics.waitForCustomUaAnalytics(
205
264
  {
@@ -207,7 +266,7 @@ export class GeneratedAnswerObject {
207
266
  eventValue: 'likeGeneratedAnswer',
208
267
  },
209
268
  (event) =>
210
- event?.customData?.generativeQuestionAnsweringId === this.streamId
269
+ event?.customData?.generativeQuestionAnsweringId === answerId
211
270
  );
212
271
  }
213
272
  return this.analytics.waitForEventProtocolAnalytics(
@@ -217,6 +276,12 @@ export class GeneratedAnswerObject {
217
276
  }
218
277
 
219
278
  async waitForDislikeGeneratedAnswerAnalytics(): Promise<Request> {
279
+ return this.waitForDislikeGeneratedAnswerAnalyticsForId(this.streamId);
280
+ }
281
+
282
+ async waitForDislikeGeneratedAnswerAnalyticsForId(
283
+ answerId: string
284
+ ): Promise<Request> {
220
285
  if (this.analyticsMode === AnalyticsModeEnum.legacy) {
221
286
  return this.analytics.waitForCustomUaAnalytics(
222
287
  {
@@ -224,7 +289,7 @@ export class GeneratedAnswerObject {
224
289
  eventValue: 'dislikeGeneratedAnswer',
225
290
  },
226
291
  (event) =>
227
- event?.customData?.generativeQuestionAnsweringId === this.streamId
292
+ event?.customData?.generativeQuestionAnsweringId === answerId
228
293
  );
229
294
  }
230
295
  return this.analytics.waitForEventProtocolAnalytics(
@@ -446,6 +511,83 @@ export class GeneratedAnswerObject {
446
511
  });
447
512
  }
448
513
 
514
+ async mockAgentAnswerResponse(
515
+ body: Array<AgentMessage>
516
+ ) {
517
+ await this.page.route('**/agents/*/answer', (route) => {
518
+ let bodyText = '';
519
+ body.forEach((data) => {
520
+ bodyText += `data:${JSON.stringify(data)} \n\n`;
521
+ });
522
+ route.fulfill({
523
+ status: 200,
524
+ body: bodyText,
525
+ headers: {'content-type': 'text/event-stream'},
526
+ });
527
+ });
528
+ }
529
+
530
+ async mockAgentFollowUpResponse(
531
+ bodies: Array<Array<AgentMessage>>
532
+ ) {
533
+ let callCount = 0;
534
+ await this.page.route('**/agents/*/follow-up', (route) => {
535
+ // After all provided response bodies are used, keep returning the last one
536
+ // so any additional intercepted follow-up requests still receive a mocked stream.
537
+ const streams = bodies[Math.min(callCount, bodies.length - 1)];
538
+ callCount++;
539
+
540
+ let bodyText = '';
541
+ streams.forEach((data) => {
542
+ bodyText += `data:${JSON.stringify(data)} \n\n`;
543
+ });
544
+ route.fulfill({
545
+ status: 200,
546
+ body: bodyText,
547
+ headers: {'content-type': 'text/event-stream'},
548
+ });
549
+ });
550
+ }
551
+
552
+ get followUpInput(): Locator {
553
+ return this.page.locator(
554
+ 'c-quantic-generated-answer-follow-up-input textarea'
555
+ );
556
+ }
557
+
558
+ get followUpSubmitButton(): Locator {
559
+ return this.page
560
+ .locator('c-quantic-generated-answer-follow-up-input')
561
+ .getByRole('button', {name: 'Submit follow-up'});
562
+ }
563
+
564
+ async typeFollowUpQuestion(question: string): Promise<void> {
565
+ await this.followUpInput.fill(question);
566
+ }
567
+
568
+ async submitFollowUp(): Promise<void> {
569
+ await this.followUpSubmitButton.click();
570
+ }
571
+
572
+ async waitForFollowUpStreamEndAnalytics(
573
+ followUpAnswerId: string
574
+ ): Promise<Request | boolean> {
575
+ if (this.analyticsMode === AnalyticsModeEnum.legacy) {
576
+ return this.analytics.waitForCustomUaAnalytics(
577
+ {
578
+ eventType: 'generatedAnswer',
579
+ eventValue: 'generatedAnswerStreamEnd',
580
+ },
581
+ (event) =>
582
+ event?.customData?.generativeQuestionAnsweringId === followUpAnswerId
583
+ );
584
+ }
585
+ return this.analytics.waitForEventProtocolAnalytics(
586
+ 'Rga.AnswerReceived',
587
+ (event) => event.answerGenerated === true
588
+ );
589
+ }
590
+
449
591
  streamEndAnalyticRequestPromise!: Promise<boolean | Request>;
450
592
  generateRequestPromise!: Promise<Request>;
451
593
  }
@@ -14,10 +14,6 @@ useCaseTestCases.forEach((useCase) => {
14
14
  let test = fixtures[useCase.value];
15
15
 
16
16
  test.describe(`quantic generated answer ${useCase.label}`, () => {
17
- test.use({
18
- genQaData,
19
- });
20
-
21
17
  analyticsModeTest.forEach((analytics) => {
22
18
  test.describe(analytics.label, () => {
23
19
  test.use({
@@ -309,7 +305,7 @@ useCaseTestCases.forEach((useCase) => {
309
305
  await generatedAnswer.streamEndAnalyticRequestPromise;
310
306
 
311
307
  const generateRequestPromise =
312
- generatedAnswer.waitForGenerateRequest();
308
+ generatedAnswer.waitForHeadAnswerRequest();
313
309
 
314
310
  const streamEndAnalyticRequestPromise =
315
311
  generatedAnswer.waitForStreamEndAnalytics();