@sunboard/react 1.1.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 +45 -25
- package/dist/index.d.mts +289 -22
- package/dist/index.mjs +1185 -133
- package/dist/styles.css +251 -0
- 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. |
|
|
@@ -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;
|
|
@@ -341,8 +343,8 @@ type AskOutcome = "GUIDE" | "ANSWERED" | "UNSUPPORTED" | "NONE";
|
|
|
341
343
|
interface AskReply {
|
|
342
344
|
id: string;
|
|
343
345
|
/** Sent back with the next question so it is read as a follow-up. */
|
|
344
|
-
|
|
345
|
-
/** This question opened its
|
|
346
|
+
threadId: string;
|
|
347
|
+
/** This question opened its thread, so the widget greets the visitor. */
|
|
346
348
|
firstTurn: boolean;
|
|
347
349
|
outcome: AskOutcome;
|
|
348
350
|
/**
|
|
@@ -356,6 +358,71 @@ interface AskReply {
|
|
|
356
358
|
}
|
|
357
359
|
/** What the server is doing while the visitor waits, in this order. */
|
|
358
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
|
+
}
|
|
359
426
|
//#endregion
|
|
360
427
|
//#region ../core/src/anchors.d.ts
|
|
361
428
|
type AnchorPart = {
|
|
@@ -406,7 +473,7 @@ interface Labels {
|
|
|
406
473
|
/** The line once the visitor picked a star. */
|
|
407
474
|
ratingThanks: string;
|
|
408
475
|
/** The thread's own name, for a screen reader. */
|
|
409
|
-
|
|
476
|
+
thread: string;
|
|
410
477
|
/** Said once to a screen reader while a question is out. */
|
|
411
478
|
writingStatus: string;
|
|
412
479
|
/** While the question is out, one for each stage the server reports. */
|
|
@@ -441,6 +508,43 @@ interface Labels {
|
|
|
441
508
|
closeAria: string;
|
|
442
509
|
poweredBy: string;
|
|
443
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
|
+
};
|
|
444
548
|
tour: {
|
|
445
549
|
back: string;
|
|
446
550
|
next: string;
|
|
@@ -505,12 +609,71 @@ export declare function detectThemeMode(): ThemeMode;
|
|
|
505
609
|
*/
|
|
506
610
|
export declare function useThemeMode(setting: ThemeModeSetting): ThemeMode;
|
|
507
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
|
|
508
671
|
//#region src/transport.d.ts
|
|
509
672
|
interface AskQuestion {
|
|
510
673
|
question: string;
|
|
511
674
|
pathname: string;
|
|
512
675
|
/** The conversation the last reply named; left out on the first question. */
|
|
513
|
-
|
|
676
|
+
threadId?: string;
|
|
514
677
|
}
|
|
515
678
|
/**
|
|
516
679
|
* What one question got. A refusal (the question was too long, or too many
|
|
@@ -526,14 +689,52 @@ type AskResult = {
|
|
|
526
689
|
message: string;
|
|
527
690
|
retryAt?: string;
|
|
528
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
|
+
}
|
|
529
728
|
interface GuidesTransport {
|
|
530
729
|
/**
|
|
531
730
|
* The identity a cached bootstrap belongs to. Undefined means this
|
|
532
731
|
* transport's data is never cached.
|
|
533
732
|
*/
|
|
534
733
|
readonly cacheKey?: string;
|
|
535
|
-
/** The visitor's
|
|
536
|
-
readonly user?:
|
|
734
|
+
/** The visitor's first name from the user token, for the greetings. */
|
|
735
|
+
readonly user?: {
|
|
736
|
+
readonly firstName?: string;
|
|
737
|
+
};
|
|
537
738
|
bootstrap(): Promise<BootstrapAnswer>;
|
|
538
739
|
/** Persist a user-state mutation; the provider applies it optimistically. */
|
|
539
740
|
mutate(action: StateAction): Promise<void>;
|
|
@@ -559,13 +760,18 @@ interface GuidesTransport {
|
|
|
559
760
|
* provider never looks at the page.
|
|
560
761
|
*/
|
|
561
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;
|
|
562
768
|
}
|
|
563
769
|
/**
|
|
564
770
|
* A scripted reply. The transport's own conversation id fills in when it names
|
|
565
771
|
* none, and `firstTurn` is true when the question named no conversation.
|
|
566
772
|
*/
|
|
567
|
-
type ScriptedReply = Omit<AskReply, "
|
|
568
|
-
|
|
773
|
+
type ScriptedReply = Omit<AskReply, "threadId" | "firstTurn"> & {
|
|
774
|
+
threadId?: string;
|
|
569
775
|
firstTurn?: boolean;
|
|
570
776
|
};
|
|
571
777
|
/**
|
|
@@ -582,7 +788,9 @@ type AskScript = (asked: AskQuestion) => {
|
|
|
582
788
|
};
|
|
583
789
|
interface MemoryTransportOptions {
|
|
584
790
|
cacheKey?: string;
|
|
585
|
-
user?:
|
|
791
|
+
user?: {
|
|
792
|
+
firstName?: string;
|
|
793
|
+
};
|
|
586
794
|
/** Opt in to the question box. Without a script there is no `ask` at all. */
|
|
587
795
|
ask?: AskScript;
|
|
588
796
|
/** Opt in to page reports. Without it there is no `reportPage`, and nothing looks at the page. */
|
|
@@ -592,6 +800,8 @@ interface MemoryTransportOptions {
|
|
|
592
800
|
* script names its own, so every stage of the waiting can be looked at.
|
|
593
801
|
*/
|
|
594
802
|
gapMs?: number;
|
|
803
|
+
/** Opt in to Talk to a person, with the team scripted. */
|
|
804
|
+
support?: SupportScript;
|
|
595
805
|
}
|
|
596
806
|
export declare class MemoryTransport implements GuidesTransport {
|
|
597
807
|
private readonly data;
|
|
@@ -603,9 +813,12 @@ export declare class MemoryTransport implements GuidesTransport {
|
|
|
603
813
|
readonly questions: AskQuestion[];
|
|
604
814
|
readonly pages: PageReport[];
|
|
605
815
|
readonly cacheKey: string | undefined;
|
|
606
|
-
readonly user:
|
|
816
|
+
readonly user: {
|
|
817
|
+
firstName?: string;
|
|
818
|
+
} | undefined;
|
|
607
819
|
readonly ask?: GuidesTransport["ask"];
|
|
608
820
|
readonly reportPage?: GuidesTransport["reportPage"];
|
|
821
|
+
readonly support?: MemorySupport;
|
|
609
822
|
constructor(data: BootstrapData, options?: MemoryTransportOptions);
|
|
610
823
|
bootstrap(): Promise<BootstrapData>;
|
|
611
824
|
mutate(action: StateAction): Promise<void>;
|
|
@@ -626,18 +839,14 @@ export declare const DEFAULT_SUNBOARD_API_URL = "https://api.sunboardhq.com";
|
|
|
626
839
|
export declare const PREVIEW_PARAM = "sunboardPreview";
|
|
627
840
|
export declare const PREVIEW_KEY_STORAGE = "sunboard:preview-key";
|
|
628
841
|
export declare const PREVIEW_EXPIRY_STORAGE = "sunboard:preview-until";
|
|
629
|
-
/** The visitor's details. They stay in the browser: none of them is sent to Sunboard. */
|
|
630
|
-
interface VisitorDetails {
|
|
631
|
-
/** The visitor's first name, as you want the assistant to greet them ("Hey Sam,"). */
|
|
632
|
-
firstName?: string;
|
|
633
|
-
}
|
|
634
842
|
interface HttpTransportConfig {
|
|
635
843
|
/** The workspace's publishable key. Safe in client bundles. */
|
|
636
844
|
publishableKey: string;
|
|
637
|
-
/**
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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;
|
|
641
850
|
/** Origin of the runtime API. Defaults to the hosted API. */
|
|
642
851
|
baseUrl?: string;
|
|
643
852
|
/**
|
|
@@ -652,8 +861,17 @@ export declare class HttpTransport implements GuidesTransport {
|
|
|
652
861
|
private readonly baseUrl;
|
|
653
862
|
private readonly previewKey;
|
|
654
863
|
readonly cacheKey: string | undefined;
|
|
655
|
-
readonly user:
|
|
864
|
+
readonly user: {
|
|
865
|
+
readonly firstName?: string;
|
|
866
|
+
};
|
|
867
|
+
readonly support: SupportTransport;
|
|
656
868
|
constructor(config: HttpTransportConfig);
|
|
869
|
+
private identity;
|
|
870
|
+
private signed;
|
|
871
|
+
private send;
|
|
872
|
+
private getJson;
|
|
873
|
+
private post;
|
|
874
|
+
private write;
|
|
657
875
|
private headers;
|
|
658
876
|
private request;
|
|
659
877
|
bootstrap(): Promise<BootstrapAnswer>;
|
|
@@ -693,7 +911,7 @@ interface GuidesApi {
|
|
|
693
911
|
* `cacheKey`. Undefined for a preview session, which keeps nothing.
|
|
694
912
|
*/
|
|
695
913
|
cacheKey: string | undefined;
|
|
696
|
-
/** The visitor's first name from the
|
|
914
|
+
/** The visitor's first name from the user token, for the assistant's greeting. */
|
|
697
915
|
firstName: string | undefined;
|
|
698
916
|
reportAnchorMiss: GuidesTransport["reportAnchorMiss"];
|
|
699
917
|
/** Sent only for a step that missed before, once per step and revision per page load. */
|
|
@@ -704,6 +922,8 @@ interface GuidesApi {
|
|
|
704
922
|
* always has it; a `MemoryTransport` needs a script.
|
|
705
923
|
*/
|
|
706
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"];
|
|
707
927
|
/** Record a usage event. Batched, fire-and-forget, never awaited. */
|
|
708
928
|
track: (event: GuideEventReport) => void;
|
|
709
929
|
/**
|
|
@@ -806,6 +1026,26 @@ type Message = {
|
|
|
806
1026
|
rating?: TourRating;
|
|
807
1027
|
};
|
|
808
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
|
|
809
1049
|
//#region src/hooks.d.ts
|
|
810
1050
|
export declare function useGuidesApi(): GuidesApi;
|
|
811
1051
|
interface LauncherOptions {
|
|
@@ -827,6 +1067,8 @@ interface LauncherList extends LauncherGroups {
|
|
|
827
1067
|
start: (guideKey: string, replyId?: string) => void;
|
|
828
1068
|
/** The thread, oldest first. Each hook instance holds its own. */
|
|
829
1069
|
messages: readonly Message[];
|
|
1070
|
+
/** The assistant thread the server named, which a conversation with the team starts from. */
|
|
1071
|
+
threadId: string | undefined;
|
|
830
1072
|
/** A question is out and its reply has not come back yet. */
|
|
831
1073
|
waiting: boolean;
|
|
832
1074
|
/** When the visitor may ask again, while the newest question was capped. */
|
|
@@ -853,6 +1095,31 @@ interface LauncherList extends LauncherGroups {
|
|
|
853
1095
|
rate: (lineId: string, rating: TourRating) => void;
|
|
854
1096
|
}
|
|
855
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;
|
|
856
1123
|
/** One guide's content plus launchTour; call with no key for launch-only. */
|
|
857
1124
|
export declare function useGuide(key?: string): {
|
|
858
1125
|
guide: GuideContent | undefined;
|
|
@@ -955,4 +1222,4 @@ interface LauncherProps {
|
|
|
955
1222
|
}
|
|
956
1223
|
export declare function Launcher({ inline }?: LauncherProps): import("react").JSX.Element | null;
|
|
957
1224
|
//#endregion
|
|
958
|
-
export type { ActiveTour, Anchor, AnchorElementState, AnchorMatch, AskFailure, AskOutcome, AskQuestion, AskReply, AskResult, AskScript, AskStage, BootstrapAnswer, BootstrapData, BootstrapOff, ComponentSettings, GuideContent, GuideStep, GuideType, GuidesApi, GuidesTransport, HttpTransportConfig, LabelOverrides, Labels, LauncherList, LauncherOptions, LauncherProps, MemoryTransportOptions, Message, Palette, PinPosition, ScriptedReply, StepPlacement, StepPrimitive, SunboardOverlayProps, SunboardProviderProps, Theme, ThemeMode, ThemeModeSetting, TourEnd, TourEnding, UserState
|
|
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 };
|