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

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 (54) hide show
  1. package/dist/cjs/asset/svgs.js +4 -2
  2. package/dist/cjs/constants.js +2 -0
  3. package/dist/cjs/defaults/ask/index.js +2 -1
  4. package/dist/cjs/defaults/ask/templates.js +2 -1
  5. package/dist/cjs/defaults/hybrid-search/index.js +2 -1
  6. package/dist/cjs/defaults/hybrid-search/templates.js +2 -1
  7. package/dist/cjs/defaults/index.js +2 -1
  8. package/dist/cjs/defaults.js +107 -10
  9. package/dist/cjs/element/container/index.js +14 -0
  10. package/dist/cjs/element/container/miso-message-item.js +91 -0
  11. package/dist/cjs/element/container/miso-thread-item.js +91 -0
  12. package/dist/cjs/element/index.js +2 -1
  13. package/dist/cjs/element/role/index.js +7 -0
  14. package/dist/cjs/element/role/miso-delete.js +20 -0
  15. package/dist/cjs/layout/button/button.js +45 -5
  16. package/dist/cjs/layout/button/checkbox.js +2 -1
  17. package/dist/cjs/layout/container/item-container.js +99 -0
  18. package/dist/cjs/layout/index.js +14 -0
  19. package/dist/cjs/layout/input/search-box.js +9 -4
  20. package/dist/cjs/layout/list/collection.js +21 -4
  21. package/dist/cjs/layout/list/messages.js +261 -224
  22. package/dist/cjs/layout/list/threads.js +106 -150
  23. package/dist/cjs/layout/templates.js +98 -38
  24. package/dist/cjs/layout/text/question.js +71 -0
  25. package/dist/cjs/layout/text/typewriter/index.js +15 -21
  26. package/dist/cjs/plugin.js +6 -3
  27. package/dist/cjs/util/dom.js +21 -0
  28. package/dist/cjs/util/notify.js +74 -0
  29. package/dist/cjs/version.js +1 -1
  30. package/package.json +3 -3
  31. package/src/CLAUDE.md +7 -2
  32. package/src/asset/svgs.js +3 -1
  33. package/src/constants.js +2 -0
  34. package/src/defaults.js +75 -5
  35. package/src/element/container/index.js +2 -0
  36. package/src/element/container/miso-message-item.js +94 -0
  37. package/src/element/container/miso-thread-item.js +94 -0
  38. package/src/element/role/index.js +1 -0
  39. package/src/element/role/miso-delete.js +16 -0
  40. package/src/layout/button/button.js +40 -4
  41. package/src/layout/button/checkbox.js +2 -1
  42. package/src/layout/container/item-container.js +99 -0
  43. package/src/layout/index.js +2 -0
  44. package/src/layout/input/search-box.js +4 -2
  45. package/src/layout/list/collection.js +22 -5
  46. package/src/layout/list/messages.js +231 -201
  47. package/src/layout/list/threads.js +107 -128
  48. package/src/layout/templates.js +100 -38
  49. package/src/layout/text/question.js +58 -0
  50. package/src/layout/text/typewriter/index.js +11 -15
  51. package/src/plugin.js +3 -2
  52. package/src/util/dom.js +16 -0
  53. package/src/util/notify.js +68 -0
  54. package/src/version.js +1 -1
@@ -1,35 +1,51 @@
1
- import { getThreadId, isThreadUnread } from '@miso.ai/client-sdk-workflow';
2
- import { LAYOUT_TYPE } from '../../constants.js';
1
+ import { isThreadUnread } from '@miso.ai/client-sdk-workflow';
2
+ import { STATUS, LAYOUT_TYPE } from '../../constants.js';
3
3
  import CollectionLayout from './collection.js';
4
+ import { listError, isSessionExpired, SESSION_EXPIRED_MESSAGE } from '../templates.js';
4
5
  import { setOrRemoveAttribute } from '../../util/dom.js';
