@rehers/rehers-roleplay-sdk 2.4.0 → 2.4.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,177 +1,269 @@
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
+ If Seamless is using the downloaded SDK file directly:
15
+
16
+ ```html
17
+ <script src="/path/to/roleplay-sdk.js"></script>
18
+ ```
19
+
20
+ If Seamless is using the npm package:
6
21
 
7
22
  ```bash
8
23
  npm install @rehers/rehers-roleplay-sdk
9
24
  ```
10
25
 
11
- Or load via CDN:
26
+ ## Get the logged-in Seamless user
12
27
 
13
- ```html
14
- <script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
28
+ Use the existing Seamless dashboard session and call:
29
+
30
+ ```js
31
+ const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
32
+ method: "GET",
33
+ credentials: "include",
34
+ headers: {
35
+ accept: "application/json, text/plain, */*",
36
+ },
37
+ }).then((res) => res.json());
15
38
  ```
16
39
 
17
- ## Usage
40
+ Normalize the response before reading fields:
41
+
42
+ ```js
43
+ const me = meResponse.data ?? meResponse;
44
+ ```
45
+
46
+ ## Required mapping for `init()`
47
+
48
+ These are the values Seamless must pass into `SeamlessRoleplay.init(...)`:
49
+
50
+ | SDK field | Seamless `/api/users/me` field |
51
+ |---|---|
52
+ | `userId` | `String(me.id)` |
53
+ | `userEmail` | `me.username` |
54
+ | `userRole` | Optional. Suggested mapping from `me.orgRole` / `me.isOrgAdmin` |
55
+
56
+ Example:
57
+
58
+ ```js
59
+ const seamlessUserId = String(me.id);
60
+ const seamlessUserEmail = me.username;
61
+ const seamlessUserRole =
62
+ me.orgRole === "owner" ? "owner" :
63
+ me.isOrgAdmin ? "admin" :
64
+ "member";
65
+ ```
66
+
67
+ For the response shape currently returned by Seamless, this means:
68
+
69
+ ```js
70
+ const seamlessUserId = String(me.id);
71
+ const seamlessUserEmail = me.username;
72
+ ```
73
+
74
+ ## Initialize the SDK once
75
+
76
+ Initialize the SDK once per page load using the logged-in Seamless user. Reuse that same initialized SDK for both the full-page embed and the Contact Search dialog.
77
+
78
+ ```js
79
+ let roleplayReadyPromise;
80
+
81
+ function ensureRoleplaySdkReady() {
82
+ if (roleplayReadyPromise) return roleplayReadyPromise;
83
+
84
+ roleplayReadyPromise = (async () => {
85
+ const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
86
+ method: "GET",
87
+ credentials: "include",
88
+ headers: {
89
+ accept: "application/json, text/plain, */*",
90
+ },
91
+ }).then((res) => res.json());
92
+
93
+ const me = meResponse.data ?? meResponse;
94
+
95
+ const seamlessUserId = String(me.id);
96
+ const seamlessUserEmail = me.username;
97
+ const seamlessUserRole =
98
+ me.orgRole === "owner" ? "owner" :
99
+ me.isOrgAdmin ? "admin" :
100
+ "member";
101
+
102
+ await new Promise((resolve, reject) => {
103
+ SeamlessRoleplay.init({
104
+ publishableKey: ROLEPLAY_PUBLISHABLE_KEY,
105
+ userId: seamlessUserId,
106
+ userEmail: seamlessUserEmail,
107
+ userRole: seamlessUserRole,
108
+ onReady: resolve,
109
+ onError: reject,
110
+ });
111
+ });
112
+ })();
113
+
114
+ return roleplayReadyPromise;
115
+ }
116
+ ```
117
+
118
+ ## Embed in the full Roleplay page
119
+
120
+ Use `mount(container)` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
18
121
 
19
122
  ```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');
