@miso.ai/client-sdk-ui 1.13.2-beta.0 → 1.14.0-beta.2

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 (214) hide show
  1. package/dist/cjs/asset/svgs.js +15 -4
  2. package/dist/cjs/combo/ask/index.js +2 -2
  3. package/dist/cjs/constants.js +118 -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 +169 -0
  7. package/dist/cjs/element/container/index.js +14 -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-recommendation.js +1 -1
  14. package/dist/cjs/element/container/miso-search.js +1 -1
  15. package/dist/cjs/element/context/miso-follow-ups.js +1 -1
  16. package/dist/cjs/element/role/index.js +42 -0
  17. package/dist/cjs/element/role/miso-messages.js +20 -0
  18. package/dist/cjs/element/role/miso-new-thread.js +20 -0
  19. package/dist/cjs/element/role/miso-rename.js +20 -0
  20. package/dist/cjs/element/role/miso-subscription.js +20 -0
  21. package/dist/cjs/element/role/miso-threads.js +20 -0
  22. package/dist/cjs/element/role/miso-title.js +20 -0
  23. package/dist/cjs/layout/affiliation/affiliation.js +1 -1
  24. package/dist/cjs/layout/banner/banner.js +2 -1
  25. package/dist/cjs/layout/button/button.js +130 -0
  26. package/dist/cjs/layout/button/checkbox.js +115 -0
  27. package/dist/cjs/layout/button/more-button.js +3 -3
  28. package/dist/cjs/layout/container/container.js +1 -1
  29. package/dist/cjs/layout/error/error.js +2 -1
  30. package/dist/cjs/layout/index.js +28 -0
  31. package/dist/cjs/layout/input/facets.js +4 -3
  32. package/dist/cjs/layout/input/option-list.js +3 -2
  33. package/dist/cjs/layout/input/search-box.js +42 -9
  34. package/dist/cjs/layout/input/select.js +2 -1
  35. package/dist/cjs/layout/list/cards.js +2 -1
  36. package/dist/cjs/layout/list/carousel.js +2 -1
  37. package/dist/cjs/layout/list/collection.js +5 -0
  38. package/dist/cjs/layout/list/gallery.js +2 -1
  39. package/dist/cjs/layout/list/horizontal.js +2 -1
  40. package/dist/cjs/layout/list/list.js +2 -1
  41. package/dist/cjs/layout/list/messages.js +346 -0
  42. package/dist/cjs/layout/list/threads.js +257 -0
  43. package/dist/cjs/layout/mixin/trackable.js +4 -5
  44. package/dist/cjs/layout/mixin/triggerable.js +2 -2
  45. package/dist/cjs/layout/radio/feedback.js +4 -3
  46. package/dist/cjs/layout/templates.js +70 -0
  47. package/dist/cjs/layout/text/text.js +2 -1
  48. package/dist/cjs/layout/text/typewriter/index.js +4 -2
  49. package/dist/cjs/plugin.js +43 -104
  50. package/dist/cjs/util/confirm.js +33 -0
  51. package/dist/cjs/util/modal.js +48 -0
  52. package/dist/cjs/util/prompt.js +47 -0
  53. package/dist/cjs/version.js +1 -1
  54. package/package.json +5 -4
  55. package/src/CLAUDE.md +48 -105
  56. package/src/asset/svgs.js +14 -3
  57. package/src/combo/ask/index.js +2 -2
  58. package/src/constants.js +37 -147
  59. package/src/defaults/ask/controls.js +1 -1
  60. package/src/defaults/hybrid-search/answer-box.js +2 -2
  61. package/src/defaults.js +107 -0
  62. package/src/element/container/index.js +2 -0
  63. package/src/element/container/miso-ask.js +3 -3
  64. package/src/element/container/miso-conversation.js +19 -0
  65. package/src/element/container/miso-explore.js +1 -1
  66. package/src/element/container/miso-history.js +19 -0
  67. package/src/element/container/miso-hybrid-search.js +1 -1
  68. package/src/element/container/miso-recommendation.js +1 -1
  69. package/src/element/container/miso-search.js +1 -1
  70. package/src/element/context/miso-follow-ups.js +1 -1
  71. package/src/element/role/index.js +6 -0
  72. package/src/element/role/miso-messages.js +16 -0
  73. package/src/element/role/miso-new-thread.js +16 -0
  74. package/src/element/role/miso-rename.js +16 -0
  75. package/src/element/role/miso-subscription.js +16 -0
  76. package/src/element/role/miso-threads.js +16 -0
  77. package/src/element/role/miso-title.js +16 -0
  78. package/src/layout/affiliation/affiliation.js +2 -2
  79. package/src/layout/banner/banner.js +2 -1
  80. package/src/layout/button/button.js +116 -0
  81. package/src/layout/button/checkbox.js +91 -0
  82. package/src/layout/button/more-button.js +3 -3
  83. package/src/layout/container/container.js +2 -2
  84. package/src/layout/error/error.js +2 -1
  85. package/src/layout/index.js +4 -0
  86. package/src/layout/input/facets.js +3 -2
  87. package/src/layout/input/option-list.js +3 -2
  88. package/src/layout/input/search-box.js +41 -6
  89. package/src/layout/input/select.js +2 -1
  90. package/src/layout/list/cards.js +2 -1
  91. package/src/layout/list/carousel.js +2 -1
  92. package/src/layout/list/collection.js +6 -1
  93. package/src/layout/list/gallery.js +2 -1
  94. package/src/layout/list/horizontal.js +2 -1
  95. package/src/layout/list/list.js +2 -1
  96. package/src/layout/list/messages.js +318 -0
  97. package/src/layout/list/threads.js +206 -0
  98. package/src/layout/mixin/trackable.js +2 -3
  99. package/src/layout/mixin/triggerable.js +1 -1
  100. package/src/layout/radio/feedback.js +3 -2
  101. package/src/layout/templates.js +62 -0
  102. package/src/layout/text/text.js +2 -1
  103. package/src/layout/text/typewriter/index.js +5 -3
  104. package/src/plugin.js +31 -117
  105. package/src/util/confirm.js +28 -0
  106. package/src/util/modal.js +49 -0
  107. package/src/util/prompt.js +42 -0
  108. package/src/version.js +1 -1
  109. package/dist/cjs/actor/data.js +0 -132
  110. package/dist/cjs/actor/feedback.js +0 -29
  111. package/dist/cjs/actor/fields.js +0 -57
  112. package/dist/cjs/actor/filters.js +0 -161
  113. package/dist/cjs/actor/hub.js +0 -57
  114. package/dist/cjs/actor/index.js +0 -73
  115. package/dist/cjs/actor/interactions.js +0 -36
  116. package/dist/cjs/actor/sessions.js +0 -25
  117. package/dist/cjs/actor/tracker-states.js +0 -61
  118. package/dist/cjs/actor/tracker.js +0 -109
  119. package/dist/cjs/actor/trackers.js +0 -52
  120. package/dist/cjs/actor/utils.js +0 -15
  121. package/dist/cjs/actor/view.js +0 -294
  122. package/dist/cjs/actor/views.js +0 -209
  123. package/dist/cjs/source.js +0 -27
  124. package/dist/cjs/util/proxy.js +0 -88
  125. package/dist/cjs/util/trackers.js +0 -88
  126. package/dist/cjs/workflow/affiliations.js +0 -140
  127. package/dist/cjs/workflow/answer-based.js +0 -337
  128. package/dist/cjs/workflow/ask.js +0 -319
  129. package/dist/cjs/workflow/asks.js +0 -150
  130. package/dist/cjs/workflow/autocompletable.js +0 -63
  131. package/dist/cjs/workflow/autocomplete.js +0 -120
  132. package/dist/cjs/workflow/base.js +0 -418
  133. package/dist/cjs/workflow/context.js +0 -19
  134. package/dist/cjs/workflow/explore.js +0 -210
  135. package/dist/cjs/workflow/explores.js +0 -21
  136. package/dist/cjs/workflow/hybrid-search-answer.js +0 -88
  137. package/dist/cjs/workflow/hybrid-search-views.js +0 -109
  138. package/dist/cjs/workflow/hybrid-search.js +0 -213
  139. package/dist/cjs/workflow/index.js +0 -41
  140. package/dist/cjs/workflow/options/api.js +0 -41
  141. package/dist/cjs/workflow/options/auto-query.js +0 -97
  142. package/dist/cjs/workflow/options/configurable.js +0 -45
  143. package/dist/cjs/workflow/options/data-processor.js +0 -20
  144. package/dist/cjs/workflow/options/filters.js +0 -15
  145. package/dist/cjs/workflow/options/index.js +0 -126
  146. package/dist/cjs/workflow/options/interactions.js +0 -41
  147. package/dist/cjs/workflow/options/layouts.js +0 -79
  148. package/dist/cjs/workflow/options/options.js +0 -156
  149. package/dist/cjs/workflow/options/pagination.js +0 -25
  150. package/dist/cjs/workflow/options/roles.js +0 -15
  151. package/dist/cjs/workflow/options/templates.js +0 -30
  152. package/dist/cjs/workflow/options/trackers.js +0 -83
  153. package/dist/cjs/workflow/options/utils.js +0 -33
  154. package/dist/cjs/workflow/processors.js +0 -607
  155. package/dist/cjs/workflow/recommendation.js +0 -57
  156. package/dist/cjs/workflow/recommendations.js +0 -18
  157. package/dist/cjs/workflow/search-based.js +0 -337
  158. package/dist/cjs/workflow/search.js +0 -64
  159. package/dist/cjs/workflow/unit.js +0 -57
  160. package/dist/cjs/workflow/units.js +0 -42
  161. package/dist/cjs/workflow/use-link.js +0 -49
  162. package/src/actor/data.js +0 -106
  163. package/src/actor/feedback.js +0 -28
  164. package/src/actor/fields.js +0 -51
  165. package/src/actor/filters.js +0 -149
  166. package/src/actor/hub.js +0 -47
  167. package/src/actor/index.js +0 -10
  168. package/src/actor/interactions.js +0 -32
  169. package/src/actor/sessions.js +0 -21
  170. package/src/actor/tracker-states.js +0 -57
  171. package/src/actor/tracker.js +0 -104
  172. package/src/actor/trackers.js +0 -45
  173. package/src/actor/utils.js +0 -7
  174. package/src/actor/view.js +0 -272
  175. package/src/actor/views.js +0 -224
  176. package/src/source.js +0 -17
  177. package/src/util/proxy.js +0 -81
  178. package/src/util/trackers.js +0 -84
  179. package/src/workflow/affiliations.js +0 -121
  180. package/src/workflow/answer-based.js +0 -312
  181. package/src/workflow/ask.js +0 -284
  182. package/src/workflow/asks.js +0 -143
  183. package/src/workflow/autocompletable.js +0 -67
  184. package/src/workflow/autocomplete.js +0 -114
  185. package/src/workflow/base.js +0 -396
  186. package/src/workflow/context.js +0 -16
  187. package/src/workflow/explore.js +0 -194
  188. package/src/workflow/explores.js +0 -18
  189. package/src/workflow/hybrid-search-answer.js +0 -73
  190. package/src/workflow/hybrid-search-views.js +0 -116
  191. package/src/workflow/hybrid-search.js +0 -230
  192. package/src/workflow/index.js +0 -5
  193. package/src/workflow/options/api.js +0 -31
  194. package/src/workflow/options/auto-query.js +0 -70
  195. package/src/workflow/options/configurable.js +0 -48
  196. package/src/workflow/options/data-processor.js +0 -15
  197. package/src/workflow/options/filters.js +0 -10
  198. package/src/workflow/options/index.js +0 -11
  199. package/src/workflow/options/interactions.js +0 -36
  200. package/src/workflow/options/layouts.js +0 -73
  201. package/src/workflow/options/options.js +0 -175
  202. package/src/workflow/options/pagination.js +0 -18
  203. package/src/workflow/options/roles.js +0 -12
  204. package/src/workflow/options/templates.js +0 -27
  205. package/src/workflow/options/trackers.js +0 -80
  206. package/src/workflow/options/utils.js +0 -26
  207. package/src/workflow/processors.js +0 -490
  208. package/src/workflow/recommendation.js +0 -63
  209. package/src/workflow/recommendations.js +0 -14
  210. package/src/workflow/search-based.js +0 -329
  211. package/src/workflow/search.js +0 -73
  212. package/src/workflow/unit.js +0 -51
  213. package/src/workflow/units.js +0 -43
  214. package/src/workflow/use-link.js +0 -41
