@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.
- package/dist/cjs/defaults/ask/index.js +2 -1
- package/dist/cjs/defaults/ask/templates.js +2 -1
- package/dist/cjs/defaults/hybrid-search/index.js +2 -1
- package/dist/cjs/defaults/hybrid-search/templates.js +2 -1
- package/dist/cjs/defaults/index.js +2 -1
- package/dist/cjs/defaults.js +9 -2
- package/dist/cjs/element/index.js +2 -1
- package/dist/cjs/layout/list/collection.js +11 -1
- package/dist/cjs/layout/list/messages.js +30 -6
- package/dist/cjs/layout/list/threads.js +17 -3
- package/dist/cjs/layout/templates.js +37 -4
- package/dist/cjs/layout/text/typewriter/index.js +6 -5
- package/dist/cjs/plugin.js +6 -3
- package/dist/cjs/util/notify.js +74 -0
- package/dist/cjs/version.js +1 -1
- package/package.json +3 -3
- package/src/CLAUDE.md +2 -1
- package/src/defaults.js +5 -2
- package/src/layout/list/collection.js +10 -1
- package/src/layout/list/messages.js +35 -3
- package/src/layout/list/threads.js +22 -3
- package/src/layout/templates.js +31 -4
- package/src/layout/text/typewriter/index.js +6 -5
- package/src/plugin.js +3 -2
- package/src/util/notify.js +68 -0
- package/src/version.js +1 -1
|
@@ -24,7 +24,8 @@ Object.keys(_controls).forEach(function (key) {
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/cjs/defaults.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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)}${
|
|
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
|
|
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"
|
|
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 &&
|
|
167
|
-
// instant mode:
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
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
|
}));
|
package/dist/cjs/plugin.js
CHANGED
|
@@ -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
|
-
|
|
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">×</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
|
+
}
|
package/dist/cjs/version.js
CHANGED
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.
|
|
34
|
-
"@miso.ai/client-sdk-workflow": "1.14.0-beta.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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: {
|
|
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
|
-
|
|
46
|
-
|
|
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
|
package/src/layout/templates.js
CHANGED
|
@@ -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
|
|
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"
|
|
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 &&
|
|
144
|
-
// instant mode:
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
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">×</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.
|
|
1
|
+
export default '1.14.0-beta.4';
|