@miso.ai/client-sdk-ui 1.13.2-beta.1 → 1.14.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/dist/cjs/asset/svgs.js +17 -4
  2. package/dist/cjs/combo/ask/index.js +2 -2
  3. package/dist/cjs/constants.js +120 -98
  4. package/dist/cjs/defaults/ask/controls.js +1 -1
  5. package/dist/cjs/defaults/hybrid-search/answer-box.js +4 -4
  6. package/dist/cjs/defaults.js +259 -0
  7. package/dist/cjs/element/container/index.js +28 -0
  8. package/dist/cjs/element/container/miso-ask.js +3 -3
  9. package/dist/cjs/element/container/miso-conversation.js +21 -0
  10. package/dist/cjs/element/container/miso-explore.js +1 -1
  11. package/dist/cjs/element/container/miso-history.js +21 -0
  12. package/dist/cjs/element/container/miso-hybrid-search.js +1 -1
  13. package/dist/cjs/element/container/miso-message-item.js +91 -0
  14. package/dist/cjs/element/container/miso-recommendation.js +1 -1
  15. package/dist/cjs/element/container/miso-search.js +1 -1
  16. package/dist/cjs/element/container/miso-thread-item.js +91 -0
  17. package/dist/cjs/element/context/miso-follow-ups.js +1 -1
  18. package/dist/cjs/element/role/index.js +49 -0
  19. package/dist/cjs/element/role/miso-delete.js +20 -0
  20. package/dist/cjs/element/role/miso-messages.js +20 -0
  21. package/dist/cjs/element/role/miso-new-thread.js +20 -0
  22. package/dist/cjs/element/role/miso-rename.js +20 -0
  23. package/dist/cjs/element/role/miso-subscription.js +20 -0
  24. package/dist/cjs/element/role/miso-threads.js +20 -0
  25. package/dist/cjs/element/role/miso-title.js +20 -0
  26. package/dist/cjs/layout/affiliation/affiliation.js +1 -1
  27. package/dist/cjs/layout/banner/banner.js +2 -1
  28. package/dist/cjs/layout/button/button.js +170 -0
  29. package/dist/cjs/layout/button/checkbox.js +116 -0
  30. package/dist/cjs/layout/button/more-button.js +3 -3
  31. package/dist/cjs/layout/container/container.js +1 -1
  32. package/dist/cjs/layout/container/item-container.js +99 -0
  33. package/dist/cjs/layout/error/error.js +2 -1
  34. package/dist/cjs/layout/index.js +42 -0
  35. package/dist/cjs/layout/input/facets.js +4 -3
  36. package/dist/cjs/layout/input/option-list.js +3 -2
  37. package/dist/cjs/layout/input/search-box.js +50 -12
  38. package/dist/cjs/layout/input/select.js +2 -1
  39. package/dist/cjs/layout/list/cards.js +2 -1
  40. package/dist/cjs/layout/list/carousel.js +2 -1
  41. package/dist/cjs/layout/list/collection.js +13 -1
  42. package/dist/cjs/layout/list/gallery.js +2 -1
  43. package/dist/cjs/layout/list/horizontal.js +2 -1
  44. package/dist/cjs/layout/list/list.js +2 -1
  45. package/dist/cjs/layout/list/messages.js +359 -0
  46. package/dist/cjs/layout/list/threads.js +199 -0
  47. package/dist/cjs/layout/mixin/trackable.js +4 -5
  48. package/dist/cjs/layout/mixin/triggerable.js +2 -2
  49. package/dist/cjs/layout/radio/feedback.js +4 -3
  50. package/dist/cjs/layout/templates.js +97 -0
  51. package/dist/cjs/layout/text/question.js +71 -0
  52. package/dist/cjs/layout/text/text.js +2 -1
  53. package/dist/cjs/layout/text/typewriter/index.js +18 -23
  54. package/dist/cjs/plugin.js +43 -104
  55. package/dist/cjs/util/confirm.js +33 -0
  56. package/dist/cjs/util/dom.js +21 -0
  57. package/dist/cjs/util/modal.js +48 -0
  58. package/dist/cjs/util/prompt.js +47 -0
  59. package/dist/cjs/version.js +1 -1
  60. package/package.json +5 -4
  61. package/src/CLAUDE.md +51 -104
  62. package/src/asset/svgs.js +16 -3
  63. package/src/combo/ask/index.js +2 -2
  64. package/src/constants.js +39 -147
  65. package/src/defaults/ask/controls.js +1 -1
  66. package/src/defaults/hybrid-search/answer-box.js +2 -2
  67. package/src/defaults.js +174 -0
  68. package/src/element/container/index.js +4 -0
  69. package/src/element/container/miso-ask.js +3 -3
  70. package/src/element/container/miso-conversation.js +19 -0
  71. package/src/element/container/miso-explore.js +1 -1
  72. package/src/element/container/miso-history.js +19 -0
  73. package/src/element/container/miso-hybrid-search.js +1 -1
  74. package/src/element/container/miso-message-item.js +94 -0
  75. package/src/element/container/miso-recommendation.js +1 -1
  76. package/src/element/container/miso-search.js +1 -1
  77. package/src/element/container/miso-thread-item.js +94 -0
  78. package/src/element/context/miso-follow-ups.js +1 -1
  79. package/src/element/role/index.js +7 -0
  80. package/src/element/role/miso-delete.js +16 -0
  81. package/src/element/role/miso-messages.js +16 -0
  82. package/src/element/role/miso-new-thread.js +16 -0
  83. package/src/element/role/miso-rename.js +16 -0
  84. package/src/element/role/miso-subscription.js +16 -0
  85. package/src/element/role/miso-threads.js +16 -0
  86. package/src/element/role/miso-title.js +16 -0
  87. package/src/layout/affiliation/affiliation.js +2 -2
  88. package/src/layout/banner/banner.js +2 -1
  89. package/src/layout/button/button.js +152 -0
  90. package/src/layout/button/checkbox.js +92 -0
  91. package/src/layout/button/more-button.js +3 -3
  92. package/src/layout/container/container.js +2 -2
  93. package/src/layout/container/item-container.js +99 -0
  94. package/src/layout/error/error.js +2 -1
  95. package/src/layout/index.js +6 -0
  96. package/src/layout/input/facets.js +3 -2
  97. package/src/layout/input/option-list.js +3 -2
  98. package/src/layout/input/search-box.js +45 -8
  99. package/src/layout/input/select.js +2 -1
  100. package/src/layout/list/cards.js +2 -1
  101. package/src/layout/list/carousel.js +2 -1
  102. package/src/layout/list/collection.js +15 -2
  103. package/src/layout/list/gallery.js +2 -1
  104. package/src/layout/list/horizontal.js +2 -1
  105. package/src/layout/list/list.js +2 -1
  106. package/src/layout/list/messages.js +316 -0
  107. package/src/layout/list/threads.js +166 -0
  108. package/src/layout/mixin/trackable.js +2 -3
  109. package/src/layout/mixin/triggerable.js +1 -1
  110. package/src/layout/radio/feedback.js +3 -2
  111. package/src/layout/templates.js +97 -0
  112. package/src/layout/text/question.js +58 -0
  113. package/src/layout/text/text.js +2 -1
  114. package/src/layout/text/typewriter/index.js +15 -18
  115. package/src/plugin.js +31 -117
  116. package/src/util/confirm.js +28 -0
  117. package/src/util/dom.js +16 -0
  118. package/src/util/modal.js +49 -0
  119. package/src/util/prompt.js +42 -0
  120. package/src/version.js +1 -1
  121. package/dist/cjs/actor/data.js +0 -132
  122. package/dist/cjs/actor/feedback.js +0 -29
  123. package/dist/cjs/actor/fields.js +0 -57
  124. package/dist/cjs/actor/filters.js +0 -161
  125. package/dist/cjs/actor/hub.js +0 -57
  126. package/dist/cjs/actor/index.js +0 -73
  127. package/dist/cjs/actor/interactions.js +0 -36
  128. package/dist/cjs/actor/sessions.js +0 -25
  129. package/dist/cjs/actor/tracker-states.js +0 -61
  130. package/dist/cjs/actor/tracker.js +0 -109
  131. package/dist/cjs/actor/trackers.js +0 -52
  132. package/dist/cjs/actor/utils.js +0 -15
  133. package/dist/cjs/actor/view.js +0 -294
  134. package/dist/cjs/actor/views.js +0 -209
  135. package/dist/cjs/source.js +0 -27
  136. package/dist/cjs/util/proxy.js +0 -88
  137. package/dist/cjs/util/trackers.js +0 -88
  138. package/dist/cjs/workflow/affiliations.js +0 -140
  139. package/dist/cjs/workflow/answer-based.js +0 -337
  140. package/dist/cjs/workflow/ask.js +0 -319
  141. package/dist/cjs/workflow/asks.js +0 -150
  142. package/dist/cjs/workflow/autocompletable.js +0 -63
  143. package/dist/cjs/workflow/autocomplete.js +0 -120
  144. package/dist/cjs/workflow/base.js +0 -418
  145. package/dist/cjs/workflow/context.js +0 -19
  146. package/dist/cjs/workflow/explore.js +0 -210
  147. package/dist/cjs/workflow/explores.js +0 -21
  148. package/dist/cjs/workflow/hybrid-search-answer.js +0 -88
  149. package/dist/cjs/workflow/hybrid-search-views.js +0 -109
  150. package/dist/cjs/workflow/hybrid-search.js +0 -213
  151. package/dist/cjs/workflow/index.js +0 -41
  152. package/dist/cjs/workflow/options/api.js +0 -41
  153. package/dist/cjs/workflow/options/auto-query.js +0 -97
  154. package/dist/cjs/workflow/options/configurable.js +0 -45
  155. package/dist/cjs/workflow/options/data-processor.js +0 -20
  156. package/dist/cjs/workflow/options/filters.js +0 -15
  157. package/dist/cjs/workflow/options/index.js +0 -126
  158. package/dist/cjs/workflow/options/interactions.js +0 -41
  159. package/dist/cjs/workflow/options/layouts.js +0 -79
  160. package/dist/cjs/workflow/options/options.js +0 -156
  161. package/dist/cjs/workflow/options/pagination.js +0 -25
  162. package/dist/cjs/workflow/options/roles.js +0 -15
  163. package/dist/cjs/workflow/options/templates.js +0 -30
  164. package/dist/cjs/workflow/options/trackers.js +0 -83
  165. package/dist/cjs/workflow/options/utils.js +0 -33
  166. package/dist/cjs/workflow/processors.js +0 -607
  167. package/dist/cjs/workflow/recommendation.js +0 -57
  168. package/dist/cjs/workflow/recommendations.js +0 -18
  169. package/dist/cjs/workflow/search-based.js +0 -337
  170. package/dist/cjs/workflow/search.js +0 -64
  171. package/dist/cjs/workflow/unit.js +0 -57
  172. package/dist/cjs/workflow/units.js +0 -42
  173. package/dist/cjs/workflow/use-link.js +0 -49
  174. package/src/actor/data.js +0 -106
  175. package/src/actor/feedback.js +0 -28
  176. package/src/actor/fields.js +0 -51
  177. package/src/actor/filters.js +0 -149
  178. package/src/actor/hub.js +0 -47
  179. package/src/actor/index.js +0 -10
  180. package/src/actor/interactions.js +0 -32
  181. package/src/actor/sessions.js +0 -21
  182. package/src/actor/tracker-states.js +0 -57
  183. package/src/actor/tracker.js +0 -104
  184. package/src/actor/trackers.js +0 -45
  185. package/src/actor/utils.js +0 -7
  186. package/src/actor/view.js +0 -272
  187. package/src/actor/views.js +0 -224
  188. package/src/source.js +0 -17
  189. package/src/util/proxy.js +0 -81
  190. package/src/util/trackers.js +0 -84
  191. package/src/workflow/affiliations.js +0 -121
  192. package/src/workflow/answer-based.js +0 -312
  193. package/src/workflow/ask.js +0 -284
  194. package/src/workflow/asks.js +0 -143
  195. package/src/workflow/autocompletable.js +0 -67
  196. package/src/workflow/autocomplete.js +0 -114
  197. package/src/workflow/base.js +0 -396
  198. package/src/workflow/context.js +0 -16
  199. package/src/workflow/explore.js +0 -194
  200. package/src/workflow/explores.js +0 -18
  201. package/src/workflow/hybrid-search-answer.js +0 -73
  202. package/src/workflow/hybrid-search-views.js +0 -116
  203. package/src/workflow/hybrid-search.js +0 -230
  204. package/src/workflow/index.js +0 -5
  205. package/src/workflow/options/api.js +0 -31
  206. package/src/workflow/options/auto-query.js +0 -70
  207. package/src/workflow/options/configurable.js +0 -48
  208. package/src/workflow/options/data-processor.js +0 -15
  209. package/src/workflow/options/filters.js +0 -10
  210. package/src/workflow/options/index.js +0 -11
  211. package/src/workflow/options/interactions.js +0 -36
  212. package/src/workflow/options/layouts.js +0 -73
  213. package/src/workflow/options/options.js +0 -175
  214. package/src/workflow/options/pagination.js +0 -18
  215. package/src/workflow/options/roles.js +0 -12
  216. package/src/workflow/options/templates.js +0 -27
  217. package/src/workflow/options/trackers.js +0 -80
  218. package/src/workflow/options/utils.js +0 -26
  219. package/src/workflow/processors.js +0 -490
  220. package/src/workflow/recommendation.js +0 -63
  221. package/src/workflow/recommendations.js +0 -14
  222. package/src/workflow/search-based.js +0 -329
  223. package/src/workflow/search.js +0 -73
  224. package/src/workflow/unit.js +0 -51
  225. package/src/workflow/units.js +0 -43
  226. package/src/workflow/use-link.js +0 -41
