@rehers/rehers-roleplay-sdk 2.5.0 → 2.5.1
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 +148 -436
- package/index.d.ts +0 -4
- package/package.json +1 -1
- package/react.d.ts +2 -21
- package/react.js +2 -55
- package/roleplay-sdk.js +5 -31
package/README.md
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
# Roleplay SDK for
|
|
2
|
-
|
|
3
|
-
This SDK is only for embedding Roleplay inside the Seamless.AI dashboard.
|
|
4
|
-
|
|
5
|
-
Use it in these two places:
|
|
6
|
-
|
|
7
|
-
- The dedicated Roleplay page or tab inside the Seamless dashboard
|
|
8
|
-
- The Contact Search screen, where clicking `Roleplay` opens a dialog for a single contact
|
|
9
|
-
|
|
10
|
-
The SDK must be initialized with the currently logged-in Seamless user before either embed flow is used.
|
|
1
|
+
# Roleplay SDK for Seamless.AI
|
|
11
2
|
|
|
12
3
|
## Install
|
|
13
4
|
|
|
@@ -17,512 +8,233 @@ npm install @rehers/rehers-roleplay-sdk
|
|
|
17
8
|
|
|
18
9
|
---
|
|
19
10
|
|
|
20
|
-
##
|
|
11
|
+
## 1. Wrap your app with the Provider
|
|
21
12
|
|
|
22
|
-
|
|
13
|
+
Add this once, above all your routes. It initializes the SDK for the logged-in Seamless user.
|
|
23
14
|
|
|
24
15
|
```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
|
|
16
|
+
import { SeamlessRoleplayProvider } from "@rehers/rehers-roleplay-sdk/react";
|
|
58
17
|
|
|
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
18
|
function App() {
|
|
19
|
+
// You already have the logged-in user from your auth/session.
|
|
20
|
+
// The SDK needs their id and email.
|
|
21
|
+
const me = useCurrentUser(); // however you get the logged-in user
|
|
22
|
+
|
|
63
23
|
return (
|
|
64
24
|
<SeamlessRoleplayProvider
|
|
65
|
-
publishableKey=
|
|
66
|
-
userId={
|
|
67
|
-
userEmail={
|
|
68
|
-
userRole={
|
|
25
|
+
publishableKey="pk_live_..."
|
|
26
|
+
userId={String(me.id)}
|
|
27
|
+
userEmail={me.username}
|
|
28
|
+
userRole={me.orgRole}
|
|
69
29
|
onReady={() => console.log("Roleplay SDK ready")}
|
|
70
30
|
onError={(err) => console.error("Roleplay SDK error", err)}
|
|
71
31
|
>
|
|
72
|
-
<
|
|
32
|
+
<Routes>
|
|
33
|
+
<Route path="/roleplay" element={<RoleplayPage />} />
|
|
34
|
+
<Route path="/contacts" element={<ContactSearch />} />
|
|
35
|
+
{/* ...your other routes */}
|
|
36
|
+
</Routes>
|
|
73
37
|
</SeamlessRoleplayProvider>
|
|
74
38
|
);
|
|
75
39
|
}
|
|
76
40
|
```
|
|
77
41
|
|
|
78
|
-
|
|
42
|
+
If you fetch the user via API:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const res = await fetch("https://api.seamless.ai/api/users/me", {
|
|
46
|
+
credentials: "include",
|
|
47
|
+
}).then((r) => r.json());
|
|
48
|
+
|
|
49
|
+
const me = res.data ?? res;
|
|
50
|
+
// me.id → pass as userId (convert to string)
|
|
51
|
+
// me.username → pass as userEmail
|
|
52
|
+
// me.orgRole → pass as userRole directly
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's the only setup. Everything below just works.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2. Full Roleplay page
|
|
79
60
|
|
|
80
|
-
|
|
61
|
+
For the dedicated Roleplay tab/page, drop in `RoleplayEmbed`. It fills its container.
|
|
81
62
|
|
|
82
63
|
```tsx
|
|
64
|
+
import { RoleplayEmbed } from "@rehers/rehers-roleplay-sdk/react";
|
|
65
|
+
|
|
83
66
|
function RoleplayPage() {
|
|
84
67
|
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
|
-
/>
|
|
68
|
+
<RoleplayEmbed style={{ width: "100%", height: "100%" }} />
|
|
91
69
|
);
|
|
92
70
|
}
|
|
93
71
|
```
|
|
94
72
|
|
|
95
|
-
|
|
73
|
+
Make sure the parent has a height (`height: 100%`, `height: 100vh`, `flex: 1`, etc).
|
|
74
|
+
|
|
75
|
+
Mounts automatically. Cleans up automatically when the user navigates away.
|
|
76
|
+
|
|
77
|
+
---
|
|
96
78
|
|
|
97
|
-
|
|
79
|
+
## 3. Roleplay dialog on Contact Search
|
|
98
80
|
|
|
99
|
-
|
|
81
|
+
When a user clicks "Roleplay" on a contact row, a dialog opens over the page.
|
|
100
82
|
|
|
101
83
|
```tsx
|
|
102
|
-
|
|
103
|
-
|
|
84
|
+
import { useState } from "react";
|
|
85
|
+
import { RoleplayDialog } from "@rehers/rehers-roleplay-sdk/react";
|
|
86
|
+
|
|
87
|
+
function ContactSearch() {
|
|
88
|
+
const [activeContact, setActiveContact] = useState(null);
|
|
104
89
|
|
|
105
90
|
return (
|
|
106
91
|
<>
|
|
107
|
-
|
|
108
|
-
|
|
92
|
+
{/* Your existing contact table — just add an onClick to each row's button */}
|
|
93
|
+
<table>
|
|
94
|
+
{contacts.map((contact) => (
|
|
95
|
+
<tr key={contact.id}>
|
|
96
|
+
<td>{contact.name}</td>
|
|
97
|
+
<td>{contact.company}</td>
|
|
98
|
+
<td>{contact.title}</td>
|
|
99
|
+
<td>
|
|
100
|
+
<button onClick={() => setActiveContact(contact)}>
|
|
101
|
+
Roleplay
|
|
102
|
+
</button>
|
|
103
|
+
</td>
|
|
104
|
+
</tr>
|
|
105
|
+
))}
|
|
106
|
+
</table>
|
|
107
|
+
|
|
108
|
+
{/* This renders nothing visible — the SDK creates the overlay */}
|
|
109
109
|
<RoleplayDialog
|
|
110
|
-
open={
|
|
111
|
-
name={
|
|
112
|
-
domain={
|
|
113
|
-
company={
|
|
114
|
-
title={
|
|
115
|
-
liUrl={
|
|
116
|
-
companyDescription={
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
}}
|
|
110
|
+
open={activeContact !== null}
|
|
111
|
+
name={activeContact?.name ?? ""}
|
|
112
|
+
domain={activeContact?.domain ?? ""}
|
|
113
|
+
company={activeContact?.company ?? ""}
|
|
114
|
+
title={activeContact?.title ?? ""}
|
|
115
|
+
liUrl={activeContact?.linkedinUrl}
|
|
116
|
+
companyDescription={activeContact?.companyDescription}
|
|
117
|
+
onClose={() => setActiveContact(null)}
|
|
118
|
+
onError={(err) => console.error("Error", err)}
|
|
127
119
|
/>
|
|
128
120
|
</>
|
|
129
121
|
);
|
|
130
122
|
}
|
|
131
123
|
```
|
|
132
124
|
|
|
133
|
-
|
|
125
|
+
**How it works:** Set `activeContact` to a contact object to open the dialog. Set it back to `null` to close. The `onClose` callback fires when the user closes it themselves.
|
|
126
|
+
|
|
127
|
+
---
|
|
134
128
|
|
|
135
|
-
|
|
129
|
+
## 4. Add to Scenario (bulk)
|
|
136
130
|
|
|
137
|
-
|
|
131
|
+
When users select multiple contacts and click "Add to Scenario", a scenario picker dialog opens.
|
|
138
132
|
|
|
139
133
|
```tsx
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
import { useState } from "react";
|
|
135
|
+
import { AddToScenarioDialog } from "@rehers/rehers-roleplay-sdk/react";
|
|
136
|
+
|
|
137
|
+
function ContactSearch() {
|
|
138
|
+
const [selectedContacts, setSelectedContacts] = useState([]);
|
|
139
|
+
const [showScenario, setShowScenario] = useState(false);
|
|
142
140
|
|
|
143
141
|
return (
|
|
144
142
|
<>
|
|
145
|
-
|
|
143
|
+
{/* Your existing table with checkboxes that populate selectedContacts */}
|
|
144
|
+
|
|
145
|
+
<button
|
|
146
|
+
disabled={selectedContacts.length === 0}
|
|
147
|
+
onClick={() => setShowScenario(true)}
|
|
148
|
+
>
|
|
149
|
+
Add {selectedContacts.length} to Scenario
|
|
150
|
+
</button>
|
|
146
151
|
|
|
147
152
|
<AddToScenarioDialog
|
|
148
|
-
open={
|
|
149
|
-
contacts={selectedContacts.map((
|
|
150
|
-
name:
|
|
151
|
-
company:
|
|
152
|
-
title:
|
|
153
|
-
domain:
|
|
154
|
-
liUrl:
|
|
155
|
-
companyDescription: contact.companyDescription,
|
|
153
|
+
open={showScenario}
|
|
154
|
+
contacts={selectedContacts.map((c) => ({
|
|
155
|
+
name: c.name,
|
|
156
|
+
company: c.company,
|
|
157
|
+
title: c.title,
|
|
158
|
+
domain: c.domain,
|
|
159
|
+
liUrl: c.linkedinUrl,
|
|
156
160
|
}))}
|
|
157
161
|
onComplete={(data) => {
|
|
158
|
-
console.log(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
onClose={() => setShowATS(false)}
|
|
162
|
-
onError={(err) => {
|
|
163
|
-
console.error("Add to scenario error", err);
|
|
162
|
+
console.log(`Added ${data.addedCount} to ${data.scenarioName}`);
|
|
163
|
+
setShowScenario(false);
|
|
164
|
+
setSelectedContacts([]);
|
|
164
165
|
}}
|
|
166
|
+
onClose={() => setShowScenario(false)}
|
|
167
|
+
onError={(err) => console.error("Error", err)}
|
|
165
168
|
/>
|
|
166
169
|
</>
|
|
167
170
|
);
|
|
168
171
|
}
|
|
169
172
|
```
|
|
170
173
|
|
|
171
|
-
|
|
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`.
|
|
174
|
+
Accepts 1 to 25 contacts. Each contact needs `name`, `company`, `title`, and `domain`.
|
|
252
175
|
|
|
253
176
|
---
|
|
254
177
|
|
|
255
|
-
##
|
|
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
|
|
178
|
+
## Contact field mapping
|
|
262
179
|
|
|
263
|
-
|
|
264
|
-
import SeamlessRoleplay from "@rehers/rehers-roleplay-sdk";
|
|
265
|
-
```
|
|
180
|
+
When passing contact data to `RoleplayDialog` or `AddToScenarioDialog`:
|
|
266
181
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
```html
|
|
270
|
-
<script src="/path/to/roleplay-sdk.js"></script>
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
#### Option 3: CDN script
|
|
274
|
-
|
|
275
|
-
```html
|
|
276
|
-
<script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Get the logged-in Seamless user
|
|
280
|
-
|
|
281
|
-
Use the existing Seamless dashboard session and call:
|
|
282
|
-
|
|
283
|
-
```js
|
|
284
|
-
const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
|
|
285
|
-
method: "GET",
|
|
286
|
-
credentials: "include",
|
|
287
|
-
headers: {
|
|
288
|
-
accept: "application/json, text/plain, */*",
|
|
289
|
-
},
|
|
290
|
-
}).then((res) => res.json());
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
Normalize the response before reading fields:
|
|
294
|
-
|
|
295
|
-
```js
|
|
296
|
-
const me = meResponse.data ?? meResponse;
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### Required mapping for `init()`
|
|
300
|
-
|
|
301
|
-
These are the values Seamless must pass into `SeamlessRoleplay.init(...)`:
|
|
302
|
-
|
|
303
|
-
| SDK field | Seamless `/api/users/me` field |
|
|
182
|
+
| SDK prop | Your contact field |
|
|
304
183
|
|---|---|
|
|
305
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
| `
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
```js
|
|
312
|
-
const seamlessUserId = String(me.id);
|
|
313
|
-
const seamlessUserEmail = me.username;
|
|
314
|
-
const seamlessUserRole =
|
|
315
|
-
me.orgRole === "owner" ? "owner" :
|
|
316
|
-
me.isOrgAdmin ? "admin" :
|
|
317
|
-
"member";
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Initialize the SDK once
|
|
321
|
-
|
|
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.
|
|
323
|
-
|
|
324
|
-
```js
|
|
325
|
-
let roleplayReadyPromise;
|
|
326
|
-
|
|
327
|
-
function ensureRoleplaySdkReady() {
|
|
328
|
-
if (roleplayReadyPromise) return roleplayReadyPromise;
|
|
329
|
-
|
|
330
|
-
roleplayReadyPromise = (async () => {
|
|
331
|
-
const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
|
|
332
|
-
method: "GET",
|
|
333
|
-
credentials: "include",
|
|
334
|
-
headers: {
|
|
335
|
-
accept: "application/json, text/plain, */*",
|
|
336
|
-
},
|
|
337
|
-
}).then((res) => res.json());
|
|
338
|
-
|
|
339
|
-
const me = meResponse.data ?? meResponse;
|
|
340
|
-
|
|
341
|
-
const seamlessUserId = String(me.id);
|
|
342
|
-
const seamlessUserEmail = me.username;
|
|
343
|
-
const seamlessUserRole =
|
|
344
|
-
me.orgRole === "owner" ? "owner" :
|
|
345
|
-
me.isOrgAdmin ? "admin" :
|
|
346
|
-
"member";
|
|
347
|
-
|
|
348
|
-
await new Promise((resolve, reject) => {
|
|
349
|
-
SeamlessRoleplay.init({
|
|
350
|
-
publishableKey: ROLEPLAY_PUBLISHABLE_KEY,
|
|
351
|
-
userId: seamlessUserId,
|
|
352
|
-
userEmail: seamlessUserEmail,
|
|
353
|
-
userRole: seamlessUserRole,
|
|
354
|
-
onReady: resolve,
|
|
355
|
-
onError: reject,
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
})();
|
|
359
|
-
|
|
360
|
-
return roleplayReadyPromise;
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### Embed in the full Roleplay page
|
|
365
|
-
|
|
366
|
-
Use `mount(container)` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
|
|
367
|
-
|
|
368
|
-
```html
|
|
369
|
-
<div id="roleplay-root" style="width: 100%; height: 100%;"></div>
|
|
370
|
-
```
|
|
184
|
+
| `name` | Full name |
|
|
185
|
+
| `domain` | Company domain (e.g. `"stripe.com"`) |
|
|
186
|
+
| `company` | Company name |
|
|
187
|
+
| `title` | Job title |
|
|
188
|
+
| `liUrl` | LinkedIn URL (optional) |
|
|
189
|
+
| `companyDescription` | Company description (optional) |
|
|
371
190
|
|
|
372
|
-
|
|
373
|
-
async function mountRoleplayPage() {
|
|
374
|
-
await ensureRoleplaySdkReady();
|
|
191
|
+
---
|
|
375
192
|
|
|
376
|
-
|
|
377
|
-
SeamlessRoleplay.mount(container);
|
|
378
|
-
}
|
|
379
|
-
```
|
|
193
|
+
## That's it
|
|
380
194
|
|
|
381
|
-
|
|
195
|
+
- **Provider** wraps your app once — handles auth, sessions, cleanup
|
|
196
|
+
- **RoleplayEmbed** is your full Roleplay page — one component
|
|
197
|
+
- **RoleplayDialog** opens per-contact from Contact Search — controlled by a boolean
|
|
198
|
+
- **AddToScenarioDialog** opens for bulk import — controlled by a boolean
|
|
382
199
|
|
|
383
|
-
|
|
384
|
-
SeamlessRoleplay.unmount();
|
|
385
|
-
```
|
|
200
|
+
All three clean up after themselves. No manual `destroy()`, no `useEffect`, no refs.
|
|
386
201
|
|
|
387
|
-
|
|
202
|
+
TypeScript types are included — your editor will autocomplete all props.
|
|
388
203
|
|
|
389
|
-
|
|
204
|
+
---
|
|
390
205
|
|
|
391
|
-
|
|
392
|
-
async function openRoleplayForContact(contact) {
|
|
393
|
-
await ensureRoleplaySdkReady();
|
|
394
|
-
|
|
395
|
-
SeamlessRoleplay.open({
|
|
396
|
-
name: contact.name,
|
|
397
|
-
domain: contact.domain,
|
|
398
|
-
company: contact.company,
|
|
399
|
-
title: contact.title,
|
|
400
|
-
liUrl: contact.linkedinUrl,
|
|
401
|
-
companyDescription: contact.companyDescription,
|
|
402
|
-
onCallStarted(data) {
|
|
403
|
-
console.log("Roleplay call started", data.callId);
|
|
404
|
-
},
|
|
405
|
-
onCallEnded(data) {
|
|
406
|
-
console.log("Roleplay call ended", data.callId, data.duration);
|
|
407
|
-
},
|
|
408
|
-
onClose() {
|
|
409
|
-
console.log("Roleplay dialog closed");
|
|
410
|
-
},
|
|
411
|
-
onError(err) {
|
|
412
|
-
console.error("Roleplay dialog error", err);
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
```
|
|
206
|
+
## Trial and upgrade handling
|
|
417
207
|
|
|
418
|
-
|
|
208
|
+
If a user doesn't have a Roleplay subscription, the SDK shows the upgrade/trial UI automatically inside the iframe. No client-side logic needed.
|
|
419
209
|
|
|
420
|
-
|
|
421
|
-
|---|---|
|
|
422
|
-
| `name` | Contact full name |
|
|
423
|
-
| `domain` | Company domain |
|
|
424
|
-
| `company` | Company name |
|
|
425
|
-
| `title` | Contact title |
|
|
426
|
-
| `liUrl` | LinkedIn profile URL, if available |
|
|
427
|
-
| `companyDescription` | Company description, if available |
|
|
210
|
+
---
|
|
428
211
|
|
|
429
|
-
|
|
212
|
+
## Advanced: Vanilla JavaScript
|
|
430
213
|
|
|
431
|
-
If
|
|
214
|
+
If you need the vanilla SDK without React:
|
|
432
215
|
|
|
433
216
|
```js
|
|
434
|
-
|
|
435
|
-
await ensureRoleplaySdkReady();
|
|
436
|
-
|
|
437
|
-
SeamlessRoleplay.addToScenario({
|
|
438
|
-
contacts: contacts.map((contact) => ({
|
|
439
|
-
name: contact.name,
|
|
440
|
-
company: contact.company,
|
|
441
|
-
title: contact.title,
|
|
442
|
-
domain: contact.domain,
|
|
443
|
-
liUrl: contact.linkedinUrl,
|
|
444
|
-
companyDescription: contact.companyDescription,
|
|
445
|
-
})),
|
|
446
|
-
onComplete(data) {
|
|
447
|
-
console.log("Scenario import complete", data);
|
|
448
|
-
},
|
|
449
|
-
onClose() {
|
|
450
|
-
console.log("Add to scenario dialog closed");
|
|
451
|
-
},
|
|
452
|
-
onError(err) {
|
|
453
|
-
console.error("Add to scenario error", err);
|
|
454
|
-
},
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### API reference
|
|
460
|
-
|
|
461
|
-
#### `SeamlessRoleplay.init(options)`
|
|
462
|
-
|
|
463
|
-
Initializes the SDK with the logged-in Seamless user.
|
|
217
|
+
import "@rehers/rehers-roleplay-sdk";
|
|
464
218
|
|
|
465
|
-
|
|
219
|
+
// Initialize once
|
|
466
220
|
SeamlessRoleplay.init({
|
|
467
|
-
publishableKey,
|
|
468
|
-
userId,
|
|
469
|
-
userEmail,
|
|
470
|
-
|
|
471
|
-
onReady,
|
|
472
|
-
onError,
|
|
221
|
+
publishableKey: "pk_live_...",
|
|
222
|
+
userId: "...",
|
|
223
|
+
userEmail: "...",
|
|
224
|
+
onReady() { console.log("ready"); },
|
|
473
225
|
});
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
#### `SeamlessRoleplay.mount(container)`
|
|
477
|
-
|
|
478
|
-
Mounts the full Roleplay app into a dashboard container.
|
|
479
226
|
|
|
480
|
-
|
|
227
|
+
// Full page embed
|
|
228
|
+
SeamlessRoleplay.mount(document.getElementById("container"));
|
|
481
229
|
|
|
482
|
-
|
|
230
|
+
// Contact dialog
|
|
231
|
+
SeamlessRoleplay.open({ name: "...", domain: "...", company: "...", title: "..." });
|
|
483
232
|
|
|
484
|
-
|
|
233
|
+
// Bulk import
|
|
234
|
+
SeamlessRoleplay.addToScenario({ contacts: [...], onComplete(data) { } });
|
|
485
235
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
Closes the active dialog.
|
|
491
|
-
|
|
492
|
-
#### `SeamlessRoleplay.unmount()`
|
|
493
|
-
|
|
494
|
-
Unmounts the full-page Roleplay embed.
|
|
495
|
-
|
|
496
|
-
#### `SeamlessRoleplay.destroy()`
|
|
497
|
-
|
|
498
|
-
Destroys the SDK state, timers, mount, and dialogs.
|
|
499
|
-
|
|
500
|
-
---
|
|
501
|
-
|
|
502
|
-
## Trial and upgrade handling
|
|
503
|
-
|
|
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.
|
|
505
|
-
|
|
506
|
-
## TypeScript
|
|
507
|
-
|
|
508
|
-
Type declarations are included with the SDK package.
|
|
509
|
-
|
|
510
|
-
```ts
|
|
511
|
-
// Vanilla SDK types
|
|
512
|
-
import type {
|
|
513
|
-
SeamlessRoleplaySDK,
|
|
514
|
-
SeamlessRoleplayInitOptions,
|
|
515
|
-
SeamlessRoleplayOpenData,
|
|
516
|
-
AddToScenarioOptions,
|
|
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";
|
|
236
|
+
// Cleanup
|
|
237
|
+
SeamlessRoleplay.close(); // close dialog
|
|
238
|
+
SeamlessRoleplay.unmount(); // remove embed
|
|
239
|
+
SeamlessRoleplay.destroy(); // full cleanup
|
|
528
240
|
```
|
package/index.d.ts
CHANGED
|
@@ -30,10 +30,6 @@ export interface SeamlessRoleplayOpenData {
|
|
|
30
30
|
companyDescription?: string;
|
|
31
31
|
/** Optional LinkedIn profile URL */
|
|
32
32
|
liUrl?: string;
|
|
33
|
-
/** Called when the roleplay call starts */
|
|
34
|
-
onCallStarted?: (data: { callId: string }) => void;
|
|
35
|
-
/** Called when the roleplay call ends */
|
|
36
|
-
onCallEnded?: (data: { callId: string; duration?: number }) => void;
|
|
37
33
|
/** Called when the dialog/mount is closed */
|
|
38
34
|
onClose?: () => void;
|
|
39
35
|
/** Called on error during the session */
|
package/package.json
CHANGED
package/react.d.ts
CHANGED
|
@@ -34,37 +34,18 @@ export interface RoleplayDialogProps {
|
|
|
34
34
|
title: string;
|
|
35
35
|
companyDescription?: string;
|
|
36
36
|
liUrl?: string;
|
|
37
|
-
onCallStarted?: (data: {
|
|
38
|
-
callId: string;
|
|
39
|
-
}) => void;
|
|
40
|
-
onCallEnded?: (data: {
|
|
41
|
-
callId: string;
|
|
42
|
-
duration?: number;
|
|
43
|
-
}) => void;
|
|
44
37
|
onClose?: () => void;
|
|
45
38
|
onError?: (data: {
|
|
46
39
|
code: string;
|
|
47
40
|
message: string;
|
|
48
41
|
}) => void;
|
|
49
42
|
}
|
|
50
|
-
export declare function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl,
|
|
43
|
+
export declare function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onClose, onError, }: RoleplayDialogProps): null;
|
|
51
44
|
export interface RoleplayEmbedProps {
|
|
52
45
|
className?: string;
|
|
53
46
|
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
47
|
}
|
|
67
|
-
export declare function RoleplayEmbed({ className, style
|
|
48
|
+
export declare function RoleplayEmbed({ className, style }: RoleplayEmbedProps): import("react/jsx-runtime").JSX.Element;
|
|
68
49
|
export interface AddToScenarioDialogProps {
|
|
69
50
|
open: boolean;
|
|
70
51
|
contacts: AddToScenarioContact[];
|
package/react.js
CHANGED
|
@@ -75,10 +75,8 @@ export function useSeamlessRoleplay() {
|
|
|
75
75
|
}
|
|
76
76
|
return ctx;
|
|
77
77
|
}
|
|
78
|
-
export function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl,
|
|
78
|
+
export function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onClose, onError, }) {
|
|
79
79
|
const { isReady, sdk } = useSeamlessRoleplay();
|
|
80
|
-
const onCallStartedRef = useCallbackRef(onCallStarted);
|
|
81
|
-
const onCallEndedRef = useCallbackRef(onCallEnded);
|
|
82
80
|
const onCloseRef = useCallbackRef(onClose);
|
|
83
81
|
const onErrorRef = useCallbackRef(onError);
|
|
84
82
|
const isOpenRef = useRef(false);
|
|
@@ -94,8 +92,6 @@ export function RoleplayDialog({ open: isOpen, name, domain, company, title, com
|
|
|
94
92
|
title,
|
|
95
93
|
companyDescription,
|
|
96
94
|
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
95
|
onClose: () => {
|
|
100
96
|
var _a;
|
|
101
97
|
isOpenRef.current = false;
|
|
@@ -117,63 +113,14 @@ export function RoleplayDialog({ open: isOpen, name, domain, company, title, com
|
|
|
117
113
|
}, [isReady, isOpen, name, domain, company, title, companyDescription, liUrl, sdk]);
|
|
118
114
|
return null;
|
|
119
115
|
}
|
|
120
|
-
|
|
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, }) {
|
|
116
|
+
export function RoleplayEmbed({ className, style }) {
|
|
125
117
|
const { isReady, sdk } = useSeamlessRoleplay();
|
|
126
118
|
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
119
|
useEffect(() => {
|
|
132
120
|
if (!isReady || !containerRef.current)
|
|
133
121
|
return;
|
|
134
122
|
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
123
|
return () => {
|
|
175
|
-
if (listener)
|
|
176
|
-
window.removeEventListener("message", listener);
|
|
177
124
|
sdk.unmount();
|
|
178
125
|
};
|
|
179
126
|
}, [isReady, sdk]);
|
package/roleplay-sdk.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// ── Mount state (persistent embed — survives dialog open/close) ───
|
|
36
36
|
var mountIframe = null;
|
|
37
37
|
var mountContainer = null;
|
|
38
|
-
var mountCallbacks = {
|
|
38
|
+
var mountCallbacks = { onClose: null, onError: null };
|
|
39
39
|
var mountListener = null;
|
|
40
40
|
|
|
41
41
|
// ── Dialog state (overlay — dialog or add-to-scenario) ────────────
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
var dialogIframe = null;
|
|
44
44
|
var dialogMode = null; // "dialog" | "add-to-scenario"
|
|
45
45
|
var dialogContactData = null;
|
|
46
|
-
var dialogCallbacks = {
|
|
46
|
+
var dialogCallbacks = { onClose: null, onError: null };
|
|
47
47
|
var dialogAddToScenarioCallbacks = { onComplete: null, onClose: null, onError: null };
|
|
48
48
|
var dialogAddToScenarioPendingContacts = null;
|
|
49
49
|
var dialogListener = null;
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
dialogContactData = null;
|
|
200
200
|
dialogAddToScenarioPendingContacts = null;
|
|
201
201
|
dialogMode = null;
|
|
202
|
-
dialogCallbacks = {
|
|
202
|
+
dialogCallbacks = { onClose: null, onError: null };
|
|
203
203
|
dialogAddToScenarioCallbacks = { onComplete: null, onClose: null, onError: null };
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
|
|
219
219
|
mountIframe = null;
|
|
220
220
|
mountContainer = null;
|
|
221
|
-
mountCallbacks = {
|
|
221
|
+
mountCallbacks = { onClose: null, onError: null };
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// ── Message dispatch ──────────────────────────────────────────────
|
|
@@ -284,18 +284,6 @@
|
|
|
284
284
|
});
|
|
285
285
|
break;
|
|
286
286
|
|
|
287
|
-
case "ROLEPLAY_CALL_STARTED":
|
|
288
|
-
if (mountCallbacks.onCallStarted) {
|
|
289
|
-
mountCallbacks.onCallStarted({ callId: data.callId });
|
|
290
|
-
}
|
|
291
|
-
break;
|
|
292
|
-
|
|
293
|
-
case "ROLEPLAY_CALL_ENDED":
|
|
294
|
-
if (mountCallbacks.onCallEnded) {
|
|
295
|
-
mountCallbacks.onCallEnded({ callId: data.callId, duration: data.duration });
|
|
296
|
-
}
|
|
297
|
-
break;
|
|
298
|
-
|
|
299
287
|
case "ROLEPLAY_ERROR":
|
|
300
288
|
if (mountCallbacks.onError) {
|
|
301
289
|
mountCallbacks.onError({ code: data.code, message: data.message });
|
|
@@ -334,18 +322,6 @@
|
|
|
334
322
|
});
|
|
335
323
|
break;
|
|
336
324
|
|
|
337
|
-
case "ROLEPLAY_CALL_STARTED":
|
|
338
|
-
if (dialogCallbacks.onCallStarted) {
|
|
339
|
-
dialogCallbacks.onCallStarted({ callId: data.callId });
|
|
340
|
-
}
|
|
341
|
-
break;
|
|
342
|
-
|
|
343
|
-
case "ROLEPLAY_CALL_ENDED":
|
|
344
|
-
if (dialogCallbacks.onCallEnded) {
|
|
345
|
-
dialogCallbacks.onCallEnded({ callId: data.callId, duration: data.duration });
|
|
346
|
-
}
|
|
347
|
-
break;
|
|
348
|
-
|
|
349
325
|
case "ROLEPLAY_ERROR":
|
|
350
326
|
if (dialogCallbacks.onError) {
|
|
351
327
|
dialogCallbacks.onError({ code: data.code, message: data.message });
|
|
@@ -486,8 +462,6 @@
|
|
|
486
462
|
if (dialogOverlay || dialogIframe) teardownDialog();
|
|
487
463
|
|
|
488
464
|
dialogContactData = data;
|
|
489
|
-
dialogCallbacks.onCallStarted = data.onCallStarted || null;
|
|
490
|
-
dialogCallbacks.onCallEnded = data.onCallEnded || null;
|
|
491
465
|
dialogCallbacks.onClose = data.onClose || null;
|
|
492
466
|
dialogCallbacks.onError = data.onError || null;
|
|
493
467
|
dialogMode = "dialog";
|
|
@@ -585,7 +559,7 @@
|
|
|
585
559
|
// Tear down any existing mount (re-mount)
|
|
586
560
|
if (mountIframe) teardownMount();
|
|
587
561
|
|
|
588
|
-
mountCallbacks = {
|
|
562
|
+
mountCallbacks = { onClose: null, onError: null };
|
|
589
563
|
mountContainer = container;
|
|
590
564
|
|
|
591
565
|
// Listen for messages
|