@rehers/rehers-roleplay-sdk 2.4.2 → 2.5.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 +274 -33
- package/package.json +23 -3
- package/react.d.ts +78 -0
- package/react.js +224 -0
package/README.md
CHANGED
|
@@ -9,39 +9,274 @@ Use it in these two places:
|
|
|
9
9
|
|
|
10
10
|
The SDK must be initialized with the currently logged-in Seamless user before either embed flow is used.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Install
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
```bash
|
|
15
|
+
npm install @rehers/rehers-roleplay-sdk
|
|
16
|
+
```
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
---
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
## React Integration (Recommended)
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
If the Seamless dashboard is built with React, use the React bindings. They handle SDK lifecycle, cleanup, and stale closures automatically.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import {
|
|
26
|
+
SeamlessRoleplayProvider,
|
|
27
|
+
RoleplayDialog,
|
|
28
|
+
RoleplayEmbed,
|
|
29
|
+
AddToScenarioDialog,
|
|
30
|
+
useSeamlessRoleplay,
|
|
31
|
+
} from "@rehers/rehers-roleplay-sdk/react";
|
|
22
32
|
```
|
|
23
33
|
|
|
24
|
-
###
|
|
34
|
+
### Get the logged-in Seamless user
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
Use the existing Seamless dashboard session and call:
|
|
27
37
|
|
|
28
|
-
```
|
|
29
|
-
|
|
38
|
+
```ts
|
|
39
|
+
const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
|
|
40
|
+
method: "GET",
|
|
41
|
+
credentials: "include",
|
|
42
|
+
headers: {
|
|
43
|
+
accept: "application/json, text/plain, */*",
|
|
44
|
+
},
|
|
45
|
+
}).then((res) => res.json());
|
|
46
|
+
|
|
47
|
+
const me = meResponse.data ?? meResponse;
|
|
48
|
+
|
|
49
|
+
const seamlessUserId = String(me.id);
|
|
50
|
+
const seamlessUserEmail = me.username;
|
|
51
|
+
const seamlessUserRole =
|
|
52
|
+
me.orgRole === "owner" ? "owner" :
|
|
53
|
+
me.isOrgAdmin ? "admin" :
|
|
54
|
+
"member";
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Wrap your app with the Provider
|
|
58
|
+
|
|
59
|
+
Add `SeamlessRoleplayProvider` once, near the root of the Seamless dashboard. It initializes the SDK and makes it available to all child components.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
function App() {
|
|
63
|
+
return (
|
|
64
|
+
<SeamlessRoleplayProvider
|
|
65
|
+
publishableKey={ROLEPLAY_PUBLISHABLE_KEY}
|
|
66
|
+
userId={seamlessUserId}
|
|
67
|
+
userEmail={seamlessUserEmail}
|
|
68
|
+
userRole={seamlessUserRole}
|
|
69
|
+
onReady={() => console.log("Roleplay SDK ready")}
|
|
70
|
+
onError={(err) => console.error("Roleplay SDK error", err)}
|
|
71
|
+
>
|
|
72
|
+
<Dashboard />
|
|
73
|
+
</SeamlessRoleplayProvider>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Embed the full Roleplay page
|
|
79
|
+
|
|
80
|
+
Use `RoleplayEmbed` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
function RoleplayPage() {
|
|
84
|
+
return (
|
|
85
|
+
<RoleplayEmbed
|
|
86
|
+
style={{ width: "100%", height: "100vh" }}
|
|
87
|
+
onCallStarted={(data) => console.log("Call started", data.callId)}
|
|
88
|
+
onCallEnded={(data) => console.log("Call ended", data.callId, data.duration)}
|
|
89
|
+
onError={(err) => console.error("Roleplay error", err)}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The embed mounts when the component mounts and cleans up automatically when it unmounts. No manual teardown needed.
|
|
96
|
+
|
|
97
|
+
### Open a dialog from Contact Search
|
|
98
|
+
|
|
99
|
+
Use `RoleplayDialog` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
function ContactRow({ contact }) {
|
|
103
|
+
const [showRoleplay, setShowRoleplay] = useState(false);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
<button onClick={() => setShowRoleplay(true)}>Roleplay</button>
|
|
108
|
+
|
|
109
|
+
<RoleplayDialog
|
|
110
|
+
open={showRoleplay}
|
|
111
|
+
name={contact.name}
|
|
112
|
+
domain={contact.domain}
|
|
113
|
+
company={contact.company}
|
|
114
|
+
title={contact.title}
|
|
115
|
+
liUrl={contact.linkedinUrl}
|
|
116
|
+
companyDescription={contact.companyDescription}
|
|
117
|
+
onCallStarted={(data) => {
|
|
118
|
+
console.log("Roleplay call started", data.callId);
|
|
119
|
+
}}
|
|
120
|
+
onCallEnded={(data) => {
|
|
121
|
+
console.log("Roleplay call ended", data.callId, data.duration);
|
|
122
|
+
}}
|
|
123
|
+
onClose={() => setShowRoleplay(false)}
|
|
124
|
+
onError={(err) => {
|
|
125
|
+
console.error("Roleplay dialog error", err);
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
30
131
|
```
|
|
31
132
|
|
|
133
|
+
The dialog opens when `open` becomes `true` and closes when it becomes `false`. All callbacks always see the latest props and state — no stale closures.
|
|
134
|
+
|
|
135
|
+
### Add contacts to a scenario
|
|
136
|
+
|
|
137
|
+
Use `AddToScenarioDialog` to send multiple contacts into a scenario picker dialog.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
function BulkActions({ selectedContacts }) {
|
|
141
|
+
const [showATS, setShowATS] = useState(false);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<button onClick={() => setShowATS(true)}>Add to Scenario</button>
|
|
146
|
+
|
|
147
|
+
<AddToScenarioDialog
|
|
148
|
+
open={showATS}
|
|
149
|
+
contacts={selectedContacts.map((contact) => ({
|
|
150
|
+
name: contact.name,
|
|
151
|
+
company: contact.company,
|
|
152
|
+
title: contact.title,
|
|
153
|
+
domain: contact.domain,
|
|
154
|
+
liUrl: contact.linkedinUrl,
|
|
155
|
+
companyDescription: contact.companyDescription,
|
|
156
|
+
}))}
|
|
157
|
+
onComplete={(data) => {
|
|
158
|
+
console.log("Scenario import complete", data);
|
|
159
|
+
setShowATS(false);
|
|
160
|
+
}}
|
|
161
|
+
onClose={() => setShowATS(false)}
|
|
162
|
+
onError={(err) => {
|
|
163
|
+
console.error("Add to scenario error", err);
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Supports 1 to 25 contacts per call. Each contact requires `name`, `company`, `title`, and `domain`.
|
|
172
|
+
|
|
173
|
+
### Use the hook for imperative access
|
|
174
|
+
|
|
175
|
+
If you need direct access to SDK methods instead of declarative components:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
function CustomButton({ contact }) {
|
|
179
|
+
const { isReady, open, close } = useSeamlessRoleplay();
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<button
|
|
183
|
+
disabled={!isReady}
|
|
184
|
+
onClick={() =>
|
|
185
|
+
open({
|
|
186
|
+
...contact,
|
|
187
|
+
onClose: () => console.log("closed"),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
Start Roleplay
|
|
192
|
+
</button>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### React props reference
|
|
198
|
+
|
|
199
|
+
#### `SeamlessRoleplayProvider`
|
|
200
|
+
|
|
201
|
+
| Prop | Type | Required | Description |
|
|
202
|
+
|---|---|---|---|
|
|
203
|
+
| `publishableKey` | `string` | Yes | Publishable API key (`pk_live_...` or `pk_test_...`) |
|
|
204
|
+
| `userId` | `string` | Yes | `String(me.id)` from Seamless `/api/users/me` |
|
|
205
|
+
| `userEmail` | `string` | Yes | `me.username` from Seamless `/api/users/me` |
|
|
206
|
+
| `userRole` | `"owner" \| "admin" \| "member"` | No | User role for syncing permissions |
|
|
207
|
+
| `userToken` | `string` | No | Signed JWT for identity verification |
|
|
208
|
+
| `origin` | `string` | No | Override iframe origin (dev/testing only) |
|
|
209
|
+
| `onReady` | `() => void` | No | Called when SDK session is ready |
|
|
210
|
+
| `onError` | `(error) => void` | No | Called on initialization error |
|
|
211
|
+
|
|
212
|
+
#### `RoleplayEmbed`
|
|
213
|
+
|
|
214
|
+
| Prop | Type | Required | Description |
|
|
215
|
+
|---|---|---|---|
|
|
216
|
+
| `className` | `string` | No | CSS class for the container div |
|
|
217
|
+
| `style` | `CSSProperties` | No | Inline styles for the container div |
|
|
218
|
+
| `onCallStarted` | `(data) => void` | No | Called when a roleplay call starts |
|
|
219
|
+
| `onCallEnded` | `(data) => void` | No | Called when a roleplay call ends |
|
|
220
|
+
| `onClose` | `() => void` | No | Called when the embed is closed |
|
|
221
|
+
| `onError` | `(data) => void` | No | Called on error |
|
|
222
|
+
|
|
223
|
+
#### `RoleplayDialog`
|
|
224
|
+
|
|
225
|
+
| Prop | Type | Required | Description |
|
|
226
|
+
|---|---|---|---|
|
|
227
|
+
| `open` | `boolean` | Yes | Whether the dialog is open |
|
|
228
|
+
| `name` | `string` | Yes | Contact full name |
|
|
229
|
+
| `domain` | `string` | Yes | Company domain (e.g. `"stripe.com"`) |
|
|
230
|
+
| `company` | `string` | Yes | Company name |
|
|
231
|
+
| `title` | `string` | Yes | Contact job title |
|
|
232
|
+
| `companyDescription` | `string` | No | Company description |
|
|
233
|
+
| `liUrl` | `string` | No | LinkedIn profile URL |
|
|
234
|
+
| `onCallStarted` | `(data) => void` | No | Called when the roleplay call starts |
|
|
235
|
+
| `onCallEnded` | `(data) => void` | No | Called when the roleplay call ends |
|
|
236
|
+
| `onClose` | `() => void` | No | Called when the dialog is closed |
|
|
237
|
+
| `onError` | `(data) => void` | No | Called on error |
|
|
238
|
+
|
|
239
|
+
#### `AddToScenarioDialog`
|
|
240
|
+
|
|
241
|
+
| Prop | Type | Required | Description |
|
|
242
|
+
|---|---|---|---|
|
|
243
|
+
| `open` | `boolean` | Yes | Whether the dialog is open |
|
|
244
|
+
| `contacts` | `AddToScenarioContact[]` | Yes | Array of contacts (1–25) |
|
|
245
|
+
| `onComplete` | `(data) => void` | No | Called on successful import |
|
|
246
|
+
| `onClose` | `() => void` | No | Called when dialog is closed |
|
|
247
|
+
| `onError` | `(error) => void` | No | Called on error |
|
|
248
|
+
|
|
249
|
+
#### `useSeamlessRoleplay()`
|
|
250
|
+
|
|
251
|
+
Returns `{ isReady, error, sdk }`. Must be used inside a `SeamlessRoleplayProvider`.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Raw JavaScript Integration
|
|
256
|
+
|
|
257
|
+
If the Seamless dashboard does not use React, or if you need direct imperative control, use the vanilla SDK directly.
|
|
258
|
+
|
|
259
|
+
### Load the SDK
|
|
260
|
+
|
|
261
|
+
#### Option 1: npm import
|
|
262
|
+
|
|
32
263
|
```js
|
|
33
264
|
import SeamlessRoleplay from "@rehers/rehers-roleplay-sdk";
|
|
34
265
|
```
|
|
35
266
|
|
|
36
|
-
|
|
267
|
+
#### Option 2: Script tag (hosted file)
|
|
268
|
+
|
|
269
|
+
```html
|
|
270
|
+
<script src="/path/to/roleplay-sdk.js"></script>
|
|
271
|
+
```
|
|
37
272
|
|
|
38
|
-
|
|
273
|
+
#### Option 3: CDN script
|
|
39
274
|
|
|
40
275
|
```html
|
|
41
276
|
<script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
|
|
42
277
|
```
|
|
43
278
|
|
|
44
|
-
|
|
279
|
+
### Get the logged-in Seamless user
|
|
45
280
|
|
|
46
281
|
Use the existing Seamless dashboard session and call:
|
|
47
282
|
|
|
@@ -61,7 +296,7 @@ Normalize the response before reading fields:
|
|
|
61
296
|
const me = meResponse.data ?? meResponse;
|
|
62
297
|
```
|
|
63
298
|
|
|
64
|
-
|
|
299
|
+
### Required mapping for `init()`
|
|
65
300
|
|
|
66
301
|
These are the values Seamless must pass into `SeamlessRoleplay.init(...)`:
|
|
67
302
|
|
|
@@ -82,14 +317,7 @@ const seamlessUserRole =
|
|
|
82
317
|
"member";
|
|
83
318
|
```
|
|
84
319
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
```js
|
|
88
|
-
const seamlessUserId = String(me.id);
|
|
89
|
-
const seamlessUserEmail = me.username;
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Initialize the SDK once
|
|
320
|
+
### Initialize the SDK once
|
|
93
321
|
|
|
94
322
|
Initialize the SDK once per page load using the logged-in Seamless user. Reuse that same initialized SDK for both the full-page embed and the Contact Search dialog.
|
|
95
323
|
|
|
@@ -133,7 +361,7 @@ function ensureRoleplaySdkReady() {
|
|
|
133
361
|
}
|
|
134
362
|
```
|
|
135
363
|
|
|
136
|
-
|
|
364
|
+
### Embed in the full Roleplay page
|
|
137
365
|
|
|
138
366
|
Use `mount(container)` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
|
|
139
367
|
|
|
@@ -156,7 +384,7 @@ If the Seamless page or tab is torn down, unmount the SDK:
|
|
|
156
384
|
SeamlessRoleplay.unmount();
|
|
157
385
|
```
|
|
158
386
|
|
|
159
|
-
|
|
387
|
+
### Embed in Contact Search
|
|
160
388
|
|
|
161
389
|
Use `open(contactData)` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
|
|
162
390
|
|
|
@@ -198,7 +426,7 @@ The contact object passed to `open(...)` should map to:
|
|
|
198
426
|
| `liUrl` | LinkedIn profile URL, if available |
|
|
199
427
|
| `companyDescription` | Company description, if available |
|
|
200
428
|
|
|
201
|
-
|
|
429
|
+
### Add contacts to a scenario
|
|
202
430
|
|
|
203
431
|
If Seamless wants to send multiple contacts into a scenario picker dialog, use `addToScenario(...)`.
|
|
204
432
|
|
|
@@ -228,9 +456,9 @@ async function addContactsToScenario(contacts) {
|
|
|
228
456
|
}
|
|
229
457
|
```
|
|
230
458
|
|
|
231
|
-
|
|
459
|
+
### API reference
|
|
232
460
|
|
|
233
|
-
|
|
461
|
+
#### `SeamlessRoleplay.init(options)`
|
|
234
462
|
|
|
235
463
|
Initializes the SDK with the logged-in Seamless user.
|
|
236
464
|
|
|
@@ -245,30 +473,32 @@ SeamlessRoleplay.init({
|
|
|
245
473
|
});
|
|
246
474
|
```
|
|
247
475
|
|
|
248
|
-
|
|
476
|
+
#### `SeamlessRoleplay.mount(container)`
|
|
249
477
|
|
|
250
478
|
Mounts the full Roleplay app into a dashboard container.
|
|
251
479
|
|
|
252
|
-
|
|
480
|
+
#### `SeamlessRoleplay.open(contactData)`
|
|
253
481
|
|
|
254
482
|
Opens the Roleplay dialog for a single contact.
|
|
255
483
|
|
|
256
|
-
|
|
484
|
+
#### `SeamlessRoleplay.addToScenario(options)`
|
|
257
485
|
|
|
258
486
|
Opens the bulk add-to-scenario dialog for 1 to 25 contacts.
|
|
259
487
|
|
|
260
|
-
|
|
488
|
+
#### `SeamlessRoleplay.close()`
|
|
261
489
|
|
|
262
490
|
Closes the active dialog.
|
|
263
491
|
|
|
264
|
-
|
|
492
|
+
#### `SeamlessRoleplay.unmount()`
|
|
265
493
|
|
|
266
494
|
Unmounts the full-page Roleplay embed.
|
|
267
495
|
|
|
268
|
-
|
|
496
|
+
#### `SeamlessRoleplay.destroy()`
|
|
269
497
|
|
|
270
498
|
Destroys the SDK state, timers, mount, and dialogs.
|
|
271
499
|
|
|
500
|
+
---
|
|
501
|
+
|
|
272
502
|
## Trial and upgrade handling
|
|
273
503
|
|
|
274
504
|
If the backend responds with `USER_NOT_FOUND` during SDK session creation, the SDK handles the upgrade or trial UI automatically. Seamless does not need separate client-side logic for that case.
|
|
@@ -278,10 +508,21 @@ If the backend responds with `USER_NOT_FOUND` during SDK session creation, the S
|
|
|
278
508
|
Type declarations are included with the SDK package.
|
|
279
509
|
|
|
280
510
|
```ts
|
|
511
|
+
// Vanilla SDK types
|
|
281
512
|
import type {
|
|
282
513
|
SeamlessRoleplaySDK,
|
|
283
514
|
SeamlessRoleplayInitOptions,
|
|
284
515
|
SeamlessRoleplayOpenData,
|
|
285
516
|
AddToScenarioOptions,
|
|
286
517
|
} from "@rehers/rehers-roleplay-sdk";
|
|
518
|
+
|
|
519
|
+
// React bindings types
|
|
520
|
+
import type {
|
|
521
|
+
SeamlessRoleplayProviderProps,
|
|
522
|
+
RoleplayDialogProps,
|
|
523
|
+
RoleplayEmbedProps,
|
|
524
|
+
AddToScenarioDialogProps,
|
|
525
|
+
AddToScenarioContact,
|
|
526
|
+
AddToScenarioCompleteData,
|
|
527
|
+
} from "@rehers/rehers-roleplay-sdk/react";
|
|
287
528
|
```
|
package/package.json
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehers/rehers-roleplay-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Seamless Roleplay SDK — embed roleplay call sessions via a modal + iframe",
|
|
5
5
|
"main": "roleplay-sdk.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"default": "./roleplay-sdk.js"
|
|
11
|
+
},
|
|
12
|
+
"./react": {
|
|
13
|
+
"types": "./react.d.ts",
|
|
14
|
+
"default": "./react.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"files": [
|
|
8
18
|
"roleplay-sdk.js",
|
|
9
|
-
"index.d.ts"
|
|
19
|
+
"index.d.ts",
|
|
20
|
+
"react.js",
|
|
21
|
+
"react.d.ts"
|
|
10
22
|
],
|
|
11
|
-
"
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": ">=18.0.0",
|
|
25
|
+
"react-dom": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"react": { "optional": true },
|
|
29
|
+
"react-dom": { "optional": true }
|
|
30
|
+
},
|
|
31
|
+
"keywords": ["seamless", "roleplay", "sdk", "sales", "training", "react"],
|
|
12
32
|
"license": "UNLICENSED",
|
|
13
33
|
"repository": {
|
|
14
34
|
"type": "git",
|
package/react.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { SeamlessRoleplaySDK, AddToScenarioContact, AddToScenarioCompleteData } from "@rehers/rehers-roleplay-sdk";
|
|
3
|
+
import "@rehers/rehers-roleplay-sdk";
|
|
4
|
+
export type { AddToScenarioContact, AddToScenarioCompleteData };
|
|
5
|
+
interface SeamlessRoleplayContextValue {
|
|
6
|
+
isReady: boolean;
|
|
7
|
+
error: {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
} | null;
|
|
11
|
+
sdk: SeamlessRoleplaySDK;
|
|
12
|
+
}
|
|
13
|
+
export interface SeamlessRoleplayProviderProps {
|
|
14
|
+
publishableKey: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
userEmail: string;
|
|
17
|
+
userRole?: "owner" | "admin" | "member";
|
|
18
|
+
userToken?: string;
|
|
19
|
+
origin?: string;
|
|
20
|
+
onReady?: () => void;
|
|
21
|
+
onError?: (error: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}) => void;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export declare function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, userRole, userToken, origin, onReady, onError, children, }: SeamlessRoleplayProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export declare function useSeamlessRoleplay(): SeamlessRoleplayContextValue;
|
|
29
|
+
export interface RoleplayDialogProps {
|
|
30
|
+
open: boolean;
|
|
31
|
+
name: string;
|
|
32
|
+
domain: string;
|
|
33
|
+
company: string;
|
|
34
|
+
title: string;
|
|
35
|
+
companyDescription?: string;
|
|
36
|
+
liUrl?: string;
|
|
37
|
+
onCallStarted?: (data: {
|
|
38
|
+
callId: string;
|
|
39
|
+
}) => void;
|
|
40
|
+
onCallEnded?: (data: {
|
|
41
|
+
callId: string;
|
|
42
|
+
duration?: number;
|
|
43
|
+
}) => void;
|
|
44
|
+
onClose?: () => void;
|
|
45
|
+
onError?: (data: {
|
|
46
|
+
code: string;
|
|
47
|
+
message: string;
|
|
48
|
+
}) => void;
|
|
49
|
+
}
|
|
50
|
+
export declare function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onCallStarted, onCallEnded, onClose, onError, }: RoleplayDialogProps): null;
|
|
51
|
+
export interface RoleplayEmbedProps {
|
|
52
|
+
className?: string;
|
|
53
|
+
style?: React.CSSProperties;
|
|
54
|
+
onCallStarted?: (data: {
|
|
55
|
+
callId: string;
|
|
56
|
+
}) => void;
|
|
57
|
+
onCallEnded?: (data: {
|
|
58
|
+
callId: string;
|
|
59
|
+
duration?: number;
|
|
60
|
+
}) => void;
|
|
61
|
+
onClose?: () => void;
|
|
62
|
+
onError?: (data: {
|
|
63
|
+
code: string;
|
|
64
|
+
message: string;
|
|
65
|
+
}) => void;
|
|
66
|
+
}
|
|
67
|
+
export declare function RoleplayEmbed({ className, style, onCallStarted, onCallEnded, onClose, onError, }: RoleplayEmbedProps): import("react/jsx-runtime").JSX.Element;
|
|
68
|
+
export interface AddToScenarioDialogProps {
|
|
69
|
+
open: boolean;
|
|
70
|
+
contacts: AddToScenarioContact[];
|
|
71
|
+
onComplete?: (data: AddToScenarioCompleteData) => void;
|
|
72
|
+
onClose?: () => void;
|
|
73
|
+
onError?: (error: {
|
|
74
|
+
code: string;
|
|
75
|
+
message: string;
|
|
76
|
+
}) => void;
|
|
77
|
+
}
|
|
78
|
+
export declare function AddToScenarioDialog({ open: isOpen, contacts, onComplete, onClose, onError, }: AddToScenarioDialogProps): null;
|
package/react.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
// Side-effect import: the SDK IIFE runs and sets window.SeamlessRoleplay.
|
|
4
|
+
// Works with bundlers (Vite, webpack) and script tags alike.
|
|
5
|
+
import "@rehers/rehers-roleplay-sdk";
|
|
6
|
+
// ── Callback ref helper ─────────────────────────────────────────────
|
|
7
|
+
// Keeps a ref in sync with the latest callback to avoid stale closures.
|
|
8
|
+
function useCallbackRef(cb) {
|
|
9
|
+
const ref = useRef(cb);
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
ref.current = cb;
|
|
12
|
+
});
|
|
13
|
+
return ref;
|
|
14
|
+
}
|
|
15
|
+
// ── SDK singleton access ────────────────────────────────────────────
|
|
16
|
+
function getSDK() {
|
|
17
|
+
if (typeof window !== "undefined" && window.SeamlessRoleplay) {
|
|
18
|
+
return window.SeamlessRoleplay;
|
|
19
|
+
}
|
|
20
|
+
throw new Error("[SeamlessRoleplay/React] Could not find SeamlessRoleplay SDK. " +
|
|
21
|
+
'Make sure "@rehers/rehers-roleplay-sdk" is installed and loaded before the React wrapper.');
|
|
22
|
+
}
|
|
23
|
+
const SeamlessRoleplayContext = createContext(null);
|
|
24
|
+
let providerMountCount = 0;
|
|
25
|
+
export function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, userRole, userToken, origin, onReady, onError, children, }) {
|
|
26
|
+
const [state, setState] = useState({ isReady: false, error: null });
|
|
27
|
+
const mountedRef = useRef(false);
|
|
28
|
+
const onReadyRef = useCallbackRef(onReady);
|
|
29
|
+
const onErrorRef = useCallbackRef(onError);
|
|
30
|
+
const sdk = useMemo(() => getSDK(), []);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
providerMountCount++;
|
|
33
|
+
if (providerMountCount > 1 && process.env.NODE_ENV !== "production") {
|
|
34
|
+
console.warn("[SeamlessRoleplay/React] Multiple SeamlessRoleplayProvider instances detected. " +
|
|
35
|
+
"The SDK is a singleton — only one Provider should be mounted at a time.");
|
|
36
|
+
}
|
|
37
|
+
mountedRef.current = true;
|
|
38
|
+
setState({ isReady: false, error: null });
|
|
39
|
+
sdk.init({
|
|
40
|
+
publishableKey,
|
|
41
|
+
userId,
|
|
42
|
+
userEmail,
|
|
43
|
+
userRole,
|
|
44
|
+
userToken,
|
|
45
|
+
origin,
|
|
46
|
+
onReady: () => {
|
|
47
|
+
var _a;
|
|
48
|
+
if (!mountedRef.current)
|
|
49
|
+
return;
|
|
50
|
+
setState({ isReady: true, error: null });
|
|
51
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
52
|
+
},
|
|
53
|
+
onError: (err) => {
|
|
54
|
+
var _a;
|
|
55
|
+
if (!mountedRef.current)
|
|
56
|
+
return;
|
|
57
|
+
setState({ isReady: false, error: err });
|
|
58
|
+
(_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return () => {
|
|
62
|
+
mountedRef.current = false;
|
|
63
|
+
providerMountCount--;
|
|
64
|
+
sdk.destroy();
|
|
65
|
+
};
|
|
66
|
+
}, [sdk, publishableKey, userId, userEmail, userRole, userToken, origin]);
|
|
67
|
+
const contextValue = useMemo(() => ({ ...state, sdk }), [state, sdk]);
|
|
68
|
+
return (_jsx(SeamlessRoleplayContext.Provider, { value: contextValue, children: children }));
|
|
69
|
+
}
|
|
70
|
+
// ── Hook ────────────────────────────────────────────────────────────
|
|
71
|
+
export function useSeamlessRoleplay() {
|
|
72
|
+
const ctx = useContext(SeamlessRoleplayContext);
|
|
73
|
+
if (!ctx) {
|
|
74
|
+
throw new Error("[SeamlessRoleplay/React] useSeamlessRoleplay() must be used inside a <SeamlessRoleplayProvider>.");
|
|
75
|
+
}
|
|
76
|
+
return ctx;
|
|
77
|
+
}
|
|
78
|
+
export function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onCallStarted, onCallEnded, onClose, onError, }) {
|
|
79
|
+
const { isReady, sdk } = useSeamlessRoleplay();
|
|
80
|
+
const onCallStartedRef = useCallbackRef(onCallStarted);
|
|
81
|
+
const onCallEndedRef = useCallbackRef(onCallEnded);
|
|
82
|
+
const onCloseRef = useCallbackRef(onClose);
|
|
83
|
+
const onErrorRef = useCallbackRef(onError);
|
|
84
|
+
const isOpenRef = useRef(false);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!isReady)
|
|
87
|
+
return;
|
|
88
|
+
if (isOpen) {
|
|
89
|
+
isOpenRef.current = true;
|
|
90
|
+
sdk.open({
|
|
91
|
+
name,
|
|
92
|
+
domain,
|
|
93
|
+
company,
|
|
94
|
+
title,
|
|
95
|
+
companyDescription,
|
|
96
|
+
liUrl,
|
|
97
|
+
onCallStarted: (data) => { var _a; return (_a = onCallStartedRef.current) === null || _a === void 0 ? void 0 : _a.call(onCallStartedRef, data); },
|
|
98
|
+
onCallEnded: (data) => { var _a; return (_a = onCallEndedRef.current) === null || _a === void 0 ? void 0 : _a.call(onCallEndedRef, data); },
|
|
99
|
+
onClose: () => {
|
|
100
|
+
var _a;
|
|
101
|
+
isOpenRef.current = false;
|
|
102
|
+
(_a = onCloseRef.current) === null || _a === void 0 ? void 0 : _a.call(onCloseRef);
|
|
103
|
+
},
|
|
104
|
+
onError: (data) => { var _a; return (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, data); },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else if (isOpenRef.current) {
|
|
108
|
+
isOpenRef.current = false;
|
|
109
|
+
sdk.close();
|
|
110
|
+
}
|
|
111
|
+
return () => {
|
|
112
|
+
if (isOpenRef.current) {
|
|
113
|
+
isOpenRef.current = false;
|
|
114
|
+
sdk.close();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}, [isReady, isOpen, name, domain, company, title, companyDescription, liUrl, sdk]);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
// ── RoleplayEmbed ───────────────────────────────────────────────────
|
|
121
|
+
// The vanilla SDK's mount() doesn't accept callbacks, so RoleplayEmbed
|
|
122
|
+
// sets up its own postMessage listener after mount() appends the iframe.
|
|
123
|
+
const DEFAULT_APP_ORIGIN = "https://app.roleplaywithseamless.ai";
|
|
124
|
+
export function RoleplayEmbed({ className, style, onCallStarted, onCallEnded, onClose, onError, }) {
|
|
125
|
+
const { isReady, sdk } = useSeamlessRoleplay();
|
|
126
|
+
const containerRef = useRef(null);
|
|
127
|
+
const onCallStartedRef = useCallbackRef(onCallStarted);
|
|
128
|
+
const onCallEndedRef = useCallbackRef(onCallEnded);
|
|
129
|
+
const onCloseRef = useCallbackRef(onClose);
|
|
130
|
+
const onErrorRef = useCallbackRef(onError);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!isReady || !containerRef.current)
|
|
133
|
+
return;
|
|
134
|
+
sdk.mount(containerRef.current);
|
|
135
|
+
// Grab the iframe that mount() just appended
|
|
136
|
+
const iframe = containerRef.current.querySelector("iframe");
|
|
137
|
+
if (!iframe)
|
|
138
|
+
return;
|
|
139
|
+
const hasCallbacks = onCallStarted || onCallEnded || onClose || onError;
|
|
140
|
+
let listener = null;
|
|
141
|
+
if (hasCallbacks) {
|
|
142
|
+
listener = (event) => {
|
|
143
|
+
var _a, _b, _c, _d;
|
|
144
|
+
if (event.origin !== DEFAULT_APP_ORIGIN)
|
|
145
|
+
return;
|
|
146
|
+
if (!iframe.contentWindow || event.source !== iframe.contentWindow)
|
|
147
|
+
return;
|
|
148
|
+
const data = event.data;
|
|
149
|
+
if (!data || typeof data.type !== "string")
|
|
150
|
+
return;
|
|
151
|
+
switch (data.type) {
|
|
152
|
+
case "ROLEPLAY_CALL_STARTED":
|
|
153
|
+
(_a = onCallStartedRef.current) === null || _a === void 0 ? void 0 : _a.call(onCallStartedRef, { callId: data.callId });
|
|
154
|
+
break;
|
|
155
|
+
case "ROLEPLAY_CALL_ENDED":
|
|
156
|
+
(_b = onCallEndedRef.current) === null || _b === void 0 ? void 0 : _b.call(onCallEndedRef, {
|
|
157
|
+
callId: data.callId,
|
|
158
|
+
duration: data.duration,
|
|
159
|
+
});
|
|
160
|
+
break;
|
|
161
|
+
case "ROLEPLAY_ERROR":
|
|
162
|
+
(_c = onErrorRef.current) === null || _c === void 0 ? void 0 : _c.call(onErrorRef, {
|
|
163
|
+
code: data.code,
|
|
164
|
+
message: data.message,
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
case "ROLEPLAY_CLOSED":
|
|
168
|
+
(_d = onCloseRef.current) === null || _d === void 0 ? void 0 : _d.call(onCloseRef);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
window.addEventListener("message", listener);
|
|
173
|
+
}
|
|
174
|
+
return () => {
|
|
175
|
+
if (listener)
|
|
176
|
+
window.removeEventListener("message", listener);
|
|
177
|
+
sdk.unmount();
|
|
178
|
+
};
|
|
179
|
+
}, [isReady, sdk]);
|
|
180
|
+
return _jsx("div", { ref: containerRef, className: className, style: style });
|
|
181
|
+
}
|
|
182
|
+
export function AddToScenarioDialog({ open: isOpen, contacts, onComplete, onClose, onError, }) {
|
|
183
|
+
const { isReady, sdk } = useSeamlessRoleplay();
|
|
184
|
+
const onCompleteRef = useCallbackRef(onComplete);
|
|
185
|
+
const onCloseRef = useCallbackRef(onClose);
|
|
186
|
+
const onErrorRef = useCallbackRef(onError);
|
|
187
|
+
const isOpenRef = useRef(false);
|
|
188
|
+
const contactsRef = useRef(contacts);
|
|
189
|
+
useLayoutEffect(() => {
|
|
190
|
+
contactsRef.current = contacts;
|
|
191
|
+
});
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (!isReady)
|
|
194
|
+
return;
|
|
195
|
+
if (isOpen) {
|
|
196
|
+
isOpenRef.current = true;
|
|
197
|
+
sdk.addToScenario({
|
|
198
|
+
contacts: contactsRef.current,
|
|
199
|
+
onComplete: (data) => {
|
|
200
|
+
var _a;
|
|
201
|
+
isOpenRef.current = false;
|
|
202
|
+
(_a = onCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onCompleteRef, data);
|
|
203
|
+
},
|
|
204
|
+
onClose: () => {
|
|
205
|
+
var _a;
|
|
206
|
+
isOpenRef.current = false;
|
|
207
|
+
(_a = onCloseRef.current) === null || _a === void 0 ? void 0 : _a.call(onCloseRef);
|
|
208
|
+
},
|
|
209
|
+
onError: (err) => { var _a; return (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err); },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else if (isOpenRef.current) {
|
|
213
|
+
isOpenRef.current = false;
|
|
214
|
+
sdk.close();
|
|
215
|
+
}
|
|
216
|
+
return () => {
|
|
217
|
+
if (isOpenRef.current) {
|
|
218
|
+
isOpenRef.current = false;
|
|
219
|
+
sdk.close();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}, [isReady, isOpen, sdk]);
|
|
223
|
+
return null;
|
|
224
|
+
}
|