@miso.ai/client-sdk-ui 1.10.0-beta.1 → 1.10.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 (78) hide show
  1. package/dist/cjs/actor/data.js +1 -1
  2. package/dist/cjs/actor/tracker.js +46 -70
  3. package/dist/cjs/actor/trackers.js +5 -1
  4. package/dist/cjs/actor/utils.js +6 -2
  5. package/dist/cjs/actor/view.js +14 -3
  6. package/dist/cjs/actor/views.js +9 -1
  7. package/dist/cjs/affiliation/index.js +126 -0
  8. package/dist/cjs/combo/ask/options.js +1 -1
  9. package/dist/cjs/combo/base.js +1 -1
  10. package/dist/cjs/constants.js +6 -3
  11. package/dist/cjs/defaults/ask/controls.js +1 -1
  12. package/dist/cjs/defaults/ask/phrases.js +1 -1
  13. package/dist/cjs/defaults/ask/templates.js +14 -4
  14. package/dist/cjs/element/role/index.js +7 -0
  15. package/dist/cjs/element/role/miso-affiliation.js +20 -0
  16. package/dist/cjs/layout/affiliation/affiliation.js +330 -0
  17. package/dist/cjs/layout/banner/banner.js +1 -1
  18. package/dist/cjs/layout/container/container.js +1 -1
  19. package/dist/cjs/layout/error/error.js +1 -1
  20. package/dist/cjs/layout/index.js +7 -0
  21. package/dist/cjs/layout/input/option-list.js +35 -50
  22. package/dist/cjs/layout/input/search-box.js +17 -5
  23. package/dist/cjs/layout/list/cards.js +1 -1
  24. package/dist/cjs/layout/list/carousel.js +5 -3
  25. package/dist/cjs/layout/list/collection.js +38 -48
  26. package/dist/cjs/layout/list/list.js +1 -1
  27. package/dist/cjs/layout/radio/feedback.js +1 -1
  28. package/dist/cjs/layout/radio/radio.js +1 -1
  29. package/dist/cjs/layout/raf.js +1 -1
  30. package/dist/cjs/layout/template.js +1 -1
  31. package/dist/cjs/layout/templates.js +135 -44
  32. package/dist/cjs/layout/text/text.js +1 -1
  33. package/dist/cjs/layout/text/typewriter/index.js +3 -3
  34. package/dist/cjs/layout/trackable.js +124 -0
  35. package/dist/cjs/plugin.js +1 -1
  36. package/dist/cjs/util/bindings.js +58 -110
  37. package/dist/cjs/util/states.js +31 -24
  38. package/dist/cjs/util/trackers.js +75 -0
  39. package/dist/cjs/util/viewables.js +55 -0
  40. package/dist/cjs/version.js +1 -1
  41. package/dist/cjs/workflow/ask.js +14 -5
  42. package/dist/cjs/workflow/asks.js +1 -1
  43. package/dist/cjs/workflow/base.js +5 -5
  44. package/dist/cjs/workflow/explore.js +1 -1
  45. package/dist/cjs/workflow/options.js +6 -1
  46. package/dist/cjs/workflow/recommendation.js +2 -0
  47. package/package.json +2 -2
  48. package/src/actor/tracker.js +39 -62
  49. package/src/actor/trackers.js +3 -1
  50. package/src/actor/utils.js +6 -2
  51. package/src/actor/view.js +12 -1
  52. package/src/actor/views.js +11 -0
  53. package/src/affiliation/index.js +108 -0
  54. package/src/constants.js +10 -1
  55. package/src/defaults/ask/phrases.js +1 -1
  56. package/src/defaults/ask/templates.js +13 -2
  57. package/src/element/role/index.js +1 -0
  58. package/src/element/role/miso-affiliation.js +16 -0
  59. package/src/layout/affiliation/affiliation.js +295 -0
  60. package/src/layout/index.js +1 -0
  61. package/src/layout/input/option-list.js +35 -43
  62. package/src/layout/input/search-box.js +7 -2
  63. package/src/layout/list/carousel.js +4 -2
  64. package/src/layout/list/collection.js +37 -43
  65. package/src/layout/templates.js +101 -26
  66. package/src/layout/text/typewriter/index.js +2 -2
  67. package/src/layout/trackable.js +106 -0
  68. package/src/util/bindings.js +47 -103
  69. package/src/util/states.js +26 -20
  70. package/src/util/trackers.js +69 -0
  71. package/src/util/viewables.js +47 -0
  72. package/src/version.js +1 -1
  73. package/src/workflow/ask.js +7 -3
  74. package/src/workflow/base.js +1 -1
  75. package/src/workflow/options.js +5 -1
  76. package/src/workflow/recommendation.js +1 -0
  77. package/dist/cjs/util/tracker.js +0 -348
  78. package/src/util/tracker.js +0 -318
