@wix/himalaya-cli 0.1250.0 → 0.1252.0
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/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/wix-member-login.d.ts +6 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/serve-runtime/src/wix-bridge.d.ts +144 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/serve-storage/src/sha256.d.ts +13 -0
- package/dist/cli.mjs +554 -284
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +1 -1
- package/package.json +2 -2
- package/templates/beauty-salon-owner/dev/index.ts +668 -87
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/community-club-owner/dev/index.ts +668 -87
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/events-meetups-owner/dev/index.ts +668 -87
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/gym-fitness-owner/dev/index.ts +668 -87
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/nonprofit-cause-owner/dev/index.ts +668 -87
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/owner-lite/dev/index.ts +754 -173
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/podcast-creator-owner/dev/index.ts +668 -87
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/portfolio-creative-owner/dev/index.ts +668 -87
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/restaurant-owner/dev/index.ts +668 -87
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/retail-store-owner/dev/index.ts +668 -87
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
- package/templates/yoga-studio-owner/dev/index.ts +668 -87
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +271 -45
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// every collapsible element carries its own `*Hidden` boolean — the same shape
|
|
13
13
|
// the transcript bubbles already use for their three link slots.
|
|
14
14
|
|
|
15
|
-
import type { NormalizedWidget, WidgetKind } from "./aria-widget-model.js";
|
|
16
|
-
import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
15
|
+
import type { NormalizedWidget, WidgetKind, WidgetOption } from "./aria-widget-model.js";
|
|
16
|
+
import { SELECTED_TOKEN, VALUES_TOKEN, isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
17
17
|
import { composeGenericCard } from "./aria-generic-widget.js";
|
|
18
18
|
import { normalizeWidget } from "./aria-widget-kinds.js";
|
|
19
19
|
import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
|
|
@@ -22,6 +22,7 @@ import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
|
|
|
22
22
|
* rather than imported from `aria-chat.ts` so this module has no dependency on
|
|
23
23
|
* it at all — see `Disclose` below for why that matters. */
|
|
24
24
|
export interface WidgetSourceMessage {
|
|
25
|
+
id?: string;
|
|
25
26
|
role?: string;
|
|
26
27
|
content?: unknown;
|
|
27
28
|
}
|
|
@@ -60,6 +61,12 @@ export interface WidgetRow {
|
|
|
60
61
|
/** `true` when `subtitle` is empty, so the row collapses its second line
|
|
61
62
|
* instead of leaving a gap. `$item.` cannot express "hide when empty". */
|
|
62
63
|
subtitleHidden: boolean;
|
|
64
|
+
/** The section's own `textButton` — VISUAL_DISPLAY's "Edit" beside a row.
|
|
65
|
+
* It sends `respondText`, so a button with no text to send is hidden: a
|
|
66
|
+
* label alone is a control that does nothing. */
|
|
67
|
+
buttonLabel: string;
|
|
68
|
+
buttonHidden: boolean;
|
|
69
|
+
respondText: string;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
/** One selectable option, flattened for binding.
|
|
@@ -89,6 +96,15 @@ export interface WidgetOptionRow {
|
|
|
89
96
|
/** This option's own button `respondText`, before substitution. Carried on
|
|
90
97
|
* the row because the row is what a tap identifies. */
|
|
91
98
|
respondText: string;
|
|
99
|
+
/** The option button's own destination (`mobileAction.link` /
|
|
100
|
+
* `externalLink`), web schemes only. Anything else is dropped HERE, so the
|
|
101
|
+
* button is never painted for a navigation the opener would refuse. */
|
|
102
|
+
actionUrl: string;
|
|
103
|
+
/** `mobileAction.screen.id` — a screen of the native Wix app. This app has
|
|
104
|
+
* no such screen, so a tap says so (`notSupportedText`) instead of doing
|
|
105
|
+
* nothing. */
|
|
106
|
+
actionScreenId: string;
|
|
107
|
+
notSupportedText: string;
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
/** One slice of a CHART. `value` is a RAW magnitude, never a percentage: the
|
|
@@ -111,6 +127,9 @@ export interface WidgetStep {
|
|
|
111
127
|
/** The active widget, as the screen reads it. */
|
|
112
128
|
export interface WidgetView {
|
|
113
129
|
kind: WidgetCase;
|
|
130
|
+
/** Transcript row that owns this widget; prevents state carrying into a later
|
|
131
|
+
* identical-looking ask. */
|
|
132
|
+
sourceMessageId?: string;
|
|
114
133
|
/** `true` when there is nothing to show. The whole region is hidden on this
|
|
115
134
|
* rather than on `kind === "NONE"`, because a descriptor cannot compare —
|
|
116
135
|
* `hiddenWhen` binds a boolean and nothing else. It is what keeps the region
|
|
@@ -166,6 +185,19 @@ export interface WidgetView {
|
|
|
166
185
|
primaryEnabled: boolean;
|
|
167
186
|
/** A send started from a widget button is in flight. */
|
|
168
187
|
submitting: boolean;
|
|
188
|
+
/** A line explaining a tap that could not go anywhere — an option whose
|
|
189
|
+
* destination is a screen this app does not have. Empty otherwise. */
|
|
190
|
+
notice: string;
|
|
191
|
+
noticeHidden: boolean;
|
|
192
|
+
/** MULTI_SELECT's `optionLayout: "thumbnail"`: the options as a grid of
|
|
193
|
+
* picture tiles rather than a list of rows. Two flags, one per form, because
|
|
194
|
+
* a descriptor cannot compare the layout string. */
|
|
195
|
+
optionsGridHidden: boolean;
|
|
196
|
+
optionsListHidden: boolean;
|
|
197
|
+
/** MULTI_SELECT's `footerButtonsAsColumn`: the footer stacked rather than in
|
|
198
|
+
* a row. Same two-flag shape. */
|
|
199
|
+
footerRowHidden: boolean;
|
|
200
|
+
footerColumnHidden: boolean;
|
|
169
201
|
|
|
170
202
|
// --- CHART ---------------------------------------------------------------
|
|
171
203
|
/** `$service:`-bound, and it has to be: `DonutChart.slices_path` and
|
|
@@ -241,8 +273,31 @@ export interface WidgetView {
|
|
|
241
273
|
selectionType: string;
|
|
242
274
|
|
|
243
275
|
// --- QUESTIONNAIRE -------------------------------------------------------
|
|
276
|
+
/** Every question, with its answer so far. NOT bound by the descriptor: a
|
|
277
|
+
* question's choices are a list inside a list, and `$item.` cannot carry an
|
|
278
|
+
* array. So the screen shows ONE question at a time — `questionHead`, the
|
|
279
|
+
* current question's choices in `options`, and `questionFree`. */
|
|
244
280
|
questions: WidgetQuestion[];
|
|
245
281
|
questionsEmpty: boolean;
|
|
282
|
+
/** Which question is on screen. */
|
|
283
|
+
questionIndex: number;
|
|
284
|
+
/** The current question as a ONE-row list (empty when there is none), so its
|
|
285
|
+
* labels are `$item.`-bound like every other row on this screen. */
|
|
286
|
+
questionHead: WidgetQuestionHead[];
|
|
287
|
+
/** The current question's free-text field as a one-row list, or empty when
|
|
288
|
+
* the question offers none. */
|
|
289
|
+
questionFree: WidgetQuestionFree[];
|
|
290
|
+
/** Which control the current question's choices use. Two flags, because a
|
|
291
|
+
* descriptor cannot compare `type`. */
|
|
292
|
+
questionRadioHidden: boolean;
|
|
293
|
+
questionCheckHidden: boolean;
|
|
294
|
+
/** Back / Next. Next is hidden on the last question, where the primary
|
|
295
|
+
* (submit) takes its place, and held — the greyed twin — on a required
|
|
296
|
+
* question with no answer yet. */
|
|
297
|
+
questionBackHidden: boolean;
|
|
298
|
+
questionNextHidden: boolean;
|
|
299
|
+
questionNextDisabled: boolean;
|
|
300
|
+
questionNextEnabled: boolean;
|
|
246
301
|
/** `true` while any `required` question is unanswered. The submit is gated
|
|
247
302
|
* on it — there is no cross-field validation in `Validation`, so "every
|
|
248
303
|
* required question has an answer" has to be worked out here. */
|
|
@@ -272,8 +327,7 @@ export interface WidgetView {
|
|
|
272
327
|
treeJson: string;
|
|
273
328
|
}
|
|
274
329
|
|
|
275
|
-
/** One question of a QUESTIONNAIRE,
|
|
276
|
-
* field is present and scalar. */
|
|
330
|
+
/** One question of a QUESTIONNAIRE, and the owner's answer to it so far. */
|
|
277
331
|
export interface WidgetQuestion {
|
|
278
332
|
id: string;
|
|
279
333
|
title: string;
|
|
@@ -290,8 +344,166 @@ export interface WidgetQuestion {
|
|
|
290
344
|
/** The free-text escape hatch, per question. */
|
|
291
345
|
freePlaceholder: string;
|
|
292
346
|
freeHidden: boolean;
|
|
347
|
+
/** This question's choices, `selected` as the owner left them. */
|
|
348
|
+
choices: WidgetOptionRow[];
|
|
349
|
+
/** What the owner typed into the free-text field. */
|
|
350
|
+
freeText: string;
|
|
293
351
|
}
|
|
294
352
|
|
|
353
|
+
/** The on-screen question's labels. All-scalar: `$item.`-bound. */
|
|
354
|
+
export interface WidgetQuestionHead {
|
|
355
|
+
id: string;
|
|
356
|
+
/** The title, with a `*` when an answer is required. */
|
|
357
|
+
title: string;
|
|
358
|
+
subtitle: string;
|
|
359
|
+
subtitleHidden: boolean;
|
|
360
|
+
/** "2 of 3". Empty — and hidden — for a one-question form. */
|
|
361
|
+
progress: string;
|
|
362
|
+
progressHidden: boolean;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The on-screen question's free-text field. All-scalar: `$item.`-bound. */
|
|
366
|
+
export interface WidgetQuestionFree {
|
|
367
|
+
/** The question id — what `widgetAnswer` is told the text belongs to. */
|
|
368
|
+
id: string;
|
|
369
|
+
placeholder: string;
|
|
370
|
+
value: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The questionnaire's words. Passed in, like `Disclose`, so this module stays
|
|
374
|
+
* free of any copy source and the host can localize them. */
|
|
375
|
+
export interface QuestionnaireCopy {
|
|
376
|
+
progress(current: number, total: number): string;
|
|
377
|
+
/** Shown while a required answer is missing, once the owner has started. */
|
|
378
|
+
incomplete: string;
|
|
379
|
+
/** The submit's label when the payload's primary button has none. */
|
|
380
|
+
submit: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export const DEFAULT_QUESTIONNAIRE_COPY: QuestionnaireCopy = {
|
|
384
|
+
progress: (current, total) => `${current} of ${total}`,
|
|
385
|
+
incomplete: "Answer the required questions to continue.",
|
|
386
|
+
submit: "Submit",
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/** A question's answer as the owner would say it: the chosen choices' titles,
|
|
390
|
+
* then anything typed. Empty when unanswered. */
|
|
391
|
+
function answerOf(q: WidgetQuestion): string {
|
|
392
|
+
const parts = q.choices.filter((c) => c.selected).map((c) => c.title || c.value);
|
|
393
|
+
const typed = q.freeText.trim();
|
|
394
|
+
if (typed) parts.push(typed);
|
|
395
|
+
return parts.join(", ");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Re-derive everything a questionnaire shows from its answers and the
|
|
399
|
+
* question on screen.
|
|
400
|
+
*
|
|
401
|
+
* ONE function, called after every change — arrival, a tap, typing, Back,
|
|
402
|
+
* Next, a poll — for the reason `reselect` is one function: the head row,
|
|
403
|
+
* the choices, the Next gate and the submit gate are all pure functions of
|
|
404
|
+
* the same two inputs, and a reducer that updated three of the four is how a
|
|
405
|
+
* submit ends up enabled over a missing answer.
|
|
406
|
+
*
|
|
407
|
+
* `touched` decides whether the incomplete line may show. On arrival nothing
|
|
408
|
+
* is answered yet, and a form that opens by scolding reads as broken. */
|
|
409
|
+
export function questionnaireSynced(
|
|
410
|
+
w: WidgetView,
|
|
411
|
+
index: number,
|
|
412
|
+
copy: QuestionnaireCopy,
|
|
413
|
+
touched: boolean,
|
|
414
|
+
): WidgetView {
|
|
415
|
+
const questions = w.questions.map((q) => {
|
|
416
|
+
const answer = answerOf(q);
|
|
417
|
+
return { ...q, answer, answerHidden: !answer, missing: q.required && !answer };
|
|
418
|
+
});
|
|
419
|
+
const total = questions.length;
|
|
420
|
+
const i = total ? Math.max(0, Math.min(index, total - 1)) : 0;
|
|
421
|
+
const q = questions[i];
|
|
422
|
+
const isLast = i >= total - 1;
|
|
423
|
+
const incomplete = questions.some((x) => x.missing);
|
|
424
|
+
const multi = q?.type === "multi-select";
|
|
425
|
+
const hasChoices = !!q && q.choices.length > 0;
|
|
426
|
+
const emptySelectionSubmit = !!w.primaryRespondText
|
|
427
|
+
&& (w.primaryRespondText.includes(SELECTED_TOKEN) || w.primaryRespondText.includes(VALUES_TOKEN))
|
|
428
|
+
&& !questions.some((question) => answerOf(question));
|
|
429
|
+
const blocked = w.submitting || incomplete || emptySelectionSubmit;
|
|
430
|
+
// A primary with a destination is navigation, not questionnaire submit.
|
|
431
|
+
// Keep it available while required answers are incomplete; the submit action
|
|
432
|
+
// remains gated in the worker as well as in the descriptor.
|
|
433
|
+
const primaryDisabled = !!w.primaryUrl ? w.submitting : blocked;
|
|
434
|
+
const hasSubmit = !!(w.primaryRespondText || w.primaryUrl);
|
|
435
|
+
const primaryLabel = hasSubmit ? (w.primaryLabel || copy.submit) : "";
|
|
436
|
+
const answersText = touched && isLast && incomplete ? copy.incomplete : "";
|
|
437
|
+
return {
|
|
438
|
+
...w,
|
|
439
|
+
questions,
|
|
440
|
+
questionIndex: i,
|
|
441
|
+
options: q ? q.choices : [],
|
|
442
|
+
questionHead: q
|
|
443
|
+
? [{
|
|
444
|
+
id: q.id,
|
|
445
|
+
title: q.required ? `${q.title} *` : q.title,
|
|
446
|
+
subtitle: q.subtitle, subtitleHidden: !q.subtitle,
|
|
447
|
+
progress: total > 1 ? copy.progress(i + 1, total) : "",
|
|
448
|
+
progressHidden: total <= 1,
|
|
449
|
+
}]
|
|
450
|
+
: [],
|
|
451
|
+
questionFree: q && !q.freeHidden
|
|
452
|
+
? [{ id: q.id, placeholder: q.freePlaceholder, value: q.freeText }]
|
|
453
|
+
: [],
|
|
454
|
+
questionRadioHidden: !hasChoices || multi,
|
|
455
|
+
questionCheckHidden: !hasChoices || !multi,
|
|
456
|
+
questionBackHidden: i === 0,
|
|
457
|
+
questionNextHidden: !q || isLast,
|
|
458
|
+
questionNextDisabled: !!q?.missing,
|
|
459
|
+
questionNextEnabled: !q?.missing,
|
|
460
|
+
answersIncomplete: incomplete,
|
|
461
|
+
answersText, answersTextHidden: !answersText,
|
|
462
|
+
// The submit only exists on the last question. Before it, Next stands
|
|
463
|
+
// where it would be — two primaries side by side ask the owner to guess.
|
|
464
|
+
primaryLabel,
|
|
465
|
+
primaryHidden: !q || !isLast || !hasSubmit,
|
|
466
|
+
primaryDisabled,
|
|
467
|
+
primaryEnabled: !primaryDisabled,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** The answers, as the one message a questionnaire sends. Numbered when there
|
|
472
|
+
* is more than one question, so the assistant can tell which answer is whose;
|
|
473
|
+
* an unanswered optional question is left out rather than sent blank. */
|
|
474
|
+
export function questionnaireAnswers(w: WidgetView): string {
|
|
475
|
+
const numbered = w.questions.length > 1;
|
|
476
|
+
return w.questions
|
|
477
|
+
.map((q, i) => ({ i, a: answerOf(q) }))
|
|
478
|
+
.filter((x) => x.a)
|
|
479
|
+
.map((x) => (numbered ? `${x.i + 1}. ${x.a}` : x.a))
|
|
480
|
+
.join("\n");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Is `b` the same questionnaire as `a` — so an owner's answers to `a` still
|
|
484
|
+
* belong to it? Compared on what the owner answered AGAINST: the questions and
|
|
485
|
+
* their choices. */
|
|
486
|
+
export function sameQuestionnaire(a: WidgetView, b: WidgetView): boolean {
|
|
487
|
+
if (a.kind !== "QUESTIONNAIRE" || b.kind !== "QUESTIONNAIRE") return false;
|
|
488
|
+
if (a.questions.length !== b.questions.length) return false;
|
|
489
|
+
return a.questions.every((q, i) => {
|
|
490
|
+
const o = b.questions[i];
|
|
491
|
+
return q.title === o.title && q.type === o.type && q.choices.length === o.choices.length
|
|
492
|
+
&& q.choices.every((c, j) => c.value === o.choices[j].value);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The one-question-at-a-time fields for anything that is not a questionnaire:
|
|
497
|
+
* no question, and every control collapsed. */
|
|
498
|
+
const NO_QUESTION = {
|
|
499
|
+
questionIndex: 0,
|
|
500
|
+
questionHead: [] as WidgetQuestionHead[],
|
|
501
|
+
questionFree: [] as WidgetQuestionFree[],
|
|
502
|
+
questionRadioHidden: true, questionCheckHidden: true,
|
|
503
|
+
questionBackHidden: true, questionNextHidden: true,
|
|
504
|
+
questionNextDisabled: false, questionNextEnabled: true,
|
|
505
|
+
};
|
|
506
|
+
|
|
295
507
|
/** Nothing to show. The region binds this and collapses. */
|
|
296
508
|
export const NO_WIDGET: WidgetView = {
|
|
297
509
|
kind: "NONE",
|
|
@@ -307,6 +519,9 @@ export const NO_WIDGET: WidgetView = {
|
|
|
307
519
|
options: [], maxSelected: 0, limitReached: false, requiresSelection: false,
|
|
308
520
|
limitText: "", limitTextHidden: true,
|
|
309
521
|
primaryDisabled: false, primaryEnabled: true, submitting: false,
|
|
522
|
+
notice: "", noticeHidden: true,
|
|
523
|
+
optionsGridHidden: true, optionsListHidden: false,
|
|
524
|
+
footerRowHidden: false, footerColumnHidden: true,
|
|
310
525
|
slices: [], values: [], chartIsPie: false, chartIsSeries: false, chartKind: "",
|
|
311
526
|
chartPieHidden: true, chartSeriesHidden: true, chartEmptyHidden: true,
|
|
312
527
|
asyncSpinnerHidden: true, itemsLoadingHidden: true,
|
|
@@ -317,6 +532,7 @@ export const NO_WIDGET: WidgetView = {
|
|
|
317
532
|
query: "", suggestions: [], suggestionsEmpty: true, noMatches: false, selectionType: "",
|
|
318
533
|
questions: [], questionsEmpty: true, answersIncomplete: false,
|
|
319
534
|
answersText: "", answersTextHidden: true,
|
|
535
|
+
...NO_QUESTION,
|
|
320
536
|
providerKey: "", itemsLoading: false, itemsError: "", itemsErrorHidden: true, itemsEmpty: true,
|
|
321
537
|
treeJson: "",
|
|
322
538
|
};
|
|
@@ -359,7 +575,7 @@ function optionRow(
|
|
|
359
575
|
o: {
|
|
360
576
|
value: string; title: string; subtitle: string; text: string;
|
|
361
577
|
pictureUrl: string; buttonLabel: string; actionUrl: string; listText: string;
|
|
362
|
-
respondText: string;
|
|
578
|
+
respondText: string; actionScreenId?: string; notSupportedText?: string;
|
|
363
579
|
},
|
|
364
580
|
i: number,
|
|
365
581
|
selected: boolean,
|
|
@@ -388,7 +604,18 @@ function optionRow(
|
|
|
388
604
|
// "(unknown site)" to a button that goes nowhere. That reads as a phishing
|
|
389
605
|
// warning on the ordinary path and devalues the annotation on the path that
|
|
390
606
|
// needs it.
|
|
391
|
-
|
|
607
|
+
//
|
|
608
|
+
// Web schemes only, decided HERE and not just at the opener: the button is
|
|
609
|
+
// painted for its destination, so a `tel:` or custom-scheme url the opener
|
|
610
|
+
// refuses would otherwise put up a live-looking control that does nothing.
|
|
611
|
+
const unsupportedUrl = !!o.actionUrl && !isWebUrl(o.actionUrl);
|
|
612
|
+
const actionUrl = o.actionUrl && isWebUrl(o.actionUrl) ? o.actionUrl.trim() : "";
|
|
613
|
+
const actionScreenId = o.actionScreenId ?? "";
|
|
614
|
+
const buttonLabel = discloseButton(disclose, o.buttonLabel, actionUrl);
|
|
615
|
+
// A URL destination wins over the fallback text. If this host cannot open
|
|
616
|
+
// that URL, hide the button rather than sending a different action.
|
|
617
|
+
const respondText = unsupportedUrl ? "" : o.respondText
|
|
618
|
+
|| (genericFallback ? (o.title || o.text || o.subtitle || o.value) : "");
|
|
392
619
|
|
|
393
620
|
// An option's two display lines.
|
|
394
621
|
//
|
|
@@ -411,11 +638,15 @@ function optionRow(
|
|
|
411
638
|
subtitle: rowSubtitle, subtitleHidden: !rowSubtitle,
|
|
412
639
|
text: o.text, textHidden: !o.text,
|
|
413
640
|
pictureUrl, pictureHidden: !pictureUrl,
|
|
414
|
-
|
|
641
|
+
// A label is not enough: the button has to be able to DO something — say
|
|
642
|
+
// its text, go to its url, or explain why its screen is not here.
|
|
643
|
+
buttonLabel, buttonHidden: !(buttonLabel && (respondText || actionUrl || actionScreenId)),
|
|
415
644
|
listText: o.listText, listHidden: !o.listText,
|
|
416
645
|
selected,
|
|
417
|
-
respondText
|
|
418
|
-
|
|
646
|
+
respondText,
|
|
647
|
+
actionUrl,
|
|
648
|
+
actionScreenId,
|
|
649
|
+
notSupportedText: o.notSupportedText ?? "",
|
|
419
650
|
};
|
|
420
651
|
}
|
|
421
652
|
|
|
@@ -486,6 +717,10 @@ export function viewOf(
|
|
|
486
717
|
const sections = Array.isArray(extra.sections) ? (extra.sections as Record<string, unknown>[]) : [];
|
|
487
718
|
const rows: WidgetRow[] = sections.map((sec, i) => {
|
|
488
719
|
const subtitle = stripMarkdown(typeof sec.subtitle === "string" ? sec.subtitle : "");
|
|
720
|
+
// Painted, so flattened like every other button label (`discloseButton`).
|
|
721
|
+
// `respondText` is SENT, not painted, so it goes back verbatim.
|
|
722
|
+
const buttonLabel = stripMarkdown(typeof sec.buttonLabel === "string" ? sec.buttonLabel : "");
|
|
723
|
+
const respondText = typeof sec.respondText === "string" ? sec.respondText : "";
|
|
489
724
|
return {
|
|
490
725
|
// A stable id per row. Index-based on purpose: a section has no id of its
|
|
491
726
|
// own, and the list is rebuilt wholesale on every tick, so there is
|
|
@@ -494,13 +729,32 @@ export function viewOf(
|
|
|
494
729
|
title: stripMarkdown(typeof sec.title === "string" ? sec.title : ""),
|
|
495
730
|
subtitle,
|
|
496
731
|
subtitleHidden: !subtitle,
|
|
732
|
+
// The section's `textButton`. It can only SEND, so without a text to
|
|
733
|
+
// send it would be a label that does nothing when tapped.
|
|
734
|
+
buttonLabel,
|
|
735
|
+
buttonHidden: !(buttonLabel && respondText),
|
|
736
|
+
respondText,
|
|
497
737
|
};
|
|
498
738
|
});
|
|
499
739
|
|
|
500
740
|
const maxSelected = typeof extra.maxSelected === "number" ? extra.maxSelected : 0;
|
|
741
|
+
const thumbnail = w.kind === "MULTI_SELECT" && extra.optionLayout === "thumbnail";
|
|
742
|
+
const stacked = w.kind === "MULTI_SELECT" && extra.footerButtonsAsColumn === true;
|
|
501
743
|
// `isPreselected` seeds the selection, so the row's `selected` starts true
|
|
502
744
|
// for exactly the options the service marked.
|
|
503
|
-
const
|
|
745
|
+
const mappedOptions: WidgetOptionRow[] = w.options.map((o, i) => optionRow(o, i, o.preselected, disclose, allowPicture, w.kind === "UNKNOWN"));
|
|
746
|
+
// SINGLE_SELECT's whole row responds to a tap, even when its trailing button
|
|
747
|
+
// is hidden. A phone/custom-scheme-only destination has no action this host
|
|
748
|
+
// can perform; keep rows with a text response or explainable screen action.
|
|
749
|
+
const options = w.kind === "SINGLE_SELECT"
|
|
750
|
+
? mappedOptions.filter((row, i) => {
|
|
751
|
+
const rawUrl = w.options[i]?.actionUrl ?? "";
|
|
752
|
+
// A normal single-select row has no actionUrl and is selectable by
|
|
753
|
+
// definition. A declared unsupported URL wins over any fallback text
|
|
754
|
+
// or screen destination, so that row must be removed.
|
|
755
|
+
return !rawUrl || isWebUrl(rawUrl);
|
|
756
|
+
})
|
|
757
|
+
: mappedOptions;
|
|
504
758
|
|
|
505
759
|
// CHART. `pieSlices` is the only shape the catalogue sends, but `chartType`
|
|
506
760
|
// decides the component: a pie is a `DonutChart` at zero thickness, anything
|
|
@@ -546,16 +800,35 @@ export function viewOf(
|
|
|
546
800
|
const sub = typeof q.subtitle === "string" ? q.subtitle : "";
|
|
547
801
|
const free = typeof q.freeFormatPlaceholder === "string" ? q.freeFormatPlaceholder : "";
|
|
548
802
|
const required = q.required === true;
|
|
803
|
+
const type = typeof q.type === "string" ? q.type : "";
|
|
804
|
+
// The choices. Dropping these is what made the catalogue's required
|
|
805
|
+
// single-select unanswerable: it has no free-text field, so a form that
|
|
806
|
+
// drew only that field had nothing the owner could give it.
|
|
807
|
+
const raw = Array.isArray(q.options) ? (q.options as WidgetOption[]) : [];
|
|
808
|
+
// A single-select starts with at most ONE preselected choice, whatever the
|
|
809
|
+
// payload marked — a radio group showing two ticks is a state the control
|
|
810
|
+
// cannot otherwise reach.
|
|
811
|
+
let seeded = false;
|
|
812
|
+
const choices = raw.map((o, j) => {
|
|
813
|
+
const pre = !!o.preselected && (type !== "single-select" || !seeded);
|
|
814
|
+
if (pre) seeded = true;
|
|
815
|
+
return optionRow(o, j, pre, disclose, allowPicture);
|
|
816
|
+
});
|
|
817
|
+
// A question with no choices and no free-text option of its own can still
|
|
818
|
+
// be answered — text is the only thing it can take — so it gets the field.
|
|
819
|
+
const freeShown = !!free || choices.length === 0;
|
|
549
820
|
return {
|
|
550
821
|
id: `q${i}`,
|
|
551
822
|
title: typeof q.title === "string" ? q.title : "",
|
|
552
823
|
subtitle: sub, subtitleHidden: !sub,
|
|
553
|
-
type
|
|
824
|
+
type,
|
|
554
825
|
required,
|
|
555
|
-
//
|
|
826
|
+
// Derived by `questionnaireSynced`, which runs before this is returned.
|
|
556
827
|
missing: required,
|
|
557
828
|
answer: "", answerHidden: true,
|
|
558
|
-
freePlaceholder: free, freeHidden: !
|
|
829
|
+
freePlaceholder: free, freeHidden: !freeShown,
|
|
830
|
+
choices,
|
|
831
|
+
freeText: "",
|
|
559
832
|
};
|
|
560
833
|
});
|
|
561
834
|
|
|
@@ -581,10 +854,37 @@ export function viewOf(
|
|
|
581
854
|
// The synthesized template is the label itself, which is what `<SELECTED>`
|
|
582
855
|
// substitution produces for the known kinds anyway.
|
|
583
856
|
const generic = w.kind === "UNKNOWN";
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
857
|
+
// A QUESTIONNAIRE's submit is the one KNOWN kind that can synthesize its
|
|
858
|
+
// response template: the catalogue sends `{label: "Send answers"}` and
|
|
859
|
+
// nothing else, and what it should say is fully determined — the answers.
|
|
860
|
+
// Only do that when the button declares no destination. If it declares a
|
|
861
|
+
// phone URL or a native Wix screen this host cannot open, hide the control
|
|
862
|
+
// instead of turning the destination into an answer submit.
|
|
863
|
+
const primaryUnsupportedUrl = !!(w.primary?.externalUrl && !primaryUrl);
|
|
864
|
+
const secondaryUnsupportedUrl = !!(w.secondary?.externalUrl && !secondaryUrl);
|
|
865
|
+
const primaryRespondFallback = w.primary?.respondText ?? "";
|
|
866
|
+
const secondaryRespondFallback = w.secondary?.respondText ?? "";
|
|
867
|
+
const primaryFallbackText = (w.primary?.respondText ?? "")
|
|
868
|
+
|| (generic ? (w.primary?.label ?? "") : "")
|
|
869
|
+
|| (w.kind === "QUESTIONNAIRE" && w.primary
|
|
870
|
+
&& !w.primary.externalUrl && !w.primary.actionScreenId ? SELECTED_TOKEN : "");
|
|
871
|
+
const secondaryFallbackText = (w.secondary?.respondText ?? "")
|
|
587
872
|
|| (generic ? (w.secondary?.label ?? "") : "");
|
|
873
|
+
// A supported URL wins; an unsupported URL is suppressed so a phone/custom
|
|
874
|
+
// destination cannot turn into an unrelated response. A native Wix screen
|
|
875
|
+
// can use an explicit response as its fallback, but a questionnaire's
|
|
876
|
+
// primary is its answer submit, so never turn that screen destination into
|
|
877
|
+
// a submit. Do not synthesize a generic label as a screen fallback.
|
|
878
|
+
const primaryUnsupportedScreen = !!w.primary?.actionScreenId;
|
|
879
|
+
const secondaryUnsupportedScreen = !!w.secondary?.actionScreenId;
|
|
880
|
+
const primaryRespondText = primaryUnsupportedUrl
|
|
881
|
+
|| (primaryUnsupportedScreen && (!primaryRespondFallback || w.kind === "QUESTIONNAIRE"))
|
|
882
|
+
? ""
|
|
883
|
+
: primaryFallbackText;
|
|
884
|
+
const secondaryRespondText = secondaryUnsupportedUrl
|
|
885
|
+
|| (secondaryUnsupportedScreen && !secondaryRespondFallback)
|
|
886
|
+
? ""
|
|
887
|
+
: secondaryFallbackText;
|
|
588
888
|
const primaryLabel = w.primary ? discloseButton(disclose, w.primary.label, primaryUrl) : "";
|
|
589
889
|
const secondaryLabel = w.secondary ? discloseButton(disclose, w.secondary.label, secondaryUrl) : "";
|
|
590
890
|
// Whether each footer button is actually SHOWN — a label to read AND
|
|
@@ -627,7 +927,7 @@ export function viewOf(
|
|
|
627
927
|
|| sections.length || slices.length || steps.length || questions.length
|
|
628
928
|
|| asyncMessage || treeJson);
|
|
629
929
|
|
|
630
|
-
|
|
930
|
+
const view: WidgetView = {
|
|
631
931
|
kind: w.kind,
|
|
632
932
|
hidden: !renderable,
|
|
633
933
|
options,
|
|
@@ -672,6 +972,7 @@ export function viewOf(
|
|
|
672
972
|
questionsEmpty: w.kind === "QUESTIONNAIRE" && questions.length === 0,
|
|
673
973
|
answersIncomplete: questions.some((q) => q.missing),
|
|
674
974
|
answersText: "", answersTextHidden: true,
|
|
975
|
+
...NO_QUESTION,
|
|
675
976
|
providerKey,
|
|
676
977
|
// An ITEMS_SELECTION arrives with NO rows — they come from the provider —
|
|
677
978
|
// so it starts loading rather than starting empty.
|
|
@@ -686,6 +987,11 @@ export function viewOf(
|
|
|
686
987
|
Object.entries(msgs).filter((kv): kv is [string, string] => typeof kv[1] === "string"),
|
|
687
988
|
),
|
|
688
989
|
submitting: false,
|
|
990
|
+
notice: "", noticeHidden: true,
|
|
991
|
+
// Layout hints, honoured only where the catalogue sends them — MULTI_SELECT.
|
|
992
|
+
// Everything else keeps the one form its case draws.
|
|
993
|
+
optionsGridHidden: !thumbnail, optionsListHidden: thumbnail,
|
|
994
|
+
footerRowHidden: stacked, footerColumnHidden: !stacked,
|
|
689
995
|
text: prose, textHidden: !prose,
|
|
690
996
|
title, titleHidden: !title,
|
|
691
997
|
bottomText: bottom, bottomTextHidden: !bottom,
|
|
@@ -712,6 +1018,12 @@ export function viewOf(
|
|
|
712
1018
|
secondaryRespondText,
|
|
713
1019
|
rows,
|
|
714
1020
|
};
|
|
1021
|
+
// A questionnaire is shown one question at a time; its on-screen fields are
|
|
1022
|
+
// derived, never stored, so they start from the same function every later
|
|
1023
|
+
// change goes through. The worker re-derives with its own copy.
|
|
1024
|
+
return w.kind === "QUESTIONNAIRE"
|
|
1025
|
+
? questionnaireSynced(view, 0, DEFAULT_QUESTIONNAIRE_COPY, false)
|
|
1026
|
+
: view;
|
|
715
1027
|
}
|
|
716
1028
|
|
|
717
1029
|
/** The widget the screen should be showing, read off the server transcript.
|
|
@@ -753,5 +1065,8 @@ export function activeWidgetOf(
|
|
|
753
1065
|
if (!found.length) return NO_WIDGET;
|
|
754
1066
|
// A turn with several envelopes shows the LAST, matching "one at a time"
|
|
755
1067
|
// and the reading order — the final ask is the live one.
|
|
756
|
-
return
|
|
1068
|
+
return {
|
|
1069
|
+
...viewOf(normalizeWidget(found[found.length - 1]), disclose, subtree, allowPicture),
|
|
1070
|
+
sourceMessageId: m.id,
|
|
1071
|
+
};
|
|
757
1072
|
}
|
|
@@ -203,6 +203,8 @@ const core = createAriaChat({
|
|
|
203
203
|
// English. Says no "sign in": the owner already is, and the missing piece is
|
|
204
204
|
// the app's binding, so there is no action for them to take.
|
|
205
205
|
unavailable: () => copyParam("ownerAria.unavailable", "Aria can't reach your Wix account from here."),
|
|
206
|
+
questionnaireSubmit: () => copyParam("ownerAria.questionnaire.submit", "Submit"),
|
|
207
|
+
notSupported: () => copyParam("ownerAria.notSupported", "That isn't available in this app yet."),
|
|
206
208
|
dictationEmpty: () => copyParam("ownerAria.dictationEmpty", "Didn't catch that. Check microphone access and try again."),
|
|
207
209
|
dictationFailed: () => copyParam("ownerAria.dictationFailed", "Dictation isn't available right now."),
|
|
208
210
|
// The mention picker. Through copyParam for the same reason as the rest.
|
|
@@ -217,6 +219,8 @@ const core = createAriaChat({
|
|
|
217
219
|
// sentence around it is localized.
|
|
218
220
|
mentionItemsFailed: (category: string) =>
|
|
219
221
|
copyTemplate("ownerAria.mention.itemsFailed", "Couldn't load {category}.", { category }),
|
|
222
|
+
widgetItemsFailed: () => copyParam("ownerAria.widget.itemsFailed", "Couldn't load these items."),
|
|
223
|
+
widgetItemsUnavailable: () => copyParam("ownerAria.widget.itemsUnavailable", "Items aren't available here."),
|
|
220
224
|
attachUploading: () => copyParam("ownerAria.attach.uploading", "Uploading…"),
|
|
221
225
|
attachFailed: () => copyParam("ownerAria.attach.failed", "Couldn't upload"),
|
|
222
226
|
attachTooMany: (max) => copyTemplate("ownerAria.attach.tooMany", "You can attach up to {max} photos.", { max }),
|
|
@@ -310,6 +314,7 @@ const svc = defineService<AriaChatState, {
|
|
|
310
314
|
widgetPick: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
311
315
|
widgetQuery: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
312
316
|
widgetAnswer: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
317
|
+
widgetStep: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
313
318
|
dictate: (s: AriaChatState) => AriaChatState;
|
|
314
319
|
finishDictation: (s: AriaChatState) => AriaChatState;
|
|
315
320
|
// `undefined` for a result whose session is no longer the live one — see the core.
|
|
@@ -361,6 +366,8 @@ const svc = defineService<AriaChatState, {
|
|
|
361
366
|
widgetPick: core.widgetPick,
|
|
362
367
|
widgetQuery: core.widgetQuery,
|
|
363
368
|
widgetAnswer: core.widgetAnswer,
|
|
369
|
+
// A questionnaire's Back / Next — one question on screen at a time.
|
|
370
|
+
widgetStep: core.widgetStep,
|
|
364
371
|
// Composer dictation: the mic, the listening row's Done, and the result
|
|
365
372
|
// re-entering from `dictation.deliver` above. Exported for the same reason
|
|
366
373
|
// as the widget actions — an action the entry does not name is a control
|