@miso.ai/client-sdk-ui 1.11.0-beta.2 → 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,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,17 +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, composeFq } 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
- source_fl: [...(AnswerBasedWorkflow.DEFAULT_API_OPTIONS.payload.source_fl || []), 'title', 'snippet'],
14
+ fl: ['cover_image', 'url', 'created_at', 'updated_at', 'published_at'],
15
15
  },
16
16
  });
17
17
 
@@ -43,7 +43,25 @@ const ROLES_CONFIG = Object.freeze({
43
43
  },
44
44
  });
45
45
 
46
- 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 {
47
65
 
48
66
  constructor(plugin, client) {
49
67
  super({
@@ -56,55 +74,187 @@ export default class HybridSearch extends AnswerBasedWorkflow {
56
74
  });
57
75
  }
58
76
 
59
- _initSubscriptions(args) {
60
- super._initSubscriptions(args);
61
- this._unsubscribes = [
62
- ...this._unsubscribes,
63
- 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),
64
82
  ];
65
83
  }
66
84
 
67
- // query //
68
- _buildPayload({ q, qs, filters, ...payload } = {}) {
69
- const fq = composeFq(filters); // TODO: combine with fq in payload?
70
- return trimObj({
71
- ...payload,
72
- q, // q, not question
73
- fq,
74
- _meta: {
75
- ...payload._meta,
76
- question_source: qs || ORGANIC_QUESTION_SOURCE, // might be null, not undefined
77
- },
78
- });
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
+ }
79
94
  }
80
95
 
81
- _refine(filters) {
82
- const query = this._hub.states[fields.query()];
83
- const payload = this._buildPayload({ ...query, filters, answer: false });
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();
84
113
  const { session } = this;
114
+ if (!answer) {
115
+ session._answer = previousSession._answer || previousSession;
116
+ }
117
+ return this;
118
+ }
85
119
 
86
- const event = mergeApiOptions(this._options.resolved.api, { payload, session, _skipLoading: true });
87
- this._request(event);
120
+ // properties //
121
+ get questionId() {
122
+ return this._answer.questionId;
123
+ }
88
124
 
89
- // emulate loading status exclusively for products
90
- // TODO
125
+ get filters() {
126
+ return this._results._views.filters;
127
+ }
128
+
129
+ // query //
130
+ autoQuery(options) {
131
+ this._answer.autoQuery(options);
132
+ }
133
+
134
+ query(args) {
135
+ this._answer.query(args);
91
136
  }
92
137
 
93
- // data //
94
- _defaultProcessData(data) {
95
- data = super._defaultProcessData(data);
96
- data = writeKeywordsToData(data);
97
- data = instructPartialUpdates(data);
98
- return data;
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();
147
+ }
148
+ super._destroy();
99
149
  }
100
150
 
101
151
  }
102
152
 
103
- function instructPartialUpdates(data) {
104
- if (!data.value) {
105
- return data;
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);
173
+ }
174
+ }
175
+
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
+ }
187
+
188
+ removeComponent(element) {
189
+ this._getViewByElement(element).element = undefined;
190
+ this._removeContainerFromSubviewsIfNecessary(element._container);
191
+ }
192
+
193
+ updateComponentRole(element, oldRole, newRole) {
194
+ // TODO
195
+ }
196
+
197
+ refreshElement(element) {
198
+ const view = element.isContainer ? this._containers.get(element) : this._getViewByElement(element);
199
+ view && view.refresh({ force: true });
106
200
  }
107
- return addDataInstructions(data, data.value.products ?
108
- { includes: [ ROLE.PRODUCTS, ROLE.HITS, ROLE.FACETS, ROLE.KEYWORDS, ROLE.QUESTION ], merge: true } :
109
- { excludes: [ ROLE.PRODUCTS, ROLE.HITS, ROLE.FACETS ], merge: true });
201
+
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
+ }
210
+
211
+ _getSubviews(role) {
212
+ return this._getSubworkflow(role)._views;
213
+ }
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
+
110
260
  }
@@ -73,27 +73,6 @@ export function writeAnswerStageToMeta(data) {
73
73
  };
74
74
  }
75
75
 
76
- export function addDataInstructions(data, instructions) {
77
- if (!instructions) {
78
- return data;
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;
92
- }
93
- const { _inst, ...rest } = data;
94
- return rest;
95
- }
96
-
97
76
  export function composeFq(filters) {
98
77
  if (!filters) {
99
78
  return undefined;