@rehers/rehers-roleplay-sdk 2.4.0 → 2.4.2

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,177 +1,287 @@
1
- # @rehers/rehers-roleplay-sdk
1
+ # Roleplay SDK for the Seamless.AI Dashboard
2
2
 
3
- Lightweight vanilla JS SDK for embedding roleplay call sessions and bulk contact import. Opens a modal (dialog mode), mounts into a container (mount mode), or launches a scenario picker (add-to-scenario mode) — no framework dependencies.
3
+ This SDK is only for embedding Roleplay inside the Seamless.AI dashboard.
4
4
 
5
- ## Install
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:
6
27
 
7
28
  ```bash
8
29
  npm install @rehers/rehers-roleplay-sdk
9
30
  ```
10
31
 
11
- Or load via CDN:
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:
12
39
 
13
40
  ```html
14
41
  <script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
15
42
  ```
16
43
 
17
- ## Usage
44
+ ## Get the logged-in Seamless user
45
+
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",
51
+ credentials: "include",
52
+ headers: {
53
+ accept: "application/json, text/plain, */*",
54
+ },
55
+ }).then((res) => res.json());
56
+ ```
57
+
58
+ Normalize the response before reading fields:
59
+
60
+ ```js
61
+ const me = meResponse.data ?? meResponse;
62
+ ```
63
+
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
+ ```
84
+
85
+ For the response shape currently returned by Seamless, this means:
86
+
87
+ ```js
88
+ const seamlessUserId = String(me.id);
89
+ const seamlessUserEmail = me.username;
90
+ ```
91
+
92
+ ## Initialize the SDK once
93
+
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.
95
+
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;
133
+ }
134
+ ```
135
+
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.
18
139
 
19
140
  ```html
20
- <script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
21
- <script>
22
- // 1. Initialize with a publishable key
23
- SeamlessRoleplay.init({
24
- publishableKey: 'pk_live_abc123',
25
- userId: 'user_789',
26
- userEmail: 'john@example.com',
27
- userRole: 'member', // optional "owner", "admin", or "member"
28
- onReady: function() {
29
- console.log('SDK ready');
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);
150
+ }
151
+ ```
152
+
153
+ If the Seamless page or tab is torn down, unmount the SDK:
154
+
155
+ ```js
156
+ SeamlessRoleplay.unmount();
157
+ ```
158
+
159
+ ## Embed in Contact Search
160
+
161
+ Use `open(contactData)` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
162
+
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);
30
179
  },
31
- onError: function(err) {
32
- console.error('SDK error:', err.code, err.message);
180
+ onClose() {
181
+ console.log("Roleplay dialog closed");
182
+ },
183
+ onError(err) {
184
+ console.error("Roleplay dialog error", err);
33
185
  },
34
186
  });
187
+ }
188
+ ```
35
189
 
36
- // 2. Open the roleplay modal for a contact (dialog mode)
37
- SeamlessRoleplay.open({
38
- name: 'Jane Smith',
39
- domain: 'acme.com',
40
- company: 'Acme Corp',
41
- title: 'VP of Sales',
42
- companyDescription: 'Enterprise software company', // optional
43
- liUrl: 'https://linkedin.com/in/jane-smith', // optional
44
- onCallStarted: function(data) { console.log('Call started:', data.callId); },
45
- onCallEnded: function(data) { console.log('Call ended:', data.callId, data.duration); },
46
- onClose: function() { console.log('Dialog closed'); },
47
- onError: function(data) { console.error('Error:', data.code, data.message); },
48
- });
190
+ The contact object passed to `open(...)` should map to:
49
191
 
50
- // 2b. Or mount the full app into a container (full-page embed)
51
- SeamlessRoleplay.mount(document.getElementById('roleplay-container'));
192
+ | SDK field | Contact Search value |
193
+ |---|---|
194
+ | `name` | Contact full name |
195
+ | `domain` | Company domain |
196
+ | `company` | Company name |
197
+ | `title` | Contact title |
198
+ | `liUrl` | LinkedIn profile URL, if available |
199
+ | `companyDescription` | Company description, if available |
52
200
 
53
- // 2c. Or mount a call for a specific contact into a container
54
- SeamlessRoleplay.mount(document.getElementById('roleplay-container'), {
55
- name: 'Jane Smith',
56
- domain: 'acme.com',
57
- company: 'Acme Corp',
58
- title: 'VP of Sales',
59
- });
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();
60
208
 
61
- // 3. Add contacts to a scenario in bulk
62
209
  SeamlessRoleplay.addToScenario({
63
- contacts: [
64
- { name: 'Sarah Chen', company: 'Stripe', title: 'VP of Sales', domain: 'stripe.com', liUrl: 'https://linkedin.com/in/sarachen' },
65
- { name: 'Mike Ross', company: 'Acme', title: 'CRO', domain: 'acme.com', liUrl: 'https://linkedin.com/in/mikeross' },
66
- ],
67
- onComplete: function(data) {
68
- console.log('Added', data.addedCount, 'contacts to', data.scenarioName);
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);
69
226
  },
70
- onClose: function() { console.log('Dialog closed'); },
71
- onError: function(err) { console.error('Error:', err.code, err.message); },
72
227
  });
