@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 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 connection={{ publishableKey: SUNBOARD_KEY, userId: currentUser.id }}>
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. Create one Sunboard component
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
- userId,
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
- ### 4. Wrap your signed-in pages
131
+ ### 5. Wrap your signed-in pages
113
132
 
114
- Put the component around the pages a signed-in user sees, and pass that user's id. Use an id that
115
- stays the same for each person. The first name is optional. With it the assistant greets the
116
- user ("Hey Sam,"), without it it says "Hey there,". The name stays in the browser.
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 userId={currentUser.id} firstName={currentUser.firstName}>
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
- ### 5. Check it works
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 you pass as `userId`, so give each
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; userId: string; user?: { firstName?: string } }` | required | Your publishable key and the signed-in user's id. `user.firstName` is for the assistant's greeting and stays in the browser. |
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`, the articles it came from in `sources`, and `guideKey` when it offers a guide to start. |
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 3. The package itself is already client code.
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 4 says.
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 is pointed at the
336
- * section instead.
337
- * GUIDE offers a tour, with an answer when sections were found too.
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
- conversationId: string;
356
- /** This question opened its conversation, so the widget greets the visitor. */
346
+ threadId: string;
347
+ /** This question opened its thread, so the widget greets the visitor. */
357
348
  firstTurn: boolean;
358
349
  outcome: AskOutcome;
359
- /** Plain text with blank lines between paragraphs, `1.` and `-` lists, and `**bold**`. */
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
- conversation: string;
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 was written: no guide and no
451
- * section, a section with no backed answer, a guide with no 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
- conversationId?: string;
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 details from the connection. Never sent anywhere. */
551
- readonly user?: VisitorDetails;
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, "conversationId" | "firstTurn"> & {
583
- conversationId?: string;
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?: VisitorDetails;
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: VisitorDetails | undefined;
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
- /** Customer-supplied end-user id. */
653
- userId: string;
654
- /** The visitor's details, kept in the browser. */
655
- user?: VisitorDetails;
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: VisitorDetails | undefined;
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 connection, for the assistant's greeting. */
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, AskArticle, 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, VisitorDetails };
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 };