@sunboard/react 1.0.0 → 2.0.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 +46 -26
- package/dist/index.d.mts +299 -47
- package/dist/index.mjs +1196 -264
- package/dist/styles.css +251 -68
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,7 +15,12 @@ framework with your key already filled in, and a prompt you can hand to your cod
|
|
|
15
15
|
import "@sunboard/react/styles.css";
|
|
16
16
|
import { SunboardOverlay, SunboardProvider } from "@sunboard/react";
|
|
17
17
|
|
|
18
|
-
<SunboardProvider
|
|
18
|
+
<SunboardProvider
|
|
19
|
+
connection={{
|
|
20
|
+
publishableKey: SUNBOARD_KEY,
|
|
21
|
+
userToken,
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
19
24
|
<App />
|
|
20
25
|
<SunboardOverlay />
|
|
21
26
|
</SunboardProvider>
|
|
@@ -49,7 +54,30 @@ NEXT_PUBLIC_SUNBOARD_KEY=<your publishable key>
|
|
|
49
54
|
VITE_SUNBOARD_KEY=<your publishable key>
|
|
50
55
|
```
|
|
51
56
|
|
|
52
|
-
### 3.
|
|
57
|
+
### 3. Make a user token on your server
|
|
58
|
+
|
|
59
|
+
Every request carries a token that says who the user is, so nobody can act as another user. Make
|
|
60
|
+
it on your server with an identity secret from **Settings › Keys**. The secret never leaves your
|
|
61
|
+
server: not in client code, not in a `NEXT_PUBLIC_` or `VITE_` variable, not in a repository. In
|
|
62
|
+
Node, with `jsonwebtoken`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import jwt from "jsonwebtoken";
|
|
66
|
+
|
|
67
|
+
export function sunboardUserToken(user: { id: string; email: string; firstName?: string }) {
|
|
68
|
+
return jwt.sign(
|
|
69
|
+
{ email: user.email, name: user.firstName },
|
|
70
|
+
process.env.SUNBOARD_IDENTITY_SECRET!,
|
|
71
|
+
{ subject: user.id, expiresIn: "7d" },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Set the expiry to about how long a sign-in to your app lasts, because the widget cannot ask for a
|
|
77
|
+
new token on its own. Any language with a JWT library works the same way. Sign with HS256 and the
|
|
78
|
+
secret, put the user's id in `sub`, and add `email` and an optional `name`.
|
|
79
|
+
|
|
80
|
+
### 4. Create one Sunboard component
|
|
53
81
|
|
|
54
82
|
It connects to Sunboard, shows tours and the Help launcher, and lets a tour change pages through
|
|
55
83
|
your router. This is the Next.js version:
|
|
@@ -63,22 +91,13 @@ import type { ReactNode } from "react";
|
|
|
63
91
|
import { useRouter } from "next/navigation";
|
|
64
92
|
import { SunboardOverlay, SunboardProvider } from "@sunboard/react";
|
|
65
93
|
|
|
66
|
-
export function Sunboard({
|
|
67
|
-
userId,
|
|
68
|
-
firstName,
|
|
69
|
-
children,
|
|
70
|
-
}: {
|
|
71
|
-
userId: string;
|
|
72
|
-
firstName?: string;
|
|
73
|
-
children: ReactNode;
|
|
74
|
-
}) {
|
|
94
|
+
export function Sunboard({ userToken, children }: { userToken: string; children: ReactNode }) {
|
|
75
95
|
const router = useRouter();
|
|
76
96
|
return (
|
|
77
97
|
<SunboardProvider
|
|
78
98
|
connection={{
|
|
79
99
|
publishableKey: process.env.NEXT_PUBLIC_SUNBOARD_KEY!,
|
|
80
|
-
|
|
81
|
-
user: { firstName },
|
|
100
|
+
userToken,
|
|
82
101
|
}}
|
|
83
102
|
navigate={(path) => router.push(path)}
|
|
84
103
|
>
|
|
@@ -109,18 +128,17 @@ import sunboardCss from "@sunboard/react/styles.css?url";
|
|
|
109
128
|
`useRouter` and `useNavigate` work only inside the router, so mount the component where the
|
|
110
129
|
table below says.
|
|
111
130
|
|
|
112
|
-
###
|
|
131
|
+
### 5. Wrap your signed-in pages
|
|
113
132
|
|
|
114
|
-
Put the component around the pages a signed-in user sees, and pass
|
|
115
|
-
stays the same for each person.
|
|
116
|
-
user ("Hey Sam,"), without it it says
|
|
133
|
+
Put the component around the pages a signed-in user sees, and pass the token from step 3. Use an
|
|
134
|
+
id that stays the same for each person. Your team sees the email when the user writes to them.
|
|
135
|
+
The name is optional. With it the assistant greets the user ("Hey Sam,"), without it it says
|
|
136
|
+
"Hey there,".
|
|
117
137
|
|
|
118
138
|
```tsx
|
|
119
139
|
import { Sunboard } from "./sunboard";
|
|
120
140
|
|
|
121
|
-
<Sunboard
|
|
122
|
-
{children}
|
|
123
|
-
</Sunboard>
|
|
141
|
+
<Sunboard userToken={userToken}>{children}</Sunboard>
|
|
124
142
|
```
|
|
125
143
|
|
|
126
144
|
| Framework | Where |
|
|
@@ -133,7 +151,7 @@ import { Sunboard } from "./sunboard";
|
|
|
133
151
|
A Next.js app with route groups, such as `app/(marketing)/layout.tsx` and
|
|
134
152
|
`app/(app)/layout.tsx`, wraps `children` in the signed-in group's layout only.
|
|
135
153
|
|
|
136
|
-
###
|
|
154
|
+
### 6. Check it works
|
|
137
155
|
|
|
138
156
|
Start your app and sign in. **Settings › Install** in the Sunboard app shows **Connected**. Then
|
|
139
157
|
add your app's address under **Settings › Project**, and use **Preview** on any guide to see it
|
|
@@ -153,7 +171,7 @@ in your app.
|
|
|
153
171
|
- **Preview.** The **Preview** button on a guide in the Sunboard app opens your app with
|
|
154
172
|
`?sunboardPreview=<key>` in the address and shows paused guides as well as live ones, in that
|
|
155
173
|
browser only. `?sunboardPreview=off` ends it.
|
|
156
|
-
- **Users.** The active-user limit on your plan counts the ids
|
|
174
|
+
- **Users.** The active-user limit on your plan counts the ids in your user tokens, so give each
|
|
157
175
|
person one id that never changes.
|
|
158
176
|
|
|
159
177
|
## Reference
|
|
@@ -162,7 +180,7 @@ in your app.
|
|
|
162
180
|
|
|
163
181
|
| Prop | Type | Default | What it does |
|
|
164
182
|
| --- | --- | --- | --- |
|
|
165
|
-
| `connection` | `{ publishableKey: string;
|
|
183
|
+
| `connection` | `{ publishableKey: string; userToken: string }` | required | Your publishable key and the user token from step 3. |
|
|
166
184
|
| `navigate` | `(path: string) => void \| Promise<void>` | full page load | How a tour moves to another page. Pass your router. Without it, a tour loads the next page in full and carries on there. |
|
|
167
185
|
| `labels` | `LabelOverrides` | English | Your own words for any of the SDK's buttons and text, for example `labels={{ launcher: { open: "Support" } }}`. The rest stay English. `enLabels` has every one. |
|
|
168
186
|
| `mode` | `"auto" \| "light" \| "dark"` | `"auto"` | Light or dark. `"auto"` follows a `dark` or `light` class, or a `data-theme`, `data-mode` or `data-color-scheme` attribute, on `<html>` or `<body>`, then the system setting, and tracks changes live. |
|
|
@@ -207,7 +225,7 @@ Call `useLauncher()` once for your help menu and pass what it returns down.
|
|
|
207
225
|
| `tourRunning` | `true` while a tour runs. |
|
|
208
226
|
| `opened()` | Call it when your menu opens, so the open counts in Analytics. |
|
|
209
227
|
| `start(guideKey, replyId?)` | Starts a guide. Pass the id of the answer that offered it, and the conversation gets a line after that answer when the tour ends. |
|
|
210
|
-
| `messages` | The conversation with the assistant, oldest first. Each `Message` has an `id` and a `kind`: `question`, `waiting`, `reply`, `error` or `tour`. A `reply` holds an `AskReply` with the answer's text in `answer
|
|
228
|
+
| `messages` | The conversation with the assistant, oldest first. Each `Message` has an `id` and a `kind`: `question`, `waiting`, `reply`, `error` or `tour`. A `reply` holds an `AskReply` with the answer's text in `answer` and `guideKey` when it offers a guide to start. |
|
|
211
229
|
| `waiting` | `true` while a question is out and its answer has not come back. |
|
|
212
230
|
| `rateLimitedUntil` | While the user has asked too many questions this hour, the time they can ask again, as an ISO string. Otherwise `undefined`. |
|
|
213
231
|
| `handedOff` | `true` while a tour started from the conversation runs. It turns `false` when the tour ends, the moment to show your panel again. |
|
|
@@ -249,6 +267,8 @@ the colors in use as CSS variables to spread onto your menu so it matches the to
|
|
|
249
267
|
- **Nothing shows in my app.** Add `?sunboardDebug=1` to the page address and open the browser
|
|
250
268
|
console with the Verbose level on. A wrong key shows as a failed request. No lines at all means
|
|
251
269
|
the provider is not mounted around the page you are on.
|
|
270
|
+
- **Every request fails with 401.** The user token is not signed with the identity secret from
|
|
271
|
+
**Settings › Keys**, has expired, or has no `sub` or `email`.
|
|
252
272
|
- **Settings › Install does not say Connected.** It turns Connected on the first request from your
|
|
253
273
|
app, so load a page where the provider is mounted, signed in, and reload the Install tab.
|
|
254
274
|
- **A tour reloads the page instead of changing route.** Pass `navigate` with your router. Without
|
|
@@ -258,9 +278,9 @@ the colors in use as CSS variables to spread onto your menu so it matches the to
|
|
|
258
278
|
- **Requests to api.sunboardhq.com are blocked.** Your Content Security Policy needs it in
|
|
259
279
|
`connect-src`.
|
|
260
280
|
- **Next.js says the component needs "use client".** Put `"use client"` on the first line of your
|
|
261
|
-
`sunboard.tsx`, as in step
|
|
281
|
+
`sunboard.tsx`, as in step 4. The package itself is already client code.
|
|
262
282
|
- **useRouter or useNavigate throws.** Mount the component inside the router, where the table in
|
|
263
|
-
step
|
|
283
|
+
step 5 says.
|
|
264
284
|
|
|
265
285
|
## License
|
|
266
286
|
|
package/dist/index.d.mts
CHANGED
|
@@ -68,6 +68,8 @@ interface ComponentSettings {
|
|
|
68
68
|
launcher: {
|
|
69
69
|
showBranding: boolean;
|
|
70
70
|
supportUrl: string | null;
|
|
71
|
+
talkToPerson: boolean;
|
|
72
|
+
replyTime: string;
|
|
71
73
|
};
|
|
72
74
|
tour: {
|
|
73
75
|
showStepCount: boolean;
|
|
@@ -332,39 +334,95 @@ type GuideEventReport = {
|
|
|
332
334
|
});
|
|
333
335
|
/**
|
|
334
336
|
* ANSWERED is an answer written from the docs and checked against them.
|
|
335
|
-
* UNSUPPORTED is one the check did not back, so the visitor
|
|
336
|
-
*
|
|
337
|
-
* GUIDE offers a tour, with an answer when
|
|
337
|
+
* UNSUPPORTED is one the check did not back twice, so the visitor gets the
|
|
338
|
+
* not-covered reply. NONE found nothing to answer with.
|
|
339
|
+
* GUIDE offers a tour, with an answer when one was written and backed.
|
|
338
340
|
*/
|
|
339
341
|
type AskOutcome = "GUIDE" | "ANSWERED" | "UNSUPPORTED" | "NONE";
|
|
340
|
-
/** One section the answer came from. */
|
|
341
|
-
interface AskArticle {
|
|
342
|
-
/** The section's id. */
|
|
343
|
-
id: string;
|
|
344
|
-
/** The article's title. */
|
|
345
|
-
title: string;
|
|
346
|
-
/** Where the article is read. An article written in Sunboard has none. */
|
|
347
|
-
url?: string;
|
|
348
|
-
/** The section's heading, when it has one. */
|
|
349
|
-
heading?: string;
|
|
350
|
-
}
|
|
351
342
|
/** `id` is the stored question's id. `answer` is the whole text to show. */
|
|
352
343
|
interface AskReply {
|
|
353
344
|
id: string;
|
|
354
345
|
/** Sent back with the next question so it is read as a follow-up. */
|
|
355
|
-
|
|
356
|
-
/** This question opened its
|
|
346
|
+
threadId: string;
|
|
347
|
+
/** This question opened its thread, so the widget greets the visitor. */
|
|
357
348
|
firstTurn: boolean;
|
|
358
349
|
outcome: AskOutcome;
|
|
359
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* Plain text with blank lines between paragraphs, `1.` and `-` lists, and
|
|
352
|
+
* `**bold**`. Empty when a guide is offered with nothing written.
|
|
353
|
+
*/
|
|
360
354
|
answer: string;
|
|
361
|
-
/** At most three, in the order the answer used them. */
|
|
362
|
-
sources: AskArticle[];
|
|
363
355
|
guideKey?: string;
|
|
356
|
+
/** Set when the answer is the not-covered sentence and the project has a support link. */
|
|
364
357
|
supportUrl?: string;
|
|
365
358
|
}
|
|
366
359
|
/** What the server is doing while the visitor waits, in this order. */
|
|
367
360
|
type AskStage = "searching" | "writing" | "checking";
|
|
361
|
+
type ConversationStatus = "OPEN" | "PENDING" | "CLOSED";
|
|
362
|
+
type ConversationAuthor = "END_USER" | "TEAM";
|
|
363
|
+
/** Every conversation route answers this while the plan or the project's Talk to a person is off. */
|
|
364
|
+
interface ConversationOff {
|
|
365
|
+
off: true;
|
|
366
|
+
}
|
|
367
|
+
/** The end user's conversation that is not closed, as the widget polls it. Dates are ISO. */
|
|
368
|
+
interface ConversationSummary {
|
|
369
|
+
id: string;
|
|
370
|
+
status: ConversationStatus;
|
|
371
|
+
lastMessageAt: string;
|
|
372
|
+
lastMessageBy: ConversationAuthor;
|
|
373
|
+
lastMessageText: string;
|
|
374
|
+
/** The team member who wrote the last message, or null when the end user did. */
|
|
375
|
+
lastMessageWho: string | null;
|
|
376
|
+
/** A reply from the team the end user has not opened yet. */
|
|
377
|
+
unread: boolean;
|
|
378
|
+
}
|
|
379
|
+
/** GET /runtime/conversation. */
|
|
380
|
+
interface ConversationPoll {
|
|
381
|
+
open: ConversationSummary | null;
|
|
382
|
+
hasPast: boolean;
|
|
383
|
+
}
|
|
384
|
+
interface ConversationMessageWire {
|
|
385
|
+
id: string;
|
|
386
|
+
by: ConversationAuthor;
|
|
387
|
+
/** The team member's name. Null for the end user's own messages. */
|
|
388
|
+
who: string | null;
|
|
389
|
+
text: string;
|
|
390
|
+
sentAt: string;
|
|
391
|
+
}
|
|
392
|
+
/** A question the end user asked the assistant, shown folded in the conversation. */
|
|
393
|
+
interface FoldedQuestionWire {
|
|
394
|
+
id: string;
|
|
395
|
+
text: string;
|
|
396
|
+
askedAt: string;
|
|
397
|
+
outcome: AskOutcome;
|
|
398
|
+
}
|
|
399
|
+
/** GET /runtime/conversation/:id. With `after`, `messages` holds only the newer ones. */
|
|
400
|
+
interface ConversationView {
|
|
401
|
+
id: string;
|
|
402
|
+
status: ConversationStatus;
|
|
403
|
+
startedAt: string;
|
|
404
|
+
closedAt: string | null;
|
|
405
|
+
closed: boolean;
|
|
406
|
+
messages: ConversationMessageWire[];
|
|
407
|
+
/** Asked the assistant before the conversation started, on the thread it started from. */
|
|
408
|
+
before: FoldedQuestionWire[];
|
|
409
|
+
/** Asked the assistant while the conversation was not closed. */
|
|
410
|
+
during: FoldedQuestionWire[];
|
|
411
|
+
}
|
|
412
|
+
/** POST /runtime/conversation and POST /runtime/conversation/:id/messages. */
|
|
413
|
+
interface ConversationSent {
|
|
414
|
+
summary: ConversationSummary;
|
|
415
|
+
message: ConversationMessageWire;
|
|
416
|
+
}
|
|
417
|
+
/** One of GET /runtime/conversations, most recently closed first. */
|
|
418
|
+
interface PastConversation {
|
|
419
|
+
id: string;
|
|
420
|
+
/** The end user's first message. */
|
|
421
|
+
title: string;
|
|
422
|
+
closedAt: string | null;
|
|
423
|
+
/** The team member who closed it. */
|
|
424
|
+
closedBy: string | null;
|
|
425
|
+
}
|
|
368
426
|
//#endregion
|
|
369
427
|
//#region ../core/src/anchors.d.ts
|
|
370
428
|
type AnchorPart = {
|
|
@@ -402,8 +460,6 @@ interface Labels {
|
|
|
402
460
|
send: string;
|
|
403
461
|
/** The pill that scrolls back down to the newest message. */
|
|
404
462
|
latest: string;
|
|
405
|
-
/** On an older reply whose sources are folded away. */
|
|
406
|
-
sourcesCount: (count: number) => string;
|
|
407
463
|
/** Over the first answer of a conversation. `name` is the connection's `user.firstName`. */
|
|
408
464
|
greeting: (name?: string) => string;
|
|
409
465
|
/** Under an answer that offers a guide, above Show me how. */
|
|
@@ -417,7 +473,7 @@ interface Labels {
|
|
|
417
473
|
/** The line once the visitor picked a star. */
|
|
418
474
|
ratingThanks: string;
|
|
419
475
|
/** The thread's own name, for a screen reader. */
|
|
420
|
-
|
|
476
|
+
thread: string;
|
|
421
477
|
/** Said once to a screen reader while a question is out. */
|
|
422
478
|
writingStatus: string;
|
|
423
479
|
/** While the question is out, one for each stage the server reports. */
|
|
@@ -428,8 +484,6 @@ interface Labels {
|
|
|
428
484
|
/** After six seconds of waiting. */
|
|
429
485
|
slow: string;
|
|
430
486
|
};
|
|
431
|
-
/** The name of the group of sections an answer came from, for a screen reader. */
|
|
432
|
-
sources: string;
|
|
433
487
|
/** Over the guides listed before the first question, this page's first. */
|
|
434
488
|
commonQuestions: string;
|
|
435
489
|
/** The one button under an answer that has a guide behind it. */
|
|
@@ -447,15 +501,50 @@ interface Labels {
|
|
|
447
501
|
lostBody: string;
|
|
448
502
|
tryAgain: string;
|
|
449
503
|
/**
|
|
450
|
-
* What the server answers when nothing
|
|
451
|
-
* section,
|
|
504
|
+
* What the server answers when there is nothing to show: no guide and no
|
|
505
|
+
* section, or no guide and an answer the check did not back twice.
|
|
452
506
|
*/
|
|
453
507
|
notCovered: (product: string) => string;
|
|
454
|
-
coveredBy: (article: string, heading?: string) => string;
|
|
455
|
-
guideFound: string;
|
|
456
508
|
closeAria: string;
|
|
457
509
|
poweredBy: string;
|
|
458
510
|
};
|
|
511
|
+
/**
|
|
512
|
+
* Talk to a person. `company` is the project's name as the launcher shows
|
|
513
|
+
* it, `name` the connection's `user.firstName`, `who` a team member's name.
|
|
514
|
+
*/
|
|
515
|
+
support: {
|
|
516
|
+
talkToPerson: string;
|
|
517
|
+
/** The header of the conversation. */
|
|
518
|
+
title: (company: string) => string;
|
|
519
|
+
back: string;
|
|
520
|
+
/** The line between the assistant's turns and the conversation. */
|
|
521
|
+
talking: (company: string) => string;
|
|
522
|
+
/** The fixed first line of every conversation, and who it is from. */
|
|
523
|
+
greeting: (name?: string) => string;
|
|
524
|
+
from: (company: string) => string;
|
|
525
|
+
boxLabel: (company: string) => string;
|
|
526
|
+
placeholder: (company: string) => string;
|
|
527
|
+
/** Over a message from the team. `time` is `clock` today and `ago` before. */
|
|
528
|
+
teamMessage: (who: string | null, company: string, time: string) => string;
|
|
529
|
+
/** A question asked of the assistant while the conversation was open, folded. */
|
|
530
|
+
asked: (question: string) => string;
|
|
531
|
+
closed: (company: string) => string;
|
|
532
|
+
reopen: string;
|
|
533
|
+
/** The bar over the assistant's thread, and the row on the start screen. */
|
|
534
|
+
conversationWith: (company: string) => string;
|
|
535
|
+
replied: (who: string) => string;
|
|
536
|
+
waiting: string;
|
|
537
|
+
newReply: string;
|
|
538
|
+
openAriaNewReply: string;
|
|
539
|
+
past: string;
|
|
540
|
+
/** Under a past conversation. `when` is from `ago`. */
|
|
541
|
+
closedWhen: (when: string, who: string | null) => string;
|
|
542
|
+
ago: (days: number) => string;
|
|
543
|
+
tooMany: string;
|
|
544
|
+
writeAgainIn: (minutes: number) => string;
|
|
545
|
+
lost: string;
|
|
546
|
+
lostBody: string;
|
|
547
|
+
};
|
|
459
548
|
tour: {
|
|
460
549
|
back: string;
|
|
461
550
|
next: string;
|
|
@@ -520,12 +609,71 @@ export declare function detectThemeMode(): ThemeMode;
|
|
|
520
609
|
*/
|
|
521
610
|
export declare function useThemeMode(setting: ThemeModeSetting): ThemeMode;
|
|
522
611
|
//#endregion
|
|
612
|
+
//#region src/memory-support.d.ts
|
|
613
|
+
interface ScriptedMessage {
|
|
614
|
+
by: ConversationAuthor;
|
|
615
|
+
/** The team member, for a message from the team. */
|
|
616
|
+
who?: string;
|
|
617
|
+
text: string;
|
|
618
|
+
sentAt: string;
|
|
619
|
+
}
|
|
620
|
+
interface ScriptedConversation {
|
|
621
|
+
messages: ScriptedMessage[];
|
|
622
|
+
/** Asked the assistant before the conversation started. */
|
|
623
|
+
before?: Omit<FoldedQuestionWire, "id" | "outcome">[];
|
|
624
|
+
/** Asked the assistant while it was open. */
|
|
625
|
+
during?: Omit<FoldedQuestionWire, "id" | "outcome">[];
|
|
626
|
+
/** A reply from the team the end user has not opened. */
|
|
627
|
+
unread?: boolean;
|
|
628
|
+
/** Set on a closed conversation. */
|
|
629
|
+
closedAt?: string;
|
|
630
|
+
closedBy?: string;
|
|
631
|
+
}
|
|
632
|
+
interface SupportScript {
|
|
633
|
+
/** Conversations that exist when the page loads, at most one of them not closed. */
|
|
634
|
+
conversations?: ScriptedConversation[];
|
|
635
|
+
/** The team answers the first message of a conversation this long after it is sent. */
|
|
636
|
+
reply?: {
|
|
637
|
+
who: string;
|
|
638
|
+
text: string;
|
|
639
|
+
afterMs: number;
|
|
640
|
+
};
|
|
641
|
+
/** Every write is refused this way. */
|
|
642
|
+
refuse?: ConversationRefused["refused"];
|
|
643
|
+
}
|
|
644
|
+
declare class MemorySupport implements SupportTransport {
|
|
645
|
+
private readonly script;
|
|
646
|
+
readonly starts: ConversationStart[];
|
|
647
|
+
readonly sent: {
|
|
648
|
+
id: string;
|
|
649
|
+
text: string;
|
|
650
|
+
}[];
|
|
651
|
+
readonly reads: string[];
|
|
652
|
+
polls: number;
|
|
653
|
+
views: number;
|
|
654
|
+
private readonly conversations;
|
|
655
|
+
constructor(script: SupportScript);
|
|
656
|
+
conversationPoll(): Promise<ConversationPoll>;
|
|
657
|
+
conversation(id: string, after?: string): Promise<ConversationView | null>;
|
|
658
|
+
startConversation(start: ConversationStart): Promise<ConversationWrite>;
|
|
659
|
+
sendMessage(id: string, text: string): Promise<ConversationWrite>;
|
|
660
|
+
markRead(id: string): Promise<void>;
|
|
661
|
+
pastConversations(): Promise<PastConversation[]>;
|
|
662
|
+
/** The team writes into the open conversation. */
|
|
663
|
+
teamReplies(text: string, who: string): void;
|
|
664
|
+
/** The team closes the open conversation. */
|
|
665
|
+
teamCloses(who: string): void;
|
|
666
|
+
private write;
|
|
667
|
+
private open;
|
|
668
|
+
private find;
|
|
669
|
+
}
|
|
670
|
+
//#endregion
|
|
523
671
|
//#region src/transport.d.ts
|
|
524
672
|
interface AskQuestion {
|
|
525
673
|
question: string;
|
|
526
674
|
pathname: string;
|
|
527
675
|
/** The conversation the last reply named; left out on the first question. */
|
|
528
|
-
|
|
676
|
+
threadId?: string;
|
|
529
677
|
}
|
|
530
678
|
/**
|
|
531
679
|
* What one question got. A refusal (the question was too long, or too many
|
|
@@ -541,14 +689,52 @@ type AskResult = {
|
|
|
541
689
|
message: string;
|
|
542
690
|
retryAt?: string;
|
|
543
691
|
};
|
|
692
|
+
/** The first message of a conversation with the project's team. */
|
|
693
|
+
interface ConversationStart {
|
|
694
|
+
/** The assistant thread the end user was in, so the team sees what they asked first. */
|
|
695
|
+
threadId: string | null;
|
|
696
|
+
/** The page they wrote from. */
|
|
697
|
+
path: string;
|
|
698
|
+
text: string;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* A message the runtime did not take. `too-many` is the hourly cap and carries
|
|
702
|
+
* the time it lifts. `another-open` and `lost` both mean the conversation on
|
|
703
|
+
* screen is not the one the runtime has, or the request never landed.
|
|
704
|
+
*/
|
|
705
|
+
interface ConversationRefused {
|
|
706
|
+
refused: "too-many" | "another-open" | "lost";
|
|
707
|
+
retryAt?: string;
|
|
708
|
+
}
|
|
709
|
+
type ConversationWrite = ConversationSent | ConversationRefused | ConversationOff;
|
|
710
|
+
/**
|
|
711
|
+
* Talk to a person: the end user's side of a conversation with the project's
|
|
712
|
+
* team. Every call can answer `{ off: true }`, which turns the feature off for
|
|
713
|
+
* the visit.
|
|
714
|
+
*/
|
|
715
|
+
interface SupportTransport {
|
|
716
|
+
/** The open conversation, if any, and whether there are closed ones. */
|
|
717
|
+
conversationPoll(): Promise<ConversationPoll | ConversationOff>;
|
|
718
|
+
/** Null when the conversation is not this end user's. With `after`, only newer messages. */
|
|
719
|
+
conversation(id: string, after?: string): Promise<ConversationView | ConversationOff | null>;
|
|
720
|
+
/** Starts a conversation, or writes into the open one. */
|
|
721
|
+
startConversation(start: ConversationStart): Promise<ConversationWrite>;
|
|
722
|
+
/** Writes into a conversation, and reopens it when the team closed it. */
|
|
723
|
+
sendMessage(id: string, text: string): Promise<ConversationWrite>;
|
|
724
|
+
markRead(id: string): Promise<void>;
|
|
725
|
+
/** Closed conversations, newest first. */
|
|
726
|
+
pastConversations(): Promise<PastConversation[] | ConversationOff>;
|
|
727
|
+
}
|
|
544
728
|
interface GuidesTransport {
|
|
545
729
|
/**
|
|
546
730
|
* The identity a cached bootstrap belongs to. Undefined means this
|
|
547
731
|
* transport's data is never cached.
|
|
548
732
|
*/
|
|
549
733
|
readonly cacheKey?: string;
|
|
550
|
-
/** The visitor's
|
|
551
|
-
readonly user?:
|
|
734
|
+
/** The visitor's first name from the user token, for the greetings. */
|
|
735
|
+
readonly user?: {
|
|
736
|
+
readonly firstName?: string;
|
|
737
|
+
};
|
|
552
738
|
bootstrap(): Promise<BootstrapAnswer>;
|
|
553
739
|
/** Persist a user-state mutation; the provider applies it optimistically. */
|
|
554
740
|
mutate(action: StateAction): Promise<void>;
|
|
@@ -574,13 +760,18 @@ interface GuidesTransport {
|
|
|
574
760
|
* provider never looks at the page.
|
|
575
761
|
*/
|
|
576
762
|
reportPage?(report: PageReport): Promise<void>;
|
|
763
|
+
/**
|
|
764
|
+
* Talk to a person. Optional: without it, the assistant offers the
|
|
765
|
+
* project's support link as before.
|
|
766
|
+
*/
|
|
767
|
+
readonly support?: SupportTransport;
|
|
577
768
|
}
|
|
578
769
|
/**
|
|
579
770
|
* A scripted reply. The transport's own conversation id fills in when it names
|
|
580
771
|
* none, and `firstTurn` is true when the question named no conversation.
|
|
581
772
|
*/
|
|
582
|
-
type ScriptedReply = Omit<AskReply, "
|
|
583
|
-
|
|
773
|
+
type ScriptedReply = Omit<AskReply, "threadId" | "firstTurn"> & {
|
|
774
|
+
threadId?: string;
|
|
584
775
|
firstTurn?: boolean;
|
|
585
776
|
};
|
|
586
777
|
/**
|
|
@@ -597,7 +788,9 @@ type AskScript = (asked: AskQuestion) => {
|
|
|
597
788
|
};
|
|
598
789
|
interface MemoryTransportOptions {
|
|
599
790
|
cacheKey?: string;
|
|
600
|
-
user?:
|
|
791
|
+
user?: {
|
|
792
|
+
firstName?: string;
|
|
793
|
+
};
|
|
601
794
|
/** Opt in to the question box. Without a script there is no `ask` at all. */
|
|
602
795
|
ask?: AskScript;
|
|
603
796
|
/** Opt in to page reports. Without it there is no `reportPage`, and nothing looks at the page. */
|
|
@@ -607,6 +800,8 @@ interface MemoryTransportOptions {
|
|
|
607
800
|
* script names its own, so every stage of the waiting can be looked at.
|
|
608
801
|
*/
|
|
609
802
|
gapMs?: number;
|
|
803
|
+
/** Opt in to Talk to a person, with the team scripted. */
|
|
804
|
+
support?: SupportScript;
|
|
610
805
|
}
|
|
611
806
|
export declare class MemoryTransport implements GuidesTransport {
|
|
612
807
|
private readonly data;
|
|
@@ -618,9 +813,12 @@ export declare class MemoryTransport implements GuidesTransport {
|
|
|
618
813
|
readonly questions: AskQuestion[];
|
|
619
814
|
readonly pages: PageReport[];
|
|
620
815
|
readonly cacheKey: string | undefined;
|
|
621
|
-
readonly user:
|
|
816
|
+
readonly user: {
|
|
817
|
+
firstName?: string;
|
|
818
|
+
} | undefined;
|
|
622
819
|
readonly ask?: GuidesTransport["ask"];
|
|
623
820
|
readonly reportPage?: GuidesTransport["reportPage"];
|
|
821
|
+
readonly support?: MemorySupport;
|
|
624
822
|
constructor(data: BootstrapData, options?: MemoryTransportOptions);
|
|
625
823
|
bootstrap(): Promise<BootstrapData>;
|
|
626
824
|
mutate(action: StateAction): Promise<void>;
|
|
@@ -641,18 +839,14 @@ export declare const DEFAULT_SUNBOARD_API_URL = "https://api.sunboardhq.com";
|
|
|
641
839
|
export declare const PREVIEW_PARAM = "sunboardPreview";
|
|
642
840
|
export declare const PREVIEW_KEY_STORAGE = "sunboard:preview-key";
|
|
643
841
|
export declare const PREVIEW_EXPIRY_STORAGE = "sunboard:preview-until";
|
|
644
|
-
/** The visitor's details. They stay in the browser: none of them is sent to Sunboard. */
|
|
645
|
-
interface VisitorDetails {
|
|
646
|
-
/** The visitor's first name, as you want the assistant to greet them ("Hey Sam,"). */
|
|
647
|
-
firstName?: string;
|
|
648
|
-
}
|
|
649
842
|
interface HttpTransportConfig {
|
|
650
843
|
/** The workspace's publishable key. Safe in client bundles. */
|
|
651
844
|
publishableKey: string;
|
|
652
|
-
/**
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
845
|
+
/**
|
|
846
|
+
* A token your server signs with the project's identity secret. It names the visitor (`sub`),
|
|
847
|
+
* where your team can reach them (`email`) and their first name (`name`).
|
|
848
|
+
*/
|
|
849
|
+
userToken: string;
|
|
656
850
|
/** Origin of the runtime API. Defaults to the hosted API. */
|
|
657
851
|
baseUrl?: string;
|
|
658
852
|
/**
|
|
@@ -667,8 +861,17 @@ export declare class HttpTransport implements GuidesTransport {
|
|
|
667
861
|
private readonly baseUrl;
|
|
668
862
|
private readonly previewKey;
|
|
669
863
|
readonly cacheKey: string | undefined;
|
|
670
|
-
readonly user:
|
|
864
|
+
readonly user: {
|
|
865
|
+
readonly firstName?: string;
|
|
866
|
+
};
|
|
867
|
+
readonly support: SupportTransport;
|
|
671
868
|
constructor(config: HttpTransportConfig);
|
|
869
|
+
private identity;
|
|
870
|
+
private signed;
|
|
871
|
+
private send;
|
|
872
|
+
private getJson;
|
|
873
|
+
private post;
|
|
874
|
+
private write;
|
|
672
875
|
private headers;
|
|
673
876
|
private request;
|
|
674
877
|
bootstrap(): Promise<BootstrapAnswer>;
|
|
@@ -708,7 +911,7 @@ interface GuidesApi {
|
|
|
708
911
|
* `cacheKey`. Undefined for a preview session, which keeps nothing.
|
|
709
912
|
*/
|
|
710
913
|
cacheKey: string | undefined;
|
|
711
|
-
/** The visitor's first name from the
|
|
914
|
+
/** The visitor's first name from the user token, for the assistant's greeting. */
|
|
712
915
|
firstName: string | undefined;
|
|
713
916
|
reportAnchorMiss: GuidesTransport["reportAnchorMiss"];
|
|
714
917
|
/** Sent only for a step that missed before, once per step and revision per page load. */
|
|
@@ -719,6 +922,8 @@ interface GuidesApi {
|
|
|
719
922
|
* always has it; a `MemoryTransport` needs a script.
|
|
720
923
|
*/
|
|
721
924
|
ask: GuidesTransport["ask"];
|
|
925
|
+
/** Talk to a person's calls. Undefined when the transport has none or Sunboard is off. */
|
|
926
|
+
support: GuidesTransport["support"];
|
|
722
927
|
/** Record a usage event. Batched, fire-and-forget, never awaited. */
|
|
723
928
|
track: (event: GuideEventReport) => void;
|
|
724
929
|
/**
|
|
@@ -821,6 +1026,26 @@ type Message = {
|
|
|
821
1026
|
rating?: TourRating;
|
|
822
1027
|
};
|
|
823
1028
|
//#endregion
|
|
1029
|
+
//#region src/support.d.ts
|
|
1030
|
+
type SupportView = "assistant" | "conversation" | "past";
|
|
1031
|
+
interface Support {
|
|
1032
|
+
/** A poll or a write answered off: the feature is gone for the rest of the visit. */
|
|
1033
|
+
off: boolean;
|
|
1034
|
+
open: ConversationSummary | null;
|
|
1035
|
+
hasPast: boolean;
|
|
1036
|
+
view: SupportView;
|
|
1037
|
+
/**
|
|
1038
|
+
* The conversation on screen. Null on the conversation view before the
|
|
1039
|
+
* first message, while only the greeting shows.
|
|
1040
|
+
*/
|
|
1041
|
+
conversation: ConversationView | null;
|
|
1042
|
+
/** The conversation on screen was opened from Past conversations, so it has no composer. */
|
|
1043
|
+
readOnly: boolean;
|
|
1044
|
+
past: PastConversation[] | null;
|
|
1045
|
+
sending: boolean;
|
|
1046
|
+
failure: ConversationRefused | null;
|
|
1047
|
+
}
|
|
1048
|
+
//#endregion
|
|
824
1049
|
//#region src/hooks.d.ts
|
|
825
1050
|
export declare function useGuidesApi(): GuidesApi;
|
|
826
1051
|
interface LauncherOptions {
|
|
@@ -842,6 +1067,8 @@ interface LauncherList extends LauncherGroups {
|
|
|
842
1067
|
start: (guideKey: string, replyId?: string) => void;
|
|
843
1068
|
/** The thread, oldest first. Each hook instance holds its own. */
|
|
844
1069
|
messages: readonly Message[];
|
|
1070
|
+
/** The assistant thread the server named, which a conversation with the team starts from. */
|
|
1071
|
+
threadId: string | undefined;
|
|
845
1072
|
/** A question is out and its reply has not come back yet. */
|
|
846
1073
|
waiting: boolean;
|
|
847
1074
|
/** When the visitor may ask again, while the newest question was capped. */
|
|
@@ -868,6 +1095,31 @@ interface LauncherList extends LauncherGroups {
|
|
|
868
1095
|
rate: (lineId: string, rating: TourRating) => void;
|
|
869
1096
|
}
|
|
870
1097
|
export declare function useLauncher({ remember }?: LauncherOptions): LauncherList;
|
|
1098
|
+
interface SupportOptions {
|
|
1099
|
+
/** The panel is open, so the conversation view, when it is the view, is on screen. */
|
|
1100
|
+
onScreen: boolean;
|
|
1101
|
+
/** The assistant thread a new conversation starts from. */
|
|
1102
|
+
threadId?: string;
|
|
1103
|
+
}
|
|
1104
|
+
interface SupportPanel {
|
|
1105
|
+
/** Talk to a person is offered: the project's switch is on and the transport has the calls. */
|
|
1106
|
+
available: boolean;
|
|
1107
|
+
state: Support;
|
|
1108
|
+
/** Open the conversation view: the open conversation, or a new one. */
|
|
1109
|
+
talk: () => void;
|
|
1110
|
+
/** Write to the team. Resolves true when the message was taken. */
|
|
1111
|
+
send: (text: string) => Promise<boolean>;
|
|
1112
|
+
showPast: () => void;
|
|
1113
|
+
openPast: (id: string) => void;
|
|
1114
|
+
back: () => void;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Talk to a person. Polls the runtime only while it can matter: once on load,
|
|
1118
|
+
* then every 30 seconds while the end user has an open conversation, and the
|
|
1119
|
+
* conversation itself every 5 seconds while it is on screen. Nothing polls
|
|
1120
|
+
* while the tab is hidden.
|
|
1121
|
+
*/
|
|
1122
|
+
export declare function useSupport({ onScreen, threadId }: SupportOptions): SupportPanel;
|
|
871
1123
|
/** One guide's content plus launchTour; call with no key for launch-only. */
|
|
872
1124
|
export declare function useGuide(key?: string): {
|
|
873
1125
|
guide: GuideContent | undefined;
|
|
@@ -970,4 +1222,4 @@ interface LauncherProps {
|
|
|
970
1222
|
}
|
|
971
1223
|
export declare function Launcher({ inline }?: LauncherProps): import("react").JSX.Element | null;
|
|
972
1224
|
//#endregion
|
|
973
|
-
export type { ActiveTour, Anchor, AnchorElementState, AnchorMatch,
|
|
1225
|
+
export type { ActiveTour, Anchor, AnchorElementState, AnchorMatch, AskFailure, AskOutcome, AskQuestion, AskReply, AskResult, AskScript, AskStage, BootstrapAnswer, BootstrapData, BootstrapOff, ComponentSettings, ConversationMessageWire, ConversationOff, ConversationPoll, ConversationRefused, ConversationSent, ConversationStart, ConversationSummary, ConversationView, ConversationWrite, FoldedQuestionWire, GuideContent, GuideStep, GuideType, GuidesApi, GuidesTransport, HttpTransportConfig, LabelOverrides, Labels, LauncherList, LauncherOptions, LauncherProps, MemoryTransportOptions, Message, Palette, PastConversation, PinPosition, ScriptedReply, StepPlacement, StepPrimitive, SunboardOverlayProps, SunboardProviderProps, Support, SupportOptions, SupportPanel, SupportScript, SupportTransport, SupportView, Theme, ThemeMode, ThemeModeSetting, TourEnd, TourEnding, UserState };
|