@miso.ai/client-sdk-ui 1.14.0-beta.3 → 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.
@@ -24,7 +24,8 @@ Object.keys(_controls).forEach(function (key) {
24
24
  }
25
25
  });
26
26
  });
27
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
27
+ var cache;
28
+ function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
28
29
  const constants = exports.constants = Object.freeze(_constants);
29
30
  const phrases = exports.phrases = Object.freeze(_phrases);
30
31
  const templates = exports.templates = Object.freeze(_templates);
@@ -12,7 +12,8 @@ var CONSTANTS = _interopRequireWildcard(require("./constants.js"));
12
12
  var DEFAULT_PHRASES = _interopRequireWildcard(require("./phrases.js"));
13
13
  var _svgs = require("../../asset/svgs.js");
14
14
  var _templates = require("../common/templates.js");
15
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
15
+ var cache;
16
+ function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
16
17
  class Sections extends _templates.Sections {
17
18
  constructor() {
18
19
  super({
@@ -24,7 +24,8 @@ Object.keys(_controls).forEach(function (key) {
24
24
  }
25
25
  });
26
26
  });
27
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
27
+ var cache;
28
+ function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
28
29
  const constants = exports.constants = Object.freeze(_constants);
29
30
  const phrases = exports.phrases = Object.freeze(_phrases);
30
31
  const templates = exports.templates = Object.freeze(_templates);
@@ -10,7 +10,8 @@ var _commons = require("@miso.ai/commons");
10
10
  var CONSTANTS = _interopRequireWildcard(require("./constants.js"));
11
11
  var DEFAULT_PHRASES = _interopRequireWildcard(require("./phrases.js"));
12
12
  var _templates = require("../common/templates.js");
13
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
13
+ var cache;
14
+ function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
14
15
  class Sections extends _templates.Sections {
15
16
  constructor() {
16
17
  super({
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.hybridSearch = exports.ask = void 0;
7
7
  var _ask = _interopRequireWildcard(require("./ask/index.js"));
8
8
  var _hybridSearch = _interopRequireWildcard(require("./hybrid-search/index.js"));
9
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
9
+ var cache;
10
+ function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
10
11
  const ask = exports.ask = Object.freeze(_ask);
11
12
  const hybridSearch = exports.hybridSearch = Object.freeze(_hybridSearch);
@@ -9,7 +9,7 @@ var _constants = require("./constants.js");
9
9
  var _templates = require("./layout/templates.js");
10
10
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
11
11
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
12
- function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
12
+ function _toPrimitive(t, e) { if ("object" != typeof t || !t) return t; var r; if ("undefined" != typeof Symbol && void 0 !== (r = t[Symbol.toPrimitive])) { var i = r.call(t, e || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === e ? String : Number)(t); }
13
13
  /**
14
14
  * Default layout options of each workflow, keyed by workflow name. The UI
15
15
  * plugin seeds the workflow plugin's defaults store with these values at
@@ -203,7 +203,9 @@ const defaultLayouts = exports.defaultLayouts = Object.freeze({
203
203
  // query role (a message has no query flow of its own; the query entry
204
204
  // below is the panel's composer), with the question layout (text plus
205
205
  // authorship attributes, off the whole record), the answer typewriter
206
- // with `instant: true` (an already-finished answer renders in one shot),
206
+ // with `instant: true` (an answer starts at what is already available: a
207
+ // finished one renders in one shot, an ongoing one types out only the rest),
208
+ // the error notice worded for a message (messageErrorMessage),
207
209
  // and the sources as the compact horizontal cards of the hybrid-search
208
210
  // UI. logo: false turns the per-message banner off; the panel never
209
211
  // triggers the auto logo anyway, having no main-role component
@@ -215,6 +217,11 @@ const defaultLayouts = exports.defaultLayouts = Object.freeze({
215
217
  [_constants.ROLE.ANSWER]: [_constants.LAYOUT_TYPE.TYPEWRITER, {
216
218
  instant: true
217
219
  }],
220
+ [_constants.ROLE.ERROR]: [_constants.LAYOUT_TYPE.ERROR, {
221
+ templates: {
222
+ message: _templates.messageErrorMessage
223
+ }
224
+ }],
218
225
  [_constants.ROLE.SOURCES]: [_constants.LAYOUT_TYPE.HORIZONTAL, {
219
226
  incremental: true,
220
227
  itemType: 'article',
@@ -30,4 +30,5 @@ Object.keys(_index5).forEach(function (key) {
30
30
  }
31
31
  });
32
32
  });
33
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
33
+ var cache;
34
+ function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
+ exports.hasStatusTemplate = hasStatusTemplate;
7
8
  var _constants = require("../../constants.js");
8
9
  var _template = _interopRequireDefault(require("../template.js"));
9
10
  var _trackable = require("../mixin/trackable.js");
@@ -24,7 +25,13 @@ function root(layout, state) {
24
25
  } = state;
25
26
  const roleAttr = role ? ` data-role="${role}"` : '';
26
27
  const itemTypeAttr = options.itemType ? ` data-item-type="${options.itemType}"` : '';
27
- return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${status === _constants.STATUS.READY ? templates[status](layout, state) : ''}${templates.trigger(layout, state)}${templates.loading(layout, state)}</div>`;
28
+ return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${hasStatusTemplate(status) ? templates[status](layout, state) : ''}${templates.trigger(layout, state)}${templates.loading(layout, state)}</div>`;
29
+ }
30
+
31
+ // the statuses with a content template of their own: the list at ready, the
32
+ // error notice at erroneous
33
+ function hasStatusTemplate(status) {
34
+ return status === _constants.STATUS.READY || status === _constants.STATUS.ERRONEOUS;
28
35
  }
29
36
  function ready(layout, state) {
30
37
  const {
@@ -103,6 +110,9 @@ const DEFAULT_TEMPLATES = Object.freeze({
103
110
  root,
104
111
  [_constants.STATUS.READY]: ready,
105
112
  empty: () => ``,
113
+ // no notice by default: the ask/search UIs present errors in a section of
114
+ // their own (<miso-error>); list layouts that stand alone supply one
115
+ [_constants.STATUS.ERRONEOUS]: () => ``,
106
116
  body,
107
117
  list,
108
118
  ordered: false,
@@ -5,12 +5,21 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = void 0;
7
7
  var _constants = require("../../constants.js");
8
- var _collection = _interopRequireDefault(require("./collection.js"));
8
+ var _collection = _interopRequireWildcard(require("./collection.js"));
9
+ var _templates = require("../templates.js");
10
+ var _dom = require("../../util/dom.js");
9
11
  const _excluded = ["className", "templates"];
10
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ var cache;
13
+ function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
11
14
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
12
15
  const TYPE = _constants.LAYOUT_TYPE.MESSAGES;
13
16
  const DEFAULT_CLASSNAME = 'miso-messages';
17
+ const DEFAULT_TEMPLATES = Object.freeze({
18
+ root,
19
+ [_constants.STATUS.ERRONEOUS]: _templates.listError,
20
+ errorMessage
21
+ });
22
+ const INHERITED_DEFAULT_TEMPLATES = Object.freeze(Object.assign({}, _collection.default.defaultTemplates, DEFAULT_TEMPLATES));
14
23
 
15
24
  // the infinite scroll trigger sits ABOVE the list — older messages load as
16
25
  // the user scrolls up, unlike the stock collection root's bottom trigger
@@ -26,7 +35,10 @@ function root(layout, state) {
26
35
  } = state;
27
36
  const roleAttr = role ? ` data-role="${role}"` : '';
28
37
  const itemTypeAttr = options.itemType ? ` data-item-type="${options.itemType}"` : '';
29
- return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${templates.trigger(layout, state)}${status === _constants.STATUS.READY ? templates[status](layout, state) : ''}${templates.loading(layout, state)}</div>`;
38
+ return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${templates.trigger(layout, state)}${(0, _collection.hasStatusTemplate)(status) ? templates[status](layout, state) : ''}${templates.loading(layout, state)}</div>`;
39
+ }
40
+ function errorMessage(layout, error) {
41
+ return (0, _templates.isSessionExpired)(error) ? _templates.SESSION_EXPIRED_MESSAGE : 'This conversation could not be loaded.';
30
42
  }
31
43
 
32
44
  /**
@@ -65,6 +77,9 @@ class MessagesLayout extends _collection.default {
65
77
  static get defaultClassName() {
66
78
  return DEFAULT_CLASSNAME;
67
79
  }
80
+ static get defaultTemplates() {
81
+ return INHERITED_DEFAULT_TEMPLATES;
82
+ }
68
83
  constructor(_ref = {}) {
69
84
  let {
70
85
  className = DEFAULT_CLASSNAME,
@@ -73,9 +88,7 @@ class MessagesLayout extends _collection.default {
73
88
  options = _objectWithoutPropertiesLoose(_ref, _excluded);
74
89
  super(Object.assign({
75
90
  className,
76
- templates: Object.assign({
77
- root
78
- }, templates)
91
+ templates: Object.assign({}, DEFAULT_TEMPLATES, templates)
79
92
  }, options));
80
93
  this._pinned = true; // whether the view sticks to the bottom on updates
81
94
  this._anchor = undefined; // the scroll anchor while prepended content settles
@@ -183,6 +196,7 @@ class MessagesLayout extends _collection.default {
183
196
  }
184
197
  this._watchGrowth(this._getListElement(element) || element);
185
198
  this._syncWorkflows(element);
199
+ this._syncItems(element);
186
200
  this._syncOngoing(state);
187
201
  }
188
202
  _unrender() {
@@ -204,6 +218,16 @@ class MessagesLayout extends _collection.default {
204
218
  }
205
219
  }
206
220
 
221
+ // sync in-place item state changes from the bound values onto the existing
222
+ // item elements: the live mark — the item contents render through the
223
+ // message workflows, outside this layout's render cycle
224
+ _syncItems(element) {
225
+ for (const item of this._getItemElements(element)) {
226
+ const binding = this._bindings.get(item);
227
+ binding && (0, _dom.setOrRemoveAttribute)(item, 'data-live', binding.value.live ? '' : undefined);
228
+ }
229
+ }
230
+
207
231
  // whether an answer is on its way — pending, or still being generated —
208
232
  // reported as the `ongoing` view state, e.g. for the search box to disable
209
233
  // submission
@@ -7,12 +7,21 @@ exports.default = void 0;
7
7
  var _clientSdkWorkflow = require("@miso.ai/client-sdk-workflow");
8
8
  var _constants = require("../../constants.js");
9
9
  var _collection = _interopRequireDefault(require("./collection.js"));
10
+ var _templates = require("../templates.js");
10
11
  var _dom = require("../../util/dom.js");
11
- const _excluded = ["className"];
12
+ const _excluded = ["className", "templates"];
12
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
14
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
14
15
  const TYPE = _constants.LAYOUT_TYPE.THREADS;
15
16
  const DEFAULT_CLASSNAME = 'miso-threads';
17
+ const DEFAULT_TEMPLATES = Object.freeze({
18
+ [_constants.STATUS.ERRONEOUS]: _templates.listError,
19
+ errorMessage
20
+ });
21
+ const INHERITED_DEFAULT_TEMPLATES = Object.freeze(Object.assign({}, _collection.default.defaultTemplates, DEFAULT_TEMPLATES));
22
+ function errorMessage(layout, error) {
23
+ return (0, _templates.isSessionExpired)(error) ? _templates.SESSION_EXPIRED_MESSAGE : 'Your conversations could not be loaded.';
24
+ }
16
25
 
17
26
  /**
18
27
  * The thread list of the chat history interface: a shell that renders one
@@ -47,13 +56,18 @@ class ThreadsLayout extends _collection.default {
47
56
  static get defaultClassName() {
48
57
  return DEFAULT_CLASSNAME;
49
58
  }
59
+ static get defaultTemplates() {
60
+ return INHERITED_DEFAULT_TEMPLATES;
61
+ }
50
62
  constructor(_ref = {}) {
51
63
  let {
52
- className = DEFAULT_CLASSNAME
64
+ className = DEFAULT_CLASSNAME,
65
+ templates
53
66
  } = _ref,
54
67
  options = _objectWithoutPropertiesLoose(_ref, _excluded);
55
68
  super(Object.assign({
56
- className
69
+ className,
70
+ templates: Object.assign({}, DEFAULT_TEMPLATES, templates)
57
71
  }, options));
58
72
  }
59
73
 
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.SESSION_EXPIRED_MESSAGE = void 0;
6
7
  exports.affiliation = affiliation;
7
8
  exports.affiliationInfoBlock = affiliationInfoBlock;
8
9
  exports.article = article;
@@ -22,9 +23,12 @@ exports.image = image;
22
23
  exports.imageBlock = imageBlock;
23
24
  exports.imageInfoBlock = imageInfoBlock;
24
25
  exports.indexBlock = indexBlock;
26
+ exports.isSessionExpired = isSessionExpired;
27
+ exports.listError = listError;
25
28
  exports.message = message;
26
29
  exports.messageBody = messageBody;
27
30
  exports.messageErrorBlock = messageErrorBlock;
31
+ exports.messageErrorMessage = messageErrorMessage;
28
32
  exports.messageSourcesBlock = messageSourcesBlock;
29
33
  exports.priceBlock = priceBlock;
30
34
  exports.product = product;
@@ -111,16 +115,19 @@ function threadMenuBlock({
111
115
  * item subworkflow — the messages layout is a shell: it assigns the workflow
112
116
  * (and the item-level attributes) in its post-render sync pass, and the role
113
117
  * elements inside render the actual content through the message workflow's
114
- * own layouts.
118
+ * own layouts. The item state renders as an attribute right in the template
119
+ * (and stays in sync thereafter): the live mark, for a message generated in
120
+ * this session.
115
121
  */
116
122
  function message(layout, state, data) {
117
123
  const {
118
124
  className,
119
125
  templates
120
126
  } = layout;
127
+ const liveAttr = data.live ? ' data-live' : '';
121
128
  // miso-circled-citation-index covers both the citation links in the answer
122
129
  // and the indexes on the source cards, as the hybrid-search UI scopes it
123
- return [`<miso-message-item class="${className}__item-body miso-circled-citation-index" data-role="item">`, (templates.messageBody || messageBody)(layout, data), `</miso-message-item>`].join('');
130
+ return [`<miso-message-item class="${className}__item-body miso-circled-citation-index" data-role="item"${liveAttr}>`, (templates.messageBody || messageBody)(layout, data), `</miso-message-item>`].join('');
124
131
  }
125
132
 
126
133
  // the regular content is packed together, so the erroneous presentation
@@ -146,12 +153,38 @@ function messageSourcesBlock({
146
153
  }
147
154
 
148
155
  // shown by the item's own status (visible-when): a message whose content
149
- // fetch failed for good presents this in place of its content
156
+ // fetch, or (a live one's) posting or answer polling, failed for good
157
+ // presents this in place of its content — the text through the item's own
158
+ // error role element, off the record's error mark (messageErrorMessage)
150
159
  function messageErrorBlock({
151
160
  className
152
161
  }) {
153
- return `<div class="${className}__error" visible-when="erroneous">This message could not be loaded.</div>`;
162
+ return `<div class="${className}__error" visible-when="erroneous"><miso-error></miso-error></div>`;
154
163
  }
164
+
165
+ // the error layout's message template for a message item (seeded in the
166
+ // 'conversation' layout defaults): an expired session reads apart from other
167
+ // failures
168
+ function messageErrorMessage(layout, error) {
169
+ return isSessionExpired(error) ? SESSION_EXPIRED_MESSAGE : 'Something went wrong with this message.';
170
+ }
171
+
172
+ // the list-level error notice of the chat history interface, in place of
173
+ // the list when its load failed (the thread list, or the thread on display);
174
+ // the text comes from the layout's errorMessage template, given the failure
175
+ function listError(layout, state) {
176
+ const {
177
+ className,
178
+ templates
179
+ } = layout;
180
+ return `<div class="${className}__list-error" data-role="error">${templates.errorMessage(layout, state.error)}</div>`;
181
+ }
182
+
183
+ // an expired session (e.g. an expired JWT), to be told apart from other failures
184
+ function isSessionExpired(error) {
185
+ return !!error && error.status === 401;
186
+ }
187
+ const SESSION_EXPIRED_MESSAGE = exports.SESSION_EXPIRED_MESSAGE = 'Your session has expired. Please sign in again.';
155
188
  function article(layout, state, data, meta) {
156
189
  const {
157
190
  templates
@@ -163,11 +163,12 @@ class TypewriterLayout extends _progressive.default {
163
163
  } else {
164
164
  result = rendered;
165
165
  }
166
- if (this.options.instant && state.done && !result.cursor) {
167
- // instant mode: content that finishes before any of it was typed —
168
- // arrived complete (e.g. a loaded conversation message), or completed
169
- // while the cursor was still at the start — renders in one shot; only
170
- // content streaming in as it is generated types out
166
+ if (this.options.instant && !result.cursor) {
167
+ // instant mode: while nothing is typed yet, the cursor starts at the max
168
+ // available position (the renderer clamps it to the safe right bound) —
169
+ // content that arrived complete (e.g. a loaded conversation message)
170
+ // renders in one shot; ongoing content shows what is there so far at
171
+ // once, and only the part streaming in after that types out
171
172
  result = this._renderer.update(container, result, Object.assign({}, state, {
172
173
  cursor: Infinity
173
174
  }));
@@ -17,11 +17,13 @@ var traits = _interopRequireWildcard(require("./trait/index.js"));
17
17
  var _styles = require("./styles.js");
18
18
  var _confirm = _interopRequireDefault(require("./util/confirm.js"));
19
19
  var _prompt = _interopRequireDefault(require("./util/prompt.js"));
20
+ var _notify = _interopRequireDefault(require("./util/notify.js"));
20
21
  var _misoContainer = _interopRequireDefault(require("./element/container/miso-container.js"));
21
22
  var _misoContext = _interopRequireDefault(require("./element/context/miso-context.js"));
22
23
  var _misoCombo = _interopRequireDefault(require("./element/combo/miso-combo.js"));
23
24
  const _excluded = ["containers", "roles", "contexts", "combos"];
24
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
25
+ var cache;
26
+ function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
25
27
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
26
28
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
27
29
  const PLUGIN_ID = 'std:ui';
@@ -54,7 +56,8 @@ class UiPlugin extends _commons.Component {
54
56
  defaults,
55
57
  traits,
56
58
  confirm: _confirm.default,
57
- prompt: _prompt.default
59
+ prompt: _prompt.default,
60
+ notify: _notify.default
58
61
  };
59
62
  (0, _commons.delegateGetters)(ui, this, ['layouts', 'ready']);
60
63
  (0, _commons.defineValues)(this, {
@@ -160,7 +163,7 @@ function createUiPluginContext(plugin, ui, MisoClient) {
160
163
  }
161
164
 
162
165
  // workflow accessors delegated to client.workflows for backward compatibility
163
- const WORKFLOW_ACCESSORS = ['search', 'hybridSearch', 'asks', 'ask', 'explores', 'explore', 'recommendations', 'recommendation', 'sources'];
166
+ const WORKFLOW_ACCESSORS = ['search', 'hybridSearch', 'asks', 'ask', 'explores', 'explore', 'recommendations', 'recommendation', 'history', 'conversation', 'sources', 'on'];
164
167
 
165
168
  /**
166
169
  * The UI interface for the client.
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = notify;
7
+ var _commons = require("@miso.ai/commons");
8
+ const DEFAULT_CLASSNAME = 'miso-notice';
9
+ const DEFAULT_DURATION = 5000;
10
+
11
+ /**
12
+ * Show a transient notice (a toast) in a corner of the page, dismissed
13
+ * after `duration` milliseconds (0 keeps it until closed), or by its close
14
+ * button. A notice with the very text of one on display is not stacked:
15
+ * the displayed one stays, its timer starting over. Returns a function
16
+ * that dismisses the notice. Exposed as `MisoClient.ui.notify()`.
17
+ */
18
+ function notify({
19
+ message,
20
+ type = 'info',
21
+ // 'info' | 'error'
22
+ duration = DEFAULT_DURATION,
23
+ className = DEFAULT_CLASSNAME
24
+ } = {}) {
25
+ if (!message) {
26
+ return () => {};
27
+ }
28
+ const container = getContainer(className);
29
+ let notice = [...container.children].find(element => element._misoNotice && element._misoNotice.key === `${type}:${message}`);
30
+ if (!notice) {
31
+ notice = createNotice(className, type, message);
32
+ container.appendChild(notice);
33
+ }
34
+ notice._misoNotice.schedule(duration);
35
+ return notice._misoNotice.dismiss;
36
+ }
37
+
38
+ // the stack hosting the notices, created on demand and removed when empty
39
+ function getContainer(className) {
40
+ let container = document.querySelector(`.${className}-stack`);
41
+ if (!container) {
42
+ container = document.createElement('div');
43
+ container.className = `${className}-stack`;
44
+ container.setAttribute('aria-live', 'polite');
45
+ document.body.appendChild(container);
46
+ }
47
+ return container;
48
+ }
49
+ function createNotice(className, type, message) {
50
+ const notice = document.createElement('div');
51
+ notice.className = `${className} ${className}--${type}`;
52
+ notice.setAttribute('role', type === 'error' ? 'alert' : 'status');
53
+ notice.innerHTML = [`<div class="${className}__message">${(0, _commons.escapeHtml)(message)}</div>`, `<button type="button" class="${className}__close-button" data-role="close" aria-label="Dismiss">&times;</button>`].join('');
54
+ let timer;
55
+ const dismiss = () => {
56
+ clearTimeout(timer);
57
+ const container = notice.parentElement;
58
+ notice.remove();
59
+ container && container.children.length === 0 && container.remove();
60
+ };
61
+ const schedule = duration => {
62
+ clearTimeout(timer);
63
+ timer = duration > 0 ? setTimeout(dismiss, duration) : undefined;
64
+ };
65
+ notice.addEventListener('click', event => {
66
+ event.target.closest('[data-role="close"]') && dismiss();
67
+ });
68
+ notice._misoNotice = {
69
+ key: `${type}:${message}`,
70
+ dismiss,
71
+ schedule
72
+ };
73
+ return notice;
74
+ }
@@ -4,4 +4,4 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _default = exports.default = '1.14.0-beta.3';
7
+ var _default = exports.default = '1.14.0-beta.4';
package/package.json CHANGED
@@ -30,8 +30,8 @@
30
30
  "url": "https://github.com/MisoAI/miso-client-js-sdk/issues"
31
31
  },
32
32
  "dependencies": {
33
- "@miso.ai/commons": "1.14.0-beta.3",
34
- "@miso.ai/client-sdk-workflow": "1.14.0-beta.3"
33
+ "@miso.ai/commons": "1.14.0-beta.4",
34
+ "@miso.ai/client-sdk-workflow": "1.14.0-beta.4"
35
35
  },
36
- "version": "1.14.0-beta.3"
36
+ "version": "1.14.0-beta.4"
37
37
  }
package/src/CLAUDE.md CHANGED
@@ -44,7 +44,8 @@ Web Components (`<miso-*>`). They contain **no rendering or data logic** — the
44
44
  - `<miso-ask>` — binds by `parent-question-id` attribute (absent = root ask), or `workflow="active"` to follow `asks.active`
45
45
  - `<miso-search>`, `<miso-hybrid-search>` — bind to `client.ui.search` / `.hybridSearch`
46
46
  - `<miso-explore>`, `<miso-recommendation>` — bind by `unit-id` attribute
47
- - `<miso-history>`, `<miso-conversation>` — bind to `client.workflows.history` / `.conversation`, forming the chat history interface (thread list + conversation panel); their role elements are `<miso-threads>`, `<miso-new-thread>`, `<miso-messages>`, `<miso-title>`, `<miso-rename>`, `<miso-delete>` and `<miso-subscription>`. The new-thread button is its own component (`<miso-new-thread>`, role `new_thread`), *not* part of the thread list, so it renders regardless of the list state (loading, empty, erroneous); it uses the generic `button` layout — an icon and a label, both configurable (`icon`, `text`), emitting a `submit` view event when clicked — which History answers by clearing the selection and resetting the conversation panel to new-thread mode. The thread list uses the `threads` layout as a **shell**, like the conversation's `messages` layout: each `thread` item is a `<miso-thread-item>` container element (`data-role="item"`, carrying the thread id and the state attributes) hosting the thread item subworkflow, with the title and the context menu's rename/subscription/delete controls as role elements inside rendering through that workflow's own generic layouts; selection is part of the data — `selectedThreadId` lives in the workflow's committed value and each record carries its `selected` flag right in the data layer (stamped by the workflow's default data pass, preserving the identity of records whose flag doesn't flip, so only the affected items' subworkflows see a commit; the role mapping is a plain slice) — and the list renders incrementally: fresh items land at either end — a newly created thread renders as a prepend (the list is newest-first), an older page fetched by the more flow as appends — told apart by which boundary the already-rendered records line up with (anything else re-renders in full), while in-place state changes on existing items (selected/unread, the thread id of a settled placeholder) are applied by an attribute-sync pass after each render, reading fresh values off the item bindings, and the item contents follow the record down each thread workflow's own data path — no item re-render either way. The list **pages by infinite scroll**, through the collection layout's stock machinery (the `data-role="trigger"` div after the list + the triggerable mixin, enabled by the threads layout's `infiniteScroll` option and the workflow's `'history'` pagination defaults): the trigger scrolling into view fires the `more` hub field, History appends the next page (`start`/`rows` against the user history list API, exhaustion from the response's own `has_more`), and the page size is set via `history.useApi('threads/list', { rows: N })`; the conversation uses the `messages` layout as a **shell**: each `message` item is a `<miso-message-item>` container element (question/answer/feedback/sources role elements inside, in that order — the feedback buttons right below the answer via the standard feedback layout, the sources as the hybrid-search-style compact horizontal cards under an h3 "Sources" heading (`messageSourcesBlock`, overridable — the block is `visible-when="nonempty"`, so the heading appears only once the item carries content, and a `:has()` rule hides the whole block again when there are zero source cards; a browser without `:has()` degrades to showing the heading, hybrid-search-parity); the item element carries `miso-circled-citation-index`, covering the answer's citation links and the source cards' indexes alike, as the hybrid-search UI scopes it; a message record without its answer body presents as workflow status `loading`, stamped on the element by its container layout as the standard `status` attribute — there is deliberately no per-message loading icon: the answer typewriter's blinking caret is the loading indication, as in ask (hidden at `initial` and at `done` by the stock typewriter CSS); a message whose content fetch failed for good presents as status `erroneous` and swaps its content out wholesale for the item template's muted notice (`messageErrorBlock`, `visible-when="erroneous"` — `miso-message-item` is in the visible-when container list, so the notice keys off the item's own status attribute) — the regular role elements are packed in a `__content` wrapper hidden by a class rule at erroneous status (a wrapper cannot be visible-when-driven itself: the mechanism forces `display: block !important` and only negates combined with positive terms) — and the messages layout's `ongoing` computation skips an errored last message so the composer is not held), rendered incrementally — fresh items land at either end: a posted follow-up appends at the bottom, an older page fetched by the more flow prepends at the top (told apart by which boundary the already-rendered records line up with; anything else re-renders in full) — the content itself renders through each message's own item subworkflow. The panel **pages by infinite scroll, chat-style from the bottom**: the messages layout's own root template places the stock `data-role="trigger"` div **above** the list, so scrolling up fires the `more` hub field (enabled by the layout's `infiniteScroll` option + the workflow's `'conversation'` pagination defaults) and the Conversation workflow prepends the previous page of messages. The post-render sync pass (`_syncWorkflows`) does one thing: assign each `<miso-message-item>` its workflow off the item binding (`element.workflow = conversation._getMessageWorkflow(record)` — implicit binding, item identity keyed by question id falling back to the posted record's local `placeholder_id`, so the element and workflow survive the settle). Item elements of a nested collection do not leak into the outer one: a message hosts its own source-card items (`data-role="item"` too), so `CollectionLayout._getItemElements` keeps only elements whose closest `[data-role="list"]` is the layout's own list — bindings zip values to elements by index, so a stray nested item would shift every binding after it. The question bubble is the **`question` layout** (the message workflow's default for the question role, whose mapping passes the whole record): it renders the question text and stamps the authorship as data attributes on its host element — `data-author` (the label text, configurable via the layout's `author` option; by default 'Written by Miso' for questions whose `metadata.miso_generated_by` is `answer_update_monitor`, per `isUpdateMessage`; it renders as a CSS `attr()` badge overlapping the bubble's top-left border, and monitor-generated bubbles take a distinct background) and `data-generated-by` — and hides its host while there is no question text. The shell also owns the panel-level behaviors: scroll pinning (following content growth via a ResizeObserver on the list, since answers type out through their own workflows outside the shell's render cycle), scroll anchoring while scrolled up — a prepend keeps the reading position by anchoring on the previously-first item: everything prepended, including its contents rendering in later through the item workflows, sits above the anchor, so holding the anchor stationary (at insert time and on every growth event while unpinned, the anchor's offset re-recorded as the user scrolls) holds the view, while growth below it (a streaming answer) needs no compensation; done by hand, since not every engine provides native scroll anchoring — and the `ongoing` view state read off the data — the last message's answer absent or unfinished — which e.g. disables the search box. The conversation's header roles come from the open thread's record: `<miso-title>` (role `title`) renders its title with the generic `text` layout, `<miso-rename>` (role `rename`) renders a pencil button with the generic `button` layout whose **`prompt` option** turns the click into ask-then-submit: the shared prompt dialog opens pre-filled with the control's current value — the role's mapped data value, read off the latest rendered state (`_currentValue()`; `RafLayout` retains it per element) — and the `submit` view event carries the confirmed text as `value`, which Conversation answers with `rename(value)` — `<miso-delete>` (role `delete`, placed right of the rename button) renders a red bin button with the button layout's `confirm` option (the dialog's message interpolates the mapped title), whose confirmed `submit` Conversation answers with `delete()` (the deletion fact then resets the panel to new-thread mode) — and `<miso-subscription>` (role `subscription`) renders the answer-updates toggle with the **`checkbox` layout** — a button-layout subclass; a checkbox that looks like a button (`<button role="switch" aria-checked>` with `data-checked`, an icon and label per state via `icon`/`text` and `checkedIcon`/`checkedText`). Its checked state comes from the data, so a click emits a `change` view event carrying only the requested state (`{ checked }`), read off the rendered element; the Conversation workflow answers it with `subscribe()`/`unsubscribe()` and the new state arrives back down the data path. Note the `visible-when` mechanism forces `display: block !important` on the element it toggles, so a page that lays such an element out as a flex row needs an inner wrapper (as the history demo pages do for the header). A `<miso-query>` (search-box layout) inside `<miso-conversation>` submits questions as the `submit` view event, which Conversation routes to `send()` — follow-ups, or the first question of a new thread (the conversation starts in new-thread mode). With `messages`/`threads` registered as data roles, `<miso-conversation>` carries the `empty`/`nonempty` content status, which pages use to center the composer in the empty (new-thread) state. A click on a thread item is a navigation action, not a content-engagement click: the `threads` layout overrides `_onClick` to emit a `select` view event (no click tracking), which the History workflow handles with `select()` — item selection stays list-level, while everything inside the item belongs to its thread workflow. Each item also carries a context menu (vertical dots, revealed on hover): its markup comes from the item template (`threadMenuBlock`), and its open/close behavior is driven by the thread workflow's **`item-container` layout** (`ItemContainerLayout`, the container-layout subclass for collection item subworkflows) **by contract** — the template marks the toggle button `data-role="item-menu-button"` and the popup `data-role="item-menu"`, and the layout toggles on the button and closes on any other click on the item or outside it (per-item document listeners, which is also how opening one item's menu closes another's); the shell only keeps clicks carrying those data-roles from selecting. The menu items are the thread workflow's own controls, each a generic `button` layout going through a shared modal dialog util (promise-returning native `<dialog>`s on a common core, `util/modal.js`): `<miso-rename>` (role `rename`) uses the button layout's `prompt` option (`util/prompt.js`, a modal text input pre-filled with the current title — Enter confirms, resolving to the trimmed text, or undefined on cancel; exposed as `MisoClient.ui.prompt`), and `<miso-delete>` (role `delete`) its **`confirm` option** (`util/confirm.js`, exposed as `MisoClient.ui.confirm`; the option's `message` may be a function of the control's current value — the mapped title); the confirmed `submit` view events fire the operation requests on the thread workflow's own hub, their facts riding onto the shared thread events emitter. Between the two sits `<miso-subscription>` (role `subscription`), the `checkbox` layout as a plain menu item: text only, labeled by the action a click takes ('Subscribe' / 'Unsubscribe' via `text`/`checkedText` in the `'history'` defaults — unlike the conversation header's state-labeled bell pill), its `change` view event answered by the thread workflow with `history.subscribe()`/`unsubscribe()`.
47
+ - `<miso-history>`, `<miso-conversation>` — bind to `client.workflows.history` / `.conversation`, forming the chat history interface (thread list + conversation panel); their role elements are `<miso-threads>`, `<miso-new-thread>`, `<miso-messages>`, `<miso-title>`, `<miso-rename>`, `<miso-delete>` and `<miso-subscription>`. The new-thread button is its own component (`<miso-new-thread>`, role `new_thread`), *not* part of the thread list, so it renders regardless of the list state (loading, empty, erroneous); it uses the generic `button` layout — an icon and a label, both configurable (`icon`, `text`), emitting a `submit` view event when clicked — which History answers by clearing the selection and resetting the conversation panel to new-thread mode. The thread list uses the `threads` layout as a **shell**, like the conversation's `messages` layout: each `thread` item is a `<miso-thread-item>` container element (`data-role="item"`, carrying the thread id and the state attributes) hosting the thread item subworkflow, with the title and the context menu's rename/subscription/delete controls as role elements inside rendering through that workflow's own generic layouts; selection is part of the data — `selectedThreadId` lives in the workflow's committed value and each record carries its `selected` flag right in the data layer (stamped by the workflow's default data pass, preserving the identity of records whose flag doesn't flip, so only the affected items' subworkflows see a commit; the role mapping is a plain slice) — and the list renders incrementally: fresh items land at either end — a newly created thread renders as a prepend (the list is newest-first), an older page fetched by the more flow as appends — told apart by which boundary the already-rendered records line up with (anything else re-renders in full), while in-place state changes on existing items (selected/unread, the thread id of a settled placeholder) are applied by an attribute-sync pass after each render, reading fresh values off the item bindings, and the item contents follow the record down each thread workflow's own data path — no item re-render either way. The list **pages by infinite scroll**, through the collection layout's stock machinery (the `data-role="trigger"` div after the list + the triggerable mixin, enabled by the threads layout's `infiniteScroll` option and the workflow's `'history'` pagination defaults): the trigger scrolling into view fires the `more` hub field, History appends the next page (`start`/`rows` against the user history list API, exhaustion from the response's own `has_more`), and the page size is set via `history.useApi('threads/list', { rows: N })`; the conversation uses the `messages` layout as a **shell**: each `message` item is a `<miso-message-item>` container element (question/answer/feedback/sources role elements inside, in that order — the feedback buttons right below the answer via the standard feedback layout, the sources as the hybrid-search-style compact horizontal cards under an h3 "Sources" heading (`messageSourcesBlock`, overridable — the block is `visible-when="nonempty"`, so the heading appears only once the item carries content, and a `:has()` rule hides the whole block again when there are zero source cards; a browser without `:has()` degrades to showing the heading, hybrid-search-parity); the item element carries `miso-circled-citation-index`, covering the answer's citation links and the source cards' indexes alike, as the hybrid-search UI scopes it; a message record without its answer body presents as workflow status `loading`, stamped on the element by its container layout as the standard `status` attribute — there is deliberately no per-message loading icon: the answer typewriter's blinking caret is the loading indication, as in ask (hidden at `initial` and at `done` by the stock typewriter CSS); the item element carries `data-live` for a message generated in this session (stamped by the item template, kept in sync by the shell's `_syncItems` pass), and a message without it — a reopened thread, an older page — holds its packed content back (a class rule) until the item is `ready`, so it shows in one piece, while a live message shows its question right away as its answer generates; a message whose content fetch failed for good — or, for a live message, whose posting or answer polling did (the message workflow's `writeErrorToMessage` turns the stream's failure into the same record `error` mark the conversation's failed answers request sets; the mark is the failure itself, so the presentation can tell an expired session apart) — presents as status `erroneous` and swaps its content out wholesale for the item template's muted notice (`messageErrorBlock`, `visible-when="erroneous"` — `miso-message-item` is in the visible-when container list, so the notice keys off the item's own status attribute); the notice hosts the item's own `<miso-error>` role element (the message workflow maps the error role to the record's `error` mark), rendered by the error layout with the `'conversation'` defaults' `messageErrorMessage` text template — customized via `conversation.useLayouts({ error: { templates: { message: (layout, error) => ... } } })`, the markup via the messages layout's `messageErrorBlock` — the regular role elements are packed in a `__content` wrapper hidden by a class rule at erroneous status (a wrapper cannot be visible-when-driven itself: the mechanism forces `display: block !important` and only negates combined with positive terms) — and the messages layout's `ongoing` computation skips an errored last message so the composer is not held), rendered incrementally — fresh items land at either end: a posted follow-up appends at the bottom, an older page fetched by the more flow prepends at the top (told apart by which boundary the already-rendered records line up with; anything else re-renders in full) — the content itself renders through each message's own item subworkflow. The panel **pages by infinite scroll, chat-style from the bottom**: the messages layout's own root template places the stock `data-role="trigger"` div **above** the list, so scrolling up fires the `more` hub field (enabled by the layout's `infiniteScroll` option + the workflow's `'conversation'` pagination defaults) and the Conversation workflow prepends the previous page of messages. The post-render sync pass (`_syncWorkflows`) does one thing: assign each `<miso-message-item>` its workflow off the item binding (`element.workflow = conversation._getMessageWorkflow(record)` — implicit binding, item identity keyed by question id falling back to the posted record's local `placeholder_id`, so the element and workflow survive the settle). Item elements of a nested collection do not leak into the outer one: a message hosts its own source-card items (`data-role="item"` too), so `CollectionLayout._getItemElements` keeps only elements whose closest `[data-role="list"]` is the layout's own list — bindings zip values to elements by index, so a stray nested item would shift every binding after it. The question bubble is the **`question` layout** (the message workflow's default for the question role, whose mapping passes the whole record): it renders the question text and stamps the authorship as data attributes on its host element — `data-author` (the label text, configurable via the layout's `author` option; by default 'Written by Miso' for questions whose `metadata.miso_generated_by` is `answer_update_monitor`, per `isUpdateMessage`; it renders as a CSS `attr()` badge overlapping the bubble's top-left border, and monitor-generated bubbles take a distinct background) and `data-generated-by` — and hides its host while there is no question text. The shell also owns the panel-level behaviors: scroll pinning (following content growth via a ResizeObserver on the list, since answers type out through their own workflows outside the shell's render cycle), scroll anchoring while scrolled up — a prepend keeps the reading position by anchoring on the previously-first item: everything prepended, including its contents rendering in later through the item workflows, sits above the anchor, so holding the anchor stationary (at insert time and on every growth event while unpinned, the anchor's offset re-recorded as the user scrolls) holds the view, while growth below it (a streaming answer) needs no compensation; done by hand, since not every engine provides native scroll anchoring — and the `ongoing` view state read off the data — the last message's answer absent or unfinished — which e.g. disables the search box. The conversation's header roles come from the open thread's record: `<miso-title>` (role `title`) renders its title with the generic `text` layout, `<miso-rename>` (role `rename`) renders a pencil button with the generic `button` layout whose **`prompt` option** turns the click into ask-then-submit: the shared prompt dialog opens pre-filled with the control's current value — the role's mapped data value, read off the latest rendered state (`_currentValue()`; `RafLayout` retains it per element) — and the `submit` view event carries the confirmed text as `value`, which Conversation answers with `rename(value)` — `<miso-delete>` (role `delete`, placed right of the rename button) renders a red bin button with the button layout's `confirm` option (the dialog's message interpolates the mapped title), whose confirmed `submit` Conversation answers with `delete()` (the deletion event then resets the panel to new-thread mode) — and `<miso-subscription>` (role `subscription`) renders the answer-updates toggle with the **`checkbox` layout** — a button-layout subclass; a checkbox that looks like a button (`<button role="switch" aria-checked>` with `data-checked`, an icon and label per state via `icon`/`text` and `checkedIcon`/`checkedText`). Its checked state comes from the data, so a click emits a `change` view event carrying only the requested state (`{ checked }`), read off the rendered element; the Conversation workflow answers it with `subscribe()`/`unsubscribe()` and the new state arrives back down the data path. Note the `visible-when` mechanism forces `display: block !important` on the element it toggles, so a page that lays such an element out as a flex row needs an inner wrapper (as the conversation header does: `.miso-conversation__header` > `.miso-conversation__header-row`). The panels' default layout styles are **opted in by class** — `<miso-history class="miso-history">` (scrolls on its own) and `<miso-conversation class="miso-conversation">` (a column in which the messages scroll, with `.miso-conversation__header`/`__header-row`/`__intro` for the header and the new-thread headline, and the composer centered in the empty state) — so a page strips them off by omitting the class names. A `<miso-query>` (search-box layout) inside `<miso-conversation>` submits questions as the `submit` view event, which Conversation routes to `send()` — follow-ups, or the first question of a new thread (the conversation starts in new-thread mode). With `messages`/`threads` registered as data roles, `<miso-conversation>` carries the `empty`/`nonempty` content status, which pages use to center the composer in the empty (new-thread) state. A click on a thread item is a navigation action, not a content-engagement click: the `threads` layout overrides `_onClick` to emit a `select` view event (no click tracking), which the History workflow handles with `select()` — item selection stays list-level, while everything inside the item belongs to its thread workflow. Each item also carries a context menu (vertical dots, revealed on hover): its markup comes from the item template (`threadMenuBlock`), and its open/close behavior is driven by the thread workflow's **`item-container` layout** (`ItemContainerLayout`, the container-layout subclass for collection item subworkflows) **by contract** — the template marks the toggle button `data-role="item-menu-button"` and the popup `data-role="item-menu"`, and the layout toggles on the button and closes on any other click on the item or outside it (per-item document listeners, which is also how opening one item's menu closes another's); the shell only keeps clicks carrying those data-roles from selecting. The menu items are the thread workflow's own controls, each a generic `button` layout going through a shared modal dialog util (promise-returning native `<dialog>`s on a common core, `util/modal.js`): `<miso-rename>` (role `rename`) uses the button layout's `prompt` option (`util/prompt.js`, a modal text input pre-filled with the current title — Enter confirms, resolving to the trimmed text, or undefined on cancel; exposed as `MisoClient.ui.prompt`; next to them sits `MisoClient.ui.notify({ message, type, duration })`, `util/notify.js`, a transient corner notice — a plain utility nothing in the SDK calls on its own: a page wires it up itself, e.g. to the thread event errors gathered at `client.workflows.on('error', ...)`), and `<miso-delete>` (role `delete`) its **`confirm` option** (`util/confirm.js`, exposed as `MisoClient.ui.confirm`; the option's `message` may be a function of the control's current value — the mapped title); the confirmed `submit` view events fire the operation requests on the thread workflow's own hub, their events riding onto the shared thread events emitter. Between the two sits `<miso-subscription>` (role `subscription`), the `checkbox` layout as a plain menu item: text only, labeled by the action a click takes ('Subscribe' / 'Unsubscribe' via `text`/`checkedText` in the `'history'` defaults — unlike the conversation header's state-labeled bell pill), its `change` view event answered by the thread workflow with `history.subscribe()`/`unsubscribe()`.
48
+ - **List-level error notices**: when the thread list or the thread on display fails to load (workflow status `erroneous`), the `threads`/`messages` layouts render their `[STATUS.ERRONEOUS]` template in place of the list — a status-keyed template like `[STATUS.READY]`, empty in the collection layout's defaults (the ask/search UIs present errors through `<miso-error>` instead). The two shells declare `listError` for it in their `DEFAULT_TEMPLATES` (the `INHERITED_DEFAULT_TEMPLATES` pattern): a muted `__list-error` notice whose text comes from their `errorMessage(layout, error)` template, worded apart for an expired session (`isSessionExpired`: status 401, e.g. an expired JWT). The view passes the failure through as `state.error` (`ViewActor._sliceData`), so either template can be overridden — `useLayouts({ threads: { templates: { errorMessage: (layout, error) => ... } } })` for the text, `[STATUS.ERRONEOUS]` (key `'erroneous'`) for the markup; likewise `messages`. A failed *more* page does not reach this: the history list parks as exhausted at ready status, and a failed message (content fetch, or a live message's posting/polling) presents the per-item `messageErrorBlock` instead.
48
49
  - `<miso-message-item>` — a **container element inside another container element** (`<miso-conversation>`), hosting the `message-item` workflow — an answer-based workflow that posts and streams its own content when live (ask-style), and is otherwise fed by the conversation via `updateData()` with its data actor off. Normally **bound implicitly**: the messages layout renders one per item and assigns `element.workflow` off the item binding (which also covers a just-posted message with no question id yet); a `question-id` attribute binds explicitly through the parent container's workflow instead (`conversation._getMessageWorkflow`). Its role children (`<miso-question>`, `<miso-answer>`, `<miso-sources>`, `<miso-feedback>`, ...) bind to the message workflow as usual (`getContainer` resolves to the closest container ancestor) and render one message via the standard answer-based layouts (the message workflow shares the conversation's options, whose `'conversation'` defaults hold the message roles too — omitting the query role, turning the container logo off, and giving the answer typewriter `instant: true` — already-finished answers render in one shot); citation/link clicks and feedback go out as message-level interactions, deduplicated per message.
49
50
  - `<miso-thread-item>` — the thread list's analogue of `<miso-message-item>`: a container element inside `<miso-history>`, hosting the `thread-item` workflow — data actor off, fed by History via `updateData()` with only the records that actually changed (record identity is the change signal). Normally **bound implicitly**: the threads layout renders one per item and assigns `element.workflow` off the item binding; a `thread-id` attribute binds explicitly through the parent container's workflow instead (`history._getThreadWorkflow`). Its role children — `<miso-title>` (generic `text` layout, with an 'Untitled' fallback via a `content` template), `<miso-rename>`/`<miso-delete>` (generic `button` layout with the `prompt`/`confirm` dialog options), and `<miso-subscription>` (the `checkbox` layout, the context menu's toggle) — bind to the thread workflow as usual; title and subscription map dot-paths into the record (`'title'`, `'subscribed'`), while rename and delete map the whole record — their button layouts derive the dialog text via the `value` option and render disabled (the `disabled` option) until the record carries a thread id, so a thread being created is inert in the UI too. Its container role uses the `item-container` layout, which drives the context menu found in the item's markup via the `item-menu` data-role contract (a custom body without menu markup simply has nothing to drive).
50
51
  - **Role components** (`element/role/`): register with their ancestor container; the role determines which slice of data they display. Either dedicated tags (`<miso-answer>`, `<miso-sources>`, `<miso-products>`, `<miso-query>`, ...) or generic `<miso-component role="...">`. `<miso-results>` is deprecated in favor of `<miso-products>`.
package/src/defaults.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { escapeHtml } from '@miso.ai/commons';
2
2
  import { ROLE, LAYOUT_TYPE } from './constants.js';
3
- import { compactArticle, compactArticleInfoBlock } from './layout/templates.js';
3
+ import { compactArticle, compactArticleInfoBlock, messageErrorMessage } from './layout/templates.js';
4
4
 
5
5
  /**
6
6
  * Default layout options of each workflow, keyed by workflow name. The UI
@@ -142,7 +142,9 @@ export const defaultLayouts = Object.freeze({
142
142
  // query role (a message has no query flow of its own; the query entry
143
143
  // below is the panel's composer), with the question layout (text plus
144
144
  // authorship attributes, off the whole record), the answer typewriter
145
- // with `instant: true` (an already-finished answer renders in one shot),
145
+ // with `instant: true` (an answer starts at what is already available: a
146
+ // finished one renders in one shot, an ongoing one types out only the rest),
147
+ // the error notice worded for a message (messageErrorMessage),
146
148
  // and the sources as the compact horizontal cards of the hybrid-search
147
149
  // UI. logo: false turns the per-message banner off; the panel never
148
150
  // triggers the auto logo anyway, having no main-role component
@@ -151,6 +153,7 @@ export const defaultLayouts = Object.freeze({
151
153
  [ROLE.CONTAINER]: [LAYOUT_TYPE.CONTAINER, { logo: false }],
152
154
  [ROLE.QUESTION]: LAYOUT_TYPE.QUESTION,
153
155
  [ROLE.ANSWER]: [LAYOUT_TYPE.TYPEWRITER, { instant: true }],
156
+ [ROLE.ERROR]: [LAYOUT_TYPE.ERROR, { templates: { message: messageErrorMessage } }],
154
157
  [ROLE.SOURCES]: [
155
158
  LAYOUT_TYPE.HORIZONTAL,
156
159
  {
@@ -9,7 +9,13 @@ function root(layout, state) {
9
9
  const { status } = state;
10
10
  const roleAttr = role ? ` data-role="${role}"` : '';
11
11
  const itemTypeAttr = options.itemType ? ` data-item-type="${options.itemType}"` : '';
12
- return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${status === STATUS.READY ? templates[status](layout, state) : ''}${templates.trigger(layout, state)}${templates.loading(layout, state)}</div>`;
12
+ return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${hasStatusTemplate(status) ? templates[status](layout, state) : ''}${templates.trigger(layout, state)}${templates.loading(layout, state)}</div>`;
13
+ }
14
+
15
+ // the statuses with a content template of their own: the list at ready, the
16
+ // error notice at erroneous
17
+ export function hasStatusTemplate(status) {
18
+ return status === STATUS.READY || status === STATUS.ERRONEOUS;
13
19
  }
14
20
 
15
21
  function ready(layout, state) {
@@ -74,6 +80,9 @@ const DEFAULT_TEMPLATES = Object.freeze({
74
80
  root,
75
81
  [STATUS.READY]: ready,
76
82
  empty: () => ``,
83
+ // no notice by default: the ask/search UIs present errors in a section of
84
+ // their own (<miso-error>); list layouts that stand alone supply one
85
+ [STATUS.ERRONEOUS]: () => ``,
77
86
  body,
78
87
  list,
79
88
  ordered: false,
@@ -1,9 +1,22 @@
1
1
  import { STATUS, LAYOUT_TYPE } from '../../constants.js';
2
- import CollectionLayout from './collection.js';
2
+ import CollectionLayout, { hasStatusTemplate } from './collection.js';
3
+ import { listError, isSessionExpired, SESSION_EXPIRED_MESSAGE } from '../templates.js';
4
+ import { setOrRemoveAttribute } from '../../util/dom.js';
3
5
 
4
6
  const TYPE = LAYOUT_TYPE.MESSAGES;
5
7
  const DEFAULT_CLASSNAME = 'miso-messages';
6
8
 
9
+ const DEFAULT_TEMPLATES = Object.freeze({
10
+ root,
11
+ [STATUS.ERRONEOUS]: listError,
12
+ errorMessage,
13
+ });
14
+
15
+ const INHERITED_DEFAULT_TEMPLATES = Object.freeze({
16
+ ...CollectionLayout.defaultTemplates,
17
+ ...DEFAULT_TEMPLATES,
18
+ });
19
+
7
20
  // the infinite scroll trigger sits ABOVE the list — older messages load as
8
21
  // the user scrolls up, unlike the stock collection root's bottom trigger
9
22
  function root(layout, state) {
@@ -11,7 +24,11 @@ function root(layout, state) {
11
24
  const { status } = state;
12
25
  const roleAttr = role ? ` data-role="${role}"` : '';
13
26
  const itemTypeAttr = options.itemType ? ` data-item-type="${options.itemType}"` : '';
14
- return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${templates.trigger(layout, state)}${status === STATUS.READY ? templates[status](layout, state) : ''}${templates.loading(layout, state)}</div>`;
27
+ return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${templates.trigger(layout, state)}${hasStatusTemplate(status) ? templates[status](layout, state) : ''}${templates.loading(layout, state)}</div>`;
28
+ }
29
+
30
+ function errorMessage(layout, error) {
31
+ return isSessionExpired(error) ? SESSION_EXPIRED_MESSAGE : 'This conversation could not be loaded.';
15
32
  }
16
33
 
17
34
  /**
@@ -53,8 +70,12 @@ export default class MessagesLayout extends CollectionLayout {
53
70
  return DEFAULT_CLASSNAME;
54
71
  }
55
72
 
73
+ static get defaultTemplates() {
74
+ return INHERITED_DEFAULT_TEMPLATES;
75
+ }
76
+
56
77
  constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
57
- super({ className, templates: { root, ...templates }, ...options });
78
+ super({ className, templates: { ...DEFAULT_TEMPLATES, ...templates }, ...options });
58
79
  this._pinned = true; // whether the view sticks to the bottom on updates
59
80
  this._anchor = undefined; // the scroll anchor while prepended content settles
60
81
  this._ongoing = undefined;
@@ -146,6 +167,7 @@ export default class MessagesLayout extends CollectionLayout {
146
167
  }
147
168
  this._watchGrowth(this._getListElement(element) || element);
148
169
  this._syncWorkflows(element);
170
+ this._syncItems(element);
149
171
  this._syncOngoing(state);
150
172
  }
151
173
 
@@ -168,6 +190,16 @@ export default class MessagesLayout extends CollectionLayout {
168
190
  }
169
191
  }
170
192
 
193
+ // sync in-place item state changes from the bound values onto the existing
194
+ // item elements: the live mark — the item contents render through the
195
+ // message workflows, outside this layout's render cycle
196
+ _syncItems(element) {
197
+ for (const item of this._getItemElements(element)) {
198
+ const binding = this._bindings.get(item);
199
+ binding && setOrRemoveAttribute(item, 'data-live', binding.value.live ? '' : undefined);
200
+ }
201
+ }
202
+
171
203
  // whether an answer is on its way — pending, or still being generated —
172
204
  // reported as the `ongoing` view state, e.g. for the search box to disable
173
205
  // submission
@@ -1,11 +1,26 @@
1
1
  import { isThreadUnread } from '@miso.ai/client-sdk-workflow';
2
- import { LAYOUT_TYPE } from '../../constants.js';
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
6
 
6
7
  const TYPE = LAYOUT_TYPE.THREADS;
7
8
  const DEFAULT_CLASSNAME = 'miso-threads';
8
9
 
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.';
22
+ }
23
+
9
24
  /**
10
25
  * The thread list of the chat history interface: a shell that renders one
11
26
  * <miso-thread-item> container element per thread item, incrementally —
@@ -42,8 +57,12 @@ export default class ThreadsLayout extends CollectionLayout {
42
57
  return DEFAULT_CLASSNAME;
43
58
  }
44
59
 
45
- constructor({ className = DEFAULT_CLASSNAME, ...options } = {}) {
46
- super({ className, ...options });
60
+ static get defaultTemplates() {
61
+ return INHERITED_DEFAULT_TEMPLATES;
62
+ }
63
+
64
+ constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
65
+ super({ className, templates: { ...DEFAULT_TEMPLATES, ...templates }, ...options });
47
66
  }
48
67
 
49
68
  // item identity: the thread id, or the local placeholder id standing in
@@ -81,14 +81,17 @@ export function threadMenuBlock({ className }) {
81
81
  * item subworkflow — the messages layout is a shell: it assigns the workflow
82
82
  * (and the item-level attributes) in its post-render sync pass, and the role
83
83
  * elements inside render the actual content through the message workflow's
84
- * own layouts.
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.
85
87
  */
86
88
  export function message(layout, state, data) {
87
89
  const { className, templates } = layout;
90
+ const liveAttr = data.live ? ' data-live' : '';
88
91
  // miso-circled-citation-index covers both the citation links in the answer
89
92
  // and the indexes on the source cards, as the hybrid-search UI scopes it
90
93
  return [
91
- `<miso-message-item class="${className}__item-body miso-circled-citation-index" data-role="item">`,
94
+ `<miso-message-item class="${className}__item-body miso-circled-citation-index" data-role="item"${liveAttr}>`,
92
95
  (templates.messageBody || messageBody)(layout, data),
93
96
  `</miso-message-item>`,
94
97
  ].join('');
@@ -121,11 +124,35 @@ export function messageSourcesBlock({ className }) {
121
124
  }
122
125
 
123
126
  // shown by the item's own status (visible-when): a message whose content
124
- // fetch failed for good presents this in place of its 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)
125
130
  export function messageErrorBlock({ className }) {
126
- return `<div class="${className}__error" visible-when="erroneous">This message could not be loaded.</div>`;
131
+ return `<div class="${className}__error" visible-when="erroneous"><miso-error></miso-error></div>`;
127
132
  }
128
133
 
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.';
139
+ }
140
+
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>`;
148
+ }
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.';
129
156
 
130
157
  export function article(layout, state, data, meta) {
131
158
  const { templates } = layout;
@@ -140,11 +140,12 @@ export default class TypewriterLayout extends ProgressiveLayout {
140
140
  } else {
141
141
  result = rendered;
142
142
  }
143
- if (this.options.instant && state.done && !result.cursor) {
144
- // instant mode: content that finishes before any of it was typed —
145
- // arrived complete (e.g. a loaded conversation message), or completed
146
- // while the cursor was still at the start — renders in one shot; only
147
- // content streaming in as it is generated types out
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
148
149
  result = this._renderer.update(container, result, { ...state, cursor: Infinity });
149
150
  } else if (!newStreak) {
150
151
  const cursor = this._getNextCursor(rendered.cursor, state.doneAt, rendered.timestamp, state.timestamp);
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.
@@ -0,0 +1,68 @@
1
+ import { escapeHtml } from '@miso.ai/commons';
2
+
3
+ const DEFAULT_CLASSNAME = 'miso-notice';
4
+ const DEFAULT_DURATION = 5000;
5
+
6
+ /**
7
+ * Show a transient notice (a toast) in a corner of the page, dismissed
8
+ * after `duration` milliseconds (0 keeps it until closed), or by its close
9
+ * button. A notice with the very text of one on display is not stacked:
10
+ * the displayed one stays, its timer starting over. Returns a function
11
+ * that dismisses the notice. Exposed as `MisoClient.ui.notify()`.
12
+ */
13
+ export default function notify({
14
+ message,
15
+ type = 'info', // 'info' | 'error'
16
+ duration = DEFAULT_DURATION,
17
+ className = DEFAULT_CLASSNAME,
18
+ } = {}) {
19
+ if (!message) {
20
+ return () => {};
21
+ }
22
+ const container = getContainer(className);
23
+ let notice = [...container.children].find(element => element._misoNotice && element._misoNotice.key === `${type}:${message}`);
24
+ if (!notice) {
25
+ notice = createNotice(className, type, message);
26
+ container.appendChild(notice);
27
+ }
28
+ notice._misoNotice.schedule(duration);
29
+ return notice._misoNotice.dismiss;
30
+ }
31
+
32
+ // the stack hosting the notices, created on demand and removed when empty
33
+ function getContainer(className) {
34
+ let container = document.querySelector(`.${className}-stack`);
35
+ if (!container) {
36
+ container = document.createElement('div');
37
+ container.className = `${className}-stack`;
38
+ container.setAttribute('aria-live', 'polite');
39
+ document.body.appendChild(container);
40
+ }
41
+ return container;
42
+ }
43
+
44
+ function createNotice(className, type, message) {
45
+ const notice = document.createElement('div');
46
+ notice.className = `${className} ${className}--${type}`;
47
+ notice.setAttribute('role', type === 'error' ? 'alert' : 'status');
48
+ notice.innerHTML = [
49
+ `<div class="${className}__message">${escapeHtml(message)}</div>`,
50
+ `<button type="button" class="${className}__close-button" data-role="close" aria-label="Dismiss">&times;</button>`,
51
+ ].join('');
52
+ let timer;
53
+ const dismiss = () => {
54
+ clearTimeout(timer);
55
+ const container = notice.parentElement;
56
+ notice.remove();
57
+ container && container.children.length === 0 && container.remove();
58
+ };
59
+ const schedule = (duration) => {
60
+ clearTimeout(timer);
61
+ timer = duration > 0 ? setTimeout(dismiss, duration) : undefined;
62
+ };
63
+ notice.addEventListener('click', (event) => {
64
+ event.target.closest('[data-role="close"]') && dismiss();
65
+ });
66
+ notice._misoNotice = { key: `${type}:${message}`, dismiss, schedule };
67
+ return notice;
68
+ }
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.14.0-beta.3';
1
+ export default '1.14.0-beta.4';