@wix/himalaya-cli 0.960.0 → 0.962.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/cli.mjs +1 -0
- package/package.json +2 -2
- package/templates/beauty-salon-owner/config.ts +1 -1
- package/templates/beauty-salon-owner/dev/index.ts +3075 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/community-club-owner/config.ts +1 -1
- package/templates/community-club-owner/dev/index.ts +3075 -0
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/community-club-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/events-meetups-owner/config.ts +1 -1
- package/templates/events-meetups-owner/dev/index.ts +3075 -0
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/events-meetups-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/gym-fitness-owner/config.ts +1 -1
- package/templates/gym-fitness-owner/dev/index.ts +3075 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/nonprofit-cause-owner/config.ts +1 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +3075 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/owner-lite/config.ts +1 -1
- package/templates/owner-lite/dev/index.ts +3075 -0
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/owner-lite/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/owner-lite/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/podcast-creator-owner/config.ts +1 -1
- package/templates/podcast-creator-owner/dev/index.ts +3075 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/portfolio-creative-owner/config.ts +1 -1
- package/templates/portfolio-creative-owner/dev/index.ts +3075 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/restaurant-owner/config.ts +1 -1
- package/templates/restaurant-owner/dev/index.ts +3075 -0
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/restaurant-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/retail-store-owner/config.ts +1 -1
- package/templates/retail-store-owner/dev/index.ts +3075 -0
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/retail-store-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/yoga-studio-owner/config.ts +1 -1
- package/templates/yoga-studio-owner/dev/index.ts +3075 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +466 -9
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-generic-widget.ts +251 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-kinds.ts +332 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +331 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +722 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// The active response widget, flattened into something a descriptor can bind
|
|
2
|
+
// to.
|
|
3
|
+
//
|
|
4
|
+
// `aria-widget-kinds.ts` turns a scanned envelope into a
|
|
5
|
+
// `NormalizedWidget`. That model is shaped for CODE — nested objects, a typed
|
|
6
|
+
// `extra` bag, `null` for an absent button. A descriptor cannot read any of
|
|
7
|
+
// that: `$service:` resolves a path to a SCALAR, and `$item.` substitutes whole
|
|
8
|
+
// prop values only, so a row whose field is missing renders as nothing at all.
|
|
9
|
+
//
|
|
10
|
+
// This module is the adapter between the two. Everything it produces is either
|
|
11
|
+
// a scalar or an array of all-scalar rows, every field is always present, and
|
|
12
|
+
// every collapsible element carries its own `*Hidden` boolean — the same shape
|
|
13
|
+
// the transcript bubbles already use for their three link slots.
|
|
14
|
+
|
|
15
|
+
import type { NormalizedWidget, WidgetKind } from "./aria-widget-model.js";
|
|
16
|
+
import { composeGenericCard } from "./aria-generic-widget.js";
|
|
17
|
+
import { normalizeWidget } from "./aria-widget-kinds.js";
|
|
18
|
+
import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
|
|
19
|
+
|
|
20
|
+
/** Just enough of a transcript row to find the active widget in. Declared here
|
|
21
|
+
* rather than imported from `aria-chat.ts` so this module has no dependency on
|
|
22
|
+
* it at all — see `Disclose` below for why that matters. */
|
|
23
|
+
export interface WidgetSourceMessage {
|
|
24
|
+
role?: string;
|
|
25
|
+
content?: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** How a button's label is rendered when it carries an off-Wix destination.
|
|
29
|
+
*
|
|
30
|
+
* Passed IN rather than imported, and the reason is mechanical: `aria-chat.ts`
|
|
31
|
+
* imports this module to build its state, so importing `disclosedLabel` back
|
|
32
|
+
* out of it would close a runtime cycle through the bundler. Injection keeps
|
|
33
|
+
* the single implementation — the caller hands over the very same
|
|
34
|
+
* `disclosedLabel` the transcript's link rows use — while leaving this module
|
|
35
|
+
* standalone and testable without booting the chat core. */
|
|
36
|
+
export type Disclose = (link: { label: string; url: string }) => string;
|
|
37
|
+
|
|
38
|
+
/** What the region keys on. The widget kinds, plus the one state that is not a
|
|
39
|
+
* widget: nothing to show, region collapsed. */
|
|
40
|
+
export type WidgetCase = WidgetKind | "NONE";
|
|
41
|
+
|
|
42
|
+
/** Every case the descriptor must carry, in a fixed order.
|
|
43
|
+
*
|
|
44
|
+
* Exported so the `FragmentSlot` and the test that pins it read the SAME list
|
|
45
|
+
* rather than two hand-kept copies — a case the union has and the descriptor
|
|
46
|
+
* does not is a widget that renders nothing, silently. */
|
|
47
|
+
export const WIDGET_CASES: readonly WidgetCase[] = [
|
|
48
|
+
"SINGLE_SELECT", "MULTI_SELECT", "SINGLE_SELECT_BUTTON", "RADIO_BUTTON", "PLANS",
|
|
49
|
+
"PREMIUM_UPSELL", "VISUAL_DISPLAY", "AUTOCOMPLETE", "ITEMS_SELECTION", "QUESTIONNAIRE",
|
|
50
|
+
"CHART", "TODO_PLAN", "ASYNC_TOOL", "OAUTH_REQUIRED", "UNKNOWN", "NONE",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** One body row of a display widget. All-scalar, every field present: this is
|
|
54
|
+
* `$item.`-templated, and a row missing a field substitutes as nothing. */
|
|
55
|
+
export interface WidgetRow {
|
|
56
|
+
id: string;
|
|
57
|
+
title: string;
|
|
58
|
+
subtitle: string;
|
|
59
|
+
/** `true` when `subtitle` is empty, so the row collapses its second line
|
|
60
|
+
* instead of leaving a gap. `$item.` cannot express "hide when empty". */
|
|
61
|
+
subtitleHidden: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One selectable option, flattened for binding.
|
|
65
|
+
*
|
|
66
|
+
* Same rule as `WidgetRow` and for the same reason: `$item.` substitutes whole
|
|
67
|
+
* prop values, so every field is present and scalar or the row renders as
|
|
68
|
+
* nothing at all. `selected` is a boolean the worker owns — a `$service:`
|
|
69
|
+
* binding is a read-only snapshot, so a checkbox reads it and writes through a
|
|
70
|
+
* `widgetToggle` dispatch rather than mutating anything itself. */
|
|
71
|
+
export interface WidgetOptionRow {
|
|
72
|
+
id: string;
|
|
73
|
+
value: string;
|
|
74
|
+
title: string;
|
|
75
|
+
subtitle: string;
|
|
76
|
+
subtitleHidden: boolean;
|
|
77
|
+
text: string;
|
|
78
|
+
textHidden: boolean;
|
|
79
|
+
pictureUrl: string;
|
|
80
|
+
pictureHidden: boolean;
|
|
81
|
+
/** The per-option button's label, already host-disclosed. */
|
|
82
|
+
buttonLabel: string;
|
|
83
|
+
buttonHidden: boolean;
|
|
84
|
+
/** PLANS' benefit list, joined — an array cannot ride on a templated row. */
|
|
85
|
+
listText: string;
|
|
86
|
+
listHidden: boolean;
|
|
87
|
+
selected: boolean;
|
|
88
|
+
/** This option's own button `respondText`, before substitution. Carried on
|
|
89
|
+
* the row because the row is what a tap identifies. */
|
|
90
|
+
respondText: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One slice of a CHART. `value` is a RAW magnitude, never a percentage: the
|
|
94
|
+
* renderer computes each share, because one handed percentages that do not sum
|
|
95
|
+
* to 100 cannot draw them honestly. */
|
|
96
|
+
export interface WidgetSlice {
|
|
97
|
+
label: string;
|
|
98
|
+
value: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One step of a TODO_PLAN, shaped for `Timeline`. */
|
|
102
|
+
export interface WidgetStep {
|
|
103
|
+
id: string;
|
|
104
|
+
title: string;
|
|
105
|
+
subtitle: string;
|
|
106
|
+
/** `Timeline` draws a completed entry differently. */
|
|
107
|
+
done: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The active widget, as the screen reads it. */
|
|
111
|
+
export interface WidgetView {
|
|
112
|
+
kind: WidgetCase;
|
|
113
|
+
/** `true` when there is nothing to show. The whole region is hidden on this
|
|
114
|
+
* rather than on `kind === "NONE"`, because a descriptor cannot compare —
|
|
115
|
+
* `hiddenWhen` binds a boolean and nothing else. It is what keeps the region
|
|
116
|
+
* from leaving a gap in the transcript when the assistant is just talking. */
|
|
117
|
+
hidden: boolean;
|
|
118
|
+
/** Prose. For PREMIUM_UPSELL this is the body under `title`. */
|
|
119
|
+
text: string;
|
|
120
|
+
textHidden: boolean;
|
|
121
|
+
title: string;
|
|
122
|
+
titleHidden: boolean;
|
|
123
|
+
bottomText: string;
|
|
124
|
+
bottomTextHidden: boolean;
|
|
125
|
+
badge: string;
|
|
126
|
+
badgeHidden: boolean;
|
|
127
|
+
imageUrl: string;
|
|
128
|
+
imageHidden: boolean;
|
|
129
|
+
/** Footer buttons, already host-disclosed (see `discloseButton`). */
|
|
130
|
+
primaryLabel: string;
|
|
131
|
+
primaryHidden: boolean;
|
|
132
|
+
primaryUrl: string;
|
|
133
|
+
/** The footer buttons' `respondText`, before substitution. */
|
|
134
|
+
primaryRespondText: string;
|
|
135
|
+
secondaryRespondText: string;
|
|
136
|
+
secondaryLabel: string;
|
|
137
|
+
secondaryHidden: boolean;
|
|
138
|
+
secondaryUrl: string;
|
|
139
|
+
rows: WidgetRow[];
|
|
140
|
+
/** The selectable options. Separate from `rows`, which is VISUAL_DISPLAY's
|
|
141
|
+
* body sections — the catalogue sends BOTH at once, so conflating them
|
|
142
|
+
* silently drops one. */
|
|
143
|
+
options: WidgetOptionRow[];
|
|
144
|
+
/** How many options may be chosen at once, `0` for no limit. Enforced in the
|
|
145
|
+
* worker: no component carries a selection-count prop, so this cannot be
|
|
146
|
+
* declared in the descriptor. */
|
|
147
|
+
maxSelected: number;
|
|
148
|
+
/** `true` once the limit is reached, so further options can be disabled. */
|
|
149
|
+
limitReached: boolean;
|
|
150
|
+
/** The line explaining why, empty when there is nothing to explain. */
|
|
151
|
+
limitText: string;
|
|
152
|
+
limitTextHidden: boolean;
|
|
153
|
+
/** The payload said the primary button needs a selection
|
|
154
|
+
* (`disableIfNothingSelected`). Kept as its own field because
|
|
155
|
+
* `primaryDisabled` is derived and has to be recomputed on every toggle —
|
|
156
|
+
* a derived value cannot also be its own input. */
|
|
157
|
+
requiresSelection: boolean;
|
|
158
|
+
/** `true` when the primary button must not be tappable — either
|
|
159
|
+
* `requiresSelection` with nothing chosen, or a send in flight. */
|
|
160
|
+
primaryDisabled: boolean;
|
|
161
|
+
/** Its exact inverse, carried rather than computed, because a descriptor
|
|
162
|
+
* cannot negate: `hiddenPath` binds a boolean and there is no `not`. The
|
|
163
|
+
* live button hides on `primaryDisabled` and the greyed twin hides on this,
|
|
164
|
+
* so exactly one of the two is ever on screen. */
|
|
165
|
+
primaryEnabled: boolean;
|
|
166
|
+
/** A send started from a widget button is in flight. */
|
|
167
|
+
submitting: boolean;
|
|
168
|
+
|
|
169
|
+
// --- CHART ---------------------------------------------------------------
|
|
170
|
+
/** `$service:`-bound, and it has to be: `DonutChart.slices_path` and
|
|
171
|
+
* `ChartSeries.values_path` are SERVICE-ONLY, so a chart cannot live in a
|
|
172
|
+
* templated row. That constraint is why the widget region exists at all.
|
|
173
|
+
* <!-- platform-lint-ok: a property of the two native decoders, not of every surface --> */
|
|
174
|
+
slices: WidgetSlice[];
|
|
175
|
+
/** The raw magnitudes, for the non-pie chart kinds. */
|
|
176
|
+
values: number[];
|
|
177
|
+
/** `"pie"` draws a `DonutChart` at `thickness: 0`; anything else is a
|
|
178
|
+
* `Chart`. Carried as a boolean as well, because a descriptor cannot
|
|
179
|
+
* compare strings. */
|
|
180
|
+
chartIsPie: boolean;
|
|
181
|
+
/** Hide the pie. A dedicated flag because `chartIsSeries` is NOT its
|
|
182
|
+
* negation: an EMPTY chart is neither, so hiding each component on the
|
|
183
|
+
* other's flag left both on screen as blank boxes. */
|
|
184
|
+
chartPieHidden: boolean;
|
|
185
|
+
chartSeriesHidden: boolean;
|
|
186
|
+
chartEmptyHidden: boolean;
|
|
187
|
+
/** Hide the async spinner. `asyncUntracked` is not the right flag: a TRACKED
|
|
188
|
+
* job that reaches `finished` or `failed` still has an id, so hiding on
|
|
189
|
+
* untracked left a terminal job spinning forever. */
|
|
190
|
+
asyncSpinnerHidden: boolean;
|
|
191
|
+
/** Hide the provider spinner. Separate from `itemsErrorHidden`, which is
|
|
192
|
+
* TRUE while loading and FALSE after a failure — using it hid the spinner
|
|
193
|
+
* exactly when it was needed and showed it beside the error. */
|
|
194
|
+
itemsLoadingHidden: boolean;
|
|
195
|
+
chartIsSeries: boolean;
|
|
196
|
+
chartKind: string;
|
|
197
|
+
chartEmpty: boolean;
|
|
198
|
+
description: string;
|
|
199
|
+
descriptionHidden: boolean;
|
|
200
|
+
|
|
201
|
+
// --- TODO_PLAN -----------------------------------------------------------
|
|
202
|
+
steps: WidgetStep[];
|
|
203
|
+
stepsEmpty: boolean;
|
|
204
|
+
/** 0–1. `Timeline` shows WHICH steps are done; this says how far along the
|
|
205
|
+
* whole plan is, which a list of ticks does not answer at a glance. */
|
|
206
|
+
progress: number;
|
|
207
|
+
progressText: string;
|
|
208
|
+
|
|
209
|
+
// --- ASYNC_TOOL ----------------------------------------------------------
|
|
210
|
+
asyncJobId: string;
|
|
211
|
+
/** The job's last known state. Kept so a tick can tell "no change" from
|
|
212
|
+
* "not asked yet" without a second field. */
|
|
213
|
+
asyncState: string;
|
|
214
|
+
/** The message for the job's CURRENT state, already chosen from
|
|
215
|
+
* `statusMessages` — a descriptor cannot pick between four strings. */
|
|
216
|
+
asyncMessage: string;
|
|
217
|
+
/** `true` while the job is still running, so the spinner shows. */
|
|
218
|
+
asyncRunning: boolean;
|
|
219
|
+
/** A job with no id: the status messages are still worth showing, but there
|
|
220
|
+
* is nothing to poll for, so it never spins. */
|
|
221
|
+
asyncUntracked: boolean;
|
|
222
|
+
/** The four status lines, kept so a later state change can pick a different
|
|
223
|
+
* one without re-parsing the payload. */
|
|
224
|
+
extraStatusMessages: Record<string, string>;
|
|
225
|
+
|
|
226
|
+
// --- AUTOCOMPLETE --------------------------------------------------------
|
|
227
|
+
/** What the owner has typed. There is no autocomplete component anywhere in
|
|
228
|
+
* the proto — `TextFieldProps.suggestions` is the OS keyboard strip, not a
|
|
229
|
+
* dropdown — so this is a composition: `SearchField` → worker filter → a
|
|
230
|
+
* `$service:`-bound list that collapses when empty. */
|
|
231
|
+
query: string;
|
|
232
|
+
/** The options that survive `query`. A separate array from `options`, so
|
|
233
|
+
* filtering never destroys what the service sent. */
|
|
234
|
+
suggestions: WidgetOptionRow[];
|
|
235
|
+
suggestionsEmpty: boolean;
|
|
236
|
+
/** `true` when a query is typed and matches nothing — which is a different
|
|
237
|
+
* state from "nothing typed yet", and reads differently to the owner. */
|
|
238
|
+
noMatches: boolean;
|
|
239
|
+
/** `single` or `multi`. */
|
|
240
|
+
selectionType: string;
|
|
241
|
+
|
|
242
|
+
// --- QUESTIONNAIRE -------------------------------------------------------
|
|
243
|
+
questions: WidgetQuestion[];
|
|
244
|
+
questionsEmpty: boolean;
|
|
245
|
+
/** `true` while any `required` question is unanswered. The submit is gated
|
|
246
|
+
* on it — there is no cross-field validation in `Validation`, so "every
|
|
247
|
+
* required question has an answer" has to be worked out here. */
|
|
248
|
+
answersIncomplete: boolean;
|
|
249
|
+
answersText: string;
|
|
250
|
+
answersTextHidden: boolean;
|
|
251
|
+
|
|
252
|
+
// --- ITEMS_SELECTION -----------------------------------------------------
|
|
253
|
+
providerKey: string;
|
|
254
|
+
/** The provider query is in flight. */
|
|
255
|
+
itemsLoading: boolean;
|
|
256
|
+
/** It failed. The chat stays usable; only the widget says so. */
|
|
257
|
+
itemsError: string;
|
|
258
|
+
itemsErrorHidden: boolean;
|
|
259
|
+
itemsEmpty: boolean;
|
|
260
|
+
|
|
261
|
+
// --- UNKNOWN --------------------------------------------------------------
|
|
262
|
+
/** A generic card composed by the worker for a widget kind this build does
|
|
263
|
+
* not recognise, as a JSON string for a `WorkerSubtree` slot.
|
|
264
|
+
*
|
|
265
|
+
* Empty for every KNOWN kind — those have a real `FragmentSlot` case — and
|
|
266
|
+
* empty too when the composer could not build one it is sure of. Empty is
|
|
267
|
+
* not a special case the descriptor has to test for: an empty string fails
|
|
268
|
+
* validation, and a failed validation renders the declared fallback, which
|
|
269
|
+
* is the plain prose card. The fail-closed path and the nothing-to-show
|
|
270
|
+
* path are the same path. */
|
|
271
|
+
treeJson: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** One question of a QUESTIONNAIRE, flattened. `$item.`-templated, so every
|
|
275
|
+
* field is present and scalar. */
|
|
276
|
+
export interface WidgetQuestion {
|
|
277
|
+
id: string;
|
|
278
|
+
title: string;
|
|
279
|
+
subtitle: string;
|
|
280
|
+
subtitleHidden: boolean;
|
|
281
|
+
/** `single-select`, `multi-select`, `text`, … as the service spells it. */
|
|
282
|
+
type: string;
|
|
283
|
+
required: boolean;
|
|
284
|
+
/** `true` when `required` and nothing chosen or typed. */
|
|
285
|
+
missing: boolean;
|
|
286
|
+
/** The chosen option's title, or the typed text. Empty when unanswered. */
|
|
287
|
+
answer: string;
|
|
288
|
+
answerHidden: boolean;
|
|
289
|
+
/** The free-text escape hatch, per question. */
|
|
290
|
+
freePlaceholder: string;
|
|
291
|
+
freeHidden: boolean;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Nothing to show. The region binds this and collapses. */
|
|
295
|
+
export const NO_WIDGET: WidgetView = {
|
|
296
|
+
kind: "NONE",
|
|
297
|
+
hidden: true,
|
|
298
|
+
text: "", textHidden: true,
|
|
299
|
+
title: "", titleHidden: true,
|
|
300
|
+
bottomText: "", bottomTextHidden: true,
|
|
301
|
+
badge: "", badgeHidden: true,
|
|
302
|
+
imageUrl: "", imageHidden: true,
|
|
303
|
+
primaryLabel: "", primaryHidden: true, primaryUrl: "", primaryRespondText: "",
|
|
304
|
+
secondaryLabel: "", secondaryHidden: true, secondaryUrl: "", secondaryRespondText: "",
|
|
305
|
+
rows: [],
|
|
306
|
+
options: [], maxSelected: 0, limitReached: false, requiresSelection: false,
|
|
307
|
+
limitText: "", limitTextHidden: true,
|
|
308
|
+
primaryDisabled: false, primaryEnabled: true, submitting: false,
|
|
309
|
+
slices: [], values: [], chartIsPie: false, chartIsSeries: false, chartKind: "",
|
|
310
|
+
chartPieHidden: true, chartSeriesHidden: true, chartEmptyHidden: true,
|
|
311
|
+
asyncSpinnerHidden: true, itemsLoadingHidden: true,
|
|
312
|
+
chartEmpty: true, description: "", descriptionHidden: true,
|
|
313
|
+
steps: [], stepsEmpty: true, progress: 0, progressText: "",
|
|
314
|
+
asyncJobId: "", asyncState: "", asyncMessage: "", asyncRunning: false, asyncUntracked: false,
|
|
315
|
+
extraStatusMessages: {},
|
|
316
|
+
query: "", suggestions: [], suggestionsEmpty: true, noMatches: false, selectionType: "",
|
|
317
|
+
questions: [], questionsEmpty: true, answersIncomplete: false,
|
|
318
|
+
answersText: "", answersTextHidden: true,
|
|
319
|
+
providerKey: "", itemsLoading: false, itemsError: "", itemsErrorHidden: true, itemsEmpty: true,
|
|
320
|
+
treeJson: "",
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/** A button's label as the owner should see it.
|
|
324
|
+
*
|
|
325
|
+
* A widget-borne url is model-influenced content and the label beside it is
|
|
326
|
+
* too — nothing makes them agree, which is the shape of a phishing link. This
|
|
327
|
+
* reuses `disclosedLabel` from the transcript's link rows rather than adding a
|
|
328
|
+
* second URL check: that one already survives the two defeats it was
|
|
329
|
+
* hardened against (a backslash ending the authority, and `endsWith` being a
|
|
330
|
+
* string
|
|
331
|
+
* test) and renders an unreadable authority as "unknown site" instead of
|
|
332
|
+
* silently. A second implementation is a second thing to get wrong.
|
|
333
|
+
*
|
|
334
|
+
* A button with no url is left alone: there is no destination to disclose. */
|
|
335
|
+
function discloseButton(disclose: Disclose, label: string, url: string): string {
|
|
336
|
+
return url ? disclose({ label, url }) : label;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** One option, flattened. `selected` is passed in because it is worker state
|
|
340
|
+
* rather than anything the payload said — except on the first build, where the
|
|
341
|
+
* service's `isPreselected` seeds it. */
|
|
342
|
+
function optionRow(
|
|
343
|
+
o: {
|
|
344
|
+
value: string; title: string; subtitle: string; text: string;
|
|
345
|
+
pictureUrl: string; buttonLabel: string; actionUrl: string; listText: string;
|
|
346
|
+
respondText: string;
|
|
347
|
+
},
|
|
348
|
+
i: number,
|
|
349
|
+
selected: boolean,
|
|
350
|
+
disclose: Disclose,
|
|
351
|
+
/** May this option's picture be loaded? A different question from the link
|
|
352
|
+
* disclosure beside it — see `isWixMediaHost`. */
|
|
353
|
+
allowPicture: (url: string) => boolean,
|
|
354
|
+
/** UNKNOWN kinds synthesize a missing `respondText` from the row's own
|
|
355
|
+
* label — see `viewOf`. Known kinds must NOT: an absent template there is
|
|
356
|
+
* a payload bug for a shape that has a real case, and inventing one would
|
|
357
|
+
* hide it. */
|
|
358
|
+
genericFallback = false,
|
|
359
|
+
): WidgetOptionRow {
|
|
360
|
+
// An option's PICTURE is a url off the wire like any other, and this is the
|
|
361
|
+
// one the epic's acceptance list calls out. It is not a label, so there is
|
|
362
|
+
// nothing to disclose INTO — an off-Wix picture is dropped rather than
|
|
363
|
+
// annotated, because a decorative image cannot carry its own provenance and
|
|
364
|
+
// a silently-loaded remote asset is a tracking pixel with extra steps.
|
|
365
|
+
// Judged by `allowPicture` — the MEDIA list — rather than by the link
|
|
366
|
+
// discloser. `static.wixstatic.com` is where every Wix-hosted image lives,
|
|
367
|
+
// so asking the link question about it blanked the pictures the service
|
|
368
|
+
// really sends.
|
|
369
|
+
const pictureUrl = o.pictureUrl && allowPicture(o.pictureUrl) ? o.pictureUrl : "";
|
|
370
|
+
// Via `discloseButton`, NOT `disclose` directly: an option button usually has
|
|
371
|
+
// no url at all (it sends `respondText`), and disclosing an empty url appends
|
|
372
|
+
// "(unknown site)" to a button that goes nowhere. That reads as a phishing
|
|
373
|
+
// warning on the ordinary path and devalues the annotation on the path that
|
|
374
|
+
// needs it.
|
|
375
|
+
const buttonLabel = discloseButton(disclose, o.buttonLabel, o.actionUrl);
|
|
376
|
+
|
|
377
|
+
// An option's two display lines.
|
|
378
|
+
//
|
|
379
|
+
// The service uses `subtitle` and `text` as ALTERNATES, never together:
|
|
380
|
+
// AUTOCOMPLETE and PLANS send `subtitle`, while SINGLE_SELECT, MULTI_SELECT
|
|
381
|
+
// and RADIO_BUTTON send `text`. A row that binds only `subtitle` therefore
|
|
382
|
+
// drops the descriptive line for three of the fourteen kinds — and a
|
|
383
|
+
// RADIO_BUTTON option, which may carry `text` and no `title` at all, renders
|
|
384
|
+
// as an empty row. So fold here rather than in each case's composition: the
|
|
385
|
+
// raw `text` is still exported below for anything that wants both.
|
|
386
|
+
const hasTitle = !!o.title;
|
|
387
|
+
const rowTitle = hasTitle ? o.title : (o.text || o.subtitle);
|
|
388
|
+
const rowSubtitle = hasTitle
|
|
389
|
+
? (o.subtitle || o.text)
|
|
390
|
+
: (o.text && o.subtitle ? o.subtitle : "");
|
|
391
|
+
return {
|
|
392
|
+
id: `o${i}`,
|
|
393
|
+
value: o.value,
|
|
394
|
+
title: rowTitle,
|
|
395
|
+
subtitle: rowSubtitle, subtitleHidden: !rowSubtitle,
|
|
396
|
+
text: o.text, textHidden: !o.text,
|
|
397
|
+
pictureUrl, pictureHidden: !pictureUrl,
|
|
398
|
+
buttonLabel, buttonHidden: !buttonLabel,
|
|
399
|
+
listText: o.listText, listHidden: !o.listText,
|
|
400
|
+
selected,
|
|
401
|
+
respondText: o.respondText
|
|
402
|
+
|| (genericFallback ? (o.title || o.text || o.subtitle || o.value) : ""),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Does this url belong to a host the discloser leaves alone?
|
|
407
|
+
*
|
|
408
|
+
* `disclosedLabel` answers "what should the owner read", which for an on-Wix
|
|
409
|
+
* destination is the label unchanged. Feeding it a sentinel and checking
|
|
410
|
+
* whether it came back untouched is how a caller asks the same question
|
|
411
|
+
* without a second host check — the thing the epic's security section forbids
|
|
412
|
+
* writing twice. */
|
|
413
|
+
function isDisclosedSame(disclose: Disclose, url: string): boolean {
|
|
414
|
+
const probe = "\u0000wix-probe";
|
|
415
|
+
return disclose({ label: probe, url }) === probe;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Re-derive an ASYNC_TOOL widget for a new job state.
|
|
419
|
+
*
|
|
420
|
+
* Separate from `viewOf` because it runs on a LIVE widget rather than a fresh
|
|
421
|
+
* payload — the status messages were parsed once and only the state moves. */
|
|
422
|
+
export function asyncAdvanced(w: WidgetView, state: string): WidgetView {
|
|
423
|
+
const msgs = (w.extraStatusMessages ?? {}) as Record<string, string>;
|
|
424
|
+
const running = state === "initialized" || state === "processing";
|
|
425
|
+
return {
|
|
426
|
+
...w,
|
|
427
|
+
asyncState: state,
|
|
428
|
+
asyncMessage: msgs[state] || w.asyncMessage,
|
|
429
|
+
asyncRunning: running,
|
|
430
|
+
// Recomputed here too: a terminal job keeps its id, so a spinner hidden
|
|
431
|
+
// on trackedness alone would never stop.
|
|
432
|
+
asyncSpinnerHidden: !running,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Turn a normalized widget into the flat view the descriptor binds. */
|
|
437
|
+
/** Where the generic UNKNOWN card is mounted, when the app mounts one.
|
|
438
|
+
*
|
|
439
|
+
* Optional because the second app that mounts Aria has not adopted the
|
|
440
|
+
* subtree — `config.workerSubtreeScreens` is per screen, and a descriptor
|
|
441
|
+
* that is not on the allowlist rejects the node outright. Absent means "the
|
|
442
|
+
* prose card, as before". */
|
|
443
|
+
export interface SubtreeSlot { slotId: string; service: string }
|
|
444
|
+
|
|
445
|
+
export function viewOf(
|
|
446
|
+
w: NormalizedWidget,
|
|
447
|
+
disclose: Disclose,
|
|
448
|
+
subtree?: SubtreeSlot,
|
|
449
|
+
/** Defaults to the link discloser, which is what every caller did before the
|
|
450
|
+
* media list existed — wrong for pictures, but never MORE permissive. */
|
|
451
|
+
allowPicture: (url: string) => boolean = (url) => isDisclosedSame(disclose, url),
|
|
452
|
+
): WidgetView {
|
|
453
|
+
const extra = w.extra as Record<string, unknown>;
|
|
454
|
+
const title = typeof extra.title === "string" ? extra.title : "";
|
|
455
|
+
// Through the SAME media check as an option's picture. Counting `imageUrl`
|
|
456
|
+
// towards renderability made an image-only VISUAL_DISPLAY reachable, and the
|
|
457
|
+
// descriptor binds this path straight into an `Image` — so an off-Wix url
|
|
458
|
+
// here is a remote asset fetched on sight, which is a tracking pixel with
|
|
459
|
+
// extra steps. The option rows have been gated since #2605; this path had
|
|
460
|
+
// never been reachable on its own before, which is exactly why it was easy
|
|
461
|
+
// to widen without noticing.
|
|
462
|
+
const rawImageUrl = typeof extra.imageUrl === "string" ? extra.imageUrl : "";
|
|
463
|
+
const imageUrl = rawImageUrl && allowPicture(rawImageUrl) ? rawImageUrl : "";
|
|
464
|
+
|
|
465
|
+
const sections = Array.isArray(extra.sections) ? (extra.sections as Record<string, unknown>[]) : [];
|
|
466
|
+
const rows: WidgetRow[] = sections.map((sec, i) => {
|
|
467
|
+
const subtitle = typeof sec.subtitle === "string" ? sec.subtitle : "";
|
|
468
|
+
return {
|
|
469
|
+
// A stable id per row. Index-based on purpose: a section has no id of its
|
|
470
|
+
// own, and the list is rebuilt wholesale on every tick, so there is
|
|
471
|
+
// nothing for a content-derived key to preserve.
|
|
472
|
+
id: `s${i}`,
|
|
473
|
+
title: typeof sec.title === "string" ? sec.title : "",
|
|
474
|
+
subtitle,
|
|
475
|
+
subtitleHidden: !subtitle,
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const maxSelected = typeof extra.maxSelected === "number" ? extra.maxSelected : 0;
|
|
480
|
+
// `isPreselected` seeds the selection, so the row's `selected` starts true
|
|
481
|
+
// for exactly the options the service marked.
|
|
482
|
+
const options: WidgetOptionRow[] = w.options.map((o, i) => optionRow(o, i, o.preselected, disclose, allowPicture, w.kind === "UNKNOWN"));
|
|
483
|
+
|
|
484
|
+
// CHART. `pieSlices` is the only shape the catalogue sends, but `chartType`
|
|
485
|
+
// decides the component: a pie is a `DonutChart` at zero thickness, anything
|
|
486
|
+
// else a `Chart` over the same magnitudes.
|
|
487
|
+
const rawSlices = Array.isArray(extra.slices) ? (extra.slices as { label?: unknown; value?: unknown }[]) : [];
|
|
488
|
+
const slices: WidgetSlice[] = rawSlices.map((sl) => ({
|
|
489
|
+
label: typeof sl.label === "string" ? sl.label : "",
|
|
490
|
+
value: typeof sl.value === "number" && Number.isFinite(sl.value) ? sl.value : 0,
|
|
491
|
+
}));
|
|
492
|
+
const chartKind = typeof extra.chartType === "string" ? extra.chartType : "";
|
|
493
|
+
// "doughnut" is the spelling the reference catalogue uses; "donut" is the
|
|
494
|
+
// component's. Accepting only the component's spelling classified the real
|
|
495
|
+
// payload as a SERIES and drew it as bars.
|
|
496
|
+
const isPie = !chartKind || chartKind === "pie" || chartKind === "donut" || chartKind === "doughnut";
|
|
497
|
+
const description = typeof extra.description === "string" ? extra.description : "";
|
|
498
|
+
|
|
499
|
+
// TODO_PLAN. `status` is the service's word; `done` is what `Timeline` draws.
|
|
500
|
+
const rawItems = Array.isArray(extra.items) ? (extra.items as { label?: unknown; status?: unknown }[]) : [];
|
|
501
|
+
const steps: WidgetStep[] = rawItems.map((it, i) => {
|
|
502
|
+
const status = typeof it.status === "string" ? it.status : "";
|
|
503
|
+
return {
|
|
504
|
+
id: `p${i}`,
|
|
505
|
+
title: typeof it.label === "string" ? it.label : "",
|
|
506
|
+
subtitle: status,
|
|
507
|
+
done: /^(done|complete|completed|finished|success)$/i.test(status),
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
const doneCount = steps.filter((p) => p.done).length;
|
|
511
|
+
|
|
512
|
+
// ASYNC_TOOL. Four messages, one state — the descriptor cannot choose, so the
|
|
513
|
+
// choice is made here.
|
|
514
|
+
const msgs = (extra.statusMessages ?? {}) as Record<string, unknown>;
|
|
515
|
+
const asyncJobId = typeof extra.asyncJobId === "string" ? extra.asyncJobId : "";
|
|
516
|
+
const jobState = typeof extra.asyncState === "string" ? extra.asyncState : "initialized";
|
|
517
|
+
const pick = (k: string): string => (typeof msgs[k] === "string" ? (msgs[k] as string) : "");
|
|
518
|
+
const asyncMessage = pick(jobState) || pick("initialized") || pick("processing");
|
|
519
|
+
const asyncRunning = !!asyncJobId && (jobState === "initialized" || jobState === "processing");
|
|
520
|
+
|
|
521
|
+
// QUESTIONNAIRE. Every label arrived wrapped (`{text: …}`) and the normalizer
|
|
522
|
+
// unwrapped them; this only flattens for binding.
|
|
523
|
+
const rawQs = Array.isArray(extra.questions) ? (extra.questions as Record<string, unknown>[]) : [];
|
|
524
|
+
const questions: WidgetQuestion[] = rawQs.map((q, i) => {
|
|
525
|
+
const sub = typeof q.subtitle === "string" ? q.subtitle : "";
|
|
526
|
+
const free = typeof q.freeFormatPlaceholder === "string" ? q.freeFormatPlaceholder : "";
|
|
527
|
+
const required = q.required === true;
|
|
528
|
+
return {
|
|
529
|
+
id: `q${i}`,
|
|
530
|
+
title: typeof q.title === "string" ? q.title : "",
|
|
531
|
+
subtitle: sub, subtitleHidden: !sub,
|
|
532
|
+
type: typeof q.type === "string" ? q.type : "",
|
|
533
|
+
required,
|
|
534
|
+
// Nothing is answered on arrival, so a required question starts missing.
|
|
535
|
+
missing: required,
|
|
536
|
+
answer: "", answerHidden: true,
|
|
537
|
+
freePlaceholder: free, freeHidden: !free,
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const providerKey = typeof extra.providerKey === "string" ? extra.providerKey : "";
|
|
542
|
+
|
|
543
|
+
const primaryUrl = w.primary?.externalUrl ?? "";
|
|
544
|
+
const secondaryUrl = w.secondary?.externalUrl ?? "";
|
|
545
|
+
// An UNKNOWN kind is the one case where a MISSING `respondText` must not
|
|
546
|
+
// make a control inert.
|
|
547
|
+
//
|
|
548
|
+
// `widgetRespond` needs a template and returns the state untouched when
|
|
549
|
+
// there is none — correct for a KNOWN kind, where an absent template is a
|
|
550
|
+
// payload bug for a shape that has a real case. But the generic card mints a
|
|
551
|
+
// button for every option precisely because an option the owner cannot
|
|
552
|
+
// choose is decoration, and a future kind has no reason to carry the
|
|
553
|
+
// template field read today. Without this the generic card rendered
|
|
554
|
+
// perfectly and every control did nothing, which looks exactly like it
|
|
555
|
+
// working.
|
|
556
|
+
//
|
|
557
|
+
// The synthesized template is the label itself, which is what `<SELECTED>`
|
|
558
|
+
// substitution produces for the known kinds anyway.
|
|
559
|
+
const generic = w.kind === "UNKNOWN";
|
|
560
|
+
const primaryRespondText = (w.primary?.respondText ?? "")
|
|
561
|
+
|| (generic ? (w.primary?.label ?? "") : "");
|
|
562
|
+
const secondaryRespondText = (w.secondary?.respondText ?? "")
|
|
563
|
+
|| (generic ? (w.secondary?.label ?? "") : "");
|
|
564
|
+
const primaryLabel = w.primary ? discloseButton(disclose, w.primary.label, primaryUrl) : "";
|
|
565
|
+
const secondaryLabel = w.secondary ? discloseButton(disclose, w.secondary.label, secondaryUrl) : "";
|
|
566
|
+
|
|
567
|
+
// Hoisted out of the object literal below so `renderable` can see it. It
|
|
568
|
+
// was computed inline, so naming it in the guard was a ReferenceError that
|
|
569
|
+
// took the whole `tick` down — the turn never ended and the poll ladder
|
|
570
|
+
// stayed pinned fast. `aria-chat.test.ts` caught it; nothing in the widget
|
|
571
|
+
// suites would have, because they never run a turn.
|
|
572
|
+
const treeJson = subtree && w.kind === "UNKNOWN"
|
|
573
|
+
? composeGenericCard(w, {
|
|
574
|
+
slotId: subtree.slotId,
|
|
575
|
+
service: subtree.service,
|
|
576
|
+
allowPicture,
|
|
577
|
+
})
|
|
578
|
+
: "";
|
|
579
|
+
|
|
580
|
+
// A payload we could not read normalizes to an EMPTY `UNKNOWN`, and an
|
|
581
|
+
// empty card is not nothing: the `Card` keeps its padding and background,
|
|
582
|
+
// so the region rendered as a blank box with a gap where a widget should
|
|
583
|
+
// be. Nothing renderable means nothing rendered.
|
|
584
|
+
// EVERY field the descriptor can draw. `title` (prose and plan cards),
|
|
585
|
+
// `imageUrl` (VISUAL_DISPLAY) and `description` (CHART) were missing, so a
|
|
586
|
+
// title-only upsell or an image-only display — both valid — collapsed even
|
|
587
|
+
// though they had something to show. Hiding a widget that has content is a
|
|
588
|
+
// worse failure than the blank card this guard exists to prevent, so the
|
|
589
|
+
// list has to match what the descriptor renders, not what I remembered.
|
|
590
|
+
const renderable = !!(w.text || w.bottomText || w.badge || title || imageUrl
|
|
591
|
+
|| description || options.length || primaryLabel || secondaryLabel
|
|
592
|
+
|| sections.length || slices.length || steps.length || questions.length
|
|
593
|
+
|| asyncMessage || treeJson);
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
kind: w.kind,
|
|
597
|
+
hidden: !renderable,
|
|
598
|
+
options,
|
|
599
|
+
maxSelected,
|
|
600
|
+
limitReached: maxSelected > 0 && options.filter((o) => o.selected).length >= maxSelected,
|
|
601
|
+
limitText: "", limitTextHidden: true,
|
|
602
|
+
requiresSelection: !!w.primary?.disableIfEmpty,
|
|
603
|
+
// A widget arrives with nothing chosen unless the service preselected, so
|
|
604
|
+
// a `disableIfNothingSelected` primary starts disabled.
|
|
605
|
+
primaryDisabled: !!w.primary?.disableIfEmpty && !options.some((o) => o.selected),
|
|
606
|
+
primaryEnabled: !(!!w.primary?.disableIfEmpty && !options.some((o) => o.selected)),
|
|
607
|
+
slices,
|
|
608
|
+
values: slices.map((sl) => sl.value),
|
|
609
|
+
chartIsPie: w.kind === "CHART" && isPie && slices.length > 0,
|
|
610
|
+
chartIsSeries: w.kind === "CHART" && !isPie && slices.length > 0,
|
|
611
|
+
chartPieHidden: !(w.kind === "CHART" && isPie && slices.length > 0),
|
|
612
|
+
chartSeriesHidden: !(w.kind === "CHART" && !isPie && slices.length > 0),
|
|
613
|
+
chartKind,
|
|
614
|
+
chartEmpty: w.kind === "CHART" && slices.length === 0,
|
|
615
|
+
chartEmptyHidden: !(w.kind === "CHART" && slices.length === 0),
|
|
616
|
+
description, descriptionHidden: !description,
|
|
617
|
+
steps,
|
|
618
|
+
stepsEmpty: w.kind === "TODO_PLAN" && steps.length === 0,
|
|
619
|
+
progress: steps.length ? doneCount / steps.length : 0,
|
|
620
|
+
progressText: steps.length ? `${doneCount} of ${steps.length} done` : "",
|
|
621
|
+
asyncJobId,
|
|
622
|
+
asyncState: asyncJobId ? jobState : "",
|
|
623
|
+
asyncMessage,
|
|
624
|
+
asyncRunning,
|
|
625
|
+
// A job the service never named. The status messages still say something
|
|
626
|
+
// useful, so this renders them — it just never spins and never polls.
|
|
627
|
+
asyncUntracked: w.kind === "ASYNC_TOOL" && !asyncJobId,
|
|
628
|
+
// The spinner follows RUNNING, not trackedness.
|
|
629
|
+
asyncSpinnerHidden: !asyncRunning,
|
|
630
|
+
query: "",
|
|
631
|
+
// Nothing typed yet means every option is a suggestion.
|
|
632
|
+
suggestions: options,
|
|
633
|
+
suggestionsEmpty: options.length === 0,
|
|
634
|
+
noMatches: false,
|
|
635
|
+
selectionType: typeof extra.selectionType === "string" ? extra.selectionType : "",
|
|
636
|
+
questions,
|
|
637
|
+
questionsEmpty: w.kind === "QUESTIONNAIRE" && questions.length === 0,
|
|
638
|
+
answersIncomplete: questions.some((q) => q.missing),
|
|
639
|
+
answersText: "", answersTextHidden: true,
|
|
640
|
+
providerKey,
|
|
641
|
+
// An ITEMS_SELECTION arrives with NO rows — they come from the provider —
|
|
642
|
+
// so it starts loading rather than starting empty.
|
|
643
|
+
itemsLoading: w.kind === "ITEMS_SELECTION" && !!providerKey,
|
|
644
|
+
itemsLoadingHidden: !(w.kind === "ITEMS_SELECTION" && !!providerKey),
|
|
645
|
+
itemsError: "", itemsErrorHidden: true,
|
|
646
|
+
itemsEmpty: w.kind === "ITEMS_SELECTION" && options.length === 0,
|
|
647
|
+
// Only for a kind we do not know. A known kind has a real case, and RFC
|
|
648
|
+
// 0046 sends enumerable variants to `FragmentSlot`.
|
|
649
|
+
treeJson,
|
|
650
|
+
extraStatusMessages: Object.fromEntries(
|
|
651
|
+
Object.entries(msgs).filter((kv): kv is [string, string] => typeof kv[1] === "string"),
|
|
652
|
+
),
|
|
653
|
+
submitting: false,
|
|
654
|
+
text: w.text, textHidden: !w.text,
|
|
655
|
+
title, titleHidden: !title,
|
|
656
|
+
bottomText: w.bottomText, bottomTextHidden: !w.bottomText,
|
|
657
|
+
badge: w.badge, badgeHidden: !w.badge,
|
|
658
|
+
imageUrl, imageHidden: !imageUrl,
|
|
659
|
+
// Shown only when there is a LABEL to read and a DESTINATION to go to.
|
|
660
|
+
//
|
|
661
|
+
// The label half is obvious: the service does send `{mobileAction:{…}}`
|
|
662
|
+
// with nothing written on it, and a control with an empty title is a tap
|
|
663
|
+
// target the owner cannot read.
|
|
664
|
+
//
|
|
665
|
+
// The destination half is a scope decision, and a deliberate visible gap. A
|
|
666
|
+
// button whose only payload is `respondText` needs the send-back loop, which
|
|
667
|
+
// is #2606; until that lands, painting it would put up a control that does
|
|
668
|
+
// nothing when tapped. This repo's own rule for the Aria header is "no dead
|
|
669
|
+
// controls", and a button that looks live and is not is worse than one that
|
|
670
|
+
// is not there yet. #2606 removes the `&& url` and the buttons appear.
|
|
671
|
+
// #2605 hid a button that had nothing to DO. Now that `respondText` can be
|
|
672
|
+
// sent, having something to SAY counts too — which is the `&& url` that
|
|
673
|
+
// issue said this one would remove.
|
|
674
|
+
primaryLabel, primaryHidden: !(primaryLabel && (primaryUrl || primaryRespondText)), primaryUrl,
|
|
675
|
+
primaryRespondText,
|
|
676
|
+
secondaryLabel, secondaryHidden: !(secondaryLabel && (secondaryUrl || secondaryRespondText)), secondaryUrl,
|
|
677
|
+
secondaryRespondText,
|
|
678
|
+
rows,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** The widget the screen should be showing, read off the server transcript.
|
|
683
|
+
*
|
|
684
|
+
* The LAST assistant turn's last envelope, and nothing else. Two decisions
|
|
685
|
+
* behind that, both from the epic:
|
|
686
|
+
*
|
|
687
|
+
* - **One at a time.** The assistant asks one question per turn, and
|
|
688
|
+
* `FragmentSlot` binds one case path. A widget scrolled into history keeps
|
|
689
|
+
* its prose (#2603 already put that in the bubble) and loses its controls,
|
|
690
|
+
* which is correct: a stale questionnaire you can still submit is a bug.
|
|
691
|
+
* - **Assistant turns only.** A user's own text is literal — scanning it would
|
|
692
|
+
* let an owner who typed `<<…>>` inject a widget into their own chat.
|
|
693
|
+
*
|
|
694
|
+
* Answers `NO_WIDGET` for an empty transcript, so a new chat collapses the
|
|
695
|
+
* region without anything having to remember to clear it. */
|
|
696
|
+
export function activeWidgetOf(
|
|
697
|
+
msgs: WidgetSourceMessage[],
|
|
698
|
+
disclose: Disclose,
|
|
699
|
+
subtree?: SubtreeSlot,
|
|
700
|
+
allowPicture?: (url: string) => boolean,
|
|
701
|
+
): WidgetView {
|
|
702
|
+
// ONLY the newest message is consulted. This used to walk backwards past
|
|
703
|
+
// `USER` rows and empty assistant placeholders looking for a widget, which
|
|
704
|
+
// is wrong in the one state __HIMI_APP_NAME__ is in most often: between persisting
|
|
705
|
+
// the owner's message and the reply arriving, the newest row IS the owner's
|
|
706
|
+
// message — so the scan skipped it, found the PREVIOUS assistant turn, and
|
|
707
|
+
// repainted the controls for the question that had just been superseded.
|
|
708
|
+
// `send` deliberately clears the widget; the very next poll put it back.
|
|
709
|
+
//
|
|
710
|
+
// A widget belongs to the turn that asked it. If the newest turn is the
|
|
711
|
+
// owner's, or is a placeholder with nothing in it yet, then nothing is being
|
|
712
|
+
// asked right now — and that is what NO_WIDGET means.
|
|
713
|
+
const m = msgs[msgs.length - 1];
|
|
714
|
+
if (!m || (m.role || "ASSISTANT") === "USER") return NO_WIDGET;
|
|
715
|
+
const raw = typeof m.content === "string" ? m.content : "";
|
|
716
|
+
if (!raw) return NO_WIDGET;
|
|
717
|
+
const found = widgetsOfParts(scanEnvelopes(raw, 0, true).parts);
|
|
718
|
+
if (!found.length) return NO_WIDGET;
|
|
719
|
+
// A turn with several envelopes shows the LAST, matching "one at a time"
|
|
720
|
+
// and the reading order — the final ask is the live one.
|
|
721
|
+
return viewOf(normalizeWidget(found[found.length - 1]), disclose, subtree, allowPicture);
|
|
722
|
+
}
|