5
- import confirm from '../../util/confirm.js';
6
- import prompt from '../../util/prompt.js';
7
6
 
8
7
  const TYPE = LAYOUT_TYPE.THREADS;
9
8
  const DEFAULT_CLASSNAME = 'miso-threads';
10
9
 
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 });
10
+ const DEFAULT_TEMPLATES = Object.freeze({
11
+ [STATUS.ERRONEOUS]: listError,
12
+ errorMessage,
13
+ });
14
+
15
+ const INHERITED_DEFAULT_TEMPLATES = Object.freeze({
16
+ ...CollectionLayout.defaultTemplates,
17
+ ...DEFAULT_TEMPLATES,
18
+ });
19
+
20
+ function errorMessage(layout, error) {
21
+ return isSessionExpired(error) ? SESSION_EXPIRED_MESSAGE : 'Your conversations could not be loaded.';
16
22
  }
17
23
 
18
24
  /**
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.
25
+ * The thread list of the chat history interface: a shell that renders one
26
+ * <miso-thread-item> container element per thread item, incrementally —
27
+ * fresh items land at either end of the list: a newly created thread
28
+ * renders as a prepended item (the list is newest-first), while an older
29
+ * page fetched by the more flow (infinite scroll: the collection layout's
30
+ * stock trigger, viewable at the list's bottom, fires the `more` hub
31
+ * field) renders as appended items — told apart by which boundary the
32
+ * already-rendered records line up with. The content
33
+ * of an item is not rendered here: each <miso-thread-item> is assigned its item
34
+ * subworkflow (workflow._getThreadWorkflow, off the item binding) in the
35
+ * post-render sync pass, and the role elements inside (the title, and the
36
+ * context menu's rename/delete buttons) render through that workflow's own
37
+ * layouts — so a record change re-renders the affected item's roles alone.
28
38
  *
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).
39
+ * The shell keeps the list-level behaviors. Selection comes with the data:
40
+ * the history workflow stamps `selectedThreadId` into its value and
41
+ * decorates each thread record with a `selected` flag, so any selection
42
+ * change flows down the regular data path; in-place state changes on
43
+ * existing items (selected, unread, the thread identity of a settled
44
+ * placeholder) are applied by `_syncItems` after each render, reading the
45
+ * fresh values off the item bindings — no item re-render needed. Each item
46
+ * carries a context menu (vertical dots), but that is the thread workflow's
47
+ * too: its item-container layout inserts and toggles it — the shell only
48
+ * keeps menu clicks (by their item-menu data-roles) from selecting.
33
49
  */
34
50
  export default class ThreadsLayout extends CollectionLayout {
35
51
 
@@ -41,45 +57,67 @@ export default class ThreadsLayout extends CollectionLayout {
41
57
  return DEFAULT_CLASSNAME;
42
58
  }
43
59
 
60
+ static get defaultTemplates() {
61
+ return INHERITED_DEFAULT_TEMPLATES;
62
+ }
63
+
44
64
  constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
45
- super({
46
- className,
47
- templates: { item, ...templates },
48
- ...options,
49
- });
65
+ super({ className, templates: { ...DEFAULT_TEMPLATES, ...templates }, ...options });
50
66
  }
51
67
 
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));
68
+ // item identity: the thread id, or the local placeholder id standing in
69
+ // for it while a thread is being created — the workflow adopts the thread
70
+ // id at the settle, so the element and its workflow survive it
71
+ _getItemKey(thread) {
72
+ return thread.thread_id || thread.placeholder_id || thread;
73
+ }
74
+
75
+ // incremental renders cover a pure prepend (a created thread) or a pure
76
+ // append (an older page) — anything else re-renders in full
77
+ _shallRenderIncrementally(state, rendered) {
78
+ if (!super._shallRenderIncrementally(state, rendered)) {
79
+ return false;
64
80
  }
81
+ const values = this._getItems(state) || [];
82
+ const renderedValues = rendered.value;
83
+ const count = values.length - renderedValues.length;
84
+ return count === 0 || // in-place changes only
85
+ this._getItemKey(values[0]) === this._getItemKey(renderedValues[0]) || // appended
86
+ this._getItemKey(values[count]) === this._getItemKey(renderedValues[0]); // prepended
65
87
  }
