@wix/himalaya-cli 0.1633.0 → 0.1635.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-module.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +22 -10
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-wire.d.ts +16 -6
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments.d.ts +11 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +6 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +10 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-feed-viewer/src/index.d.ts +10 -6
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-v1/src/index.d.ts +34 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +84 -16
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +67 -10
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +83 -15
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members-feed-viewer.mjs +8 -7
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members.mjs +29 -0
- package/dist/browser-plane/assets.json +166 -0
- package/dist/browser-plane/host.html +156 -0
- package/dist/browser-plane/package-reader.js +4 -0
- package/dist/browser-plane/preview-deferred/368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5.bin +0 -0
- package/dist/browser-plane/preview-deferred/37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29.bin +0 -0
- package/dist/browser-plane/preview-deferred/4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02.bin +0 -0
- package/dist/browser-plane/preview-deferred/521a0e528916d678d7588b2cddf3dadd3bb959a6d62426382f61c77a82017e35.bin +1 -0
- package/dist/browser-plane/preview-deferred/62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9.bin +0 -0
- package/dist/browser-plane/preview-deferred/642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e.bin +0 -0
- package/dist/browser-plane/preview-deferred/67feb362281a3040ee05592769227c58a98ec3c8073dac95ecb5629141c67061.bin +56 -0
- package/dist/browser-plane/preview-deferred/97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1.bin +0 -0
- package/dist/browser-plane/preview-deferred/a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e.bin +28 -0
- package/dist/browser-plane/preview-deferred/bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa.bin +0 -0
- package/dist/browser-plane/preview-deferred/d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134.bin +0 -0
- package/dist/browser-plane/preview-deferred/dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b.bin +0 -0
- package/dist/browser-plane/preview-deferred/dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5.bin +0 -0
- package/dist/browser-plane/preview-deferred/ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf.bin +0 -0
- package/dist/browser-plane/sw.js +13 -0
- package/dist/browser-plane/t5-frame.html +350 -0
- package/dist/browser-plane/t5-worker-frame.html +350 -0
- package/dist/cli.mjs +6594 -5078
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +34 -1
- package/guides/designing-himi-screens.md +5 -1
- package/guides/himi-json-output.md +16 -1
- package/guides/iap-products.md +8 -0
- package/guides/one-app-to-himi-app.md +81 -5
- package/guides/sdk-reference.json +56 -11
- package/guides/skill.md +1 -0
- package/package.json +3 -3
- package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/branded-lite/config.ts +7 -0
- package/templates/branded-lite/dev/index.ts +1109 -2307
- package/templates/branded-lite/himalaya.content.json +3 -0
- package/templates/branded-lite/l10n/en.json +6 -6
- package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/branded-lite/tier5-src/_shared/business-chat-history.ts +4 -1
- package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +2 -7
- package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/branded-lite/tier5-src/_shared/header-badges.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/himi-cms.ts +2 -1
- package/templates/branded-lite/tier5-src/_shared/inbox-rooms.ts +68 -0
- package/templates/branded-lite/tier5-src/_shared/member-activity.ts +34 -0
- package/templates/branded-lite/tier5-src/_shared/member-row-extras.ts +167 -0
- package/templates/branded-lite/tier5-src/_shared/notification-target.ts +141 -0
- package/templates/branded-lite/tier5-src/_shared/source-badges.ts +6 -4
- package/templates/branded-lite/tier5-src/_shared/wix-member-links.ts +114 -0
- package/templates/branded-lite/tier5-src/business-chat/index.ts +114 -59
- package/templates/branded-lite/tier5-src/himi-branded-lite-cms/index.ts +160 -39
- package/templates/branded-lite/tier5-src/himi-branded-lite-inbox/index.ts +83 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +62 -7
- package/templates/branded-lite/tier5-src/wix-spaces-members/index.ts +60 -12
- package/templates/community-club/dev/index.ts +210 -2044
- package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/community-club/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/community-club/tier5-src/_shared/himi-cms.ts +2 -1
- package/templates/community-club/tier5-src/himi-branded-lite-cms/index.ts +160 -39
- package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/restaurant/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/retail-store/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/salon-spa/dev/index.ts +112 -70
- package/templates/salon-spa/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-history.ts +4 -1
- package/templates/salon-spa/tier5-src/_shared/business-chat-presenter.ts +2 -7
- package/templates/salon-spa/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/salon-spa/tier5-src/_shared/inbox-rooms.ts +68 -0
- package/templates/salon-spa/tier5-src/business-chat/index.ts +114 -59
- package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +0 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +0 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The destination data the inbox tap needs from the Wix member-link catalog (the one an app's push taps use):
|
|
2
|
+
// which links a member notification carries, and the canonical Himi screen and param each one opens.
|
|
3
|
+
//
|
|
4
|
+
// This is a COPY, on purpose. A worker's shared helpers are copied into the starter app by the template
|
|
5
|
+
// generator, which follows only relative imports that stay inside the bundles source folder, so a worker
|
|
6
|
+
// cannot import the catalog from its own package without breaking `himi init`.
|
|
7
|
+
// The copy is pinned to the catalog by the notification-target parity test (every id, link, param and
|
|
8
|
+
// screen must match), so a catalog change fails that test until this file follows it.
|
|
9
|
+
//
|
|
10
|
+
// Generated from the catalog's `id`, `matches`, `screenParam` and `canonicalScreen`; `vertical`, `audience`
|
|
11
|
+
// and the commentary belong to the catalog and are not repeated here.
|
|
12
|
+
|
|
13
|
+
export interface MemberLinkDestination {
|
|
14
|
+
id: string;
|
|
15
|
+
/** Longest first, as in the catalog. */
|
|
16
|
+
matches: readonly string[];
|
|
17
|
+
/** The capture that names the item; absent for a list screen. */
|
|
18
|
+
screenParam?: string;
|
|
19
|
+
canonicalScreen: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const MEMBER_LINK_DESTINATIONS: readonly MemberLinkDestination[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "store.order",
|
|
25
|
+
matches: [
|
|
26
|
+
"my-account/:site/stores/orders/:orderId",
|
|
27
|
+
],
|
|
28
|
+
screenParam: "orderId",
|
|
29
|
+
canonicalScreen: "himi_order",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "bookings.session",
|
|
33
|
+
matches: [
|
|
34
|
+
"bookings/app/:site/:businessId/:sessionId",
|
|
35
|
+
],
|
|
36
|
+
canonicalScreen: "himi_bookings",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "groups.post",
|
|
40
|
+
matches: [
|
|
41
|
+
"clubs-member/club/:groupId/feed/post/:postId/:commentId",
|
|
42
|
+
"clubs-member/club/:groupId/feed/post/:postId",
|
|
43
|
+
],
|
|
44
|
+
screenParam: "groupId",
|
|
45
|
+
canonicalScreen: "himi_group",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "forum.thread",
|
|
49
|
+
matches: [
|
|
50
|
+
"forum/post/:postId/:commentId/:replyId",
|
|
51
|
+
"forum/post/:postId/:commentId",
|
|
52
|
+
"forum/post/:postId",
|
|
53
|
+
],
|
|
54
|
+
screenParam: "postId",
|
|
55
|
+
canonicalScreen: "himi_forum_post",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "forum.create",
|
|
59
|
+
matches: [
|
|
60
|
+
"forum/create-post",
|
|
61
|
+
],
|
|
62
|
+
canonicalScreen: "himi_create_post",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "blog.post",
|
|
66
|
+
matches: [
|
|
67
|
+
"blog/post/:postId",
|
|
68
|
+
],
|
|
69
|
+
screenParam: "postId",
|
|
70
|
+
canonicalScreen: "himi_blog_post",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "members.profile",
|
|
74
|
+
matches: [
|
|
75
|
+
"members-area/member-profile/:memberId",
|
|
76
|
+
],
|
|
77
|
+
screenParam: "memberId",
|
|
78
|
+
canonicalScreen: "himi_member",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "events.event",
|
|
82
|
+
matches: [
|
|
83
|
+
"my-account/app/140603ad-af8d-84a5-2c80-a0f60cb47351/:instanceId/:eventId/guest-list/:path",
|
|
84
|
+
"my-account/app/140603ad-af8d-84a5-2c80-a0f60cb47351/:instanceId/:eventId/details",
|
|
85
|
+
],
|
|
86
|
+
screenParam: "eventId",
|
|
87
|
+
canonicalScreen: "himi_event",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "galleries.shared",
|
|
91
|
+
matches: [
|
|
92
|
+
"shared-gallery/:site/preview/:itemId",
|
|
93
|
+
],
|
|
94
|
+
canonicalScreen: "himi_galleries",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "galleries.album",
|
|
98
|
+
matches: [
|
|
99
|
+
"lists/:site/:listId/media/:itemId/:commentId",
|
|
100
|
+
"lists/:site/:listId/:tab/:commentId",
|
|
101
|
+
"spaces/lists/:site/:listId/:tab",
|
|
102
|
+
"lists/:site/:listId/:itemId",
|
|
103
|
+
],
|
|
104
|
+
screenParam: "listId",
|
|
105
|
+
canonicalScreen: "himi_photo_album",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "notifications.list",
|
|
109
|
+
matches: [
|
|
110
|
+
"notifications/list/:site",
|
|
111
|
+
],
|
|
112
|
+
canonicalScreen: "himi_notifications",
|
|
113
|
+
},
|
|
114
|
+
];
|
|
@@ -13,7 +13,7 @@ import { hostSiteFromInput } from '../_shared/branded-lite-hosted.js';
|
|
|
13
13
|
import { productImage, searchStoreProducts } from '../_shared/himi-store.js';
|
|
14
14
|
import { queryServices } from '@himalaya/wix-bookings';
|
|
15
15
|
|
|
16
|
-
type PendingIntent = BusinessChatIntent;
|
|
16
|
+
type PendingIntent = BusinessChatIntent & { suggestionId?:string; answerTo?:string };
|
|
17
17
|
// Only what the screen reads. This state crosses into the worker on every keystroke, so a row carries no raw
|
|
18
18
|
// parts, plain text or text runs: they would be re-sent each time for nothing.
|
|
19
19
|
type BusinessMessageRow = Pick<MessageRow,'id'|'sender'|'when'|'createdDate'|'mine'> & {
|
|
@@ -23,7 +23,7 @@ type BusinessMessageRow = Pick<MessageRow,'id'|'sender'|'when'|'createdDate'|'mi
|
|
|
23
23
|
/** Only the member's own row that the server has not returned yet. */
|
|
24
24
|
pending?:boolean;
|
|
25
25
|
/** Confirmed local delivery, retained until the authoritative history returns it. */
|
|
26
|
-
acceptedIntent?:
|
|
26
|
+
acceptedIntent?:PendingIntent;
|
|
27
27
|
};
|
|
28
28
|
type State = {
|
|
29
29
|
phase:'loading'|'ready'|'error'|'guest'; error:string; notice:string; member:boolean;
|
|
@@ -115,12 +115,31 @@ async function sendBusinessVisitorOption(options:BusinessVisitorOptions|null|und
|
|
|
115
115
|
function liveDescription(state:string,reason:string):string{return businessRealtimeStatusLabel(state,reason);}
|
|
116
116
|
/** The member's own message as it shows the instant they tap Send, before the server has answered. It carries the
|
|
117
117
|
* id sent to the server, so the real message replaces it once history returns it. */
|
|
118
|
-
function pendingRow(intent:
|
|
118
|
+
function pendingRow(intent:PendingIntent):BusinessMessageRow{
|
|
119
119
|
const when=intent.delivery==='sending'?businessChatCopy('sending'):intent.delivery==='unconfirmed'?businessChatCopy('unconfirmed'):messageTimeLabel(intent.createdDate);
|
|
120
120
|
return {id:intent.id,sender:'',when,createdDate:intent.createdDate,mine:true,
|
|
121
121
|
senderAvatar:'',senderName:intent.senderName??'',senderInitials:'',senderBadge:'',imageUrl:'',imageDescription:'',noImage:true,bodyText:intent.text,bodyMarkdown:intent.text,
|
|
122
122
|
suggestions:[],articleActions:[],couponCode:'',noCoupon:true,notMine:false,isCard:false,notCard:true,noSuggestions:true,noActions:true,cardTitle:'',cardDetail:'',noCardText:true,dayLabel:'',dayHidden:true,startsDay:false,scrollAnchor:`himi_business_chat.message_${intent.id}`,pending:true,acceptedIntent:intent.delivery==='sent'?intent:undefined};
|
|
123
123
|
}
|
|
124
|
+
/** Keep a selected option in its turn while Wix omits its visible text from history. */
|
|
125
|
+
/** Selected options whose exact record history has returned. A state built from an older snapshot has lost
|
|
126
|
+
* the anchor that matched them by text, so every path asks this before it adds a local row. */
|
|
127
|
+
const seenSelections=new Set<string>();
|
|
128
|
+
function placeLocalRow(messages:BusinessMessageRow[],intent:PendingIntent,source:State['transcriptSource']):BusinessMessageRow[]{
|
|
129
|
+
const after=intent.afterId?messages.findIndex(row=>row.id===intent.afterId):-1;
|
|
130
|
+
const beforeAll=intent.fromStart||!!intent.suggestionId&&source==='smart-chat';
|
|
131
|
+
const at=after>=0?after+1:beforeAll?0:messages.length;
|
|
132
|
+
return [...messages.slice(0,at),pendingRow(intent),...messages.slice(at)];
|
|
133
|
+
}
|
|
134
|
+
/** The selection is visible only when the exact answer's selected record produced a row. */
|
|
135
|
+
function selectedOptionVisible(aiMessages:unknown[],messages:BusinessMessageRow[],intent:PendingIntent):boolean{
|
|
136
|
+
if(!intent.answerTo)return false;
|
|
137
|
+
return aiMessages.some(value=>{
|
|
138
|
+
const message=value as {id?:unknown;sender?:unknown;messageType?:unknown;answerTo?:unknown}|null;
|
|
139
|
+
return message?.sender==='SITE_VISITOR'&&message.messageType==='VISITOR_OPTION_SELECTED'&&message.answerTo===intent.answerTo&&
|
|
140
|
+
messages.some(row=>row.id===`smart-chat:${message.id}`&&row.mine&&row.bodyText.trim()===intent.text.trim());
|
|
141
|
+
});
|
|
142
|
+
}
|
|
124
143
|
function shape(s:State):State{const used=new Set(s.usedSuggestionIds);const messages=s.messages.map(row=>({...row,noSuggestions:s.phase!=='ready'||!(row.suggestions||[]).length,suggestions:(row.suggestions||[]).map(button=>used.has(button.id)?{...button,enabled:false,disabled:true}:button)}));const quickReplies=(s.quickReplies||[]).map(button=>used.has(button.id)?{...button,enabled:false,disabled:true}:button);const quickEnabled=s.phase==='ready'&&quickReplies.some(button=>button.enabled);return {...s,messages,quickReplies,hasQuickReplies:quickEnabled,noQuickReplies:!quickEnabled,scrollAnchor:messages[messages.length-1]?.scrollAnchor||'',liveLabel:liveDescription(s.liveState,s.liveReason),hasActiveRoom:!!s.roomId,noActiveRoom:!s.roomId,hasHostedName:!!s.hostedName,noHostedName:!s.hostedName,isGuest:s.phase==='guest',notGuest:s.phase!=='guest',realtimeDiagnostic:`state=${s.liveState}; reason=${s.liveReason||'none'}; events=${s.realtimeEventCount}; lastType=${/^(message|messagesent)$/i.test(s.lastRealtimeEventType)?s.lastRealtimeEventType:'none'}`,errorText:s.error,loadingHidden:s.phase!=='loading'||!!s.error,noError:!s.error,noNotice:!s.notice,noMessages:!messages.length,hasMessages:!!messages.length,noEmpty:s.phase!=='ready'||!!messages.length,
|
|
125
144
|
notLoading:s.phase!=='loading',sendDisabled:!canSend(s,s.draft),sendEnabled:canSend(s,s.draft),noLive:!s.roomId||s.liveState==='connected'};}
|
|
126
145
|
/** Smart Chat resolves the conversation from the member identity, including the first message.
|
|
@@ -276,7 +295,7 @@ function safeError(error:unknown,operation:'load'|'send'|'init'):string{
|
|
|
276
295
|
}
|
|
277
296
|
async function endSubscription():Promise<void>{const id=subscriptionId;subscriptionId='';if(id)try{await unsubscribeRealtime({subscriptionId:id});}catch{}}
|
|
278
297
|
function clearPrivate(s:State,phase:'guest'|'error'='guest'):State{
|
|
279
|
-
attachmentUrls.clear();productImages.clear();productImageLookups.clear();serviceIds.clear();serviceIdLookups.clear();currentIdentity=null;businessParticipantId='';epoch++;void endSubscription();
|
|
298
|
+
attachmentUrls.clear();seenSelections.clear();productImages.clear();productImageLookups.clear();serviceIds.clear();serviceIdLookups.clear();currentIdentity=null;businessParticipantId='';epoch++;void endSubscription();
|
|
280
299
|
return shape({...initial,phase,error:phase==='guest'?businessChatCopy('signInPrompt'):s.error});
|
|
281
300
|
}
|
|
282
301
|
let refreshTail:Promise<unknown>=Promise.resolve();
|
|
@@ -327,15 +346,20 @@ async function refreshHistory(s:State):Promise<State|undefined>{
|
|
|
327
346
|
// What was typed while this ran is kept even on the first load, when the live state is still empty.
|
|
328
347
|
const liveDraft=draftRevision!==startDraftRevision?latest.draft:s.draft;
|
|
329
348
|
const base:State=same?withLiveFields({...s,roomId:latest.roomId||roomId},latest,liveDraft):{...s,draft:liveDraft};
|
|
330
|
-
const
|
|
331
|
-
|
|
349
|
+
const observed=(visible:BusinessMessageRow[],intent:PendingIntent|null)=>{
|
|
350
|
+
if(intent&&selectedOptionVisible(aiRead.messages,visible,intent))seenSelections.add(intent.id);
|
|
351
|
+
return intentObservedIn(visible,intent,historySource);
|
|
352
|
+
};
|
|
353
|
+
const intentObserved=observed(messages,base.pendingIntent);
|
|
354
|
+
// A selected option never sits in the composer, so its text must not clear a draft that happens to match.
|
|
355
|
+
const draft=intentObserved&&base.pendingIntent&&!base.pendingIntent.suggestionId&&base.draft.trim()===base.pendingIntent.text?'':base.draft;
|
|
332
356
|
// A send the server has not returned yet stays on screen until history does. Seeing it in history only replaces the
|
|
333
357
|
// placeholder: a send still in flight stays pending until its own request settles it, so its outcome is never lost.
|
|
334
358
|
// An unconfirmed one (its request is over) no longer holds Send once its words show up; its notice stays.
|
|
335
|
-
const shown=!intentObserved&&base.pendingIntent?[...messages,pendingRow(base.pendingIntent)]:messages;
|
|
359
|
+
const shown=!intentObserved&&base.pendingIntent?base.pendingIntent.suggestionId?placeLocalRow(messages,base.pendingIntent,historySource):[...messages,pendingRow(base.pendingIntent)]:messages;
|
|
336
360
|
const pendingIntent=intentObserved&&base.pendingIntent?.delivery==='unconfirmed'?null:base.pendingIntent;
|
|
337
361
|
const loaded:State={...base,historyRevision:++historyRevision,historySource,transcriptSource:historySource,smartChatEmpty:!aiRead.error&&aiRead.messages.length===0,messages:shown,pendingIntent,visitorOptions,quickReplies:businessVisitorOptionSuggestions(visitorOptions),draft,phase:'ready',error:'',loadFailure:''};
|
|
338
|
-
return shape(same?withAcceptedRows(loaded,latest):loaded);
|
|
362
|
+
return shape(same?withAcceptedRows(loaded,latest,observed):loaded);
|
|
339
363
|
}catch(error){
|
|
340
364
|
const wrapped=error instanceof Error?error:new Error('load_failed');
|
|
341
365
|
Object.assign(wrapped,{businessLoadFailure:safeLoadFailure(error)});
|
|
@@ -347,25 +371,27 @@ async function refreshHistory(s:State):Promise<State|undefined>{
|
|
|
347
371
|
function withLiveFields(st:State,latest:State,draft:string):State{
|
|
348
372
|
// The pending intent is always the live one: a send settled meanwhile (null now) must not come back from a snapshot.
|
|
349
373
|
return {...st,draft,pendingIntent:latest.pendingIntent,
|
|
350
|
-
|
|
374
|
+
// A rejected send removes its ID from the live state. Unioning an older snapshot would disable it again.
|
|
375
|
+
usedSuggestionIds:latest.usedSuggestionIds,
|
|
351
376
|
liveState:latest.liveState,liveReason:latest.liveReason,realtimeEventCount:Math.max(st.realtimeEventCount,latest.realtimeEventCount),lastRealtimeEventType:latest.lastRealtimeEventType,
|
|
352
377
|
scrollSend:Math.max(st.scrollSend,latest.scrollSend)};
|
|
353
378
|
}
|
|
354
379
|
/** A successful empty pre-send read anchors accepted Smart Chat rows, never an
|
|
355
380
|
* older Members payload retained after a failed read. Keep both contexts intact. */
|
|
356
381
|
function intentObservedIn(messages:BusinessMessageRow[],intent:BusinessChatIntent|null,source:State['transcriptSource']):boolean{
|
|
382
|
+
if(intent&&seenSelections.has(intent.id))return true;
|
|
357
383
|
return pendingIntentWasObserved(messages,source==='smart-chat'&&intent?.delivery==='sent'&&intent.smartChatFromStart
|
|
358
384
|
?{...intent,afterId:undefined,fromStart:true}:intent);
|
|
359
385
|
}
|
|
360
386
|
/** Keep confirmed local deliveries independently of in-flight intent and history revision.
|
|
361
387
|
* Reconcile by the existing stored-id/anchor contract, never by ownership or text alone. */
|
|
362
|
-
function withAcceptedRows(st:State,latest:State):State{
|
|
388
|
+
function withAcceptedRows(st:State,latest:State,observed:(messages:BusinessMessageRow[],intent:PendingIntent)=>boolean=(messages,intent)=>intentObservedIn(messages,intent,st.transcriptSource)):State{
|
|
363
389
|
let messages=st.messages;
|
|
364
390
|
for(const row of [...st.messages,...latest.messages]){
|
|
365
391
|
const intent=row.acceptedIntent;
|
|
366
392
|
if(!intent)continue;
|
|
367
|
-
if(
|
|
368
|
-
else if(!messages.some(candidate=>candidate.id===intent.id))messages=[...messages,row];
|
|
393
|
+
if(observed(messages,intent))messages=messages.filter(candidate=>!(candidate.id===intent.id&&candidate.pending));
|
|
394
|
+
else if(!messages.some(candidate=>candidate.id===intent.id))messages=intent.suggestionId?placeLocalRow(messages,intent,st.transcriptSource):[...messages,row];
|
|
369
395
|
}
|
|
370
396
|
return {...st,messages,historySource:smartChatAcceptedEpoch===epoch?'smart-chat':st.historySource};
|
|
371
397
|
}
|
|
@@ -384,6 +410,7 @@ function withLiveDraft(st:State):State{
|
|
|
384
410
|
return intent&&!intentObservedIn(merged.messages,intent,merged.transcriptSource)&&!merged.messages.some(row=>row.id===intent.id)
|
|
385
411
|
?{...merged,messages:[...merged.messages,pendingRow(intent)]}:merged;
|
|
386
412
|
}
|
|
413
|
+
type SuggestionSession={member:boolean;roomId:string;principal:string;currentPrincipal:string;scope:string;currentScope:string};
|
|
387
414
|
type SendResult={id:string;outcome:'sent'|'rejected'|'unconfirmed'|'signed_out';draft:string;storedId?:string;error?:unknown};
|
|
388
415
|
/** Posts one message and reports how it ended; never throws. The caller already showed the message. The call lasts
|
|
389
416
|
* until the AI has answered: history usually shows the stored copy first, which replaces the placeholder, while Send
|
|
@@ -400,6 +427,35 @@ async function deliver(pending:BusinessChatIntent,draft:string):Promise<void>{
|
|
|
400
427
|
}
|
|
401
428
|
if(sendEpoch===epoch)void svc.settleSend(result);
|
|
402
429
|
}
|
|
430
|
+
/** An AI visitor option uses the same visible pending row as a typed send. Its Wix request can take until the AI finishes;
|
|
431
|
+
* the row and disabled chip are already painted while it runs. */
|
|
432
|
+
async function deliverSuggestion(pending:PendingIntent,rows:{suggestions?:BusinessSuggestion[]}[],options:BusinessVisitorOptions|null,session:SuggestionSession):Promise<void>{
|
|
433
|
+
const sendEpoch=epoch;
|
|
434
|
+
let result:SendResult={id:pending.id,outcome:'sent',draft:''};
|
|
435
|
+
try{
|
|
436
|
+
await Promise.resolve();
|
|
437
|
+
const sent=await dispatchBusinessSuggestion(rows,pending.suggestionId!,session,suggestion=>sendBusinessVisitorOption(options,suggestion.label));
|
|
438
|
+
if(!sent)result={id:pending.id,outcome:'rejected',draft:'',error:new Error('suggestion_unavailable')};
|
|
439
|
+
}catch(error){
|
|
440
|
+
const lost=/member_sign_in_required|MemberAccessChanged|\b401\b|\b403\b/i.test(String(error));
|
|
441
|
+
result={id:pending.id,draft:'',error,outcome:lost?'signed_out':businessChatSendRejected(error)?'rejected':'unconfirmed'};
|
|
442
|
+
}
|
|
443
|
+
if(sendEpoch===epoch)void svc.settleSend(result);
|
|
444
|
+
}
|
|
445
|
+
/** A Wix Chat postback is silent: it disables its chip immediately but does not invent an outgoing text message. */
|
|
446
|
+
async function deliverButtonSuggestion(id:string,rows:{suggestions?:BusinessSuggestion[]}[],session:SuggestionSession):Promise<void>{
|
|
447
|
+
const sendEpoch=epoch;
|
|
448
|
+
let result:SendResult={id,outcome:'sent',draft:''};
|
|
449
|
+
try{
|
|
450
|
+
await Promise.resolve();
|
|
451
|
+
const sent=await dispatchBusinessSuggestion(rows,id,session,suggestion=>chat.sendButton(me(),session.roomId,{id:newBusinessChatIntent(suggestion.label,session.roomId,session.principal).id,text:suggestion.label,appId:suggestion.appId!,interactionId:suggestion.interactionId!}));
|
|
452
|
+
if(!sent)result={id,outcome:'rejected',draft:'',error:new Error('suggestion_unavailable')};
|
|
453
|
+
}catch(error){
|
|
454
|
+
const lost=/member_sign_in_required|MemberAccessChanged|\b401\b|\b403\b/i.test(String(error));
|
|
455
|
+
result={id,draft:'',error,outcome:lost?'signed_out':businessChatSendRejected(error)?'rejected':'unconfirmed'};
|
|
456
|
+
}
|
|
457
|
+
if(sendEpoch===epoch)void svc.settleButtonSuggestion(result);
|
|
458
|
+
}
|
|
403
459
|
const svc=defineService<State,Record<string,(s:State,a?:unknown)=>State|undefined|Promise<State|undefined>>>('businessChat',{
|
|
404
460
|
state:shape(initial),notHydrated:Object.keys(initial) as (keyof State)[],
|
|
405
461
|
actions:{
|
|
@@ -510,6 +566,12 @@ const svc=defineService<State,Record<string,(s:State,a?:unknown)=>State|undefine
|
|
|
510
566
|
if(result.outcome==='signed_out')return clearPrivate(s,'guest');
|
|
511
567
|
if(result.outcome==='rejected'){
|
|
512
568
|
// The row goes, and the text comes back unless the member has already started something new.
|
|
569
|
+
if(pending.suggestionId){
|
|
570
|
+
// A selected option has no draft to restore: its chip is offered again.
|
|
571
|
+
const restore=(buttons:BusinessSuggestion[])=>buttons.map(button=>button.id===pending.suggestionId?{...button,enabled:true,disabled:false}:button);
|
|
572
|
+
return shape({...s,messages:s.messages.filter(row=>row.id!==pending.id).map(row=>({...row,suggestions:restore(row.suggestions)})),quickReplies:restore(s.quickReplies),
|
|
573
|
+
pendingIntent:null,usedSuggestionIds:s.usedSuggestionIds.filter(id=>id!==pending.suggestionId),error:safeError(result.error,'send'),notice:businessChatCopy('suggestionRejected')});
|
|
574
|
+
}
|
|
513
575
|
return shape({...s,messages:s.messages.filter(row=>row.id!==pending.id),pendingIntent:null,draft:s.draft.trim()?s.draft:result.draft,error:safeError(result.error,'send'),notice:businessChatCopy('rejected')});
|
|
514
576
|
}
|
|
515
577
|
// History cannot tell this send from the same words written elsewhere (the website), so an indeterminate failure is
|
|
@@ -517,20 +579,22 @@ const svc=defineService<State,Record<string,(s:State,a?:unknown)=>State|undefine
|
|
|
517
579
|
if(result.outcome==='unconfirmed'){
|
|
518
580
|
const unconfirmed={...pending,delivery:'unconfirmed' as const};
|
|
519
581
|
const seen=intentObservedIn(s.messages,pending,s.transcriptSource);
|
|
520
|
-
return shape({...s,pendingIntent:seen?null:unconfirmed,messages:replaceRow(unconfirmed),error:safeError(result.error,'send'),notice:businessChatCopy('checkDelivery')});
|
|
582
|
+
return shape({...s,pendingIntent:seen?null:unconfirmed,messages:replaceRow(unconfirmed),error:safeError(result.error,'send'),notice:businessChatCopy(pending.suggestionId?'suggestionUnconfirmed':'checkDelivery')});
|
|
521
583
|
}
|
|
522
584
|
const sent={...pending,storedId:result.storedId,delivery:'sent' as const};
|
|
585
|
+
// A selected option keeps the notice it always had; a typed message shows none once accepted.
|
|
586
|
+
const sentNotice=sent.suggestionId?businessChatCopy('suggestionSent'):'';
|
|
523
587
|
// Every typed send uses Smart Chat, even when a Members room already exists.
|
|
524
588
|
smartChatAcceptedEpoch=epoch;
|
|
525
|
-
const accepted=shape({...s,historySource:'smart-chat',pendingIntent:sent,messages:replaceRow(sent),error:'',notice:
|
|
589
|
+
const accepted=shape({...s,historySource:'smart-chat',pendingIntent:sent,messages:replaceRow(sent),error:'',notice:sentNotice});
|
|
526
590
|
const sendEpoch=epoch;
|
|
527
591
|
// The server accepted it, so Send is free again whatever history says; the row stays until history returns it.
|
|
528
592
|
// A refresh that ran while the committed intent still said sending leaves a 'Sending…' row: swap it for the accepted one.
|
|
529
593
|
// History that already holds the message under the server's id needs no placeholder for it. Only this send's intent
|
|
530
594
|
// is cleared: a newer one the live state carried in is that send's to settle.
|
|
531
595
|
const ownIntent=(st:State)=>st.pendingIntent&&st.pendingIntent.id!==sent.id?st.pendingIntent:null;
|
|
532
|
-
const settled=(st:State):State=>intentObservedIn(st.messages,sent,st.transcriptSource)?shape({...st,pendingIntent:ownIntent(st),notice:
|
|
533
|
-
shape({...st,pendingIntent:ownIntent(st),notice:
|
|
596
|
+
const settled=(st:State):State=>intentObservedIn(st.messages,sent,st.transcriptSource)?shape({...st,pendingIntent:ownIntent(st),notice:sentNotice,messages:st.messages.filter(row=>!(row.id===sent.id&&row.pending))}):
|
|
597
|
+
shape({...st,pendingIntent:ownIntent(st),notice:sentNotice,messages:st.messages.some(row=>row.id===sent.id)?st.messages.map(row=>row.id===sent.id&&row.pending?pendingRow(sent):row):sent.suggestionId?st.messages:[...st.messages,pendingRow(sent)]});
|
|
534
598
|
// The stream finishes after the AI has persisted its answer. Reload even while connected:
|
|
535
599
|
// the Members-room subscription may never see the separate Smart Chat room's event.
|
|
536
600
|
// An existing-room refresh owns the next read. A first acceptance must wait for
|
|
@@ -569,53 +633,44 @@ const svc=defineService<State,Record<string,(s:State,a?:unknown)=>State|undefine
|
|
|
569
633
|
return settled(latestResolved());
|
|
570
634
|
}
|
|
571
635
|
},
|
|
572
|
-
|
|
636
|
+
settleButtonSuggestion:async(s,a)=>{
|
|
637
|
+
const result=a as SendResult|undefined;
|
|
638
|
+
if(!result||!s.usedSuggestionIds.includes(result.id))return undefined;
|
|
639
|
+
if(result.outcome==='signed_out')return clearPrivate(s,'guest');
|
|
640
|
+
if(result.outcome==='rejected'){
|
|
641
|
+
const restore=(buttons:BusinessSuggestion[])=>buttons.map(button=>button.id===result.id?{...button,enabled:true,disabled:false}:button);
|
|
642
|
+
return shape({...s,usedSuggestionIds:s.usedSuggestionIds.filter(id=>id!==result.id),quickReplies:restore(s.quickReplies),
|
|
643
|
+
messages:s.messages.map(row=>({...row,suggestions:restore(row.suggestions)})),error:safeError(result.error,'send'),notice:businessChatCopy('suggestionRejected')});
|
|
644
|
+
}
|
|
645
|
+
if(result.outcome==='unconfirmed')return shape({...s,error:safeError(result.error,'send'),notice:businessChatCopy('suggestionUnconfirmed')});
|
|
646
|
+
const accepted=shape({...s,error:'',notice:businessChatCopy('suggestionSent')});
|
|
647
|
+
try{
|
|
648
|
+
const loaded=await loadLatest(accepted,true);
|
|
649
|
+
return loaded?shape({...loaded,notice:businessChatCopy('suggestionSent')}):undefined;
|
|
650
|
+
}catch{return shape({...withLiveDraft(accepted),notice:businessChatCopy('suggestionSent')});}
|
|
651
|
+
},
|
|
652
|
+
sendSuggestion:(s,a)=>{
|
|
573
653
|
if(suggestionFlight||!s.member||s.phase!=='ready'||!!s.pendingIntent)return s;
|
|
574
654
|
const id=param(a,'id');
|
|
575
655
|
if(!id||s.usedSuggestionIds.includes(id))return s;
|
|
576
656
|
const rows=[{suggestions:s.quickReplies} as {suggestions?:BusinessSuggestion[]},...s.messages];
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
657
|
+
const suggestion=findBusinessSuggestion(rows,id);
|
|
658
|
+
// A first conversation has no room yet: only an AI option can start it.
|
|
659
|
+
if(!suggestion||!s.roomId&&suggestion.kind!=='visitor_option')return s;
|
|
660
|
+
const currentPrincipal=currentIdentity?.userId||currentIdentity?.email||'';
|
|
661
|
+
if(!s.principal||s.principal!==currentPrincipal||s.scope!==currentIdentity?.scope)return s;
|
|
662
|
+
const session:SuggestionSession={member:s.member,roomId:s.roomId,principal:s.principal,currentPrincipal,scope:s.scope,currentScope:currentIdentity?.scope||''};
|
|
581
663
|
suggestionFlight=true;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if(sendEpoch!==epoch)return undefined;
|
|
593
|
-
if(!sent)return withLiveDraft(s);
|
|
594
|
-
const latest=readServiceState();
|
|
595
|
-
if(latest.principal!==s.principal||latest.scope!==s.scope||(latest.roomId!==s.roomId&&(!!s.roomId||findBusinessSuggestion(rows,id)?.kind!=='visitor_option')))return undefined;
|
|
596
|
-
if(findBusinessSuggestion(rows,id)?.kind==='visitor_option')smartChatAcceptedEpoch=epoch;
|
|
597
|
-
accepted=shape({...latest,historySource:smartChatAcceptedEpoch===epoch?'smart-chat':latest.historySource,usedSuggestionIds:[...new Set([...s.usedSuggestionIds,...latest.usedSuggestionIds])],scrollSend:latest.scrollSend+1,error:'',notice:businessChatCopy('suggestionSent')});
|
|
598
|
-
const loaded=await loadLatest(keptAccepted(),true);
|
|
599
|
-
if(!loaded||sendEpoch!==epoch)return undefined;
|
|
600
|
-
accepted=loaded;
|
|
601
|
-
if(!accepted.roomId){
|
|
602
|
-
const room=await resolveExistingBusinessRoom(sendEpoch,s.businessName);
|
|
603
|
-
if(sendEpoch!==epoch)return undefined;
|
|
604
|
-
const current=keptAccepted();
|
|
605
|
-
if(room&&!current.roomId)businessParticipantId=room.participantId;
|
|
606
|
-
accepted={...current,roomId:current.roomId||room?.id||'',historySource:'smart-chat'};
|
|
607
|
-
}
|
|
608
|
-
return keptAccepted();
|
|
609
|
-
}catch(error){
|
|
610
|
-
if(sendEpoch!==epoch)return undefined;
|
|
611
|
-
if(/member_sign_in_required|MemberAccessChanged|\b401\b|\b403\b/i.test(String(error)))return clearPrivate(s,'guest');
|
|
612
|
-
const latest=readServiceState();
|
|
613
|
-
if(latest.principal!==s.principal||latest.scope!==s.scope)return undefined;
|
|
614
|
-
if(accepted)return keptAccepted();
|
|
615
|
-
s=shape({...latest,usedSuggestionIds:[...new Set([...s.usedSuggestionIds,...latest.usedSuggestionIds])]});
|
|
616
|
-
const definitive=businessChatSendRejected(error);
|
|
617
|
-
return shape({...withLiveDraft(s),usedSuggestionIds:definitive?s.usedSuggestionIds.filter(value=>value!==id):s.usedSuggestionIds,error:safeError(error,'send'),notice:definitive?businessChatCopy('suggestionRejected'):businessChatCopy('suggestionUnconfirmed')});
|
|
618
|
-
}finally{suggestionFlight=false;}
|
|
664
|
+
if(suggestion.kind!=='visitor_option'){
|
|
665
|
+
void deliverButtonSuggestion(id,rows,session).finally(()=>{suggestionFlight=false;});
|
|
666
|
+
return shape({...s,usedSuggestionIds:[...s.usedSuggestionIds,id],error:'',notice:''});
|
|
667
|
+
}
|
|
668
|
+
const ownName=[...s.messages].reverse().find(row=>row.mine&&row.senderName)?.senderName??'';
|
|
669
|
+
const intent:PendingIntent={...newBusinessChatIntent(suggestion.label,s.roomId,s.principal),suggestionId:id,answerTo:suggestion.messageId,
|
|
670
|
+
senderName:ownName,...sendAnchor(s.messages,suggestion.label,true),smartChatFromStart:s.transcriptSource==='members'&&s.smartChatEmpty};
|
|
671
|
+
void deliverSuggestion(intent,rows,s.visitorOptions,session).finally(()=>{suggestionFlight=false;});
|
|
672
|
+
return shape({...s,usedSuggestionIds:[...s.usedSuggestionIds,id],pendingIntent:intent,
|
|
673
|
+
messages:[...s.messages,pendingRow(intent)],scrollSend:s.scrollSend+1,error:'',notice:''});
|
|
619
674
|
},
|
|
620
675
|
onEvent:async(s,a)=>{
|
|
621
676
|
if(initFlight){eventDuringInit=true;return undefined;}
|
|
@@ -649,7 +704,7 @@ const svc=defineService<State,Record<string,(s:State,a?:unknown)=>State|undefine
|
|
|
649
704
|
}
|
|
650
705
|
catch(error){if(startEpoch!==epoch)return undefined;const lost=/member_sign_in_required|MemberAccessChanged|\b401\b|\b403\b/i.test(String(error));return lost?clearPrivate(next,'guest'):shape({...withLiveDraft(next),error:safeError(error,'load'),loadFailure:typeof (error as {businessLoadFailure?:unknown})?.businessLoadFailure==='string'?(error as {businessLoadFailure:string}).businessLoadFailure:safeLoadFailure(error)});}
|
|
651
706
|
},
|
|
652
|
-
dispose:async s=>{epoch++;attachmentUrls.clear();productImages.clear();productImageLookups.clear();serviceIds.clear();serviceIdLookups.clear();currentIdentity=null;businessParticipantId='';await endSubscription();return shape({...initial,phase:'guest'});},
|
|
707
|
+
dispose:async s=>{epoch++;attachmentUrls.clear();seenSelections.clear();productImages.clear();productImageLookups.clear();serviceIds.clear();serviceIdLookups.clear();currentIdentity=null;businessParticipantId='';await endSubscription();return shape({...initial,phase:'guest'});},
|
|
653
708
|
}
|
|
654
709
|
});
|
|
655
710
|
readServiceState=()=>svc.getState();
|