@coveo/quantic 3.40.0 → 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 (34) hide show
  1. package/force-app/main/default/labels/CustomLabels.labels-meta.xml +77 -0
  2. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/__tests__/quanticGeneratedAnswerBody.test.js +341 -0
  3. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js +148 -0
  4. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js-meta.xml +5 -0
  5. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.css +3 -0
  6. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.html +53 -0
  7. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/cannotAnswer.html +7 -0
  8. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/error.html +7 -0
  9. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/quanticGeneratedAnswerStreamOfThought.test.js +348 -0
  10. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.css +17 -0
  11. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js +163 -0
  12. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js-meta.xml +5 -0
  13. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.css +1 -0
  14. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.html +32 -0
  15. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.css +1 -0
  16. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.html +65 -0
  17. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/__tests__/quanticGeneratedAnswerThread.test.js +285 -0
  18. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.css +47 -0
  19. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.html +67 -0
  20. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js +93 -0
  21. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js-meta.xml +5 -0
  22. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.css +0 -4
  23. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.html +1 -1
  24. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +6 -6
  25. package/force-app/main/default/staticresources/coveoheadless/definitions/api/commerce/common/pagination.d.ts +1 -1
  26. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-generated-answer.d.ts +1 -0
  27. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-interactive-citation.d.ts +3 -3
  28. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/generated-answer/interactive-citation-analytics-client.d.ts +3 -0
  29. package/force-app/main/default/staticresources/coveoheadless/definitions/features/analytics/analytics-utils.d.ts +3 -2
  30. package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-analytics-actions.d.ts +2 -0
  31. package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
  32. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +7 -7
  33. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
  34. package/package.json +2 -2
