@miso.ai/client-sdk-ui 1.12.2 → 1.12.3-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 (35) hide show
  1. package/dist/cjs/constants.js +8 -3
  2. package/dist/cjs/defaults/ask/phrases.js +7 -3
  3. package/dist/cjs/defaults/ask/templates.js +19 -19
  4. package/dist/cjs/defaults/common/templates.js +6 -4
  5. package/dist/cjs/element/context/miso-follow-ups.js +45 -13
  6. package/dist/cjs/element/role/index.js +7 -0
  7. package/dist/cjs/element/role/miso-follow-up-questions.js +20 -0
  8. package/dist/cjs/layout/input/option-list.js +4 -5
  9. package/dist/cjs/layout/text/typewriter/index.js +19 -1
  10. package/dist/cjs/version.js +1 -1
  11. package/dist/cjs/workflow/answer-based.js +10 -5
  12. package/dist/cjs/workflow/ask.js +66 -5
  13. package/dist/cjs/workflow/asks.js +2 -1
  14. package/dist/cjs/workflow/base.js +1 -1
  15. package/dist/cjs/workflow/hybrid-search-answer.js +1 -1
  16. package/dist/cjs/workflow/processors.js +32 -1
  17. package/dist/cjs/workflow/search-based.js +24 -1
  18. package/package.json +2 -2
  19. package/src/constants.js +8 -1
  20. package/src/defaults/ask/phrases.js +5 -2
  21. package/src/defaults/ask/templates.js +12 -12
  22. package/src/defaults/common/templates.js +6 -6
  23. package/src/element/context/miso-follow-ups.js +36 -11
  24. package/src/element/role/index.js +1 -0
  25. package/src/element/role/miso-follow-up-questions.js +16 -0
  26. package/src/layout/input/option-list.js +3 -4
  27. package/src/layout/text/typewriter/index.js +14 -1
  28. package/src/version.js +1 -1
  29. package/src/workflow/answer-based.js +11 -6
  30. package/src/workflow/ask.js +48 -5
  31. package/src/workflow/asks.js +2 -1
  32. package/src/workflow/base.js +1 -1
  33. package/src/workflow/hybrid-search-answer.js +2 -2
  34. package/src/workflow/processors.js +28 -2
  35. package/src/workflow/search-based.js +24 -2
@@ -239,7 +239,7 @@ class SearchBasedWorkflow extends _base.default {
239
239
  _meta
240
240
  } = request.payload;
241
241
  if (!_meta || !_meta.page) {
242
- return data; // not from "more" request
242
+ return (0, _processors.writeMisoIdAsRootMisoId)(data); // not from "more" request
243
243
  }
244
244
  // concat records if it's from "more" request
245
245
  const currentData = this._hub.states[_index.fields.data()];
@@ -260,6 +260,29 @@ class SearchBasedWorkflow extends _base.default {
260
260
  });
261
261
  this._views.filters.apply();
262
262
  }
263
+
264
+ // interactions //
265
+ _defaultProcessInteraction(payload, args) {
266
+ payload = super._defaultProcessInteraction(payload, args);
267
+ payload = this._writeRootMisoIdToInteraction(payload);
268
+ return payload;
269
+ }
270
+ _writeRootMisoIdToInteraction(payload) {
271
+ const data = this._hub.states[_index.fields.data()];
272
+ const {
273
+ root_miso_id
274
+ } = data && data.value || {};
275
+ if (!root_miso_id) {
276
+ return payload;
277
+ }
278
+ return (0, _processors.mergeInteraction)(payload, {
279
+ context: {
280
+ custom_context: {
281
+ root_miso_id
282
+ }
283
+ }
284
+ });
285
+ }
263
286
  }
264
287
  exports.default = SearchBasedWorkflow;
265
288
  (0, _index3.makeConfigurable)(SearchBasedWorkflow.prototype, [_constants.WORKFLOW_CONFIGURABLE.PAGINATION, _constants.WORKFLOW_CONFIGURABLE.FILTERS]);
package/package.json CHANGED
@@ -26,7 +26,7 @@
26
26
  "url": "https://github.com/MisoAI/miso-client-js-sdk/issues"
27
27
  },
28
28
  "dependencies": {
29
- "@miso.ai/commons": "1.12.2"
29
+ "@miso.ai/commons": "1.12.3-beta.0"
30
30
  },
