@coveo/quantic 3.39.1 → 3.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/force-app/main/default/classes/HeadlessControllerTest.cls +1 -0
- package/force-app/main/default/classes/InsightControllerTest.cls +1 -0
- package/force-app/main/default/classes/RecommendationsControllerTest.cls +1 -1
- package/force-app/main/default/classes/SampleTokenProvider.cls +1 -0
- package/force-app/main/default/classes/SampleTokenProviderTest.cls +1 -0
- package/force-app/main/default/labels/CustomLabels.labels-meta.xml +77 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +54 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +26 -3
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +5 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +3 -1
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/__tests__/quanticGeneratedAnswerBody.test.js +341 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js +148 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js-meta.xml +5 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.css +3 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.html +53 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/cannotAnswer.html +7 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/error.html +7 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerContent/__tests__/quanticGeneratedAnswerContent.test.js +269 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js +136 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerContent/templates/generatedMarkdownContent.css +10 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/quanticGeneratedAnswerStreamOfThought.test.js +348 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.css +17 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js +163 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js-meta.xml +5 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.css +1 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.html +32 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.css +1 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.html +65 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/__tests__/quanticGeneratedAnswerThread.test.js +285 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.css +47 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.html +67 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js +93 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js-meta.xml +5 -0
- package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.css +0 -4
- package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.html +1 -1
- package/force-app/main/default/lwc/quanticUtils/__tests__/markdownUtils.test.js +38 -0
- package/force-app/main/default/lwc/quanticUtils/markdownUtils.js +18 -0
- package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +6 -6
- package/force-app/main/default/staticresources/coveoheadless/definitions/api/commerce/common/pagination.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-generated-answer.d.ts +1 -0
- package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-interactive-citation.d.ts +3 -3
- package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/generated-answer/interactive-citation-analytics-client.d.ts +3 -0
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/analytics/analytics-utils.d.ts +3 -2
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-analytics-actions.d.ts +2 -0
- package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
- package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +7 -7
- package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
- package/package.json +2 -2
package/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerComponentForInit,
|
|
3
|
+
initializeWithHeadless,
|
|
4
|
+
getHeadlessBundle,
|
|
5
|
+
} from 'c/quanticHeadlessLoader';
|
|
1
6
|
import {
|
|
2
7
|
transformMarkdownToHtml,
|
|
3
8
|
loadMarkdownDependencies,
|
|
9
|
+
LinkUtils,
|
|
4
10
|
} from 'c/quanticUtils';
|
|
5
11
|
import {LightningElement, api} from 'lwc';
|
|
6
12
|
// @ts-ignore
|
|
@@ -8,6 +14,21 @@ import generatedMarkdownContentTemplate from './templates/generatedMarkdownConte
|
|
|
8
14
|
// @ts-ignore
|
|
9
15
|
import generatedTextContentTemplate from './templates/generatedTextContent.html';
|
|
10
16
|
|
|
17
|
+
/** @typedef {import("coveo").SearchEngine} SearchEngine */
|
|
18
|
+
/** @typedef {import("coveo").InsightEngine} InsightEngine */
|
|
19
|
+
|
|
20
|
+
const INLINE_LINK_ICON = `
|
|
21
|
+
<svg
|
|
22
|
+
class="slds-icon answer-content__link-icon-svg"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
focusable="false"
|
|
25
|
+
viewBox="0 0 52 52"
|
|
26
|
+
>
|
|
27
|
+
<path d="M48.5 2h-15c-.8 0-1.5.7-1.5 1.5v3c0 .8.7 1.5 1.5 1.5h6.4L22.1 25.8c-.6.6-.6 1.5 0 2.1l2.1 2.1c.6.6 1.5.6 2.1 0L44 12.3v6.2c0 .8.7 1.5 1.5 1.5h3c.8 0 1.5-.7 1.5-1.5v-15c0-.8-.7-1.5-1.5-1.5z"></path>
|
|
28
|
+
<path d="M38 28.5V46H8V16h17.5c.8 0 1.5-.7 1.5-1.5v-3c0-.8-.7-1.5-1.5-1.5H6c-2.2 0-4 1.8-4 4v34c0 2.2 1.8 4 4 4h34c2.2 0 4-1.8 4-4V28.5c0-.8-.7-1.5-1.5-1.5h-3c-.8 0-1.5.7-1.5 1.5z"></path>
|
|
29
|
+
</svg>
|
|
30
|
+
`;
|
|
31
|
+
|
|
11
32
|
/**
|
|
12
33
|
* The `QuanticGeneratedAnswerContent` component displays the generated answer content.
|
|
13
34
|
* @category Internal
|
|
@@ -16,6 +37,18 @@ import generatedTextContentTemplate from './templates/generatedTextContent.html'
|
|
|
16
37
|
* <c-quantic-generated-answer-content answer-content-format={answerContentFormat} answer={answer} is-streaming={isStreaming}></c-quantic-generated-answer-content>
|
|
17
38
|
*/
|
|
18
39
|
export default class QuanticGeneratedAnswerContent extends LightningElement {
|
|
40
|
+
/**
|
|
41
|
+
* The ID of the engine instance the component registers to.
|
|
42
|
+
* @api
|
|
43
|
+
* @type {string}
|
|
44
|
+
*/
|
|
45
|
+
@api engineId;
|
|
46
|
+
/**
|
|
47
|
+
* The unique identifier of the generated answer.
|
|
48
|
+
* @api
|
|
49
|
+
* @type {string}
|
|
50
|
+
*/
|
|
51
|
+
@api answerId;
|
|
19
52
|
/**
|
|
20
53
|
* If the answer is streaming, it will render a blinking cursor at the end of the answer.
|
|
21
54
|
* @api
|
|
@@ -65,6 +98,32 @@ export default class QuanticGeneratedAnswerContent extends LightningElement {
|
|
|
65
98
|
/** @type {'text/plain' | 'text/markdown'} */
|
|
66
99
|
_answerContentFormat = 'text/plain';
|
|
67
100
|
_markdownDependenciesLoaded = false;
|
|
101
|
+
/** @type {Array<function>} */
|
|
102
|
+
_inlineLinkBindings = [];
|
|
103
|
+
/** @type {AnyHeadless} */
|
|
104
|
+
headless;
|
|
105
|
+
/** @type {SearchEngine | InsightEngine} */
|
|
106
|
+
engine;
|
|
107
|
+
|
|
108
|
+
connectedCallback() {
|
|
109
|
+
registerComponentForInit(this, this.engineId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
renderedCallback() {
|
|
113
|
+
initializeWithHeadless(this, this.engineId, this.initialize);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {SearchEngine | InsightEngine} engine
|
|
118
|
+
*/
|
|
119
|
+
initialize = (engine) => {
|
|
120
|
+
this.headless = getHeadlessBundle(this.engineId);
|
|
121
|
+
this.engine = engine;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
disconnectedCallback() {
|
|
125
|
+
this.cleanUpInlineLinkBindings();
|
|
126
|
+
}
|
|
68
127
|
|
|
69
128
|
loadMarkdownDependencies() {
|
|
70
129
|
if (
|
|
@@ -105,6 +164,7 @@ export default class QuanticGeneratedAnswerContent extends LightningElement {
|
|
|
105
164
|
// eslint-disable-next-line @lwc/lwc/no-inner-html
|
|
106
165
|
answerContainer.innerHTML = newHTMLContent;
|
|
107
166
|
}
|
|
167
|
+
this.processInlineLinks(answerContainer);
|
|
108
168
|
}
|
|
109
169
|
}
|
|
110
170
|
// Fallback to display answer as text if the Marked library failed to load
|
|
@@ -114,6 +174,82 @@ export default class QuanticGeneratedAnswerContent extends LightningElement {
|
|
|
114
174
|
this.dispatchEvent(new CustomEvent('quantic__answercontentupdated'));
|
|
115
175
|
}
|
|
116
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Decorates each inline link with an icon and binds analytics, replacing any previous bindings.
|
|
179
|
+
* @param {Element|null} answerContainer
|
|
180
|
+
*/
|
|
181
|
+
processInlineLinks(answerContainer) {
|
|
182
|
+
if (!answerContainer) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.cleanUpInlineLinkBindings();
|
|
186
|
+
/** @type {NodeListOf<HTMLAnchorElement>} */
|
|
187
|
+
const anchors = answerContainer.querySelectorAll(
|
|
188
|
+
'a[data-answer-inline-link]'
|
|
189
|
+
);
|
|
190
|
+
anchors.forEach((anchor) => {
|
|
191
|
+
anchor.target = '_blank';
|
|
192
|
+
anchor.rel = 'noopener';
|
|
193
|
+
this.bindAnalyticsToInlineLink(anchor);
|
|
194
|
+
this.appendInlineLinkIcon(anchor);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Appends the external link icon to the given anchor.
|
|
200
|
+
* @param {HTMLAnchorElement} anchor
|
|
201
|
+
*/
|
|
202
|
+
appendInlineLinkIcon(anchor) {
|
|
203
|
+
const icon = document.createElement('span');
|
|
204
|
+
icon.classList.add(
|
|
205
|
+
'slds-icon_container',
|
|
206
|
+
'slds-icon-utility-new_window',
|
|
207
|
+
'slds-current-color',
|
|
208
|
+
'slds-m-left_xxx-small',
|
|
209
|
+
'answer-content__link-icon'
|
|
210
|
+
);
|
|
211
|
+
// eslint-disable-next-line @lwc/lwc/no-inner-html
|
|
212
|
+
icon.innerHTML = INLINE_LINK_ICON;
|
|
213
|
+
anchor.appendChild(icon);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Creates an `InteractiveGeneratedAnswerInlineLink` controller for the given anchor
|
|
218
|
+
* and binds analytics event listeners to it.
|
|
219
|
+
* @param {HTMLAnchorElement} anchor
|
|
220
|
+
*/
|
|
221
|
+
bindAnalyticsToInlineLink(anchor) {
|
|
222
|
+
if (
|
|
223
|
+
!this.headless?.buildInteractiveGeneratedAnswerInlineLink ||
|
|
224
|
+
!this.engine
|
|
225
|
+
) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const controller = this.headless.buildInteractiveGeneratedAnswerInlineLink(
|
|
229
|
+
this.engine,
|
|
230
|
+
{
|
|
231
|
+
options: {
|
|
232
|
+
link: {
|
|
233
|
+
linkURL: anchor.getAttribute('href') || '',
|
|
234
|
+
linkText: anchor.textContent?.trim() || '',
|
|
235
|
+
},
|
|
236
|
+
answerId: this.answerId,
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
this._inlineLinkBindings.push(
|
|
241
|
+
LinkUtils.bindAnalyticsToLink(anchor, controller)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Cleans up all inline link bindings.
|
|
247
|
+
*/
|
|
248
|
+
cleanUpInlineLinkBindings() {
|
|
249
|
+
this._inlineLinkBindings.forEach((remove) => remove());
|
|
250
|
+
this._inlineLinkBindings = [];
|
|
251
|
+
}
|
|
252
|
+
|
|
117
253
|
get generatedAnswerContentClass() {
|
|
118
254
|
return `generated-answer-content__answer ${this.isStreaming ? 'generated-answer-content__answer--streaming' : ''}`;
|
|
119
255
|
}
|
|
@@ -152,3 +152,13 @@ tbody tr td:first-of-type {
|
|
|
152
152
|
tbody tr td:last-of-type td {
|
|
153
153
|
border-bottom: unset;
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
.answer-content__link-icon-svg {
|
|
157
|
+
width: 0.625rem;
|
|
158
|
+
height: 0.625rem;
|
|
159
|
+
fill: currentColor;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.answer-content__link-icon {
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import {resolveSteps} from '../quanticGeneratedAnswerStreamOfThought.js';
|
|
2
|
+
import {buildCreateTestComponent, cleanup, flushPromises} from 'c/testUtils';
|
|
3
|
+
import QuanticGeneratedAnswerStreamOfThought from '../quanticGeneratedAnswerStreamOfThought';
|
|
4
|
+
|
|
5
|
+
const selectors = {
|
|
6
|
+
stepItem: '[data-testid="step-item"]',
|
|
7
|
+
spinner: '[data-testid="spinner"]',
|
|
8
|
+
checkmark: '[data-testid="checkmark"]',
|
|
9
|
+
stepLabel: '[data-testid="step-label"]',
|
|
10
|
+
collapseButton: '[data-testid="collapse-button"]',
|
|
11
|
+
collapsedSummary: '[data-testid="collapsed-summary"]',
|
|
12
|
+
collapsedSummaryLabel: '[data-testid="collapsed-summary-label"]',
|
|
13
|
+
chevronUp: '[data-testid="chevron-up"]',
|
|
14
|
+
chevronDown: '[data-testid="chevron-down"]',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const createTestComponent = buildCreateTestComponent(
|
|
18
|
+
QuanticGeneratedAnswerStreamOfThought,
|
|
19
|
+
'c-quantic-generated-answer-stream-of-thought',
|
|
20
|
+
{agentSteps: [], isStreaming: false}
|
|
21
|
+
);
|
|
22
|
+
|
|
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
|
+
describe('quantic generated answer stream of thought component', () => {
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
cleanup();
|
|
66
|
+
});
|
|
67
|
+
describe('during streaming', () => {
|
|
68
|
+
it('should render nothing when there are no steps', async () => {
|
|
69
|
+
const element = createTestComponent({
|
|
70
|
+
agentSteps: [],
|
|
71
|
+
isStreaming: true,
|
|
72
|
+
});
|
|
73
|
+
await flushPromises();
|
|
74
|
+
|
|
75
|
+
expect(element.shadowRoot.firstChild).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should show all steps progressively', async () => {
|
|
79
|
+
const element = createTestComponent({
|
|
80
|
+
agentSteps: [
|
|
81
|
+
{name: 'thinking', status: 'completed'},
|
|
82
|
+
{name: 'searching', status: 'active'},
|
|
83
|
+
],
|
|
84
|
+
isStreaming: true,
|
|
85
|
+
});
|
|
86
|
+
await flushPromises();
|
|
87
|
+
|
|
88
|
+
const stepItems = element.shadowRoot.querySelectorAll(selectors.stepItem);
|
|
89
|
+
expect(stepItems).toHaveLength(2);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should render steps in the correct order', async () => {
|
|
93
|
+
const element = createTestComponent({
|
|
94
|
+
agentSteps: [
|
|
95
|
+
{name: 'thinking', status: 'completed', startedAt: 0},
|
|
96
|
+
{name: 'searching', status: 'completed', startedAt: 10},
|
|
97
|
+
{name: 'thinking', status: 'completed', startedAt: 20},
|
|
98
|
+
{name: 'searching', status: 'completed', startedAt: 30},
|
|
99
|
+
{name: 'thinking', status: 'completed', startedAt: 40},
|
|
100
|
+
{name: 'answering', status: 'active', startedAt: 30},
|
|
101
|
+
],
|
|
102
|
+
isStreaming: true,
|
|
103
|
+
});
|
|
104
|
+
await flushPromises();
|
|
105
|
+
|
|
106
|
+
const stepItems = element.shadowRoot.querySelectorAll(selectors.stepItem);
|
|
107
|
+
const names = Array.from(stepItems).map((el) =>
|
|
108
|
+
el.getAttribute('data-step-name')
|
|
109
|
+
);
|
|
110
|
+
expect(names).toEqual([
|
|
111
|
+
'thinking-before-search',
|
|
112
|
+
'searching',
|
|
113
|
+
'thinking-after-search',
|
|
114
|
+
'searching',
|
|
115
|
+
'thinking-after-search',
|
|
116
|
+
'answering',
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should show checkmarks for completed steps and spinners for active steps', async () => {
|
|
121
|
+
const element = createTestComponent({
|
|
122
|
+
agentSteps: [
|
|
123
|
+
{name: 'thinking', status: 'completed'},
|
|
124
|
+
{name: 'searching', status: 'active'},
|
|
125
|
+
],
|
|
126
|
+
isStreaming: true,
|
|
127
|
+
});
|
|
128
|
+
await flushPromises();
|
|
129
|
+
|
|
130
|
+
const checkmarks = element.shadowRoot.querySelectorAll(
|
|
131
|
+
selectors.checkmark
|
|
132
|
+
);
|
|
133
|
+
const spinners = element.shadowRoot.querySelectorAll(selectors.spinner);
|
|
134
|
+
expect(checkmarks).toHaveLength(1);
|
|
135
|
+
expect(spinners).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should not show a collapse button', async () => {
|
|
139
|
+
const element = createTestComponent({
|
|
140
|
+
agentSteps: [{name: 'thinking', status: 'active'}],
|
|
141
|
+
isStreaming: true,
|
|
142
|
+
});
|
|
143
|
+
await flushPromises();
|
|
144
|
+
|
|
145
|
+
const collapseButton = element.shadowRoot.querySelector(
|
|
146
|
+
selectors.collapseButton
|
|
147
|
+
);
|
|
148
|
+
const collapsedSummary = element.shadowRoot.querySelector(
|
|
149
|
+
selectors.collapsedSummary
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(collapseButton).toBeNull();
|
|
153
|
+
expect(collapsedSummary).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('after streaming completes', () => {
|
|
158
|
+
const multipleSteps = [
|
|
159
|
+
{name: 'thinking', status: 'completed'},
|
|
160
|
+
{name: 'searching', status: 'completed'},
|
|
161
|
+
{name: 'thinking', status: 'completed'},
|
|
162
|
+
{name: 'answering', status: 'active'},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
it('should auto-collapse to show only the last step', async () => {
|
|
166
|
+
const element = createTestComponent({
|
|
167
|
+
agentSteps: multipleSteps,
|
|
168
|
+
isStreaming: true,
|
|
169
|
+
});
|
|
170
|
+
await flushPromises();
|
|
171
|
+
let collapsedSummary = element.shadowRoot.querySelector(
|
|
172
|
+
selectors.collapsedSummary
|
|
173
|
+
);
|
|
174
|
+
let stepItems = element.shadowRoot.querySelectorAll(selectors.stepItem);
|
|
175
|
+
|
|
176
|
+
expect(collapsedSummary).toBeNull();
|
|
177
|
+
expect(stepItems).toHaveLength(multipleSteps.length);
|
|
178
|
+
|
|
179
|
+
element.isStreaming = false;
|
|
180
|
+
element.agentSteps = [
|
|
181
|
+
{name: 'thinking', status: 'completed'},
|
|
182
|
+
{name: 'searching', status: 'completed'},
|
|
183
|
+
{name: 'thinking', status: 'completed'},
|
|
184
|
+
{name: 'answering', status: 'completed'},
|
|
185
|
+
];
|
|
186
|
+
await flushPromises();
|
|
187
|
+
|
|
188
|
+
collapsedSummary = element.shadowRoot.querySelector(
|
|
189
|
+
selectors.collapsedSummary
|
|
190
|
+
);
|
|
191
|
+
stepItems = element.shadowRoot.querySelectorAll(selectors.stepItem);
|
|
192
|
+
const summaryLabel = element.shadowRoot.querySelector(
|
|
193
|
+
selectors.collapsedSummaryLabel
|
|
194
|
+
);
|
|
195
|
+
const checkmark = collapsedSummary.querySelector(selectors.checkmark);
|
|
196
|
+
|
|
197
|
+
expect(collapsedSummary).not.toBeNull();
|
|
198
|
+
expect(summaryLabel).not.toBeNull();
|
|
199
|
+
expect(checkmark).not.toBeNull();
|
|
200
|
+
expect(stepItems).toHaveLength(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should have aria-expanded set to false when collapsed', async () => {
|
|
204
|
+
const element = createTestComponent({
|
|
205
|
+
agentSteps: multipleSteps,
|
|
206
|
+
isStreaming: false,
|
|
207
|
+
});
|
|
208
|
+
await flushPromises();
|
|
209
|
+
|
|
210
|
+
const collapsedSummary = element.shadowRoot.querySelector(
|
|
211
|
+
selectors.collapsedSummary
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(collapsedSummary).not.toBeNull();
|
|
215
|
+
expect(collapsedSummary.getAttribute('aria-expanded')).toBe('false');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should render nothing when there are no steps and not streaming', async () => {
|
|
219
|
+
const element = createTestComponent({
|
|
220
|
+
agentSteps: [],
|
|
221
|
+
isStreaming: false,
|
|
222
|
+
});
|
|
223
|
+
await flushPromises();
|
|
224
|
+
|
|
225
|
+
expect(element.shadowRoot.firstChild).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('expand/collapse interaction', () => {
|
|
230
|
+
const multipleSteps = [
|
|
231
|
+
{name: 'thinking', status: 'completed'},
|
|
232
|
+
{name: 'searching', status: 'completed'},
|
|
233
|
+
{name: 'thinking', status: 'completed'},
|
|
234
|
+
{name: 'answering', status: 'completed'},
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
it('should expand to show all steps and collapse button when collapsed summary row is clicked', async () => {
|
|
238
|
+
const element = createTestComponent({
|
|
239
|
+
agentSteps: multipleSteps,
|
|
240
|
+
isStreaming: false,
|
|
241
|
+
});
|
|
242
|
+
await flushPromises();
|
|
243
|
+
|
|
244
|
+
const collapsedSummary = element.shadowRoot.querySelector(
|
|
245
|
+
selectors.collapsedSummary
|
|
246
|
+
);
|
|
247
|
+
expect(collapsedSummary).not.toBeNull();
|
|
248
|
+
|
|
249
|
+
collapsedSummary.click();
|
|
250
|
+
await flushPromises();
|
|
251
|
+
|
|
252
|
+
const stepItems = element.shadowRoot.querySelectorAll(selectors.stepItem);
|
|
253
|
+
const collapseButton = element.shadowRoot.querySelector(
|
|
254
|
+
selectors.collapseButton
|
|
255
|
+
);
|
|
256
|
+
expect(stepItems).toHaveLength(multipleSteps.length);
|
|
257
|
+
expect(collapseButton).not.toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should collapse back when collapse button is clicked', async () => {
|
|
261
|
+
const element = createTestComponent({
|
|
262
|
+
agentSteps: multipleSteps,
|
|
263
|
+
isStreaming: false,
|
|
264
|
+
});
|
|
265
|
+
await flushPromises();
|
|
266
|
+
|
|
267
|
+
const collapsedSummary = element.shadowRoot.querySelector(
|
|
268
|
+
selectors.collapsedSummary
|
|
269
|
+
);
|
|
270
|
+
collapsedSummary.click();
|
|
271
|
+
await flushPromises();
|
|
272
|
+
|
|
273
|
+
const collapseButton = element.shadowRoot.querySelector(
|
|
274
|
+
selectors.collapseButton
|
|
275
|
+
);
|
|
276
|
+
expect(collapseButton).not.toBeNull();
|
|
277
|
+
|
|
278
|
+
collapseButton.click();
|
|
279
|
+
await flushPromises();
|
|
280
|
+
|
|
281
|
+
expect(
|
|
282
|
+
element.shadowRoot.querySelector(selectors.collapsedSummary)
|
|
283
|
+
).not.toBeNull();
|
|
284
|
+
expect(
|
|
285
|
+
element.shadowRoot.querySelectorAll(selectors.stepItem)
|
|
286
|
+
).toHaveLength(0);
|
|
287
|
+
expect(
|
|
288
|
+
element.shadowRoot.querySelector(selectors.collapseButton)
|
|
289
|
+
).toBeNull();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should set aria-expanded to true when expanded', async () => {
|
|
293
|
+
const element = createTestComponent({
|
|
294
|
+
agentSteps: multipleSteps,
|
|
295
|
+
isStreaming: false,
|
|
296
|
+
});
|
|
297
|
+
await flushPromises();
|
|
298
|
+
|
|
299
|
+
const collapsedSummary = element.shadowRoot.querySelector(
|
|
300
|
+
selectors.collapsedSummary
|
|
301
|
+
);
|
|
302
|
+
collapsedSummary.click();
|
|
303
|
+
await flushPromises();
|
|
304
|
+
|
|
305
|
+
const collapseButton = element.shadowRoot.querySelector(
|
|
306
|
+
selectors.collapseButton
|
|
307
|
+
);
|
|
308
|
+
expect(collapseButton).not.toBeNull();
|
|
309
|
+
expect(collapseButton.getAttribute('aria-expanded')).toBe('true');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should set aria-expanded to false when collapsed', async () => {
|
|
313
|
+
const element = createTestComponent({
|
|
314
|
+
agentSteps: multipleSteps,
|
|
315
|
+
isStreaming: false,
|
|
316
|
+
});
|
|
317
|
+
await flushPromises();
|
|
318
|
+
|
|
319
|
+
const collapsedSummary = element.shadowRoot.querySelector(
|
|
320
|
+
selectors.collapsedSummary
|
|
321
|
+
);
|
|
322
|
+
expect(collapsedSummary).not.toBeNull();
|
|
323
|
+
expect(collapsedSummary.getAttribute('aria-expanded')).toBe('false');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('single answering step', () => {
|
|
328
|
+
it('should show the step without a collapse button when only answering exists', async () => {
|
|
329
|
+
const element = createTestComponent({
|
|
330
|
+
agentSteps: [{name: 'answering', status: 'completed'}],
|
|
331
|
+
isStreaming: false,
|
|
332
|
+
});
|
|
333
|
+
await flushPromises();
|
|
334
|
+
|
|
335
|
+
const stepItems = element.shadowRoot.querySelectorAll(selectors.stepItem);
|
|
336
|
+
const collapseButton = element.shadowRoot.querySelector(
|
|
337
|
+
selectors.collapseButton
|
|
338
|
+
);
|
|
339
|
+
const collapsedSummary = element.shadowRoot.querySelector(
|
|
340
|
+
selectors.collapsedSummary
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
expect(stepItems).toHaveLength(1);
|
|
344
|
+
expect(collapseButton).toBeNull();
|
|
345
|
+
expect(collapsedSummary).toBeNull();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.stream-of-thought__collapsed-summary,
|
|
2
|
+
.stream-of-thought__collapse-button {
|
|
3
|
+
border: none;
|
|
4
|
+
background: transparent;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.stream-of-thought__collapsed-summary:focus-visible,
|
|
8
|
+
.stream-of-thought__collapse-button:focus-visible {
|
|
9
|
+
border-radius: var(--slds-g-sizing-border-4, 0.25rem);
|
|
10
|
+
outline: 2px solid var(--lwc-brandPrimary, #1b96ff);
|
|
11
|
+
outline-offset: 2px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.stream-of-thought__spinner-container {
|
|
15
|
+
width: 0.8rem;
|
|
16
|
+
height: 0.8rem;
|
|
17
|
+
}
|