@miso.ai/client-sdk-ui 1.10.3-beta.0 → 1.10.4-beta.0

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 (99) hide show
  1. package/dist/cjs/actor/autocomplete.js +4 -1
  2. package/dist/cjs/actor/data.js +15 -6
  3. package/dist/cjs/actor/fields.js +10 -10
  4. package/dist/cjs/actor/filters.js +114 -0
  5. package/dist/cjs/actor/hub.js +2 -1
  6. package/dist/cjs/actor/sessions.js +3 -5
  7. package/dist/cjs/actor/view.js +19 -6
  8. package/dist/cjs/actor/views.js +32 -19
  9. package/dist/cjs/constants.js +9 -2
  10. package/dist/cjs/defaults/ask/constants.js +3 -2
  11. package/dist/cjs/defaults/ask/templates.js +112 -219
  12. package/dist/cjs/defaults/common/templates.js +234 -0
  13. package/dist/cjs/defaults/hybrid-search/answer-box.js +115 -0
  14. package/dist/cjs/defaults/hybrid-search/constants.js +8 -0
  15. package/dist/cjs/defaults/hybrid-search/controls.js +17 -0
  16. package/dist/cjs/defaults/hybrid-search/index.js +31 -0
  17. package/dist/cjs/defaults/hybrid-search/phrases.js +27 -0
  18. package/dist/cjs/defaults/hybrid-search/templates.js +127 -0
  19. package/dist/cjs/defaults/index.js +4 -2
  20. package/dist/cjs/element/container/index.js +7 -0
  21. package/dist/cjs/element/container/miso-hybrid-search.js +21 -0
  22. package/dist/cjs/element/role/index.js +21 -0
  23. package/dist/cjs/element/role/miso-facets.js +20 -0
  24. package/dist/cjs/element/role/miso-hits.js +20 -0
  25. package/dist/cjs/element/role/miso-keywords.js +20 -0
  26. package/dist/cjs/layout/index.js +7 -0
  27. package/dist/cjs/layout/input/facets.js +294 -0
  28. package/dist/cjs/layout/input/option-list.js +4 -4
  29. package/dist/cjs/layout/input/search-box.js +6 -8
  30. package/dist/cjs/layout/list/collection.js +1 -2
  31. package/dist/cjs/layout/raf.js +1 -0
  32. package/dist/cjs/layout/template.js +3 -1
  33. package/dist/cjs/layout/templates.js +15 -0
  34. package/dist/cjs/layout/text/text.js +38 -5
  35. package/dist/cjs/layout/trackable.js +2 -16
  36. package/dist/cjs/plugin.js +4 -1
  37. package/dist/cjs/{source/index.js → source.js} +7 -4
  38. package/dist/cjs/util/bindings.js +3 -0
  39. package/dist/cjs/version.js +1 -1
  40. package/dist/cjs/workflow/answer-based.js +335 -0
  41. package/dist/cjs/workflow/ask.js +98 -271
  42. package/dist/cjs/workflow/base.js +69 -35
  43. package/dist/cjs/workflow/explore.js +10 -0
  44. package/dist/cjs/workflow/hybrid-search.js +140 -0
  45. package/dist/cjs/workflow/index.js +7 -0
  46. package/dist/cjs/workflow/processors.js +113 -0
  47. package/dist/cjs/workflow/recommendation.js +12 -3
  48. package/dist/cjs/workflow/search.js +34 -12
  49. package/package.json +2 -2
  50. package/src/actor/autocomplete.js +4 -2
  51. package/src/actor/data.js +8 -2
  52. package/src/actor/fields.js +10 -10
  53. package/src/actor/filters.js +103 -0
  54. package/src/actor/hub.js +2 -1
  55. package/src/actor/sessions.js +3 -4
  56. package/src/actor/view.js +19 -2
  57. package/src/actor/views.js +30 -16
  58. package/src/constants.js +10 -0
  59. package/src/defaults/ask/constants.js +1 -0
  60. package/src/defaults/ask/templates.js +70 -179
  61. package/src/defaults/common/templates.js +173 -0
  62. package/src/defaults/hybrid-search/answer-box.js +118 -0
  63. package/src/defaults/hybrid-search/constants.js +2 -0
  64. package/src/defaults/hybrid-search/controls.js +11 -0
  65. package/src/defaults/hybrid-search/index.js +9 -0
  66. package/src/defaults/hybrid-search/phrases.js +22 -0
  67. package/src/defaults/hybrid-search/templates.js +83 -0
  68. package/src/defaults/index.js +2 -0
  69. package/src/element/container/index.js +1 -0
  70. package/src/element/container/miso-hybrid-search.js +19 -0
  71. package/src/element/role/index.js +3 -0
  72. package/src/element/role/miso-facets.js +16 -0
  73. package/src/element/role/miso-hits.js +16 -0
  74. package/src/element/role/miso-keywords.js +16 -0
  75. package/src/layout/index.js +1 -0
  76. package/src/layout/input/facets.js +242 -0
  77. package/src/layout/input/option-list.js +3 -4
  78. package/src/layout/input/search-box.js +6 -7
  79. package/src/layout/list/collection.js +1 -2
  80. package/src/layout/raf.js +1 -0
  81. package/src/layout/template.js +2 -1
  82. package/src/layout/templates.js +15 -0
  83. package/src/layout/text/text.js +28 -3
  84. package/src/layout/trackable.js +2 -15
  85. package/src/plugin.js +6 -2
  86. package/src/{source/index.js → source.js} +7 -4
  87. package/src/util/bindings.js +4 -0
  88. package/src/version.js +1 -1
  89. package/src/workflow/answer-based.js +280 -0
  90. package/src/workflow/ask.js +82 -204
  91. package/src/workflow/base.js +64 -27
  92. package/src/workflow/explore.js +10 -1
  93. package/src/workflow/hybrid-search.js +125 -0
  94. package/src/workflow/index.js +1 -0
  95. package/src/workflow/processors.js +95 -0
  96. package/src/workflow/recommendation.js +9 -2
  97. package/src/workflow/search.js +35 -7
  98. package/dist/cjs/source/api.js +0 -23
  99. package/src/source/api.js +0 -12
