@miso.ai/client-sdk-ui 1.11.0-beta.1 → 1.11.0-beta.10

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.
Files changed (118) hide show
  1. package/dist/cjs/actor/data.js +6 -7
  2. package/dist/cjs/actor/feedback.js +6 -39
  3. package/dist/cjs/actor/fields.js +14 -6
  4. package/dist/cjs/actor/hub.js +1 -2
  5. package/dist/cjs/actor/index.js +0 -7
  6. package/dist/cjs/actor/interactions.js +4 -15
  7. package/dist/cjs/actor/sessions.js +2 -33
  8. package/dist/cjs/actor/tracker.js +25 -26
  9. package/dist/cjs/actor/utils.js +0 -49
  10. package/dist/cjs/actor/view.js +40 -40
  11. package/dist/cjs/actor/views.js +32 -8
  12. package/dist/cjs/asset/svgs.js +18 -8
  13. package/dist/cjs/constants.js +18 -19
  14. package/dist/cjs/defaults/hybrid-search/answer-box.js +19 -3
  15. package/dist/cjs/defaults/hybrid-search/phrases.js +5 -5
  16. package/dist/cjs/defaults/hybrid-search/templates.js +37 -9
  17. package/dist/cjs/element/role/index.js +12 -5
  18. package/dist/cjs/element/role/{miso-hits.js → miso-more.js} +4 -4
  19. package/dist/cjs/element/role/miso-total.js +20 -0
  20. package/dist/cjs/layout/affiliation/affiliation.js +0 -3
  21. package/dist/cjs/layout/banner/banner.js +0 -4
  22. package/dist/cjs/layout/button/more-button.js +73 -0
  23. package/dist/cjs/layout/container/container.js +0 -3
  24. package/dist/cjs/layout/error/error.js +0 -4
  25. package/dist/cjs/layout/index.js +7 -0
  26. package/dist/cjs/layout/input/facets.js +5 -5
  27. package/dist/cjs/layout/input/option-list.js +0 -4
  28. package/dist/cjs/layout/input/search-box.js +33 -12
  29. package/dist/cjs/layout/list/cards.js +0 -3
  30. package/dist/cjs/layout/list/carousel.js +0 -3
  31. package/dist/cjs/layout/list/collection.js +25 -7
  32. package/dist/cjs/layout/list/list.js +0 -3
  33. package/dist/cjs/layout/radio/feedback.js +0 -3
  34. package/dist/cjs/layout/radio/radio.js +0 -4
  35. package/dist/cjs/layout/raf.js +4 -2
  36. package/dist/cjs/layout/text/text.js +0 -4
  37. package/dist/cjs/layout/text/typewriter/index.js +0 -3
  38. package/dist/cjs/layout/triggerable.js +79 -0
  39. package/dist/cjs/util/states.js +0 -15
  40. package/dist/cjs/version.js +1 -1
  41. package/dist/cjs/workflow/answer-based.js +109 -111
  42. package/dist/cjs/workflow/ask.js +35 -27
  43. package/dist/cjs/workflow/asks.js +1 -1
  44. package/dist/cjs/workflow/autocompletable.js +33 -0
  45. package/dist/cjs/workflow/autocomplete.js +122 -0
  46. package/dist/cjs/workflow/base.js +169 -102
  47. package/dist/cjs/workflow/explore.js +26 -19
  48. package/dist/cjs/workflow/hybrid-search-answer.js +99 -0
  49. package/dist/cjs/workflow/hybrid-search-results.js +70 -0
  50. package/dist/cjs/workflow/hybrid-search-views.js +122 -0
  51. package/dist/cjs/workflow/hybrid-search.js +98 -103
  52. package/dist/cjs/workflow/options.js +32 -17
  53. package/dist/cjs/workflow/processors.js +185 -17
  54. package/dist/cjs/workflow/recommendation.js +36 -49
  55. package/dist/cjs/workflow/search-based.js +186 -0
  56. package/dist/cjs/workflow/search.js +21 -68
  57. package/package.json +2 -2
  58. package/src/actor/data.js +2 -4
  59. package/src/actor/feedback.js +6 -23
  60. package/src/actor/fields.js +14 -6
  61. package/src/actor/hub.js +1 -2
  62. package/src/actor/index.js +0 -1
  63. package/src/actor/interactions.js +4 -10
  64. package/src/actor/sessions.js +3 -30
  65. package/src/actor/tracker.js +25 -19
  66. package/src/actor/utils.js +0 -47
  67. package/src/actor/view.js +27 -15
  68. package/src/actor/views.js +36 -8
  69. package/src/asset/svgs.js +19 -7
  70. package/src/constants.js +25 -16
  71. package/src/defaults/hybrid-search/answer-box.js +17 -5
  72. package/src/defaults/hybrid-search/phrases.js +4 -4
  73. package/src/defaults/hybrid-search/templates.js +25 -9
  74. package/src/element/role/index.js +2 -1
  75. package/src/element/role/{miso-hits.js → miso-more.js} +3 -3
  76. package/src/element/role/miso-total.js +16 -0
  77. package/src/layout/affiliation/affiliation.js +1 -5
  78. package/src/layout/banner/banner.js +0 -5
  79. package/src/layout/button/more-button.js +69 -0
  80. package/src/layout/container/container.js +1 -5
  81. package/src/layout/error/error.js +0 -5
  82. package/src/layout/index.js +2 -1
  83. package/src/layout/input/facets.js +5 -6
  84. package/src/layout/input/option-list.js +0 -5
  85. package/src/layout/input/search-box.js +27 -14
  86. package/src/layout/list/cards.js +0 -4
  87. package/src/layout/list/carousel.js +0 -4
  88. package/src/layout/list/collection.js +22 -9
  89. package/src/layout/list/list.js +0 -4
  90. package/src/layout/radio/feedback.js +0 -4
  91. package/src/layout/radio/radio.js +0 -5
  92. package/src/layout/raf.js +2 -2
  93. package/src/layout/text/text.js +0 -5
  94. package/src/layout/text/typewriter/index.js +1 -5
  95. package/src/layout/trackable.js +1 -1
  96. package/src/layout/triggerable.js +76 -0
  97. package/src/util/states.js +0 -14
  98. package/src/version.js +1 -1
  99. package/src/workflow/answer-based.js +101 -77
  100. package/src/workflow/ask.js +33 -25
  101. package/src/workflow/asks.js +1 -1
  102. package/src/workflow/autocompletable.js +29 -0
  103. package/src/workflow/autocomplete.js +113 -0
  104. package/src/workflow/base.js +147 -93
  105. package/src/workflow/explore.js +23 -8
  106. package/src/workflow/hybrid-search-answer.js +92 -0
  107. package/src/workflow/hybrid-search-results.js +63 -0
  108. package/src/workflow/hybrid-search-views.js +124 -0
  109. package/src/workflow/hybrid-search.js +102 -73
  110. package/src/workflow/options.js +26 -11
  111. package/src/workflow/processors.js +165 -14
  112. package/src/workflow/recommendation.js +32 -33
  113. package/src/workflow/search-based.js +183 -0
  114. package/src/workflow/search.js +21 -65
  115. package/dist/cjs/actor/autocomplete.js +0 -160
  116. package/dist/cjs/workflow/utils.js +0 -42
  117. package/src/actor/autocomplete.js +0 -133
  118. package/src/workflow/utils.js +0 -29