@@ -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,18 @@ 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
365
  // TODO: q -> value
337
366
  this._view.hub.update(fields.query(), { q: value });
367
+ if (this.options.clearOnSubmit) {
368
+ const inputElement = this._element && this._element.querySelector(`[data-role="input"]`);
369
+ if (inputElement) {
370
+ inputElement.value = '';
371
+ }
372
+ }
338
373
  this._trackSubmit(value);
339
374
  }
340
375
 
@@ -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, threadMenuBlock, message, messageQuestionBlock, messageAnswerBlock, 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
+ threadMenuBlock,
71
+ message,
72
+ messageQuestionBlock,
73
+ messageAnswerBlock,
69
74
  root,
70
75
  [STATUS.READY]: ready,
71
76
  empty: () => ``,
@@ -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,318 @@
1
+ import { Resolution, pacer, requestAnimationFrame as raf } from '@miso.ai/commons';
2
+ import { hasAnswer, getGeneratedBy } from '@miso.ai/client-sdk-workflow';
3
+ import { LAYOUT_TYPE } from '../../constants.js';
4
+ import CollectionLayout from './collection.js';
5
+ import { cursorClassName } from '../text/typewriter/utils.js';
6
+ import { messageAuthor } from '../templates.js';
7
+ import { setOrRemoveAttribute } from '../../util/dom.js';
8
+
9
+ const TYPE = LAYOUT_TYPE.MESSAGES;
10
+ const DEFAULT_CLASSNAME = 'miso-messages';
11
+ const TYPEWRITER_CLASSNAME = 'miso-typewriter';
12
+
13
+ /**
14
+ * The conversation panel of the chat history interface: a list of `message`
15
+ * items (question bubble + answer body), rendered incrementally — appended
16
+ * messages (e.g. a posted follow-up question) render as new items, while
17
+ * in-place changes are applied to existing items by a post-render pass
18
+ * (`_syncMessages`):
19
+ *
20
+ * - question texts are filled in when they arrive (the head request carries
21
+ * question ids only)
22
+ * - a finished answer body is transformed from markdown to HTML in one shot
23
+ * through the `std:ui-markdown` plugin
24
+ * - a streaming answer (a follow-up being generated, `finished: false`) is
25
+ * driven by a per-message typewriter: a progressive markdown renderer with
26
+ * a paced cursor, like the typewriter layout of the ask workflow
27
+ */
28
+ export default class MessagesLayout extends CollectionLayout {
29
+
30
+ static get type() {
31
+ return TYPE;
32
+ }
33
+
34
+ static get defaultClassName() {
35
+ return DEFAULT_CLASSNAME;
36
+ }
37
+
38
+ constructor({ className = DEFAULT_CLASSNAME, ...options } = {}) {
39
+ super({ className, ...options });
40
+ this._renderedAnswers = new WeakMap(); // answer element -> rendered markdown
41
+ this._typewriters = new WeakMap(); // answer element -> MessageTypewriter
42
+ this._readiness = new Resolution();
43
+ this._pinned = true; // whether the view sticks to the bottom on updates
44
+ this._displaying = false; // whether a typewriter is still displaying an answer
45
+ // kick off sooner
46
+ MessagesLayout.MisoClient.plugins.install('std:ui-markdown');
47
+ }
48
+
49
+ initialize(view) {
50
+ super.initialize(view);
51
+ this._setup();
52
+ }
53
+
54
+ // setup //
55
+ async _setup() {
56
+ try {
57
+ const plugin = await MessagesLayout.MisoClient.plugins.install('std:ui-markdown');
58
+ if (!this._view) {
59
+ return; // destroyed
60
+ }
61
+ this._markdown = plugin.getContext(this._view.workflow._client);
62
+ this._readiness.resolve();
63
+ } catch (e) {
64
+ this._readiness.reject(e);
65
+ }
66
+ }
67
+
68
+ async _ready() {
69
+ return this._readiness.promise;
70
+ }
71
+
72
+ // render //
73
+ async render(element, state, controls = {}) {
74
+ // capture the view state callback, so the typewriter can report the
75
+ // `ongoing` state outside the render cycle
76
+ this._notifyUpdate = controls.notifyUpdate;
77
+ await super.render(element, state, controls);
78
+ }
79
+
80
+ _afterRender(element, state) {
81
+ super._afterRender(element, state); // syncs bindings to the latest values
82
+ if (!state.incremental) {
83
+ this._displaying = false; // a fresh render (thread load) drops any ongoing typewriter
84
+ }
85
+ if (!state.incremental || state.html) {
86
+ // a fresh render (thread load) or appended items (posted follow-up):
87
+ // jump to the bottom
88
+ this._scrollToBottom({ force: true });
89
+ }
90
+ this._syncMessages(element).catch(error => console.error(error));
91
+ }
92
+
93
+ async _syncMessages(element) {
94
+ await this._ready();
95
+ const items = this._getItemElements(element);
96
+ let displaying = false;
97
+ for (let i = 0; i < items.length; i++) {
98
+ const item = items[i];
99
+ const binding = this._bindings.get(item);
100
+ if (!binding) {
101
+ continue;
102
+ }
103
+ const message = binding.value;
104
+ const last = i === items.length - 1;
105
+ this._syncQuestion(item, message);
106
+ this._syncAnswer(item, message, { last });
107
+ if (last) {
108
+ displaying = this._isDisplaying(item, message);
109
+ }
110
+ }
111
+ this._setDisplaying(displaying);
112
+ }
113
+
114
+ // whether the (last) message pair is still being displayed: its answer is
115
+ // pending, streaming, or still being typed — live (posted in this session)
116
+ // or not: an answer still generating is picked up by the answers polling
117
+ // even after switching away from the thread and back, and must keep
118
+ // blocking submission all the same
119
+ _isDisplaying(item, message) {
120
+ if (!hasAnswer(message)) {
121
+ return true; // waiting for the answer body
122
+ }
123
+ const answerElement = item.querySelector('[data-role="answer"]');
124
+ const typewriter = answerElement && this._typewriters.get(answerElement);
125
+ return typewriter ? !typewriter.done : message.finished === false;
126
+ }
127
+
128
+ _syncQuestion(item, message) {
129
+ const questionElement = item.querySelector('[data-role="question"]');
130
+ if (!questionElement) {
131
+ return;
132
+ }
133
+ // the authorship arrives with the answers response, after the stub render
134
+ setOrRemoveAttribute(questionElement, 'data-author', messageAuthor(message));
135
+ setOrRemoveAttribute(questionElement, 'data-generated-by', getGeneratedBy(message));
136
+ const { question } = message;
137
+ if (!question || questionElement.textContent === question) {
138
+ return;
139
+ }
140
+ questionElement.textContent = question;
141
+ questionElement.hidden = false;
142
+ }
143
+
144
+ _syncAnswer(item, message, { last = false } = {}) {
145
+ const answerElement = item.querySelector('[data-role="answer"]');
146
+ if (!answerElement) {
147
+ return;
148
+ }
149
+ // an ongoing typewriter keeps consuming updates until it finishes typing
150
+ const typewriter = this._typewriters.get(answerElement);
151
+ if (typewriter) {
152
+ typewriter.update(message);
153
+ return;
154
+ }
155
+ if (!hasAnswer(message)) {
156
+ return; // still loading; the spinner stays
157
+ }
158
+ if (message.live || message.finished === false) {
159
+ // an answer being generated live in this session: drive it with a
160
+ // typewriter (even if the data arrived complete in one shot)
161
+ const typewriter = new MessageTypewriter(this._markdown, answerElement, {
162
+ onUpdate: () => this._scrollToBottom(), // keep pinned to the bottom while typing
163
+ onDone: () => this._setDisplaying(false),
164
+ });
165
+ this._typewriters.set(answerElement, typewriter);
166
+ typewriter.update(message);
167
+ return;
168
+ }
169
+ // a finished answer: transform in one shot
170
+ if (this._renderedAnswers.get(answerElement) === message.answer) {
171
+ return; // already rendered
172
+ }
173
+ this._renderedAnswers.set(answerElement, message.answer);
174
+ this._transform(answerElement, message).then(() => {
175
+ last && this._scrollToBottom();
176
+ }).catch(error => {
177
+ this._renderedAnswers.delete(answerElement);
178
+ console.error(error);
179
+ });
180
+ }
181
+
182
+ async _transform(answerElement, { answer, sources }) {
183
+ answerElement.innerHTML = await this._markdown.transform(answer, sources);
184
+ }
185
+
186
+ // whether the last answer is still being displayed (typed); reported as
187
+ // the `ongoing` view state, e.g. for the search box to disable submission
188
+ _setDisplaying(displaying) {
189
+ if (this._displaying === displaying) {
190
+ return;
191
+ }
192
+ this._displaying = displaying;
193
+ this._notifyUpdate && this._notifyUpdate({ ongoing: displaying });
194
+ }
195
+
196
+ // scrolling //
197
+ /**
198
+ * Scroll the containing scrollable to the bottom. Unless forced, only
199
+ * applies while the user is pinned to the bottom — scrolling up to read
200
+ * releases the pin, scrolling back down restores it.
201
+ */
202
+ _scrollToBottom({ force = false } = {}) {
203
+ const scrollable = this._getScrollable();
204
+ if (!scrollable || !(force || this._pinned)) {
205
+ return;
206
+ }
207
+ scrollable.scrollTop = scrollable.scrollHeight;
208
+ this._pinned = true;
209
+ }
210
+
211
+ _getScrollable() {
212
+ const element = this._element;
213
+ if (!element || typeof getComputedStyle === 'undefined') {
214
+ return undefined;
215
+ }
216
+ for (let el = element; el && el.nodeType === 1 && el !== document.body; el = el.parentElement) {
217
+ const { overflowY } = getComputedStyle(el);
218
+ if (overflowY === 'auto' || overflowY === 'scroll') {
219
+ this._watchScroll(el);
220
+ return el;
221
+ }
222
+ }
223
+ return undefined;
224
+ }
225
+
226
+ _watchScroll(scrollable) {
227
+ if (this._watchedScrollable === scrollable) {
228
+ return;
229
+ }
230
+ this._unwatchScroll && this._unwatchScroll();
231
+ const onScroll = () => {
232
+ this._pinned = scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight < 40;
233
+ };
234
+ scrollable.addEventListener('scroll', onScroll, { passive: true });
235
+ this._watchedScrollable = scrollable;
236
+ const unwatch = this._unwatchScroll = () => {
237
+ scrollable.removeEventListener('scroll', onScroll);
238
+ this._watchedScrollable = undefined;
239
+ this._unwatchScroll = undefined;
240
+ };
241
+ this._unsubscribes.push(() => this._unwatchScroll === unwatch && unwatch());
242
+ }
243
+
244
+ }
245
+
246
+ // TODO: can't we just use Controller?
247
+ /**
248
+ * Types a streaming answer into an element: a progressive markdown renderer
249
+ * fed by data updates, advanced by a paced cursor on animation frames — the
250
+ * typewriting effect, without the full typewriter layout machinery.
251
+ */
252
+ class MessageTypewriter {
253
+
254
+ constructor(markdown, element, { onUpdate, onDone } = {}) {
255
+ this._element = element;
256
+ this._onUpdate = onUpdate;
257
+ this._onDone = onDone;
258
+ // the element acts as a typewriter container: it starts as the caret ref
259
+ // (the cursor class moves into the content as it types), and the preset
260
+ // stamps the `done` class on finish, which hides the caret via CSS
261
+ element.classList.add(TYPEWRITER_CLASSNAME, cursorClassName(TYPEWRITER_CLASSNAME));
262
+ this._renderer = markdown.createRenderer({
263
+ cursorClass: cursorClassName(TYPEWRITER_CLASSNAME),
264
+ getSource: index => (this._sources || [])[index],
265
+ });
266
+ this._getNextCursor = pacer();
267
+ this._rendered = this._renderer.clear(element);
268
+ this._timestamp = undefined;
269
+ this._value = '';
270
+ this._sources = undefined;
271
+ this._dataDone = false;
272
+ this._doneAt = undefined;
273
+ this._requested = false;
274
+ }
275
+
276
+ get done() {
277
+ return this._rendered.done;
278
+ }
279
+
280
+ update({ answer = '', finished, sources }) {
281
+ this._value = answer;
282
+ this._sources = sources;
283
+ this._dataDone = finished !== false;
284
+ this._requestFrame();
285
+ }
286
+
287
+ _requestFrame() {
288
+ if (this._requested || this._rendered.done) {
289
+ return;
290
+ }
291
+ this._requested = true;
292
+ raf(timestamp => this._frame(timestamp));
293
+ }
294
+
295
+ _frame(timestamp) {
296
+ this._requested = false;
297
+ if (!this._element.isConnected) {
298
+ // the element is gone (re-render, thread switch); stop typing
299
+ this._onDone && this._onDone();
300
+ return;
301
+ }
302
+ const prev = this._rendered;
303
+ const prevTimestamp = this._timestamp !== undefined ? this._timestamp : timestamp;
304
+ if (this._dataDone && this._doneAt === undefined) {
305
+ this._doneAt = timestamp;
306
+ }
307
+ const cursor = this._getNextCursor(prev.cursor, this._doneAt, prevTimestamp, timestamp);
308
+ this._rendered = this._renderer.update(this._element, prev, { value: this._value, cursor, timestamp, done: this._dataDone });
309
+ this._timestamp = timestamp;
310
+ this._onUpdate && this._onUpdate();
311
+ if (!this._rendered.done) {
312
+ this._requestFrame();
313
+ } else {
314
+ this._onDone && this._onDone();
315
+ }
316
+ }
317
+
318
+ }