@miso.ai/client-sdk-ui 1.11.0-beta.1 → 1.11.0-beta.3
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/dist/cjs/actor/data.js +6 -7
- package/dist/cjs/actor/hub.js +1 -2
- package/dist/cjs/actor/sessions.js +4 -5
- package/dist/cjs/actor/views.js +3 -0
- package/dist/cjs/defaults/hybrid-search/answer-box.js +2 -2
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/workflow/answer-based.js +26 -8
- package/dist/cjs/workflow/ask.js +1 -0
- package/dist/cjs/workflow/base.js +18 -13
- package/dist/cjs/workflow/hybrid-search-answer.js +86 -0
- package/dist/cjs/workflow/hybrid-search-results.js +88 -0
- package/dist/cjs/workflow/hybrid-search.js +185 -77
- package/dist/cjs/workflow/processors.js +15 -16
- package/package.json +2 -2
- package/src/actor/data.js +2 -4
- package/src/actor/hub.js +1 -2
- package/src/actor/sessions.js +4 -3
- package/src/actor/views.js +4 -0
- package/src/defaults/hybrid-search/answer-box.js +4 -4
- package/src/layout/trackable.js +1 -1
- package/src/version.js +1 -1
- package/src/workflow/answer-based.js +27 -6
- package/src/workflow/ask.js +2 -1
- package/src/workflow/base.js +14 -11
- package/src/workflow/hybrid-search-answer.js +78 -0
- package/src/workflow/hybrid-search-results.js +79 -0
- package/src/workflow/hybrid-search.js +189 -54
- package/src/workflow/processors.js +12 -17
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import AnswerBasedWorkflow from './answer-based.js';
|
|
2
|
+
import { ROLE, STATUS, ORGANIC_QUESTION_SOURCE } from '../constants.js';
|
|
3
|
+
import { writeKeywordsToData } from './processors.js';
|
|
4
|
+
|
|
5
|
+
const ROLES_CONFIG = Object.freeze({
|
|
6
|
+
[ROLE.QUESTION]: {
|
|
7
|
+
mapping: ROLE.KEYWORDS,
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export default class HybridSearchAnswer extends AnswerBasedWorkflow {
|
|
12
|
+
|
|
13
|
+
constructor(superworkflow) {
|
|
14
|
+
super({
|
|
15
|
+
name: 'hybrid-search/answer',
|
|
16
|
+
plugin: superworkflow._plugin,
|
|
17
|
+
client: superworkflow._client,
|
|
18
|
+
options: superworkflow._options,
|
|
19
|
+
roles: Object.keys(AnswerBasedWorkflow.DEFAULT_LAYOUTS),
|
|
20
|
+
rolesConfig: ROLES_CONFIG,
|
|
21
|
+
superworkflow,
|
|
22
|
+
});
|
|
23
|
+
this.useInteractions(false);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_initProperties(args) {
|
|
27
|
+
super._initProperties(args);
|
|
28
|
+
this._superworkflow = args.superworkflow;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_initReset() {} // no reset here, will manually reset later
|
|
32
|
+
|
|
33
|
+
restart() {
|
|
34
|
+
super.restart();
|
|
35
|
+
// cascade restart to the sibling
|
|
36
|
+
const results = this._superworkflow._results;
|
|
37
|
+
results.restart();
|
|
38
|
+
this._resultsSession = results.session; // keep track of the session
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// query //
|
|
42
|
+
_buildPayload({ q, qs, ...payload } = {}) {
|
|
43
|
+
return {
|
|
44
|
+
...payload,
|
|
45
|
+
q, // q, not question
|
|
46
|
+
_meta: {
|
|
47
|
+
...payload._meta,
|
|
48
|
+
question_source: qs || ORGANIC_QUESTION_SOURCE, // might be null, not undefined
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// data //
|
|
54
|
+
_updateData(data) {
|
|
55
|
+
// we want this to be available everywhere
|
|
56
|
+
data = writeKeywordsToData(data);
|
|
57
|
+
|
|
58
|
+
switch (data.status) {
|
|
59
|
+
case STATUS.INITIAL:
|
|
60
|
+
case STATUS.LOADING:
|
|
61
|
+
// share the initial/loading status with its sibling
|
|
62
|
+
this._dispatchDataToSibling(data);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
super._updateData(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_handleHeadResponse(data) {
|
|
69
|
+
super._handleHeadResponse(data);
|
|
70
|
+
// share the search results with its sibling
|
|
71
|
+
this._dispatchDataToSibling(data);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_dispatchDataToSibling(data) {
|
|
75
|
+
this._superworkflow._results.updateData({ ...data, session: this._resultsSession });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { trimObj } from '@miso.ai/commons';
|
|
2
|
+
import Workflow from './base.js';
|
|
3
|
+
import { fields } from '../actor/index.js';
|
|
4
|
+
import { ROLE } from '../constants.js';
|
|
5
|
+
import { mergeApiOptions } from './options.js';
|
|
6
|
+
import { writeKeywordsToData, composeFq } from './processors.js';
|
|
7
|
+
|
|
8
|
+
const ROLES_CONFIG = Object.freeze({
|
|
9
|
+
[ROLE.FACETS]: {
|
|
10
|
+
mapping: 'facet_counts',
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export default class HybridSearchResults extends Workflow {
|
|
15
|
+
|
|
16
|
+
constructor(superworkflow) {
|
|
17
|
+
super({
|
|
18
|
+
name: 'hybrid-search/results',
|
|
19
|
+
plugin: superworkflow._plugin,
|
|
20
|
+
client: superworkflow._client,
|
|
21
|
+
options: superworkflow._options,
|
|
22
|
+
roles: [ROLE.PRODUCTS, ROLE.KEYWORDS, ROLE.HITS, ROLE.FACETS],
|
|
23
|
+
rolesConfig: ROLES_CONFIG,
|
|
24
|
+
superworkflow,
|
|
25
|
+
});
|
|
26
|
+
this.useInteractions(false);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_initProperties(args) {
|
|
30
|
+
super._initProperties(args);
|
|
31
|
+
this._superworkflow = args.superworkflow;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_initSubscriptions(args) {
|
|
35
|
+
super._initSubscriptions(args);
|
|
36
|
+
this._unsubscribes = [
|
|
37
|
+
...this._unsubscribes,
|
|
38
|
+
this._hub.on(fields.filters(), filters => this._refine(filters)),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_initReset() {} // no reset here, will manually reset later
|
|
43
|
+
|
|
44
|
+
// query //
|
|
45
|
+
_refine(filters) {
|
|
46
|
+
// start a new session
|
|
47
|
+
this.restart();
|
|
48
|
+
|
|
49
|
+
// get stored query from sibling
|
|
50
|
+
const query = this._superworkflow._answer._hub.states[fields.query()];
|
|
51
|
+
const payload = this._buildPayload({ ...query, filters, answer: false });
|
|
52
|
+
const { session } = this;
|
|
53
|
+
const event = mergeApiOptions(this._options.resolved.api, { payload, session });
|
|
54
|
+
|
|
55
|
+
this._request(event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_buildPayload({ filters, ...payload } = {}) {
|
|
59
|
+
// borrow the work from the sibling
|
|
60
|
+
payload = this._superworkflow._answer._buildPayload(payload);
|
|
61
|
+
const fq = composeFq(filters); // TODO: combine with fq in payload?
|
|
62
|
+
return trimObj({
|
|
63
|
+
...payload,
|
|
64
|
+
fq,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_request(event) {
|
|
69
|
+
this._hub.update(fields.request(), event);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// data //
|
|
73
|
+
_defaultProcessData(data) {
|
|
74
|
+
data = super._defaultProcessData(data);
|
|
75
|
+
data = writeKeywordsToData(data);
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { API } from '@miso.ai/commons';
|
|
2
|
+
import { InteractionsActor } from '../actor/index.js';
|
|
3
|
+
import Workflow from './base.js';
|
|
2
4
|
import AnswerBasedWorkflow from './answer-based.js';
|
|
3
|
-
import {
|
|
4
|
-
import { fields } from '../actor/index.js';
|
|
5
|
-
import { ROLE, ORGANIC_QUESTION_SOURCE } from '../constants.js';
|
|
5
|
+
import { ROLE } from '../constants.js';
|
|
6
6
|
import { ListLayout, TextLayout, FacetsLayout } from '../layout/index.js';
|
|
7
|
-
import
|
|
7
|
+
import HybridSearchResults from './hybrid-search-results.js';
|
|
8
8
|
|
|
9
9
|
const DEFAULT_API_OPTIONS = Object.freeze({
|
|
10
10
|
...AnswerBasedWorkflow.DEFAULT_API_OPTIONS,
|
|
11
11
|
name: API.NAME.SEARCH,
|
|
12
12
|
payload: {
|
|
13
13
|
...AnswerBasedWorkflow.DEFAULT_API_OPTIONS.payload,
|
|
14
|
+
fl: ['cover_image', 'url', 'created_at', 'updated_at', 'published_at'],
|
|
14
15
|
},
|
|
15
16
|
});
|
|
16
17
|
|
|
@@ -42,7 +43,25 @@ const ROLES_CONFIG = Object.freeze({
|
|
|
42
43
|
},
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
const SUBWORKFLOW = Object.freeze({
|
|
47
|
+
ANSWER: 'answer',
|
|
48
|
+
RESULTS: 'results',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function getSubworkflowByRole(role) {
|
|
52
|
+
// TODO: ROLE.ERROR
|
|
53
|
+
switch (role) {
|
|
54
|
+
case ROLE.PRODUCTS:
|
|
55
|
+
case ROLE.HITS:
|
|
56
|
+
case ROLE.FACETS:
|
|
57
|
+
case ROLE.KEYWORDS:
|
|
58
|
+
return SUBWORKFLOW.RESULTS;
|
|
59
|
+
default:
|
|
60
|
+
return SUBWORKFLOW.ANSWER;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default class HybridSearch extends Workflow {
|
|
46
65
|
|
|
47
66
|
constructor(plugin, client) {
|
|
48
67
|
super({
|
|
@@ -55,71 +74,187 @@ export default class HybridSearch extends AnswerBasedWorkflow {
|
|
|
55
74
|
});
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
super.
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
this.
|
|
77
|
+
_initProperties(args) {
|
|
78
|
+
super._initProperties(args);
|
|
79
|
+
this._subworkflows = [
|
|
80
|
+
this._answer = new HybridSearchAnswer(this),
|
|
81
|
+
this._results = new HybridSearchResults(this),
|
|
63
82
|
];
|
|
64
83
|
}
|
|
65
84
|
|
|
85
|
+
_getSubworkflow(name) {
|
|
86
|
+
switch (name) {
|
|
87
|
+
case SUBWORKFLOW.ANSWER:
|
|
88
|
+
return this._answer;
|
|
89
|
+
case SUBWORKFLOW.RESULTS:
|
|
90
|
+
return this._results;
|
|
91
|
+
default:
|
|
92
|
+
throw new Error(`Invalid subworkflow: ${name}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_initActors() {
|
|
97
|
+
const hub = this._hub;
|
|
98
|
+
const client = this._client;
|
|
99
|
+
const options = this._options;
|
|
100
|
+
|
|
101
|
+
this._views = new HybridSearchViewsActor(this);
|
|
102
|
+
this._interactions = new InteractionsActor(hub, { client, options });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_initReset() {
|
|
106
|
+
this._results.reset();
|
|
107
|
+
this._answer.reset();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
restart({ answer = true } = {}) {
|
|
111
|
+
const previousSession = this.session;
|
|
112
|
+
this._sessions.restart();
|
|
113
|
+
const { session } = this;
|
|
114
|
+
if (!answer) {
|
|
115
|
+
session._answer = previousSession._answer || previousSession;
|
|
116
|
+
}
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// properties //
|
|
121
|
+
get questionId() {
|
|
122
|
+
return this._answer.questionId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get filters() {
|
|
126
|
+
return this._results._views.filters;
|
|
127
|
+
}
|
|
128
|
+
|
|
66
129
|
// query //
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
_meta: {
|
|
74
|
-
...payload._meta,
|
|
75
|
-
question_source: qs || ORGANIC_QUESTION_SOURCE, // might be null, not undefined
|
|
76
|
-
},
|
|
77
|
-
});
|
|
130
|
+
autoQuery(options) {
|
|
131
|
+
this._answer.autoQuery(options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
query(args) {
|
|
135
|
+
this._answer.query(args);
|
|
78
136
|
}
|
|
79
137
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
138
|
+
// interactions //
|
|
139
|
+
_preprocessInteraction(payload) {
|
|
140
|
+
return this._answer._preprocessInteraction(payload);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// destroy //
|
|
144
|
+
_destroy() {
|
|
145
|
+
for (const subworkflow of this._subworkflows) {
|
|
146
|
+
subworkflow.destroy();
|
|
83
147
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
148
|
+
super._destroy();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class HybridSearchViewsActor {
|
|
154
|
+
|
|
155
|
+
constructor(workflow) {
|
|
156
|
+
this._workflow = workflow;
|
|
157
|
+
this._containers = new Set();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get(role) {
|
|
161
|
+
return this._getSubviews(role).get(role);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
addContainer(element) {
|
|
165
|
+
if (this._containers.has(element)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this._containers.add(element);
|
|
169
|
+
|
|
170
|
+
const { components } = element;
|
|
171
|
+
for (const component of components) {
|
|
172
|
+
this.addComponent(component);
|
|
92
173
|
}
|
|
93
|
-
return clauses.map(c => `(${c})`).join(' AND ');
|
|
94
174
|
}
|
|
95
175
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
176
|
+
removeContainer(element) {
|
|
177
|
+
for (const subworkflow of this._workflow._subworkflows) {
|
|
178
|
+
subworkflow._views.removeContainer(element);
|
|
179
|
+
}
|
|
180
|
+
this._containers.delete(element);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
addComponent(element) {
|
|
184
|
+
this._getViewByElement(element).element = element;
|
|
185
|
+
this._addContainerToSubviews(element._container, element);
|
|
186
|
+
}
|
|
100
187
|
|
|
101
|
-
|
|
102
|
-
this.
|
|
188
|
+
removeComponent(element) {
|
|
189
|
+
this._getViewByElement(element).element = undefined;
|
|
190
|
+
this._removeContainerFromSubviewsIfNecessary(element._container);
|
|
191
|
+
}
|
|
103
192
|
|
|
104
|
-
|
|
193
|
+
updateComponentRole(element, oldRole, newRole) {
|
|
105
194
|
// TODO
|
|
106
195
|
}
|
|
107
196
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
data = writeKeywordsToData(data);
|
|
112
|
-
data = instructPartialUpdates(data);
|
|
113
|
-
return data;
|
|
197
|
+
refreshElement(element) {
|
|
198
|
+
const view = element.isContainer ? this._containers.get(element) : this._getViewByElement(element);
|
|
199
|
+
view && view.refresh({ force: true });
|
|
114
200
|
}
|
|
115
201
|
|
|
116
|
-
|
|
202
|
+
// helpers //
|
|
203
|
+
_getSubworkflow(role) {
|
|
204
|
+
return this._workflow._getSubworkflow(getSubworkflowByRole(role));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_getAllSubviews() {
|
|
208
|
+
return this._workflow._subworkflows.flatMap(subworkflow => subworkflow._views);
|
|
209
|
+
}
|
|
117
210
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return data;
|
|
211
|
+
_getSubviews(role) {
|
|
212
|
+
return this._getSubworkflow(role)._views;
|
|
121
213
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{
|
|
214
|
+
|
|
215
|
+
_getViewByElement(element) {
|
|
216
|
+
let { role } = element;
|
|
217
|
+
if (!role) {
|
|
218
|
+
throw new Error('Component must have a role');
|
|
219
|
+
}
|
|
220
|
+
return this.get(role);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_addContainerToSubviews(container, child) {
|
|
224
|
+
if (!container) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const { role } = child;
|
|
228
|
+
const subviews = this._getSubviews(role);
|
|
229
|
+
if (subviews._containers.has(container)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// check if the container is in other subviews
|
|
233
|
+
for (const otherSubviews of this._getAllSubviews()) {
|
|
234
|
+
if (otherSubviews === subviews) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (otherSubviews._containers.has(container)) {
|
|
238
|
+
// find the conflicting element
|
|
239
|
+
for (const component of container.components) {
|
|
240
|
+
if (otherSubviews.containsElement(component)) {
|
|
241
|
+
throw new Error(`<${container.tagName}> cannot contain both <${component.tagName}> and <${child.tagName}>, for they don't share the same data lifecycle.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`Failed to add container <${container.tagName}> with child component <${child.tagName}>.`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// add the container to the subviews
|
|
248
|
+
subviews.addContainer(container);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_removeContainerFromSubviewsIfNecessary(container) {
|
|
252
|
+
if (!container || container.components.length > 0) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
for (const subviews of this._getAllSubviews()) {
|
|
256
|
+
subviews.removeContainer(container);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
125
260
|
}
|
|
@@ -73,23 +73,18 @@ export function writeAnswerStageToMeta(data) {
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function
|
|
77
|
-
if (!
|
|
78
|
-
return
|
|
76
|
+
export function composeFq(filters) {
|
|
77
|
+
if (!filters) {
|
|
78
|
+
return undefined;
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function removeDataInstructions(data) {
|
|
90
|
-
if (typeof data !== 'object') {
|
|
91
|
-
return data;
|
|
80
|
+
const clauses = [];
|
|
81
|
+
const { facets } = filters;
|
|
82
|
+
for (const field in facets) {
|
|
83
|
+
const values = facets[field];
|
|
84
|
+
if (!values || !values.length) {
|
|
85
|
+
continue; // just in case
|
|
86
|
+
}
|
|
87
|
+
clauses.push(`${field}:(${values.map(v => `"${v}"`).join(' OR ')})`);
|
|
92
88
|
}
|
|
93
|
-
|
|
94
|
-
return rest;
|
|
89
|
+
return clauses.map(c => `(${c})`).join(' AND ');
|
|
95
90
|
}
|