@miso.ai/client-sdk-ui 1.13.2-beta.1 → 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
@@ -0,0 +1,206 @@
1
+ import { getThreadId, isThreadUnread } from '@miso.ai/client-sdk-workflow';
2
+ import { LAYOUT_TYPE } from '../../constants.js';
3
+ import CollectionLayout from './collection.js';
4
+ import { setOrRemoveAttribute } from '../../util/dom.js';
5
+ import confirm from '../../util/confirm.js';
6
+ import prompt from '../../util/prompt.js';
7
+
8
+ const TYPE = LAYOUT_TYPE.THREADS;
9
+ const DEFAULT_CLASSNAME = 'miso-threads';
10
+
11
+ // the thread template renders the <li> itself (it is the item, carrying the
12
+ // item role and state), so the stock <li> wrapping is skipped
13
+ function item(layout, state, value, index) {
14
+ const { templates, options } = layout;
15
+ return templates[options.itemType](layout, state, value, { index });
16
+ }
17
+
18
+ /**
19
+ * The thread list of the chat history interface: a list of `thread` items.
20
+ *
21
+ * Selection comes with the data: the history workflow stamps
22
+ * `selectedThreadId` into its value and the role mapping decorates each
23
+ * thread record with a `selected` flag, so any selection change flows down
24
+ * the regular data path and refreshes the view. The list renders
25
+ * incrementally (append-only); in-place state changes on existing items
26
+ * (selected, unread) are applied by `_syncSelection` after each render,
27
+ * reading the fresh values off the item bindings — no item re-render needed.
28
+ *
29
+ * Each item carries a context menu (vertical dots) with thread actions,
30
+ * each asking through a shared modal dialog util, then emitting a view event
31
+ * for the history workflow to act on: `rename` (a prompt dialog pre-filled
32
+ * with the current title) and `delete` (a confirm dialog).
33
+ */
34
+ export default class ThreadsLayout extends CollectionLayout {
35
+
36
+ static get type() {
37
+ return TYPE;
38
+ }
39
+
40
+ static get defaultClassName() {
41
+ return DEFAULT_CLASSNAME;
42
+ }
43
+
44
+ constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
45
+ super({
46
+ className,
47
+ templates: { item, ...templates },
48
+ ...options,
49
+ });
50
+ }
51
+
52
+ initialize(view) {
53
+ super.initialize(view);
54
+ // clicking outside the list closes any open context menu
55
+ if (typeof document !== 'undefined') {
56
+ const onDocumentClick = (event) => {
57
+ const element = this._element;
58
+ if (element && !element.contains(event.target)) {
59
+ this._closeMenus();
60
+ }
61
+ };
62
+ document.addEventListener('click', onDocumentClick);
63
+ this._unsubscribes.push(() => document.removeEventListener('click', onDocumentClick));
64
+ }
65
+ }
66
+
67
+ // the list is newest-first, so incremental renders PREPEND the fresh items
68
+ // (the collection layout's stock incremental mode is append-only)
69
+ _html(state, rendered, incremental) {
70
+ if (incremental) {
71
+ const values = this._getItems(state) || [];
72
+ const fresh = values.slice(0, values.length - rendered.value.length);
73
+ return fresh.length > 0 ? this.templates.items(this, state, fresh, { offset: 0 }) : '';
74
+ }
75
+ return this.templates.root(this, state);
76
+ }
77
+
78
+ _render(element, { state }, { notifyUpdate }) {
79
+ const { incremental, html } = state;
80
+ if (incremental) {
81
+ if (html) {
82
+ this._getListElement(element).insertAdjacentHTML('afterbegin', html);
83
+ } else {
84
+ notifyUpdate(false);
85
+ }
86
+ } else {
87
+ element.innerHTML = html;
88
+ }
89
+ }
90
+
91
+ _afterRender(element, state) {
92
+ super._afterRender(element, state); // syncs bindings to the latest values
93
+ this._syncItems(element);
94
+ }
95
+
96
+ // sync in-place item changes from the bound values onto the existing item
97
+ // elements: selection/unread state, and record changes that arrive without
98
+ // a re-render (rename, a placeholder thread resolving to its real record)
99
+ _syncItems(element) {
100
+ for (const item of this._getItemElements(element)) {
101
+ const binding = this._bindings.get(item);
102
+ if (!binding) {
103
+ continue;
104
+ }
105
+ const { value } = binding;
106
+ setOrRemoveAttribute(item, 'data-selected', value.selected ? '' : undefined);
107
+ setOrRemoveAttribute(item, 'data-unread', isThreadUnread(value) ? '' : undefined);
108
+ setOrRemoveAttribute(item, 'data-thread-id', getThreadId(value) || undefined);
109
+ const titleElement = item.querySelector(`.${this.className}__title`);
110
+ const title = value.title || 'Untitled';
111
+ if (titleElement && titleElement.textContent !== title) {
112
+ titleElement.textContent = title;
113
+ }
114
+ }
115
+ }
116
+
117
+ // a click on a thread item means selection — a navigation action, not a
118
+ // content-engagement click: emit a select view event and skip click tracking
119
+ _onClick(event) {
120
+ if (event.target.closest(`[data-role="thread-menu-button"]`)) {
121
+ this._toggleMenu(event.target.closest(`[data-role="item"]`));
122
+ return;
123
+ }
124
+ if (event.target.closest(`[data-role="thread-rename"]`)) {
125
+ this._closeMenus();
126
+ const binding = this._bindings.get(event.target.closest(`[data-role="item"]`));
127
+ binding && this._requestRename(binding);
128
+ return;
129
+ }
130
+ if (event.target.closest(`[data-role="thread-delete"]`)) {
131
+ this._closeMenus();
132
+ const binding = this._bindings.get(event.target.closest(`[data-role="item"]`));
133
+ binding && this._requestDelete(binding);
134
+ return;
135
+ }
136
+ if (event.target.closest(`[data-role="thread-menu"]`)) {
137
+ return; // other clicks inside the menu don't select
138
+ }
139
+ this._closeMenus();
140
+ const element = event.target.closest(`[data-role="item"]`);
141
+ if (!element) {
142
+ return;
143
+ }
144
+ const binding = this._bindings.get(element);
145
+ if (!binding) {
146
+ return;
147
+ }
148
+ const { session } = this._view._state;
149
+ const { value } = binding;
150
+ this._view._emit('select', { session, value, element: binding.element, domEvent: event });
151
+ }
152
+
153
+ // context menu //
154
+ _toggleMenu(item) {
155
+ const menu = item && item.querySelector(`[data-role="thread-menu"]`);
156
+ if (!menu) {
157
+ return;
158
+ }
159
+ const open = menu.hidden;
160
+ this._closeMenus();
161
+ menu.hidden = !open;
162
+ }
163
+
164
+ _closeMenus() {
165
+ const element = this._element;
166
+ if (!element) {
167
+ return;
168
+ }
169
+ for (const menu of element.querySelectorAll(`[data-role="thread-menu"]:not([hidden])`)) {
170
+ menu.hidden = true;
171
+ }
172
+ }
173
+
174
+ // rename //
175
+ async _requestRename(binding) {
176
+ const { value, element } = binding;
177
+ const title = await prompt({
178
+ title: 'Rename thread',
179
+ value: value.title || '',
180
+ placeholder: 'Thread name',
181
+ confirmText: 'Rename',
182
+ });
183
+ if (!title || title === value.title) {
184
+ return; // cancelled, cleared, or unchanged
185
+ }
186
+ const { session } = this._view._state;
187
+ this._view._emit('rename', { session, value, element, title });
188
+ }
189
+
190
+ // delete //
191
+ async _requestDelete(binding) {
192
+ const { value, element } = binding;
193
+ const confirmed = await confirm({
194
+ title: 'Delete thread',
195
+ message: `Are you sure you want to delete "${value.title || 'this thread'}"? This cannot be undone.`,
196
+ confirmText: 'Delete',
197
+ danger: true,
198
+ });
199
+ if (!confirmed) {
200
+ return;
201
+ }
202
+ const { session } = this._view._state;
203
+ this._view._emit('delete', { session, value, element });
204
+ }
205
+
206
+ }
@@ -1,7 +1,7 @@
1
1
  import { mixin } from '@miso.ai/commons';