123
+ <div id="roleplay-root" style="width: 100%; height: 100%;"></div>
124
+ ```
125
+
126
+ ```js
127
+ async function mountRoleplayPage() {
128
+ await ensureRoleplaySdkReady();
129
+
130
+ const container = document.getElementById("roleplay-root");
131
+ SeamlessRoleplay.mount(container);
132
+ }
133
+ ```
134
+
135
+ If the Seamless page or tab is torn down, unmount the SDK:
136
+
137
+ ```js
138
+ SeamlessRoleplay.unmount();
139
+ ```
140
+
141
+ ## Embed in Contact Search
142
+
143
+ Use `open(contactData)` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
144
+
145
+ ```js
146
+ async function openRoleplayForContact(contact) {
147
+ await ensureRoleplaySdkReady();
148
+
149
+ SeamlessRoleplay.open({
150
+ name: contact.name,
151
+ domain: contact.domain,
152
+ company: contact.company,
153
+ title: contact.title,
154
+ liUrl: contact.linkedinUrl,
155
+ companyDescription: contact.companyDescription,
156
+ onCallStarted(data) {
157
+ console.log("Roleplay call started", data.callId);
158
+ },
159
+ onCallEnded(data) {
160
+ console.log("Roleplay call ended", data.callId, data.duration);
30
161
  },
31
- onError: function(err) {
32
- console.error('SDK error:', err.code, err.message);
162
+ onClose() {
163
+ console.log("Roleplay dialog closed");
164
+ },
165
+ onError(err) {
166
+ console.error("Roleplay dialog error", err);
33
167
  },
34
168
  });
169
+ }
170
+ ```
35
171
 
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
- });
172
+ The contact object passed to `open(...)` should map to:
49
173
 
50
- // 2b. Or mount the full app into a container (full-page embed)
51
- SeamlessRoleplay.mount(document.getElementById('roleplay-container'));
174
+ | SDK field | Contact Search value |
175
+ |---|---|
176
+ | `name` | Contact full name |
177
+ | `domain` | Company domain |
178
+ | `company` | Company name |
179
+ | `title` | Contact title |
180
+ | `liUrl` | LinkedIn profile URL, if available |
181
+ | `companyDescription` | Company description, if available |
52
182
 
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
- });
183
+ ## Optional: add contacts to a scenario
184
+
185
+ If Seamless wants to send multiple contacts into a scenario picker dialog, use `addToScenario(...)`.
186
+
187
+ ```js
188
+ async function addContactsToScenario(contacts) {
189
+ await ensureRoleplaySdkReady();
60
190
 
61
- // 3. Add contacts to a scenario in bulk
62
191
  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);
192
+ contacts: contacts.map((contact) => ({
193
+ name: contact.name,
194
+ company: contact.company,
195
+ title: contact.title,
196
+ domain: contact.domain,
197
+ liUrl: contact.linkedinUrl,
198
+ companyDescription: contact.companyDescription,
199
+ })),
200
+ onComplete(data) {
201
+ console.log("Scenario import complete", data);
202
+ },
203
+ onClose() {
204
+ console.log("Add to scenario dialog closed");
205
+ },
206
+ onError(err) {
207
+ console.error("Add to scenario error", err);
69
208
  },
70
- onClose: function() { console.log('Dialog closed'); },
71
- onError: function(err) { console.error('Error:', err.code, err.message); },
72
209
  });
73
-
74
- // Close and destroy
75
- SeamlessRoleplay.close();
76
- SeamlessRoleplay.destroy();
77
- </script>
210
+ }
78
211
  ```
79
212
 
80
- ## Trial Mode
213
+ ## Minimal API surface
214
+
215
+ ### `SeamlessRoleplay.init(options)`
216
+
217
+ Initializes the SDK with the logged-in Seamless user.
218
+
219
+ ```js
220
+ SeamlessRoleplay.init({
221
+ publishableKey,
222
+ userId,
223
+ userEmail,
224
+ userRole,
225
+ onReady,
226
+ onError,
227
+ });
228
+ ```
81
229
 
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.
230
+ ### `SeamlessRoleplay.mount(container)`
83
231
 
84
- ## API
232
+ Mounts the full Roleplay app into a dashboard container.
85
233
 
86
- ### `SeamlessRoleplay.init(options)`
234
+ ### `SeamlessRoleplay.open(contactData)`
87
235
 
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()`)
236
+ Opens the Roleplay dialog for a single contact.
122
237
 
123
238
  ### `SeamlessRoleplay.addToScenario(options)`
124
239
 
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 })` |
240
+ Opens the bulk add-to-scenario dialog for 1 to 25 contacts.
139
241
 
140
242
  ### `SeamlessRoleplay.close()`
141
243
 
142
- Closes the active dialog or unmounts.
244
+ Closes the active dialog.
143
245
 
144
- ### `SeamlessRoleplay.destroy()`
246
+ ### `SeamlessRoleplay.unmount()`
145
247
 
146
- Tears down everything clears tokens, timers, and DOM elements.
248
+ Unmounts the full-page Roleplay embed.
147
249
 
148
- ## Auth Flow
250
+ ### `SeamlessRoleplay.destroy()`
149
251
 
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" }
162
-
163
- Token stored in memory (not localStorage)
164
- Auto-refreshes at 80% of TTL
165
- ```
252
+ Destroys the SDK state, timers, mount, and dialogs.
166
253
 
167
- ## Error Handling
254
+ ## Trial and upgrade handling
168
255
 
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.
256
+ 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
257
 
171
258
  ## TypeScript
172
259
 
173
- Full type declarations are included:
260
+ Type declarations are included with the SDK package.
174
261
 
175
262
  ```ts
176
- import type { SeamlessRoleplaySDK, SeamlessRoleplayOpenData, AddToScenarioOptions } from '@rehers/rehers-roleplay-sdk';
263
+ import type {
264
+ SeamlessRoleplaySDK,
265
+ SeamlessRoleplayInitOptions,
266
+ SeamlessRoleplayOpenData,
267
+ AddToScenarioOptions,
268
+ } from "@rehers/rehers-roleplay-sdk";
177
269
  ```
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.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",
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