@miso.ai/client-sdk-ui 1.14.0-beta.2 → 1.14.0-beta.3
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/asset/svgs.js +4 -2
- package/dist/cjs/constants.js +2 -0
- package/dist/cjs/defaults.js +100 -10
- package/dist/cjs/element/container/index.js +14 -0
- package/dist/cjs/element/container/miso-message-item.js +91 -0
- package/dist/cjs/element/container/miso-thread-item.js +91 -0
- package/dist/cjs/element/role/index.js +7 -0
- package/dist/cjs/element/role/miso-delete.js +20 -0
- package/dist/cjs/layout/button/button.js +45 -5
- package/dist/cjs/layout/button/checkbox.js +2 -1
- package/dist/cjs/layout/container/item-container.js +99 -0
- package/dist/cjs/layout/index.js +14 -0
- package/dist/cjs/layout/input/search-box.js +9 -4
- package/dist/cjs/layout/list/collection.js +10 -3
- package/dist/cjs/layout/list/messages.js +239 -226
- package/dist/cjs/layout/list/threads.js +97 -155
- package/dist/cjs/layout/templates.js +65 -38
- package/dist/cjs/layout/text/question.js +71 -0
- package/dist/cjs/layout/text/typewriter/index.js +14 -21
- package/dist/cjs/util/dom.js +21 -0
- package/dist/cjs/version.js +1 -1
- package/package.json +3 -3
- package/src/CLAUDE.md +6 -2
- package/src/asset/svgs.js +3 -1
- package/src/constants.js +2 -0
- package/src/defaults.js +71 -4
- package/src/element/container/index.js +2 -0
- package/src/element/container/miso-message-item.js +94 -0
- package/src/element/container/miso-thread-item.js +94 -0
- package/src/element/role/index.js +1 -0
- package/src/element/role/miso-delete.js +16 -0
- package/src/layout/button/button.js +40 -4
- package/src/layout/button/checkbox.js +2 -1
- package/src/layout/container/item-container.js +99 -0
- package/src/layout/index.js +2 -0
- package/src/layout/input/search-box.js +4 -2
- package/src/layout/list/collection.js +12 -4
- package/src/layout/list/messages.js +202 -204
- package/src/layout/list/threads.js +90 -130
- package/src/layout/templates.js +74 -39
- package/src/layout/text/question.js +58 -0
- package/src/layout/text/typewriter/index.js +10 -15
- package/src/util/dom.js +16 -0
- package/src/version.js +1 -1
package/dist/cjs/layout/index.js
CHANGED
|
@@ -81,6 +81,12 @@ Object.defineProperty(exports, "HorizontalLayout", {
|
|
|
81
81
|
return _horizontal.default;
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
|
+
Object.defineProperty(exports, "ItemContainerLayout", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
get: function () {
|
|
87
|
+
return _itemContainer.default;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
84
90
|
Object.defineProperty(exports, "ListLayout", {
|
|
85
91
|
enumerable: true,
|
|
86
92
|
get: function () {
|
|
@@ -105,6 +111,12 @@ Object.defineProperty(exports, "OptionListLayout", {
|
|
|
105
111
|
return _optionList.default;
|
|
106
112
|
}
|
|
107
113
|
});
|
|
114
|
+
Object.defineProperty(exports, "QuestionLayout", {
|
|
115
|
+
enumerable: true,
|
|
116
|
+
get: function () {
|
|
117
|
+
return _question.default;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
108
120
|
Object.defineProperty(exports, "SearchBoxLayout", {
|
|
109
121
|
enumerable: true,
|
|
110
122
|
get: function () {
|
|
@@ -143,6 +155,7 @@ Object.defineProperty(exports, "TypewriterLayout", {
|
|
|
143
155
|
});
|
|
144
156
|
var _template = _interopRequireDefault(require("./template.js"));
|
|
145
157
|
var _container = _interopRequireDefault(require("./container/container.js"));
|
|
158
|
+
var _itemContainer = _interopRequireDefault(require("./container/item-container.js"));
|
|
146
159
|
var _error = _interopRequireDefault(require("./error/error.js"));
|
|
147
160
|
var _banner = _interopRequireDefault(require("./banner/banner.js"));
|
|
148
161
|
var _collection = _interopRequireDefault(require("./list/collection.js"));
|
|
@@ -157,6 +170,7 @@ var _searchBox = _interopRequireDefault(require("./input/search-box.js"));
|
|
|
157
170
|
var _optionList = _interopRequireDefault(require("./input/option-list.js"));
|
|
158
171
|
var _feedback = _interopRequireDefault(require("./radio/feedback.js"));
|
|
159
172
|
var _text = _interopRequireDefault(require("./text/text.js"));
|
|
173
|
+
var _question = _interopRequireDefault(require("./text/question.js"));
|
|
160
174
|
var _index = _interopRequireDefault(require("./text/typewriter/index.js"));
|
|
161
175
|
var _affiliation = _interopRequireDefault(require("./affiliation/affiliation.js"));
|
|
162
176
|
var _facets = _interopRequireDefault(require("./input/facets.js"));
|
|
@@ -406,10 +406,15 @@ class SearchBoxLayout extends _template.default {
|
|
|
406
406
|
return;
|
|
407
407
|
}
|
|
408
408
|
this.close();
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
// a view event, like the buttons: the workflow routes it into its query
|
|
410
|
+
// flow (or its own submission semantics), the layout stays hands-off
|
|
411
|
+
const {
|
|
412
|
+
session
|
|
413
|
+
} = this._view._state || {};
|
|
414
|
+
this._view._emit('submit', Object.freeze({
|
|
415
|
+
session,
|
|
416
|
+
value
|
|
417
|
+
}));
|
|
413
418
|
if (this.options.clearOnSubmit) {
|
|
414
419
|
const inputElement = this._element && this._element.querySelector(`[data-role="input"]`);
|
|
415
420
|
if (inputElement) {
|
|
@@ -96,10 +96,10 @@ const DEFAULT_TEMPLATES = Object.freeze({
|
|
|
96
96
|
image: _templates.image,
|
|
97
97
|
question: _templates.question,
|
|
98
98
|
thread: _templates.thread,
|
|
99
|
+
threadBody: _templates.threadBody,
|
|
99
100
|
threadMenuBlock: _templates.threadMenuBlock,
|
|
100
101
|
message: _templates.message,
|
|
101
|
-
|
|
102
|
-
messageAnswerBlock: _templates.messageAnswerBlock,
|
|
102
|
+
messageBody: _templates.messageBody,
|
|
103
103
|
root,
|
|
104
104
|
[_constants.STATUS.READY]: ready,
|
|
105
105
|
empty: () => ``,
|
|
@@ -208,7 +208,14 @@ class CollectionLayout extends _template.default {
|
|
|
208
208
|
return element.querySelector('[data-role="list"]');
|
|
209
209
|
}
|
|
210
210
|
_getItemElements(element) {
|
|
211
|
-
|
|
211
|
+
if (!element) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
// an item may host a nested collection with items of its own (a
|
|
215
|
+
// conversation message and its source cards): ours are only those whose
|
|
216
|
+
// closest list is this layout's own list
|
|
217
|
+
const listElement = this._getListElement(element);
|
|
218
|
+
return Array.from((listElement || element).querySelectorAll(`[data-role="item"]`)).filter(item => item.closest(`[data-role="list"]`) === listElement);
|
|
212
219
|
}
|
|
213
220
|
_onClick(event) {
|
|
214
221
|
const element = event.target.closest(`[data-role="item"]`);
|
|
@@ -4,34 +4,59 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
-
var _commons = require("@miso.ai/commons");
|
|
8
|
-
var _clientSdkWorkflow = require("@miso.ai/client-sdk-workflow");
|
|
9
7
|
var _constants = require("../../constants.js");
|
|
10
8
|
var _collection = _interopRequireDefault(require("./collection.js"));
|
|
11
|
-
|
|
12
|
-
var _templates = require("../templates.js");
|
|
13
|
-
var _dom = require("../../util/dom.js");
|
|
14
|
-
const _excluded = ["className"];
|
|
9
|
+
const _excluded = ["className", "templates"];
|
|
15
10
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
11
|
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; }
|
|
17
12
|
const TYPE = _constants.LAYOUT_TYPE.MESSAGES;
|
|
18
13
|
const DEFAULT_CLASSNAME = 'miso-messages';
|
|
19
|
-
|
|
14
|
+
|
|
15
|
+
// the infinite scroll trigger sits ABOVE the list — older messages load as
|
|
16
|
+
// the user scrolls up, unlike the stock collection root's bottom trigger
|
|
17
|
+
function root(layout, state) {
|
|
18
|
+
const {
|
|
19
|
+
className,
|
|
20
|
+
role,
|
|
21
|
+
templates,
|
|
22
|
+
options
|
|
23
|
+
} = layout;
|
|
24
|
+
const {
|
|
25
|
+
status
|
|
26
|
+
} = state;
|
|
27
|
+
const roleAttr = role ? ` data-role="${role}"` : '';
|
|
28
|
+
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>`;
|
|
30
|
+
}
|
|
20
31
|
|
|
21
32
|
/**
|
|
22
|
-
* The conversation panel of the chat history interface: a
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* (
|
|
33
|
+
* The conversation panel of the chat history interface: a shell that renders
|
|
34
|
+
* one <miso-message-item> container element per message item, incrementally —
|
|
35
|
+
* fresh items land at either end of the list: appended messages (a posted
|
|
36
|
+
* follow-up question) render as new items at the bottom, while an older
|
|
37
|
+
* page fetched by the more flow (infinite scroll: the trigger, placed above
|
|
38
|
+
* the list, fires the `more` hub field as the user scrolls up) renders as
|
|
39
|
+
* prepended items — told apart by which boundary the already-rendered
|
|
40
|
+
* records line up with.
|
|
41
|
+
* The content of a message is not rendered here: each <miso-message-item> is
|
|
42
|
+
* assigned its item subworkflow (workflow._getMessageWorkflow, off the item
|
|
43
|
+
* binding) in the post-render sync pass, and the role elements inside
|
|
44
|
+
* (question, answer, ...) render through that workflow's own layouts.
|
|
45
|
+
*
|
|
46
|
+
* All message-level presentation is the message workflow's own: a record
|
|
47
|
+
* without its answer body presents as status `loading`, stamped on the
|
|
48
|
+
* <miso-message-item> element by its container layout — there is deliberately no
|
|
49
|
+
* per-message loading icon; the answer typewriter's blinking caret is the
|
|
50
|
+
* loading indication — and the question bubble (text, authorship
|
|
51
|
+
* attributes) renders through the `question` layout.
|
|
27
52
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
53
|
+
* The shell keeps the panel-level behaviors: scroll pinning (the view sticks
|
|
54
|
+
* to the bottom while the user has not scrolled up, following content growth
|
|
55
|
+
* as answers type out), scroll anchoring while scrolled up (prepended items
|
|
56
|
+
* — and their contents, rendering in later through their own workflows —
|
|
57
|
+
* must not shift the reading position), and the `ongoing` view state read
|
|
58
|
+
* off the data — the last message's answer absent or unfinished — which
|
|
59
|
+
* e.g. disables the search box while an answer is on its way.
|
|
35
60
|
*/
|
|
36
61
|
class MessagesLayout extends _collection.default {
|
|
37
62
|
static get type() {
|
|
@@ -42,172 +67,157 @@ class MessagesLayout extends _collection.default {
|
|
|
42
67
|
}
|
|
43
68
|
constructor(_ref = {}) {
|
|
44
69
|
let {
|
|
45
|
-
className = DEFAULT_CLASSNAME
|
|
70
|
+
className = DEFAULT_CLASSNAME,
|
|
71
|
+
templates
|
|
46
72
|
} = _ref,
|
|
47
73
|
options = _objectWithoutPropertiesLoose(_ref, _excluded);
|
|
48
74
|
super(Object.assign({
|
|
49
|
-
className
|
|
75
|
+
className,
|
|
76
|
+
templates: Object.assign({
|
|
77
|
+
root
|
|
78
|
+
}, templates)
|
|
50
79
|
}, options));
|
|
51
|
-
this._renderedAnswers = new WeakMap(); // answer element -> rendered markdown
|
|
52
|
-
this._typewriters = new WeakMap(); // answer element -> MessageTypewriter
|
|
53
|
-
this._readiness = new _commons.Resolution();
|
|
54
80
|
this._pinned = true; // whether the view sticks to the bottom on updates
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
MessagesLayout.MisoClient.plugins.install('std:ui-markdown');
|
|
58
|
-
}
|
|
59
|
-
initialize(view) {
|
|
60
|
-
super.initialize(view);
|
|
61
|
-
this._setup();
|
|
81
|
+
this._anchor = undefined; // the scroll anchor while prepended content settles
|
|
82
|
+
this._ongoing = undefined;
|
|
62
83
|
}
|
|
63
84
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return; // destroyed
|
|
70
|
-
}
|
|
71
|
-
this._markdown = plugin.getContext(this._view.workflow._client);
|
|
72
|
-
this._readiness.resolve();
|
|
73
|
-
} catch (e) {
|
|
74
|
-
this._readiness.reject(e);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
async _ready() {
|
|
78
|
-
return this._readiness.promise;
|
|
85
|
+
// item identity: the question id, or the local placeholder id standing in
|
|
86
|
+
// for it while a posted question awaits its response — the placeholder id
|
|
87
|
+
// survives the settle, so the element and its workflow do too
|
|
88
|
+
_getItemKey(message) {
|
|
89
|
+
return message.placeholder_id || message.question_id || message;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
// render //
|
|
82
93
|
async render(element, state, controls = {}) {
|
|
83
|
-
// capture the view state callback
|
|
84
|
-
// `ongoing` state outside the render cycle
|
|
94
|
+
// capture the view state callback for the ongoing report
|
|
85
95
|
this._notifyUpdate = controls.notifyUpdate;
|
|
86
96
|
await super.render(element, state, controls);
|
|
87
97
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!state
|
|
94
|
-
|
|
95
|
-
// jump to the bottom
|
|
96
|
-
this._scrollToBottom({
|
|
97
|
-
force: true
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
this._syncMessages(element).catch(error => console.error(error));
|
|
101
|
-
}
|
|
102
|
-
async _syncMessages(element) {
|
|
103
|
-
await this._ready();
|
|
104
|
-
const items = this._getItemElements(element);
|
|
105
|
-
let displaying = false;
|
|
106
|
-
for (let i = 0; i < items.length; i++) {
|
|
107
|
-
const item = items[i];
|
|
108
|
-
const binding = this._bindings.get(item);
|
|
109
|
-
if (!binding) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
const message = binding.value;
|
|
113
|
-
const last = i === items.length - 1;
|
|
114
|
-
this._syncQuestion(item, message);
|
|
115
|
-
this._syncAnswer(item, message, {
|
|
116
|
-
last
|
|
117
|
-
});
|
|
118
|
-
if (last) {
|
|
119
|
-
displaying = this._isDisplaying(item, message);
|
|
120
|
-
}
|
|
98
|
+
|
|
99
|
+
// incremental renders cover a pure append (a posted follow-up at the
|
|
100
|
+
// bottom) or a pure prepend (an older page above) — anything else
|
|
101
|
+
// re-renders in full
|
|
102
|
+
_shallRenderIncrementally(state, rendered) {
|
|
103
|
+
if (!super._shallRenderIncrementally(state, rendered)) {
|
|
104
|
+
return false;
|
|
121
105
|
}
|
|
122
|
-
this.
|
|
106
|
+
const values = this._getItems(state) || [];
|
|
107
|
+
const renderedValues = rendered.value;
|
|
108
|
+
const count = values.length - renderedValues.length;
|
|
109
|
+
return count === 0 ||
|
|
110
|
+
// in-place changes only
|
|
111
|
+
this._getItemKey(values[0]) === this._getItemKey(renderedValues[0]) ||
|
|
112
|
+
// appended
|
|
113
|
+
this._getItemKey(values[count]) === this._getItemKey(renderedValues[0]); // prepended
|
|
123
114
|
}
|
|
124
115
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!(0, _clientSdkWorkflow.hasAnswer)(message)) {
|
|
132
|
-
return true; // waiting for the answer body
|
|
116
|
+
// the fresh items land at the tail (a posted follow-up) or at the head
|
|
117
|
+
// (an older page fetched by the more flow): told apart by which boundary
|
|
118
|
+
// the already-rendered records line up with
|
|
119
|
+
_html(state, rendered, incremental) {
|
|
120
|
+
if (!incremental) {
|
|
121
|
+
return this.templates.root(this, state);
|
|
133
122
|
}
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const questionElement = item.querySelector('[data-role="question"]');
|
|
140
|
-
if (!questionElement) {
|
|
141
|
-
return;
|
|
123
|
+
const values = this._getItems(state) || [];
|
|
124
|
+
const renderedValues = rendered.value;
|
|
125
|
+
const count = values.length - renderedValues.length;
|
|
126
|
+
if (count === 0) {
|
|
127
|
+
return '';
|
|
142
128
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
129
|
+
if (this._getItemKey(values[0]) === this._getItemKey(renderedValues[0])) {
|
|
130
|
+
const fresh = values.slice(renderedValues.length);
|
|
131
|
+
return {
|
|
132
|
+
position: 'beforeend',
|
|
133
|
+
items: this.templates.items(this, state, fresh, {
|
|
134
|
+
offset: renderedValues.length
|
|
135
|
+
})
|
|
136
|
+
};
|
|
151
137
|
}
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
const fresh = values.slice(0, count);
|
|
139
|
+
return {
|
|
140
|
+
position: 'afterbegin',
|
|
141
|
+
items: this.templates.items(this, state, fresh, {
|
|
142
|
+
offset: 0
|
|
143
|
+
})
|
|
144
|
+
};
|
|
154
145
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
146
|
+
_render(element, {
|
|
147
|
+
state
|
|
148
|
+
}, {
|
|
149
|
+
notifyUpdate
|
|
150
|
+
}) {
|
|
151
|
+
const {
|
|
152
|
+
incremental,
|
|
153
|
+
html
|
|
154
|
+
} = state;
|
|
155
|
+
if (incremental) {
|
|
156
|
+
if (html) {
|
|
157
|
+
const listElement = this._getListElement(element);
|
|
158
|
+
if (html.position === 'afterbegin') {
|
|
159
|
+
this._prependAnchored(listElement, html.items);
|
|
160
|
+
} else {
|
|
161
|
+
listElement.insertAdjacentHTML('beforeend', html.items);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
notifyUpdate(false);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
element.innerHTML = html;
|
|
170
168
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
}
|
|
170
|
+
_afterRender(element, state) {
|
|
171
|
+
super._afterRender(element, state); // syncs bindings and (re)arms the trigger
|
|
172
|
+
if (!state.incremental) {
|
|
173
|
+
// a fresh render (thread load): jump to the bottom
|
|
174
|
+
this._dropAnchor();
|
|
175
|
+
this._scrollToBottom({
|
|
176
|
+
force: true
|
|
177
|
+
});
|
|
178
|
+
} else if (state.html && state.html.position === 'beforeend') {
|
|
179
|
+
// appended items (posted follow-up): jump to the bottom
|
|
180
|
+
this._scrollToBottom({
|
|
181
|
+
force: true
|
|
178
182
|
});
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
}
|
|
184
|
+
this._watchGrowth(this._getListElement(element) || element);
|
|
185
|
+
this._syncWorkflows(element);
|
|
186
|
+
this._syncOngoing(state);
|
|
187
|
+
}
|
|
188
|
+
_unrender() {
|
|
189
|
+
super._unrender();
|
|
190
|
+
this._dropAnchor();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// assign each <miso-message-item> its item subworkflow, off the item binding
|
|
194
|
+
_syncWorkflows(element) {
|
|
195
|
+
const workflow = this._view && this._view.workflow;
|
|
196
|
+
if (!workflow || typeof workflow._getMessageWorkflow !== 'function') {
|
|
181
197
|
return;
|
|
182
198
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
for (const item of this._getItemElements(element)) {
|
|
200
|
+
const binding = this._bindings.get(item);
|
|
201
|
+
if (binding && item.isContainer) {
|
|
202
|
+
item.workflow = workflow._getMessageWorkflow(binding.value);
|
|
203
|
+
}
|
|
186
204
|
}
|
|
187
|
-
this._renderedAnswers.set(answerElement, message.answer);
|
|
188
|
-
this._transform(answerElement, message).then(() => {
|
|
189
|
-
last && this._scrollToBottom();
|
|
190
|
-
}).catch(error => {
|
|
191
|
-
this._renderedAnswers.delete(answerElement);
|
|
192
|
-
console.error(error);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
async _transform(answerElement, {
|
|
196
|
-
answer,
|
|
197
|
-
sources
|
|
198
|
-
}) {
|
|
199
|
-
answerElement.innerHTML = await this._markdown.transform(answer, sources);
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
// whether
|
|
203
|
-
// the `ongoing` view state, e.g. for the search box to disable
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
// whether an answer is on its way — pending, or still being generated —
|
|
208
|
+
// reported as the `ongoing` view state, e.g. for the search box to disable
|
|
209
|
+
// submission
|
|
210
|
+
_syncOngoing(state) {
|
|
211
|
+
const messages = state.value || [];
|
|
212
|
+
const last = messages[messages.length - 1];
|
|
213
|
+
// a message settled by a failed content fetch is not on its way anymore
|
|
214
|
+
const ongoing = !!last && !last.error && (last.answer === undefined || last.finished === false);
|
|
215
|
+
if (this._ongoing === ongoing) {
|
|
206
216
|
return;
|
|
207
217
|
}
|
|
208
|
-
this.
|
|
218
|
+
this._ongoing = ongoing;
|
|
209
219
|
this._notifyUpdate && this._notifyUpdate({
|
|
210
|
-
ongoing
|
|
220
|
+
ongoing
|
|
211
221
|
});
|
|
212
222
|
}
|
|
213
223
|
|
|
@@ -250,6 +260,14 @@ class MessagesLayout extends _collection.default {
|
|
|
250
260
|
this._unwatchScroll && this._unwatchScroll();
|
|
251
261
|
const onScroll = () => {
|
|
252
262
|
this._pinned = scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight < 40;
|
|
263
|
+
if (this._pinned) {
|
|
264
|
+
// back at the bottom: the reading position needs no anchor
|
|
265
|
+
this._anchor = undefined;
|
|
266
|
+
} else if (this._anchor && this._anchor.element.isConnected) {
|
|
267
|
+
// follow the user: the anchor holds the position they scrolled to,
|
|
268
|
+
// not the one where the prepend happened
|
|
269
|
+
this._anchor.top = this._relativeTop(this._anchor.element, scrollable);
|
|
270
|
+
}
|
|
253
271
|
};
|
|
254
272
|
scrollable.addEventListener('scroll', onScroll, {
|
|
255
273
|
passive: true
|
|
@@ -262,85 +280,80 @@ class MessagesLayout extends _collection.default {
|
|
|
262
280
|
};
|
|
263
281
|
this._unsubscribes.push(() => this._unwatchScroll === unwatch && unwatch());
|
|
264
282
|
}
|
|
265
|
-
}
|
|
266
283
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.
|
|
280
|
-
this.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this._renderer = markdown.createRenderer({
|
|
287
|
-
cursorClass: (0, _utils.cursorClassName)(TYPEWRITER_CLASSNAME),
|
|
288
|
-
getSource: index => (this._sources || [])[index]
|
|
289
|
-
});
|
|
290
|
-
this._getNextCursor = (0, _commons.pacer)();
|
|
291
|
-
this._rendered = this._renderer.clear(element);
|
|
292
|
-
this._timestamp = undefined;
|
|
293
|
-
this._value = '';
|
|
294
|
-
this._sources = undefined;
|
|
295
|
-
this._dataDone = false;
|
|
296
|
-
this._doneAt = undefined;
|
|
297
|
-
this._requested = false;
|
|
284
|
+
// the message contents render (and type out) through their own workflows,
|
|
285
|
+
// outside this layout's render cycle: follow the content growth — pinned,
|
|
286
|
+
// the view sticks to the bottom; scrolled up with an anchor active (a
|
|
287
|
+
// prepended page still rendering its contents in), the anchor is held
|
|
288
|
+
// stationary instead
|
|
289
|
+
_watchGrowth(element) {
|
|
290
|
+
if (!element || this._watchedGrowth === element || typeof ResizeObserver === 'undefined') {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this._unwatchGrowth && this._unwatchGrowth();
|
|
294
|
+
const observer = new ResizeObserver(() => this._onContentGrowth());
|
|
295
|
+
observer.observe(element);
|
|
296
|
+
this._watchedGrowth = element;
|
|
297
|
+
const unwatch = this._unwatchGrowth = () => {
|
|
298
|
+
observer.disconnect();
|
|
299
|
+
this._watchedGrowth = undefined;
|
|
300
|
+
this._unwatchGrowth = undefined;
|
|
301
|
+
};
|
|
302
|
+
this._unsubscribes.push(() => this._unwatchGrowth === unwatch && unwatch());
|
|
298
303
|
}
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
_onContentGrowth() {
|
|
305
|
+
if (this._pinned) {
|
|
306
|
+
this._scrollToBottom();
|
|
307
|
+
} else {
|
|
308
|
+
this._restoreAnchor();
|
|
309
|
+
}
|
|
301
310
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
+
|
|
312
|
+
// scroll anchoring for prepended content //
|
|
313
|
+
// The anchor is the previously-first item: everything prepended — and
|
|
314
|
+
// its content, rendering in later through the item workflows — sits
|
|
315
|
+
// above it, so holding the anchor stationary holds the reading position,
|
|
316
|
+
// while growth below it (a streaming answer at the bottom) moves nothing
|
|
317
|
+
// above and needs no compensation. Not the browser's own scroll
|
|
318
|
+
// anchoring, which not every engine provides.
|
|
319
|
+
_prependAnchored(listElement, items) {
|
|
320
|
+
this._setAnchor();
|
|
321
|
+
listElement.insertAdjacentHTML('afterbegin', items);
|
|
322
|
+
this._restoreAnchor();
|
|
311
323
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
_setAnchor() {
|
|
325
|
+
const scrollable = this._getScrollable();
|
|
326
|
+
const element = this._getItemElements(this._element)[0];
|
|
327
|
+
this._anchor = scrollable && element ? {
|
|
328
|
+
element,
|
|
329
|
+
scrollable,
|
|
330
|
+
top: this._relativeTop(element, scrollable)
|
|
331
|
+
} : undefined;
|
|
318
332
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (!
|
|
322
|
-
// the element is gone (re-render, thread switch); stop typing
|
|
323
|
-
this._onDone && this._onDone();
|
|
333
|
+
_restoreAnchor() {
|
|
334
|
+
const anchor = this._anchor;
|
|
335
|
+
if (!anchor) {
|
|
324
336
|
return;
|
|
325
337
|
}
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
338
|
+
const {
|
|
339
|
+
element,
|
|
340
|
+
scrollable,
|
|
341
|
+
top
|
|
342
|
+
} = anchor;
|
|
343
|
+
if (!element.isConnected) {
|
|
344
|
+
this._anchor = undefined;
|
|
345
|
+
return;
|
|
330
346
|
}
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
cursor,
|
|
335
|
-
timestamp,
|
|
336
|
-
done: this._dataDone
|
|
337
|
-
});
|
|
338
|
-
this._timestamp = timestamp;
|
|
339
|
-
this._onUpdate && this._onUpdate();
|
|
340
|
-
if (!this._rendered.done) {
|
|
341
|
-
this._requestFrame();
|
|
342
|
-
} else {
|
|
343
|
-
this._onDone && this._onDone();
|
|
347
|
+
const delta = this._relativeTop(element, scrollable) - top;
|
|
348
|
+
if (delta) {
|
|
349
|
+
scrollable.scrollTop += delta;
|
|
344
350
|
}
|
|
345
351
|
}
|
|
346
|
-
|
|
352
|
+
_relativeTop(element, scrollable) {
|
|
353
|
+
return element.getBoundingClientRect().top - scrollable.getBoundingClientRect().top;
|
|
354
|
+
}
|
|
355
|
+
_dropAnchor() {
|
|
356
|
+
this._anchor = undefined;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
exports.default = MessagesLayout;
|