@miso.ai/client-sdk-ui 1.13.2-beta.0 → 1.14.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/asset/svgs.js +15 -4
- package/dist/cjs/combo/ask/index.js +2 -2
- package/dist/cjs/constants.js +118 -98
- package/dist/cjs/defaults/ask/controls.js +1 -1
- package/dist/cjs/defaults/hybrid-search/answer-box.js +4 -4
- package/dist/cjs/defaults.js +169 -0
- package/dist/cjs/element/container/index.js +14 -0
- package/dist/cjs/element/container/miso-ask.js +3 -3
- package/dist/cjs/element/container/miso-conversation.js +21 -0
- package/dist/cjs/element/container/miso-explore.js +1 -1
- package/dist/cjs/element/container/miso-history.js +21 -0
- package/dist/cjs/element/container/miso-hybrid-search.js +1 -1
- package/dist/cjs/element/container/miso-recommendation.js +1 -1
- package/dist/cjs/element/container/miso-search.js +1 -1
- package/dist/cjs/element/context/miso-follow-ups.js +1 -1
- package/dist/cjs/element/role/index.js +42 -0
- package/dist/cjs/element/role/miso-messages.js +20 -0
- package/dist/cjs/element/role/miso-new-thread.js +20 -0
- package/dist/cjs/element/role/miso-rename.js +20 -0
- package/dist/cjs/element/role/miso-subscription.js +20 -0
- package/dist/cjs/element/role/miso-threads.js +20 -0
- package/dist/cjs/element/role/miso-title.js +20 -0
- package/dist/cjs/layout/affiliation/affiliation.js +1 -1
- package/dist/cjs/layout/banner/banner.js +2 -1
- package/dist/cjs/layout/button/button.js +130 -0
- package/dist/cjs/layout/button/checkbox.js +115 -0
- package/dist/cjs/layout/button/more-button.js +3 -3
- package/dist/cjs/layout/container/container.js +1 -1
- package/dist/cjs/layout/error/error.js +2 -1
- package/dist/cjs/layout/index.js +28 -0
- package/dist/cjs/layout/input/facets.js +4 -3
- package/dist/cjs/layout/input/option-list.js +3 -2
- package/dist/cjs/layout/input/search-box.js +42 -9
- package/dist/cjs/layout/input/select.js +2 -1
- package/dist/cjs/layout/list/cards.js +2 -1
- package/dist/cjs/layout/list/carousel.js +2 -1
- package/dist/cjs/layout/list/collection.js +5 -0
- package/dist/cjs/layout/list/gallery.js +2 -1
- package/dist/cjs/layout/list/horizontal.js +2 -1
- package/dist/cjs/layout/list/list.js +2 -1
- package/dist/cjs/layout/list/messages.js +346 -0
- package/dist/cjs/layout/list/threads.js +257 -0
- package/dist/cjs/layout/mixin/trackable.js +4 -5
- package/dist/cjs/layout/mixin/triggerable.js +2 -2
- package/dist/cjs/layout/radio/feedback.js +4 -3
- package/dist/cjs/layout/templates.js +70 -0
- package/dist/cjs/layout/text/text.js +2 -1
- package/dist/cjs/layout/text/typewriter/index.js +4 -2
- package/dist/cjs/plugin.js +43 -104
- package/dist/cjs/util/confirm.js +33 -0
- package/dist/cjs/util/modal.js +48 -0
- package/dist/cjs/util/prompt.js +47 -0
- package/dist/cjs/version.js +1 -1
- package/package.json +5 -4
- package/src/CLAUDE.md +48 -105
- package/src/asset/svgs.js +14 -3
- package/src/combo/ask/index.js +2 -2
- package/src/constants.js +37 -147
- package/src/defaults/ask/controls.js +1 -1
- package/src/defaults/hybrid-search/answer-box.js +2 -2
- package/src/defaults.js +107 -0
- package/src/element/container/index.js +2 -0
- package/src/element/container/miso-ask.js +3 -3
- package/src/element/container/miso-conversation.js +19 -0
- package/src/element/container/miso-explore.js +1 -1
- package/src/element/container/miso-history.js +19 -0
- package/src/element/container/miso-hybrid-search.js +1 -1
- package/src/element/container/miso-recommendation.js +1 -1
- package/src/element/container/miso-search.js +1 -1
- package/src/element/context/miso-follow-ups.js +1 -1
- package/src/element/role/index.js +6 -0
- package/src/element/role/miso-messages.js +16 -0
- package/src/element/role/miso-new-thread.js +16 -0
- package/src/element/role/miso-rename.js +16 -0
- package/src/element/role/miso-subscription.js +16 -0
- package/src/element/role/miso-threads.js +16 -0
- package/src/element/role/miso-title.js +16 -0
- package/src/layout/affiliation/affiliation.js +2 -2
- package/src/layout/banner/banner.js +2 -1
- package/src/layout/button/button.js +116 -0
- package/src/layout/button/checkbox.js +91 -0
- package/src/layout/button/more-button.js +3 -3
- package/src/layout/container/container.js +2 -2
- package/src/layout/error/error.js +2 -1
- package/src/layout/index.js +4 -0
- package/src/layout/input/facets.js +3 -2
- package/src/layout/input/option-list.js +3 -2
- package/src/layout/input/search-box.js +41 -6
- package/src/layout/input/select.js +2 -1
- package/src/layout/list/cards.js +2 -1
- package/src/layout/list/carousel.js +2 -1
- package/src/layout/list/collection.js +6 -1
- package/src/layout/list/gallery.js +2 -1
- package/src/layout/list/horizontal.js +2 -1
- package/src/layout/list/list.js +2 -1
- package/src/layout/list/messages.js +318 -0
- package/src/layout/list/threads.js +206 -0
- package/src/layout/mixin/trackable.js +2 -3
- package/src/layout/mixin/triggerable.js +1 -1
- package/src/layout/radio/feedback.js +3 -2
- package/src/layout/templates.js +62 -0
- package/src/layout/text/text.js +2 -1
- package/src/layout/text/typewriter/index.js +5 -3
- package/src/plugin.js +31 -117
- package/src/util/confirm.js +28 -0
- package/src/util/modal.js +49 -0
- package/src/util/prompt.js +42 -0
- package/src/version.js +1 -1
- package/dist/cjs/actor/data.js +0 -132
- package/dist/cjs/actor/feedback.js +0 -29
- package/dist/cjs/actor/fields.js +0 -57
- package/dist/cjs/actor/filters.js +0 -161
- package/dist/cjs/actor/hub.js +0 -57
- package/dist/cjs/actor/index.js +0 -73
- package/dist/cjs/actor/interactions.js +0 -36
- package/dist/cjs/actor/sessions.js +0 -25
- package/dist/cjs/actor/tracker-states.js +0 -61
- package/dist/cjs/actor/tracker.js +0 -109
- package/dist/cjs/actor/trackers.js +0 -52
- package/dist/cjs/actor/utils.js +0 -15
- package/dist/cjs/actor/view.js +0 -294
- package/dist/cjs/actor/views.js +0 -209
- package/dist/cjs/source.js +0 -27
- package/dist/cjs/util/proxy.js +0 -88
- package/dist/cjs/util/trackers.js +0 -88
- package/dist/cjs/workflow/affiliations.js +0 -140
- package/dist/cjs/workflow/answer-based.js +0 -337
- package/dist/cjs/workflow/ask.js +0 -319
- package/dist/cjs/workflow/asks.js +0 -150
- package/dist/cjs/workflow/autocompletable.js +0 -63
- package/dist/cjs/workflow/autocomplete.js +0 -120
- package/dist/cjs/workflow/base.js +0 -418
- package/dist/cjs/workflow/context.js +0 -19
- package/dist/cjs/workflow/explore.js +0 -210
- package/dist/cjs/workflow/explores.js +0 -21
- package/dist/cjs/workflow/hybrid-search-answer.js +0 -88
- package/dist/cjs/workflow/hybrid-search-views.js +0 -109
- package/dist/cjs/workflow/hybrid-search.js +0 -213
- package/dist/cjs/workflow/index.js +0 -41
- package/dist/cjs/workflow/options/api.js +0 -41
- package/dist/cjs/workflow/options/auto-query.js +0 -97
- package/dist/cjs/workflow/options/configurable.js +0 -45
- package/dist/cjs/workflow/options/data-processor.js +0 -20
- package/dist/cjs/workflow/options/filters.js +0 -15
- package/dist/cjs/workflow/options/index.js +0 -126
- package/dist/cjs/workflow/options/interactions.js +0 -41
- package/dist/cjs/workflow/options/layouts.js +0 -79
- package/dist/cjs/workflow/options/options.js +0 -156
- package/dist/cjs/workflow/options/pagination.js +0 -25
- package/dist/cjs/workflow/options/roles.js +0 -15
- package/dist/cjs/workflow/options/templates.js +0 -30
- package/dist/cjs/workflow/options/trackers.js +0 -83
- package/dist/cjs/workflow/options/utils.js +0 -33
- package/dist/cjs/workflow/processors.js +0 -607
- package/dist/cjs/workflow/recommendation.js +0 -57
- package/dist/cjs/workflow/recommendations.js +0 -18
- package/dist/cjs/workflow/search-based.js +0 -337
- package/dist/cjs/workflow/search.js +0 -64
- package/dist/cjs/workflow/unit.js +0 -57
- package/dist/cjs/workflow/units.js +0 -42
- package/dist/cjs/workflow/use-link.js +0 -49
- package/src/actor/data.js +0 -106
- package/src/actor/feedback.js +0 -28
- package/src/actor/fields.js +0 -51
- package/src/actor/filters.js +0 -149
- package/src/actor/hub.js +0 -47
- package/src/actor/index.js +0 -10
- package/src/actor/interactions.js +0 -32
- package/src/actor/sessions.js +0 -21
- package/src/actor/tracker-states.js +0 -57
- package/src/actor/tracker.js +0 -104
- package/src/actor/trackers.js +0 -45
- package/src/actor/utils.js +0 -7
- package/src/actor/view.js +0 -272
- package/src/actor/views.js +0 -224
- package/src/source.js +0 -17
- package/src/util/proxy.js +0 -81
- package/src/util/trackers.js +0 -84
- package/src/workflow/affiliations.js +0 -121
- package/src/workflow/answer-based.js +0 -312
- package/src/workflow/ask.js +0 -284
- package/src/workflow/asks.js +0 -143
- package/src/workflow/autocompletable.js +0 -67
- package/src/workflow/autocomplete.js +0 -114
- package/src/workflow/base.js +0 -396
- package/src/workflow/context.js +0 -16
- package/src/workflow/explore.js +0 -194
- package/src/workflow/explores.js +0 -18
- package/src/workflow/hybrid-search-answer.js +0 -73
- package/src/workflow/hybrid-search-views.js +0 -116
- package/src/workflow/hybrid-search.js +0 -230
- package/src/workflow/index.js +0 -5
- package/src/workflow/options/api.js +0 -31
- package/src/workflow/options/auto-query.js +0 -70
- package/src/workflow/options/configurable.js +0 -48
- package/src/workflow/options/data-processor.js +0 -15
- package/src/workflow/options/filters.js +0 -10
- package/src/workflow/options/index.js +0 -11
- package/src/workflow/options/interactions.js +0 -36
- package/src/workflow/options/layouts.js +0 -73
- package/src/workflow/options/options.js +0 -175
- package/src/workflow/options/pagination.js +0 -18
- package/src/workflow/options/roles.js +0 -12
- package/src/workflow/options/templates.js +0 -27
- package/src/workflow/options/trackers.js +0 -80
- package/src/workflow/options/utils.js +0 -26
- package/src/workflow/processors.js +0 -490
- package/src/workflow/recommendation.js +0 -63
- package/src/workflow/recommendations.js +0 -14
- package/src/workflow/search-based.js +0 -329
- package/src/workflow/search.js +0 -73
- package/src/workflow/unit.js +0 -51
- package/src/workflow/units.js +0 -43
- package/src/workflow/use-link.js +0 -41
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { defineValues, escapeHtml, findInAncestors, debounce } from '@miso.ai/commons';
|
|
2
|
-
import { STATUS, EVENT_TYPE } from '../../constants.js';
|
|
3
|
-
import { fields } from '
|
|
2
|
+
import { STATUS, EVENT_TYPE, LAYOUT_TYPE } from '../../constants.js';
|
|
3
|
+
import { fields } from '@miso.ai/client-sdk-workflow';
|
|
4
4
|
import TemplateBasedLayout from '../template.js';
|
|
5
5
|
import { imageBlock, productInfoBlock } from '../templates.js';
|
|
6
6
|
import { getIcon } from '../../asset/svgs.js';
|
|
7
7
|
import { addOrRemoveClass } from '../../util/dom.js';
|
|
8
8
|
|
|
9
|
-
const TYPE =
|
|
9
|
+
const TYPE = LAYOUT_TYPE.SEARCH_BOX;
|
|
10
10
|
const DEFAULT_CLASSNAME = 'miso-search-box';
|
|
11
11
|
const DEFAULT_AUTOCOMPLETE_CLASSNAME = 'miso-autocomplete';
|
|
12
12
|
|
|
@@ -150,6 +150,30 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
150
150
|
proxyElement.on('focusout', (e) => this._handleFocusOut(e)),
|
|
151
151
|
hub.on(fields.completions(), () => view.refresh({ force: true })),
|
|
152
152
|
];
|
|
153
|
+
if (this.options.disableWhenOngoing) {
|
|
154
|
+
// disable submission while the main view is still displaying an answer
|
|
155
|
+
const mainRole = view.workflow._roles && view.workflow._roles.main;
|
|
156
|
+
if (mainRole) {
|
|
157
|
+
this._unsubscribes.push(hub.on(fields.view(mainRole), state => this._syncDisabled(!!state.ongoing)));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_syncDisabled(disabled) {
|
|
163
|
+
if (this._disabled === disabled) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this._disabled = disabled;
|
|
167
|
+
this._syncDisabledAttributes();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_syncDisabledAttributes() {
|
|
171
|
+
// the user can keep typing; only submission is disabled
|
|
172
|
+
const element = this._element;
|
|
173
|
+
const button = element && element.querySelector(`[data-role="submit"]`);
|
|
174
|
+
if (button) {
|
|
175
|
+
button.disabled = !!this._disabled;
|
|
176
|
+
}
|
|
153
177
|
}
|
|
154
178
|
|
|
155
179
|
focus() {
|
|
@@ -197,6 +221,7 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
197
221
|
_afterRender(element, state) {
|
|
198
222
|
this._fullfillFocusRequest(element);
|
|
199
223
|
this._fullfillValueRequest(element);
|
|
224
|
+
this._syncDisabledAttributes(); // a re-render replaces the input element
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
_updateCompletions(element, { state, rendered }, { writeToState }) {
|
|
@@ -289,7 +314,9 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
289
314
|
_handleKeyDown({ key, target, isComposing }) {
|
|
290
315
|
if (!isComposing && key === 'Enter' && target.matches('[data-role="input"]')) {
|
|
291
316
|
this._submit(target.value);
|
|
292
|
-
|
|
317
|
+
if (this.options.blurOnSubmit !== false) {
|
|
318
|
+
target.blur();
|
|
319
|
+
}
|
|
293
320
|
}
|
|
294
321
|
// TODO: select completion items with arrow keys
|
|
295
322
|
}
|
|
@@ -300,7 +327,9 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
300
327
|
for (let element = target; element && element !== this._element; element = element.parentElement) {
|
|
301
328
|
if (element.matches('[data-role="submit"]') || element.matches('[data-role="button"]') || element.matches('[type="submit"]')) {
|
|
302
329
|
if (inputElement) {
|
|
303
|
-
|
|
330
|
+
if (this.options.blurOnSubmit !== false) {
|
|
331
|
+
inputElement.blur();
|
|
332
|
+
}
|
|
304
333
|
this._submit(inputElement.value);
|
|
305
334
|
}
|
|
306
335
|
return;
|
|
@@ -329,12 +358,18 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
329
358
|
}
|
|
330
359
|
|
|
331
360
|
async _submit(value) {
|
|
332
|
-
if (!value || !value.trim()) {
|
|
361
|
+
if (this._disabled || !value || !value.trim()) {
|
|
333
362
|
return;
|
|
334
363
|
}
|
|
335
364
|
this.close();
|
|
336
365
|
// TODO: q -> value
|
|
337
366
|
this._view.hub.update(fields.query(), { q: value });
|
|
367
|
+
if (this.options.clearOnSubmit) {
|
|
368
|
+
const inputElement = this._element && this._element.querySelector(`[data-role="input"]`);
|
|
369
|
+
if (inputElement) {
|
|
370
|
+
inputElement.value = '';
|
|
371
|
+
}
|
|
372
|
+
}
|
|
338
373
|
this._trackSubmit(value);
|
|
339
374
|
}
|
|
340
375
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { LAYOUT_TYPE } from '../../constants.js';
|
|
1
2
|
import { escapeHtml, kebabOrSnakeToHuman } from '@miso.ai/commons';
|
|
2
3
|
import TemplateBasedLayout from '../template.js';
|
|
3
4
|
import { makeTrackable } from '../mixin/trackable.js';
|
|
4
5
|
|
|
5
|
-
const TYPE =
|
|
6
|
+
const TYPE = LAYOUT_TYPE.SELECT;
|
|
6
7
|
const DEFAULT_CLASSNAME = 'miso-select';
|
|
7
8
|
|
|
8
9
|
function root(layout, state) {
|
package/src/layout/list/cards.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { LAYOUT_TYPE } from '../../constants.js';
|
|
1
2
|
import CollectionLayout from './collection.js';
|
|
2
3
|
import { makeSwipeable } from '../mixin/swipeable.js';
|
|
3
4
|
import { CHEVRON, CHEVRON_NEGATIVE } from '../../asset/svgs.js';
|
|
4
5
|
|
|
5
|
-
const TYPE =
|
|
6
|
+
const TYPE = LAYOUT_TYPE.CAROUSEL;
|
|
6
7
|
const DEFAULT_CLASSNAME = 'miso-carousel';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_TEMPLATES = Object.freeze({
|
|
@@ -2,7 +2,7 @@ import { STATUS } from '../../constants.js';
|
|
|
2
2
|
import TemplateBasedLayout from '../template.js';
|
|
3
3
|
import { makeTrackable } from '../mixin/trackable.js';
|
|
4
4
|
import { makeTriggerable } from '../mixin/triggerable.js';
|
|
5
|
-
import { product, article, compactArticle, image, question, productInfoBlock, articleInfoBlock, compactArticleInfoBlock, imageInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, authorsBlock, authorsAndDateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock } from '../templates.js';
|
|
5
|
+
import { product, article, compactArticle, image, question, thread, threadMenuBlock, message, messageQuestionBlock, messageAnswerBlock, productInfoBlock, articleInfoBlock, compactArticleInfoBlock, imageInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, authorsBlock, authorsAndDateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock } from '../templates.js';
|
|
6
6
|
|
|
7
7
|
function root(layout, state) {
|
|
8
8
|
const { className, role, templates, options } = layout;
|
|
@@ -66,6 +66,11 @@ const DEFAULT_TEMPLATES = Object.freeze({
|
|
|
66
66
|
compactArticle,
|
|
67
67
|
image,
|
|
68
68
|
question,
|
|
69
|
+
thread,
|
|
70
|
+
threadMenuBlock,
|
|
71
|
+
message,
|
|
72
|
+
messageQuestionBlock,
|
|
73
|
+
messageAnswerBlock,
|
|
69
74
|
root,
|
|
70
75
|
[STATUS.READY]: ready,
|
|
71
76
|
empty: () => ``,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LAYOUT_TYPE } from '../../constants.js';
|
|
1
2
|
import { trimObj } from '@miso.ai/commons';
|
|
2
3
|
import CollectionLayout from './collection.js';
|
|
3
4
|
import { addOrRemoveClass, resolveCssLength, cssAspectRatio } from '../../util/dom.js';
|
|
@@ -34,7 +35,7 @@ const DEFAULT_TEMPLATES = Object.freeze({
|
|
|
34
35
|
item,
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
const TYPE =
|
|
38
|
+
const TYPE = LAYOUT_TYPE.GALLERY;
|
|
38
39
|
const DEFAULT_CLASSNAME = 'miso-gallery';
|
|
39
40
|
|
|
40
41
|
export default class GalleryLayout extends CollectionLayout {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { LAYOUT_TYPE } from '../../constants.js';
|
|
1
2
|
import CollectionLayout from './collection.js';
|
|
2
3
|
|
|
3
|
-
const TYPE =
|
|
4
|
+
const TYPE = LAYOUT_TYPE.HORIZONTAL;
|
|
4
5
|
const DEFAULT_CLASSNAME = 'miso-horizontal';
|
|
5
6
|
|
|
6
7
|
export default class HorizontalLayout extends CollectionLayout {
|
package/src/layout/list/list.js
CHANGED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { Resolution, pacer, requestAnimationFrame as raf } from '@miso.ai/commons';
|
|
2
|
+
import { hasAnswer, getGeneratedBy } from '@miso.ai/client-sdk-workflow';
|
|
3
|
+
import { LAYOUT_TYPE } from '../../constants.js';
|
|
4
|
+
import CollectionLayout from './collection.js';
|
|
5
|
+
import { cursorClassName } from '../text/typewriter/utils.js';
|
|
6
|
+
import { messageAuthor } from '../templates.js';
|
|
7
|
+
import { setOrRemoveAttribute } from '../../util/dom.js';
|
|
8
|
+
|
|
9
|
+
const TYPE = LAYOUT_TYPE.MESSAGES;
|
|
10
|
+
const DEFAULT_CLASSNAME = 'miso-messages';
|
|
11
|
+
const TYPEWRITER_CLASSNAME = 'miso-typewriter';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The conversation panel of the chat history interface: a list of `message`
|
|
15
|
+
* items (question bubble + answer body), rendered incrementally — appended
|
|
16
|
+
* messages (e.g. a posted follow-up question) render as new items, while
|
|
17
|
+
* in-place changes are applied to existing items by a post-render pass
|
|
18
|
+
* (`_syncMessages`):
|
|
19
|
+
*
|
|
20
|
+
* - question texts are filled in when they arrive (the head request carries
|
|
21
|
+
* question ids only)
|
|
22
|
+
* - a finished answer body is transformed from markdown to HTML in one shot
|
|
23
|
+
* through the `std:ui-markdown` plugin
|
|
24
|
+
* - a streaming answer (a follow-up being generated, `finished: false`) is
|
|
25
|
+
* driven by a per-message typewriter: a progressive markdown renderer with
|
|
26
|
+
* a paced cursor, like the typewriter layout of the ask workflow
|
|
27
|
+
*/
|
|
28
|
+
export default class MessagesLayout extends CollectionLayout {
|
|
29
|
+
|
|
30
|
+
static get type() {
|
|
31
|
+
return TYPE;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static get defaultClassName() {
|
|
35
|
+
return DEFAULT_CLASSNAME;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor({ className = DEFAULT_CLASSNAME, ...options } = {}) {
|
|
39
|
+
super({ className, ...options });
|
|
40
|
+
this._renderedAnswers = new WeakMap(); // answer element -> rendered markdown
|
|
41
|
+
this._typewriters = new WeakMap(); // answer element -> MessageTypewriter
|
|
42
|
+
this._readiness = new Resolution();
|
|
43
|
+
this._pinned = true; // whether the view sticks to the bottom on updates
|
|
44
|
+
this._displaying = false; // whether a typewriter is still displaying an answer
|
|
45
|
+
// kick off sooner
|
|
46
|
+
MessagesLayout.MisoClient.plugins.install('std:ui-markdown');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
initialize(view) {
|
|
50
|
+
super.initialize(view);
|
|
51
|
+
this._setup();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// setup //
|
|
55
|
+
async _setup() {
|
|
56
|
+
try {
|
|
57
|
+
const plugin = await MessagesLayout.MisoClient.plugins.install('std:ui-markdown');
|
|
58
|
+
if (!this._view) {
|
|
59
|
+
return; // destroyed
|
|
60
|
+
}
|
|
61
|
+
this._markdown = plugin.getContext(this._view.workflow._client);
|
|
62
|
+
this._readiness.resolve();
|
|
63
|
+
} catch (e) {
|
|
64
|
+
this._readiness.reject(e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async _ready() {
|
|
69
|
+
return this._readiness.promise;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// render //
|
|
73
|
+
async render(element, state, controls = {}) {
|
|
74
|
+
// capture the view state callback, so the typewriter can report the
|
|
75
|
+
// `ongoing` state outside the render cycle
|
|
76
|
+
this._notifyUpdate = controls.notifyUpdate;
|
|
77
|
+
await super.render(element, state, controls);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_afterRender(element, state) {
|
|
81
|
+
super._afterRender(element, state); // syncs bindings to the latest values
|
|
82
|
+
if (!state.incremental) {
|
|
83
|
+
this._displaying = false; // a fresh render (thread load) drops any ongoing typewriter
|
|
84
|
+
}
|
|
85
|
+
if (!state.incremental || state.html) {
|
|
86
|
+
// a fresh render (thread load) or appended items (posted follow-up):
|
|
87
|
+
// jump to the bottom
|
|
88
|
+
this._scrollToBottom({ force: true });
|
|
89
|
+
}
|
|
90
|
+
this._syncMessages(element).catch(error => console.error(error));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async _syncMessages(element) {
|
|
94
|
+
await this._ready();
|
|
95
|
+
const items = this._getItemElements(element);
|
|
96
|
+
let displaying = false;
|
|
97
|
+
for (let i = 0; i < items.length; i++) {
|
|
98
|
+
const item = items[i];
|
|
99
|
+
const binding = this._bindings.get(item);
|
|
100
|
+
if (!binding) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const message = binding.value;
|
|
104
|
+
const last = i === items.length - 1;
|
|
105
|
+
this._syncQuestion(item, message);
|
|
106
|
+
this._syncAnswer(item, message, { last });
|
|
107
|
+
if (last) {
|
|
108
|
+
displaying = this._isDisplaying(item, message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this._setDisplaying(displaying);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// whether the (last) message pair is still being displayed: its answer is
|
|
115
|
+
// pending, streaming, or still being typed — live (posted in this session)
|
|
116
|
+
// or not: an answer still generating is picked up by the answers polling
|
|
117
|
+
// even after switching away from the thread and back, and must keep
|
|
118
|
+
// blocking submission all the same
|
|
119
|
+
_isDisplaying(item, message) {
|
|
120
|
+
if (!hasAnswer(message)) {
|
|
121
|
+
return true; // waiting for the answer body
|
|
122
|
+
}
|
|
123
|
+
const answerElement = item.querySelector('[data-role="answer"]');
|
|
124
|
+
const typewriter = answerElement && this._typewriters.get(answerElement);
|
|
125
|
+
return typewriter ? !typewriter.done : message.finished === false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_syncQuestion(item, message) {
|
|
129
|
+
const questionElement = item.querySelector('[data-role="question"]');
|
|
130
|
+
if (!questionElement) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// the authorship arrives with the answers response, after the stub render
|
|
134
|
+
setOrRemoveAttribute(questionElement, 'data-author', messageAuthor(message));
|
|
135
|
+
setOrRemoveAttribute(questionElement, 'data-generated-by', getGeneratedBy(message));
|
|
136
|
+
const { question } = message;
|
|
137
|
+
if (!question || questionElement.textContent === question) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
questionElement.textContent = question;
|
|
141
|
+
questionElement.hidden = false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_syncAnswer(item, message, { last = false } = {}) {
|
|
145
|
+
const answerElement = item.querySelector('[data-role="answer"]');
|
|
146
|
+
if (!answerElement) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// an ongoing typewriter keeps consuming updates until it finishes typing
|
|
150
|
+
const typewriter = this._typewriters.get(answerElement);
|
|
151
|
+
if (typewriter) {
|
|
152
|
+
typewriter.update(message);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!hasAnswer(message)) {
|
|
156
|
+
return; // still loading; the spinner stays
|
|
157
|
+
}
|
|
158
|
+
if (message.live || message.finished === false) {
|
|
159
|
+
// an answer being generated live in this session: drive it with a
|
|
160
|
+
// typewriter (even if the data arrived complete in one shot)
|
|
161
|
+
const typewriter = new MessageTypewriter(this._markdown, answerElement, {
|
|
162
|
+
onUpdate: () => this._scrollToBottom(), // keep pinned to the bottom while typing
|
|
163
|
+
onDone: () => this._setDisplaying(false),
|
|
164
|
+
});
|
|
165
|
+
this._typewriters.set(answerElement, typewriter);
|
|
166
|
+
typewriter.update(message);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// a finished answer: transform in one shot
|
|
170
|
+
if (this._renderedAnswers.get(answerElement) === message.answer) {
|
|
171
|
+
return; // already rendered
|
|
172
|
+
}
|
|
173
|
+
this._renderedAnswers.set(answerElement, message.answer);
|
|
174
|
+
this._transform(answerElement, message).then(() => {
|
|
175
|
+
last && this._scrollToBottom();
|
|
176
|
+
}).catch(error => {
|
|
177
|
+
this._renderedAnswers.delete(answerElement);
|
|
178
|
+
console.error(error);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async _transform(answerElement, { answer, sources }) {
|
|
183
|
+
answerElement.innerHTML = await this._markdown.transform(answer, sources);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// whether the last answer is still being displayed (typed); reported as
|
|
187
|
+
// the `ongoing` view state, e.g. for the search box to disable submission
|
|
188
|
+
_setDisplaying(displaying) {
|
|
189
|
+
if (this._displaying === displaying) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this._displaying = displaying;
|
|
193
|
+
this._notifyUpdate && this._notifyUpdate({ ongoing: displaying });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// scrolling //
|
|
197
|
+
/**
|
|
198
|
+
* Scroll the containing scrollable to the bottom. Unless forced, only
|
|
199
|
+
* applies while the user is pinned to the bottom — scrolling up to read
|
|
200
|
+
* releases the pin, scrolling back down restores it.
|
|
201
|
+
*/
|
|
202
|
+
_scrollToBottom({ force = false } = {}) {
|
|
203
|
+
const scrollable = this._getScrollable();
|
|
204
|
+
if (!scrollable || !(force || this._pinned)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
scrollable.scrollTop = scrollable.scrollHeight;
|
|
208
|
+
this._pinned = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_getScrollable() {
|
|
212
|
+
const element = this._element;
|
|
213
|
+
if (!element || typeof getComputedStyle === 'undefined') {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
for (let el = element; el && el.nodeType === 1 && el !== document.body; el = el.parentElement) {
|
|
217
|
+
const { overflowY } = getComputedStyle(el);
|
|
218
|
+
if (overflowY === 'auto' || overflowY === 'scroll') {
|
|
219
|
+
this._watchScroll(el);
|
|
220
|
+
return el;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_watchScroll(scrollable) {
|
|
227
|
+
if (this._watchedScrollable === scrollable) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this._unwatchScroll && this._unwatchScroll();
|
|
231
|
+
const onScroll = () => {
|
|
232
|
+
this._pinned = scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight < 40;
|
|
233
|
+
};
|
|
234
|
+
scrollable.addEventListener('scroll', onScroll, { passive: true });
|
|
235
|
+
this._watchedScrollable = scrollable;
|
|
236
|
+
const unwatch = this._unwatchScroll = () => {
|
|
237
|
+
scrollable.removeEventListener('scroll', onScroll);
|
|
238
|
+
this._watchedScrollable = undefined;
|
|
239
|
+
this._unwatchScroll = undefined;
|
|
240
|
+
};
|
|
241
|
+
this._unsubscribes.push(() => this._unwatchScroll === unwatch && unwatch());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// TODO: can't we just use Controller?
|
|
247
|
+
/**
|
|
248
|
+
* Types a streaming answer into an element: a progressive markdown renderer
|
|
249
|
+
* fed by data updates, advanced by a paced cursor on animation frames — the
|
|
250
|
+
* typewriting effect, without the full typewriter layout machinery.
|
|
251
|
+
*/
|
|
252
|
+
class MessageTypewriter {
|
|
253
|
+
|
|
254
|
+
constructor(markdown, element, { onUpdate, onDone } = {}) {
|
|
255
|
+
this._element = element;
|
|
256
|
+
this._onUpdate = onUpdate;
|
|
257
|
+
this._onDone = onDone;
|
|
258
|
+
// the element acts as a typewriter container: it starts as the caret ref
|
|
259
|
+
// (the cursor class moves into the content as it types), and the preset
|
|
260
|
+
// stamps the `done` class on finish, which hides the caret via CSS
|
|
261
|
+
element.classList.add(TYPEWRITER_CLASSNAME, cursorClassName(TYPEWRITER_CLASSNAME));
|
|
262
|
+
this._renderer = markdown.createRenderer({
|
|
263
|
+
cursorClass: cursorClassName(TYPEWRITER_CLASSNAME),
|
|
264
|
+
getSource: index => (this._sources || [])[index],
|
|
265
|
+
});
|
|
266
|
+
this._getNextCursor = pacer();
|
|
267
|
+
this._rendered = this._renderer.clear(element);
|
|
268
|
+
this._timestamp = undefined;
|
|
269
|
+
this._value = '';
|
|
270
|
+
this._sources = undefined;
|
|
271
|
+
this._dataDone = false;
|
|
272
|
+
this._doneAt = undefined;
|
|
273
|
+
this._requested = false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
get done() {
|
|
277
|
+
return this._rendered.done;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
update({ answer = '', finished, sources }) {
|
|
281
|
+
this._value = answer;
|
|
282
|
+
this._sources = sources;
|
|
283
|
+
this._dataDone = finished !== false;
|
|
284
|
+
this._requestFrame();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_requestFrame() {
|
|
288
|
+
if (this._requested || this._rendered.done) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this._requested = true;
|
|
292
|
+
raf(timestamp => this._frame(timestamp));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
_frame(timestamp) {
|
|
296
|
+
this._requested = false;
|
|
297
|
+
if (!this._element.isConnected) {
|
|
298
|
+
// the element is gone (re-render, thread switch); stop typing
|
|
299
|
+
this._onDone && this._onDone();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const prev = this._rendered;
|
|
303
|
+
const prevTimestamp = this._timestamp !== undefined ? this._timestamp : timestamp;
|
|
304
|
+
if (this._dataDone && this._doneAt === undefined) {
|
|
305
|
+
this._doneAt = timestamp;
|
|
306
|
+
}
|
|
307
|
+
const cursor = this._getNextCursor(prev.cursor, this._doneAt, prevTimestamp, timestamp);
|
|
308
|
+
this._rendered = this._renderer.update(this._element, prev, { value: this._value, cursor, timestamp, done: this._dataDone });
|
|
309
|
+
this._timestamp = timestamp;
|
|
310
|
+
this._onUpdate && this._onUpdate();
|
|
311
|
+
if (!this._rendered.done) {
|
|
312
|
+
this._requestFrame();
|
|
313
|
+
} else {
|
|
314
|
+
this._onDone && this._onDone();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
}
|