@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 CHANGED
@@ -1,287 +1,240 @@
1
- # Roleplay SDK for the Seamless.AI Dashboard
1
+ # Roleplay SDK for Seamless.AI
2
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.
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
- ```js
33
- import SeamlessRoleplay from "@rehers/rehers-roleplay-sdk";
34
- ```
35
-
36
- ### Option 3: CDN script
37
-
38
- If Seamless wants to load the SDK without bundling it:
39
-
40
- ```html
41
- <script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
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
- ## Get the logged-in Seamless user
42
+ If you fetch the user via API:
45
43
 
46
- Use the existing Seamless dashboard session and call:
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
- headers: {
53
- accept: "application/json, text/plain, */*",
54
- },
55
- }).then((res) => res.json());
56
- ```
47
+ }).then((r) => r.json());
57
48
 
58
- Normalize the response before reading fields:
59
-
60
- ```js
61
- const me = meResponse.data ?? meResponse;
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
- ## Required mapping for `init()`
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
- For the response shape currently returned by Seamless, this means:
57
+ ---
86
58
 
87
- ```js
88
- const seamlessUserId = String(me.id);
89
- const seamlessUserEmail = me.username;
90
- ```
59
+ ## 2. Full Roleplay page
91
60
 
92
- ## Initialize the SDK once
61
+ For the dedicated Roleplay tab/page, drop in `RoleplayEmbed`. It fills its container.
93
62
 
94
- 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.
63
+ ```tsx
64
+ import { RoleplayEmbed } from "@rehers/rehers-roleplay-sdk/react";
95
65
 
96
- ```js
97
- let roleplayReadyPromise;
98
-
99
- function ensureRoleplaySdkReady() {
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
- ## Embed in the full Roleplay page
137
-
138
- Use `mount(container)` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
139
-
140
- ```html
141
- <div id="roleplay-root" style="width: 100%; height: 100%;"></div>
142
- ```
143
-
144
- ```js
145
- async function mountRoleplayPage() {
146
- await ensureRoleplaySdkReady();
147
-
148
- const container = document.getElementById("roleplay-root");
149
- SeamlessRoleplay.mount(container);
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
- If the Seamless page or tab is torn down, unmount the SDK:
154
-
155
- ```js
156
- SeamlessRoleplay.unmount();
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
- ## Embed in Contact Search
174
+ Accepts 1 to 25 contacts. Each contact needs `name`, `company`, `title`, and `domain`.
160
175
 
161
- Use `open(contactData)` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
176
+ ---
162
177
 
163
- ```js
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
- The contact object passed to `open(...)` should map to:
180
+ When passing contact data to `RoleplayDialog` or `AddToScenarioDialog`:
191
181
 
192
- | SDK field | Contact Search value |
182
+ | SDK prop | Your contact field |
193
183
  |---|---|
194
- | `name` | Contact full name |
195
- | `domain` | Company domain |
184
+ | `name` | Full name |
185
+ | `domain` | Company domain (e.g. `"stripe.com"`) |
196
186
  | `company` | Company name |
197
- | `title` | Contact title |
198
- | `liUrl` | LinkedIn profile URL, if available |
199
- | `companyDescription` | Company description, if available |
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
- ### `SeamlessRoleplay.mount(container)`
191
+ ---
249
192
 
250
- Mounts the full Roleplay app into a dashboard container.
193
+ ## That's it
251
194
 
252
- ### `SeamlessRoleplay.open(contactData)`
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
- Opens the Roleplay dialog for a single contact.
200
+ All three clean up after themselves. No manual `destroy()`, no `useEffect`, no refs.
255
201
 
256
- ### `SeamlessRoleplay.addToScenario(options)`
202
+ TypeScript types are included — your editor will autocomplete all props.
257
203
 
258
- Opens the bulk add-to-scenario dialog for 1 to 25 contacts.
204
+ ---
259
205
 
260
- ### `SeamlessRoleplay.close()`
206
+ ## Trial and upgrade handling
261
207
 
262
- Closes the active dialog.
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
- ### `SeamlessRoleplay.unmount()`
210
+ ---
265
211
 
266
- Unmounts the full-page Roleplay embed.
212
+ ## Advanced: Vanilla JavaScript
267
213
 
268
- ### `SeamlessRoleplay.destroy()`
214
+ If you need the vanilla SDK without React:
269
215
 
270
- Destroys the SDK state, timers, mount, and dialogs.
216
+ ```js
217
+ import "@rehers/rehers-roleplay-sdk";
271
218
 
272
- ## Trial and upgrade handling
219
+ // Initialize once
220
+ SeamlessRoleplay.init({
221
+ publishableKey: "pk_live_...",
222
+ userId: "...",
223
+ userEmail: "...",
224
+ onReady() { console.log("ready"); },
225
+ });
273
226
 
274
- 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.
227
+ // Full page embed
228
+ SeamlessRoleplay.mount(document.getElementById("container"));
275
229
 
276
- ## TypeScript
230
+ // Contact dialog
231
+ SeamlessRoleplay.open({ name: "...", domain: "...", company: "...", title: "..." });
277
232
 
278
- Type declarations are included with the SDK package.
233
+ // Bulk import
234
+ SeamlessRoleplay.addToScenario({ contacts: [...], onComplete(data) { } });
279
235
 
280
- ```ts
281
- import type {
282
- SeamlessRoleplaySDK,
283
- SeamlessRoleplayInitOptions,
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.4.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
- "keywords": ["seamless", "roleplay", "sdk", "sales", "training"],
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 = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
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 = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
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 = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
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 = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
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 = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
562
+ mountCallbacks = { onClose: null, onError: null };
589
563
  mountContainer = container;
590
564
 
591
565
  // Listen for messages