@@ -1,9 +1,10 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import { escapeHtml } from '@miso.ai/commons';
2
- import { fields } from '../../actor/index.js';
3
+ import { fields } from '@miso.ai/client-sdk-workflow';
3
4
  import TemplateBasedLayout from '../template.js';
4
5
  import Bindings from '../../util/bindings.js';
5
6
 
6
- const TYPE = 'facets';
7
+ const TYPE = LAYOUT_TYPE.FACETS;
7
8
  const DEFAULT_CLASSNAME = 'miso-facets';
8
9
  const DEDAULT_FACET_CLASSNAME = 'miso-facet';
9
10
  const CUSTOM_ATTRIBUTES_PREFIX = 'custom_attributes.';
@@ -1,10 +1,11 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import { escapeHtml } from '@miso.ai/commons';
2
- import { fields } from '../../actor/index.js';
3
+ import { fields } from '@miso.ai/client-sdk-workflow';
3
4
  import TemplateBasedLayout from '../template.js';
4
5
  import { makeTrackable } from '../mixin/trackable.js';
5
6
  import { addOrRemoveClass } from '../../util/dom.js';
6
7
 
7
- const TYPE = 'option-list';
8
+ const TYPE = LAYOUT_TYPE.OPTION_LIST;
8
9
  const DEFAULT_CLASSNAME = 'miso-option-list';
