@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.
@@ -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 { trimObj, API } from '@miso.ai/commons';
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 { mergeApiOptions } from './options.js';
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 { writeKeywordsToData, addDataInstructions } from './processors.js';
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
- export default class HybridSearch extends AnswerBasedWorkflow {
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
- _initSubscriptions(args) {
59
- super._initSubscriptions(args);
60
- this._unsubscribes = [
61
- ...this._unsubscribes,
62
- this._hub.on(fields.filters(), filters => this._refine(filters)),
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
- _buildPayload({ q, qs, filters, ...payload } = {}) {
68
- const fq = this._buildFq(filters); // TODO: combine with fq in payload?
69
- return trimObj({
70
- ...payload,
71
- q, // q, not question
72
- fq,
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
- _buildFq(filters) {
81
- if (!filters) {
82
- return undefined;
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
- const clauses = [];
85
- const { facets } = filters;
86
- for (const field in facets) {
87
- const values = facets[field];
88
- if (!values || !values.length) {
89
- continue; // just in case
90
- }
91
- clauses.push(`${field}:(${values.map(v => `"${v}"`).join(' OR ')})`);
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
- _refine(filters) {
97
- const query = this._hub.states[fields.query()];
98
- const payload = this._buildPayload({ ...query, filters, answer: false });
99
- const { session } = this;
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
- const event = mergeApiOptions(this._options.resolved.api, { payload, session, _skipLoading: true });
102
- this._request(event);
188
+ removeComponent(element) {
189
+ this._getViewByElement(element).element = undefined;
190
+ this._removeContainerFromSubviewsIfNecessary(element._container);
191
+ }
103
192
 
104
- // emulate loading status exclusively for products
193
+ updateComponentRole(element, oldRole, newRole) {
105
194
  // TODO
106
195
  }
107
196
 
108
- // data //
109
- _defaultProcessData(data) {
110
- data = super._defaultProcessData(data);
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
- function instructPartialUpdates(data) {
119
- if (!data.value) {
120
- return data;
211
+ _getSubviews(role) {
212
+ return this._getSubworkflow(role)._views;
121
213
  }
122
- return addDataInstructions(data, data.value.products ?
123
- { includes: [ ROLE.PRODUCTS, ROLE.HITS, ROLE.FACETS, ROLE.KEYWORDS, ROLE.QUESTION ], merge: true } :
124
- { excludes: [ ROLE.PRODUCTS, ROLE.HITS, ROLE.FACETS ], merge: true });
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 addDataInstructions(data, instructions) {
77
- if (!instructions) {
78
- return data;
76
+ export function composeFq(filters) {
77
+ if (!filters) {
78
+ return undefined;
79
79
  }
80
- return {
81
- _inst: {
82
- ...data._inst,
83
- ...instructions,
84
- },
85
- ...data,
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
- const { _inst, ...rest } = data;
94
- return rest;
89
+ return clauses.map(c => `(${c})`).join(' AND ');
95
90
  }