66
88
 
67
- // the list is newest-first, so incremental renders PREPEND the fresh items
68
- // (the collection layout's stock incremental mode is append-only)
89
+ // the fresh items land at the head (a newly created thread — the list is
90
+ // newest-first) or at the tail (an older page fetched by the more flow):
91
+ // told apart by which boundary the already-rendered records line up with
69
92
  _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 }) : '';
93
+ if (!incremental) {
94
+ return this.templates.root(this, state);
95
+ }
96
+ const values = this._getItems(state) || [];
97
+ const renderedValues = rendered.value;
98
+ const count = values.length - renderedValues.length;
99
+ if (count === 0) {
100
+ return '';
101
+ }
102
+ if (this._getItemKey(values[0]) === this._getItemKey(renderedValues[0])) {
103
+ const fresh = values.slice(renderedValues.length);
104
+ return {
105
+ position: 'beforeend',
106
+ items: this.templates.items(this, state, fresh, { offset: renderedValues.length }),
107
+ };
74
108
  }
75
- return this.templates.root(this, state);
109
+ const fresh = values.slice(0, count);
110
+ return {
111
+ position: 'afterbegin',
112
+ items: this.templates.items(this, state, fresh, { offset: 0 }),
113
+ };
76
114
  }
77
115
 
78
116
  _render(element, { state }, { notifyUpdate }) {
79
117
  const { incremental, html } = state;
80
118
  if (incremental) {
81
119
  if (html) {
82
- this._getListElement(element).insertAdjacentHTML('afterbegin', html);
120
+ this._getListElement(element).insertAdjacentHTML(html.position, html.items);
83
121
  } else {
84
122
  notifyUpdate(false);
85
123
  }
@@ -90,12 +128,28 @@ export default class ThreadsLayout extends CollectionLayout {
90
128
 
91
129
  _afterRender(element, state) {
92
130
  super._afterRender(element, state); // syncs bindings to the latest values
131
+ this._syncWorkflows(element);
93
132
  this._syncItems(element);
94
133
  }
95
134
 
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)
135
+ // assign each <miso-thread-item> its item subworkflow, off the item binding
136
+ _syncWorkflows(element) {
137
+ const workflow = this._view && this._view.workflow;
138
+ if (!workflow || typeof workflow._getThreadWorkflow !== 'function') {
139
+ return;
140
+ }
141
+ for (const item of this._getItemElements(element)) {
142
+ const binding = this._bindings.get(item);
143
+ if (binding && item.isContainer) {
144
+ item.workflow = workflow._getThreadWorkflow(binding.value);
145
+ }
146
+ }
147
+ }
148
+
149
+ // sync in-place item state changes from the bound values onto the existing
150
+ // item elements: selection/unread state, and the thread identity of a
151
+ // placeholder that settled into its real record — the item contents render
152
+ // through the thread workflows, outside this layout's render cycle
99
153
  _syncItems(element) {
100
154
  for (const item of this._getItemElements(element)) {
101
155
  const binding = this._bindings.get(item);
@@ -105,38 +159,16 @@ export default class ThreadsLayout extends CollectionLayout {
105
159
  const { value } = binding;
106
160
  setOrRemoveAttribute(item, 'data-selected', value.selected ? '' : undefined);
107
161
  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
- }
162
+ setOrRemoveAttribute(item, 'data-thread-id', value.thread_id || undefined);
114
163
  }
115
164
  }
116
165
 
117
166
  // a click on a thread item means selection — a navigation action, not a
118
167
  // content-engagement click: emit a select view event and skip click tracking
119
168
  _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;
169
+ if (event.target.closest(`[data-role="item-menu-button"], [data-role="item-menu"]`)) {
170
+ return; // the item's own context menu, owned by its container layout
135
171
  }
136
- if (event.target.closest(`[data-role="thread-menu"]`)) {
137
- return; // other clicks inside the menu don't select
138
- }
139
- this._closeMenus();
140
172
  const element = event.target.closest(`[data-role="item"]`);
141
173
  if (!element) {
142
174
  return;
@@ -150,57 +182,4 @@ export default class ThreadsLayout extends CollectionLayout {
150
182
  this._view._emit('select', { session, value, element: binding.element, domEvent: event });
151
183
  }
152
184
 
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
185
  }
@@ -1,5 +1,5 @@
1
1
  import { escapeHtml } from '@miso.ai/commons';
2
- import { getThreadId, isThreadUnread, getGeneratedBy } from '@miso.ai/client-sdk-workflow';
2
+ import { isThreadUnread } from '@miso.ai/client-sdk-workflow';
3
3
  import { getIcon } from '../asset/svgs.js';
4
4
  import { ATTR_DATA_MISO_PRODUCT_ID } from '../constants.js';
5
5
 
@@ -32,66 +32,128 @@ export function question(layout, state, data) {
32
32
  return `${openTag}${data.value || data.text || data}${closeTag}`;
33
33
  }
34
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
35
+ /**
36
+ * A thread item is a <miso-thread-item> container element hosting the thread
37
+ * item subworkflow — the threads layout is a shell: it assigns the workflow
38
+ * (and syncs the item-level attributes) in its post-render pass, and the
39
+ * content renders through the thread workflow's own layouts: the title role
40
+ * element in the body, plus the context menu (with the
41
+ * rename/subscription/delete role elements) inserted as chrome by the workflow's item-container layout. The
42
+ * item state renders as attributes right in the template (and stays in
43
+ * sync thereafter): the thread identity, the selection, and the unread
44
+ * mark.
45
+ */
38
46
  export function thread(layout, state, data) {
39
47
  const { className, templates } = layout;
40
- const threadId = getThreadId(data);
48
+ const threadId = data.thread_id;
41
49
  const threadIdAttr = threadId ? ` data-thread-id="${threadId}"` : '';
42
50
  const unreadAttr = isThreadUnread(data) ? ' data-unread' : '';
43
51
  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>`;
52
+ return [
53
+ `<miso-thread-item class="${className}__item-body" data-role="item"${threadIdAttr}${unreadAttr}${selectedAttr}>`,
54
+ (templates.threadBody || threadBody)(layout, data),
55
+ `</miso-thread-item>`,
56
+ ].join('');
57
+ }
58
+
59
+ export function threadBody(layout, data) {
60
+ const { className, templates } = layout;
61
+ return `<miso-title class="${className}__title"></miso-title>` +
62
+ (templates.threadMenuBlock || threadMenuBlock)(layout, data);
46
63
  }
47
64
 
65
+ // the item-menu data-roles are the contract with the thread workflow's
66
+ // item-container layout, which drives the menu's open/close behavior; the
67
+ // menu items are the thread workflow's own controls, rendered by the
68
+ // generic button layout with a prompt (rename) or a confirm (delete) dialog,
69
+ // and by the checkbox layout (the subscription toggle)
48
70
  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>` +
71
+ return `<button type="button" class="${className}__menu-button" data-role="item-menu-button" aria-label="Thread actions" aria-haspopup="menu">${getIcon('dots-vertical')}</button>` +
72
+ `<div class="${className}__menu" data-role="item-menu" role="menu" hidden>` +
73
+ `<miso-rename class="${className}__menu-item"></miso-rename>` +
74
+ `<miso-subscription class="${className}__menu-item"></miso-subscription>` +
75
+ `<miso-delete class="${className}__menu-item ${className}__menu-item--danger"></miso-delete>` +
53
76
  `</div>`;
54
77
  }
55
78
 
79
+ /**
80
+ * A message item is a <miso-message-item> container element hosting the message
81
+ * item subworkflow — the messages layout is a shell: it assigns the workflow
82
+ * (and the item-level attributes) in its post-render sync pass, and the role
83
+ * elements inside render the actual content through the message workflow's
84
+ * own layouts. The item state renders as an attribute right in the template
85
+ * (and stays in sync thereafter): the live mark, for a message generated in
86
+ * this session.
87
+ */
56
88
  export function message(layout, state, data) {
57
89
  const { className, templates } = layout;
90
+ const liveAttr = data.live ? ' data-live' : '';
91
+ // miso-circled-citation-index covers both the citation links in the answer
92
+ // and the indexes on the source cards, as the hybrid-search UI scopes it
58
93
  return [
59
- `<div class="${className}__item-body" data-role="item">`,
60
- (templates.messageQuestionBlock || messageQuestionBlock)(layout, data),
61
- (templates.messageAnswerBlock || messageAnswerBlock)(layout, data),
62
- `</div>`,
94
+ `<miso-message-item class="${className}__item-body miso-circled-citation-index" data-role="item"${liveAttr}>`,
95
+ (templates.messageBody || messageBody)(layout, data),
96
+ `</miso-message-item>`,
63
97
  ].join('');
64
98
  }
65
99
 
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>`;
100
+ // the regular content is packed together, so the erroneous presentation
101
+ // swaps it out wholesale for the error notice (the content wrapper hides by
102
+ // a class rule off the item's status — visible-when suits the notice, but
103
+ // not a wrapper: it forces `display: block !important` and only negates in
104
+ // combination with positive terms)
105
+ export function messageBody(layout) {
106
+ const { className, templates } = layout;
107
+ return `<div class="${className}__content">` +
108
+ `<miso-question class="${className}__question" hidden></miso-question>` +
109
+ `<miso-answer class="${className}__answer"></miso-answer>` +
110
+ `<miso-feedback class="${className}__feedback"></miso-feedback>` +
111
+ (templates.messageSourcesBlock || messageSourcesBlock)(layout) +
112
+ `</div>` +
113
+ (templates.messageErrorBlock || messageErrorBlock)(layout);
114
+ }
115
+
116
+ // the sources with their heading, as the hybrid-search UI presents them —
117
+ // shown once the item carries content (visible-when nonempty), so no
118
+ // orphan heading sits over a message still loading
119
+ export function messageSourcesBlock({ className }) {
120
+ return `<div class="${className}__sources-container" visible-when="nonempty">` +
121
+ `<h3 class="${className}__sources-phrase">Sources</h3>` +
122
+ `<miso-sources class="${className}__sources"></miso-sources>` +
123
+ `</div>`;
124
+ }
125
+
126
+ // shown by the item's own status (visible-when): a message whose content
127
+ // fetch, or (a live one's) posting or answer polling, failed for good
128
+ // presents this in place of its content — the text through the item's own
129
+ // error role element, off the record's error mark (messageErrorMessage)
130
+ export function messageErrorBlock({ className }) {
131
+ return `<div class="${className}__error" visible-when="erroneous"><miso-error></miso-error></div>`;
75
132
  }
76
133
 
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;
134
+ // the error layout's message template for a message item (seeded in the
135
+ // 'conversation' layout defaults): an expired session reads apart from other
136
+ // failures
137
+ export function messageErrorMessage(layout, error) {
138
+ return isSessionExpired(error) ? SESSION_EXPIRED_MESSAGE : 'Something went wrong with this message.';
84
139
  }
85
140
 
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>`;
141
+
142
+ // the list-level error notice of the chat history interface, in place of
143
+ // the list when its load failed (the thread list, or the thread on display);
144
+ // the text comes from the layout's errorMessage template, given the failure
145
+ export function listError(layout, state) {
146
+ const { className, templates } = layout;
147
+ return `<div class="${className}__list-error" data-role="error">${templates.errorMessage(layout, state.error)}</div>`;
93
148
  }
94
149
 
150
+ // an expired session (e.g. an expired JWT), to be told apart from other failures
151
+ export function isSessionExpired(error) {
152
+ return !!error && error.status === 401;
153
+ }
154
+
155
+ export const SESSION_EXPIRED_MESSAGE = 'Your session has expired. Please sign in again.';
156
+
95
157
  export function article(layout, state, data, meta) {
96
158
  const { templates } = layout;
97
159
  const [openTag, closeTag] = tagPair(layout, data);
@@ -0,0 +1,58 @@
1
+ import { escapeHtml } from '@miso.ai/commons';
2
+ import { isUpdateMessage } from '@miso.ai/client-sdk-workflow';
3
+ import { LAYOUT_TYPE } from '../../constants.js';
4
+ import { setOrRemoveAttribute } from '../../util/dom.js';
5
+ import TextLayout from './text.js';
6
+
7
+ const TYPE = LAYOUT_TYPE.QUESTION;
8
+ const DEFAULT_CLASSNAME = 'miso-question';
9
+
10
+ // the author label: only a question written by the answer-updates monitor is
11
+ // labeled — the user's own questions carry none
12
+ function defaultAuthor(message) {
13
+ return isUpdateMessage(message) ? 'Written by Miso' : undefined;
14
+ }
15
+
16
+ function content(layout, { value }) {
17
+ const question = value && value.question;
18
+ return question ? escapeHtml(question) : '';
19
+ }
20
+
21
+ /**
22
+ * The question of a message: a text layout taking the whole message record —
23
+ * the text is the `question` field, and the authorship is stamped as data
24
+ * attributes on the host element (`data-author` drives the CSS label badge,
25
+ * `data-generated-by` the bubble color), re-applied on every render, since
26
+ * the metadata arrives with the answers response, after the stub render. The
27
+ * host element hides while there is no question text to show. The author
28
+ * label is configurable via the `author` option (message -> label text).
29
+ */
30
+ export default class QuestionLayout extends TextLayout {
31
+
32
+ static get type() {
33
+ return TYPE;
34
+ }
35
+
36
+ static get defaultClassName() {
37
+ return DEFAULT_CLASSNAME;
38
+ }
39
+
40
+ constructor({ className = DEFAULT_CLASSNAME, tag = 'div', author = defaultAuthor, templates, ...options } = {}) {
41
+ super({
42
+ className,
43
+ tag,
44
+ author,
45
+ templates: { content, ...templates },
46
+ ...options,
47
+ });
48
+ }
49
+
50
+ _afterRender(element, state) {
51
+ super._afterRender(element, state);
52
+ const message = state.value || {};
53
+ element.hidden = !message.question;
54
+ setOrRemoveAttribute(element, 'data-author', this.options.author(message));
55
+ setOrRemoveAttribute(element, 'data-generated-by', (message.metadata && message.metadata.miso_generated_by) || undefined);
56
+ }
57
+
58
+ }
@@ -1,5 +1,6 @@
1
1
  import { trimObj, defineValues, Resolution, pacer } from '@miso.ai/commons';
2
2
  import { STATUS, LAYOUT_TYPE } from '../../../constants.js';
3
+ import { dumpElementAttributes } from '../../../util/dom.js';
3
4
  import ProgressiveLayout from '../../progressive.js';
4
5
  import PlaintextRenderer from './plaintext.js';
5
6
  import { containerElement, cursorClassName, fromSameSession, normalizeOnDebug } from './utils.js';
@@ -137,6 +138,16 @@ export default class TypewriterLayout extends ProgressiveLayout {
137
138
  // new typing streak
138
139
  result = this._renderer.clear(container, rendered);
139
140
  } else {
141
+ result = rendered;
142
+ }
143
+ if (this.options.instant && !result.cursor) {
144
+ // instant mode: while nothing is typed yet, the cursor starts at the max
145
+ // available position (the renderer clamps it to the safe right bound) —
146
+ // content that arrived complete (e.g. a loaded conversation message)
147
+ // renders in one shot; ongoing content shows what is there so far at
148
+ // once, and only the part streaming in after that types out
149
+ result = this._renderer.update(container, result, { ...state, cursor: Infinity });
150
+ } else if (!newStreak) {
140
151
  const cursor = this._getNextCursor(rendered.cursor, state.doneAt, rendered.timestamp, state.timestamp);
141
152
  result = this._renderer.update(container, rendered, { ...state, cursor });
142
153
  }
@@ -195,18 +206,3 @@ export default class TypewriterLayout extends ProgressiveLayout {
195
206
  }
196
207
  }
197
208
  }
198
-
199
- function dumpElementAttributes(element) {
200
- const { attributes } = element;
201
- const attrs = {};
202
- for (const { name, value } of attributes) {
203
- if (name.startsWith('data-')) {
204
- attrs[name] = value;
205
- }
206
- }
207
- try {
208
- return JSON.stringify(attrs);
209
- } catch (e) {
210
- return '{}';
211
- }
212
- }
package/src/plugin.js CHANGED
@@ -11,6 +11,7 @@ import * as traits from './trait/index.js';
11
11
  import { loadStylesIfNecessary } from './styles.js';
12
12
  import confirm from './util/confirm.js';
13
13
  import prompt from './util/prompt.js';
14
+ import notify from './util/notify.js';
14
15
 
15
16
  import MisoContainerElement from './element/container/miso-container.js';
16
17
  import MisoContextElement from './element/context/miso-context.js';
@@ -44,7 +45,7 @@ export default class UiPlugin extends Component {
44
45
  MisoClient.on('create', this._injectClient.bind(this));
45
46
 
46
47
  // the UI interface for MisoClient
47
- const ui = { defaults, traits, confirm, prompt };
48
+ const ui = { defaults, traits, confirm, prompt, notify };
48
49
  delegateGetters(ui, this, ['layouts', 'ready']);
49
50
  defineValues(this, { MisoClient });
50
51
  defineValues(MisoClient, { ui });
@@ -131,7 +132,7 @@ function createUiPluginContext(plugin, ui, MisoClient) {
131
132
  }
132
133
 
133
134
  // workflow accessors delegated to client.workflows for backward compatibility
134
- const WORKFLOW_ACCESSORS = ['search', 'hybridSearch', 'asks', 'ask', 'explores', 'explore', 'recommendations', 'recommendation', 'sources'];
135
+ const WORKFLOW_ACCESSORS = ['search', 'hybridSearch', 'asks', 'ask', 'explores', 'explore', 'recommendations', 'recommendation', 'history', 'conversation', 'sources', 'on'];
135
136
 
136
137
  /**
137
138
  * The UI interface for the client.
package/src/util/dom.js CHANGED
@@ -1,3 +1,19 @@
1
+ // data-* attributes of an element as a JSON string, for click interactions
2
+ export function dumpElementAttributes(element) {
3
+ const { attributes } = element;
4
+ const attrs = {};
5
+ for (const { name, value } of attributes) {
6
+ if (name.startsWith('data-')) {
7
+ attrs[name] = value;
8
+ }
9
+ }
10
+ try {
11
+ return JSON.stringify(attrs);
12
+ } catch (e) {
13
+ return '{}';
14
+ }
15
+ }
16
+
1
17
  export function setOrRemoveAttribute(element, name, value) {
2
18
  if (value === undefined) {
3
19
  element.removeAttribute(name);