9
10
 
10
11
  function root(layout, state) {
@@ -1,12 +1,12 @@
1
1
  import { defineValues, escapeHtml, findInAncestors, debounce } from '@miso.ai/commons';
2
- import { STATUS, EVENT_TYPE } from '../../constants.js';
3
- import { fields } from '../../actor/index.js';
2
+ import { STATUS, EVENT_TYPE, LAYOUT_TYPE } from '../../constants.js';
3
+ import { fields } from '@miso.ai/client-sdk-workflow';
4
4
  import TemplateBasedLayout from '../template.js';
5
5
  import { imageBlock, productInfoBlock } from '../templates.js';
6
6
  import { getIcon } from '../../asset/svgs.js';
7
7
  import { addOrRemoveClass } from '../../util/dom.js';
8
8
 
9
- const TYPE = 'search-box';
9
+ const TYPE = LAYOUT_TYPE.SEARCH_BOX;
10
10
  const DEFAULT_CLASSNAME = 'miso-search-box';
11
11
  const DEFAULT_AUTOCOMPLETE_CLASSNAME = 'miso-autocomplete';
12
12
 
@@ -150,6 +150,30 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
150
150
  proxyElement.on('focusout', (e) => this._handleFocusOut(e)),
151
151
  hub.on(fields.completions(), () => view.refresh({ force: true })),
152
152
  ];
