@rehers/rehers-roleplay-sdk 2.4.2 → 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 +190 -237
- package/index.d.ts +0 -4
- package/package.json +23 -3
- package/react.d.ts +59 -0
- package/react.js +171 -0
- package/roleplay-sdk.js +5 -31
package/README.md
CHANGED
|
@@ -1,287 +1,240 @@
|
|
|
1
|
-
# Roleplay SDK for
|
|
1
|
+
# Roleplay SDK for Seamless.AI
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
11
|
-
|
|
12
|
-
## Load the SDK
|
|
13
|
-
|
|
14
|
-
Seamless can load the SDK in any of these three ways.
|
|
15
|
-
|
|
16
|
-
### Option 1: Downloaded SDK file
|
|
17
|
-
|
|
18
|
-
If Seamless is hosting the SDK file directly inside the dashboard:
|
|
19
|
-
|
|
20
|
-
```html
|
|
21
|
-
<script src="/path/to/roleplay-sdk.js"></script>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Option 2: npm package
|
|
25
|
-
|
|
26
|
-
If Seamless is bundling the SDK through the frontend codebase:
|
|
3
|
+
## Install
|
|
27
4
|
|
|
28
5
|
```bash
|
|
29
6
|
npm install @rehers/rehers-roleplay-sdk
|
|
30
7
|
```
|
|
31
8
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Wrap your app with the Provider
|
|
12
|
+
|
|
13
|
+
Add this once, above all your routes. It initializes the SDK for the logged-in Seamless user.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { SeamlessRoleplayProvider } from "@rehers/rehers-roleplay-sdk/react";
|
|
17
|
+
|
|
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
|
+
|
|
23
|
+
return (
|
|
24
|
+
<SeamlessRoleplayProvider
|
|
25
|
+
publishableKey="pk_live_..."
|
|
26
|
+
userId={String(me.id)}
|
|
27
|
+
userEmail={me.username}
|
|
28
|
+
userRole={me.orgRole}
|
|
29
|
+
onReady={() => console.log("Roleplay SDK ready")}
|
|
30
|
+
onError={(err) => console.error("Roleplay SDK error", err)}
|
|
31
|
+
>
|
|
32
|
+
<Routes>
|
|
33
|
+
<Route path="/roleplay" element={<RoleplayPage />} />
|
|
34
|
+
<Route path="/contacts" element={<ContactSearch />} />
|
|
35
|
+
{/* ...your other routes */}
|
|
36
|
+
</Routes>
|
|
37
|
+
</SeamlessRoleplayProvider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
42
40
|
```
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
If you fetch the user via API:
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```js
|
|
49
|
-
const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
|
|
50
|
-
method: "GET",
|
|
44
|
+
```ts
|
|
45
|
+
const res = await fetch("https://api.seamless.ai/api/users/me", {
|
|
51
46
|
credentials: "include",
|
|
52
|
-
|
|
53
|
-
accept: "application/json, text/plain, */*",
|
|
54
|
-
},
|
|
55
|
-
}).then((res) => res.json());
|
|
56
|
-
```
|
|
47
|
+
}).then((r) => r.json());
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
62
53
|
```
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
These are the values Seamless must pass into `SeamlessRoleplay.init(...)`:
|
|
67
|
-
|
|
68
|
-
| SDK field | Seamless `/api/users/me` field |
|
|
69
|
-
|---|---|
|
|
70
|
-
| `userId` | `String(me.id)` |
|
|
71
|
-
| `userEmail` | `me.username` |
|
|
72
|
-
| `userRole` | Optional. Suggested mapping from `me.orgRole` / `me.isOrgAdmin` |
|
|
73
|
-
|
|
74
|
-
Example:
|
|
75
|
-
|
|
76
|
-
```js
|
|
77
|
-
const seamlessUserId = String(me.id);
|
|
78
|
-
const seamlessUserEmail = me.username;
|
|
79
|
-
const seamlessUserRole =
|
|
80
|
-
me.orgRole === "owner" ? "owner" :
|
|
81
|
-
me.isOrgAdmin ? "admin" :
|
|
82
|
-
"member";
|
|
83
|
-
```
|
|
55
|
+
That's the only setup. Everything below just works.
|
|
84
56
|
|
|
85
|
-
|
|
57
|
+
---
|
|
86
58
|
|
|
87
|
-
|
|
88
|
-
const seamlessUserId = String(me.id);
|
|
89
|
-
const seamlessUserEmail = me.username;
|
|
90
|
-
```
|
|
59
|
+
## 2. Full Roleplay page
|
|
91
60
|
|
|
92
|
-
|
|
61
|
+
For the dedicated Roleplay tab/page, drop in `RoleplayEmbed`. It fills its container.
|
|
93
62
|
|
|
94
|
-
|
|
63
|
+
```tsx
|
|
64
|
+
import { RoleplayEmbed } from "@rehers/rehers-roleplay-sdk/react";
|
|
95
65
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (roleplayReadyPromise) return roleplayReadyPromise;
|
|
101
|
-
|
|
102
|
-
roleplayReadyPromise = (async () => {
|
|
103
|
-
const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
|
|
104
|
-
method: "GET",
|
|
105
|
-
credentials: "include",
|
|
106
|
-
headers: {
|
|
107
|
-
accept: "application/json, text/plain, */*",
|
|
108
|
-
},
|
|
109
|
-
}).then((res) => res.json());
|
|
110
|
-
|
|
111
|
-
const me = meResponse.data ?? meResponse;
|
|
112
|
-
|
|
113
|
-
const seamlessUserId = String(me.id);
|
|
114
|
-
const seamlessUserEmail = me.username;
|
|
115
|
-
const seamlessUserRole =
|
|
116
|
-
me.orgRole === "owner" ? "owner" :
|
|
117
|
-
me.isOrgAdmin ? "admin" :
|
|
118
|
-
"member";
|
|
119
|
-
|
|
120
|
-
await new Promise((resolve, reject) => {
|
|
121
|
-
SeamlessRoleplay.init({
|
|
122
|
-
publishableKey: ROLEPLAY_PUBLISHABLE_KEY,
|
|
123
|
-
userId: seamlessUserId,
|
|
124
|
-
userEmail: seamlessUserEmail,
|
|
125
|
-
userRole: seamlessUserRole,
|
|
126
|
-
onReady: resolve,
|
|
127
|
-
onError: reject,
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
})();
|
|
131
|
-
|
|
132
|
-
return roleplayReadyPromise;
|
|
66
|
+
function RoleplayPage() {
|
|
67
|
+
return (
|
|
68
|
+
<RoleplayEmbed style={{ width: "100%", height: "100%" }} />
|
|
69
|
+
);
|
|
133
70
|
}
|
|
134
71
|
```
|
|
135
72
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. Roleplay dialog on Contact Search
|
|
80
|
+
|
|
81
|
+
When a user clicks "Roleplay" on a contact row, a dialog opens over the page.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useState } from "react";
|
|
85
|
+
import { RoleplayDialog } from "@rehers/rehers-roleplay-sdk/react";
|
|
86
|
+
|
|
87
|
+
function ContactSearch() {
|
|
88
|
+
const [activeContact, setActiveContact] = useState(null);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
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
|
+
<RoleplayDialog
|
|
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)}
|
|
119
|
+
/>
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
150
122
|
}
|
|
151
123
|
```
|
|
152
124
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
---
|
|
128
|
+
|
|
129
|
+
## 4. Add to Scenario (bulk)
|
|
130
|
+
|
|
131
|
+
When users select multiple contacts and click "Add to Scenario", a scenario picker dialog opens.
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
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);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<>
|
|
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>
|
|
151
|
+
|
|
152
|
+
<AddToScenarioDialog
|
|
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,
|
|
160
|
+
}))}
|
|
161
|
+
onComplete={(data) => {
|
|
162
|
+
console.log(`Added ${data.addedCount} to ${data.scenarioName}`);
|
|
163
|
+
setShowScenario(false);
|
|
164
|
+
setSelectedContacts([]);
|
|
165
|
+
}}
|
|
166
|
+
onClose={() => setShowScenario(false)}
|
|
167
|
+
onError={(err) => console.error("Error", err)}
|
|
168
|
+
/>
|
|
169
|
+
</>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
157
172
|
```
|
|
158
173
|
|
|
159
|
-
|
|
174
|
+
Accepts 1 to 25 contacts. Each contact needs `name`, `company`, `title`, and `domain`.
|
|
160
175
|
|
|
161
|
-
|
|
176
|
+
---
|
|
162
177
|
|
|
163
|
-
|
|
164
|
-
async function openRoleplayForContact(contact) {
|
|
165
|
-
await ensureRoleplaySdkReady();
|
|
166
|
-
|
|
167
|
-
SeamlessRoleplay.open({
|
|
168
|
-
name: contact.name,
|
|
169
|
-
domain: contact.domain,
|
|
170
|
-
company: contact.company,
|
|
171
|
-
title: contact.title,
|
|
172
|
-
liUrl: contact.linkedinUrl,
|
|
173
|
-
companyDescription: contact.companyDescription,
|
|
174
|
-
onCallStarted(data) {
|
|
175
|
-
console.log("Roleplay call started", data.callId);
|
|
176
|
-
},
|
|
177
|
-
onCallEnded(data) {
|
|
178
|
-
console.log("Roleplay call ended", data.callId, data.duration);
|
|
179
|
-
},
|
|
180
|
-
onClose() {
|
|
181
|
-
console.log("Roleplay dialog closed");
|
|
182
|
-
},
|
|
183
|
-
onError(err) {
|
|
184
|
-
console.error("Roleplay dialog error", err);
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
```
|
|
178
|
+
## Contact field mapping
|
|
189
179
|
|
|
190
|
-
|
|
180
|
+
When passing contact data to `RoleplayDialog` or `AddToScenarioDialog`:
|
|
191
181
|
|
|
192
|
-
| SDK
|
|
182
|
+
| SDK prop | Your contact field |
|
|
193
183
|
|---|---|
|
|
194
|
-
| `name` |
|
|
195
|
-
| `domain` | Company domain |
|
|
184
|
+
| `name` | Full name |
|
|
185
|
+
| `domain` | Company domain (e.g. `"stripe.com"`) |
|
|
196
186
|
| `company` | Company name |
|
|
197
|
-
| `title` |
|
|
198
|
-
| `liUrl` | LinkedIn
|
|
199
|
-
| `companyDescription` | Company description
|
|
200
|
-
|
|
201
|
-
## Optional: add contacts to a scenario
|
|
202
|
-
|
|
203
|
-
If Seamless wants to send multiple contacts into a scenario picker dialog, use `addToScenario(...)`.
|
|
204
|
-
|
|
205
|
-
```js
|
|
206
|
-
async function addContactsToScenario(contacts) {
|
|
207
|
-
await ensureRoleplaySdkReady();
|
|
208
|
-
|
|
209
|
-
SeamlessRoleplay.addToScenario({
|
|
210
|
-
contacts: contacts.map((contact) => ({
|
|
211
|
-
name: contact.name,
|
|
212
|
-
company: contact.company,
|
|
213
|
-
title: contact.title,
|
|
214
|
-
domain: contact.domain,
|
|
215
|
-
liUrl: contact.linkedinUrl,
|
|
216
|
-
companyDescription: contact.companyDescription,
|
|
217
|
-
})),
|
|
218
|
-
onComplete(data) {
|
|
219
|
-
console.log("Scenario import complete", data);
|
|
220
|
-
},
|
|
221
|
-
onClose() {
|
|
222
|
-
console.log("Add to scenario dialog closed");
|
|
223
|
-
},
|
|
224
|
-
onError(err) {
|
|
225
|
-
console.error("Add to scenario error", err);
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Minimal API surface
|
|
232
|
-
|
|
233
|
-
### `SeamlessRoleplay.init(options)`
|
|
234
|
-
|
|
235
|
-
Initializes the SDK with the logged-in Seamless user.
|
|
236
|
-
|
|
237
|
-
```js
|
|
238
|
-
SeamlessRoleplay.init({
|
|
239
|
-
publishableKey,
|
|
240
|
-
userId,
|
|
241
|
-
userEmail,
|
|
242
|
-
userRole,
|
|
243
|
-
onReady,
|
|
244
|
-
onError,
|
|
245
|
-
});
|
|
246
|
-
```
|
|
187
|
+
| `title` | Job title |
|
|
188
|
+
| `liUrl` | LinkedIn URL (optional) |
|
|
189
|
+
| `companyDescription` | Company description (optional) |
|
|
247
190
|
|
|
248
|
-
|
|
191
|
+
---
|
|
249
192
|
|
|
250
|
-
|
|
193
|
+
## That's it
|
|
251
194
|
|
|
252
|
-
|
|
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
|
|
253
199
|
|
|
254
|
-
|
|
200
|
+
All three clean up after themselves. No manual `destroy()`, no `useEffect`, no refs.
|
|
255
201
|
|
|
256
|
-
|
|
202
|
+
TypeScript types are included — your editor will autocomplete all props.
|
|
257
203
|
|
|
258
|
-
|
|
204
|
+
---
|
|
259
205
|
|
|
260
|
-
|
|
206
|
+
## Trial and upgrade handling
|
|
261
207
|
|
|
262
|
-
|
|
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.
|
|
263
209
|
|
|
264
|
-
|
|
210
|
+
---
|
|
265
211
|
|
|
266
|
-
|
|
212
|
+
## Advanced: Vanilla JavaScript
|
|
267
213
|
|
|
268
|
-
|
|
214
|
+
If you need the vanilla SDK without React:
|
|
269
215
|
|
|
270
|
-
|
|
216
|
+
```js
|
|
217
|
+
import "@rehers/rehers-roleplay-sdk";
|
|
271
218
|
|
|
272
|
-
|
|
219
|
+
// Initialize once
|
|
220
|
+
SeamlessRoleplay.init({
|
|
221
|
+
publishableKey: "pk_live_...",
|
|
222
|
+
userId: "...",
|
|
223
|
+
userEmail: "...",
|
|
224
|
+
onReady() { console.log("ready"); },
|
|
225
|
+
});
|
|
273
226
|
|
|
274
|
-
|
|
227
|
+
// Full page embed
|
|
228
|
+
SeamlessRoleplay.mount(document.getElementById("container"));
|
|
275
229
|
|
|
276
|
-
|
|
230
|
+
// Contact dialog
|
|
231
|
+
SeamlessRoleplay.open({ name: "...", domain: "...", company: "...", title: "..." });
|
|
277
232
|
|
|
278
|
-
|
|
233
|
+
// Bulk import
|
|
234
|
+
SeamlessRoleplay.addToScenario({ contacts: [...], onComplete(data) { } });
|
|
279
235
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
SeamlessRoleplayOpenData,
|
|
285
|
-
AddToScenarioOptions,
|
|
286
|
-
} from "@rehers/rehers-roleplay-sdk";
|
|
236
|
+
// Cleanup
|
|
237
|
+
SeamlessRoleplay.close(); // close dialog
|
|
238
|
+
SeamlessRoleplay.unmount(); // remove embed
|
|
239
|
+
SeamlessRoleplay.destroy(); // full cleanup
|
|
287
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
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehers/rehers-roleplay-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
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,59 @@
|
|
|
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
|
+
onClose?: () => void;
|
|
38
|
+
onError?: (data: {
|
|
39
|
+
code: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}) => void;
|
|
42
|
+
}
|
|
43
|
+
export declare function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onClose, onError, }: RoleplayDialogProps): null;
|
|
44
|
+
export interface RoleplayEmbedProps {
|
|
45
|
+
className?: string;
|
|
46
|
+
style?: React.CSSProperties;
|
|
47
|
+
}
|
|
48
|
+
export declare function RoleplayEmbed({ className, style }: RoleplayEmbedProps): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
export interface AddToScenarioDialogProps {
|
|
50
|
+
open: boolean;
|
|
51
|
+
contacts: AddToScenarioContact[];
|
|
52
|
+
onComplete?: (data: AddToScenarioCompleteData) => void;
|
|
53
|
+
onClose?: () => void;
|
|
54
|
+
onError?: (error: {
|
|
55
|
+
code: string;
|
|
56
|
+
message: string;
|
|
57
|
+
}) => void;
|
|
58
|
+
}
|
|
59
|
+
export declare function AddToScenarioDialog({ open: isOpen, contacts, onComplete, onClose, onError, }: AddToScenarioDialogProps): null;
|
package/react.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
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, onClose, onError, }) {
|
|
79
|
+
const { isReady, sdk } = useSeamlessRoleplay();
|
|
80
|
+
const onCloseRef = useCallbackRef(onClose);
|
|
81
|
+
const onErrorRef = useCallbackRef(onError);
|
|
82
|
+
const isOpenRef = useRef(false);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!isReady)
|
|
85
|
+
return;
|
|
86
|
+
if (isOpen) {
|
|
87
|
+
isOpenRef.current = true;
|
|
88
|
+
sdk.open({
|
|
89
|
+
name,
|
|
90
|
+
domain,
|
|
91
|
+
company,
|
|
92
|
+
title,
|
|
93
|
+
companyDescription,
|
|
94
|
+
liUrl,
|
|
95
|
+
onClose: () => {
|
|
96
|
+
var _a;
|
|
97
|
+
isOpenRef.current = false;
|
|
98
|
+
(_a = onCloseRef.current) === null || _a === void 0 ? void 0 : _a.call(onCloseRef);
|
|
99
|
+
},
|
|
100
|
+
onError: (data) => { var _a; return (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, data); },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else if (isOpenRef.current) {
|
|
104
|
+
isOpenRef.current = false;
|
|
105
|
+
sdk.close();
|
|
106
|
+
}
|
|
107
|
+
return () => {
|
|
108
|
+
if (isOpenRef.current) {
|
|
109
|
+
isOpenRef.current = false;
|
|
110
|
+
sdk.close();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}, [isReady, isOpen, name, domain, company, title, companyDescription, liUrl, sdk]);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
export function RoleplayEmbed({ className, style }) {
|
|
117
|
+
const { isReady, sdk } = useSeamlessRoleplay();
|
|
118
|
+
const containerRef = useRef(null);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!isReady || !containerRef.current)
|
|
121
|
+
return;
|
|
122
|
+
sdk.mount(containerRef.current);
|
|
123
|
+
return () => {
|
|
124
|
+
sdk.unmount();
|
|
125
|
+
};
|
|
126
|
+
}, [isReady, sdk]);
|
|
127
|
+
return _jsx("div", { ref: containerRef, className: className, style: style });
|
|
128
|
+
}
|
|
129
|
+
export function AddToScenarioDialog({ open: isOpen, contacts, onComplete, onClose, onError, }) {
|
|
130
|
+
const { isReady, sdk } = useSeamlessRoleplay();
|
|
131
|
+
const onCompleteRef = useCallbackRef(onComplete);
|
|
132
|
+
const onCloseRef = useCallbackRef(onClose);
|
|
133
|
+
const onErrorRef = useCallbackRef(onError);
|
|
134
|
+
const isOpenRef = useRef(false);
|
|
135
|
+
const contactsRef = useRef(contacts);
|
|
136
|
+
useLayoutEffect(() => {
|
|
137
|
+
contactsRef.current = contacts;
|
|
138
|
+
});
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!isReady)
|
|
141
|
+
return;
|
|
142
|
+
if (isOpen) {
|
|
143
|
+
isOpenRef.current = true;
|
|
144
|
+
sdk.addToScenario({
|
|
145
|
+
contacts: contactsRef.current,
|
|
146
|
+
onComplete: (data) => {
|
|
147
|
+
var _a;
|
|
148
|
+
isOpenRef.current = false;
|
|
149
|
+
(_a = onCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onCompleteRef, data);
|
|
150
|
+
},
|
|
151
|
+
onClose: () => {
|
|
152
|
+
var _a;
|
|
153
|
+
isOpenRef.current = false;
|
|
154
|
+
(_a = onCloseRef.current) === null || _a === void 0 ? void 0 : _a.call(onCloseRef);
|
|
155
|
+
},
|
|
156
|
+
onError: (err) => { var _a; return (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err); },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else if (isOpenRef.current) {
|
|
160
|
+
isOpenRef.current = false;
|
|
161
|
+
sdk.close();
|
|
162
|
+
}
|
|
163
|
+
return () => {
|
|
164
|
+
if (isOpenRef.current) {
|
|
165
|
+
isOpenRef.current = false;
|
|
166
|
+
sdk.close();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}, [isReady, isOpen, sdk]);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
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
|