@miso.ai/client-sdk-ui 1.13.2-beta.1 → 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 +17 -4
- package/dist/cjs/combo/ask/index.js +2 -2
- package/dist/cjs/constants.js +120 -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 +259 -0
- package/dist/cjs/element/container/index.js +28 -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-message-item.js +91 -0
- package/dist/cjs/element/container/miso-recommendation.js +1 -1
- package/dist/cjs/element/container/miso-search.js +1 -1
- package/dist/cjs/element/container/miso-thread-item.js +91 -0
- package/dist/cjs/element/context/miso-follow-ups.js +1 -1
- package/dist/cjs/element/role/index.js +49 -0
- package/dist/cjs/element/role/miso-delete.js +20 -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 +170 -0
- package/dist/cjs/layout/button/checkbox.js +116 -0
- package/dist/cjs/layout/button/more-button.js +3 -3
- package/dist/cjs/layout/container/container.js +1 -1
- package/dist/cjs/layout/container/item-container.js +99 -0
- package/dist/cjs/layout/error/error.js +2 -1
- package/dist/cjs/layout/index.js +42 -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 +50 -12
- 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 +13 -1
- 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 +359 -0
- package/dist/cjs/layout/list/threads.js +199 -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 +97 -0
- package/dist/cjs/layout/text/question.js +71 -0
- package/dist/cjs/layout/text/text.js +2 -1
- package/dist/cjs/layout/text/typewriter/index.js +18 -23
- package/dist/cjs/plugin.js +43 -104
- package/dist/cjs/util/confirm.js +33 -0
- package/dist/cjs/util/dom.js +21 -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 +51 -104
- package/src/asset/svgs.js +16 -3
- package/src/combo/ask/index.js +2 -2
- package/src/constants.js +39 -147
- package/src/defaults/ask/controls.js +1 -1
- package/src/defaults/hybrid-search/answer-box.js +2 -2
- package/src/defaults.js +174 -0
- package/src/element/container/index.js +4 -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-message-item.js +94 -0
- package/src/element/container/miso-recommendation.js +1 -1
- package/src/element/container/miso-search.js +1 -1
- package/src/element/container/miso-thread-item.js +94 -0
- package/src/element/context/miso-follow-ups.js +1 -1
- package/src/element/role/index.js +7 -0
- package/src/element/role/miso-delete.js +16 -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 +152 -0
- package/src/layout/button/checkbox.js +92 -0
- package/src/layout/button/more-button.js +3 -3
- package/src/layout/container/container.js +2 -2
- package/src/layout/container/item-container.js +99 -0
- package/src/layout/error/error.js +2 -1
- package/src/layout/index.js +6 -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 +45 -8
- 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 +15 -2
- 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 +316 -0
- package/src/layout/list/threads.js +166 -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 +97 -0
- package/src/layout/text/question.js +58 -0
- package/src/layout/text/text.js +2 -1
- package/src/layout/text/typewriter/index.js +15 -18
- package/src/plugin.js +31 -117
- package/src/util/confirm.js +28 -0
- package/src/util/dom.js +16 -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,9 +1,10 @@
|
|
|
1
|
+
import { LAYOUT_TYPE } from '../../constants.js';
|
|
1
2
|
import { escapeHtml } from '@miso.ai/commons';
|
|
2
|
-
import { fields } from '
|
|
3
|
+
import { fields } from '@miso.ai/client-sdk-workflow';
|
|
3
4
|
import TemplateBasedLayout from '../template.js';
|
|
4
5
|
import Bindings from '../../util/bindings.js';
|
|
5
6
|
|
|
6
|
-
const TYPE =
|
|
7
|
+
const TYPE = LAYOUT_TYPE.FACETS;
|
|
7
8
|
const DEFAULT_CLASSNAME = 'miso-facets';
|
|
8
9
|
const DEDAULT_FACET_CLASSNAME = 'miso-facet';
|
|
9
10
|
const CUSTOM_ATTRIBUTES_PREFIX = 'custom_attributes.';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { LAYOUT_TYPE } from '../../constants.js';
|
|
1
2
|
import { escapeHtml } from '@miso.ai/commons';
|
|
2
|
-
import { fields } from '
|
|
3
|
+
import { fields } from '@miso.ai/client-sdk-workflow';
|
|
3
4
|
import TemplateBasedLayout from '../template.js';
|
|
4
5
|
import { makeTrackable } from '../mixin/trackable.js';
|
|
5
6
|
import { addOrRemoveClass } from '../../util/dom.js';
|
|
6
7
|
|
|
7
|
-
const TYPE =
|
|
8
|
+
const TYPE = LAYOUT_TYPE.OPTION_LIST;
|
|
8
9
|
const DEFAULT_CLASSNAME = 'miso-option-list';
|
|
9
10
|
|
|
10
11
|
function root(layout, state) {
|
|
@@ -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,20 @@ 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
|
-
//
|
|
337
|
-
|
|
365
|
+
// a view event, like the buttons: the workflow routes it into its query
|
|
366
|
+
// flow (or its own submission semantics), the layout stays hands-off
|
|
367
|
+
const { session } = this._view._state || {};
|
|
368
|
+
this._view._emit('submit', Object.freeze({ session, value }));
|
|
369
|
+
if (this.options.clearOnSubmit) {
|
|
370
|
+
const inputElement = this._element && this._element.querySelector(`[data-role="input"]`);
|
|
371
|
+
if (inputElement) {
|
|
372
|
+
inputElement.value = '';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
338
375
|
this._trackSubmit(value);
|
|
339
376
|
}
|
|
340
377
|
|
|
@@ -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, threadBody, threadMenuBlock, message, messageBody, 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
|
+
threadBody,
|
|
71
|
+
threadMenuBlock,
|
|
72
|
+
message,
|
|
73
|
+
messageBody,
|
|
69
74
|
root,
|
|
70
75
|
[STATUS.READY]: ready,
|
|
71
76
|
empty: () => ``,
|
|
@@ -176,7 +181,15 @@ export default class CollectionLayout extends TemplateBasedLayout {
|
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
_getItemElements(element) {
|
|
179
|
-
|
|
184
|
+
if (!element) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
// an item may host a nested collection with items of its own (a
|
|
188
|
+
// conversation message and its source cards): ours are only those whose
|
|
189
|
+
// closest list is this layout's own list
|
|
190
|
+
const listElement = this._getListElement(element);
|
|
191
|
+
return Array.from((listElement || element).querySelectorAll(`[data-role="item"]`))
|
|
192
|
+
.filter(item => item.closest(`[data-role="list"]`) === listElement);
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
_onClick(event) {
|
|
@@ -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,316 @@
|
|
|
1
|
+
import { STATUS, LAYOUT_TYPE } from '../../constants.js';
|
|
2
|
+
import CollectionLayout from './collection.js';
|
|
3
|
+
|
|
4
|
+
const TYPE = LAYOUT_TYPE.MESSAGES;
|
|
5
|
+
const DEFAULT_CLASSNAME = 'miso-messages';
|
|
6
|
+
|
|
7
|
+
// the infinite scroll trigger sits ABOVE the list — older messages load as
|
|
8
|
+
// the user scrolls up, unlike the stock collection root's bottom trigger
|
|
9
|
+
function root(layout, state) {
|
|
10
|
+
const { className, role, templates, options } = layout;
|
|
11
|
+
const { status } = state;
|
|
12
|
+
const roleAttr = role ? ` data-role="${role}"` : '';
|
|
13
|
+
const itemTypeAttr = options.itemType ? ` data-item-type="${options.itemType}"` : '';
|
|
14
|
+
return `<div class="${className} ${status}"${roleAttr}${itemTypeAttr}>${templates.trigger(layout, state)}${status === STATUS.READY ? templates[status](layout, state) : ''}${templates.loading(layout, state)}</div>`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The conversation panel of the chat history interface: a shell that renders
|
|
19
|
+
* one <miso-message-item> container element per message item, incrementally —
|
|
20
|
+
* fresh items land at either end of the list: appended messages (a posted
|
|
21
|
+
* follow-up question) render as new items at the bottom, while an older
|
|
22
|
+
* page fetched by the more flow (infinite scroll: the trigger, placed above
|
|
23
|
+
* the list, fires the `more` hub field as the user scrolls up) renders as
|
|
24
|
+
* prepended items — told apart by which boundary the already-rendered
|
|
25
|
+
* records line up with.
|
|
26
|
+
* The content of a message is not rendered here: each <miso-message-item> is
|
|
27
|
+
* assigned its item subworkflow (workflow._getMessageWorkflow, off the item
|
|
28
|
+
* binding) in the post-render sync pass, and the role elements inside
|
|
29
|
+
* (question, answer, ...) render through that workflow's own layouts.
|
|
30
|
+
*
|
|
31
|
+
* All message-level presentation is the message workflow's own: a record
|
|
32
|
+
* without its answer body presents as status `loading`, stamped on the
|
|
33
|
+
* <miso-message-item> element by its container layout — there is deliberately no
|
|
34
|
+
* per-message loading icon; the answer typewriter's blinking caret is the
|
|
35
|
+
* loading indication — and the question bubble (text, authorship
|
|
36
|
+
* attributes) renders through the `question` layout.
|
|
37
|
+
*
|
|
38
|
+
* The shell keeps the panel-level behaviors: scroll pinning (the view sticks
|
|
39
|
+
* to the bottom while the user has not scrolled up, following content growth
|
|
40
|
+
* as answers type out), scroll anchoring while scrolled up (prepended items
|
|
41
|
+
* — and their contents, rendering in later through their own workflows —
|
|
42
|
+
* must not shift the reading position), and the `ongoing` view state read
|
|
43
|
+
* off the data — the last message's answer absent or unfinished — which
|
|
44
|
+
* e.g. disables the search box while an answer is on its way.
|
|
45
|
+
*/
|
|
46
|
+
export default class MessagesLayout extends CollectionLayout {
|
|
47
|
+
|
|
48
|
+
static get type() {
|
|
49
|
+
return TYPE;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static get defaultClassName() {
|
|
53
|
+
return DEFAULT_CLASSNAME;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
|
|
57
|
+
super({ className, templates: { root, ...templates }, ...options });
|
|
58
|
+
this._pinned = true; // whether the view sticks to the bottom on updates
|
|
59
|
+
this._anchor = undefined; // the scroll anchor while prepended content settles
|
|
60
|
+
this._ongoing = undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// item identity: the question id, or the local placeholder id standing in
|
|
64
|
+
// for it while a posted question awaits its response — the placeholder id
|
|
65
|
+
// survives the settle, so the element and its workflow do too
|
|
66
|
+
_getItemKey(message) {
|
|
67
|
+
return message.placeholder_id || message.question_id || message;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// render //
|
|
71
|
+
async render(element, state, controls = {}) {
|
|
72
|
+
// capture the view state callback for the ongoing report
|
|
73
|
+
this._notifyUpdate = controls.notifyUpdate;
|
|
74
|
+
await super.render(element, state, controls);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// incremental renders cover a pure append (a posted follow-up at the
|
|
78
|
+
// bottom) or a pure prepend (an older page above) — anything else
|
|
79
|
+
// re-renders in full
|
|
80
|
+
_shallRenderIncrementally(state, rendered) {
|
|
81
|
+
if (!super._shallRenderIncrementally(state, rendered)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const values = this._getItems(state) || [];
|
|
85
|
+
const renderedValues = rendered.value;
|
|
86
|
+
const count = values.length - renderedValues.length;
|
|
87
|
+
return count === 0 || // in-place changes only
|
|
88
|
+
this._getItemKey(values[0]) === this._getItemKey(renderedValues[0]) || // appended
|
|
89
|
+
this._getItemKey(values[count]) === this._getItemKey(renderedValues[0]); // prepended
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// the fresh items land at the tail (a posted follow-up) or at the head
|
|
93
|
+
// (an older page fetched by the more flow): told apart by which boundary
|
|
94
|
+
// the already-rendered records line up with
|
|
95
|
+
_html(state, rendered, incremental) {
|
|
96
|
+
if (!incremental) {
|
|
97
|
+
return this.templates.root(this, state);
|
|
98
|
+
}
|
|
99
|
+
const values = this._getItems(state) || [];
|
|
100
|
+
const renderedValues = rendered.value;
|
|
101
|
+
const count = values.length - renderedValues.length;
|
|
102
|
+
if (count === 0) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
if (this._getItemKey(values[0]) === this._getItemKey(renderedValues[0])) {
|
|
106
|
+
const fresh = values.slice(renderedValues.length);
|
|
107
|
+
return {
|
|
108
|
+
position: 'beforeend',
|
|
109
|
+
items: this.templates.items(this, state, fresh, { offset: renderedValues.length }),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const fresh = values.slice(0, count);
|
|
113
|
+
return {
|
|
114
|
+
position: 'afterbegin',
|
|
115
|
+
items: this.templates.items(this, state, fresh, { offset: 0 }),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_render(element, { state }, { notifyUpdate }) {
|
|
120
|
+
const { incremental, html } = state;
|
|
121
|
+
if (incremental) {
|
|
122
|
+
if (html) {
|
|
123
|
+
const listElement = this._getListElement(element);
|
|
124
|
+
if (html.position === 'afterbegin') {
|
|
125
|
+
this._prependAnchored(listElement, html.items);
|
|
126
|
+
} else {
|
|
127
|
+
listElement.insertAdjacentHTML('beforeend', html.items);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
notifyUpdate(false);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
element.innerHTML = html;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_afterRender(element, state) {
|
|
138
|
+
super._afterRender(element, state); // syncs bindings and (re)arms the trigger
|
|
139
|
+
if (!state.incremental) {
|
|
140
|
+
// a fresh render (thread load): jump to the bottom
|
|
141
|
+
this._dropAnchor();
|
|
142
|
+
this._scrollToBottom({ force: true });
|
|
143
|
+
} else if (state.html && state.html.position === 'beforeend') {
|
|
144
|
+
// appended items (posted follow-up): jump to the bottom
|
|
145
|
+
this._scrollToBottom({ force: true });
|
|
146
|
+
}
|
|
147
|
+
this._watchGrowth(this._getListElement(element) || element);
|
|
148
|
+
this._syncWorkflows(element);
|
|
149
|
+
this._syncOngoing(state);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_unrender() {
|
|
153
|
+
super._unrender();
|
|
154
|
+
this._dropAnchor();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// assign each <miso-message-item> its item subworkflow, off the item binding
|
|
158
|
+
_syncWorkflows(element) {
|
|
159
|
+
const workflow = this._view && this._view.workflow;
|
|
160
|
+
if (!workflow || typeof workflow._getMessageWorkflow !== 'function') {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const item of this._getItemElements(element)) {
|
|
164
|
+
const binding = this._bindings.get(item);
|
|
165
|
+
if (binding && item.isContainer) {
|
|
166
|
+
item.workflow = workflow._getMessageWorkflow(binding.value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// whether an answer is on its way — pending, or still being generated —
|
|
172
|
+
// reported as the `ongoing` view state, e.g. for the search box to disable
|
|
173
|
+
// submission
|
|
174
|
+
_syncOngoing(state) {
|
|
175
|
+
const messages = state.value || [];
|
|
176
|
+
const last = messages[messages.length - 1];
|
|
177
|
+
// a message settled by a failed content fetch is not on its way anymore
|
|
178
|
+
const ongoing = !!last && !last.error && (last.answer === undefined || last.finished === false);
|
|
179
|
+
if (this._ongoing === ongoing) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this._ongoing = ongoing;
|
|
183
|
+
this._notifyUpdate && this._notifyUpdate({ ongoing });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// scrolling //
|
|
187
|
+
/**
|
|
188
|
+
* Scroll the containing scrollable to the bottom. Unless forced, only
|
|
189
|
+
* applies while the user is pinned to the bottom — scrolling up to read
|
|
190
|
+
* releases the pin, scrolling back down restores it.
|
|
191
|
+
*/
|
|
192
|
+
_scrollToBottom({ force = false } = {}) {
|
|
193
|
+
const scrollable = this._getScrollable();
|
|
194
|
+
if (!scrollable || !(force || this._pinned)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
scrollable.scrollTop = scrollable.scrollHeight;
|
|
198
|
+
this._pinned = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_getScrollable() {
|
|
202
|
+
const element = this._element;
|
|
203
|
+
if (!element || typeof getComputedStyle === 'undefined') {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
for (let el = element; el && el.nodeType === 1 && el !== document.body; el = el.parentElement) {
|
|
207
|
+
const { overflowY } = getComputedStyle(el);
|
|
208
|
+
if (overflowY === 'auto' || overflowY === 'scroll') {
|
|
209
|
+
this._watchScroll(el);
|
|
210
|
+
return el;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_watchScroll(scrollable) {
|
|
217
|
+
if (this._watchedScrollable === scrollable) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this._unwatchScroll && this._unwatchScroll();
|
|
221
|
+
const onScroll = () => {
|
|
222
|
+
this._pinned = scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight < 40;
|
|
223
|
+
if (this._pinned) {
|
|
224
|
+
// back at the bottom: the reading position needs no anchor
|
|
225
|
+
this._anchor = undefined;
|
|
226
|
+
} else if (this._anchor && this._anchor.element.isConnected) {
|
|
227
|
+
// follow the user: the anchor holds the position they scrolled to,
|
|
228
|
+
// not the one where the prepend happened
|
|
229
|
+
this._anchor.top = this._relativeTop(this._anchor.element, scrollable);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
scrollable.addEventListener('scroll', onScroll, { passive: true });
|
|
233
|
+
this._watchedScrollable = scrollable;
|
|
234
|
+
const unwatch = this._unwatchScroll = () => {
|
|
235
|
+
scrollable.removeEventListener('scroll', onScroll);
|
|
236
|
+
this._watchedScrollable = undefined;
|
|
237
|
+
this._unwatchScroll = undefined;
|
|
238
|
+
};
|
|
239
|
+
this._unsubscribes.push(() => this._unwatchScroll === unwatch && unwatch());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// the message contents render (and type out) through their own workflows,
|
|
243
|
+
// outside this layout's render cycle: follow the content growth — pinned,
|
|
244
|
+
// the view sticks to the bottom; scrolled up with an anchor active (a
|
|
245
|
+
// prepended page still rendering its contents in), the anchor is held
|
|
246
|
+
// stationary instead
|
|
247
|
+
_watchGrowth(element) {
|
|
248
|
+
if (!element || this._watchedGrowth === element || typeof ResizeObserver === 'undefined') {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this._unwatchGrowth && this._unwatchGrowth();
|
|
252
|
+
const observer = new ResizeObserver(() => this._onContentGrowth());
|
|
253
|
+
observer.observe(element);
|
|
254
|
+
this._watchedGrowth = element;
|
|
255
|
+
const unwatch = this._unwatchGrowth = () => {
|
|
256
|
+
observer.disconnect();
|
|
257
|
+
this._watchedGrowth = undefined;
|
|
258
|
+
this._unwatchGrowth = undefined;
|
|
259
|
+
};
|
|
260
|
+
this._unsubscribes.push(() => this._unwatchGrowth === unwatch && unwatch());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
_onContentGrowth() {
|
|
264
|
+
if (this._pinned) {
|
|
265
|
+
this._scrollToBottom();
|
|
266
|
+
} else {
|
|
267
|
+
this._restoreAnchor();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// scroll anchoring for prepended content //
|
|
272
|
+
// The anchor is the previously-first item: everything prepended — and
|
|
273
|
+
// its content, rendering in later through the item workflows — sits
|
|
274
|
+
// above it, so holding the anchor stationary holds the reading position,
|
|
275
|
+
// while growth below it (a streaming answer at the bottom) moves nothing
|
|
276
|
+
// above and needs no compensation. Not the browser's own scroll
|
|
277
|
+
// anchoring, which not every engine provides.
|
|
278
|
+
_prependAnchored(listElement, items) {
|
|
279
|
+
this._setAnchor();
|
|
280
|
+
listElement.insertAdjacentHTML('afterbegin', items);
|
|
281
|
+
this._restoreAnchor();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_setAnchor() {
|
|
285
|
+
const scrollable = this._getScrollable();
|
|
286
|
+
const element = this._getItemElements(this._element)[0];
|
|
287
|
+
this._anchor = scrollable && element
|
|
288
|
+
? { element, scrollable, top: this._relativeTop(element, scrollable) }
|
|
289
|
+
: undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_restoreAnchor() {
|
|
293
|
+
const anchor = this._anchor;
|
|
294
|
+
if (!anchor) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const { element, scrollable, top } = anchor;
|
|
298
|
+
if (!element.isConnected) {
|
|
299
|
+
this._anchor = undefined;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const delta = this._relativeTop(element, scrollable) - top;
|
|
303
|
+
if (delta) {
|
|
304
|
+
scrollable.scrollTop += delta;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_relativeTop(element, scrollable) {
|
|
309
|
+
return element.getBoundingClientRect().top - scrollable.getBoundingClientRect().top;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_dropAnchor() {
|
|
313
|
+
this._anchor = undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
}
|