@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 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. |
@@ -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;
@@ -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
- conversationId: string;
345
- /** 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. */
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
- conversation: string;
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
- conversationId?: string;
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 details from the connection. Never sent anywhere. */
536
- readonly user?: VisitorDetails;
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, "conversationId" | "firstTurn"> & {
568
- conversationId?: string;
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?: VisitorDetails;
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: VisitorDetails | undefined;
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
- /** Customer-supplied end-user id. */
638
- userId: string;
639
- /** The visitor's details, kept in the browser. */
640
- 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;
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: VisitorDetails | undefined;
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 connection, for the assistant's greeting. */
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, 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 };