@@ -1,21 +1,24 @@
1
1
  import { API } from '@miso.ai/commons';
2
2
  import Workflow from './base.js';
3
- import { mergeApiOptions } from './options.js';
4
3
  import { fields } from '../actor/index.js';
5
4
  import { ROLE, EVENT_TYPE } from '../constants.js';
6
5
  import { ListLayout, SearchBoxLayout } from '../layout/index.js';
6
+ import { mergeRolesOptions } from './options.js';
7
7
 
8
8
  const DEFAULT_API_OPTIONS = Object.freeze({
9
+ ...Workflow.DEFAULT_API_OPTIONS,
9
10
  group: API.GROUP.ASK,
10
11
  name: API.NAME.RELATED_QUESTIONS,
11
12
  });
12
13
 
13
14
  const DEFAULT_LAYOUTS = Object.freeze({
15
+ ...Workflow.DEFAULT_LAYOUTS,
14
16
  [ROLE.RELATED_QUESTIONS]: [ListLayout.type, { itemType: 'question', link: { rel: 'noopener nofollow' } }],
15
17
  [ROLE.QUERY]: [SearchBoxLayout.type, { placeholder: 'Ask a question' }],
16
18
  });
17
19
 
18
20
  const DEFAULT_TRACKERS = Object.freeze({
21
+ ...Workflow.DEFAULT_TRACKERS,
19
22
  [ROLE.RELATED_QUESTIONS]: {},
20
23
  [ROLE.CONTAINER]: {},
21
24
  [ROLE.QUERY]: {
@@ -24,11 +27,17 @@ const DEFAULT_TRACKERS = Object.freeze({
24
27
  });
25
28
 
26
29
  const DEFAULT_OPTIONS = Object.freeze({
30
+ ...Workflow.DEFAULT_OPTIONS,
27
31
  api: DEFAULT_API_OPTIONS,
28
32
  layouts: DEFAULT_LAYOUTS,
29
33
  trackers: DEFAULT_TRACKERS,
30
34
  });
31
35
 
36
+ const ROLES_OPTIONS = mergeRolesOptions(Workflow.ROLES_OPTIONS, {
37
+ main: ROLE.RELATED_QUESTIONS,
38
+ members: Object.keys(DEFAULT_LAYOUTS),
39
+ });
40
+
32
41
  export default class Explore extends Workflow {
33
42
 
34
43
  constructor(plugin, client) {
@@ -36,7 +45,7 @@ export default class Explore extends Workflow {
36
45
  name: 'explore',
37
46
  plugin,
38
47
  client,
39
- roles: Object.keys(DEFAULT_LAYOUTS),
48
+ roles: ROLES_OPTIONS,
40
49
  defaults: DEFAULT_OPTIONS,
41
50
  });
42
51
  }
@@ -52,7 +61,7 @@ export default class Explore extends Workflow {
52
61
  this._unsubscribes = [
53
62
  ...this._unsubscribes,
54
63
  this._views.get(ROLE.RELATED_QUESTIONS).on('click', event => this._handleRelatedQuestionClick(event)),
55
- this._hub.on(fields.query(), payload => this.query(payload)),
64
+ this._hub.on(fields.query(), args => this._submit(args)),
56
65
  ];
57
66
  }
58
67
 
@@ -82,17 +91,16 @@ export default class Explore extends Workflow {
82
91
 
83
92
  // lifecycle //
84
93
  start({ relatedQuestions = true } = {}) {
85
- if (relatedQuestions && this._linkFn === undefined) {
94
+ if (this._linkFn === undefined) {
86
95
  throw new Error('Define link mapping function before calling start()');
87
96
  }
88
- this._sessions.start();
89
97
  // in explore workflow, start() triggers query
90
98
  // TODO: we should still make the query lifecycle
91
- const { session } = this;
92
99
  if (relatedQuestions) {
93
- this._hub.update(fields.request(), mergeApiOptions(this._options.resolved.api, { session }));
100
+ this._request();
94
101
  } else {
95
102
  // TODO: ad-hoc
103
+ const { session } = this;
96
104
  this.updateData({ session, request: {}, value: { related_questions: [] } });
97
105
  }
98
106
  return this;
@@ -124,7 +132,14 @@ export default class Explore extends Workflow {
124
132
  this._events.emit('select', Object.freeze({ ...event, question }));
125
133
  }
126
134
 
127
- query({ q } = {}) {
135
+ query(args) {
136
+ if (!args.q) {
137
+ throw new Error(`q is required in query() call`);
138
+ }
139
+ this._hub.update(fields.query(), args);
140
+ }
141
+
142
+ _submit({ q } = {}) {
128
143
  if (!this._linkFn) {
129
144
  return;
130
145
  }
@@ -0,0 +1,92 @@
1
+ import AnswerBasedWorkflow from './answer-based.js';
2
+ import { ROLE, STATUS, ORGANIC_QUESTION_SOURCE } from '../constants.js';
3
+ import { mergeRolesOptions } from './options.js';
4
+ import { writeKeywordsToData } from './processors.js';
5
+ import { makeAutocompletable } from './autocompletable.js';
6
+
7
+ const ROLES_OPTIONS = mergeRolesOptions(AnswerBasedWorkflow.ROLES_OPTIONS, {
8
+ mappings: {
9
+ [ROLE.QUESTION]: ROLE.KEYWORDS,
10
+ },
11
+ });
12
+
13
+ export default class HybridSearchAnswer extends AnswerBasedWorkflow {
14
+
15
+ constructor(superworkflow) {
16
+ super({
17
+ name: 'hybrid-search/answer',
18
+ plugin: superworkflow._plugin,
19
+ client: superworkflow._client,
20
+ options: superworkflow._options,
21
+ defaults: superworkflow._defaults,
22
+ roles: ROLES_OPTIONS,
23
+ superworkflow,
24
+ });
25
+ }
26
+
27
+ _initProperties(args) {
28
+ super._initProperties(args);
29
+ this._initAutocomplete(args);
30
+ this._superworkflow = args.superworkflow;
31
+ }
32
+
33
+ _initSession() {} // no reset here, will manually reset later
34
+
35
+ restart() {
36
+ super.restart();
37
+ // cascade restart to the sibling
38
+ const results = this._superworkflow._results;
39
+ results._views.filters.reset();
40
+ results.restart();
41
+ this._resultsSession = results.session; // keep track of the session
42
+ }
43
+
44
+ // query //
45
+ _buildPayload({ q, qs, ...payload } = {}) {
46
+ return {
47
+ ...payload,
48
+ q, // q, not question
49
+ _meta: {
50
+ ...payload._meta,
51
+ question_source: qs || ORGANIC_QUESTION_SOURCE, // might be null, not undefined
52
+ },
53
+ };
54
+ }
55
+
56
+ // data //
57
+ _defaultProcessData(data) {
58
+ data = super._defaultProcessData(data);
59
+ data = writeKeywordsToData(data);
60
+ return data;
61
+ }
62
+
63
+ _updateDataInHub(data) {
64
+ switch (data.status) {
65
+ case STATUS.INITIAL:
66
+ case STATUS.LOADING:
67
+ // share the initial/loading status with its sibling
68
+ this._dispatchDataToSibling(data);
69
+ break;
70
+ }
71
+ super._updateDataInHub(data);
72
+ }
73
+
74
+ _handleHeadResponse(data) {
75
+ super._handleHeadResponse(data);
76
+ // share the search results with its sibling
77
+ this._dispatchDataToSibling(data);
78
+ }
79
+
80
+ _dispatchDataToSibling(data) {
81
+ this._resultsSession && this._superworkflow._results.updateData({ ...data, session: this._resultsSession });
82
+ }
83
+
84
+ // destroy //
85
+ _destroy(options) {
86
+ this._destroyAutocomplete();
87
+ super._destroy(options);
88
+ }
89
+
90
+ }
91
+
92
+ makeAutocompletable(HybridSearchAnswer.prototype);
@@ -0,0 +1,63 @@
1
+ import { trimObj } from '@miso.ai/commons';
2
+ import SearchBasedWorkflow from './search-based.js';
3
+ import { fields } from '../actor/index.js';
4
+ import { ROLE } from '../constants.js';
5
+ import { mergeRolesOptions } from './options.js';
6
+ import { writeFiltersToPayload } from './processors.js';
7
+
8
+ const ROLES_OPTIONS = mergeRolesOptions(SearchBasedWorkflow.ROLES_OPTIONS, {
9
+ members: [ROLE.PRODUCTS, ROLE.KEYWORDS, ROLE.TOTAL, ROLE.FACETS, ROLE.MORE],
10
+ });
11
+
12
+ export default class HybridSearchResults extends SearchBasedWorkflow {
13
+
14
+ constructor(superworkflow) {
15
+ super({
16
+ name: 'hybrid-search/results',
17
+ plugin: superworkflow._plugin,
18
+ client: superworkflow._client,
19
+ options: superworkflow._options,
20
+ defaults: superworkflow._defaults,
21
+ roles: ROLES_OPTIONS,
22
+ superworkflow,
23
+ });
24
+ }
25
+
26
+ _initProperties(args) {
27
+ super._initProperties(args);
28
+ this._superworkflow = args.superworkflow;
29
+ }
30
+
31
+ _initSession() {} // no reset here, will manually reset later
32
+
33
+ // lifecycle //
34
+ _emitLifecycleEvent(name, event) {
35
+ super._emitLifecycleEvent(name, event);
36
+ this._superworkflow._emitLifecycleEvent(name, event);
37
+ }
38
+
39
+ // query //
40
+ _getQuery() {
41
+ // get stored query from sibling
42
+ return this._superworkflow._answer._hub.states[fields.query()];
43
+ }
44
+
45
+ _buildPayload({ filters, ...payload } = {}) {
46
+ // borrow the work from the sibling
47
+ payload = this._superworkflow._answer._buildPayload(payload);
48
+ payload = writeFiltersToPayload(payload, filters);
49
+ payload = this._writeQuestionIdToPayload(payload);
50
+ return { ...payload, answer: false };
51
+ }
52
+
53
+ _writeQuestionIdToPayload(payload) {
54
+ return {
55
+ ...payload,
56
+ _meta: trimObj({
57
+ ...payload._meta,
58
+ question_id: this._superworkflow._answer.questionId,
59
+ }),
60
+ };
61
+ }
62
+
63
+ }
@@ -0,0 +1,124 @@
1
+ import { ROLE } from '../constants.js';
2
+
3
+ function getSubworkflowByRole(role) {
4
+ // TODO: ROLE.ERROR
5
+ switch (role) {
6
+ case ROLE.PRODUCTS:
7
+ case ROLE.TOTAL:
8
+ case ROLE.FACETS:
9
+ case ROLE.KEYWORDS:
10
+ case ROLE.MORE:
11
+ return 'results';
12
+ default:
13
+ return 'answer';
14
+ }
15
+ }
16
+
17
+ export default class HybridSearchViewsActor {
18
+
19
+ constructor(workflow) {
20
+ this._workflow = workflow;
21
+ this._containers = new Set();
22
+ }
23
+
24
+ get(role) {
25
+ return this._getSubviews(role).get(role);
26
+ }
27
+
28
+ addContainer(element) {
29
+ if (this._containers.has(element)) {
30
+ return;
31
+ }
32
+ this._containers.add(element);
33
+
34
+ const { components } = element;
35
+ for (const component of components) {
36
+ this.addComponent(component);
37
+ }
38
+ }
39
+
40
+ removeContainer(element) {
41
+ for (const subworkflow of this._workflow._subworkflows) {
42
+ subworkflow._views.removeContainer(element);
43
+ }
44
+ this._containers.delete(element);
45
+ }
46
+
47
+ addComponent(element) {
48
+ this._getViewByElement(element).element = element;
49
+ this._addContainerToSubviews(element._container, element);
50
+ }
51
+
52
+ removeComponent(element) {
53
+ this._getViewByElement(element).element = undefined;
54
+ this._removeContainerFromSubviewsIfNecessary(element._container);
55
+ }
56
+
57
+ updateComponentRole(element, oldRole, newRole) {
58
+ // TODO
59
+ }
60
+
61
+ refreshElement(element) {
62
+ const view = element.isContainer ? this._containers.get(element) : this._getViewByElement(element);
63
+ view && view.refresh({ force: true });
64
+ }
65
+
66
+ // helpers //
67
+ _getSubworkflow(role) {
68
+ return this._workflow._getSubworkflow(getSubworkflowByRole(role));
69
+ }
70
+
71
+ _getAllSubviews() {
72
+ return this._workflow._subworkflows.flatMap(subworkflow => subworkflow._views);
73
+ }
74
+
75
+ _getSubviews(role) {
76
+ return this._getSubworkflow(role)._views;
77
+ }
78
+
79
+ _getViewByElement(element) {
80
+ let { role } = element;
81
+ if (!role) {
82
+ throw new Error('Component must have a role');
83
+ }
84
+ return this.get(role);
85
+ }
86
+
87
+ _addContainerToSubviews(container, child) {
88
+ if (!container) {
89
+ return;
90
+ }
91
+ const { role } = child;
92
+ const subviews = this._getSubviews(role);
93
+ if (subviews._containers.has(container)) {
94
+ return;
95
+ }
96
+ // check if the container is in other subviews
97
+ for (const otherSubviews of this._getAllSubviews()) {
98
+ if (otherSubviews === subviews) {
99
+ continue;
100
+ }
101
+ if (otherSubviews._containers.has(container)) {
102
+ // find the conflicting element
103
+ for (const component of container.components) {
104
+ if (otherSubviews.containsElement(component)) {
105
+ throw new Error(`<${container.tagName}> cannot contain both <${component.tagName}> and <${child.tagName}>, for they don't share the same data lifecycle.`);
106
+ }
107
+ }
108
+ throw new Error(`Failed to add container <${container.tagName}> with child component <${child.tagName}>.`);
109
+ }
110
+ }
111
+ // add the container to the subviews
112
+ subviews.addContainer(container);
113
+ }
114
+
115
+ _removeContainerFromSubviewsIfNecessary(container) {
116
+ if (!container || container.components.length > 0) {
117
+ return;
118
+ }
119
+ for (const subviews of this._getAllSubviews()) {
120
+ subviews.removeContainer(container);
121
+ }
122
+ }
123
+
124
+ }
@@ -1,125 +1,154 @@
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';
4
+ import SearchBasedWorkflow from './search-based.js';
2
5
  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';
6
- import { ListLayout, TextLayout, FacetsLayout } from '../layout/index.js';
7
- import { writeKeywordsToData, addDataInstructions } from './processors.js';
6
+ import { ROLE } from '../constants.js';
7
+ import { SearchBoxLayout, TextLayout } from '../layout/index.js';
8
+ import HybridSearchAnswer from './hybrid-search-answer.js';
9
+ import HybridSearchResults from './hybrid-search-results.js';
10
+ import HybridSearchViewsActor from './hybrid-search-views.js';
8
11
 
9
12
  const DEFAULT_API_OPTIONS = Object.freeze({
10
- ...AnswerBasedWorkflow.DEFAULT_API_OPTIONS,
13
+ group: API.GROUP.ASK,
11
14
  name: API.NAME.SEARCH,
12
15
  payload: {
13
16
  ...AnswerBasedWorkflow.DEFAULT_API_OPTIONS.payload,
17
+ ...SearchBasedWorkflow.DEFAULT_API_OPTIONS.payload,
18
+ source_fl: ['cover_image', 'url', 'created_at', 'updated_at', 'published_at', 'title'],
14
19
  },
15
20
  });
16
21
 
17
22
  const DEFAULT_LAYOUTS = Object.freeze({
18
23
  ...AnswerBasedWorkflow.DEFAULT_LAYOUTS,
19
- [ROLE.PRODUCTS]: [ListLayout.type, { itemType: 'article' }],
20
- [ROLE.KEYWORDS]: [TextLayout.type, { raw: true }],
21
- [ROLE.HITS]: [TextLayout.type, { raw: true, format: 'number' }],
22
- [ROLE.FACETS]: [FacetsLayout.type],
24
+ ...SearchBasedWorkflow.DEFAULT_LAYOUTS,
25
+ [ROLE.QUERY]: [SearchBoxLayout.type, { placeholder: '' }],
26
+ [ROLE.QUESTION]: [TextLayout.type, { raw: true }],
23
27
  });
24
28
 
25
29
  const DEFAULT_TRACKERS = Object.freeze({
26
30
  ...AnswerBasedWorkflow.DEFAULT_TRACKERS,
31
+ ...SearchBasedWorkflow.DEFAULT_TRACKERS,
32
+ });
33
+
34
+ const DEFAULT_PAGINATION = Object.freeze({
35
+ ...SearchBasedWorkflow.DEFAULT_PAGINATION,
36
+ active: true,
37
+ });
38
+
39
+ const DEFAULT_AUTOCOMPLETE_OPTIONS = Object.freeze({
40
+ api: {
41
+ group: API.GROUP.ASK,
42
+ name: API.NAME.SEARCH_AUTOCOMPLETE,
43
+ payload: {
44
+ completion_fields: ['suggested_queries'],
45
+ },
46
+ },
27
47
  });
28
48
 
29
49
  const DEFAULT_OPTIONS = Object.freeze({
30
50
  ...AnswerBasedWorkflow.DEFAULT_OPTIONS,
51
+ ...SearchBasedWorkflow.DEFAULT_OPTIONS,
31
52
  api: DEFAULT_API_OPTIONS,
32
53
  layouts: DEFAULT_LAYOUTS,
33
54
  trackers: DEFAULT_TRACKERS,
55
+ autocomplete: DEFAULT_AUTOCOMPLETE_OPTIONS,
56
+ pagination: DEFAULT_PAGINATION,
34
57
  });
35
58
 
36
- const ROLES_CONFIG = Object.freeze({
37
- [ROLE.QUESTION]: {
38
- mapping: ROLE.KEYWORDS,
39
- },
40
- [ROLE.FACETS]: {
41
- mapping: 'facet_counts',
42
- },
59
+ const ROLES_OPTIONS = Object.freeze({
60
+ main: ROLE.PRODUCTS,
61
+ members: Object.keys(DEFAULT_LAYOUTS),
62
+ mappings: Object.freeze({
63
+ ...AnswerBasedWorkflow.ROLES_OPTIONS.mappings,
64
+ ...SearchBasedWorkflow.ROLES_OPTIONS.mappings,
65
+ }),
43
66
  });
44
67
 
45
- export default class HybridSearch extends AnswerBasedWorkflow {
68
+ export default class HybridSearch extends Workflow {
46
69
 
47
70
  constructor(plugin, client) {
48
71
  super({
49
72
  name: 'hybrid-search',
50
73
  plugin,
51
74
  client,
52
- roles: Object.keys(DEFAULT_LAYOUTS),
53
- rolesConfig: ROLES_CONFIG,
75
+ roles: ROLES_OPTIONS,
54
76
  defaults: DEFAULT_OPTIONS,
55
77
  });
56
78
  }
57
79
 
58
- _initSubscriptions(args) {
59
- super._initSubscriptions(args);
60
- this._unsubscribes = [
61
- ...this._unsubscribes,
62
- this._hub.on(fields.filters(), filters => this._refine(filters)),
80
+ _initProperties(args) {
81
+ super._initProperties(args);
82
+ this._subworkflows = [
83
+ this._answer = new HybridSearchAnswer(this),
84
+ this._results = new HybridSearchResults(this),
63
85
  ];
64
86
  }
65
87
 
66
- // 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
- });
88
+ _getSubworkflow(name) {
89
+ switch (name) {
90
+ case 'answer':
91
+ return this._answer;
92
+ case 'results':
93
+ return this._results;
94
+ default:
95
+ throw new Error(`Invalid subworkflow: ${name}`);
96
+ }
78
97
  }
79
98
 
80
- _buildFq(filters) {
81
- if (!filters) {
82
- return undefined;
83
- }
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 ')})`);
92
- }
93
- return clauses.map(c => `(${c})`).join(' AND ');
99
+ _initActors() {
100
+ const hub = this._hub;
101
+ const client = this._client;
102
+ const options = this._options;
103
+
104
+ this._views = new HybridSearchViewsActor(this);
105
+ this._interactions = new InteractionsActor(hub, { client, options });
94
106
  }
95
107
 
96
- _refine(filters) {
97
- const query = this._hub.states[fields.query()];
98
- const payload = this._buildPayload({ ...query, filters, answer: false });
99
- const { session } = this;
108
+ _initSession() {
109
+ this._results._sessions.restart();
110
+ this._answer._sessions.restart();
111
+ }
100
112
 
101
- const event = mergeApiOptions(this._options.resolved.api, { payload, session, _skipLoading: true });
102
- this._request(event);
113
+ // configuration //
114
+ useLink(fn) {
115
+ this._answer.useLink(fn);
116
+ return this;
117
+ }
103
118
 
104
- // emulate loading status exclusively for products
105
- // TODO
119
+ // properties //
120
+ get questionId() {
121
+ return this._answer.questionId;
106
122
  }
107
123
 
108
- // data //
109
- _defaultProcessData(data) {
110
- data = super._defaultProcessData(data);
111
- data = writeKeywordsToData(data);
112
- data = instructPartialUpdates(data);
113
- return data;
124
+ get filters() {
125
+ return this._results._views.filters;
114
126
  }
115
127
 
116
- }
128
+ get autocomplete() {
129
+ return this._answer.autocomplete;
130
+ }
131
+
132
+ // query //
133
+ autoQuery(options) {
134
+ this._answer.autoQuery(options);
135
+ }
117
136
 
118
- function instructPartialUpdates(data) {
119
- if (!data.value) {
120
- return data;
137
+ query(args) {
138
+ this._answer.query(args);
121
139
  }
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 });
140
+
141
+ // interactions //
142
+ _defaultProcessInteraction(payload, args) {
143
+ return this._answer._defaultProcessInteraction(payload, args);
144
+ }
145
+
146
+ // destroy //
147
+ _destroy() {
148
+ for (const subworkflow of this._subworkflows) {
149
+ subworkflow.destroy();
150
+ }
151
+ super._destroy();
152
+ }
153
+
125
154
  }
@@ -110,15 +110,14 @@ export function normalizeInteractionsOptions(options) {
110
110
  return { ...options, preprocess };
111
111
  }
112
112
 
113
- export function normalizeAutocompleteOptions(options) {
113
+ export function normalizePaginationOptions(options) {
114
114
  if (options === undefined) {
115
- throw new Error(`Expect autocomplete options to be an object or a boolean value: ${options}`);
115
+ throw new Error(`Expect pagination options to be an object or a boolean value: ${options}`);
116
116
  }
117
117
  if (typeof options === 'boolean') {
118
- options = { actor: options };
118
+ options = { active: options };
119
119
  }
120
- const processData = normalizeDataProcessorOptions(options.processData);
121
- return { ...options, processData };
120
+ return options;
122
121
  }
123
122
 
124
123
  // merge //
@@ -206,17 +205,33 @@ export function mergeInteractionsOptions(...optionsList) {
206
205
  }));
207
206
  }
208
207
 
209
- export function mergeAutocompleteOptions(...optionsList) {
208
+ export function mergePaginationOptions(...optionsList) {
210
209
  if (optionsList[optionsList.length - 1] === false) {
211
210
  return false;
212
211
  }
212
+ return mergeOptions(optionsList, (merged, options) => Object.assign(merged, options));
213
+ }
214
+
215
+ export function mergeAutocompleteOptions(...optionsList) {
213
216
  return mergeOptions(optionsList, (merged, options) => Object.assign(merged, {
214
217
  ...options,
215
218
  api: mergeApiOptions(merged.api, options.api),
216
- processData: mergeDataProcessorOptions(merged.processData, options.processData),
217
219
  }));
218
220
  }
219
221
 
222
+ export function mergeRolesOptions(base, overrides) {
223
+ return Object.freeze({
224
+ ...base,
225
+ ...overrides,
226
+ members: mergeRolesMembers(base.members, overrides.members),
227
+ mappings: Object.freeze({ ...base.mappings, ...overrides.mappings }),
228
+ });
229
+ }
230
+
231
+ function mergeRolesMembers(base = [], overrides = []) {
232
+ return [...new Set([...base, ...overrides])];
233
+ }
234
+
220
235
 
221
236
 
222
237
  // manifest //
@@ -247,9 +262,9 @@ const FEATURES = [
247
262
  merge: mergeInteractionsOptions,
248
263
  },
249
264
  {
250
- key: 'autocomplete',
251
- normalize: normalizeAutocompleteOptions,
252
- merge: mergeAutocompleteOptions,
265
+ key: 'pagination',
266
+ normalize: normalizePaginationOptions,
267
+ merge: mergePaginationOptions,
253
268
  },
254
269
  ];
255
270
 
@@ -272,7 +287,7 @@ export class WorkflowOptions {
272
287
 
273
288
  constructor(context = {}, defaults = {}) {
274
289
  this._events = new EventEmitter({ target: this });
275
- this._defaults = defaults;
290
+ this._defaults = defaults; // TODO: we should call normalize() on defaults
276
291
  this._context = context || {};
277
292
  this._locals = {};
278
293