@@ -1,94 +1,63 @@
1
- import { STATUS, ATTR_DATA_MISO_PRODUCT_ID } from '../constants.js';
1
+ import { asArray } from '@miso.ai/commons';
2
+ import { EVENT_TYPE, TRACKING_EVENT_TYPES, TRACKING_STATUS, validateEventType } from '../constants.js';
2
3
  import * as fields from './fields.js';
3
- import _Tracker from '../util/tracker.js';
4
- import { fallbackListFunction } from '../util/bindings.js';
4
+ import States from '../util/states.js';
5
+ import { normalizeTrackerOptions } from '../util/trackers.js';
5
6
  import { toInteraction } from './utils.js';
6
7
 
7
- function isReady(viewState) {
8
- return viewState && viewState.status === STATUS.READY;
9
- }
8
+ const { IMPRESSION, VIEWABLE, CLICK } = EVENT_TYPE;
10
9
 
11
10
  export default class Tracker {
12
11
 
13
12
  constructor(hub, view, { active, bindings, ...options } = {}) {
14
13
  this._hub = hub;
15
14
  this._view = view;
16
- this._bindings = bindings;
17
- const role = this._role = view.role;
15
+ this._role = view.role;
18
16
  // TODO: know item type
19
17
 
20
- this._tracker = new _Tracker({
21
- bindings: {
22
- list: element => this._listBindingsFn()(element),
23
- },
24
- proxyElement: view.proxyElement,
25
- sessionId: () => this._getSessionId(),
26
- active: active || (() => this._isViewReady()),
27
- ...options,
28
- });
18
+ this._states = undefined;
19
+ this.config(options);
29
20
 
30
21
  this._unsubscribes = [
31
- this._tracker.on('*', data => this._handleEvent(data)),
32
- hub.on(fields.view(role), () => this.refresh()),
22
+ ...TRACKING_EVENT_TYPES.map(type => this._view.trackingEvents.on(type, values => this._trigger(type, values))),
23
+ hub.on(fields.view(view.role), () => this._syncSession()),
33
24
  ];
34
25
 
35
- this.refresh();
26
+ this._syncSession();
36
27
  }
37
28
 
38
29
  config(options) {
39
- this._tracker.config(options);
30
+ this._options = normalizeTrackerOptions(options);
40
31
  }
41
32
 
42
33
  start() {
43
34
  throw new Error(`workflow.tracker.start() is deprecated. Use workflow.startTracker() instead.`);
44
35
  }
45
36
 
46
- refresh() {
47
- this._tracker.refresh();
48
- }
49
-
50
- impression(productIds, options) {
51
- this._assertViewReady();
52
- this._tracker.impression(productIds.map(product_id => ({ product_id })), options);
37
+ impression(items, options) {
38
+ this._trigger(IMPRESSION, items, true);
53
39
  }
54
40
 
55
- viewable(productIds, options) {
56
- this._assertViewReady();
57
- this._tracker.viewable(productIds.map(product_id => ({ product_id })), options);
41
+ viewable(items, options) {
42
+ this._trigger(VIEWABLE, items, true);
58
43
  }
59
44
 
60
- click(productIds, options) {
61
- this._assertViewReady();
62
- this._tracker.click(productIds.map(product_id => ({ product_id })), options);
63
- }
64
-
65
- _listBindingsFn() {
66
- if (this._bindings && typeof this._bindings.list === 'function') {
67
- return this._bindings.list;
68
- }
69
- const { layout } = this._view;
70
- if (layout && layout.bindings && typeof layout.bindings.list === 'function') {
71
- return layout.bindings.list;
72
- }
73
- // TODO: only if item is product typed
74
- return fallbackListFunction(ATTR_DATA_MISO_PRODUCT_ID);
75
- }
76
-
77
- _assertViewReady() {
78
- if (!this._hub.active) {
79
- throw new Error(`Workflow is not active. Call workflow.start() to activate it.`);
80
- }
81
- if (!isReady(this._getViewState())) {
82
- throw new Error(`Workflow is not rendered yet. If you handle rendering by yourself, call workflow.notifyViewUpdate() when DOM is ready.`);
83
- }
45
+ click(items, options) {
46
+ this._trigger(CLICK, items, true);
84
47
  }
85
48
 
86
49
  _getViewState() {
87
50
  return this._hub.states[fields.view(this._role)];
88
51
  }
89
52
 
90
- _isViewReady() {
91
- return this._hub.active && isReady(this._getViewState());
53
+ _syncSession() {
54
+ const sessionId = this._getSessionId();
55
+ if (!sessionId) {
56
+ return;
57
+ }
58
+ if (!this._states || sessionId !== this._states.sessionId) {
59
+ this._states = new States(sessionId);
60
+ }
92
61
  }
93
62
 
94
63
  _getSessionId() {
@@ -106,16 +75,25 @@ export default class Tracker {
106
75
  return request && request.payload;
107
76
  }
108
77
 
109
- getState(productId) {
110
- return this._tracker.getState(productId);
78
+ getState(item) {
79
+ return this._states.getFullState(item);
111
80
  }
112
81
 
113
- _handleEvent(data) {
82
+ _trigger(type, items, manual = false) {
83
+ validateEventType(type);
84
+
85
+ items = this._states.untriggered(asArray(items), type);
86
+ if (items.length === 0) {
87
+ return;
88
+ }
89
+ this._states.set(items, type, TRACKING_STATUS.TRIGGERED);
90
+
114
91
  const property = this._role === 'results' ? 'products' : this._role; // TODO: ad-hoc, see #83
115
92
  const misoId = this._getMisoId();
116
93
  const request = this._getRequestPayload();
94
+
117
95
  // TODO: should trigger 'tracker' and let workflow translate to interactions
118
- this._hub.trigger(fields.interaction(), toInteraction({ property, misoId, request }, data));
96
+ this._hub.trigger(fields.interaction(), toInteraction({ property, misoId, request }, { event: type, values: items, manual }));
119
97
  }
120
98
 
121
99
  _destroy() {
@@ -123,7 +101,6 @@ export default class Tracker {
123
101
  unsubscribe();
124
102
  }
125
103
  this._unsubscribes = [];
126
- this._tracker.destroy();
127
104
  }
128
105
 
129
106
  }
@@ -12,7 +12,9 @@ export default class Trackers {
12
12
  }
13
13
 
14
14
  _syncConfig() {
15
- for (const [role, options] of Object.entries(this._options.resolved.trackers)) {
15
+ // TODO: trackers itself can be falsy
16
+ const { trackers } = this._options.resolved;
17
+ for (const [role, options] of Object.entries(trackers)) {
16
18
  this._createOrConfigTracker(role, options);
17
19
  }
18
20
  }
@@ -22,8 +22,10 @@ export function toInteraction({ property, misoId, request }, { event, values, ma
22
22
  // TODO: ad-hoc
23
23
  const question_source = request && request._meta && request._meta.question_source;
24
24
  const isProductType = isProduct(property);
25
- const product_ids = isProductType ? values.map(v => v.product_id).filter(v => v) : [];
26
- const items = isProductType ? undefined : values.map(v => v.text || v);
25
+ const product_ids = isProductType ? values.map(v => typeof v === 'string' ? v : v.product_id).filter(v => v) : [];
26
+ const items = isProductType ? undefined : values.map(v => v.text || v.id || v);
27
+ const channel = property === ROLE.AFFILIATION ? values[0] && values[0].channel : undefined;
28
+ const positions = property === ROLE.AFFILIATION ? values.map(v => v.position) : undefined;
27
29
  const payload = trimObj({
28
30
  type: event === 'viewable' ? 'viewable_impression' : event,
29
31
  product_ids,
@@ -32,7 +34,9 @@ export function toInteraction({ property, misoId, request }, { event, values, ma
32
34
  custom_context: trimObj({
33
35
  api_ts,
34
36
  property,
37
+ channel,
35
38
  items,
39
+ positions,
36
40
  trigger: manual ? 'manual' : 'auto',
37
41
  question_source,
38
42
  }),
package/src/actor/view.js CHANGED
@@ -21,10 +21,13 @@ export default class ViewActor {
21
21
  constructor(views, role) {
22
22
  this._events = new EventEmitter({ target: this });
23
23
  this._views = views;
24
+ const trackingEvents = {};
25
+ this._trackingEvents = new EventEmitter({ target: trackingEvents });
24
26
 
25
27
  defineValues(this, {
26
28
  role,
27
29
  interface: new View(this),
30
+ trackingEvents,
28
31
  });
29
32
  }
30
33
 
@@ -134,6 +137,14 @@ export default class ViewActor {
134
137
  this.hub.update(fields.view(role), state, { silent });
135
138
  }
136
139
 
140
+ get trackerOptions() {
141
+ return this._views._getTrackerOptions(this.role);
142
+ }
143
+
144
+ _track(type, items) {
145
+ this._trackingEvents.emit(type, items);
146
+ }
147
+
137
148
  _sliceData({ value, error, status, meta, ...rest }) {
138
149
  const sliced = {
139
150
  value: this.role === ROLE.ERROR ? error : (value && value[this.role]),
@@ -205,7 +216,7 @@ export default class ViewActor {
205
216
  class View {
206
217
 
207
218
  constructor(actor) {
208
- delegateGetters(this, actor, ['role', 'layout', 'element', 'proxyElement', 'refresh', 'on']);
219
+ delegateGetters(this, actor, ['role', 'layout', 'element', 'proxyElement', 'refresh', 'on', 'trackingEvents']);
209
220
  }
210
221
 
211
222
  }
@@ -2,6 +2,7 @@ import { defineValues, delegateGetters } from '@miso.ai/commons';
2
2
  import { STATUS, ROLE } from '../constants.js';
3
3
  import * as fields from './fields.js';
4
4
  import ViewActor from './view.js';
5
+ import { normalizeTrackerOptions } from '../util/trackers.js';
5
6
 
6
7
  export default class ViewsActor {
7
8
 
@@ -34,9 +35,11 @@ export default class ViewsActor {
34
35
  this._unsubscribes = [
35
36
  () => window.removeEventListener('resize', syncSize),
36
37
  hub.on(fields.data(), () => this.refresh()),
38
+ options.on('trackers', () => this._syncTrackers()),
37
39
  options.on('layouts', () => this._syncLayouts()),
38
40
  ];
39
41
 
42
+ this._syncTrackers();
40
43
  this._syncLayouts();
41
44
  }
42
45
 
@@ -185,6 +188,14 @@ export default class ViewsActor {
185
188
  return this._data;
186
189
  }
187
190
 
191
+ _syncTrackers() {
192
+ this._trackerOptions = this._options.resolved.trackers;
193
+ }
194
+
195
+ _getTrackerOptions(role) {
196
+ return normalizeTrackerOptions((this._trackerOptions && this._trackerOptions[role]) || false);
197
+ }
198
+
188
199
  _error(e) {
189
200
  // TODO: hub trigger error event
190
201
  console.error(e);
@@ -0,0 +1,108 @@
1
+ import { trimObj } from '@miso.ai/commons';
2
+
3
+ export function processData(data) {
4
+ if (!data || !data.value) {
5
+ return data;
6
+ }
7
+ const { value } = data;
8
+ return {
9
+ ...data,
10
+ value: processValue(value)
11
+ };
12
+ }
13
+
14
+ function processValue(value) {
15
+ if (value.sovrn_aff) {
16
+ const { sovrn_aff, ...rest } = value;
17
+ value = {
18
+ ...rest,
19
+ affiliation: processSovrnData(sovrn_aff),
20
+ }
21
+ }
22
+ return postProcessValue(value);
23
+ }
24
+
25
+ function postProcessValue(value) {
26
+ const { affiliation } = value;
27
+ if (!affiliation) {
28
+ return value;
29
+ }
30
+ let { channel, products } = affiliation;
31
+ if (!products) {
32
+ return value;
33
+ }
34
+ products = products.map(product => ({ ...product }));
35
+ let i = 0;
36
+ for (const product of products) {
37
+ if (channel) {
38
+ product.channel = channel;
39
+ }
40
+ product.position = i++;
41
+ }
42
+ return {
43
+ ...value,
44
+ affiliation: {
45
+ ...affiliation,
46
+ products,
47
+ },
48
+ };
49
+ }
50
+
51
+ function processSovrnData({ products, ...sovrn_aff }) {
52
+ return {
53
+ channel: 'sovrn',
54
+ ...sovrn_aff,
55
+ products: products.map(processSovrnProduct),
56
+ };
57
+ }
58
+
59
+ export function processSovrnProduct({
60
+ id,
61
+ deeplink: url,
62
+ headline: title,
63
+ description,
64
+ currency,
65
+ retailPrice: original_price,
66
+ salePrice: sale_price,
67
+ discountRate: discount_rate_percent,
68
+ image: cover_image,
69
+ thumbnail,
70
+ merchant,
71
+ ...product
72
+ }) {
73
+ const brand = merchant && merchant.name;
74
+ const brand_logo = merchant && merchant.logo;
75
+ return trimObj({
76
+ id: `sovrn-${id}`,
77
+ title,
78
+ description,
79
+ cover_image,
80
+ url,
81
+ currency,
82
+ original_price,
83
+ sale_price,
84
+ discount_rate_percent,
85
+ brand,
86
+ brand_logo,
87
+ custom_attributes: flatten({
88
+ sovrn_id: id,
89
+ thumbnail,
90
+ merchant,
91
+ ...product,
92
+ }),
93
+ });
94
+ }
95
+
96
+ function flatten(obj, prefix = '') {
97
+ return Object.keys(obj).reduce((acc, key) => {
98
+ const value = obj[key];
99
+ if (value === undefined) {
100
+ return acc;
101
+ }
102
+ const prefixedKey = prefix ? `${prefix}_${key}` : key;
103
+ if (typeof value === 'object') {
104
+ return { ...acc, ...flatten(value, prefixedKey) };
105
+ }
106
+ return { ...acc, [prefixedKey]: value };
107
+ }, {});
108
+ }
package/src/constants.js CHANGED
@@ -19,6 +19,7 @@ export const ROLE = Object.freeze({
19
19
  RELATED_RESOURCES: 'related_resources',
20
20
  QUERY_SUGGESTIONS: 'query_suggestions',
21
21
  RELATED_QUESTIONS: 'related_questions',
22
+ AFFILIATION: 'affiliation',
22
23
  ERROR: 'error',
23
24
  });
24
25
 
@@ -34,6 +35,7 @@ const DATA_ROLE_SET = new Set([
34
35
  ROLE.RELATED_RESOURCES,
35
36
  ROLE.QUERY_SUGGESTIONS,
36
37
  ROLE.RELATED_QUESTIONS,
38
+ ROLE.AFFILIATION,
37
39
  ]);
38
40
 
39
41
  export function isDataRole(role) {
@@ -47,6 +49,7 @@ export const LAYOUT_CATEGORY = Object.freeze({
47
49
  QUERY: 'query',
48
50
  RADIO: 'radio',
49
51
  TEXT: 'text',
52
+ AFFILIATION: 'affiliation',
50
53
  ERROR: 'error',
51
54
  });
52
55
 
@@ -56,6 +59,12 @@ export const EVENT_TYPE = Object.freeze({
56
59
  CLICK: 'click',
57
60
  });
58
61
 
62
+ export const TRACKING_EVENT_TYPES = [
63
+ EVENT_TYPE.IMPRESSION,
64
+ EVENT_TYPE.VIEWABLE,
65
+ EVENT_TYPE.CLICK,
66
+ ];
67
+
59
68
  export const TRACKING_STATUS = Object.freeze({
60
69
  UNTRACKED: 'untracked',
61
70
  TRACKING: 'tracking',
@@ -74,7 +83,7 @@ export const STATUS = Object.freeze({
74
83
  });
75
84
 
76
85
  export function validateEventType(value) {
77
- if (value !== EVENT_TYPE.IMPRESSION && value !== EVENT_TYPE.VIEWABLE && value !== EVENT_TYPE.CLICK) {
86
+ if (TRACKING_EVENT_TYPES.indexOf(value) < 0) {
78
87
  throw new Error(`Unrecognized event type: ${value}`);
79
88
  }
80
89
  }
@@ -6,7 +6,7 @@ export const question = 'You asked...';
6
6
  /**
7
7
  * The phrase displayed right before <miso-sources>
8
8
  */
9
- export const sources = 'My reply is based on the following';
9
+ export const sources = 'This reply is based on the following articles';
10
10
 
11
11
  /**
12
12
  * The phrase displayed right before <miso-query-suggestions>
@@ -106,6 +106,7 @@ function querySuggestions(options) {
106
106
  function answerGroup(options) {
107
107
  return [
108
108
  answer(options),
109
+ affiliation(options),
109
110
  sources(options),
110
111
  container(options, { name: 'bottom-spacing', visibleWhen: 'ongoing' }),
111
112
  ];
@@ -119,7 +120,7 @@ function answer(options) {
119
120
  '<miso-answer></miso-answer>',
120
121
  '<miso-feedback></miso-feedback>',
121
122
  ]);
122
- };
123
+ }
123
124
 
124
125
  function sources(options) {
125
126
  return container(options, { name: 'sources', visibleWhen: 'nonempty' }, [
@@ -127,7 +128,17 @@ function sources(options) {
127
128
  phrase(options, { name: 'sources', tag: 'h3' }),
128
129
  '<miso-sources></miso-sources>',
129
130
  ]);
130
- };
131
+ }
132
+
133
+ function affiliation(options) {
134
+ const { affiliation = true } = options || {};
135
+ if (!affiliation) {
136
+ return '';
137
+ }
138
+ return container(options, { name: 'affiliation', visibleWhen: 'nonempty' }, [
139
+ '<miso-affiliation></miso-affiliation>',
140
+ ]);
141
+ }
131
142
 
132
143
  function phrase(options, { name, tag = 'div' }) {
133
144
  const { classPrefix } = options;
@@ -9,4 +9,5 @@ export { default as MisoAnswerElement } from './miso-answer.js';
9
9
  export { default as MisoSourcesElement } from './miso-sources.js';
10
10
  export { default as MisoRelatedResourcesElement } from './miso-related-resources.js';
11
11
  export { default as MisoRelatedQuestionsElement } from './miso-related-questions.js';
12
+ export { default as MisoSovrnAffiliationElement } from './miso-affiliation.js';
12
13
  export { default as MisoErrorElement } from './miso-error.js';
@@ -0,0 +1,16 @@
1
+ import MisoComponentElement from './miso-component.js';
2
+ import { ROLE } from '../../constants.js';
3
+
4
+ export default class MisoAffiliationElement extends MisoComponentElement {
5
+
6
+ constructor() {
7
+ super({
8
+ role: ROLE.AFFILIATION,
9
+ });
10
+ }
11
+
12
+ static get tagName() {
13
+ return 'miso-affiliation';
14
+ }
15
+
16
+ }