@doany-ai/inbox 0.0.0-stage → 0.1.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/README.md +175 -2
- package/dist/components/inbox-widget.d.ts +35 -0
- package/dist/components/inbox-widget.js +442 -0
- package/dist/components/parts.d.ts +67 -0
- package/dist/components/parts.js +115 -0
- package/dist/core.d.ts +6 -0
- package/dist/core.js +7 -0
- package/dist/format.d.ts +43 -0
- package/dist/format.js +136 -0
- package/dist/hooks.d.ts +69 -0
- package/dist/hooks.js +112 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +10 -0
- package/dist/labels.d.ts +209 -0
- package/dist/labels.js +210 -0
- package/dist/session.d.ts +204 -0
- package/dist/session.js +613 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.js +86 -0
- package/dist/types.d.ts +189 -0
- package/dist/types.js +1 -0
- package/package.json +61 -4
package/dist/labels.d.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { InboxMessage, InboxSuggestedQuestion } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Every word the chat shows besides the business's own (its name, the
|
|
4
|
+
* button's text, its greeting and away notice, which come from the platform)
|
|
5
|
+
* and the messages themselves. A component takes a part of it as `labels` and
|
|
6
|
+
* keeps the default for the rest:
|
|
7
|
+
*
|
|
8
|
+
* <InboxWidget labels={{ ask: "Poser une question", send: "Envoyer" }} />
|
|
9
|
+
*
|
|
10
|
+
* The platform never sends client-facing wording for its fixed lines: an event
|
|
11
|
+
* carries a code (`payload.code`), and `inboxEventLabelKeys` names the label
|
|
12
|
+
* each code shows, so a translated site shows no English.
|
|
13
|
+
*/
|
|
14
|
+
export interface InboxLabels {
|
|
15
|
+
/** The round button's name when the business's settings carry none (they normally do). */
|
|
16
|
+
askUs: string;
|
|
17
|
+
/** The close control in each header, and the button while the panel is open. */
|
|
18
|
+
close: string;
|
|
19
|
+
/** In the conversation's header: back to Messages. */
|
|
20
|
+
back: string;
|
|
21
|
+
/** For assistive technology, on the button and on the Messages tab when a reply has not been seen. */
|
|
22
|
+
unread: string;
|
|
23
|
+
/**
|
|
24
|
+
* The conversation's first bubble while the AI answers, or the chat holds an
|
|
25
|
+
* answer: the receptionist says it is an AI. A site may reword it; it cannot
|
|
26
|
+
* remove it.
|
|
27
|
+
*/
|
|
28
|
+
aiNotice: (name: string, business: string) => string;
|
|
29
|
+
/** The greeting when the AI answers and the business set none: large on Home, and the conversation's second bubble. */
|
|
30
|
+
greeting: (name: string, business: string) => string;
|
|
31
|
+
/** The greeting when only the team reads the chat and the business set none. */
|
|
32
|
+
teamGreeting: (business: string) => string;
|
|
33
|
+
/** The two tabs under Home and Messages. */
|
|
34
|
+
homeTab: string;
|
|
35
|
+
messagesTab: string;
|
|
36
|
+
/** Messages, with no conversation yet. */
|
|
37
|
+
noMessages: string;
|
|
38
|
+
/** The cards on Home. */
|
|
39
|
+
ask: string;
|
|
40
|
+
/** Under Ask a question. */
|
|
41
|
+
askSub: (name: string) => string;
|
|
42
|
+
book: string;
|
|
43
|
+
call: string;
|
|
44
|
+
message: string;
|
|
45
|
+
/** Under Send a message, and under the business's name in a conversation the AI does not answer. */
|
|
46
|
+
messageSub: string;
|
|
47
|
+
/** Under the receptionist's name in the conversation's header. */
|
|
48
|
+
teamCanHelp: string;
|
|
49
|
+
/** The conversation's ⋯ menu. */
|
|
50
|
+
more: string;
|
|
51
|
+
/** A message's time when it is under a minute old. */
|
|
52
|
+
justNow: string;
|
|
53
|
+
/** When the business is closed: its next opening, as a short weekday and a time. */
|
|
54
|
+
backAt: (day: string, time: string) => string;
|
|
55
|
+
/** While the business is open, how quickly its team usually answers: 5, 15 or 30 minutes, 1, 2, 4 or 8 hours, or a day. */
|
|
56
|
+
repliesWithin: (minutes: number) => string;
|
|
57
|
+
/** That time on its own, as it ends a sentence: "15 minutes", "an hour", "4 hours", "a day". */
|
|
58
|
+
replyTime: (minutes: number) => string;
|
|
59
|
+
/** The box: an email row until this browser has a chat, then the message. */
|
|
60
|
+
emailLabel: string;
|
|
61
|
+
emailPlaceholder: string;
|
|
62
|
+
emailRequired: string;
|
|
63
|
+
emailFormat: string;
|
|
64
|
+
messageLabel: string;
|
|
65
|
+
messagePlaceholder: string;
|
|
66
|
+
send: string;
|
|
67
|
+
sending: string;
|
|
68
|
+
/**
|
|
69
|
+
* The questions offered before the first message when the business saved
|
|
70
|
+
* none, by code (`inboxSuggestedQuestionLabelKeys`). Pressing one asks it.
|
|
71
|
+
*/
|
|
72
|
+
suggestPrices: string;
|
|
73
|
+
suggestBook: string;
|
|
74
|
+
suggestHours: string;
|
|
75
|
+
suggestAddress: string;
|
|
76
|
+
/** For assistive technology: the name of that group of questions. */
|
|
77
|
+
suggestedQuestions: string;
|
|
78
|
+
/**
|
|
79
|
+
* Above the box until the first message, on a site that passes
|
|
80
|
+
* `privacyHref`. `{link}` is where `privacyPolicy` goes, as the link to the
|
|
81
|
+
* site's privacy page; without it the link follows the words.
|
|
82
|
+
*/
|
|
83
|
+
privacyNotice: string;
|
|
84
|
+
privacyPolicy: string;
|
|
85
|
+
/** The × that hides the privacy line. */
|
|
86
|
+
dismiss: string;
|
|
87
|
+
/** In the ⋯ menu, while the AI answers: hands the chat to a person. */
|
|
88
|
+
askForPerson: string;
|
|
89
|
+
/** In the byline under the receptionist's bubbles: "Annie · AI receptionist · 5 min. ago". */
|
|
90
|
+
aiReceptionist: string;
|
|
91
|
+
/** The byline's name under a team reply that carries none. */
|
|
92
|
+
team: string;
|
|
93
|
+
/** Announced while an answer is on its way. */
|
|
94
|
+
typing: string;
|
|
95
|
+
/** In the ⋯ menu: the address the chat was started with. */
|
|
96
|
+
chattingAs: (email: string) => string;
|
|
97
|
+
/** In the ⋯ menu: forgets the chat in this browser. */
|
|
98
|
+
startNewChat: string;
|
|
99
|
+
/** A first message that has not reached the business yet. */
|
|
100
|
+
notSent: string;
|
|
101
|
+
tryAgain: string;
|
|
102
|
+
/**
|
|
103
|
+
* The chat is with the team now: after Talk to a person, after Send a
|
|
104
|
+
* message, and whenever the AI passes a question on. Followed by how long
|
|
105
|
+
* the team takes (`waitText`).
|
|
106
|
+
*/
|
|
107
|
+
teamWillReply: string;
|
|
108
|
+
/** How long the team takes, while the business is open and the platform knows. `time` is `replyTime`'s. */
|
|
109
|
+
typicallyReplies: (business: string, time: string) => string;
|
|
110
|
+
/** The same place, while it does not know. */
|
|
111
|
+
replyHereOrEmail: string;
|
|
112
|
+
/** The fixed lines, by event code (`inboxEventLabelKeys`). */
|
|
113
|
+
bookingMade: string;
|
|
114
|
+
safety: string;
|
|
115
|
+
/** The AI could not answer. Followed by how long the team takes. */
|
|
116
|
+
cannotAnswer: string;
|
|
117
|
+
/** The AI is at a limit. Followed by `teamWillReply` and how long the team takes. */
|
|
118
|
+
unavailable: string;
|
|
119
|
+
teamRepliedByEmail: string;
|
|
120
|
+
/** An event this version does not know. */
|
|
121
|
+
unknownEvent: string;
|
|
122
|
+
/** An emailed link that no longer works. */
|
|
123
|
+
linkExpired: string;
|
|
124
|
+
/** No more chats can start with a typed email today. */
|
|
125
|
+
contactLimit: string;
|
|
126
|
+
/** No more new chats right now. */
|
|
127
|
+
tryLater: string;
|
|
128
|
+
rateLimited: string;
|
|
129
|
+
/** The address is a preview the platform takes nothing from. */
|
|
130
|
+
notPublished: string;
|
|
131
|
+
/** The business turned the button off during the chat. */
|
|
132
|
+
off: string;
|
|
133
|
+
/** The business blocked this client. */
|
|
134
|
+
closed: string;
|
|
135
|
+
sendFailed: string;
|
|
136
|
+
/** At a limit: how to reach the business instead. */
|
|
137
|
+
emailUs: string;
|
|
138
|
+
/** An offer card's button. */
|
|
139
|
+
bookOffer: string;
|
|
140
|
+
poweredBy: string;
|
|
141
|
+
}
|
|
142
|
+
export declare const defaultInboxLabels: InboxLabels;
|
|
143
|
+
/** The defaults with a site's changes on top. A key set to `undefined` keeps its default. */
|
|
144
|
+
export declare const inboxLabels: (labels?: Partial<InboxLabels>) => InboxLabels;
|
|
145
|
+
/**
|
|
146
|
+
* Which label each event code shows. The server stores codes, never words;
|
|
147
|
+
* a code this version does not know shows `unknownEvent`. The three codes for
|
|
148
|
+
* "the team has this chat" share one label.
|
|
149
|
+
*/
|
|
150
|
+
export declare const inboxEventLabelKeys: {
|
|
151
|
+
readonly asked_for_person: "teamWillReply";
|
|
152
|
+
readonly booking_made: "bookingMade";
|
|
153
|
+
readonly safety: "safety";
|
|
154
|
+
readonly cannot_answer: "cannotAnswer";
|
|
155
|
+
readonly unavailable: "unavailable";
|
|
156
|
+
readonly team_has_it: "teamWillReply";
|
|
157
|
+
readonly sent_to_team: "teamWillReply";
|
|
158
|
+
readonly team_replied_by_email: "teamRepliedByEmail";
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Which label each suggested question's code shows. The platform sends a code
|
|
162
|
+
* for a question it built from what the business has, never its words.
|
|
163
|
+
*/
|
|
164
|
+
export declare const inboxSuggestedQuestionLabelKeys: {
|
|
165
|
+
readonly prices: "suggestPrices";
|
|
166
|
+
readonly book: "suggestBook";
|
|
167
|
+
readonly hours: "suggestHours";
|
|
168
|
+
readonly address: "suggestAddress";
|
|
169
|
+
};
|
|
170
|
+
export declare const isHandoverCode: (code: string | undefined) => boolean;
|
|
171
|
+
/** What the chat knows about the wait when it draws a handover line. */
|
|
172
|
+
export interface InboxWait {
|
|
173
|
+
business: string;
|
|
174
|
+
/** `availability.replies_within` while the business is open; null while closed or unknown. */
|
|
175
|
+
repliesWithin: number | null;
|
|
176
|
+
/** "Back Mon 9:00" while the business is closed. */
|
|
177
|
+
backAt: string | null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The handover lines that say how long the team takes: for each run of them
|
|
181
|
+
* with no team reply in between, the latest only, so the wait is said once,
|
|
182
|
+
* after the client's latest message.
|
|
183
|
+
*/
|
|
184
|
+
export declare function saysTheWait(messages: readonly InboxMessage[]): ReadonlySet<string>;
|
|
185
|
+
/**
|
|
186
|
+
* How long the team takes: the business's next opening while it is closed,
|
|
187
|
+
* "{business} typically replies in {time}." while it is open and the platform
|
|
188
|
+
* knows, else "They'll reply here or by email."
|
|
189
|
+
*/
|
|
190
|
+
export declare function waitText(labels: InboxLabels, wait: InboxWait): string;
|
|
191
|
+
/**
|
|
192
|
+
* The words for one event code. A handover code ends with the wait. `wait` is
|
|
193
|
+
* null for a handover line that a later one repeats before the team has
|
|
194
|
+
* replied: the wait is said once, under the latest, so such a line keeps only
|
|
195
|
+
* words of its own ("I'm not sure about that. I'll pass this to the team.")
|
|
196
|
+
* or is empty and not drawn. An empty translation of the safety line falls
|
|
197
|
+
* back to the default.
|
|
198
|
+
*/
|
|
199
|
+
export declare function eventText(code: string | undefined, labels: InboxLabels, wait?: InboxWait | null): string;
|
|
200
|
+
/**
|
|
201
|
+
* The questions to offer, as the words a press sends: the business's own as
|
|
202
|
+
* written, a code as its label. A code this version does not know, an empty
|
|
203
|
+
* one and a repeat are left out.
|
|
204
|
+
*/
|
|
205
|
+
export declare function suggestedQuestionTexts(questions: readonly InboxSuggestedQuestion[] | undefined, labels: InboxLabels): string[];
|
|
206
|
+
/** The privacy line around its link: the words before `{link}` and after it. */
|
|
207
|
+
export declare function privacyNoticeParts(labels: InboxLabels): [string, string];
|
|
208
|
+
/** The line no prop hides: a site may reword it, and an empty result falls back to the default. */
|
|
209
|
+
export declare function aiNoticeText(labels: InboxLabels, name: string, business: string): string;
|
package/dist/labels.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
function replyTime(minutes) {
|
|
2
|
+
if (minutes < 60)
|
|
3
|
+
return `${minutes} minutes`;
|
|
4
|
+
if (minutes < 120)
|
|
5
|
+
return "an hour";
|
|
6
|
+
if (minutes < 1440)
|
|
7
|
+
return `${Math.round(minutes / 60)} hours`;
|
|
8
|
+
return "a day";
|
|
9
|
+
}
|
|
10
|
+
export const defaultInboxLabels = {
|
|
11
|
+
askUs: "Ask us",
|
|
12
|
+
close: "Close",
|
|
13
|
+
back: "Back",
|
|
14
|
+
unread: "New reply",
|
|
15
|
+
aiNotice: (name, business) => `Hi there! You’re speaking with ${name}, ${business}’s AI receptionist. I can make mistakes, and you can ask for the team at any time.`,
|
|
16
|
+
greeting: () => "How can I help?",
|
|
17
|
+
teamGreeting: (business) => `Hi, welcome to ${business}. Send us a message and the team will reply here or by email.`,
|
|
18
|
+
homeTab: "Home",
|
|
19
|
+
messagesTab: "Messages",
|
|
20
|
+
noMessages: "No messages yet.",
|
|
21
|
+
ask: "Ask a question",
|
|
22
|
+
askSub: (name) => `${name} and the team can help`,
|
|
23
|
+
book: "Book online",
|
|
24
|
+
call: "Call us",
|
|
25
|
+
message: "Send a message",
|
|
26
|
+
messageSub: "The team will reply soon",
|
|
27
|
+
teamCanHelp: "The team can also help",
|
|
28
|
+
more: "More",
|
|
29
|
+
justNow: "Just now",
|
|
30
|
+
backAt: (day, time) => `Back ${day} ${time}`,
|
|
31
|
+
repliesWithin: (minutes) => `Usually replies within ${replyTime(minutes)}`,
|
|
32
|
+
replyTime,
|
|
33
|
+
emailLabel: "Email",
|
|
34
|
+
emailPlaceholder: "email@example.com",
|
|
35
|
+
emailRequired: "Enter your email so the team can reply.",
|
|
36
|
+
emailFormat: "Enter a valid email address.",
|
|
37
|
+
messageLabel: "Message",
|
|
38
|
+
messagePlaceholder: "Type your message",
|
|
39
|
+
send: "Send",
|
|
40
|
+
sending: "Sending…",
|
|
41
|
+
suggestPrices: "What are your prices?",
|
|
42
|
+
suggestBook: "Can I book a time?",
|
|
43
|
+
suggestHours: "When are you open?",
|
|
44
|
+
suggestAddress: "Where are you?",
|
|
45
|
+
suggestedQuestions: "Suggested questions",
|
|
46
|
+
privacyNotice: "By chatting, you agree to our {link}.",
|
|
47
|
+
privacyPolicy: "privacy policy",
|
|
48
|
+
dismiss: "Dismiss",
|
|
49
|
+
askForPerson: "Talk to a person",
|
|
50
|
+
aiReceptionist: "AI receptionist",
|
|
51
|
+
team: "Team",
|
|
52
|
+
typing: "Typing…",
|
|
53
|
+
chattingAs: (email) => `Chatting as ${email}`,
|
|
54
|
+
startNewChat: "Start a new chat",
|
|
55
|
+
notSent: "Not sent.",
|
|
56
|
+
tryAgain: "Try again",
|
|
57
|
+
teamWillReply: "The team will get back to you on this.",
|
|
58
|
+
typicallyReplies: (business, time) => `${business} typically replies in ${time}.`,
|
|
59
|
+
replyHereOrEmail: "They’ll reply here or by email.",
|
|
60
|
+
bookingMade: "You’re booked. The confirmation is in your email.",
|
|
61
|
+
safety: "If you or someone else is in immediate danger, or you may hurt yourself, please contact your local emergency services now. This chat is not watched live.",
|
|
62
|
+
cannotAnswer: "I’m not sure about that. I’ll pass this to the team.",
|
|
63
|
+
unavailable: "I can’t answer more right now.",
|
|
64
|
+
teamRepliedByEmail: "The team replied by email.",
|
|
65
|
+
unknownEvent: "The team will reply here or by email.",
|
|
66
|
+
linkExpired: "This link has expired.",
|
|
67
|
+
contactLimit: "We can’t start new chats today. Please try again tomorrow.",
|
|
68
|
+
tryLater: "Please try again later.",
|
|
69
|
+
rateLimited: "Too many messages. Wait a moment and try again.",
|
|
70
|
+
notPublished: "This site isn’t published yet.",
|
|
71
|
+
off: "Chat isn’t available right now.",
|
|
72
|
+
closed: "This chat is closed.",
|
|
73
|
+
sendFailed: "Your message wasn’t sent.",
|
|
74
|
+
emailUs: "Email us",
|
|
75
|
+
bookOffer: "Book",
|
|
76
|
+
poweredBy: "Powered by DoAny",
|
|
77
|
+
};
|
|
78
|
+
/** The defaults with a site's changes on top. A key set to `undefined` keeps its default. */
|
|
79
|
+
export const inboxLabels = (labels) => labels
|
|
80
|
+
? { ...defaultInboxLabels, ...Object.fromEntries(Object.entries(labels).filter(([, value]) => value !== undefined)) }
|
|
81
|
+
: defaultInboxLabels;
|
|
82
|
+
/**
|
|
83
|
+
* Which label each event code shows. The server stores codes, never words;
|
|
84
|
+
* a code this version does not know shows `unknownEvent`. The three codes for
|
|
85
|
+
* "the team has this chat" share one label.
|
|
86
|
+
*/
|
|
87
|
+
export const inboxEventLabelKeys = {
|
|
88
|
+
asked_for_person: "teamWillReply",
|
|
89
|
+
booking_made: "bookingMade",
|
|
90
|
+
safety: "safety",
|
|
91
|
+
cannot_answer: "cannotAnswer",
|
|
92
|
+
unavailable: "unavailable",
|
|
93
|
+
team_has_it: "teamWillReply",
|
|
94
|
+
sent_to_team: "teamWillReply",
|
|
95
|
+
team_replied_by_email: "teamRepliedByEmail",
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Which label each suggested question's code shows. The platform sends a code
|
|
99
|
+
* for a question it built from what the business has, never its words.
|
|
100
|
+
*/
|
|
101
|
+
export const inboxSuggestedQuestionLabelKeys = {
|
|
102
|
+
prices: "suggestPrices",
|
|
103
|
+
book: "suggestBook",
|
|
104
|
+
hours: "suggestHours",
|
|
105
|
+
address: "suggestAddress",
|
|
106
|
+
};
|
|
107
|
+
/** The codes that leave the client waiting for the team: each ends with how long that takes. */
|
|
108
|
+
const HANDOVER_CODES = new Set(["asked_for_person", "sent_to_team", "team_has_it", "cannot_answer", "unavailable"]);
|
|
109
|
+
export const isHandoverCode = (code) => code !== undefined && HANDOVER_CODES.has(code);
|
|
110
|
+
const words = (labels, key) => labels[key] || defaultInboxLabels[key];
|
|
111
|
+
/** A site's function label, or the default when it throws or gives nothing. */
|
|
112
|
+
function said(mine, fallback, ...args) {
|
|
113
|
+
try {
|
|
114
|
+
const text = mine(...args);
|
|
115
|
+
if (typeof text === "string" && text.trim())
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// The default below.
|
|
120
|
+
}
|
|
121
|
+
return fallback(...args);
|
|
122
|
+
}
|
|
123
|
+
const codeOf = (message) => message.kind === "event" && typeof message.payload?.code === "string" ? message.payload.code : undefined;
|
|
124
|
+
/**
|
|
125
|
+
* The handover lines that say how long the team takes: for each run of them
|
|
126
|
+
* with no team reply in between, the latest only, so the wait is said once,
|
|
127
|
+
* after the client's latest message.
|
|
128
|
+
*/
|
|
129
|
+
export function saysTheWait(messages) {
|
|
130
|
+
const ids = new Set();
|
|
131
|
+
let said = false;
|
|
132
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
133
|
+
const message = messages[index];
|
|
134
|
+
const code = codeOf(message);
|
|
135
|
+
if (message.author === "team" || code === "team_replied_by_email")
|
|
136
|
+
said = false;
|
|
137
|
+
else if (isHandoverCode(code) && !said) {
|
|
138
|
+
ids.add(message.id);
|
|
139
|
+
said = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return ids;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* How long the team takes: the business's next opening while it is closed,
|
|
146
|
+
* "{business} typically replies in {time}." while it is open and the platform
|
|
147
|
+
* knows, else "They'll reply here or by email."
|
|
148
|
+
*/
|
|
149
|
+
export function waitText(labels, wait) {
|
|
150
|
+
if (wait.backAt)
|
|
151
|
+
return wait.backAt;
|
|
152
|
+
if (wait.repliesWithin != null && wait.business) {
|
|
153
|
+
const time = said(labels.replyTime, defaultInboxLabels.replyTime, wait.repliesWithin);
|
|
154
|
+
return said(labels.typicallyReplies, defaultInboxLabels.typicallyReplies, wait.business, time);
|
|
155
|
+
}
|
|
156
|
+
return words(labels, "replyHereOrEmail");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The words for one event code. A handover code ends with the wait. `wait` is
|
|
160
|
+
* null for a handover line that a later one repeats before the team has
|
|
161
|
+
* replied: the wait is said once, under the latest, so such a line keeps only
|
|
162
|
+
* words of its own ("I'm not sure about that. I'll pass this to the team.")
|
|
163
|
+
* or is empty and not drawn. An empty translation of the safety line falls
|
|
164
|
+
* back to the default.
|
|
165
|
+
*/
|
|
166
|
+
export function eventText(code, labels, wait) {
|
|
167
|
+
const key = (code && inboxEventLabelKeys[code]) || "unknownEvent";
|
|
168
|
+
if (key === "safety" && !labels.safety.trim())
|
|
169
|
+
return defaultInboxLabels.safety;
|
|
170
|
+
const text = words(labels, key);
|
|
171
|
+
if (!isHandoverCode(code))
|
|
172
|
+
return text;
|
|
173
|
+
if (wait === null)
|
|
174
|
+
return key === "teamWillReply" ? "" : text;
|
|
175
|
+
const tail = waitText(labels, wait ?? { business: "", repliesWithin: null, backAt: null });
|
|
176
|
+
return code === "unavailable" ? `${text} ${words(labels, "teamWillReply")} ${tail}` : `${text} ${tail}`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The questions to offer, as the words a press sends: the business's own as
|
|
180
|
+
* written, a code as its label. A code this version does not know, an empty
|
|
181
|
+
* one and a repeat are left out.
|
|
182
|
+
*/
|
|
183
|
+
export function suggestedQuestionTexts(questions, labels) {
|
|
184
|
+
const found = [];
|
|
185
|
+
for (const question of questions ?? []) {
|
|
186
|
+
const key = typeof question?.code === "string" ? inboxSuggestedQuestionLabelKeys[question.code] : undefined;
|
|
187
|
+
const text = (typeof question?.text === "string" ? question.text : key ? words(labels, key) : "").trim();
|
|
188
|
+
if (text && !found.includes(text))
|
|
189
|
+
found.push(text);
|
|
190
|
+
}
|
|
191
|
+
return found;
|
|
192
|
+
}
|
|
193
|
+
/** The privacy line around its link: the words before `{link}` and after it. */
|
|
194
|
+
export function privacyNoticeParts(labels) {
|
|
195
|
+
const notice = words(labels, "privacyNotice");
|
|
196
|
+
const at = notice.indexOf("{link}");
|
|
197
|
+
return at < 0 ? [`${notice} `, ""] : [notice.slice(0, at), notice.slice(at + "{link}".length)];
|
|
198
|
+
}
|
|
199
|
+
/** The line no prop hides: a site may reword it, and an empty result falls back to the default. */
|
|
200
|
+
export function aiNoticeText(labels, name, business) {
|
|
201
|
+
const reworded = (() => {
|
|
202
|
+
try {
|
|
203
|
+
return labels.aiNotice(name, business);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
return typeof reworded === "string" && reworded.trim() ? reworded : defaultInboxLabels.aiNotice(name, business);
|
|
210
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { type InboxStorage } from "./storage.js";
|
|
2
|
+
import type { InboxBooked, InboxClient, InboxContact, InboxMe, InboxMessage } from "./types.js";
|
|
3
|
+
/** While an answer is on its way. */
|
|
4
|
+
export declare const POLL_REPLY_MS = 2000;
|
|
5
|
+
/** With the panel open. */
|
|
6
|
+
export declare const POLL_OPEN_MS = 5000;
|
|
7
|
+
/** With the panel closed, on a page with a chat. */
|
|
8
|
+
export declare const POLL_CLOSED_MS = 30000;
|
|
9
|
+
/** Polling stops after this long with nothing happening, and restarts on tab focus or panel open. */
|
|
10
|
+
export declare const IDLE_MS: number;
|
|
11
|
+
/** The longest wait between reads after failures. */
|
|
12
|
+
export declare const MAX_BACKOFF_MS = 60000;
|
|
13
|
+
/** A booking still waiting for its payment is reported again no sooner than this after the last time. */
|
|
14
|
+
export declare const BOOKING_REPORT_MS = 15000;
|
|
15
|
+
/** A booking still waiting for its payment after this long is no longer reported. */
|
|
16
|
+
export declare const BOOKING_REPORT_FOR_MS: number;
|
|
17
|
+
/**
|
|
18
|
+
* Why the last call was refused, for the page to say:
|
|
19
|
+
* - `contact_limit`: no more chats can start with a typed email today;
|
|
20
|
+
* - `chat_limit`: no more new chats right now;
|
|
21
|
+
* - `rate_limited`: too many messages, wait a moment;
|
|
22
|
+
* - `off`: the business turned the button off;
|
|
23
|
+
* - `closed`: the business blocked this client;
|
|
24
|
+
* - `not_published`: this address is a preview the platform takes nothing from;
|
|
25
|
+
* - `details_required`: the platform needs an email (an account it could not verify);
|
|
26
|
+
* - `failed`: anything else.
|
|
27
|
+
*/
|
|
28
|
+
export type InboxProblem = "contact_limit" | "chat_limit" | "rate_limited" | "off" | "closed" | "not_published" | "details_required" | "failed";
|
|
29
|
+
/** A first message sent but not confirmed: shown locked, resent unchanged. */
|
|
30
|
+
export interface InboxPendingMessage {
|
|
31
|
+
text: string;
|
|
32
|
+
intent: "ask" | "person";
|
|
33
|
+
email: string | null;
|
|
34
|
+
state: "sending" | "failed";
|
|
35
|
+
}
|
|
36
|
+
/** A first message the platform refused for good: the page puts it back in the box. */
|
|
37
|
+
export interface InboxDraft {
|
|
38
|
+
text: string;
|
|
39
|
+
intent: "ask" | "person";
|
|
40
|
+
contact: InboxContact | null;
|
|
41
|
+
}
|
|
42
|
+
export interface InboxState {
|
|
43
|
+
/** Whether there is a chat is known (the visitor's sign-in is). */
|
|
44
|
+
ready: boolean;
|
|
45
|
+
/** This browser holds a chat. */
|
|
46
|
+
hasChat: boolean;
|
|
47
|
+
/** The address the chat started with; null when signed in, opened from an emailed link, or none. */
|
|
48
|
+
email: string | null;
|
|
49
|
+
me: InboxMe | null;
|
|
50
|
+
/** The chat, oldest first. */
|
|
51
|
+
messages: InboxMessage[];
|
|
52
|
+
/** An answer is on its way: show typing dots. */
|
|
53
|
+
replyPending: boolean;
|
|
54
|
+
paused: boolean;
|
|
55
|
+
/** The business turned the button off during the chat: earlier messages stay, nothing can be sent. */
|
|
56
|
+
off: boolean;
|
|
57
|
+
/** The business blocked this client. */
|
|
58
|
+
closed: boolean;
|
|
59
|
+
pending: InboxPendingMessage | null;
|
|
60
|
+
draft: InboxDraft | null;
|
|
61
|
+
/** A call is in flight. */
|
|
62
|
+
busy: boolean;
|
|
63
|
+
problem: InboxProblem | null;
|
|
64
|
+
/** Seconds the platform asked to wait, with `rate_limited`. */
|
|
65
|
+
retryAfter: number | null;
|
|
66
|
+
/** The emailed link could not be opened. */
|
|
67
|
+
linkExpired: boolean;
|
|
68
|
+
/** A team reply came while the panel was closed. */
|
|
69
|
+
unread: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface InboxStartInput {
|
|
72
|
+
text: string;
|
|
73
|
+
/** `ask` (default), or `person` for Send a message: the AI does not answer. */
|
|
74
|
+
intent?: "ask" | "person";
|
|
75
|
+
/** Required unless a verified site account is signed in. */
|
|
76
|
+
contact?: InboxContact;
|
|
77
|
+
}
|
|
78
|
+
export interface InboxSessionOptions {
|
|
79
|
+
/** The chat is offered on this page (or the panel is open): read it. Default true. */
|
|
80
|
+
active: boolean;
|
|
81
|
+
/** The panel is open: read every 5 seconds, mark replies seen. */
|
|
82
|
+
open: boolean;
|
|
83
|
+
/** The page is visible: replies may be marked seen. */
|
|
84
|
+
visible: boolean;
|
|
85
|
+
pagePath?: string;
|
|
86
|
+
language?: string;
|
|
87
|
+
capabilities?: string[];
|
|
88
|
+
}
|
|
89
|
+
interface Refusal {
|
|
90
|
+
status: number | undefined;
|
|
91
|
+
notFound: boolean;
|
|
92
|
+
problem: InboxProblem;
|
|
93
|
+
retryAfter: number | null;
|
|
94
|
+
}
|
|
95
|
+
/** What a refusal means here. The SDK's error keeps the status, the code and the response body (`data`). */
|
|
96
|
+
export declare function refusalOf(error: unknown): Refusal;
|
|
97
|
+
/**
|
|
98
|
+
* One browser's chat with the business: the thread and its polling, over the
|
|
99
|
+
* SDK's `client.inbox`, which holds the token and a first message still owed.
|
|
100
|
+
* No DOM: the default widget and a site's own chat UI drive it the same way.
|
|
101
|
+
* `useInbox()` wraps it for React.
|
|
102
|
+
*/
|
|
103
|
+
export declare class InboxSession {
|
|
104
|
+
private readonly client;
|
|
105
|
+
private readonly storage;
|
|
106
|
+
private state;
|
|
107
|
+
private readonly listeners;
|
|
108
|
+
private options;
|
|
109
|
+
private actor;
|
|
110
|
+
private chat;
|
|
111
|
+
private cursor;
|
|
112
|
+
private serverMessages;
|
|
113
|
+
private localMessages;
|
|
114
|
+
private lastSend;
|
|
115
|
+
private timer;
|
|
116
|
+
private inFlight;
|
|
117
|
+
private failures;
|
|
118
|
+
private waitSeconds;
|
|
119
|
+
private lastActivity;
|
|
120
|
+
private stopped;
|
|
121
|
+
/** Bumped whenever the chat changes hands, so a late answer for the old one is dropped. */
|
|
122
|
+
private generation;
|
|
123
|
+
private attached;
|
|
124
|
+
private seenFor;
|
|
125
|
+
/** Bookings already reported, by registration id. */
|
|
126
|
+
private readonly reported;
|
|
127
|
+
/**
|
|
128
|
+
* Bookings reported while waiting for their payment, oldest first. The
|
|
129
|
+
* platform notes a booking only once it is confirmed, so each is reported
|
|
130
|
+
* again alongside the reads until its line is in the thread.
|
|
131
|
+
*/
|
|
132
|
+
private unpaid;
|
|
133
|
+
private resolveReady;
|
|
134
|
+
private readonly whenReady;
|
|
135
|
+
/** Called after each read, so the page can re-read the button's settings alongside it. */
|
|
136
|
+
onPoll: (() => void) | null;
|
|
137
|
+
constructor(client: InboxClient, storage: InboxStorage);
|
|
138
|
+
subscribe: (listener: () => void) => (() => void);
|
|
139
|
+
getState: () => InboxState;
|
|
140
|
+
private set;
|
|
141
|
+
/** Starts polling (when there is a chat). A detached session keeps its state and makes no calls. */
|
|
142
|
+
attach(): void;
|
|
143
|
+
detach(): void;
|
|
144
|
+
setOptions(options: Partial<InboxSessionOptions>): void;
|
|
145
|
+
/**
|
|
146
|
+
* Who is using the site: `guest`, or `user:<id>` for a signed-in account.
|
|
147
|
+
* The SDK keeps a chat per account, so a change reads the chat again.
|
|
148
|
+
*/
|
|
149
|
+
setActor(actor: string): void;
|
|
150
|
+
/** Reads which chat this browser holds, from the SDK, and starts from there. */
|
|
151
|
+
private load;
|
|
152
|
+
private reset;
|
|
153
|
+
private showPending;
|
|
154
|
+
/** The first message of a chat. Resolves true once the platform has it. */
|
|
155
|
+
start: (input: InboxStartInput) => Promise<boolean>;
|
|
156
|
+
/** Sends the first message that was not confirmed again, unchanged. */
|
|
157
|
+
retryPending: () => Promise<boolean>;
|
|
158
|
+
private sendPending;
|
|
159
|
+
/** A later message in the chat. Resolves true once the platform has it; on false the page keeps the words. */
|
|
160
|
+
send: (input: string) => Promise<boolean>;
|
|
161
|
+
/** Hands the chat to a person, with the words in the box if any. */
|
|
162
|
+
askForPerson: (input?: string) => Promise<boolean>;
|
|
163
|
+
/**
|
|
164
|
+
* Opens the token of a reply's emailed link: the chat moves to this browser.
|
|
165
|
+
* A link that no longer works leaves this browser's own chat as it was and
|
|
166
|
+
* sets `linkExpired`.
|
|
167
|
+
*/
|
|
168
|
+
resume: (linkToken: string) => Promise<boolean>;
|
|
169
|
+
/**
|
|
170
|
+
* A booking the site's flow made from an offer in this chat (the flow's
|
|
171
|
+
* `onBooked`). The platform notes it on the chat, and the business's Inbox
|
|
172
|
+
* and Calendar link the two, when the booking is this chat's and confirmed.
|
|
173
|
+
* Each booking is reported once; one still waiting for its payment
|
|
174
|
+
* (`registration.status` `pending_payment`) is not booked yet, so it is
|
|
175
|
+
* reported again alongside the reads, for up to an hour, until the thread
|
|
176
|
+
* shows its line. Resolves false when there was nothing to report or the
|
|
177
|
+
* report did not go through; the booking stands either way.
|
|
178
|
+
*/
|
|
179
|
+
reportBooking: (result: InboxBooked) => Promise<boolean>;
|
|
180
|
+
/** Reports the bookings still waiting for their payment again. A report that fails waits for the next read. */
|
|
181
|
+
private reportUnpaid;
|
|
182
|
+
/** Forgets this browser's chat with no call: the next message starts a new one. */
|
|
183
|
+
newChat: () => void;
|
|
184
|
+
/** Something happened (the tab came back, the panel opened): polling restarts if it had stopped. */
|
|
185
|
+
wake: (now?: boolean) => void;
|
|
186
|
+
/** The panel shows the newest reply: clear the button's mark. */
|
|
187
|
+
markRead: () => void;
|
|
188
|
+
private touch;
|
|
189
|
+
private clearTimer;
|
|
190
|
+
/** The next read: 2 s while an answer is on its way, 5 s with the panel open, 30 s closed; longer after failures; none after 30 idle minutes. */
|
|
191
|
+
private schedule;
|
|
192
|
+
private poll;
|
|
193
|
+
/** The platform's rows in its order, each once; returns the ones not seen before. */
|
|
194
|
+
private merge;
|
|
195
|
+
private thread;
|
|
196
|
+
private newestTeamMessage;
|
|
197
|
+
private apply;
|
|
198
|
+
/** Tells the platform the newest team reply was shown: only with the panel open and the page visible. */
|
|
199
|
+
private maybeSeen;
|
|
200
|
+
private refused;
|
|
201
|
+
/** Stops every timer. The session can be attached again. */
|
|
202
|
+
dispose(): void;
|
|
203
|
+
}
|
|
204
|
+
export {};
|