@wix/himalaya-cli 0.1037.0 → 0.1039.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/stdlib/wix-sdk/genie-chat/src/index.d.ts +119 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-genie-chat.mjs +51 -1
- package/dist/cli.mjs +2523 -1327
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/designing-himi-screens.md +3 -2
- package/guides/getting-started.md +3 -2
- package/guides/headless-loop.md +11 -3
- package/guides/himi-skill.md +381 -0
- package/guides/himi-test.md +66 -1
- package/guides/index.json +4 -0
- package/guides/recipe-dynamic-list.md +3 -3
- package/guides/recipe-form-submit.md +3 -3
- package/guides/recipe-onboarding.md +3 -3
- package/guides/recipe-search.md +3 -3
- package/guides/sdk-reference.json +74 -2
- package/guides/skill.md +122 -0
- package/guides/trim-a-template.md +5 -0
- package/guides/wix-site-to-himi-app.md +4 -2
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +58 -22
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +144 -29
- package/templates/beauty-salon-owner/dev/index.ts +2148 -1262
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/branded-lite/dev/index.ts +61 -25
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +144 -29
- package/templates/community-club-owner/dev/index.ts +2151 -1265
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/community-club-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/events-meetups-owner/dev/index.ts +2151 -1265
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/events-meetups-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/gym-fitness/dev/index.ts +58 -22
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +144 -29
- package/templates/gym-fitness-owner/dev/index.ts +2148 -1262
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/nonprofit-cause-owner/dev/index.ts +2148 -1262
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/owner-lite/dev/index.ts +2145 -1259
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/owner-lite/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/podcast-creator-owner/dev/index.ts +2151 -1265
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/portfolio-creative-owner/dev/index.ts +2148 -1262
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/restaurant-owner/dev/index.ts +2148 -1262
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/restaurant-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/retail-store/dev/index.ts +1 -1
- package/templates/retail-store-owner/dev/index.ts +2151 -1265
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/retail-store-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
- package/templates/yoga-studio/dev/index.ts +58 -22
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +144 -29
- package/templates/yoga-studio-owner/dev/index.ts +2148 -1262
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +338 -26
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-mentions.ts +202 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Mentions for the Aria chat: let the owner point at a product, a contact, a
|
|
2
|
+
// coupon… and have the reference travel WITH the turn, beside the text.
|
|
3
|
+
//
|
|
4
|
+
// The shape follows the native Aria app (iOS + Android), which talks to the same
|
|
5
|
+
// Wix services:
|
|
6
|
+
//
|
|
7
|
+
// - The picker is two levels: the item PROVIDERS the site has installed (the
|
|
8
|
+
// categories), then one provider's ITEMS, searchable.
|
|
9
|
+
// - Only an ALLOWLISTED provider is offered, under a fixed label. The service
|
|
10
|
+
// lists every provider an installed app registers, and most of them are not
|
|
11
|
+
// things an owner talks to an assistant about.
|
|
12
|
+
// - A pick becomes a chip above the composer; the message text carries NO
|
|
13
|
+
// token. On send, each chip becomes a `global` reference whose `data` names
|
|
14
|
+
// the item and its category — see `mentionReference` in the chat client.
|
|
15
|
+
//
|
|
16
|
+
// This file is pure: the catalogue, the model the chat core keeps, and the flat
|
|
17
|
+
// view the descriptor binds. The network calls and the state plumbing live in
|
|
18
|
+
// aria-chat.ts beside everything else that talks to the chat-service.
|
|
19
|
+
//
|
|
20
|
+
// Copied verbatim into every generated starter app, so it explains itself
|
|
21
|
+
// without pointing at anything only this repo has.
|
|
22
|
+
|
|
23
|
+
/** One allowlisted category: the label shown AND sent (the model reads the
|
|
24
|
+
* English label, so it is not localized), and whether its items have
|
|
25
|
+
* pictures worth showing. */
|
|
26
|
+
export interface MentionCategory { label: string; showsImage: boolean }
|
|
27
|
+
|
|
28
|
+
/** The providers a picker offers, keyed by the provider's `providerName`
|
|
29
|
+
* lowercased. Transcribed from the native Aria app's `MentionProviderCatalog`
|
|
30
|
+
* — the same fifteen entries, labels and picture flags — so both clients
|
|
31
|
+
* offer the same categories and send the same `type`. */
|
|
32
|
+
export const MENTION_CATALOG: Readonly<Record<string, MentionCategory>> = {
|
|
33
|
+
"contacts": { label: "Contacts", showsImage: true },
|
|
34
|
+
"product": { label: "Products", showsImage: true },
|
|
35
|
+
"bookings services": { label: "Bookings Services", showsImage: true },
|
|
36
|
+
"categories": { label: "Product Categories", showsImage: false },
|
|
37
|
+
"cms": { label: "CMS", showsImage: false },
|
|
38
|
+
"wix forms": { label: "Forms", showsImage: false },
|
|
39
|
+
"pay links": { label: "Pay Links", showsImage: false },
|
|
40
|
+
"coupons": { label: "Coupons", showsImage: false },
|
|
41
|
+
"menu item": { label: "Menu Items", showsImage: true },
|
|
42
|
+
"wix blog": { label: "Blog Posts", showsImage: true },
|
|
43
|
+
"campaign selection": { label: "Email Marketing Campaigns", showsImage: true },
|
|
44
|
+
"pricing plans": { label: "Pricing Plans", showsImage: true },
|
|
45
|
+
"wix table reservation experience": { label: "Experiences", showsImage: true },
|
|
46
|
+
"onlineprogramsprovider": { label: "Online Programs", showsImage: true },
|
|
47
|
+
"wix events": { label: "Events", showsImage: true },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** An installed, allowlisted provider. */
|
|
51
|
+
export interface MentionProvider {
|
|
52
|
+
key: string;
|
|
53
|
+
label: string;
|
|
54
|
+
showsImage: boolean;
|
|
55
|
+
paging: "CURSOR" | "OFFSET" | "NONE";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The allowlisted subset of what the site has installed, one entry per
|
|
59
|
+
* category label (two installed apps can register the same category — the
|
|
60
|
+
* first wins, as natively), sorted by label so the order does not depend on
|
|
61
|
+
* the order the service happened to list them in. */
|
|
62
|
+
export function allowlisted(
|
|
63
|
+
installed: Array<{ key: string; name: string; paging: "CURSOR" | "OFFSET" | "NONE" }>,
|
|
64
|
+
): MentionProvider[] {
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
const out: MentionProvider[] = [];
|
|
67
|
+
for (const p of installed) {
|
|
68
|
+
const cat = MENTION_CATALOG[p.name.toLowerCase()];
|
|
69
|
+
if (!cat || seen.has(cat.label)) continue;
|
|
70
|
+
seen.add(cat.label);
|
|
71
|
+
out.push({ key: p.key, label: cat.label, showsImage: cat.showsImage, paging: p.paging });
|
|
72
|
+
}
|
|
73
|
+
return out.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One item row in the picker. */
|
|
77
|
+
export interface MentionItemRow {
|
|
78
|
+
/** Row id — positional, because an item id is the service's and is not
|
|
79
|
+
* guaranteed to be safe inside a component id. */
|
|
80
|
+
id: string;
|
|
81
|
+
/** The item id the reference names, and what a tap on the row carries: a
|
|
82
|
+
* newer search can re-order the rows between paint and tap, so a
|
|
83
|
+
* positional id would pick the wrong item. */
|
|
84
|
+
itemId: string;
|
|
85
|
+
title: string;
|
|
86
|
+
subtitle: string;
|
|
87
|
+
subtitleHidden: boolean;
|
|
88
|
+
pictureUrl: string;
|
|
89
|
+
pictureHidden: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A pick, waiting above the composer to be sent with the next turn. */
|
|
93
|
+
export interface MentionPick {
|
|
94
|
+
/** Chip id, minted by the core. */
|
|
95
|
+
id: string;
|
|
96
|
+
/** Provider + item identify a pick; an item id is only unique within its
|
|
97
|
+
* provider, so picking the same item twice keeps one chip. */
|
|
98
|
+
providerKey: string;
|
|
99
|
+
itemId: string;
|
|
100
|
+
/** The item's name. */
|
|
101
|
+
label: string;
|
|
102
|
+
/** The category's English label — what the reference's `data.type` carries. */
|
|
103
|
+
category: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Everything the picker knows. Kept by the chat core OUTSIDE the reducer
|
|
107
|
+
* state — see `mentionModel` there for why. */
|
|
108
|
+
export interface MentionModel {
|
|
109
|
+
open: boolean;
|
|
110
|
+
/** The business the providers and picks belong to. A switch drops both: a
|
|
111
|
+
* product of the previous business is not something to send to this one. */
|
|
112
|
+
site: string;
|
|
113
|
+
/** Null until listed for `site`. */
|
|
114
|
+
providers: MentionProvider[] | null;
|
|
115
|
+
providersLoading: boolean;
|
|
116
|
+
providersFailed: string;
|
|
117
|
+
/** The provider being browsed, or null at the category level. */
|
|
118
|
+
provider: MentionProvider | null;
|
|
119
|
+
query: string;
|
|
120
|
+
items: MentionItemRow[];
|
|
121
|
+
itemsLoading: boolean;
|
|
122
|
+
itemsFailed: string;
|
|
123
|
+
picks: MentionPick[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function emptyMentionModel(): MentionModel {
|
|
127
|
+
return {
|
|
128
|
+
open: false, site: "", providers: null, providersLoading: false, providersFailed: "",
|
|
129
|
+
provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "", picks: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Worker-emitted mention copy. */
|
|
134
|
+
export interface MentionLabels {
|
|
135
|
+
/** Panel title at the category level. */
|
|
136
|
+
title: string;
|
|
137
|
+
/** No allowlisted provider is installed. */
|
|
138
|
+
noProviders: string;
|
|
139
|
+
/** A provider answered with nothing (or nothing matching). */
|
|
140
|
+
noItems: string;
|
|
141
|
+
/** The provider list could not be fetched. */
|
|
142
|
+
providersFailed: string;
|
|
143
|
+
/** This host cannot reach the provider service at all (the web preview). */
|
|
144
|
+
unavailable: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The flat shape the descriptor binds, in the polarity the renderers want
|
|
148
|
+
* (`hiddenPath` HIDES when true). Derived from the model and nothing else. */
|
|
149
|
+
export interface MentionView {
|
|
150
|
+
/** The whole panel. */
|
|
151
|
+
hidden: boolean;
|
|
152
|
+
/** The "@ Mention" entry control. Hidden while the panel is open — one way
|
|
153
|
+
* in, one way out. */
|
|
154
|
+
openHidden: boolean;
|
|
155
|
+
title: string;
|
|
156
|
+
backHidden: boolean;
|
|
157
|
+
providers: Array<{ id: string; key: string; label: string }>;
|
|
158
|
+
providersHidden: boolean;
|
|
159
|
+
/** The search field + item list (the second level). */
|
|
160
|
+
itemsHidden: boolean;
|
|
161
|
+
items: MentionItemRow[];
|
|
162
|
+
loadingHidden: boolean;
|
|
163
|
+
error: string;
|
|
164
|
+
errorHidden: boolean;
|
|
165
|
+
empty: string;
|
|
166
|
+
emptyHidden: boolean;
|
|
167
|
+
/** The picks, as chips above the composer. */
|
|
168
|
+
chips: Array<{ id: string; label: string }>;
|
|
169
|
+
chipsHidden: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function mentionView(m: MentionModel, labels: MentionLabels): MentionView {
|
|
173
|
+
const atItems = !!m.provider;
|
|
174
|
+
const loading = atItems ? m.itemsLoading : m.providersLoading;
|
|
175
|
+
const error = atItems ? m.itemsFailed : m.providersFailed;
|
|
176
|
+
const providers = m.providers ?? [];
|
|
177
|
+
// "Nothing here" is only said once an answer has actually come back — a
|
|
178
|
+
// list still loading, or one that failed, is not an empty one.
|
|
179
|
+
const empty = !m.open || loading || error ? ""
|
|
180
|
+
: atItems ? (m.items.length ? "" : labels.noItems)
|
|
181
|
+
: (m.providers !== null && providers.length === 0 ? labels.noProviders : "");
|
|
182
|
+
return {
|
|
183
|
+
hidden: !m.open,
|
|
184
|
+
openHidden: m.open,
|
|
185
|
+
title: atItems ? m.provider!.label : labels.title,
|
|
186
|
+
backHidden: !atItems,
|
|
187
|
+
providers: providers.map((p) => ({ id: p.key, key: p.key, label: p.label })),
|
|
188
|
+
providersHidden: !m.open || atItems || providers.length === 0,
|
|
189
|
+
itemsHidden: !m.open || !atItems,
|
|
190
|
+
items: atItems ? m.items : [],
|
|
191
|
+
loadingHidden: !m.open || !loading,
|
|
192
|
+
error: m.open ? error : "",
|
|
193
|
+
errorHidden: !m.open || !error,
|
|
194
|
+
empty,
|
|
195
|
+
emptyHidden: !empty,
|
|
196
|
+
chips: m.picks.map((p) => ({ id: p.id, label: p.label })),
|
|
197
|
+
chipsHidden: m.picks.length === 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** How many item rows one query asks for — the native page size. */
|
|
202
|
+
export const MENTION_PAGE_SIZE = 30;
|
|
@@ -201,6 +201,18 @@ const core = createAriaChat({
|
|
|
201
201
|
// English. Says no "sign in": the owner already is, and the missing piece is
|
|
202
202
|
// the app's binding, so there is no action for them to take.
|
|
203
203
|
unavailable: () => copyParam("ownerAria.unavailable", "Aria can't reach your Wix account from here."),
|
|
204
|
+
// The mention picker. Through copyParam for the same reason as the rest.
|
|
205
|
+
// The CATEGORY labels are not here, deliberately: they are what the
|
|
206
|
+
// reference's `data.type` carries to the model, so they stay English.
|
|
207
|
+
mentionTitle: () => copyParam("ownerAria.mention.title", "Mention"),
|
|
208
|
+
mentionNoProviders: () => copyParam("ownerAria.mention.noProviders", "There's nothing on this site to mention yet."),
|
|
209
|
+
mentionNoItems: () => copyParam("ownerAria.mention.noItems", "Nothing found."),
|
|
210
|
+
mentionFailed: () => copyParam("ownerAria.mention.failed", "Couldn't load what you can mention."),
|
|
211
|
+
mentionUnavailable: () => copyParam("ownerAria.mention.unavailable", "Mentions aren't available here."),
|
|
212
|
+
// Takes the CATEGORY label, which stays English (see above) — only the
|
|
213
|
+
// sentence around it is localized.
|
|
214
|
+
mentionItemsFailed: (category: string) =>
|
|
215
|
+
copyTemplate("ownerAria.mention.itemsFailed", "Couldn't load {category}.", { category }),
|
|
204
216
|
attachUploading: () => copyParam("ownerAria.attach.uploading", "Uploading…"),
|
|
205
217
|
attachFailed: () => copyParam("ownerAria.attach.failed", "Couldn't upload"),
|
|
206
218
|
attachTooMany: (max) => copyTemplate("ownerAria.attach.tooMany", "You can attach up to {max} photos.", { max }),
|
|
@@ -281,6 +293,13 @@ const svc = defineService<AriaChatState, {
|
|
|
281
293
|
widgetPick: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
282
294
|
widgetQuery: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
283
295
|
widgetAnswer: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
296
|
+
mentionOpen: (s: AriaChatState) => Promise<AriaChatState | undefined>;
|
|
297
|
+
mentionClose: (s: AriaChatState) => AriaChatState;
|
|
298
|
+
mentionBack: (s: AriaChatState) => AriaChatState;
|
|
299
|
+
mentionProvider: (s: AriaChatState, arg?: AriaActionArg) => Promise<AriaChatState | undefined>;
|
|
300
|
+
mentionQuery: (s: AriaChatState, arg?: AriaActionArg) => Promise<AriaChatState | undefined>;
|
|
301
|
+
mentionPick: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
302
|
+
mentionRemove: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
284
303
|
attach: (s: AriaChatState, arg?: AriaActionArg) => Promise<AriaChatState>;
|
|
285
304
|
attachmentSettled: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
286
305
|
removeAttachment: (s: AriaChatState, arg?: AriaActionArg) => AriaChatState;
|
|
@@ -321,6 +340,16 @@ const svc = defineService<AriaChatState, {
|
|
|
321
340
|
widgetPick: core.widgetPick,
|
|
322
341
|
widgetQuery: core.widgetQuery,
|
|
323
342
|
widgetAnswer: core.widgetAnswer,
|
|
343
|
+
// Mentions: the "@ Mention" control, the two-level picker and the
|
|
344
|
+
// chips above the composer. Every one is named by the descriptor, so every
|
|
345
|
+
// one has to be exported here — see the note on the picker family above.
|
|
346
|
+
mentionOpen: core.mentionOpen,
|
|
347
|
+
mentionClose: core.mentionClose,
|
|
348
|
+
mentionBack: core.mentionBack,
|
|
349
|
+
mentionProvider: core.mentionProvider,
|
|
350
|
+
mentionQuery: core.mentionQuery,
|
|
351
|
+
mentionPick: core.mentionPick,
|
|
352
|
+
mentionRemove: core.mentionRemove,
|
|
324
353
|
// The composer's photos. `attachmentSettled` is dispatched by the upload
|
|
325
354
|
// itself (see `attachments.settled` above), never by the descriptor.
|
|
326
355
|
attach: core.attach,
|
|
@@ -53,6 +53,11 @@ interface AskState {
|
|
|
53
53
|
results: Record<string, QueryResult>;
|
|
54
54
|
/** The validated-on-arrival encoded component tree. */
|
|
55
55
|
treeJson: string;
|
|
56
|
+
/** always "ready" once this worker exists. The slot's fallback keys a
|
|
57
|
+
* FragmentSlot on it: an unresolved path (worker not hydrated yet) takes the
|
|
58
|
+
* default loading case, so only a tree that really failed validation shows
|
|
59
|
+
* "Can't show that answer". */
|
|
60
|
+
phase: "ready";
|
|
56
61
|
/** Authored-UI visibility flags. */
|
|
57
62
|
isWorking: boolean;
|
|
58
63
|
notWorking: boolean;
|
|
@@ -130,6 +135,7 @@ const initialState: AskState = {
|
|
|
130
135
|
repairs: "",
|
|
131
136
|
results: {},
|
|
132
137
|
treeJson: enc(STARTER),
|
|
138
|
+
phase: "ready",
|
|
133
139
|
isWorking: false,
|
|
134
140
|
notWorking: true,
|
|
135
141
|
recents: [],
|
|
@@ -19,6 +19,11 @@ import { stashCopy, copyParam, copyTemplate } from '../_shared/himi-copy.js';
|
|
|
19
19
|
import { DASHBOARD_PATHS, deriveManage, parseAreas, recentAreas, rememberArea, withMetrics, type ManageArea, type Metric } from './logic.js';
|
|
20
20
|
|
|
21
21
|
interface State {
|
|
22
|
+
/** `loading` until the descriptor has delivered the areas, then
|
|
23
|
+
* `ready`. The descriptor keys a FragmentSlot on it whose DEFAULT case is the
|
|
24
|
+
* skeleton, so an unresolved path (worker not booted yet) is loading too —
|
|
25
|
+
* never the filtered-empty "No matching business areas" state. */
|
|
26
|
+
status: 'loading' | 'ready';
|
|
22
27
|
areas: ManageArea[];
|
|
23
28
|
results: ManageArea[];
|
|
24
29
|
sections: Array<{ id: string; label: string; count: string; areas: ManageArea[] }>;
|
|
@@ -37,6 +42,17 @@ interface State {
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
const PAGE = 50;
|
|
45
|
+
|
|
46
|
+
/** The "All" chip label — ONE function for the initial state and every derive
|
|
47
|
+
* after load, so the chip never changes text when the areas arrive. The copy
|
|
48
|
+
* pack only reaches the worker with onAppear's `load`, after boot, so the
|
|
49
|
+
* descriptor also bakes the pack's label into the screen input (present at
|
|
50
|
+
* boot); the stashed pack wins once it exists, and both pass the same trim. */
|
|
51
|
+
function allLabel(): string {
|
|
52
|
+
const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input?.allLabel;
|
|
53
|
+
const bootLabel = typeof input === 'string' && input.trim() ? input : 'All';
|
|
54
|
+
return copyParam('ownerManage.all', bootLabel).trim() || 'All';
|
|
55
|
+
}
|
|
40
56
|
const read = (arg: unknown) => arg && typeof arg === 'object' ? arg as Record<string, unknown> : {};
|
|
41
57
|
const str = (arg: unknown, field: string) => String(read(arg)[field] ?? '');
|
|
42
58
|
const value = (record: unknown, path: string): unknown => path.split('.').reduce<unknown>((current, key) =>
|
|
@@ -168,13 +184,38 @@ function derive(state: State, areas = state.areas): State {
|
|
|
168
184
|
state.query,
|
|
169
185
|
state.group,
|
|
170
186
|
copyParam('ownerManage.areas', 'business areas'),
|
|
171
|
-
|
|
187
|
+
allLabel(),
|
|
172
188
|
);
|
|
173
189
|
const recents = recentAreas(recentIds, areas);
|
|
174
|
-
|
|
190
|
+
// "No matching" is a statement about a filter over KNOWN areas. Before the
|
|
191
|
+
// areas arrive — or when this app provisions none at all — there is nothing a
|
|
192
|
+
// filter could have excluded, so "Clear filters" would be a lie.
|
|
193
|
+
const noEmpty = next.noEmpty || state.status !== 'ready' || areas.length === 0;
|
|
194
|
+
return { ...state, ...next, noEmpty, recents, hasRecents: recents.length > 0, noRecents: recents.length === 0 };
|
|
175
195
|
}
|
|
176
196
|
|
|
197
|
+
// Loads are not serialized: on a warm reappearance the host's bare refresh and
|
|
198
|
+
// the descriptor's onAppear can both be in flight, and whichever settles last
|
|
199
|
+
// replaces the whole state. Each load takes a generation, and a superseded one
|
|
200
|
+
// never applies its own stale snapshot: it returns the newest load's result if
|
|
201
|
+
// that has settled, and otherwise the newest load's still-loading start state.
|
|
202
|
+
let loadGeneration = 0;
|
|
203
|
+
let newestStarted: State | undefined;
|
|
204
|
+
let newestSettled: { generation: number; state: State } | undefined;
|
|
205
|
+
|
|
177
206
|
async function loadMetrics(state: State): Promise<State> {
|
|
207
|
+
const generation = ++loadGeneration;
|
|
208
|
+
newestStarted = state;
|
|
209
|
+
const settled = await readMetrics(state);
|
|
210
|
+
if (generation === loadGeneration) {
|
|
211
|
+
newestSettled = { generation, state: settled };
|
|
212
|
+
return settled;
|
|
213
|
+
}
|
|
214
|
+
if (newestSettled && newestSettled.generation === loadGeneration) return newestSettled.state;
|
|
215
|
+
return newestStarted ?? settled;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readMetrics(state: State): Promise<State> {
|
|
178
219
|
const keys = [...new Set(state.areas.map(area => area.metricKey))];
|
|
179
220
|
const settled = await Promise.allSettled(keys.map(metricFor));
|
|
180
221
|
const metrics: Record<string, Metric | undefined> = {};
|
|
@@ -220,17 +261,33 @@ const svc = defineService<State, {
|
|
|
220
261
|
handoff: (s: State, arg?: unknown) => Promise<State>;
|
|
221
262
|
}>('himiOwnerLiteManage', {
|
|
222
263
|
state: {
|
|
223
|
-
|
|
264
|
+
status: 'loading',
|
|
265
|
+
areas: [], results: [], sections: [], recents: [], query: '', group: 'all',
|
|
266
|
+
// 'All' is always a valid filter, so the chip row paints from the first frame
|
|
267
|
+
// instead of appearing (and shifting the layout) when the areas arrive.
|
|
268
|
+
groups: [{ id: 'all', label: allLabel() }], summary: '', noEmpty: true,
|
|
224
269
|
hasRecents: false, noRecents: true, isLoading: true, notLoading: false, summaryNote: '', handoffError: '',
|
|
225
270
|
},
|
|
226
271
|
actions: {
|
|
227
272
|
load: async (state, arg) => {
|
|
228
273
|
stashCopy(arg);
|
|
274
|
+
// A `load` with no areas comes from the host, not the descriptor: the
|
|
275
|
+
// warm-worker refresh lifecycle calls it bare. Before the areas are known
|
|
276
|
+
// it must not settle (that derived "0 business areas" and painted the
|
|
277
|
+
// filtered-empty state); once they are, it re-reads their summaries.
|
|
278
|
+
if (!('areas' in read(arg))) {
|
|
279
|
+
return state.status === 'ready'
|
|
280
|
+
? loadMetrics(derive({ ...state, isLoading: true, notLoading: false, summaryNote: '', handoffError: '' }))
|
|
281
|
+
: state;
|
|
282
|
+
}
|
|
229
283
|
const parsed = (() => { try { return JSON.parse(str(arg, 'areas') || '[]'); } catch { return []; } })();
|
|
230
284
|
const areas = parseAreas(parsed);
|
|
231
|
-
return loadMetrics(derive({ ...state, areas, isLoading: true, notLoading: false, summaryNote: '', handoffError: '' }, areas));
|
|
285
|
+
return loadMetrics(derive({ ...state, status: 'ready', areas, isLoading: true, notLoading: false, summaryNote: '', handoffError: '' }, areas));
|
|
232
286
|
},
|
|
233
|
-
|
|
287
|
+
// Refresh re-reads summaries for KNOWN areas. Before the descriptor has
|
|
288
|
+
// delivered them there is nothing to refresh, and settling here would open
|
|
289
|
+
// the skeleton gate over an empty hub.
|
|
290
|
+
refresh: async state => state.status !== 'ready' ? state : loadMetrics(derive({ ...state, isLoading: true, notLoading: false, summaryNote: '', handoffError: '' })),
|
|
234
291
|
search: (state, arg) => derive({ ...state, query: str(arg, 'value').slice(0, 256) }),
|
|
235
292
|
filter: (state, arg) => {
|
|
236
293
|
const ids = read(arg).selectedIds;
|
|
@@ -262,5 +319,6 @@ const svc = defineService<State, {
|
|
|
262
319
|
},
|
|
263
320
|
});
|
|
264
321
|
|
|
265
|
-
svc.load()
|
|
322
|
+
// No boot `svc.load()`: the areas ride the descriptor's onAppear, and a boot load
|
|
323
|
+
// without them painted "0 business areas" / "No matching" before they arrived.
|
|
266
324
|
export { svc };
|