73
-
74
- // Close and destroy
75
- SeamlessRoleplay.close();
76
- SeamlessRoleplay.destroy();
77
- </script>
228
+ }
78
229
  ```
79
230
 
80
- ## Trial Mode
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
+ ```
81
247
 
82
- When the backend returns `USER_NOT_FOUND`, it includes a `paymentLink` in the response. The SDK automatically captures it and renders a trial screen directing the user to sign up — no client-side configuration needed.
248
+ ### `SeamlessRoleplay.mount(container)`
83
249
 
84
- ## API
250
+ Mounts the full Roleplay app into a dashboard container.
85
251
 
86
- ### `SeamlessRoleplay.init(options)`
252
+ ### `SeamlessRoleplay.open(contactData)`
87
253
 
88
- | Option | Type | Required | Description |
89
- |---|---|---|---|
90
- | `publishableKey` | `string` | Yes | Publishable API key |
91
- | `userId` | `string` | Yes | Your user's unique identifier |
92
- | `userEmail` | `string` | Yes | User email for secure account matching |
93
- | `userRole` | `string` | No | User role — `"owner"`, `"admin"`, or `"member"` |
94
- | `userToken` | `string` | No | Signed JWT for identity verification |
95
- | `origin` | `string` | No | Override app origin (dev only) |
96
- | `onReady` | `function` | No | Called when session is ready |
97
- | `onError` | `function` | No | Called on init error `({ code, message })` |
98
-
99
- ### `SeamlessRoleplay.open(data)`
100
-
101
- Opens the roleplay in a modal dialog overlay.
102
-
103
- | Field | Type | Required | Description |
104
- |---|---|---|---|
105
- | `name` | `string` | Yes | Full name of the contact |
106
- | `domain` | `string` | Yes | Company domain (e.g. "stripe.com") |
107
- | `company` | `string` | Yes | Company name |
108
- | `title` | `string` | Yes | Job title |
109
- | `companyDescription` | `string` | No | Brief company description |
110
- | `liUrl` | `string` | No | LinkedIn profile URL |
111
- | `onCallStarted` | `function` | No | `({ callId })` |
112
- | `onCallEnded` | `function` | No | `({ callId, duration? })` |
113
- | `onClose` | `function` | No | Called when dialog closes |
114
- | `onError` | `function` | No | `({ code, message })` |
115
-
116
- ### `SeamlessRoleplay.mount(container, data?)`
117
-
118
- Mounts into a DOM element. Two modes:
119
-
120
- - **`mount(container)`** — embeds the full Roleplay app (dashboard, scenarios, call logs)
121
- - **`mount(container, data)`** — embeds a roleplay call for a specific contact (same `data` options as `open()`)
254
+ Opens the Roleplay dialog for a single contact.
122
255
 
123
256
  ### `SeamlessRoleplay.addToScenario(options)`
124
257
 