31
- "version": "1.12.2"
31
+ "version": "1.12.3-beta.0"
32
32
  }
package/src/constants.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export const ATTR_DATA_MISO_UNIT_ID = `data-miso-unit-id`;
2
2
  export const ATTR_DATA_MISO_PRODUCT_ID = `data-miso-product-id`;
3
3
  export const DEFAULT_UNIT_ID = 'default';
4
- export const ORGANIC_QUESTION_SOURCE = '_organic';
5
4
 
6
5
  export const ROLE = Object.freeze({
7
6
  CONTAINER: 'container',
@@ -19,6 +18,7 @@ export const ROLE = Object.freeze({
19
18
  IMAGES: 'images',
20
19
  SOURCES: 'sources',
21
20
  RELATED_RESOURCES: 'related_resources',
21
+ FOLLOW_UP_QUESTIONS: 'follow_up_questions',
22
22
  QUERY_SUGGESTIONS: 'query_suggestions',
23
23
  RELATED_QUESTIONS: 'related_questions',
24
24
  AFFILIATION: 'affiliation',
@@ -42,6 +42,7 @@ const DATA_ROLE_SET = new Set([
42
42
  ROLE.IMAGES,
43
43
  ROLE.SOURCES,
44
44
  ROLE.RELATED_RESOURCES,
45
+ ROLE.FOLLOW_UP_QUESTIONS,
45
46
  ROLE.QUERY_SUGGESTIONS,
46
47
  ROLE.RELATED_QUESTIONS,
47
48
  ROLE.AFFILIATION,
@@ -110,6 +111,12 @@ export const STATUS = Object.freeze({
110
111
  UNANSWERABLE: 'unanswerable',
111
112
  });
112
113
 
114
+ export const QUESTION_SOURCE = Object.freeze({
115
+ ORGANIC: '_organic',
116
+ SUGGESTED_QUESTIONS: '_suggested_questions',
117
+ INLINE_QUESTION_LINK: '_inline_question_link',
118
+ });
119
+
113
120
  export function validateEventType(value) {
114
121
  if (TRACKING_EVENT_TYPES.indexOf(value) < 0) {
115
122
  throw new Error(`Unrecognized event type: ${value}`);
@@ -9,13 +9,16 @@ export const question = 'You asked...';
9
9
  export const sources = 'This reply is based on the following articles';
10
10
 
11
11
  /**
12
- * The phrase displayed right before <miso-query-suggestions>
12
+ * The phrase displayed right before <miso-follow-up-questions>
13
13
  */
14
- export const querySuggestions = 'Related questions you can explore';
14
+ export const followUpQuestions = 'Related questions you can explore';
15
15
 
16
16
  /**
17
17
  * The phrase displayed right before <miso-related-resources>
18
18
  */
19
19
  export const relatedResources = 'Go beyond, and learn more about this topic';
20
20
 
21
+ /**
22
+ * The phrase displayed at the header of <miso-reasoning> box
23
+ */
21
24
  export const thoughts = 'Thoughts';
@@ -18,6 +18,7 @@ class Sections extends _Sections {
18
18
  this.answer(options),
19
19
  this.affiliation(options),
20
20
  this.sources(options),
21
+ this.followUpQuestions(options),
21
22
  container(options, { name: 'bottom-spacing', visibleWhen: 'ongoing' }),
22
23
  ];
23
24
  }
@@ -76,6 +77,16 @@ class Sections extends _Sections {
76
77
  ]);
77
78
  }
78
79
 
80
+ followUpQuestions(options) {
81
+ const { container, phrase } = this._helpers;
82
+ const name = 'follow-up-questions';
83
+ const extraNames = ['query-suggestions']; // backward compatibility (styling)
84
+ return container(options, { name, extraNames, visibleWhen: 'nonempty' }, [
85
+ phrase(options, { name, extraNames, tag: 'h3' }),
86
+ '<miso-follow-up-questions></miso-follow-up-questions>',
87
+ ]);
88
+ }
89
+
79
90
  followUps(options) {
80
91
  const { features = {} } = options;
81
92
  return features.followUpQuestions === false ? '' : `<miso-follow-ups id="miso-ask-combo__follow-ups" class="miso-ask-combo__follow-ups"></miso-follow-ups>`;
@@ -91,16 +102,6 @@ class Sections extends _Sections {
91
102
  return section(options, { name: 'related-resources' }, body);
92
103
  }
93
104
 
94
- querySuggestions(options) {
95
- const { container, phrase } = this._helpers;
96
- const { features = {} } = options;
97
- return features.querySuggestions === false ? '' :
98
- container(options, { name: 'query-suggestions', visibleWhen: 'initial+nonempty' }, [
99
- phrase(options, { name: 'query-suggestions', tag: 'h3' }),
100
- '<miso-query-suggestions></miso-query-suggestions>',
101
- ]);
102
- }
103
-
104
105
  }
105
106
 
106
107
  export const helpers = externalize(new Helpers({ ...CONSTANTS, DEFAULT_PHRASES }));
@@ -122,10 +123,9 @@ export function root(options = {}) {
122
123
 
123
124
  export function followUp(options = {}) {
124
125
  const { normalizeOptions, section, container } = helpers;
125
- const { querySuggestions, answerGroup } = sections;
126
+ const { answerGroup } = sections;
126
127
  options = normalizeOptions(options);
127
128
  return section(options, { name: 'follow-up' }, [
128
- querySuggestions(options),
129
129
  container(options, { name: 'query', visibleWhen: 'initial loading' }, '<miso-query></miso-query>'),
130
130
  ...answerGroup(options),
131
131
  ]);
@@ -94,17 +94,17 @@ export class Helpers {
94
94
 
95
95
  normalizePhraseOptionKey(key) {
96
96
  key = kebabOrSnakeToLowerCamel(key);
97
- if (key === 'relatedQuestions') {
98
- return 'querySuggestions'; // legacy key
97
+ if (key === 'relatedQuestions' || key === 'querySuggestions') {
98
+ return 'followUpQuestions'; // legacy key
99
99
  }
100
100
  return key;
101
101
  }
102
102
 
103
103
  // html //
104
- phrase(options, { name, tag = 'div', inline = false }) {
104
+ phrase(options, { name, extraNames = [], tag = 'div', inline = false }) {
105
105
  const { classPrefix } = options;
106
106
  const mainClass = inline ? `${classPrefix}__phrase-inline` : `${classPrefix}__phrase`;
107
- return `<${tag} class="${mainClass} ${classPrefix}__${name}-phrase">${this.phraseText(options, kebabToLowerCamel(name))}</${tag}>`;
107
+ return `<${tag} class="${mainClass} ${[name, ...extraNames].map(name => `${classPrefix}__${name}-phrase`).join(' ')}">${this.phraseText(options, kebabToLowerCamel(name))}</${tag}>`;
108
108
  }
109
109
 
110
110
  phraseText(options, key) {
@@ -126,8 +126,8 @@ export class Helpers {
126
126
  }, body);
127
127
  }
128
128
 
129
- container({ classPrefix, circledCitationIndex, parentQuestionId }, { name, visibleWhen, trait, logo = false }, body) {
130
- const classes = [`${classPrefix}__${name}-container`];
129
+ container({ classPrefix, circledCitationIndex, parentQuestionId }, { name, extraNames = [], visibleWhen, trait, logo = false }, body) {
130
+ const classes = [name, ...extraNames].map(name => `${classPrefix}__${name}-container`);
131
131
  circledCitationIndex && classes.push('miso-circled-citation-index');
132
132
  return this.element(this._context.CONAINER_TAG, { classes, visibleWhen, trait, logo, parentQuestionId }, body);
133
133
  }
@@ -14,6 +14,7 @@ export default class MisoFollowUpsElement extends MisoContextElement {
14
14
 
15
15
  constructor() {
16
16
  super();
17
+ this._elementToWorkflow = new WeakMap();
17
18
  this._unsubscribes = [];
18
19
  }
19
20
 
@@ -41,21 +42,45 @@ export default class MisoFollowUpsElement extends MisoContextElement {
41
42
  return;
42
43
  }
43
44
 
44
- // 1. when an answer is fully populated, insert a new section for the follow-up question
45
- this._unsubscribes.push(context.on('done', (event) => {
46
- const template = event.workflow._options.resolved.templates.followUp;
47
- if (!template) {
45
+ // 1. call auto-next if unset
46
+ if (!context._autoNextFn) {
47
+ context.autoNext();
48
+ }
49
+
50
+ // 2. when a new workflow is created, insert a new section
51
+ this._unsubscribes.push(context.on('create', (workflow) => {
52
+ const { parentQuestionId } = workflow;
53
+ const template = workflow._options.resolved.templates.followUp;
54
+ if (!parentQuestionId || !template) {
48
55
  return;
49
56
  }
50
- this.insertAdjacentHTML('beforeend', template({ parentQuestionId: event.workflow.questionId }));
57
+ const oldChildCount = this.children.length;
58
+ this.insertAdjacentHTML('beforeend', template({ parentQuestionId }));
59
+ const newChildCount = this.children.length;
60
+ // keep track of the elements
61
+ for (const element of Array.prototype.slice.call(this.children, oldChildCount, newChildCount)) {
62
+ this._elementToWorkflow.set(element, workflow);
63
+ }
51
64
  }));
52
65
 
53
- // 2. if user starts over, clean up existing follow-up questions
54
- this._unsubscribes.push(context.root.on('loading', () => {
55
- // clean up the entire follow-ups section
56
- this.innerHTML = '';
57
- // destroy all follow-up workflows
58
- context.reset({ root: false });
66
+ // 3. when a workflow is loading, scroll into view
67
+ this._unsubscribes.push(context.on('loading', ({ workflow }) => {
68
+ for (const element of this.children) {
69
+ if (this._elementToWorkflow.get(element) === workflow) {
70
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
71
+ break;
72
+ }
73
+ }
74
+ }));
75
+
76
+ // 4. when a workflow is destroyed, remove the section
77
+ this._unsubscribes.push(context.on('destroy', ({ workflow }) => {
78
+ for (const element of this.children) {
79
+ if (this._elementToWorkflow.get(element) === workflow) {
80
+ this._elementToWorkflow.delete(element);
81
+ element.remove();
82
+ }
83
+ }
59
84
  }));
60
85
  }
61
86
 
@@ -1,5 +1,6 @@
1
1
  export { default as MisoComponentElement } from './miso-component.js';
2
2
  export { default as MisoQueryElement } from './miso-query.js';
3
+ export { default as MisoFollowUpQuestionsElement } from './miso-follow-up-questions.js';
3
4
  export { default as MisoQuerySuggestionsElement } from './miso-query-suggestions.js';
4
5
  export { default as MisoFeedbackElement } from './miso-feedback.js';
5
6
  export { default as MisoResultsElement } from './miso-results.js';
@@ -0,0 +1,16 @@
1
+ import MisoComponentElement from './miso-component.js';
2
+ import { ROLE } from '../../constants.js';
3
+
4
+ export default class MisoFollowUpQuestionsElement extends MisoComponentElement {
5
+
6
+ constructor() {
7
+ super({
8
+ role: ROLE.FOLLOW_UP_QUESTIONS,
9
+ });
10
+ }
11
+
12
+ static get tagName() {
13
+ return 'miso-follow-up-questions';
14
+ }
15
+
16
+ }
@@ -116,15 +116,14 @@ export default class OptionListLayout extends TemplateBasedLayout {
116
116
  return;
117
117
  }
118
118
  this._trackClick(event, binding);
119
- this._submit(option.text);
119
+ this._select(option.text);
120
120
  }
121
121
 
122
- async _submit(value) {
122
+ async _select(value) {
123
123
  if (!value) {
124
124
  return;
125
125
  }
126
- // TODO: q -> value
127
- this._view.hub.update(fields.query(), { q: value });
126
+ this._view._events.emit('select', { value });
128
127
  }
129
128
 
130
129
  destroy() {
@@ -151,8 +151,9 @@ export default class TypewriterLayout extends ProgressiveLayout {
151
151
  // event //
152
152
  _onClick(event) {
153
153
  const { target } = event;
154
- const citationLinkElement = target.closest(`[data-role="citation-link"]`);
154
+
155
155
  // citation link click
156
+ const citationLinkElement = target.closest(`[data-role="citation-link"]`);
156
157
  if (citationLinkElement) {
157
158
  const index = parseInt(citationLinkElement.dataset.index);
158
159
  if (!Number.isNaN(index)) {
@@ -160,6 +161,18 @@ export default class TypewriterLayout extends ProgressiveLayout {
160
161
  }
161
162
  return;
162
163
  }
164
+
165
+ // follow-up link click
166
+ const followUpLinkElement = target.closest(`[data-role="follow-up-link"]`);
167
+ if (followUpLinkElement) {
168
+ let { q } = followUpLinkElement.dataset;
169
+ q = q ? q.trim() : undefined;
170
+ if (q) {
171
+ this._view._events.emit('follow-up-click', { event, q });
172
+ }
173
+ return;
174
+ }
175
+
163
176
  // generic link click
164
177
  const anchorElement = target.closest('a');
165
178
  if (anchorElement) {
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.12.2';
1
+ export default '1.12.3-beta.0';
@@ -1,7 +1,7 @@
1
1
  import { API } from '@miso.ai/commons';
2
2
  import Workflow from './base.js';
3
3
  import { fields, FeedbackActor } from '../actor/index.js';
4
- import { ROLE, STATUS, ORGANIC_QUESTION_SOURCE } from '../constants.js';
4
+ import { ROLE, STATUS, QUESTION_SOURCE } from '../constants.js';
5
5
  import { SearchBoxLayout, TextLayout, ListLayout, GalleryLayout, TypewriterLayout, FeedbackLayout, AffiliationLayout } from '../layout/index.js';
6
6
  import { processData as processAffiliationData } from '../affiliation/index.js';
7
7
  import { mergeRolesOptions, autoQuery as autoQueryFn, DEFAULT_AUTO_QUERY_PARAM, DEFAULT_TRACKER_OPTIONS } from './options/index.js';
@@ -174,7 +174,7 @@ export default class AnswerBasedWorkflow extends Workflow {
174
174
  }
175
175
 
176
176
  _writeQuestionSourceToSession(args) {
177
- this.session.meta.question_source = args.qs || ORGANIC_QUESTION_SOURCE; // might be null, not undefined
177
+ this.session.meta.question_source = args.qs || QUESTION_SOURCE.ORGANIC; // might be null, not undefined
178
178
  }
179
179
 
180
180
  _buildPayload() {
@@ -234,12 +234,12 @@ export default class AnswerBasedWorkflow extends Workflow {
234
234
 
235
235
  const url = new URL(window.location);
236
236
  const currentQuestion = url.searchParams.get(param);
237
- const currentQuestionSource = url.searchParams.get(sourceParam) || ORGANIC_QUESTION_SOURCE;
237
+ const currentQuestionSource = url.searchParams.get(sourceParam) || QUESTION_SOURCE.ORGANIC;
238
238
  if (question === currentQuestion && questionSource === currentQuestionSource) {
239
239
  return;
240
240
  }
241
241
  url.searchParams.set(param, question);
242
- if (questionSource === ORGANIC_QUESTION_SOURCE) {
242
+ if (questionSource === QUESTION_SOURCE.ORGANIC) {
243
243
  url.searchParams.delete(sourceParam);
244
244
  } else {
245
245
  url.searchParams.set(sourceParam, questionSource);
@@ -294,10 +294,13 @@ export default class AnswerBasedWorkflow extends Workflow {
294
294
 
295
295
  // handlers //
296
296
  _onCitationClick({ index, event }) {
297
+ if (event.button !== 0) {
298
+ return; // only left click
299
+ }
297
300
  if (isTracked(event)) {
298
301
  return;
299
302
  }
300
- const { value } = this.states[fields.data()] || {};
303
+ const { value } = this._hub.states[fields.data()] || {};
301
304
  const { sources } = value || {};
302
305
  // index is 1-based
303
306
  const source = sources && sources[index - 1];
@@ -307,11 +310,13 @@ export default class AnswerBasedWorkflow extends Workflow {
307
310
  // TODO: should we track impression as well?
308
311
  markAsTracked(event);
309
312
  // distinguish from regular sources element click
310
- // TODO
311
313
  this._views.trackers.sources.click([source.product_id], { event_target: 'citation-link' });
312
314
  }
313
315
 
314
316
  _onAnswerLinkClick({ event, ...item }) {
317
+ if (event.button !== 0) {
318
+ return; // only left click
319
+ }
315
320
  if (isTracked(event)) {
316
321
  return;
317
322
  }
@@ -1,11 +1,11 @@
1
1
  import { defineValues, trimObj, API } from '@miso.ai/commons';
2
2
  import AnswerBasedWorkflow from './answer-based.js';
3
3
  import { fields } from '../actor/index.js';
4
- import { ROLE, STATUS, ORGANIC_QUESTION_SOURCE } from '../constants.js';
4
+ import { ROLE, STATUS, QUESTION_SOURCE } from '../constants.js';
5
5
  import { OptionListLayout, ListLayout, SearchBoxLayout, TypewriterLayout } from '../layout/index.js';
6
6
  import { mergeInteraction } from './processors.js';
7
7
  import { mergeRolesOptions, DEFAULT_TRACKER_OPTIONS } from './options/index.js';
8
- import { mappingSuggestionsData } from './processors.js';
8
+ import { mappingSuggestionsData, mappingFollowUpQuestionsData } from './processors.js';
9
9
  import { followUp as followUpTemplate } from '../defaults/ask/templates.js';
10
10
 
11
11
  const DEFAULT_API_OPTIONS = Object.freeze({
@@ -23,6 +23,7 @@ const DEFAULT_LAYOUTS = Object.freeze({
23
23
  [ROLE.REASONING]: TypewriterLayout.type,
24
24
  [ROLE.RELATED_RESOURCES]: [ListLayout.type, { incremental: true, itemType: 'article' }],
25
25
  [ROLE.QUERY_SUGGESTIONS]: OptionListLayout.type,
26
+ [ROLE.FOLLOW_UP_QUESTIONS]: OptionListLayout.type,
26
27
  });
27
28
 
28
29
  const DEFAULT_TRACKERS = Object.freeze({
@@ -53,6 +54,7 @@ const ROLES_OPTIONS = mergeRolesOptions(AnswerBasedWorkflow.ROLES_OPTIONS, {
53
54
  members: Object.keys(DEFAULT_LAYOUTS),
54
55
  mappings: {
55
56
  [ROLE.QUERY_SUGGESTIONS]: mappingSuggestionsData,
57
+ [ROLE.FOLLOW_UP_QUESTIONS]: mappingFollowUpQuestionsData,
56
58
  },
57
59
  });
58
60
 
@@ -79,6 +81,9 @@ export default class Ask extends AnswerBasedWorkflow {
79
81
  this._unsubscribes = [
80
82
  ...this._unsubscribes,
81
83
  this._hub.on(fields.view(ROLE.REASONING), state => this._onReasoningViewUpdate(state)),
84
+ this._views.get(ROLE.ANSWER).on('follow-up-click', event => this._onFollowUpClick(event)),
85
+ this._views.get(ROLE.FOLLOW_UP_QUESTIONS).on('select', event => this._onFollowUpQuestionSelect(event)),
86
+ this._views.get(ROLE.QUERY_SUGGESTIONS).on('select', event => this._onQuerySuggestionSelect(event)),
82
87
  ];
83
88
  }
84
89
 
@@ -91,6 +96,8 @@ export default class Ask extends AnswerBasedWorkflow {
91
96
 
92
97
  // lifecycle //
93
98
  restart() {
99
+ const { next } = this;
100
+ next && next.destroy();
94
101
  if (this._questionId) {
95
102
  this._context._byQid.delete(this._questionId);
96
103
  }
@@ -126,7 +133,7 @@ export default class Ask extends AnswerBasedWorkflow {
126
133
  parent_question_id: parentQuestionId,
127
134
  _meta: {
128
135
  ...options._meta,
129
- question_source: qs || ORGANIC_QUESTION_SOURCE, // might be null, not undefined
136
+ question_source: qs || QUESTION_SOURCE.ORGANIC, // might be null, not undefined
130
137
  },
131
138
  });
132
139
  payload = this._writeWikiLinkTemplateToPayload(payload);
@@ -163,8 +170,11 @@ export default class Ask extends AnswerBasedWorkflow {
163
170
  let { property } = (payload.context && payload.context.custom_context) || {};
164
171
  let question_id, parent_question_id, question_source;
165
172
 
166
- if (args.role === ROLE.QUERY_SUGGESTIONS) {
173
+ if (args.role === ROLE.FOLLOW_UP_QUESTIONS || args.role === ROLE.QUERY_SUGGESTIONS) {
167
174
  property = 'suggested_followup_questions';
175
+ }
176
+
177
+ if (args.role === ROLE.QUERY_SUGGESTIONS) {
168
178
  parent_question_id = this.previous && this.previous.parentQuestionId;
169
179
  question_id = this.parentQuestionId;
170
180
  question_source = this.previous && this.previous.session.meta.question_source;
@@ -187,6 +197,36 @@ export default class Ask extends AnswerBasedWorkflow {
187
197
  });
188
198
  }
189
199
 
200
+ // handlers //
201
+ _onFollowUpClick({ q, event } = {}) {
202
+ if (event.button !== 0) {
203
+ return; // only left click
204
+ }
205
+ this._submitFollowUpQuestion({ q, qs: QUESTION_SOURCE.INLINE_QUESTION_LINK });
206
+ // TODO: track click?
207
+ }
208
+
209
+ _onFollowUpQuestionSelect({ value: q }) {
210
+ this._submitFollowUpQuestion({ q, qs: QUESTION_SOURCE.SUGGESTED_QUESTIONS });
211
+ }
212
+
213
+ _submitFollowUpQuestion({ q = '', qs } = {}) {
214
+ q = q.trim();
215
+ if (!q) {
216
+ return;
217
+ }
218
+ const next = this.getOrCreateNext();
219
+ next.query({ q, qs });
220
+ }
221
+
222
+ _onQuerySuggestionSelect({ value: q }) {
223
+ if (!q) {
224
+ return;
225
+ }
226
+ // backward compatible: query on current workflow
227
+ this.query({ q, qs: QUESTION_SOURCE.SUGGESTED_QUESTIONS });
228
+ }
229
+
190
230
  // helpers //
191
231
  _writeQuestionId(questionId) {
192
232
  // capture question ID and register at context
@@ -212,7 +252,10 @@ export default class Ask extends AnswerBasedWorkflow {
212
252
 
213
253
  // destroy //
214
254
  _destroy(options) {
215
- const { parentQuestionId, questionId } = this;
255
+ const { next, parentQuestionId, questionId } = this;
256
+ if (next) {
257
+ next.destroy(options);
258
+ }
216
259
  if (parentQuestionId) {
217
260
  this._context._byPqid.delete(parentQuestionId);
218
261
  }
@@ -19,7 +19,7 @@ function autoNextOptionsAsPredicate(options = DEFAULT_AUTO_NEXT_OPTIONS) {
19
19
  case 'function':
20
20
  return options;
21
21
  default:
22
- return false;
22
+ return () => false;
23
23
  }
24
24
  }
25
25
 
@@ -31,6 +31,7 @@ export default class Asks extends WorkflowContext {
31
31
  this._byPqid = new Map();
32
32
  this._root = new Ask(this); // initialize with default workflow
33
33
  this._events.on('*', event => this._createNextIfNecessary(event));
34
+ this._autoNextFn = undefined;
34
35
  }
35
36
 
36
37
  get root() {
@@ -308,7 +308,7 @@ export default class Workflow extends Component {
308
308
 
309
309
  // destroy //
310
310
  destroy(options) {
311
- this._events.emit('destroy');
311
+ this._emit('destroy');
312
312
  this._destroy(options);
313
313
  }
314
314
 
@@ -1,5 +1,5 @@
1
1
  import AnswerBasedWorkflow from './answer-based.js';
2
- import { ROLE, STATUS, ORGANIC_QUESTION_SOURCE } from '../constants.js';
2
+ import { ROLE, STATUS, QUESTION_SOURCE } from '../constants.js';
3
3
  import { mergeRolesOptions } from './options/index.js';
4
4
  import { writeKeywordsToData, writeMisoIdToSession, writeMisoIdFromSession, writeUnanswerableToMeta } from './processors.js';
5
5
  import { makeAutocompletable } from './autocompletable.js';
@@ -54,7 +54,7 @@ export default class HybridSearchAnswer extends AnswerBasedWorkflow {
54
54
  ...payload,
55
55
  _meta: {
56
56
  ...payload._meta,
57
- question_source: qs || ORGANIC_QUESTION_SOURCE, // might be null, not undefined
57
+ question_source: qs || QUESTION_SOURCE.ORGANIC, // might be null, not undefined
58
58
  },
59
59
  };
60
60
  }
@@ -54,6 +54,13 @@ export function mappingSuggestionsData(_, { workflow } = {}) {
54
54
  return value && value.map(text => ({ text }));
55
55
  }
56
56
 
57
+ export function mappingFollowUpQuestionsData(data) {
58
+ if (!data || !data.value) {
59
+ return undefined;
60
+ }
61
+ return (data.value.followup_questions || []).map(text => ({ text }));
62
+ }
63
+
57
64
  // data //
58
65
  export function getRevision(data) {
59
66
  return data && data.value && data.value.revision;
@@ -186,11 +193,29 @@ export function retainFacetCountsInData(data, currentFacetCounts) {
186
193
  };
187
194
  }
188
195
 
196
+ export function writeMisoIdAsRootMisoId(data) {
197
+ const { value } = data;
198
+ if (!value) {
199
+ return data;
200
+ }
201
+ const { miso_id } = value;
202
+ if (!miso_id) {
203
+ return data;
204
+ }
205
+ return {
206
+ ...data,
207
+ value: {
208
+ ...value,
209
+ root_miso_id: miso_id,
210
+ },
211
+ };
212
+ }
213
+
189
214
  export function concatItemsFromMoreResponse(oldData, newData, { role = ROLE.PRODUCTS } = {}) {
190
215
  if (!newData.value) {
191
216
  return oldData;
192
217
  }
193
- const { [role]: oldProducts = [], facet_counts, total } = oldData.value;
218
+ const { [role]: oldProducts = [], facet_counts, total, root_miso_id } = oldData.value;
194
219
  const { [role]: newProducts = [] } = newData.value;
195
220
  // keep old exhaustion flag, just in case
196
221
  // TODO
@@ -202,6 +227,7 @@ export function concatItemsFromMoreResponse(oldData, newData, { role = ROLE.PROD
202
227
  ...newData.value,
203
228
  facet_counts,
204
229
  total,
230
+ root_miso_id,
205
231
  [role]: [...oldProducts, ...newProducts],
206
232
  },
207
233
  }
@@ -304,7 +330,7 @@ export function mergeInteraction(base = {}, patch = {}) {
304
330
  return trimObj({
305
331
  ...base,
306
332
  ...patch,
307
- context:trimObj({
333
+ context: trimObj({
308
334
  ...base.context,
309
335
  ...patch.context,
310
336
  custom_context: trimObj({
@@ -3,7 +3,7 @@ import Workflow from './base.js';
3
3
  import { fields } from '../actor/index.js';
4
4
  import { STATUS, ROLE, WORKFLOW_CONFIGURABLE } from '../constants.js';
5
5
  import { SearchBoxLayout, ListLayout, TextLayout, FacetsLayout, SelectLayout, MoreButtonLayout } from '../layout/index.js';
6
- import { mappingSortData, writeKeywordsToData, retainFacetCountsInData, writeExhaustionToData, concatItemsFromMoreResponse } from './processors.js';
6
+ import { mappingSortData, writeKeywordsToData, retainFacetCountsInData, writeExhaustionToData, writeMisoIdAsRootMisoId, concatItemsFromMoreResponse, mergeInteraction } from './processors.js';
7
7
  import { mergeRolesOptions, autoQuery as autoQueryFn, makeConfigurable, DEFAULT_TRACKER_OPTIONS } from './options/index.js';
8
8
 
9
9
  const DEFAULT_ROWS = 10;
@@ -245,7 +245,7 @@ export default class SearchBasedWorkflow extends Workflow {
245
245
  }
246
246
  const { _meta } = request.payload;
247
247
  if (!_meta || !_meta.page) {
248
- return data; // not from "more" request
248
+ return writeMisoIdAsRootMisoId(data); // not from "more" request
249
249
  }
250
250
  // concat records if it's from "more" request
251
251
  const currentData = this._hub.states[fields.data()];
@@ -261,6 +261,28 @@ export default class SearchBasedWorkflow extends Workflow {
261
261
  this._views.filters.apply();
262
262
  }
263
263
 
264
+ // interactions //
265
+ _defaultProcessInteraction(payload, args) {
266
+ payload = super._defaultProcessInteraction(payload, args);
267
+ payload = this._writeRootMisoIdToInteraction(payload);
268
+ return payload;
269
+ }
270
+
271
+ _writeRootMisoIdToInteraction(payload) {
272
+ const data = this._hub.states[fields.data()];
273
+ const { root_miso_id } = (data && data.value) || {};
274
+ if (!root_miso_id) {
275
+ return payload;
276
+ }
277
+ return mergeInteraction(payload, {
278
+ context: {
279
+ custom_context: {
280
+ root_miso_id,
281
+ }
282
+ }
283
+ });
284
+ }
285
+
264
286
  }
265
287
 
266
288
  makeConfigurable(SearchBasedWorkflow.prototype, [WORKFLOW_CONFIGURABLE.PAGINATION, WORKFLOW_CONFIGURABLE.FILTERS]);