2
2
  import Bindings from '../../util/bindings.js';
3
3
  import Viewables from '../../util/viewables.js';
4
- import { validateClick, markAsTracked } from '../../util/trackers.js';
4
+ import { validateClick, markAsTracked } from '@miso.ai/client-sdk-workflow';
5
5
 
6
6
  export function makeTrackable(prototype) {
7
7
  mixin(prototype, TrackableMixin.prototype);
@@ -29,8 +29,7 @@ export class TrackableMixin {
29
29
  }
30
30
 
31
31
  _getItemKey(value) {
32
- // TODO: ad-hoc!
33
- return value.product_id || value.text || value.id || value;
32
+ return this._view.tracker.getItemKey(value);
34
33
  }
35
34
 
36
35
  _getItems(state) {
@@ -1,6 +1,6 @@
1
1
  import { mixin, viewable as whenViewable } from '@miso.ai/commons';
2
2
  import { STATUS } from '../../constants.js';
3
- import { fields } from '../../actor/index.js';
3
+ import { fields } from '@miso.ai/client-sdk-workflow';
4
4
 
5
5
  export function makeTriggerable(prototype) {
6
6
  mixin(prototype, TriggerableMixin.prototype);
@@ -1,8 +1,9 @@
1
- import { fields } from '../../actor/index.js';
1
+ import { LAYOUT_TYPE } from '../../constants.js';
2
+ import { fields } from '@miso.ai/client-sdk-workflow';
2
3
  import RadioLayout from './radio.js';
3
4
  import { THUMB } from '../svgs.js';
4
5
 
5
- const TYPE = 'feedback';
6
+ const TYPE = LAYOUT_TYPE.FEEDBACK;
6
7
  const DEFAULT_CLASSNAME = 'miso-feedback';
7
8
 
8
9
  const OPTIONS = [ 'helpful', 'unhelpful' ];
@@ -1,4 +1,6 @@
1
1
  import { escapeHtml } from '@miso.ai/commons';
2
+ import { getThreadId, isThreadUnread, getGeneratedBy } from '@miso.ai/client-sdk-workflow';
3
+ import { getIcon } from '../asset/svgs.js';
2
4
  import { ATTR_DATA_MISO_PRODUCT_ID } from '../constants.js';
3
5
 
4
6
  export function requiresImplementation(...names) {
@@ -30,6 +32,66 @@ export function question(layout, state, data) {
30
32
  return `${openTag}${data.value || data.text || data}${closeTag}`;
31
33
  }
32
34
 
35
+ // the <li> itself is the item: it carries the item role, the thread identity
36
+ // and the item state — a thread item is not a link, so no anchor body (and
37
+ // the wrapper div around it) is needed
38
+ export function thread(layout, state, data) {
39
+ const { className, templates } = layout;
40
+ const threadId = getThreadId(data);
41
+ const threadIdAttr = threadId ? ` data-thread-id="${threadId}"` : '';
42
+ const unreadAttr = isThreadUnread(data) ? ' data-unread' : '';
43
+ const selectedAttr = data.selected ? ' data-selected' : '';
44
+ const title = escapeHtml(data.title || 'Untitled');
45
+ return `<li class="${className}__item" data-role="item"${threadIdAttr}${unreadAttr}${selectedAttr}><div class="${className}__title">${title}</div>${(templates.threadMenuBlock || threadMenuBlock)(layout, data)}</li>`;
46
+ }
47
+
48
+ export function threadMenuBlock({ className }) {
49
+ return `<button type="button" class="${className}__menu-button" data-role="thread-menu-button" aria-label="Thread actions" aria-haspopup="menu">${getIcon('dots-vertical')}</button>` +
50
+ `<div class="${className}__menu" data-role="thread-menu" role="menu" hidden>` +
51
+ `<button type="button" class="${className}__menu-item" data-role="thread-rename" role="menuitem">Rename</button>` +
52
+ `<button type="button" class="${className}__menu-item ${className}__menu-item--danger" data-role="thread-delete" role="menuitem">Delete</button>` +
53
+ `</div>`;
54
+ }
55
+
56
+ export function message(layout, state, data) {
57
+ const { className, templates } = layout;
58
+ return [
59
+ `<div class="${className}__item-body" data-role="item">`,
60
+ (templates.messageQuestionBlock || messageQuestionBlock)(layout, data),
61
+ (templates.messageAnswerBlock || messageAnswerBlock)(layout, data),
62
+ `</div>`,
63
+ ].join('');
64
+ }
65
+
66
+ export function messageQuestionBlock({ className }, message) {
67
+ const { question } = message;
68
+ const generatedBy = getGeneratedBy(message);
69
+ const author = messageAuthor(message);
70
+ // always rendered so it can be filled in place when the question text
71
+ // arrives later (the list renders incrementally); the authorship attrs
72
+ // drive the author label and the bubble color
73
+ const attrs = `${author ? ` data-author="${escapeHtml(author)}"` : ''}${generatedBy ? ` data-generated-by="${escapeHtml(generatedBy)}"` : ''}`;
74
+ return `<div class="${className}__question" data-role="question"${attrs}${question ? '' : ' hidden'}>${question ? escapeHtml(question) : ''}</div>`;
75
+ }
76
+
77
+ /**
78
+ * The author label of a message's question bubble: labeled only when the
79
+ * question was written by the answer-updates monitor — the user's own
80
+ * questions carry no label.
81
+ */
82
+ export function messageAuthor(message) {
83
+ return getGeneratedBy(message) === 'answer_update_monitor' ? 'Written by Miso' : undefined;
84
+ }
85
+
86
+ export function messageAnswerBlock({ className }, { answer }) {
87
+ // the content is left blank here: the layout fills it in a second pass
88
+ // (markdown -> HTML); a spinner shows while the answer body is still
89
+ // being fetched from the answers API
90
+ const content = answer === undefined ?
91
+ `<div class="miso-loading" aria-label="Loading" data-role="loading"></div>` : '';
92
+ return `<div class="${className}__answer miso-markdown miso-circled-citation-index" data-role="answer">${content}</div>`;
93
+ }
94
+
33
95
  export function article(layout, state, data, meta) {
34
96
  const { templates } = layout;
35
97
  const [openTag, closeTag] = tagPair(layout, data);
@@ -1,7 +1,8 @@
1
+ import { LAYOUT_TYPE } from '../../constants.js';
1
2
  import { escapeHtml } from '@miso.ai/commons';
2
3
  import TemplateBasedLayout from '../template.js';
3
4
 
4
- const TYPE = 'text';
5
+ const TYPE = LAYOUT_TYPE.TEXT;
5
6
  const DEFAULT_CLASSNAME = 'miso-text';
6
7
 
7
8
  function root(layout, state) {
@@ -1,10 +1,10 @@
1
1
  import { trimObj, defineValues, Resolution, pacer } from '@miso.ai/commons';
2
- import { STATUS } from '../../../constants.js';
2
+ import { STATUS, LAYOUT_TYPE } from '../../../constants.js';
3
3
  import ProgressiveLayout from '../../progressive.js';
4
4
  import PlaintextRenderer from './plaintext.js';
5
5
  import { containerElement, cursorClassName, fromSameSession, normalizeOnDebug } from './utils.js';
6
6
 
7
- const TYPE = 'typewriter';
7
+ const TYPE = LAYOUT_TYPE.TYPEWRITER;
8
8
  const DEFAULT_CLASSNAME = 'miso-typewriter';
9
9
 
10
10
  export default class TypewriterLayout extends ProgressiveLayout {
@@ -67,10 +67,12 @@ export default class TypewriterLayout extends ProgressiveLayout {
67
67
  }
68
68
 
69
69
  async _setupForMarkdown() {
70
- const context = await this._view._views._extensions.require('markdown');
70
+ const client = this._view.workflow._client;
71
+ const plugin = await TypewriterLayout.MisoClient.plugins.install('std:ui-markdown');
71
72
  if (!this._view) {
72
73
  return; // destroyed
73
74
  }
75
+ const context = plugin.getContext(client);
74
76
  const cursorClass = cursorClassName(this.className);
75
77
  const { onDebug, onCitationLink, variant } = this.options;
76
78
  // TODO: options
package/src/plugin.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { Component, Resolution, defineAndUpgrade, delegateGetters, defineValues } from '@miso.ai/commons';
2
- import { Asks, HybridSearch, Explores, Search, Recommendations } from './workflow/index.js';
2
+ import { WorkflowPlugin } from '@miso.ai/client-sdk-workflow';
3
+ import { defaultLayouts } from './defaults.js';
3
4
  import { AskCombo } from './combo/index.js';
4
5
  import Layouts from './layouts.js';
5
6
  import * as elements from './element/index.js';
6
7
  import * as layouts from './layout/index.js';
7
- import * as sources from './source.js';
8
8
  import * as defaults from './defaults/index.js';
9
+ import { followUp as followUpTemplate } from './defaults/ask/templates.js';
9
10
  import * as traits from './trait/index.js';
10
11
  import { loadStylesIfNecessary } from './styles.js';
12
+ import confirm from './util/confirm.js';
13
+ import prompt from './util/prompt.js';
11
14
 
12
15
  import MisoContainerElement from './element/container/miso-container.js';
13
16
  import MisoContextElement from './element/context/miso-context.js';
@@ -24,20 +27,24 @@ export default class UiPlugin extends Component {
24
27
  constructor() {
25
28
  super('ui');
26
29
  this.layouts = new Layouts(this);
27
- this._recommendations = new WeakMap();
28
- this._explores = new WeakMap();
29
- this._asks = new WeakMap();
30
- this._extensions = new WeakMap();
31
-
32
30
  this._ready = new Resolution();
33
31
  }
34
32
 
35
- async install(MisoClient, { addSubtree, addExtensionPoints }) {
33
+ async install(MisoClient, context) {
34
+ const { addSubtree, addExtensionPoints } = context;
35
+ this._pluginContext = context;
36
36
  addSubtree(this);
37
+
38
+ // make sure the workflow plugin is installed, then provide the layout
39
+ // registry and seed the default layout options
40
+ MisoClient.plugins.use(WorkflowPlugin);
41
+ context.workflows.plugin.setLayouts(this.layouts);
42
+ this._seedWorkflowDefaults(context.workflows.defaults);
43
+
37
44
  MisoClient.on('create', this._injectClient.bind(this));
38
45
 
39
46
  // the UI interface for MisoClient
40
- const ui = { defaults, traits };
47
+ const ui = { defaults, traits, confirm, prompt };
41
48
  delegateGetters(ui, this, ['layouts', 'ready']);
42
49
  defineValues(this, { MisoClient });
43
50
  defineValues(MisoClient, { ui });
@@ -78,78 +85,27 @@ export default class UiPlugin extends Component {
78
85
  this._ready.resolve();
79
86
  }
80
87
 
81
- async requireExtension(name) {
82
- switch (name) {
83
- case 'markdown':
84
- return await this.MisoClient.plugins.install('std:ui-markdown');
85
- default:
86
- throw new Error(`Unknown extension: ${name}`);
87
- }
88
- }
89
-
90
88
  get ready() {
91
89
  return this._ready.promise;
92
90
  }
93
91
 
92
+ /**
93
+ * Seed the workflow plugin's defaults store with the presentational default
94
+ * options: layouts of each workflow, plus UI templates.
95
+ */
96
+ _seedWorkflowDefaults(workflowDefaults) {
97
+ for (const [name, layouts] of Object.entries(defaultLayouts)) {
98
+ workflowDefaults.set(name, { layouts });
99
+ }
100
+ workflowDefaults.set('ask', { templates: { followUp: followUpTemplate } });
101
+ }
102
+
94
103
  _injectClient(client) {
95
104
  defineValues(client, {
96
105
  ui: new Ui(this, client),
97
106
  });
98
107
  }
99
108
 
100
- _getExtensions(client) {
101
- if (this._extensions.has(client)) {
102
- return this._extensions.get(client);
103
- }
104
- const extensions = new Extensions(this, client);
105
- this._extensions.set(client, extensions);
106
- return extensions;
107
- }
108
-
109
- _getRecommendations(client) {
110
- let recommendations = this._recommendations.get(client);
111
- if (!recommendations) {
112
- this._recommendations.set(client, recommendations = new Recommendations(this, client));
113
- }
114
- return recommendations;
115
- }
116
-
117
- _getExplores(client) {
118
- let explores = this._explores.get(client);
119
- if (!explores) {
120
- this._explores.set(client, explores = new Explores(this, client));
121
- }
122
- return explores;
123
- }
124
-
125
- _getAsks(client) {
126
- let asks = this._asks.get(client);
127
- if (!asks) {
128
- this._asks.set(client, asks = new Asks(this, client));
129
- }
130
- return asks;
131
- }
132
-
133
- }
134
-
135
- // TODO: are we able to eliminate the concept of extension, just use plugins?
136
- class Extensions {
137
-
138
- constructor(plugin, client) {
139
- this._plugin = plugin;
140
- this._client = client;
141
- this._contexts = {};
142
- }
143
-
144
- async require(name) {
145
- if (this._contexts[name]) {
146
- return this._contexts[name];
147
- }
148
- const plugin = await this._plugin.requireExtension(name);
149
- const context = this._contexts[name] = plugin.getContext(this._client);
150
- return context;
151
- }
152
-
153
109
  }
154
110
 
155
111
  function createUiPluginContext(plugin, ui, MisoClient) {
@@ -174,6 +130,9 @@ function createUiPluginContext(plugin, ui, MisoClient) {
174
130
  return { plugin, addElementClasses, addLayoutClasses, addInterface };
175
131
  }
176
132
 
133
+ // workflow accessors delegated to client.workflows for backward compatibility
134
+ const WORKFLOW_ACCESSORS = ['search', 'hybridSearch', 'asks', 'ask', 'explores', 'explore', 'recommendations', 'recommendation', 'sources'];
135
+
177
136
  /**
178
137
  * The UI interface for the client.
179
138
  */
@@ -182,52 +141,7 @@ class Ui {
182
141
  constructor(plugin, client) {
183
142
  this._plugin = plugin;
184
143
  this._client = client;
185
-
186
- defineValues(this, {
187
- sources: {
188
- api: sources.api(client),
189
- },
190
- });
191
- }
192
-
193
- get recommendations() {
194
- return this._recommendations || (this._recommendations = this._plugin._getRecommendations(this._client));
195
- }
196
-
197
- get recommendation() {
198
- return this.recommendations.get(); // get default recommendation unit
199
- }
200
-
201
- get search() {
202
- if (!this._search) {
203
- this._search = new Search(this._plugin, this._client);
204
- this._client._events.emit('postworkflow', this._search);
205
- }
206
- return this._search;
207
- }
208
-
209
- get asks() {
210
- return this._asks || (this._asks = this._plugin._getAsks(this._client));
211
- }
212
-
213
- get ask() {
214
- return this.asks.root;
215
- }
216
-
217
- get hybridSearch() {
218
- if (!this._hybridSearch) {
219
- this._hybridSearch = new HybridSearch(this._plugin, this._client);
220
- this._client._events.emit('postworkflow', this._hybridSearch);
221
- }
222
- return this._hybridSearch;
223
- }
224
-
225
- get explores() {
226
- return this._explores || (this._explores = this._plugin._getExplores(this._client));
227
- }
228
-
229
- get explore() {
230
- return this.explores.get(); // get default explore unit
144
+ delegateGetters(this, client.workflows, WORKFLOW_ACCESSORS);
231
145
  }
232
146
 
233
147
  get ready() {
@@ -0,0 +1,28 @@
1
+ import { openModal } from './modal.js';
2
+
3
+ const DEFAULT_CLASSNAME = 'miso-confirm';
4
+
5
+ /**
6
+ * Show a modal confirmation dialog (a native <dialog>), resolving to whether
7
+ * the user confirmed. Cancel button, backdrop click, and the Escape key all
8
+ * resolve to false. Exposed as `MisoClient.ui.confirm()`.
9
+ */
10
+ export default function confirm({
11
+ title,
12
+ message,
13
+ confirmText = 'Confirm',
14
+ cancelText = 'Cancel',
15
+ danger = false,
16
+ className = DEFAULT_CLASSNAME,
17
+ } = {}) {
18
+ return openModal({
19
+ className,
20
+ title,
21
+ message,
22
+ confirmText,
23
+ cancelText,
24
+ danger,
25
+ result: () => true,
26
+ cancelValue: false,
27
+ });
28
+ }
@@ -0,0 +1,49 @@
1
+ import { escapeHtml } from '@miso.ai/commons';
2
+
3
+ /**
4
+ * The shared modal dialog core of confirm() and prompt(): a native <dialog>
5
+ * with title, message, an optional body, and cancel/confirm actions. Resolves
6
+ * with `result(dialog)` when confirmed; the cancel button, a backdrop click,
7
+ * and the Escape key all resolve with `cancelValue`.
8
+ */
9
+ export function openModal({
10
+ className,
11
+ title,
12
+ message,
13
+ body = '',
14
+ confirmText = 'Confirm',
15
+ cancelText = 'Cancel',
16
+ danger = false,
17
+ onOpen,
18
+ result = () => true,
19
+ cancelValue,
20
+ } = {}) {
21
+ return new Promise((resolve) => {
22
+ const dialog = document.createElement('dialog');
23
+ dialog.className = className;
24
+ dialog.innerHTML = [
25
+ title ? `<div class="${className}__title">${escapeHtml(title)}</div>` : '',
26
+ message ? `<div class="${className}__message">${escapeHtml(message)}</div>` : '',
27
+ body,
28
+ `<div class="${className}__actions">`,
29
+ `<button type="button" class="${className}__button ${className}__cancel-button" data-role="cancel">${escapeHtml(cancelText)}</button>`,
30
+ `<button type="button" class="${className}__button ${className}__confirm-button${danger ? ` ${className}__confirm-button--danger` : ''}" data-role="confirm">${escapeHtml(confirmText)}</button>`,
31
+ `</div>`,
32
+ ].join('');
33
+ dialog.addEventListener('click', (event) => {
34
+ if (event.target.closest('[data-role="confirm"]')) {
35
+ dialog.close('confirm');
36
+ } else if (event.target.closest('[data-role="cancel"]') || event.target === dialog) {
37
+ dialog.close(); // cancel button or backdrop click
38
+ }
39
+ });
40
+ dialog.addEventListener('close', () => {
41
+ const value = dialog.returnValue === 'confirm' ? result(dialog) : cancelValue;
42
+ dialog.remove();
43
+ resolve(value);
44
+ }, { once: true });
45
+ document.body.appendChild(dialog);
46
+ dialog.showModal();
47
+ onOpen && onOpen(dialog);
48
+ });
49
+ }
@@ -0,0 +1,42 @@
1
+ import { escapeHtml } from '@miso.ai/commons';
2
+ import { openModal } from './modal.js';
3
+
4
+ const DEFAULT_CLASSNAME = 'miso-prompt';
5
+
6
+ /**
7
+ * Show a modal input dialog (a native <dialog>), resolving to the entered
8
+ * (trimmed) text when confirmed. Cancel button, backdrop click, and the
9
+ * Escape key all resolve to undefined. The Enter key confirms. Exposed as
10
+ * `MisoClient.ui.prompt()`.
11
+ */
12
+ export default function prompt({
13
+ title,
14
+ message,
15
+ value = '',
16
+ placeholder,
17
+ confirmText = 'Confirm',
18
+ cancelText = 'Cancel',
19
+ className = DEFAULT_CLASSNAME,
20
+ } = {}) {
21
+ return openModal({
22
+ className,
23
+ title,
24
+ message,
25
+ body: `<input class="${className}__input" type="text" data-role="input" value="${escapeHtml(value)}"${placeholder ? ` placeholder="${escapeHtml(placeholder)}"` : ''}>`,
26
+ confirmText,
27
+ cancelText,
28
+ onOpen: (dialog) => {
29
+ const input = dialog.querySelector('[data-role="input"]');
30
+ input.addEventListener('keydown', (event) => {
31
+ if (event.key === 'Enter') {
32
+ event.preventDefault();
33
+ dialog.close('confirm');
34
+ }
35
+ });
36
+ input.focus();
37
+ input.select();
38
+ },
39
+ result: dialog => dialog.querySelector('[data-role="input"]').value.trim(),
40
+ cancelValue: undefined,
41
+ });
42
+ }