125
- Opens a compact dialog for selecting a scenario and importing contacts in bulk.
126
-
127
- | Field | Type | Required | Description |
128
- |---|---|---|---|
129
- | `contacts` | `array` | Yes | 1–25 contacts to import |
130
- | `contacts[].name` | `string` | Yes | Full name |
131
- | `contacts[].company` | `string` | Yes | Company name |
132
- | `contacts[].title` | `string` | Yes | Job title |
133
- | `contacts[].domain` | `string` | Yes | Company domain |
134
- | `contacts[].liUrl` | `string` | No | LinkedIn profile URL |
135
- | `contacts[].companyDescription` | `string` | No | Brief company description |
136
- | `onComplete` | `function` | No | `({ scenarioId, scenarioName, addedCount, skippedCount })` |
137
- | `onClose` | `function` | No | Called when dialog closes |
138
- | `onError` | `function` | No | `({ code, message })` |
258
+ Opens the bulk add-to-scenario dialog for 1 to 25 contacts.
139
259
 
140
260
  ### `SeamlessRoleplay.close()`
141
261
 
142
- Closes the active dialog or unmounts.
143
-
144
- ### `SeamlessRoleplay.destroy()`
262
+ Closes the active dialog.
145
263
 
146
- Tears down everything — clears tokens, timers, and DOM elements.
264
+ ### `SeamlessRoleplay.unmount()`
147
265
 
148
- ## Auth Flow
266
+ Unmounts the full-page Roleplay embed.
149
267
 
150
- ```
151
- SDK Backend
152
- ───────────────── ─────────────────
153
- POST /api/sdk/session
154
- X-Publishable-Key: pk_live_abc123
155
- Body: { userId, userEmail, userRole? }
156
- 1. Validate key + origin
157
- 2. Validate userId + userEmail combo
158
- 3. Find/create user
159
- 4. Mint JWT (1hr TTL)
160
- ←──── { sessionToken, expiresIn }
161
- OR { error: "USER_NOT_FOUND" }
268
+ ### `SeamlessRoleplay.destroy()`
162
269
 
163
- Token stored in memory (not localStorage)
164
- Auto-refreshes at 80% of TTL
165
- ```
270
+ Destroys the SDK state, timers, mount, and dialogs.
166
271
 
167
- ## Error Handling
272
+ ## Trial and upgrade handling
168
273
 
169
- All public methods are wrapped in try/catch — the SDK will never throw an uncaught exception that could crash the host page. Errors are logged to `console.error` with a `[SeamlessRoleplay]` prefix and routed to the relevant `onError` callback when provided.
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.
170
275
 
171
276
  ## TypeScript
172
277
 
173
- Full type declarations are included:
278
+ Type declarations are included with the SDK package.
174
279
 
175
280
  ```ts
176
- import type { SeamlessRoleplaySDK, SeamlessRoleplayOpenData, AddToScenarioOptions } from '@rehers/rehers-roleplay-sdk';
281
+ import type {
282
+ SeamlessRoleplaySDK,
283
+ SeamlessRoleplayInitOptions,
284
+ SeamlessRoleplayOpenData,
285
+ AddToScenarioOptions,
286
+ } from "@rehers/rehers-roleplay-sdk";
177
287
  ```
