@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.
- package/force-app/main/default/labels/CustomLabels.labels-meta.xml +21 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +145 -60
- package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/agentData.ts +248 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/fixture-agent.ts +77 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/fixture.ts +74 -65
- package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/pageObject.ts +157 -15
- package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/quanticGeneratedAnswer.e2e.ts +1 -5
- package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/quanticGeneratedAnswerAgent.e2e.ts +233 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +12 -108
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/conversation.html +1 -1
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +8 -47
- package/force-app/main/default/lwc/quanticGeneratedAnswerContent/__tests__/quanticGeneratedAnswerContent.test.js +27 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js +8 -1
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/__tests__/quanticGeneratedAnswerFollowUpInput.test.js +242 -10
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.css +2 -2
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.html +16 -1
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.js +47 -4
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/quanticGeneratedAnswerStreamOfThought.test.js +95 -41
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/stepsUtils.test.js +118 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js +21 -35
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/stepsUtils.js +66 -0
- package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +15 -15
- package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/product-listing/facets/headless-product-listing-facet-options.d.ts +4 -39
- package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/search/facets/headless-search-facet-options.d.ts +4 -39
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/commerce/facets/facet-set/facet-set-selector.d.ts +3 -9
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/facets/facet-search-set/category/category-facet-search-state-selector.d.ts +1 -27
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/facets/facet-search-set/specific/specific-facet-search-state-selector.d.ts +1 -27
- package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
- package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +16 -16
- package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
- package/force-app/main/default/staticresources/dompurify/purify.min.js +2 -2
- package/package.json +7 -6
|
@@ -11,8 +11,19 @@ jest.mock(
|
|
|
11
11
|
() => ({default: 'Ask follow-up'}),
|
|
12
12
|
{virtual: true}
|
|
13
13
|
);
|
|
14
|
+
jest.mock(
|
|
15
|
+
'@salesforce/label/c.quantic_FollowUpInputTooLong',
|
|
16
|
+
() => ({
|
|
17
|
+
default: 'Your question is too long. Please use {{0}} characters or fewer.',
|
|
18
|
+
}),
|
|
19
|
+
{virtual: true}
|
|
20
|
+
);
|
|
14
21
|
jest.mock('c/quanticUtils', () => ({
|
|
15
22
|
keys: {ENTER: 'Enter', ESC: 'Escape'},
|
|
23
|
+
I18nUtils: {
|
|
24
|
+
format: (label, ...args) =>
|
|
25
|
+
label.replace(/{{(\d+)}}/g, (_match, index) => args[index]),
|
|
26
|
+
},
|
|
16
27
|
}));
|
|
17
28
|
|
|
18
29
|
const selectors = {
|
|
@@ -20,9 +31,13 @@ const selectors = {
|
|
|
20
31
|
submitButton: 'lightning-button-icon',
|
|
21
32
|
container: '.follow-up-input__container',
|
|
22
33
|
expander: '.follow-up-input__expander',
|
|
34
|
+
counter: '.follow-up-input__counter',
|
|
35
|
+
validationMessage: '[role="alert"]',
|
|
23
36
|
};
|
|
24
37
|
|
|
25
38
|
const expandedClass = 'follow-up-input__expander--expanded';
|
|
39
|
+
const errorClass = 'follow-up-input__container--error';
|
|
40
|
+
const MAX_LENGTH = 300;
|
|
26
41
|
|
|
27
42
|
const createTestComponent = buildCreateTestComponent(
|
|
28
43
|
QuanticGeneratedAnswerFollowUpInput,
|
|
@@ -76,6 +91,21 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
|
|
|
76
91
|
expect(handler.mock.calls[0][0].detail.value).toBe('another question');
|
|
77
92
|
});
|
|
78
93
|
|
|
94
|
+
it('should dispatch the trimmed value when the input has leading and trailing spaces', async () => {
|
|
95
|
+
const element = createTestComponent();
|
|
96
|
+
await flushPromises();
|
|
97
|
+
|
|
98
|
+
const handler = jest.fn();
|
|
99
|
+
element.addEventListener('quantic__submitfollowup', handler);
|
|
100
|
+
|
|
101
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
102
|
+
textarea.value = ' another question ';
|
|
103
|
+
element.shadowRoot.querySelector(selectors.submitButton).click();
|
|
104
|
+
|
|
105
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(handler.mock.calls[0][0].detail.value).toBe('another question');
|
|
107
|
+
});
|
|
108
|
+
|
|
79
109
|
it('should clear the textarea after a successful submit', async () => {
|
|
80
110
|
const element = createTestComponent();
|
|
81
111
|
await flushPromises();
|
|
@@ -149,7 +179,7 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
|
|
|
149
179
|
});
|
|
150
180
|
|
|
151
181
|
describe('expandable input behaviour', () => {
|
|
152
|
-
it('should expand
|
|
182
|
+
it('should expand on focus', async () => {
|
|
153
183
|
const element = createTestComponent();
|
|
154
184
|
await flushPromises();
|
|
155
185
|
|
|
@@ -158,14 +188,10 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
|
|
|
158
188
|
await flushPromises();
|
|
159
189
|
|
|
160
190
|
const expander = element.shadowRoot.querySelector(selectors.expander);
|
|
161
|
-
const container = element.shadowRoot.querySelector(selectors.container);
|
|
162
191
|
expect(expander.classList).toContain(expandedClass);
|
|
163
|
-
expect(container.classList).toContain(
|
|
164
|
-
'follow-up-input__container--focused'
|
|
165
|
-
);
|
|
166
192
|
});
|
|
167
193
|
|
|
168
|
-
it('should collapse
|
|
194
|
+
it('should collapse on blur', async () => {
|
|
169
195
|
const element = createTestComponent();
|
|
170
196
|
await flushPromises();
|
|
171
197
|
|
|
@@ -177,11 +203,7 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
|
|
|
177
203
|
await flushPromises();
|
|
178
204
|
|
|
179
205
|
const expander = element.shadowRoot.querySelector(selectors.expander);
|
|
180
|
-
const container = element.shadowRoot.querySelector(selectors.container);
|
|
181
206
|
expect(expander.classList).not.toContain(expandedClass);
|
|
182
|
-
expect(container.classList).not.toContain(
|
|
183
|
-
'follow-up-input__container--focused'
|
|
184
|
-
);
|
|
185
207
|
});
|
|
186
208
|
|
|
187
209
|
it('should sync replica text on input', async () => {
|
|
@@ -231,4 +253,214 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
|
|
|
231
253
|
expect(textarea.blur).toHaveBeenCalled();
|
|
232
254
|
});
|
|
233
255
|
});
|
|
256
|
+
|
|
257
|
+
describe('character limit validation', () => {
|
|
258
|
+
it('should display the character counter starting at 0 / 300', async () => {
|
|
259
|
+
const element = createTestComponent();
|
|
260
|
+
await flushPromises();
|
|
261
|
+
|
|
262
|
+
const counter = element.shadowRoot.querySelector(selectors.counter);
|
|
263
|
+
expect(counter.textContent.trim()).toBe('0 / 300');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should update the character counter as the user types', async () => {
|
|
267
|
+
const element = createTestComponent();
|
|
268
|
+
await flushPromises();
|
|
269
|
+
|
|
270
|
+
const textTest = 'hello';
|
|
271
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
272
|
+
textarea.value = textTest;
|
|
273
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
274
|
+
await flushPromises();
|
|
275
|
+
|
|
276
|
+
const counter = element.shadowRoot.querySelector(selectors.counter);
|
|
277
|
+
expect(counter.textContent.trim()).toBe(`${textTest.length} / 300`);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should not count leading and trailing spaces in the character counter', async () => {
|
|
281
|
+
const element = createTestComponent();
|
|
282
|
+
await flushPromises();
|
|
283
|
+
|
|
284
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
285
|
+
textarea.value = ' hello ';
|
|
286
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
287
|
+
await flushPromises();
|
|
288
|
+
|
|
289
|
+
const counter = element.shadowRoot.querySelector(selectors.counter);
|
|
290
|
+
expect(counter.textContent.trim()).toBe('5 / 300');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should not flag the input as over the limit when trailing spaces push the raw length past the limit', async () => {
|
|
294
|
+
const element = createTestComponent();
|
|
295
|
+
await flushPromises();
|
|
296
|
+
|
|
297
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
298
|
+
textarea.value = `${'a'.repeat(MAX_LENGTH)} `;
|
|
299
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
300
|
+
await flushPromises();
|
|
301
|
+
|
|
302
|
+
const counter = element.shadowRoot.querySelector(selectors.counter);
|
|
303
|
+
expect(counter.textContent.trim()).toBe('300 / 300');
|
|
304
|
+
|
|
305
|
+
const container = element.shadowRoot.querySelector(selectors.container);
|
|
306
|
+
expect(container.classList).not.toContain(errorClass);
|
|
307
|
+
|
|
308
|
+
const message = element.shadowRoot.querySelector(
|
|
309
|
+
selectors.validationMessage
|
|
310
|
+
);
|
|
311
|
+
expect(message).toBeNull();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should show the exceeded count when over the limit', async () => {
|
|
315
|
+
const element = createTestComponent();
|
|
316
|
+
await flushPromises();
|
|
317
|
+
|
|
318
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
319
|
+
textarea.value = 'a'.repeat(MAX_LENGTH + 26);
|
|
320
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
321
|
+
await flushPromises();
|
|
322
|
+
|
|
323
|
+
const counter = element.shadowRoot.querySelector(selectors.counter);
|
|
324
|
+
expect(counter.textContent.trim()).toBe('326 / 300');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should put the input container in an error state and display a validation message when over the limit', async () => {
|
|
328
|
+
const element = createTestComponent();
|
|
329
|
+
await flushPromises();
|
|
330
|
+
|
|
331
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
332
|
+
textarea.value = 'a'.repeat(MAX_LENGTH + 1);
|
|
333
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
334
|
+
await flushPromises();
|
|
335
|
+
|
|
336
|
+
const container = element.shadowRoot.querySelector(selectors.container);
|
|
337
|
+
expect(container.classList).toContain(errorClass);
|
|
338
|
+
const message = element.shadowRoot.querySelector(
|
|
339
|
+
selectors.validationMessage
|
|
340
|
+
);
|
|
341
|
+
expect(message).not.toBeNull();
|
|
342
|
+
expect(message.textContent.trim()).toBe(
|
|
343
|
+
'Your question is too long. Please use 300 characters or fewer.'
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should not display a validation message when within the limit', async () => {
|
|
348
|
+
const element = createTestComponent();
|
|
349
|
+
await flushPromises();
|
|
350
|
+
|
|
351
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
352
|
+
textarea.value = 'a'.repeat(MAX_LENGTH);
|
|
353
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
354
|
+
await flushPromises();
|
|
355
|
+
|
|
356
|
+
const message = element.shadowRoot.querySelector(
|
|
357
|
+
selectors.validationMessage
|
|
358
|
+
);
|
|
359
|
+
expect(message).toBeNull();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should associate the textarea with the validation message via aria-describedby when over the limit', async () => {
|
|
363
|
+
const element = createTestComponent();
|
|
364
|
+
await flushPromises();
|
|
365
|
+
|
|
366
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
367
|
+
expect(textarea.getAttribute('aria-describedby')).toBeNull();
|
|
368
|
+
|
|
369
|
+
textarea.value = 'a'.repeat(MAX_LENGTH + 1);
|
|
370
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
371
|
+
await flushPromises();
|
|
372
|
+
|
|
373
|
+
const message = element.shadowRoot.querySelector(
|
|
374
|
+
selectors.validationMessage
|
|
375
|
+
);
|
|
376
|
+
expect(message.id).toBeTruthy();
|
|
377
|
+
expect(textarea.getAttribute('aria-describedby')).toBe(message.id);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should remove aria-describedby once the value is back within the limit', async () => {
|
|
381
|
+
const element = createTestComponent();
|
|
382
|
+
await flushPromises();
|
|
383
|
+
|
|
384
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
385
|
+
textarea.value = 'a'.repeat(MAX_LENGTH + 1);
|
|
386
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
387
|
+
await flushPromises();
|
|
388
|
+
|
|
389
|
+
expect(textarea.getAttribute('aria-describedby')).not.toBeNull();
|
|
390
|
+
|
|
391
|
+
textarea.value = 'a'.repeat(MAX_LENGTH);
|
|
392
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
393
|
+
await flushPromises();
|
|
394
|
+
|
|
395
|
+
expect(textarea.getAttribute('aria-describedby')).toBeNull();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should disable the submit button when over the limit', async () => {
|
|
399
|
+
const element = createTestComponent();
|
|
400
|
+
await flushPromises();
|
|
401
|
+
|
|
402
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
403
|
+
textarea.value = 'a'.repeat(MAX_LENGTH + 1);
|
|
404
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
405
|
+
await flushPromises();
|
|
406
|
+
|
|
407
|
+
const submitButton = element.shadowRoot.querySelector(
|
|
408
|
+
selectors.submitButton
|
|
409
|
+
);
|
|
410
|
+
expect(submitButton.disabled).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should not dispatch the event when over the limit', async () => {
|
|
414
|
+
const element = createTestComponent();
|
|
415
|
+
await flushPromises();
|
|
416
|
+
|
|
417
|
+
const handler = jest.fn();
|
|
418
|
+
element.addEventListener('quantic__submitfollowup', handler);
|
|
419
|
+
|
|
420
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
421
|
+
textarea.value = 'a'.repeat(MAX_LENGTH + 1);
|
|
422
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
423
|
+
await flushPromises();
|
|
424
|
+
|
|
425
|
+
textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
|
|
426
|
+
|
|
427
|
+
expect(handler).not.toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should clear the error state once the value is back within the limit', async () => {
|
|
431
|
+
const element = createTestComponent();
|
|
432
|
+
await flushPromises();
|
|
433
|
+
|
|
434
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
435
|
+
textarea.value = 'a'.repeat(MAX_LENGTH + 1);
|
|
436
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
437
|
+
await flushPromises();
|
|
438
|
+
|
|
439
|
+
let container = element.shadowRoot.querySelector(selectors.container);
|
|
440
|
+
expect(container.classList).toContain(errorClass);
|
|
441
|
+
|
|
442
|
+
textarea.value = 'a'.repeat(MAX_LENGTH);
|
|
443
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
444
|
+
await flushPromises();
|
|
445
|
+
|
|
446
|
+
container = element.shadowRoot.querySelector(selectors.container);
|
|
447
|
+
expect(container.classList).not.toContain(errorClass);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should reset the character counter after a successful submit', async () => {
|
|
451
|
+
const element = createTestComponent();
|
|
452
|
+
await flushPromises();
|
|
453
|
+
|
|
454
|
+
const textarea = element.shadowRoot.querySelector(selectors.textarea);
|
|
455
|
+
textarea.value = 'a question';
|
|
456
|
+
textarea.dispatchEvent(new CustomEvent('input'));
|
|
457
|
+
await flushPromises();
|
|
458
|
+
|
|
459
|
+
textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
|
|
460
|
+
await flushPromises();
|
|
461
|
+
|
|
462
|
+
const counter = element.shadowRoot.querySelector(selectors.counter);
|
|
463
|
+
expect(counter.textContent.trim()).toBe('0 / 300');
|
|
464
|
+
});
|
|
465
|
+
});
|
|
234
466
|
});
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
rows="1"
|
|
8
8
|
placeholder={labels.askFollowUp}
|
|
9
9
|
aria-label={labels.askFollowUp}
|
|
10
|
+
aria-invalid={textareaAriaInvalid}
|
|
11
|
+
aria-describedby={textareaAriaDescribedBy}
|
|
10
12
|
onfocus={handleFocus}
|
|
11
13
|
onblur={handleBlur}
|
|
12
14
|
oninput={handleInput}
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
></textarea>
|
|
15
17
|
</div>
|
|
16
18
|
<lightning-button-icon
|
|
17
|
-
disabled={
|
|
19
|
+
disabled={isSubmitPrevented}
|
|
18
20
|
onclick={handleSubmitFollowUp}
|
|
19
21
|
icon-name="utility:arrowup"
|
|
20
22
|
variant="brand"
|
|
@@ -27,4 +29,17 @@
|
|
|
27
29
|
<label>{labels.submitFollowUp}</label>
|
|
28
30
|
</lightning-button-icon>
|
|
29
31
|
</div>
|
|
32
|
+
<div
|
|
33
|
+
class="follow-up-input__footer slds-grid slds-p-horizontal_x-small slds-p-top_xx-small"
|
|
34
|
+
>
|
|
35
|
+
<template lwc:if={isOverCharacterLimit}>
|
|
36
|
+
<span
|
|
37
|
+
id="follow-up-validation-message"
|
|
38
|
+
class="slds-text-body_small slds-text-color_error"
|
|
39
|
+
role="alert"
|
|
40
|
+
>{validationMessage}
|
|
41
|
+
</span>
|
|
42
|
+
</template>
|
|
43
|
+
<span class={characterCounterClasses}>{characterCounterText}</span>
|
|
44
|
+
</div>
|
|
30
45
|
</template>
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import submitFollowUp from '@salesforce/label/c.quantic_SubmitFollowUp';
|
|
2
2
|
import askFollowUp from '@salesforce/label/c.quantic_AskFollowUp';
|
|
3
|
-
import
|
|
3
|
+
import followUpInputTooLong from '@salesforce/label/c.quantic_FollowUpInputTooLong';
|
|
4
|
+
import {I18nUtils, keys} from 'c/quanticUtils';
|
|
4
5
|
import {api, LightningElement} from 'lwc';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Maximum number of characters allowed in a follow-up question.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_FOLLOW_UP_QUESTION_LENGTH = 300;
|
|
11
|
+
|
|
6
12
|
/**
|
|
7
13
|
* The `QuanticGeneratedAnswerFollowUpInput` component allows users to submit follow-up questions related to a generated answer.
|
|
8
14
|
* @fires CustomEvent#quantic__submitfollowup
|
|
@@ -14,6 +20,7 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
|
|
|
14
20
|
labels = {
|
|
15
21
|
submitFollowUp,
|
|
16
22
|
askFollowUp,
|
|
23
|
+
followUpInputTooLong,
|
|
17
24
|
};
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -27,6 +34,9 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
|
|
|
27
34
|
/** @type {boolean} */
|
|
28
35
|
_focused = false;
|
|
29
36
|
|
|
37
|
+
/** @type {number} */
|
|
38
|
+
characterCount = 0;
|
|
39
|
+
|
|
30
40
|
handleKeyDown(event) {
|
|
31
41
|
// Let the browser commit IME text before handling shortcuts like Enter during composition.
|
|
32
42
|
if (event.isComposing || event.keyCode === 229) {
|
|
@@ -47,13 +57,14 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
|
|
|
47
57
|
|
|
48
58
|
handleSubmitFollowUp() {
|
|
49
59
|
if (
|
|
50
|
-
this.
|
|
60
|
+
this.isSubmitPrevented ||
|
|
51
61
|
this.refs.askFollowUpInput.value.trim() === ''
|
|
52
62
|
) {
|
|
53
63
|
return;
|
|
54
64
|
}
|
|
55
65
|
this.sendSubmitFollowUpEvent();
|
|
56
66
|
this.refs.askFollowUpInput.value = '';
|
|
67
|
+
this.characterCount = 0;
|
|
57
68
|
this.syncTextWithReplica();
|
|
58
69
|
this.collapse();
|
|
59
70
|
}
|
|
@@ -70,6 +81,7 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
|
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
handleInput() {
|
|
84
|
+
this.characterCount = this.refs.askFollowUpInput.value.trim().length;
|
|
73
85
|
this.expand();
|
|
74
86
|
this.syncTextWithReplica();
|
|
75
87
|
}
|
|
@@ -105,13 +117,44 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
|
|
|
105
117
|
bubbles: true,
|
|
106
118
|
composed: true,
|
|
107
119
|
detail: {
|
|
108
|
-
value: this.refs.askFollowUpInput.value,
|
|
120
|
+
value: this.refs.askFollowUpInput.value.trim(),
|
|
109
121
|
},
|
|
110
122
|
})
|
|
111
123
|
);
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
get inputContainerClasses() {
|
|
115
|
-
return `follow-up-input__container slds-is-relative slds-grid slds-box slds-p-around_none ${this.
|
|
127
|
+
return `follow-up-input__container slds-is-relative slds-grid slds-box slds-p-around_none ${this.isOverCharacterLimit ? 'follow-up-input__container--error' : ''}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get isOverCharacterLimit() {
|
|
131
|
+
return this.characterCount > MAX_FOLLOW_UP_QUESTION_LENGTH;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get isSubmitPrevented() {
|
|
135
|
+
return this.submitButtonDisabled || this.isOverCharacterLimit;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get textareaAriaInvalid() {
|
|
139
|
+
return this.isOverCharacterLimit ? 'true' : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get textareaAriaDescribedBy() {
|
|
143
|
+
return this.isOverCharacterLimit ? 'follow-up-validation-message' : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get characterCounterText() {
|
|
147
|
+
return `${this.characterCount} / ${MAX_FOLLOW_UP_QUESTION_LENGTH}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get characterCounterClasses() {
|
|
151
|
+
return `follow-up-input__counter slds-col_bump-left slds-text-body_small ${this.isOverCharacterLimit ? 'slds-text-color_error' : 'slds-text-color_weak'}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get validationMessage() {
|
|
155
|
+
return I18nUtils.format(
|
|
156
|
+
this.labels.followUpInputTooLong,
|
|
157
|
+
MAX_FOLLOW_UP_QUESTION_LENGTH
|
|
158
|
+
);
|
|
116
159
|
}
|
|
117
160
|
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import {resolveSteps} from '../quanticGeneratedAnswerStreamOfThought.js';
|
|
2
1
|
import {buildCreateTestComponent, cleanup, flushPromises} from 'c/testUtils';
|
|
3
2
|
import QuanticGeneratedAnswerStreamOfThought from '../quanticGeneratedAnswerStreamOfThought';
|
|
4
3
|
|
|
4
|
+
jest.mock(
|
|
5
|
+
'@salesforce/label/c.quantic_AgentGenerationStepSearchQuery',
|
|
6
|
+
() => ({default: 'Searching for {{0}}.'}),
|
|
7
|
+
{
|
|
8
|
+
virtual: true,
|
|
9
|
+
}
|
|
10
|
+
);
|
|
11
|
+
jest.mock(
|
|
12
|
+
'@salesforce/label/c.quantic_AgentGenerationStepSearchQueryCompleted',
|
|
13
|
+
() => ({default: 'Searched for {{0}}'}),
|
|
14
|
+
{
|
|
15
|
+
virtual: true,
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
5
19
|
const selectors = {
|
|
6
20
|
stepItem: '[data-testid="step-item"]',
|
|
7
21
|
spinner: '[data-testid="spinner"]',
|
|
@@ -20,46 +34,6 @@ const createTestComponent = buildCreateTestComponent(
|
|
|
20
34
|
{agentSteps: [], isStreaming: false}
|
|
21
35
|
);
|
|
22
36
|
|
|
23
|
-
describe('#resolveSteps', () => {
|
|
24
|
-
it('should return empty array for empty input', () => {
|
|
25
|
-
expect(resolveSteps([])).toEqual([]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should resolve a full sequence correctly', () => {
|
|
29
|
-
expect(
|
|
30
|
-
resolveSteps([
|
|
31
|
-
{name: 'thinking', status: 'completed', startedAt: 0},
|
|
32
|
-
{name: 'searching', status: 'completed', startedAt: 10},
|
|
33
|
-
{name: 'thinking', status: 'completed', startedAt: 20},
|
|
34
|
-
{name: 'answering', status: 'active', startedAt: 30},
|
|
35
|
-
])
|
|
36
|
-
).toEqual([
|
|
37
|
-
{name: 'thinking-before-search', status: 'completed'},
|
|
38
|
-
{name: 'searching', status: 'completed'},
|
|
39
|
-
{name: 'thinking-after-search', status: 'completed'},
|
|
40
|
-
{name: 'answering', status: 'active'},
|
|
41
|
-
]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should handle repeated searching steps', () => {
|
|
45
|
-
expect(
|
|
46
|
-
resolveSteps([
|
|
47
|
-
{name: 'thinking', status: 'completed', startedAt: 0},
|
|
48
|
-
{name: 'searching', status: 'completed', startedAt: 10},
|
|
49
|
-
{name: 'thinking', status: 'completed', startedAt: 20},
|
|
50
|
-
{name: 'searching', status: 'completed', startedAt: 30},
|
|
51
|
-
{name: 'thinking', status: 'active', startedAt: 40},
|
|
52
|
-
])
|
|
53
|
-
).toEqual([
|
|
54
|
-
{name: 'thinking-before-search', status: 'completed'},
|
|
55
|
-
{name: 'searching', status: 'completed'},
|
|
56
|
-
{name: 'thinking-after-search', status: 'completed'},
|
|
57
|
-
{name: 'searching', status: 'completed'},
|
|
58
|
-
{name: 'thinking-after-search', status: 'active'},
|
|
59
|
-
]);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
37
|
describe('quantic generated answer stream of thought component', () => {
|
|
64
38
|
afterEach(() => {
|
|
65
39
|
cleanup();
|
|
@@ -152,6 +126,86 @@ describe('quantic generated answer stream of thought component', () => {
|
|
|
152
126
|
expect(collapseButton).toBeNull();
|
|
153
127
|
expect(collapsedSummary).toBeNull();
|
|
154
128
|
});
|
|
129
|
+
|
|
130
|
+
it('should render a searching-with-query step when toolCall has a query', async () => {
|
|
131
|
+
const element = createTestComponent({
|
|
132
|
+
agentSteps: [
|
|
133
|
+
{
|
|
134
|
+
name: 'searching',
|
|
135
|
+
status: 'active',
|
|
136
|
+
startedAt: 0,
|
|
137
|
+
toolCalls: [
|
|
138
|
+
{
|
|
139
|
+
toolCallName: 'search',
|
|
140
|
+
toolCallId: 'tc-1',
|
|
141
|
+
startedAt: 0,
|
|
142
|
+
status: 'active',
|
|
143
|
+
type: 'search',
|
|
144
|
+
toolCallArgs: {q: 'how to reset password'},
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
isStreaming: true,
|
|
150
|
+
});
|
|
151
|
+
await flushPromises();
|
|
152
|
+
|
|
153
|
+
const stepItem = element.shadowRoot.querySelector(selectors.stepItem);
|
|
154
|
+
expect(stepItem.getAttribute('data-step-name')).toBe(
|
|
155
|
+
'searching-with-query'
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const stepLabel = element.shadowRoot.querySelector(selectors.stepLabel);
|
|
159
|
+
expect(stepLabel.textContent).toContain('how to reset password');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should expand multiple search toolCalls into separate step items', async () => {
|
|
163
|
+
const element = createTestComponent({
|
|
164
|
+
agentSteps: [
|
|
165
|
+
{
|
|
166
|
+
name: 'searching',
|
|
167
|
+
status: 'completed',
|
|
168
|
+
startedAt: 0,
|
|
169
|
+
toolCalls: [
|
|
170
|
+
{
|
|
171
|
+
toolCallName: 'search',
|
|
172
|
+
toolCallId: 'tc-1',
|
|
173
|
+
startedAt: 0,
|
|
174
|
+
finishedAt: 1,
|
|
175
|
+
status: 'completed',
|
|
176
|
+
type: 'search',
|
|
177
|
+
toolCallArgs: {q: 'What is Quantic'},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
toolCallName: 'search',
|
|
181
|
+
toolCallId: 'tc-2',
|
|
182
|
+
startedAt: 1,
|
|
183
|
+
finishedAt: 2,
|
|
184
|
+
status: 'completed',
|
|
185
|
+
type: 'search',
|
|
186
|
+
toolCallArgs: {q: 'What is Atomic'},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
isStreaming: true,
|
|
192
|
+
});
|
|
193
|
+
await flushPromises();
|
|
194
|
+
|
|
195
|
+
const stepItems = element.shadowRoot.querySelectorAll(selectors.stepItem);
|
|
196
|
+
expect(stepItems).toHaveLength(2);
|
|
197
|
+
stepItems.forEach((item) =>
|
|
198
|
+
expect(item.getAttribute('data-step-name')).toBe('searching-with-query')
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const stepLabelsText = Array.from(
|
|
202
|
+
element.shadowRoot.querySelectorAll(selectors.stepLabel)
|
|
203
|
+
)
|
|
204
|
+
.map((el) => el.textContent ?? '')
|
|
205
|
+
.join(' ');
|
|
206
|
+
expect(stepLabelsText).toContain('What is Quantic');
|
|
207
|
+
expect(stepLabelsText).toContain('What is Atomic');
|
|
208
|
+
});
|
|
155
209
|
});
|
|
156
210
|
|
|
157
211
|
describe('after streaming completes', () => {
|