@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.
Files changed (63) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/wix-member-login.d.ts +6 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +2 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/serve-runtime/src/wix-bridge.d.ts +144 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/serve-storage/src/sha256.d.ts +13 -0
  5. package/dist/cli.mjs +554 -284
  6. package/dist/preview-assets.generated.bin +0 -0
  7. package/guides/sdk-reference.json +1 -1
  8. package/package.json +2 -2
  9. package/templates/beauty-salon-owner/dev/index.ts +668 -87
  10. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +271 -45
  11. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  12. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  13. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  14. package/templates/community-club-owner/dev/index.ts +668 -87
  15. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +271 -45
  16. package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  17. package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  18. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  19. package/templates/events-meetups-owner/dev/index.ts +668 -87
  20. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +271 -45
  21. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  22. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  23. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  24. package/templates/gym-fitness-owner/dev/index.ts +668 -87
  25. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +271 -45
  26. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  27. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  28. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  29. package/templates/nonprofit-cause-owner/dev/index.ts +668 -87
  30. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +271 -45
  31. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  32. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  33. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  34. package/templates/owner-lite/dev/index.ts +754 -173
  35. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +271 -45
  36. package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +3 -0
  37. package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +333 -18
  38. package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  39. package/templates/podcast-creator-owner/dev/index.ts +668 -87
  40. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +271 -45
  41. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  42. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  43. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  44. package/templates/portfolio-creative-owner/dev/index.ts +668 -87
  45. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +271 -45
  46. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  47. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  48. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  49. package/templates/restaurant-owner/dev/index.ts +668 -87
  50. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +271 -45
  51. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  52. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  53. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  54. package/templates/retail-store-owner/dev/index.ts +668 -87
  55. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +271 -45
  56. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  57. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  58. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +7 -0
  59. package/templates/yoga-studio-owner/dev/index.ts +668 -87
  60. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +271 -45
  61. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +3 -0
  62. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +333 -18
  63. 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, flattened. `$item.`-templated, so every
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
- const buttonLabel = discloseButton(disclose, o.buttonLabel, o.actionUrl);
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
- buttonLabel, buttonHidden: !buttonLabel,
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: o.respondText
418
- || (genericFallback ? (o.title || o.text || o.subtitle || o.value) : ""),
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 options: WidgetOptionRow[] = w.options.map((o, i) => optionRow(o, i, o.preselected, disclose, allowPicture, w.kind === "UNKNOWN"));
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: typeof q.type === "string" ? q.type : "",
824
+ type,
554
825
  required,
555
- // Nothing is answered on arrival, so a required question starts missing.
826
+ // Derived by `questionnaireSynced`, which runs before this is returned.
556
827
  missing: required,
557
828
  answer: "", answerHidden: true,
558
- freePlaceholder: free, freeHidden: !free,
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
- const primaryRespondText = (w.primary?.respondText ?? "")
585
- || (generic ? (w.primary?.label ?? "") : "");
586
- const secondaryRespondText = (w.secondary?.respondText ?? "")
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
- return {
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 viewOf(normalizeWidget(found[found.length - 1]), disclose, subtree, allowPicture);
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