package/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export interface SeamlessRoleplayInitOptions {
2
2
  /** Publishable API key (starts with pk_live_ or pk_test_) */
3
3
  publishableKey: string;
4
- /** Your user's unique identifier */
4
+ /** Logged-in Seamless user ID. Pass String(me.id) from GET /api/users/me */
5
5
  userId: string;
6
- /** User email required for secure account matching */
6
+ /** Logged-in Seamless user email. Pass me.username from GET /api/users/me */
7
7
  userEmail: string;
8
8
  /** Optional user role for syncing permissions ("owner" | "admin" | "member") */
9
9
  userRole?: "owner" | "admin" | "member";
@@ -78,8 +78,8 @@ export interface SeamlessRoleplaySDK {
78
78
  init(options: SeamlessRoleplayInitOptions): void;
79
79
  /** Open the roleplay modal for a contact (dialog mode). */
80
80
  open(data: SeamlessRoleplayOpenData): void;
81
- /** Mount the full Roleplay app into a container (no data), or a call embed for a specific contact (with data). */
82
- mount(container: HTMLElement, data?: SeamlessRoleplayOpenData): void;
81
+ /** Mount the full Roleplay app into a container. */
82
+ mount(container: HTMLElement): void;
83
83
  /** Open the add-to-scenario dialog for bulk contact import. */
84
84
  addToScenario(options: AddToScenarioOptions): void;
85
85
  /** Close the active dialog. Does not affect mount. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehers/rehers-roleplay-sdk",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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",
package/roleplay-sdk.js CHANGED
@@ -35,7 +35,6 @@
35
35
  // ── Mount state (persistent embed — survives dialog open/close) ───
36
36
  var mountIframe = null;
37
37
  var mountContainer = null;
38
- var mountContactData = null;
39
38
  var mountCallbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
40
39
  var mountListener = null;
41
40
 
@@ -219,7 +218,6 @@
219
218
 
220
219
  mountIframe = null;
221
220
  mountContainer = null;
222
- mountContactData = null;
223
221
  mountCallbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
224
222
  }
225
223
 
@@ -279,10 +277,10 @@
279
277
  case "ROLEPLAY_READY":
280
278
  getSessionToken()
281
279
  .then(function (token) {
282
- dispatchInitToTarget(mountIframe, mountContactData, null);
280
+ dispatchInitToTarget(mountIframe, null, null);
283
281
  })
284
282
  .catch(function () {
285
- dispatchInitToTarget(mountIframe, mountContactData, null);
283
+ dispatchInitToTarget(mountIframe, null, null);
286
284
  });
287
285
  break;
288
286
 
@@ -571,13 +569,9 @@
571
569
  },
572
570
 
573
571
  /**
574
- * Mount the roleplay into a container element.
575
- *
576
- * Two modes:
577
- * mount(container) — full app embed (dashboard, scenarios, call logs, etc.)
578
- * mount(container, contactData) — roleplay call embed for a specific contact
572
+ * Mount the full Roleplay app into a container element.
579
573
  */
580
- mount: function (container, data) {
574
+ mount: function (container) {
581
575
  try {
582
576
  if (!initCalled) {
583
577
  logError("mount", "init() must be called first");
@@ -588,37 +582,22 @@
588
582
  return;
589
583
  }
590
584
 
591
- // If contact data is provided, validate required fields
592
- var hasContactData = data && data.name && data.domain && data.company && data.title;
593
- if (data && !hasContactData && (data.name || data.domain || data.company || data.title)) {
594
- logError("mount", "contact data requires { name, domain, company, title }");
595
- return;
596
- }
597
-
598
585
  // Tear down any existing mount (re-mount)
599
586
  if (mountIframe) teardownMount();
600
587
 
601
- mountContactData = hasContactData ? data : null;
602
- mountCallbacks.onCallStarted = (data && data.onCallStarted) || null;
603
- mountCallbacks.onCallEnded = (data && data.onCallEnded) || null;
604
- mountCallbacks.onClose = (data && data.onClose) || null;
605
- mountCallbacks.onError = (data && data.onError) || null;
588
+ mountCallbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
606
589
  mountContainer = container;
607
590
 
608
591
  // Listen for messages
609
592
  mountListener = handleMountMessage;
610
593
  window.addEventListener("message", mountListener);
611
594
 
612
- // No contact data → full app embed at root; with contact → /embed/roleplay-call
613
- var iframeEl = createIframe(hasContactData ? "/embed/roleplay-call" : "/");
595
+ var iframeEl = createIframe("/");
614
596
  mountIframe = iframeEl;
615
597
  container.appendChild(iframeEl);
616
598
  } catch (e) {
617
599
  logError("mount", e);
618
600
  teardownMount();
619
- if (data && typeof data.onError === "function") {
620
- try { data.onError({ code: "SDK_ERROR", message: e.message || "Unexpected error during mount" }); } catch (_) {}
621
- }
622
601
  }
623
602
  },
624
603