153
+ if (this.options.disableWhenOngoing) {
154
+ // disable submission while the main view is still displaying an answer
155
+ const mainRole = view.workflow._roles && view.workflow._roles.main;
156
+ if (mainRole) {
157
+ this._unsubscribes.push(hub.on(fields.view(mainRole), state => this._syncDisabled(!!state.ongoing)));
158
+ }
159
+ }
160
+ }
161
+
162
+ _syncDisabled(disabled) {
163
+ if (this._disabled === disabled) {
164
+ return;
165
+ }
166
+ this._disabled = disabled;
167
+ this._syncDisabledAttributes();
168
+ }
169
+
170
+ _syncDisabledAttributes() {
171
+ // the user can keep typing; only submission is disabled
172
+ const element = this._element;
173
+ const button = element && element.querySelector(`[data-role="submit"]`);
174
+ if (button) {
175
+ button.disabled = !!this._disabled;
176
+ }
153
177
  }
154
178
 
155
179
  focus() {
@@ -197,6 +221,7 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
197
221
  _afterRender(element, state) {
198
222
  this._fullfillFocusRequest(element);
199
223
  this._fullfillValueRequest(element);
224
+ this._syncDisabledAttributes(); // a re-render replaces the input element
200
225
  }
201
226
 
202
227
  _updateCompletions(element, { state, rendered }, { writeToState }) {
@@ -289,7 +314,9 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
289
314
  _handleKeyDown({ key, target, isComposing }) {
290
315
  if (!isComposing && key === 'Enter' && target.matches('[data-role="input"]')) {
291
316
  this._submit(target.value);
292
- target.blur();
317
+ if (this.options.blurOnSubmit !== false) {
318
+ target.blur();
319
+ }
293
320
  }
294
321
  // TODO: select completion items with arrow keys
295
322
  }
@@ -300,7 +327,9 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
300
327
  for (let element = target; element && element !== this._element; element = element.parentElement) {
301
328
  if (element.matches('[data-role="submit"]') || element.matches('[data-role="button"]') || element.matches('[type="submit"]')) {
302
329
  if (inputElement) {
303
- inputElement.blur();
330
+ if (this.options.blurOnSubmit !== false) {
331
+ inputElement.blur();
332
+ }
304
333
  this._submit(inputElement.value);
305
334
  }
306
335
  return;
@@ -329,12 +358,20 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
329
358
  }
330
359
 
331
360
  async _submit(value) {
332
- if (!value || !value.trim()) {
361
+ if (this._disabled || !value || !value.trim()) {
333
362
  return;
334
363
  }
335
364
  this.close();
336
- // TODO: q -> value
337
- this._view.hub.update(fields.query(), { q: value });
365
+ // a view event, like the buttons: the workflow routes it into its query
366
+ // flow (or its own submission semantics), the layout stays hands-off
367
+ const { session } = this._view._state || {};
368
+ this._view._emit('submit', Object.freeze({ session, value }));
369
+ if (this.options.clearOnSubmit) {
370
+ const inputElement = this._element && this._element.querySelector(`[data-role="input"]`);
371
+ if (inputElement) {
372
+ inputElement.value = '';
373
+ }
374
+ }
338
375
  this._trackSubmit(value);
339
376
  }
340
377
 
@@ -1,8 +1,9 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import { escapeHtml, kebabOrSnakeToHuman } from '@miso.ai/commons';
2
3
  import TemplateBasedLayout from '../template.js';
3
4
  import { makeTrackable } from '../mixin/trackable.js';
4
5
 
5
- const TYPE = 'select';
6
+ const TYPE = LAYOUT_TYPE.SELECT;
6
7
  const DEFAULT_CLASSNAME = 'miso-select';
7
8
 
8
9
  function root(layout, state) {
@@ -1,6 +1,7 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import CollectionLayout from './collection.js';
2
3
 
3
- const TYPE = 'cards';
4
+ const TYPE = LAYOUT_TYPE.CARDS;
4
5
  const DEFAULT_CLASSNAME = 'miso-cards';
5
6
 
6
7
  export default class CardsLayout extends CollectionLayout {
@@ -1,8 +1,9 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import CollectionLayout from './collection.js';
2
3
  import { makeSwipeable } from '../mixin/swipeable.js';
3
4
  import { CHEVRON, CHEVRON_NEGATIVE } from '../../asset/svgs.js';
4
5
 
5
- const TYPE = 'carousel';
6
+ const TYPE = LAYOUT_TYPE.CAROUSEL;
6
7
  const DEFAULT_CLASSNAME = 'miso-carousel';
7
8
 
8
9
  const DEFAULT_TEMPLATES = Object.freeze({
@@ -2,7 +2,7 @@ import { STATUS } from '../../constants.js';
2
2
  import TemplateBasedLayout from '../template.js';
3
3
  import { makeTrackable } from '../mixin/trackable.js';
4
4
  import { makeTriggerable } from '../mixin/triggerable.js';
5
- import { product, article, compactArticle, image, question, productInfoBlock, articleInfoBlock, compactArticleInfoBlock, imageInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, authorsBlock, authorsAndDateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock } from '../templates.js';
5
+ import { product, article, compactArticle, image, question, thread, threadBody, threadMenuBlock, message, messageBody, productInfoBlock, articleInfoBlock, compactArticleInfoBlock, imageInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, authorsBlock, authorsAndDateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock } from '../templates.js';
6
6
 
7
7
  function root(layout, state) {
8
8
  const { className, role, templates, options } = layout;
@@ -66,6 +66,11 @@ const DEFAULT_TEMPLATES = Object.freeze({
66
66
  compactArticle,
67
67
  image,
68
68
  question,
69
+ thread,
70
+ threadBody,
71
+ threadMenuBlock,
72
+ message,
73
+ messageBody,
69
74
  root,
70
75
  [STATUS.READY]: ready,
71
76
  empty: () => ``,
@@ -176,7 +181,15 @@ export default class CollectionLayout extends TemplateBasedLayout {
176
181
  }
177
182
 
178
183
  _getItemElements(element) {
179
- return element ? Array.from(element.querySelectorAll(`[data-role="item"]`)) : [];
184
+ if (!element) {
185
+ return [];
186
+ }
187
+ // an item may host a nested collection with items of its own (a
188
+ // conversation message and its source cards): ours are only those whose
189
+ // closest list is this layout's own list
190
+ const listElement = this._getListElement(element);
191
+ return Array.from((listElement || element).querySelectorAll(`[data-role="item"]`))
192
+ .filter(item => item.closest(`[data-role="list"]`) === listElement);
180
193
  }
181
194
 
182
195
  _onClick(event) {
@@ -1,3 +1,4 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import { trimObj } from '@miso.ai/commons';
2
3
  import CollectionLayout from './collection.js';
3
4
  import { addOrRemoveClass, resolveCssLength, cssAspectRatio } from '../../util/dom.js';
@@ -34,7 +35,7 @@ const DEFAULT_TEMPLATES = Object.freeze({
34
35
  item,
35
36
  });
36
37
 
37
- const TYPE = 'gallery';
38
+ const TYPE = LAYOUT_TYPE.GALLERY;
38
39
  const DEFAULT_CLASSNAME = 'miso-gallery';
39
40
 
40
41
  export default class GalleryLayout extends CollectionLayout {
@@ -1,6 +1,7 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import CollectionLayout from './collection.js';
2
3
 
3
- const TYPE = 'horizontal';
4
+ const TYPE = LAYOUT_TYPE.HORIZONTAL;
4
5
  const DEFAULT_CLASSNAME = 'miso-horizontal';
5
6
 
6
7
  export default class HorizontalLayout extends CollectionLayout {
@@ -1,6 +1,7 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import CollectionLayout from './collection.js';
2
3
 
3
- const TYPE = 'list';
4
+ const TYPE = LAYOUT_TYPE.LIST;
4
5
  const DEFAULT_CLASSNAME = 'miso-list';
5
6
 
6
7
  export default class ListLayout extends CollectionLayout {
@@ -0,0 +1,316 @@
1
+ import { STATUS, LAYOUT_TYPE } from '../../constants.js';
2
+ import CollectionLayout from './collection.js';
3
+
4
+ const TYPE = LAYOUT_TYPE.MESSAGES;
5
+ const DEFAULT_CLASSNAME = 'miso-messages';
6
+
7
+ // the infinite scroll trigger sits ABOVE the list — older messages load as
8
+ // the user scrolls up, unlike the stock collection root's bottom trigger
9
+ function root(layout, state) {
10
+ const { className, role, templates, options } = layout;
11
+ const { status } = state;
12
+ const roleAttr = role ? ` data-role="${role}"` : '';
13
+ const itemTypeAttr = options.itemType ? ` data-item-type="${options.itemType}"` : '';
14
+ return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${templates.trigger(layout, state)}${status === STATUS.READY ? templates[status](layout, state) : ''}${templates.loading(layout, state)}</div>`;
15
+ }
16
+
17
+ /**
18
+ * The conversation panel of the chat history interface: a shell that renders
19
+ * one <miso-message-item> container element per message item, incrementally —
20
+ * fresh items land at either end of the list: appended messages (a posted
21
+ * follow-up question) render as new items at the bottom, while an older
22
+ * page fetched by the more flow (infinite scroll: the trigger, placed above
23
+ * the list, fires the `more` hub field as the user scrolls up) renders as
24
+ * prepended items — told apart by which boundary the already-rendered
25
+ * records line up with.
26
+ * The content of a message is not rendered here: each <miso-message-item> is
27
+ * assigned its item subworkflow (workflow._getMessageWorkflow, off the item
28
+ * binding) in the post-render sync pass, and the role elements inside
29
+ * (question, answer, ...) render through that workflow's own layouts.
30
+ *
31
+ * All message-level presentation is the message workflow's own: a record
32
+ * without its answer body presents as status `loading`, stamped on the
33
+ * <miso-message-item> element by its container layout — there is deliberately no
34
+ * per-message loading icon; the answer typewriter's blinking caret is the
35
+ * loading indication — and the question bubble (text, authorship
36
+ * attributes) renders through the `question` layout.
37
+ *
38
+ * The shell keeps the panel-level behaviors: scroll pinning (the view sticks
39
+ * to the bottom while the user has not scrolled up, following content growth
40
+ * as answers type out), scroll anchoring while scrolled up (prepended items
41
+ * — and their contents, rendering in later through their own workflows —
42
+ * must not shift the reading position), and the `ongoing` view state read
43
+ * off the data — the last message's answer absent or unfinished — which
44
+ * e.g. disables the search box while an answer is on its way.
45
+ */
46
+ export default class MessagesLayout extends CollectionLayout {
47
+
48
+ static get type() {
49
+ return TYPE;
50
+ }
51
+
52
+ static get defaultClassName() {
53
+ return DEFAULT_CLASSNAME;
54
+ }
55
+
56
+ constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
57
+ super({ className, templates: { root, ...templates }, ...options });
58
+ this._pinned = true; // whether the view sticks to the bottom on updates
59
+ this._anchor = undefined; // the scroll anchor while prepended content settles
60
+ this._ongoing = undefined;
61
+ }
62
+
63
+ // item identity: the question id, or the local placeholder id standing in
64
+ // for it while a posted question awaits its response — the placeholder id
65
+ // survives the settle, so the element and its workflow do too
66
+ _getItemKey(message) {
67
+ return message.placeholder_id || message.question_id || message;
68
+ }
69
+
70
+ // render //
71
+ async render(element, state, controls = {}) {
72
+ // capture the view state callback for the ongoing report
73
+ this._notifyUpdate = controls.notifyUpdate;
74
+ await super.render(element, state, controls);
75
+ }
76
+
77
+ // incremental renders cover a pure append (a posted follow-up at the
78
+ // bottom) or a pure prepend (an older page above) — anything else
79
+ // re-renders in full
80
+ _shallRenderIncrementally(state, rendered) {
81
+ if (!super._shallRenderIncrementally(state, rendered)) {
82
+ return false;
83
+ }
84
+ const values = this._getItems(state) || [];
85
+ const renderedValues = rendered.value;
86
+ const count = values.length - renderedValues.length;
87
+ return count === 0 || // in-place changes only
88
+ this._getItemKey(values[0]) === this._getItemKey(renderedValues[0]) || // appended
89
+ this._getItemKey(values[count]) === this._getItemKey(renderedValues[0]); // prepended
90
+ }
91
+
92
+ // the fresh items land at the tail (a posted follow-up) or at the head
93
+ // (an older page fetched by the more flow): told apart by which boundary
94
+ // the already-rendered records line up with
95
+ _html(state, rendered, incremental) {
96
+ if (!incremental) {
97
+ return this.templates.root(this, state);
98
+ }
99
+ const values = this._getItems(state) || [];
100
+ const renderedValues = rendered.value;
101
+ const count = values.length - renderedValues.length;
102
+ if (count === 0) {
103
+ return '';
104
+ }
105
+ if (this._getItemKey(values[0]) === this._getItemKey(renderedValues[0])) {
106
+ const fresh = values.slice(renderedValues.length);
107
+ return {
108
+ position: 'beforeend',
109
+ items: this.templates.items(this, state, fresh, { offset: renderedValues.length }),
110
+ };
111
+ }
112
+ const fresh = values.slice(0, count);
113
+ return {
114
+ position: 'afterbegin',
115
+ items: this.templates.items(this, state, fresh, { offset: 0 }),
116
+ };
117
+ }
118
+
119
+ _render(element, { state }, { notifyUpdate }) {
120
+ const { incremental, html } = state;
121
+ if (incremental) {
122
+ if (html) {
123
+ const listElement = this._getListElement(element);
124
+ if (html.position === 'afterbegin') {
125
+ this._prependAnchored(listElement, html.items);
126
+ } else {
127
+ listElement.insertAdjacentHTML('beforeend', html.items);
128
+ }
129
+ } else {
130
+ notifyUpdate(false);
131
+ }
132
+ } else {
133
+ element.innerHTML = html;
134
+ }
135
+ }
136
+
137
+ _afterRender(element, state) {
138
+ super._afterRender(element, state); // syncs bindings and (re)arms the trigger
139
+ if (!state.incremental) {
140
+ // a fresh render (thread load): jump to the bottom
141
+ this._dropAnchor();
142
+ this._scrollToBottom({ force: true });
143
+ } else if (state.html && state.html.position === 'beforeend') {
144
+ // appended items (posted follow-up): jump to the bottom
145
+ this._scrollToBottom({ force: true });
146
+ }
147
+ this._watchGrowth(this._getListElement(element) || element);
148
+ this._syncWorkflows(element);
149
+ this._syncOngoing(state);
150
+ }
151
+
152
+ _unrender() {
153
+ super._unrender();
154
+ this._dropAnchor();
155
+ }
156
+
157
+ // assign each <miso-message-item> its item subworkflow, off the item binding
158
+ _syncWorkflows(element) {
159
+ const workflow = this._view && this._view.workflow;
160
+ if (!workflow || typeof workflow._getMessageWorkflow !== 'function') {
161
+ return;
162
+ }
163
+ for (const item of this._getItemElements(element)) {
164
+ const binding = this._bindings.get(item);
165
+ if (binding && item.isContainer) {
166
+ item.workflow = workflow._getMessageWorkflow(binding.value);
167
+ }
168
+ }
169
+ }
170
+
171
+ // whether an answer is on its way — pending, or still being generated —
172
+ // reported as the `ongoing` view state, e.g. for the search box to disable
173
+ // submission
174
+ _syncOngoing(state) {
175
+ const messages = state.value || [];
176
+ const last = messages[messages.length - 1];
177
+ // a message settled by a failed content fetch is not on its way anymore
178
+ const ongoing = !!last && !last.error && (last.answer === undefined || last.finished === false);
179
+ if (this._ongoing === ongoing) {
180
+ return;
181
+ }
182
+ this._ongoing = ongoing;
183
+ this._notifyUpdate && this._notifyUpdate({ ongoing });
184
+ }
185
+
186
+ // scrolling //
187
+ /**
188
+ * Scroll the containing scrollable to the bottom. Unless forced, only
189
+ * applies while the user is pinned to the bottom — scrolling up to read
190
+ * releases the pin, scrolling back down restores it.
191
+ */
192
+ _scrollToBottom({ force = false } = {}) {
193
+ const scrollable = this._getScrollable();
194
+ if (!scrollable || !(force || this._pinned)) {
195
+ return;
196
+ }
197
+ scrollable.scrollTop = scrollable.scrollHeight;
198
+ this._pinned = true;
199
+ }
200
+
201
+ _getScrollable() {
202
+ const element = this._element;
203
+ if (!element || typeof getComputedStyle === 'undefined') {
204
+ return undefined;
205
+ }
206
+ for (let el = element; el && el.nodeType === 1 && el !== document.body; el = el.parentElement) {
207
+ const { overflowY } = getComputedStyle(el);
208
+ if (overflowY === 'auto' || overflowY === 'scroll') {
209
+ this._watchScroll(el);
210
+ return el;
211
+ }
212
+ }
213
+ return undefined;
214
+ }
215
+
216
+ _watchScroll(scrollable) {
217
+ if (this._watchedScrollable === scrollable) {
218
+ return;
219
+ }
220
+ this._unwatchScroll && this._unwatchScroll();
221
+ const onScroll = () => {
222
+ this._pinned = scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight < 40;
223
+ if (this._pinned) {
224
+ // back at the bottom: the reading position needs no anchor
225
+ this._anchor = undefined;
226
+ } else if (this._anchor && this._anchor.element.isConnected) {
227
+ // follow the user: the anchor holds the position they scrolled to,
228
+ // not the one where the prepend happened
229
+ this._anchor.top = this._relativeTop(this._anchor.element, scrollable);
230
+ }
231
+ };
232
+ scrollable.addEventListener('scroll', onScroll, { passive: true });
233
+ this._watchedScrollable = scrollable;
234
+ const unwatch = this._unwatchScroll = () => {
235
+ scrollable.removeEventListener('scroll', onScroll);
236
+ this._watchedScrollable = undefined;
237
+ this._unwatchScroll = undefined;
238
+ };
239
+ this._unsubscribes.push(() => this._unwatchScroll === unwatch && unwatch());
240
+ }
241
+
242
+ // the message contents render (and type out) through their own workflows,
243
+ // outside this layout's render cycle: follow the content growth — pinned,
244
+ // the view sticks to the bottom; scrolled up with an anchor active (a
245
+ // prepended page still rendering its contents in), the anchor is held
246
+ // stationary instead
247
+ _watchGrowth(element) {
248
+ if (!element || this._watchedGrowth === element || typeof ResizeObserver === 'undefined') {
249
+ return;
250
+ }
251
+ this._unwatchGrowth && this._unwatchGrowth();
252
+ const observer = new ResizeObserver(() => this._onContentGrowth());
253
+ observer.observe(element);
254
+ this._watchedGrowth = element;
255
+ const unwatch = this._unwatchGrowth = () => {
256
+ observer.disconnect();
257
+ this._watchedGrowth = undefined;
258
+ this._unwatchGrowth = undefined;
259
+ };
260
+ this._unsubscribes.push(() => this._unwatchGrowth === unwatch && unwatch());
261
+ }
262
+
263
+ _onContentGrowth() {
264
+ if (this._pinned) {
265
+ this._scrollToBottom();
266
+ } else {
267
+ this._restoreAnchor();
268
+ }
269
+ }
270
+
271
+ // scroll anchoring for prepended content //
272
+ // The anchor is the previously-first item: everything prepended — and
273
+ // its content, rendering in later through the item workflows — sits
274
+ // above it, so holding the anchor stationary holds the reading position,
275
+ // while growth below it (a streaming answer at the bottom) moves nothing
276
+ // above and needs no compensation. Not the browser's own scroll
277
+ // anchoring, which not every engine provides.
278
+ _prependAnchored(listElement, items) {
279
+ this._setAnchor();
280
+ listElement.insertAdjacentHTML('afterbegin', items);
281
+ this._restoreAnchor();
282
+ }
283
+
284
+ _setAnchor() {
285
+ const scrollable = this._getScrollable();
286
+ const element = this._getItemElements(this._element)[0];
287
+ this._anchor = scrollable && element
288
+ ? { element, scrollable, top: this._relativeTop(element, scrollable) }
289
+ : undefined;
290
+ }
291
+
292
+ _restoreAnchor() {
293
+ const anchor = this._anchor;
294
+ if (!anchor) {
295
+ return;
296
+ }
297
+ const { element, scrollable, top } = anchor;
298
+ if (!element.isConnected) {
299
+ this._anchor = undefined;
300
+ return;
301
+ }
302
+ const delta = this._relativeTop(element, scrollable) - top;
303
+ if (delta) {
304
+ scrollable.scrollTop += delta;
305
+ }
306
+ }
307
+
308
+ _relativeTop(element, scrollable) {
309
+ return element.getBoundingClientRect().top - scrollable.getBoundingClientRect().top;
310
+ }
311
+
312
+ _dropAnchor() {
313
+ this._anchor = undefined;
314
+ }
315
+
316
+ }