@rehers/rehers-roleplay-sdk 2.4.1 → 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 +285 -26
- package/package.json +23 -3
- package/react.d.ts +78 -0
- package/react.js +224 -0
package/README.md
CHANGED
|
@@ -9,21 +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
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## React Integration (Recommended)
|
|
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";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Get the logged-in Seamless user
|
|
35
|
+
|
|
36
|
+
Use the existing Seamless dashboard session and call:
|
|
37
|
+
|
|
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
|
+
}
|
|
131
|
+
```
|
|
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
|
+
|
|
263
|
+
```js
|
|
264
|
+
import SeamlessRoleplay from "@rehers/rehers-roleplay-sdk";
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### Option 2: Script tag (hosted file)
|
|
15
268
|
|
|
16
269
|
```html
|
|
17
270
|
<script src="/path/to/roleplay-sdk.js"></script>
|
|
18
271
|
```
|
|
19
272
|
|
|
20
|
-
|
|
273
|
+
#### Option 3: CDN script
|
|
21
274
|
|
|
22
|
-
```
|
|
23
|
-
|
|
275
|
+
```html
|
|
276
|
+
<script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
|
|
24
277
|
```
|
|
25
278
|
|
|
26
|
-
|
|
279
|
+
### Get the logged-in Seamless user
|
|
27
280
|
|
|
28
281
|
Use the existing Seamless dashboard session and call:
|
|
29
282
|
|
|
@@ -43,7 +296,7 @@ Normalize the response before reading fields:
|
|
|
43
296
|
const me = meResponse.data ?? meResponse;
|
|
44
297
|
```
|
|
45
298
|
|
|
46
|
-
|
|
299
|
+
### Required mapping for `init()`
|
|
47
300
|
|
|
48
301
|
These are the values Seamless must pass into `SeamlessRoleplay.init(...)`:
|
|
49
302
|
|
|
@@ -64,14 +317,7 @@ const seamlessUserRole =
|
|
|
64
317
|
"member";
|
|
65
318
|
```
|
|
66
319
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```js
|
|
70
|
-
const seamlessUserId = String(me.id);
|
|
71
|
-
const seamlessUserEmail = me.username;
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Initialize the SDK once
|
|
320
|
+
### Initialize the SDK once
|
|
75
321
|
|
|
76
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.
|
|
77
323
|
|
|
@@ -115,7 +361,7 @@ function ensureRoleplaySdkReady() {
|
|
|
115
361
|
}
|
|
116
362
|
```
|
|
117
363
|
|
|
118
|
-
|
|
364
|
+
### Embed in the full Roleplay page
|
|
119
365
|
|
|
120
366
|
Use `mount(container)` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
|
|
121
367
|
|
|
@@ -138,7 +384,7 @@ If the Seamless page or tab is torn down, unmount the SDK:
|
|
|
138
384
|
SeamlessRoleplay.unmount();
|
|
139
385
|
```
|
|
140
386
|
|
|
141
|
-
|
|
387
|
+
### Embed in Contact Search
|
|
142
388
|
|
|
143
389
|
Use `open(contactData)` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
|
|
144
390
|
|
|
@@ -180,7 +426,7 @@ The contact object passed to `open(...)` should map to:
|
|
|
180
426
|
| `liUrl` | LinkedIn profile URL, if available |
|
|
181
427
|
| `companyDescription` | Company description, if available |
|
|
182
428
|
|
|
183
|
-
|
|
429
|
+
### Add contacts to a scenario
|
|
184
430
|
|
|
185
431
|
If Seamless wants to send multiple contacts into a scenario picker dialog, use `addToScenario(...)`.
|
|
186
432
|
|
|
@@ -210,9 +456,9 @@ async function addContactsToScenario(contacts) {
|
|
|
210
456
|
}
|
|
211
457
|
```
|
|
212
458
|
|
|
213
|
-
|
|
459
|
+
### API reference
|
|
214
460
|
|
|
215
|
-
|
|
461
|
+
#### `SeamlessRoleplay.init(options)`
|
|
216
462
|
|
|
217
463
|
Initializes the SDK with the logged-in Seamless user.
|
|
218
464
|
|
|
@@ -227,30 +473,32 @@ SeamlessRoleplay.init({
|
|
|
227
473
|
});
|
|
228
474
|
```
|
|
229
475
|
|
|
230
|
-
|
|
476
|
+
#### `SeamlessRoleplay.mount(container)`
|
|
231
477
|
|
|
232
478
|
Mounts the full Roleplay app into a dashboard container.
|
|
233
479
|
|
|
234
|
-
|
|
480
|
+
#### `SeamlessRoleplay.open(contactData)`
|
|
235
481
|
|
|
236
482
|
Opens the Roleplay dialog for a single contact.
|
|
237
483
|
|
|
238
|
-
|
|
484
|
+
#### `SeamlessRoleplay.addToScenario(options)`
|
|
239
485
|
|
|
240
486
|
Opens the bulk add-to-scenario dialog for 1 to 25 contacts.
|
|
241
487
|
|
|
242
|
-
|
|
488
|
+
#### `SeamlessRoleplay.close()`
|
|
243
489
|
|
|
244
490
|
Closes the active dialog.
|
|
245
491
|
|
|
246
|
-
|
|
492
|
+
#### `SeamlessRoleplay.unmount()`
|
|
247
493
|
|
|
248
494
|
Unmounts the full-page Roleplay embed.
|
|
249
495
|
|
|
250
|
-
|
|
496
|
+
#### `SeamlessRoleplay.destroy()`
|
|
251
497
|
|
|
252
498
|
Destroys the SDK state, timers, mount, and dialogs.
|
|
253
499
|
|
|
500
|
+
---
|
|
501
|
+
|
|
254
502
|
## Trial and upgrade handling
|
|
255
503
|
|
|
256
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.
|
|
@@ -260,10 +508,21 @@ If the backend responds with `USER_NOT_FOUND` during SDK session creation, the S
|
|
|
260
508
|
Type declarations are included with the SDK package.
|
|
261
509
|
|
|
262
510
|
```ts
|
|
511
|
+
// Vanilla SDK types
|
|
263
512
|
import type {
|
|
264
513
|
SeamlessRoleplaySDK,
|
|
265
514
|
SeamlessRoleplayInitOptions,
|
|
266
515
|
SeamlessRoleplayOpenData,
|
|
267
516
|
AddToScenarioOptions,
|
|
268
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";
|
|
269
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
|
+
}
|