@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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import analyzingQuestion from '@salesforce/label/c.quantic_AgentGenerationStepAnalyzingQuestion';
|
|
2
|
+
import analyzingQuestionCompleted from '@salesforce/label/c.quantic_AgentGenerationStepAnalyzingQuestionCompleted';
|
|
3
|
+
import search from '@salesforce/label/c.quantic_AgentGenerationStepSearch';
|
|
4
|
+
import searchCompleted from '@salesforce/label/c.quantic_AgentGenerationStepSearchCompleted';
|
|
5
|
+
import analyzingResults from '@salesforce/label/c.quantic_AgentGenerationStepAnalyzingResults';
|
|
6
|
+
import analyzingResultsCompleted from '@salesforce/label/c.quantic_AgentGenerationStepAnalyzingResultsCompleted';
|
|
7
|
+
import answering from '@salesforce/label/c.quantic_AgentGenerationStepAnswering';
|
|
8
|
+
import answeringCompleted from '@salesforce/label/c.quantic_AgentGenerationStepAnsweringCompleted';
|
|
9
|
+
import collapseButton from '@salesforce/label/c.quantic_CollapseButton';
|
|
10
|
+
import loadingLabel from '@salesforce/label/c.quantic_Loading';
|
|
11
|
+
import {LightningElement, api} from 'lwc';
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
import streamOfThoughtTemplate from './templates/streamOfThought.html';
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
import collapsedSummaryTemplate from './templates/collapsedSummary.html';
|
|
16
|
+
|
|
17
|
+
/** @typedef {import("coveo").GenerationStep} GenerationStep */
|
|
18
|
+
/** @typedef {'thinking-before-search'|'searching'|'thinking-after-search'|'answering'} ResolvedStepName */
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} ResolvedStep
|
|
21
|
+
* @property {ResolvedStepName} name
|
|
22
|
+
* @property {'active'|'completed'} status
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** @type {Record<string, {active: string, completed: string}>} */
|
|
26
|
+
const STEP_LABEL_KEYS = {
|
|
27
|
+
'thinking-before-search': {
|
|
28
|
+
active: analyzingQuestion,
|
|
29
|
+
completed: analyzingQuestionCompleted,
|
|
30
|
+
},
|
|
31
|
+
searching: {
|
|
32
|
+
active: search,
|
|
33
|
+
completed: searchCompleted,
|
|
34
|
+
},
|
|
35
|
+
'thinking-after-search': {
|
|
36
|
+
active: analyzingResults,
|
|
37
|
+
completed: analyzingResultsCompleted,
|
|
38
|
+
},
|
|
39
|
+
answering: {
|
|
40
|
+
active: answering,
|
|
41
|
+
completed: answeringCompleted,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Maps an array of raw generation steps to resolved steps with a normalized type.
|
|
47
|
+
* @param {GenerationStep[]} steps
|
|
48
|
+
* @returns {ResolvedStep[]}
|
|
49
|
+
*/
|
|
50
|
+
export function resolveSteps(steps) {
|
|
51
|
+
let searchWasPerformed = false;
|
|
52
|
+
return steps.map((step) => {
|
|
53
|
+
/** @type {ResolvedStepName} */
|
|
54
|
+
let name;
|
|
55
|
+
if (step.name === 'searching') {
|
|
56
|
+
searchWasPerformed = true;
|
|
57
|
+
name = 'searching';
|
|
58
|
+
} else if (step.name === 'answering') {
|
|
59
|
+
name = 'answering';
|
|
60
|
+
} else {
|
|
61
|
+
name = searchWasPerformed
|
|
62
|
+
? 'thinking-after-search'
|
|
63
|
+
: 'thinking-before-search';
|
|
64
|
+
}
|
|
65
|
+
return {name, status: step.status};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The `QuanticGeneratedAnswerStreamOfThought` component displays a timeline of agent reasoning
|
|
71
|
+
* steps during answer generation, showing real-time progress and completion status.
|
|
72
|
+
* @category Internal
|
|
73
|
+
* @example
|
|
74
|
+
* <c-quantic-generated-answer-stream-of-thought agent-steps={agentSteps} is-streaming={isStreaming}></c-quantic-generated-answer-stream-of-thought>
|
|
75
|
+
*/
|
|
76
|
+
export default class QuanticGeneratedAnswerStreamOfThought extends LightningElement {
|
|
77
|
+
/**
|
|
78
|
+
* Array of raw generation steps from the headless engine state.
|
|
79
|
+
* @api
|
|
80
|
+
* @type {GenerationStep[]}
|
|
81
|
+
* @default []
|
|
82
|
+
*/
|
|
83
|
+
@api agentSteps = [];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether answer generation is currently active.
|
|
87
|
+
* @api
|
|
88
|
+
* @type {boolean}
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
91
|
+
@api
|
|
92
|
+
get isStreaming() {
|
|
93
|
+
return this._isStreaming;
|
|
94
|
+
}
|
|
95
|
+
set isStreaming(value) {
|
|
96
|
+
this._isStreaming = value;
|
|
97
|
+
this._expanded = value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @type {boolean} */
|
|
101
|
+
_isStreaming = false;
|
|
102
|
+
/** @type {boolean} */
|
|
103
|
+
_expanded = true;
|
|
104
|
+
|
|
105
|
+
labels = {
|
|
106
|
+
collapse: collapseButton,
|
|
107
|
+
loading: loadingLabel,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
handleToggle = () => {
|
|
111
|
+
this._expanded = !this._expanded;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** @returns {Array<{key: number, isActive: boolean, label: string}>} */
|
|
115
|
+
get stepsToDisplay() {
|
|
116
|
+
return this.steps.map((step, index) => {
|
|
117
|
+
const labelKey = STEP_LABEL_KEYS[step.name][step.status];
|
|
118
|
+
return {
|
|
119
|
+
key: index,
|
|
120
|
+
name: step.name,
|
|
121
|
+
isActive: step.status === 'active',
|
|
122
|
+
label: labelKey,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @returns {ResolvedStep[]} */
|
|
128
|
+
get steps() {
|
|
129
|
+
return resolveSteps(this.agentSteps ?? []);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @returns {boolean} */
|
|
133
|
+
get hasSteps() {
|
|
134
|
+
return this.steps.length > 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @returns {boolean} */
|
|
138
|
+
get isCollapsible() {
|
|
139
|
+
return this.steps.length > 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @returns {boolean} */
|
|
143
|
+
get shouldShowCollapsedSummary() {
|
|
144
|
+
return !this._isStreaming && !this._expanded && this.isCollapsible;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @returns {boolean} */
|
|
148
|
+
get shouldShowCollapseButton() {
|
|
149
|
+
return !this._isStreaming && this._expanded && this.isCollapsible;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @returns {string} */
|
|
153
|
+
get collapsedSummaryLabel() {
|
|
154
|
+
const lastStep = this.steps[this.steps.length - 1];
|
|
155
|
+
const labelKey = STEP_LABEL_KEYS[lastStep.name].completed;
|
|
156
|
+
return labelKey;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
render() {
|
|
160
|
+
if (this.shouldShowCollapsedSummary) return collapsedSummaryTemplate;
|
|
161
|
+
return streamOfThoughtTemplate;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import '../quanticGeneratedAnswerStreamOfThought.css';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
data-testid="collapsed-summary"
|
|
4
|
+
class="stream-of-thought__collapsed-summary slds-grid slds-grid_vertical-align-center slds-p-around_none slds-m-vertical_xxx-small"
|
|
5
|
+
type="button"
|
|
6
|
+
aria-expanded={_expanded}
|
|
7
|
+
onclick={handleToggle}
|
|
8
|
+
>
|
|
9
|
+
<span
|
|
10
|
+
class="slds-m-right_x-small slds-grid slds-grid_align-center"
|
|
11
|
+
data-testid="checkmark"
|
|
12
|
+
>
|
|
13
|
+
<lightning-icon
|
|
14
|
+
icon-name="utility:check"
|
|
15
|
+
size="xx-small"
|
|
16
|
+
variant="success"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
></lightning-icon>
|
|
19
|
+
</span>
|
|
20
|
+
<span
|
|
21
|
+
class="slds-text-body_small slds-text-color_weak slds-m-right_x-small"
|
|
22
|
+
data-testid="collapsed-summary-label"
|
|
23
|
+
>{collapsedSummaryLabel}</span
|
|
24
|
+
>
|
|
25
|
+
<lightning-icon
|
|
26
|
+
data-testid="chevron-down"
|
|
27
|
+
icon-name="utility:chevrondown"
|
|
28
|
+
size="xx-small"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
></lightning-icon>
|
|
31
|
+
</button>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import '../quanticGeneratedAnswerStreamOfThought.css';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template lwc:if={hasSteps}>
|
|
3
|
+
<div class="slds-grid slds-grid_vertical">
|
|
4
|
+
<template for:each={stepsToDisplay} for:item="step">
|
|
5
|
+
<div
|
|
6
|
+
key={step.key}
|
|
7
|
+
data-testid="step-item"
|
|
8
|
+
data-step-name={step.name}
|
|
9
|
+
class="slds-grid slds-grid_vertical-align-center slds-m-vertical_xxx-small"
|
|
10
|
+
>
|
|
11
|
+
<template lwc:if={step.isActive}>
|
|
12
|
+
<span
|
|
13
|
+
class="slds-is-relative slds-m-right_x-small slds-grid slds-grid_align-center stream-of-thought__spinner-container"
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
data-testid="spinner"
|
|
16
|
+
>
|
|
17
|
+
<lightning-spinner
|
|
18
|
+
alternative-text={labels.loading}
|
|
19
|
+
size="xx-small"
|
|
20
|
+
></lightning-spinner>
|
|
21
|
+
</span>
|
|
22
|
+
</template>
|
|
23
|
+
<template lwc:else>
|
|
24
|
+
<span
|
|
25
|
+
class="slds-m-right_x-small slds-grid slds-grid_align-center"
|
|
26
|
+
data-testid="checkmark"
|
|
27
|
+
>
|
|
28
|
+
<lightning-icon
|
|
29
|
+
icon-name="utility:check"
|
|
30
|
+
size="xx-small"
|
|
31
|
+
variant="success"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
></lightning-icon>
|
|
34
|
+
</span>
|
|
35
|
+
</template>
|
|
36
|
+
<span
|
|
37
|
+
class="slds-text-body_small slds-text-color_weak"
|
|
38
|
+
data-testid="step-label"
|
|
39
|
+
>{step.label}</span
|
|
40
|
+
>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
<template lwc:if={shouldShowCollapseButton}>
|
|
44
|
+
<button
|
|
45
|
+
data-testid="collapse-button"
|
|
46
|
+
class="stream-of-thought__collapse-button slds-grid slds-grid_vertical-align-center slds-p-around_none"
|
|
47
|
+
type="button"
|
|
48
|
+
aria-expanded={_expanded}
|
|
49
|
+
onclick={handleToggle}
|
|
50
|
+
>
|
|
51
|
+
<span
|
|
52
|
+
class="slds-text-body_small slds-text-color_weak slds-m-right_xx-small"
|
|
53
|
+
>{labels.collapse}</span
|
|
54
|
+
>
|
|
55
|
+
<lightning-icon
|
|
56
|
+
data-testid="chevron-up"
|
|
57
|
+
icon-name="utility:chevronup"
|
|
58
|
+
size="xx-small"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
></lightning-icon>
|
|
61
|
+
</button>
|
|
62
|
+
</template>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
</template>
|
|
@@ -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
|
+
});
|
package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.css
ADDED
|
@@ -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
|
+
}
|
package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.html
ADDED
|
@@ -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>
|