@@ -0,0 +1,285 @@
1
+ // @ts-ignore
2
+ import QuanticGeneratedAnswerThread from '../quanticGeneratedAnswerThread';
3
+ import {buildCreateTestComponent, cleanup, flushPromises} from 'c/testUtils';
4
+
5
+ jest.mock('c/quanticUtils', () => ({
6
+ I18nUtils: {
7
+ format: jest.fn((label, count) => `Show ${count} previous questions`),
8
+ },
9
+ }));
10
+ jest.mock('c/quanticHeadlessLoader');
11
+
12
+ jest.mock(
13
+ '@salesforce/label/c.quantic_ShowPreviousQuestions_plural',
14
+ () => ({default: 'Show {{0}} previous questions'}),
15
+ {virtual: true}
16
+ );
17
+
18
+ const selectors = {
19
+ generatedAnswerBody: 'c-quantic-generated-answer-body',
20
+ threadItem: 'c-quantic-thread-item',
21
+ showPreviousButton: '[data-testid="show-previous-button"]',
22
+ showPreviousDot: '[data-testid="show-previous-dot"]',
23
+ };
24
+
25
+ function buildGeneratedAnswer(overrides = {}) {
26
+ return {
27
+ answerId: `answer-${Math.random()}`,
28
+ question: 'What is Coveo?',
29
+ answer: 'Coveo is a search platform.',
30
+ citations: [],
31
+ isStreaming: false,
32
+ liked: false,
33
+ disliked: false,
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ const createTestComponent = buildCreateTestComponent(
39
+ QuanticGeneratedAnswerThread,
40
+ 'c-quantic-generated-answer-thread',
41
+ {
42
+ engineId: 'test-engine',
43
+ generatedAnswers: [],
44
+ }
45
+ );
46
+
47
+ describe('c-quantic-generated-answer-thread', () => {
48
+ afterEach(() => {
49
+ cleanup();
50
+ });
51
+
52
+ describe('with a single answer', () => {
53
+ it('renders a single generated answer body without thread items', async () => {
54
+ const answers = [buildGeneratedAnswer()];
55
+ const element = createTestComponent({generatedAnswers: answers});
56
+ await flushPromises();
57
+
58
+ const body = element.shadowRoot.querySelectorAll(
59
+ selectors.generatedAnswerBody
60
+ );
61
+ expect(body).toHaveLength(1);
62
+
63
+ const threadItems = element.shadowRoot.querySelectorAll(
64
+ selectors.threadItem
65
+ );
66
+ expect(threadItems).toHaveLength(0);
67
+ });
68
+
69
+ it('does not render the show previous button', async () => {
70
+ const answers = [buildGeneratedAnswer()];
71
+ const element = createTestComponent({generatedAnswers: answers});
72
+ await flushPromises();
73
+
74
+ const button = element.shadowRoot.querySelector(
75
+ selectors.showPreviousButton
76
+ );
77
+ expect(button).toBeNull();
78
+ });
79
+ });
80
+
81
+ describe('with two answers', () => {
82
+ it('renders both answers in thread items', async () => {
83
+ const answers = [
84
+ buildGeneratedAnswer({answerId: 'a1', question: 'Q1'}),
85
+ buildGeneratedAnswer({answerId: 'a2', question: 'Q2'}),
86
+ ];
87
+ const element = createTestComponent({generatedAnswers: answers});
88
+ await flushPromises();
89
+
90
+ const threadItems = element.shadowRoot.querySelectorAll(
91
+ selectors.threadItem
92
+ );
93
+ expect(threadItems).toHaveLength(2);
94
+ });
95
+
96
+ it('does not render the show previous button', async () => {
97
+ const answers = [
98
+ buildGeneratedAnswer({answerId: 'a1'}),
99
+ buildGeneratedAnswer({answerId: 'a2'}),
100
+ ];
101
+ const element = createTestComponent({generatedAnswers: answers});
102
+ await flushPromises();
103
+
104
+ const button = element.shadowRoot.querySelector(
105
+ selectors.showPreviousButton
106
+ );
107
+ expect(button).toBeNull();
108
+ });
109
+
110
+ it('sets the last thread item as non-collapsible', async () => {
111
+ const answers = [
112
+ buildGeneratedAnswer({answerId: 'a1'}),
113
+ buildGeneratedAnswer({answerId: 'a2'}),
114
+ ];
115
+ const element = createTestComponent({generatedAnswers: answers});
116
+ await flushPromises();
117
+
118
+ const threadItems = element.shadowRoot.querySelectorAll(
119
+ selectors.threadItem
120
+ );
121
+ const lastItem = threadItems[threadItems.length - 1];
122
+ expect(lastItem.disableCollapse).toBe(true);
123
+ expect(lastItem.hideLine).toBe(true);
124
+ });
125
+
126
+ it('sets the first thread item as collapsible and collapsed', async () => {
127
+ const answers = [
128
+ buildGeneratedAnswer({answerId: 'a1'}),
129
+ buildGeneratedAnswer({answerId: 'a2'}),
130
+ ];
131
+ const element = createTestComponent({generatedAnswers: answers});
132
+ await flushPromises();
133
+
134
+ const threadItems = element.shadowRoot.querySelectorAll(
135
+ selectors.threadItem
136
+ );
137
+ const firstItem = threadItems[0];
138
+ expect(firstItem.disableCollapse).toBe(false);
139
+ expect(firstItem.isExpanded).toBe(false);
140
+ expect(firstItem.hideLine).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe('with three or more answers', () => {
145
+ it('initially shows only the show previous button and the last answer', async () => {
146
+ const answers = [
147
+ buildGeneratedAnswer({answerId: 'a1', question: 'Q1'}),
148
+ buildGeneratedAnswer({answerId: 'a2', question: 'Q2'}),
149
+ buildGeneratedAnswer({answerId: 'a3', question: 'Q3'}),
150
+ ];
151
+ const element = createTestComponent({generatedAnswers: answers});
152
+ await flushPromises();
153
+
154
+ const button = element.shadowRoot.querySelector(
155
+ selectors.showPreviousButton
156
+ );
157
+ const dot = element.shadowRoot.querySelector(selectors.showPreviousDot);
158
+ expect(button).not.toBeNull();
159
+ expect(dot).not.toBeNull();
160
+
161
+ const threadItems = element.shadowRoot.querySelectorAll(
162
+ selectors.threadItem
163
+ );
164
+ expect(threadItems).toHaveLength(1);
165
+ });
166
+
167
+ it('reveals all previous answers when the show previous button is clicked', async () => {
168
+ const answers = [
169
+ buildGeneratedAnswer({answerId: 'a1', question: 'Q1'}),
170
+ buildGeneratedAnswer({answerId: 'a2', question: 'Q2'}),
171
+ buildGeneratedAnswer({answerId: 'a3', question: 'Q3'}),
172
+ ];
173
+ const element = createTestComponent({generatedAnswers: answers});
174
+ await flushPromises();
175
+
176
+ const button = element.shadowRoot.querySelector(
177
+ selectors.showPreviousButton
178
+ );
179
+ button.click();
180
+ await flushPromises();
181
+
182
+ const threadItems = element.shadowRoot.querySelectorAll(
183
+ selectors.threadItem
184
+ );
185
+ expect(threadItems).toHaveLength(3);
186
+
187
+ const showButton = element.shadowRoot.querySelector(
188
+ selectors.showPreviousButton
189
+ );
190
+ expect(showButton).toBeNull();
191
+ });
192
+
193
+ it('sets previous answers as collapsible and collapsed after expansion', async () => {
194
+ const answers = [
195
+ buildGeneratedAnswer({answerId: 'a1', question: 'Q1'}),
196
+ buildGeneratedAnswer({answerId: 'a2', question: 'Q2'}),
197
+ buildGeneratedAnswer({answerId: 'a3', question: 'Q3'}),
198
+ ];
199
+ const element = createTestComponent({generatedAnswers: answers});
200
+ await flushPromises();
201
+
202
+ element.shadowRoot.querySelector(selectors.showPreviousButton).click();
203
+ await flushPromises();
204
+
205
+ const threadItems = element.shadowRoot.querySelectorAll(
206
+ selectors.threadItem
207
+ );
208
+ const firstItem = threadItems[0];
209
+ const secondItem = threadItems[1];
210
+
211
+ expect(firstItem.disableCollapse).toBe(false);
212
+ expect(firstItem.isExpanded).toBe(false);
213
+ expect(secondItem.disableCollapse).toBe(false);
214
+ expect(secondItem.isExpanded).toBe(false);
215
+ });
216
+
217
+ it('keeps the last answer expanded and non-collapsible after expansion', async () => {
218
+ const answers = [
219
+ buildGeneratedAnswer({answerId: 'a1', question: 'Q1'}),
220
+ buildGeneratedAnswer({answerId: 'a2', question: 'Q2'}),
221
+ buildGeneratedAnswer({answerId: 'a3', question: 'Q3'}),
222
+ ];
223
+ const element = createTestComponent({generatedAnswers: answers});
224
+ await flushPromises();
225
+
226
+ element.shadowRoot.querySelector(selectors.showPreviousButton).click();
227
+ await flushPromises();
228
+
229
+ const threadItems = element.shadowRoot.querySelectorAll(
230
+ selectors.threadItem
231
+ );
232
+ const lastItem = threadItems[threadItems.length - 1];
233
+ expect(lastItem.disableCollapse).toBe(true);
234
+ expect(lastItem.hideLine).toBe(true);
235
+ });
236
+
237
+ it('re-collapses when a new follow-up answer is added', async () => {
238
+ const answers = [
239
+ buildGeneratedAnswer({answerId: 'a1', question: 'Q1'}),
240
+ buildGeneratedAnswer({answerId: 'a2', question: 'Q2'}),
241
+ buildGeneratedAnswer({answerId: 'a3', question: 'Q3'}),
242
+ ];
243
+ const element = createTestComponent({generatedAnswers: answers});
244
+ await flushPromises();
245
+
246
+ element.shadowRoot.querySelector(selectors.showPreviousButton).click();
247
+ await flushPromises();
248
+
249
+ expect(
250
+ element.shadowRoot.querySelector(selectors.showPreviousButton)
251
+ ).toBeNull();
252
+
253
+ element.generatedAnswers = [
254
+ ...answers,
255
+ buildGeneratedAnswer({answerId: 'a4', question: 'Q4'}),
256
+ ];
257
+ await flushPromises();
258
+
259
+ expect(
260
+ element.shadowRoot.querySelector(selectors.showPreviousButton)
261
+ ).not.toBeNull();
262
+ expect(
263
+ element.shadowRoot.querySelectorAll(selectors.threadItem)
264
+ ).toHaveLength(1);
265
+ });
266
+ });
267
+
268
+ describe('engine-id and citation anchoring passthrough', () => {
269
+ it('passes engineId and disableCitationAnchoring to generated answer body', async () => {
270
+ const answers = [buildGeneratedAnswer()];
271
+ const element = createTestComponent({
272
+ generatedAnswers: answers,
273
+ engineId: 'custom-engine',
274
+ disableCitationAnchoring: true,
275
+ });
276
+ await flushPromises();
277
+
278
+ const body = element.shadowRoot.querySelector(
279
+ selectors.generatedAnswerBody
280
+ );
281
+ expect(body.engineId).toBe('custom-engine');
282
+ expect(body.disableCitationAnchoring).toBe(true);
283
+ });
284
+ });
285
+ });
@@ -0,0 +1,47 @@
1
+ .generated-answer-thread__dot-column,
2
+ .generated-answer-thread__line-column {
3
+ width: 10px;
4
+ }
5
+
6
+ .generated-answer-thread__dot {
7
+ height: 8px;
8
+ width: 8px;
9
+ border-radius: 50%;
10
+ background-color: var(--lwc-colorBorder, #dddbda);
11
+ }
12
+
13
+ .generated-answer-thread__connector {
14
+ position: relative;
15
+ width: 1px;
16
+ height: 100%;
17
+ min-height: 12px;
18
+ background-color: var(--lwc-colorBorder, #dddbda);
19
+ }
20
+
21
+ .generated-answer-thread__connector::before,
22
+ .generated-answer-thread__connector::after {
23
+ content: '';
24
+ position: absolute;
25
+ left: 0;
26
+ width: 1px;
27
+ height: 8px;
28
+ background-color: var(--lwc-colorBorder, #dddbda);
29
+ }
30
+
31
+ .generated-answer-thread__connector::before {
32
+ top: -8px;
33
+ }
34
+
35
+ .generated-answer-thread__connector::after {
36
+ bottom: -8px;
37
+ }
38
+
39
+ .generated-answer-thread__show-previous-button {
40
+ width: fit-content;
41
+ border-radius: var(--slds-g-radius-border-2, 4px);
42
+ cursor: pointer;
43
+ }
44
+
45
+ .generated-answer-thread__show-previous-button:hover {
46
+ background-color: var(--lwc-colorBackgroundRowHover, #f3f2f2);
47
+ }
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <template lwc:if={isSingleAnswer}>
3
+ <c-quantic-generated-answer-body
4
+ engine-id={engineId}
5
+ generated-answer={headAnswer}
6
+ disable-citation-anchoring={disableCitationAnchoring}
7
+ ></c-quantic-generated-answer-body>
8
+ </template>
9
+
10
+ <template lwc:else>
11
+ <ul class="generated-answer-thread">
12
+ <template lwc:if={shouldCollapseOlderAnswers}>
13
+ <li class="generated-answer-thread__show-previous">
14
+ <div class="slds-grid slds-grid_vertical-align-center">
15
+ <div
16
+ class="slds-grid slds-grid_align-center slds-grid_vertical-align-center slds-shrink-none slds-m-right_x-small generated-answer-thread__dot-column"
17
+ >
18
+ <span
19
+ class="generated-answer-thread__dot"
20
+ data-testid="show-previous-dot"
21
+ ></span>
22
+ </div>
23
+ <div>
24
+ <button
25
+ type="button"
26
+ class="slds-button_reset slds-p-horizontal_x-small slds-p-vertical_xx-small generated-answer-thread__show-previous-button"
27
+ data-testid="show-previous-button"
28
+ onclick={handleShowAllClick}
29
+ >
30
+ {showPreviousQuestionsLabel}
31
+ </button>
32
+ </div>
33
+ </div>
34
+ <div class="slds-grid">
35
+ <div
36
+ class="slds-grid slds-grid_align-center slds-shrink-none generated-answer-thread__line-column slds-m-right_x-small"
37
+ >
38
+ <span class="generated-answer-thread__connector"></span>
39
+ </div>
40
+ </div>
41
+ </li>
42
+ </template>
43
+
44
+ <template for:each={previousAnswers} for:item="answer">
45
+ <c-quantic-thread-item key={answer.answerId} title={answer.question}>
46
+ <c-quantic-generated-answer-body
47
+ engine-id={engineId}
48
+ generated-answer={answer}
49
+ disable-citation-anchoring={disableCitationAnchoring}
50
+ ></c-quantic-generated-answer-body>
51
+ </c-quantic-thread-item>
52
+ </template>
53
+
54
+ <c-quantic-thread-item
55
+ title={lastAnswer.question}
56
+ hide-line
57
+ disable-collapse
58
+ >
59
+ <c-quantic-generated-answer-body
60
+ engine-id={engineId}
61
+ generated-answer={lastAnswer}
62
+ disable-citation-anchoring={disableCitationAnchoring}
63
+ ></c-quantic-generated-answer-body>
64
+ </c-quantic-thread-item>
65
+ </ul>
66
+ </template>
67
+ </template>
@@ -0,0 +1,93 @@
1
+ import {LightningElement, api} from 'lwc';
2
+ import showPreviousQuestions_plural from '@salesforce/label/c.quantic_ShowPreviousQuestions_plural';
3
+ import {I18nUtils} from 'c/quanticUtils';
4
+
5
+ /** @typedef {import("coveo").GeneratedAnswerBase} GeneratedAnswer */
6
+
7
+ const MIN_ANSWERS_TO_COLLAPSE = 2;
8
+
9
+ /**
10
+ * The `QuanticGeneratedAnswerThread` component renders a thread of generated answers, including the initial answer and follow-up answers.
11
+ * @category Internal
12
+ * @example
13
+ * <c-quantic-generated-answer-thread engine-id={engineId} generated-answers={generatedAnswers}></c-quantic-generated-answer-thread>
14
+ */
15
+ export default class QuanticGeneratedAnswerThread extends LightningElement {
16
+ /**
17
+ * The ID of the engine instance the component registers to.
18
+ * @api
19
+ * @type {string}
20
+ */
21
+ @api engineId;
22
+
23
+ /**
24
+ * The list of generated answers to render in the thread.
25
+ * @api
26
+ * @type {GeneratedAnswer[]}
27
+ */
28
+ @api
29
+ get generatedAnswers() {
30
+ return this._generatedAnswers;
31
+ }
32
+ set generatedAnswers(value) {
33
+ const previous = this._generatedAnswers;
34
+ this._generatedAnswers = value ?? [];
35
+ if (previous?.length !== this._generatedAnswers.length) {
36
+ this._allAnswersDisplayed = false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Whether to disable citation anchoring.
42
+ * @api
43
+ * @type {boolean}
44
+ * @defaultValue `false`
45
+ */
46
+ @api disableCitationAnchoring = false;
47
+
48
+ /** @type {boolean} */
49
+ _allAnswersDisplayed = false;
50
+ /** @type {GeneratedAnswer[]} */
51
+ _generatedAnswers = [];
52
+
53
+ labels = {
54
+ showPreviousQuestions_plural,
55
+ };
56
+
57
+ get isSingleAnswer() {
58
+ return this.generatedAnswers.length === 1;
59
+ }
60
+
61
+ get shouldCollapseOlderAnswers() {
62
+ return (
63
+ this.generatedAnswers.length > MIN_ANSWERS_TO_COLLAPSE &&
64
+ !this._allAnswersDisplayed
65
+ );
66
+ }
67
+
68
+ get showPreviousQuestionsLabel() {
69
+ return I18nUtils.format(
70
+ this.labels.showPreviousQuestions_plural,
71
+ this.generatedAnswers.length - 1
72
+ );
73
+ }
74
+
75
+ get previousAnswers() {
76
+ if (this.shouldCollapseOlderAnswers) {
77
+ return [];
78
+ }
79
+ return this.generatedAnswers.slice(0, -1);
80
+ }
81
+
82
+ get headAnswer() {
83
+ return this.generatedAnswers[0];
84
+ }
85
+
86
+ get lastAnswer() {
87
+ return this.generatedAnswers[this.generatedAnswers.length - 1];
88
+ }
89
+
90
+ handleShowAllClick() {
91
+ this._allAnswersDisplayed = true;
92
+ }
93
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>66.0</apiVersion>
4
+ <isExposed>false</isExposed>
5
+ </LightningComponentBundle>
@@ -7,10 +7,6 @@
7
7
  width: 10px;
8
8
  }
9
9
 
10
- .thread-item__line-column {
11
- align-items: stretch;
12
- }
13
-
14
10
  .thread-item__dot {
15
11
  height: 8px;
16
12
  width: 8px;
@@ -6,7 +6,7 @@
6
6
  >
7
7
  <span class={dotClass} data-testid="thread-item-dot"></span>
8
8
  </div>
9
- <div class="slds-col slds-has-flexi-truncate">
9
+ <div class="slds-has-flexi-truncate">
10
10
  <template lwc:if={disableCollapse}>
11
11
  <span class={titleClass} data-testid="thread-item-title-static"
12
12
  >{title}</span