@@ -1,7 +1,8 @@
1
1
  import { defineValues, delegateGetters } from '@miso.ai/commons';
2
- import { STATUS, ROLE } from '../constants.js';
2
+ import { ROLE } from '../constants.js';
3
3
  import * as fields from './fields.js';
4
4
  import ViewActor from './view.js';
5
+ import Filters from './filters.js';
5
6
  import Tracker from './tracker.js';
6
7
  import { normalizeTrackerOptions } from '../util/trackers.js';
7
8
 
@@ -12,6 +13,7 @@ export default class ViewsActor {
12
13
  extensions,
13
14
  layouts,
14
15
  roles = [],
16
+ rolesConfig = {},
15
17
  options,
16
18
  }) {
17
19
  this._hub = hub;
@@ -19,10 +21,12 @@ export default class ViewsActor {
19
21
  this._extensions = extensions;
20
22
  this._layoutFactory = layouts;
21
23
  this._options = options;
24
+ this._rolesConfig = rolesConfig;
22
25
  this._containers = new Map();
23
26
  this._containerTracker = undefined;
24
27
  this._views = {};
25
28
  this._trackers = new Trackers(this, roles);
29
+ this._filters = new Filters(this);
26
30
 
27
31
  for (const role of roles) {
28
32
  this._views[role] = new ViewActor(this, role);
@@ -37,7 +41,7 @@ export default class ViewsActor {
37
41
 
38
42
  this._unsubscribes = [
39
43
  () => window.removeEventListener('resize', syncSize),
40
- hub.on(fields.data(), () => this.refresh()),
44
+ hub.on(fields.data(), data => this.refresh({ data })),
41
45
  options.on('trackers', () => this._syncTrackers()),
42
46
  options.on('layouts', () => this._syncLayouts()),
43
47
  ];
@@ -50,6 +54,10 @@ export default class ViewsActor {
50
54
  return this._trackers;
51
55
  }
52
56
 
57
+ get filters() {
58
+ return this._filters;
59
+ }
60
+
53
61
  // elements //
54
62
  addContainer(element) {
55
63
  if (this._containers.has(element)) {
@@ -177,22 +185,27 @@ export default class ViewsActor {
177
185
  }
178
186
  }
179
187
 
180
- async refresh({ force } = {}) {
181
- const data = this._getData();
188
+ async refresh({ force, data } = {}) {
189
+ data = this._getData(data);
182
190
  // container first
183
- await Promise.all(this._getViews(true).map(view => view.refresh({ force, data })));
184
- }
191
+ let views = this._getViews(true);
185
192
 
186
- _getData() {
187
- const { [fields.data()]: data } = this._hub.states;
188
- // compare to cached
189
- if (!this._data || this._data.data !== data) {
190
- const status = (!data || !data.session || !data.session.active) ? STATUS.INITIAL :
191
- data.error ? STATUS.ERRONEOUS : data.value ? STATUS.READY : STATUS.LOADING;
192
- const ongoing = status === STATUS.READY ? !!data.ongoing : undefined;
193
- this._data = Object.freeze({ ...data, status, ongoing });
193
+ if (!force && data._inst) {
194
+ const { includes, excludes } = data._inst;
195
+ if (includes) {
196
+ // always include containers
197
+ views = views.filter(view => view.role === ROLE.CONTAINER || includes.includes(view.role));
198
+ }
199
+ if (excludes) {
200
+ views = views.filter(view => !excludes.includes(view.role));
201
+ }
194
202
  }
195
- return this._data;
203
+
204
+ await Promise.all(views.map(view => view.refresh({ force, data })));
205
+ }
206
+
207
+ _getData(data) {
208
+ return data || this._hub.states[fields.data()];
196
209
  }
197
210
 
198
211
  _syncTrackers() {
@@ -227,6 +240,7 @@ export default class ViewsActor {
227
240
  unsubscribe();
228
241
  }
229
242
  this._unsubscribes = [];
243
+ this._filters._destroy();
230
244
  }
231
245
 
232
246
  }
@@ -235,7 +249,7 @@ class Views {
235
249
 
236
250
  constructor(actor) {
237
251
  this._actor = actor;
238
- delegateGetters(this, actor, ['syncSize', 'refresh', 'trackers']);
252
+ delegateGetters(this, actor, ['syncSize', 'refresh', 'trackers', 'filters']);
239
253
  }
240
254
 
241
255
  get(role) {
package/src/constants.js CHANGED
@@ -20,6 +20,9 @@ export const ROLE = Object.freeze({
20
20
  QUERY_SUGGESTIONS: 'query_suggestions',
21
21
  RELATED_QUESTIONS: 'related_questions',
22
22
  AFFILIATION: 'affiliation',
23
+ KEYWORDS: 'keywords',
24
+ FACETS: 'facets',
25
+ HITS: 'hits',
23
26
  ERROR: 'error',
24
27
  });
25
28
 
@@ -36,18 +39,25 @@ const DATA_ROLE_SET = new Set([
36
39
  ROLE.QUERY_SUGGESTIONS,
37
40
  ROLE.RELATED_QUESTIONS,
38
41
  ROLE.AFFILIATION,
42
+ ROLE.FACETS,
43
+ ROLE.HITS,
39
44
  ]);
40
45
 
41
46
  export function isDataRole(role) {
42
47
  return DATA_ROLE_SET.has(role);
43
48
  }
44
49
 
50
+ export const DATA_ASPECT = Object.freeze({
51
+ AUTOCOMPLETE: 'autocomplete',
52
+ });
53
+
45
54
  export const LAYOUT_CATEGORY = Object.freeze({
46
55
  CONTAINER: 'container',
47
56
  BANNER: 'banner',
48
57
  LIST: 'list',
49
58
  QUERY: 'query',
50
59
  RADIO: 'radio',
60
+ FILTERS: 'filters',
51
61
  TEXT: 'text',
52
62
  AFFILIATION: 'affiliation',
53
63
  ERROR: 'error',
@@ -1 +1,2 @@
1
1
  export const CLASS_PREFIX = 'miso-ask-combo';
2
+ export const CONAINER_TAG = 'miso-ask';
@@ -1,11 +1,74 @@
1
- import { lowerCamelToKebab, kebabToLowerCamel, kebabOrSnakeToLowerCamel } from '@miso.ai/commons';
2
- import { CLASS_PREFIX } from './constants.js';
1
+ import { externalize } from '@miso.ai/commons';
2
+ import * as CONSTANTS from './constants.js';
3
3
  import * as DEFAULT_PHRASES from './phrases.js';
4
+ import { Helpers, Sections as _Sections } from '../common/templates.js';
4
5
 
5
- // TODO: rename classPrefix -> rootClass?
6
- // TODO: placeholder
6
+ class Sections extends _Sections {
7
+
8
+ constructor() {
9
+ super({ helpers });
10
+ }
11
+
12
+ answerGroup(options) {
13
+ const { container } = this._helpers;
14
+ return [
15
+ ...super.answerGroup(options),
16
+ container(options, { name: 'bottom-spacing', visibleWhen: 'ongoing' }),
17
+ ];
18
+ }
19
+
20
+ answer(options) {
21
+ const { container } = this._helpers;
22
+ return container(options, { name: 'answer', visibleWhen: 'ready' }, [
23
+ options.parentQuestionId ? '<hr>' : '',
24
+ ...this.question(options),
25
+ '<miso-answer></miso-answer>',
26
+ '<miso-feedback></miso-feedback>',
27
+ ]);
28
+ }
29
+
30
+ sources(options) {
31
+ const { container, phrase } = this._helpers;
32
+ return container(options, { name: 'sources', visibleWhen: 'nonempty' }, [
33
+ '<hr>',
34
+ phrase(options, { name: 'sources', tag: 'h3' }),
35
+ '<miso-sources></miso-sources>',
36
+ ]);
37
+ }
38
+
39
+ followUps(options) {
40
+ const { classPrefix, features = {} } = options;
41
+ return features.followUpQuestions === false ? '' : `<div id="${classPrefix}__follow-ups" class="${classPrefix}__follow-ups"></div>`;
42
+ }
43
+
44
+ relatedResourcesSection(options) {
45
+ const { container, phrase, section } = this._helpers;
46
+ const { features = {}, logo = true } = options;
47
+ const body = features.relatedResources === false ? '' : container(options, { name: 'related-resources', visibleWhen: 'nonempty', logo }, [
48
+ phrase(options, { name: 'related-resources', tag: 'h2' }),
49
+ '<miso-related-resources></miso-related-resources>',
50
+ ]);
51
+ return section(options, { name: 'related-resources' }, body);
52
+ }
53
+
54
+ querySuggestions(options) {
55
+ const { container, phrase } = this._helpers;
56
+ const { features = {} } = options;
57
+ return features.querySuggestions === false ? '' :
58
+ container(options, { name: 'query-suggestions', visibleWhen: 'initial+nonempty' }, [
59
+ phrase(options, { name: 'query-suggestions', tag: 'h3' }),
60
+ '<miso-query-suggestions></miso-query-suggestions>',
61
+ ]);
62
+ }
63
+
64
+ }
65
+
66
+ export const helpers = externalize(new Helpers({ ...CONSTANTS, DEFAULT_PHRASES }));
67
+ export const sections = externalize(new Sections({ helpers }));
7
68
 
8
69
  export function root(options = {}) {
70
+ const { normalizeOptions, section, container } = helpers;
71
+ const { followUps, relatedResourcesSection, answerGroup } = sections;
9
72
  options = normalizeOptions(options);
10
73
  return [
11
74
  section(options, { name: 'question' }, [
@@ -13,11 +76,13 @@ export function root(options = {}) {
13
76
  ]),
14
77
  section(options, { name: 'answer' }, answerGroup(options)),
15
78
  followUps(options),
16
- relatedResources(options),
79
+ relatedResourcesSection(options),
17
80
  ].join('');
18
81
  };
19
82
 
20
83
  export function followUp(options = {}) {
84
+ const { normalizeOptions, section, container } = helpers;
85
+ const { querySuggestions, answerGroup } = sections;
21
86
  options = normalizeOptions(options);
22
87
  return section(options, { name: 'follow-up' }, [
23
88
  querySuggestions(options),
@@ -25,177 +90,3 @@ export function followUp(options = {}) {
25
90
  ...answerGroup(options),
26
91
  ]);
27
92
  };
28
-
29
- export const sections = Object.freeze({
30
- followUps,
31
- relatedResources,
32
- querySuggestions,
33
- answerGroup,
34
- answer,
35
- sources,
36
- });
37
-
38
- export const helpers = Object.freeze({
39
- normalizeOptions,
40
- normalizePhraseOptions,
41
- normalizePhraseOptionKey,
42
- phrase,
43
- phraseText,
44
- section,
45
- container,
46
- element,
47
- attrs,
48
- });
49
-
50
-
51
-
52
- // options //
53
- function normalizeOptions({
54
- classPrefix = CLASS_PREFIX,
55
- circledCitationIndex = true,
56
- phrases,
57
- ...options
58
- } = {}) {
59
- phrases = normalizePhraseOptions(phrases);
60
- return { classPrefix, circledCitationIndex, phrases, ...options };
61
- }
62
-
63
- function normalizePhraseOptions(options = {}) {
64
- if (!options) {
65
- return {};
66
- }
67
- const normalized = {};
68
- for (const key in options) {
69
- normalized[normalizePhraseOptionKey(key)] = options[key];
70
- }
71
- return normalized;
72
- }
73
-
74
- function normalizePhraseOptionKey(key) {
75
- key = kebabOrSnakeToLowerCamel(key);
76
- if (key === 'relatedQuestions') {
77
- return 'querySuggestions'; // legacy key
78
- }
79
- return key;
80
- }
81
-
82
- // sections & containers //
83
- function followUps(options) {
84
- const { classPrefix, features = {} } = options;
85
- return features.followUpQuestions === false ? '' : `<div id="${classPrefix}__follow-ups" class="${classPrefix}__follow-ups"></div>`;
86
- }
87
-
88
- function relatedResources(options) {
89
- const { features = {}, logo = true } = options;
90
- const body = features.relatedResources === false ? '' : container(options, { name: 'related-resources', visibleWhen: 'nonempty', logo }, [
91
- phrase(options, { name: 'related-resources', tag: 'h2' }),
92
- '<miso-related-resources></miso-related-resources>',
93
- ]);
94
- return section(options, { name: 'related-resources' }, body);
95
- }
96
-
97
- function querySuggestions(options) {
98
- const { features = {} } = options;
99
- return features.querySuggestions === false ? '' :
100
- container(options, { name: 'query-suggestions', visibleWhen: 'initial+nonempty' }, [
101
- phrase(options, { name: 'query-suggestions', tag: 'h3' }),
102
- '<miso-query-suggestions></miso-query-suggestions>',
103
- ]);
104
- }
105
-
106
- function answerGroup(options) {
107
- return [
108
- answer(options),
109
- affiliation(options),
110
- sources(options),
111
- container(options, { name: 'bottom-spacing', visibleWhen: 'ongoing' }),
112
- ];
113
- }
114
-
115
- function answer(options) {
116
- return container(options, { name: 'answer', visibleWhen: 'ready' }, [
117
- options.parentQuestionId ? '<hr>' : '',
118
- phrase(options, { name: 'question', tag: 'div' }),
119
- '<miso-question></miso-question>',
120
- '<miso-answer></miso-answer>',
121
- '<miso-feedback></miso-feedback>',
122
- ]);
123
- }
124
-
125
- function sources(options) {
126
- return container(options, { name: 'sources', visibleWhen: 'nonempty' }, [
127
- '<hr>',
128
- phrase(options, { name: 'sources', tag: 'h3' }),
129
- '<miso-sources></miso-sources>',
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
- }
142
-
143
- function phrase(options, { name, tag = 'div' }) {
144
- const { classPrefix } = options;
145
- return `<${tag} class="${classPrefix}__phrase ${classPrefix}__${name}-phrase">${phraseText(options, kebabToLowerCamel(name))}</${tag}>`;
146
- }
147
-
148
- function phraseText(options, key) {
149
- const { phrases = {} } = options;
150
- const fn = phrases[key] || DEFAULT_PHRASES[key];
151
- switch (typeof fn) {
152
- case 'function':
153
- return fn(options);
154
- case 'string':
155
- return fn;
156
- default:
157
- return '';
158
- }
159
- }
160
-
161
- function section({ classPrefix }, { name }, body) {
162
- return element('section', {
163
- classes: [`${classPrefix}__section`, `${classPrefix}__${name}`],
164
- }, body);
165
- }
166
-
167
- function container({
168
- classPrefix,
169
- circledCitationIndex,
170
- parentQuestionId,
171
- }, {
172
- name,
173
- visibleWhen,
174
- logo = false,
175
- }, body) {
176
- const classes = [`${classPrefix}__${name}-container`];
177
- circledCitationIndex && classes.push('miso-circled-citation-index');
178
- return element('miso-ask', { classes, visibleWhen, logo, parentQuestionId }, body);
179
- }
180
-
181
- function element(tag, attributes = {}, body = '') {
182
- if (typeof attributes === 'string' || Array.isArray(attributes)) {
183
- body = attributes;
184
- attributes = {};
185
- }
186
- return `<${tag}${attrs(attributes)}>${Array.isArray(body) ? body.join('') : body}</${tag}>`;
187
- }
188
-
189
- function attrs({ classes = [], ...attributes } = {}) {
190
- let str = ``;
191
- if (classes.length) {
192
- str += ` class="${classes.join(' ')}"`;
193
- }
194
- for (const name in attributes) {
195
- const value = attributes[name];
196
- if (value !== undefined) {
197
- str += ` ${lowerCamelToKebab(name)}="${value}"`;
198
- }
199
- }
200
- return str;
201
- }
@@ -0,0 +1,173 @@
1
+ import { lowerCamelToKebab, kebabToLowerCamel, kebabOrSnakeToLowerCamel, defineValues } from '@miso.ai/commons';
2
+
3
+ // TODO: rename classPrefix -> rootClass?
4
+ // TODO: placeholder
5
+
6
+ export class Sections {
7
+
8
+ constructor({ helpers }) {
9
+ this._helpers = helpers;
10
+ }
11
+
12
+ errorSection(options) {
13
+ const { section, container } = this._helpers;
14
+ return section(options, { name: 'error' }, [
15
+ container(options, { name: 'error', visibleWhen: 'erroneous' }, '<miso-error></miso-error>'),
16
+ ]);
17
+ }
18
+
19
+ answerGroup(options) {
20
+ return [
21
+ this.answer(options),
22
+ this.affiliation(options),
23
+ this.sources(options),
24
+ ];
25
+ }
26
+
27
+ answer(options) {
28
+ const { container } = this._helpers;
29
+ return container(options, { name: 'answer', visibleWhen: 'ready' }, [
30
+ ...this.question(options),
31
+ '<miso-answer></miso-answer>',
32
+ '<miso-feedback></miso-feedback>',
33
+ ]);
34
+ }
35
+
36
+ question(options) {
37
+ const { phrase } = this._helpers;
38
+ return [
39
+ phrase(options, { name: 'question', tag: 'div' }),
40
+ '<miso-question></miso-question>',
41
+ ];
42
+ }
43
+
44
+ sources(options) {
45
+ const { container, phrase } = this._helpers;
46
+ return container(options, { name: 'sources', visibleWhen: 'nonempty' }, [
47
+ phrase(options, { name: 'sources', tag: 'h3' }),
48
+ '<miso-sources></miso-sources>',
49
+ ]);
50
+ }
51
+
52
+ affiliation(options) {
53
+ const { container } = this._helpers;
54
+ const { affiliation = true } = options || {};
55
+ if (!affiliation) {
56
+ return '';
57
+ }
58
+ return container(options, { name: 'affiliation', visibleWhen: 'nonempty' }, [
59
+ '<miso-affiliation></miso-affiliation>',
60
+ ]);
61
+ }
62
+
63
+ }
64
+
65
+ export class Helpers {
66
+
67
+ constructor(context) {
68
+ this._context = context;
69
+ defineValues(this, { lowerCamelToKebab, kebabToLowerCamel, kebabOrSnakeToLowerCamel });
70
+ }
71
+
72
+ // options //
73
+ normalizeOptions({
74
+ classPrefix,
75
+ circledCitationIndex = true,
76
+ phrases,
77
+ ...options
78
+ } = {}) {
79
+ classPrefix = classPrefix || this._context.CLASS_PREFIX;
80
+ phrases = this.normalizePhraseOptions(phrases);
81
+ return { classPrefix, circledCitationIndex, phrases, ...options };
82
+ }
83
+
84
+ normalizePhraseOptions(options = {}) {
85
+ if (!options) {
86
+ return {};
87
+ }
88
+ const normalized = {};
89
+ for (const key in options) {
90
+ normalized[this.normalizePhraseOptionKey(key)] = options[key];
91
+ }
92
+ return normalized;
93
+ }
94
+
95
+ normalizePhraseOptionKey(key) {
96
+ key = kebabOrSnakeToLowerCamel(key);
97
+ if (key === 'relatedQuestions') {
98
+ return 'querySuggestions'; // legacy key
99
+ }
100
+ return key;
101
+ }
102
+
103
+ // html //
104
+ phrase(options, { name, tag = 'div', inline = false }) {
105
+ const { classPrefix } = options;
106
+ const mainClass = inline ? `${classPrefix}__phrase-inline` : `${classPrefix}__phrase`;
107
+ return `<${tag} class="${mainClass} ${classPrefix}__${name}-phrase">${this.phraseText(options, kebabToLowerCamel(name))}</${tag}>`;
108
+ }
109
+
110
+ phraseText(options, key) {
111
+ const { phrases = {} } = options;
112
+ const fn = phrases[key] || this._context.DEFAULT_PHRASES[key];
113
+ switch (typeof fn) {
114
+ case 'function':
115
+ return fn(options);
116
+ case 'string':
117
+ return fn;
118
+ default:
119
+ return '';
120
+ }
121
+ }
122
+
123
+ section({ classPrefix }, { name }, body) {
124
+ return this.element('section', {
125
+ classes: [`${classPrefix}__section`, `${classPrefix}__${name}`],
126
+ }, body);
127
+ }
128
+
129
+ container({ classPrefix, circledCitationIndex, parentQuestionId }, { name, visibleWhen, logo = false }, body) {
130
+ const classes = [`${classPrefix}__${name}-container`];
131
+ circledCitationIndex && classes.push('miso-circled-citation-index');
132
+ return this.element(this._context.CONAINER_TAG, { classes, visibleWhen, logo, parentQuestionId }, body);
133
+ }
134
+
135
+ element(tag, attributes = {}, body = '') {
136
+ return `<${tag}${this.attrs(attributes)}>${Array.isArray(body) ? body.join('') : body}</${tag}>`;
137
+ }
138
+
139
+ attrs({ classes = [], ...attributes } = {}) {
140
+ let str = ``;
141
+ if (classes.length) {
142
+ str += ` class="${classes.join(' ')}"`;
143
+ }
144
+ for (const name in attributes) {
145
+ const value = attributes[name];
146
+ if (value !== undefined) {
147
+ str += ` ${lowerCamelToKebab(name)}="${value}"`;
148
+ }
149
+ }
150
+ return str;
151
+ }
152
+
153
+ }
154
+
155
+ export function externalize(object) {
156
+ const externalized = {};
157
+ const props = new Set();
158
+ for (let o = object; o !== Object.prototype; o = Object.getPrototypeOf(o)) {
159
+ const descriptors = Object.getOwnPropertyDescriptors(o);
160
+ for (const key in descriptors) {
161
+ if (key === 'constructor' || key.startsWith('_') || props.has(key)) {
162
+ continue;
163
+ }
164
+ props.add(key);
165
+ // TODO: getter, setter
166
+ const { value } = descriptors[key];
167
+ if (value) {
168
+ externalized[key] = typeof value === 'function' ? value.bind(object) : value;
169
+ }
170
+ }
171
+ }
172
+ return Object.freeze(externalized);
173
+ }
@@ -0,0 +1,118 @@
1
+ import { CLASS_PREFIX } from './constants.js';
2
+ import { STATUS } from '../../constants.js';
3
+ import { showMore as DEFAULT_PHRASE_SHOW_MORE, showLess as DEFAULT_PHRASE_SHOW_LESS } from './phrases.js';
4
+
5
+ const CLASS_OPEN = 'answer-box-open';
6
+ const CLASS_SHOWN = 'answer-box-shown';
7
+ const TOGGLE_BUTTON_SELECTOR = '[data-role="answer-box-toggle"]';
8
+
9
+ export default class AnswerBox {
10
+
11
+ constructor(client, element, { classPrefix = CLASS_PREFIX, phrases = {} } = {}) {
12
+ this._client = client;
13
+ this._workflow = client.ui.hybridSearch;
14
+ this._element = element;
15
+ this._classPrefix = classPrefix;
16
+ this._phrases = phrases;
17
+ this._init();
18
+ }
19
+
20
+ _init() {
21
+ const element = this._element;
22
+ const workflow = this._workflow;
23
+
24
+ // expose widget API
25
+ workflow.answerBox = this;
26
+ workflow._unsubscribes.push(() => this.destroy()); // TODO: review
27
+
28
+ // handle toggle button clicks
29
+ const callback = event => this._handleClick(event);
30
+ element.addEventListener('click', callback);
31
+
32
+ // sync state
33
+ this._syncWofkflowStatus();
34
+ this._syncButtonText();
35
+
36
+ this._unsubscribes = [
37
+ () => element.removeEventListener('click', callback),
38
+ workflow.on('loading', () => this.hide()),
39
+ workflow.on('error', () => this.show()),
40
+ workflow.on('ready', () => this.show()),
41
+ ];
42
+ }
43
+
44
+ _handleClick({ target, button }) {
45
+ // only left click
46
+ if (button !== 0) {
47
+ return;
48
+ }
49
+ if (target.closest(TOGGLE_BUTTON_SELECTOR)) {
50
+ this.toggleOpen();
51
+ }
52
+ }
53
+
54
+ _syncWofkflowStatus() {
55
+ const { status } = this._workflow;
56
+ switch (status) {
57
+ case STATUS.ERRONEOUS:
58
+ case STATUS.READY:
59
+ this.show();
60
+ break;
61
+ default:
62
+ this.hide();
63
+ }
64
+ }
65
+
66
+ _updateBoxStatus(method, className) {
67
+ this._element.classList[method](`${this._classPrefix}__${className}`);
68
+ }
69
+
70
+ _syncButtonText() {
71
+ const phrase = this.isOpen ? this._phrases.showLess || DEFAULT_PHRASE_SHOW_LESS : this._phrases.showMore || DEFAULT_PHRASE_SHOW_MORE;
72
+ for (const button of this._element.querySelectorAll(TOGGLE_BUTTON_SELECTOR)) {
73
+ button.textContent = phrase;
74
+ }
75
+ }
76
+
77
+ destroy() {
78
+ for (const unsubscribe of this._unsubscribes) {
79
+ unsubscribe();
80
+ }
81
+ const workflow = this._workflow;
82
+ if (workflow && workflow.answerBox === this) {
83
+ delete workflow.answerBox;
84
+ }
85
+ }
86
+
87
+ }
88
+
89
+ const GETTERS = {
90
+ isOpen: CLASS_OPEN,
91
+ shown: CLASS_SHOWN,
92
+ };
93
+
94
+ const METHODS = {
95
+ toggleOpen: ['toggle', CLASS_OPEN],
96
+ open: ['open', CLASS_OPEN],
97
+ close: ['remove', CLASS_OPEN],
98
+ toggleHidden: ['toggle', CLASS_SHOWN],
99
+ show: ['add', CLASS_SHOWN],
100
+ hide: ['remove', CLASS_SHOWN],
101
+ };
102
+
103
+ for (const [prop, className] of Object.entries(GETTERS)) {
104
+ Object.defineProperty(AnswerBox.prototype, prop, {
105
+ get() {
106
+ return this._element.classList.contains(`${this._classPrefix}__${className}`);
107
+ },
108
+ });
109
+ }
110
+
111
+ for (const [method, [action, className]] of Object.entries(METHODS)) {
112
+ AnswerBox.prototype[method] = function() {
113
+ this._updateBoxStatus(action, className);
114
+ if (className === CLASS_OPEN) {
115
+ this._syncButtonText();
116
+ }
117
+ };
118
+ }