@rehers/rehers-roleplay-sdk 1.0.0 → 2.1.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 +94 -34
- package/index.d.ts +74 -23
- package/package.json +1 -1
- package/roleplay-sdk.js +643 -214
package/README.md
CHANGED
|
@@ -1,84 +1,144 @@
|
|
|
1
|
-
# @rehers/
|
|
1
|
+
# @rehers/rehers-roleplay-sdk
|
|
2
2
|
|
|
3
|
-
Lightweight vanilla JS SDK for embedding
|
|
3
|
+
Lightweight vanilla JS SDK for embedding roleplay call sessions. Opens a modal (dialog mode) or mounts into a container (mount mode) — no framework dependencies.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @rehers/
|
|
8
|
+
npm install @rehers/rehers-roleplay-sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Or load via CDN:
|
|
12
12
|
|
|
13
13
|
```html
|
|
14
|
-
<script src="https://unpkg.com/@rehers/
|
|
14
|
+
<script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```html
|
|
20
|
-
<script src="https://unpkg.com/@rehers/
|
|
20
|
+
<script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
|
|
21
21
|
<script>
|
|
22
|
-
// 1. Initialize with a
|
|
22
|
+
// 1. Initialize with a publishable key
|
|
23
23
|
SeamlessRoleplay.init({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
publishableKey: 'pk_live_abc123',
|
|
25
|
+
userId: 'user_789',
|
|
26
|
+
userEmail: 'john@example.com', // optional
|
|
27
|
+
trialUrl: 'https://seamless.ai/pricing', // optional — shown when user not found
|
|
28
|
+
onReady: function() {
|
|
29
|
+
console.log('SDK ready');
|
|
30
|
+
},
|
|
31
|
+
onError: function(err) {
|
|
32
|
+
console.error('SDK error:', err.code, err.message);
|
|
27
33
|
},
|
|
28
34
|
});
|
|
29
35
|
|
|
30
|
-
// 2. Open the roleplay modal for a contact
|
|
36
|
+
// 2. Open the roleplay modal for a contact (dialog mode)
|
|
31
37
|
SeamlessRoleplay.open({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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); },
|
|
37
48
|
});
|
|
38
49
|
|
|
39
|
-
//
|
|
40
|
-
SeamlessRoleplay.
|
|
50
|
+
// 2b. Or mount into a container (full-page embed)
|
|
51
|
+
SeamlessRoleplay.mount(document.getElementById('roleplay-container'), {
|
|
52
|
+
name: 'Jane Smith',
|
|
53
|
+
domain: 'acme.com',
|
|
54
|
+
company: 'Acme Corp',
|
|
55
|
+
title: 'VP of Sales',
|
|
56
|
+
});
|
|
41
57
|
|
|
42
|
-
//
|
|
58
|
+
// Close and destroy
|
|
59
|
+
SeamlessRoleplay.close();
|
|
43
60
|
SeamlessRoleplay.destroy();
|
|
44
61
|
</script>
|
|
45
62
|
```
|
|
46
63
|
|
|
64
|
+
## Trial Mode
|
|
65
|
+
|
|
66
|
+
If the user doesn't have an active account, provide a `trialUrl` during init. When the backend returns `USER_NOT_FOUND`, the SDK will render a trial screen:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
SeamlessRoleplay.init({
|
|
70
|
+
publishableKey: 'pk_live_abc123',
|
|
71
|
+
userId: 'unknown_user',
|
|
72
|
+
trialUrl: 'https://seamless.ai/pricing',
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
47
76
|
## API
|
|
48
77
|
|
|
49
78
|
### `SeamlessRoleplay.init(options)`
|
|
50
79
|
|
|
51
80
|
| Option | Type | Required | Description |
|
|
52
81
|
|---|---|---|---|
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
82
|
+
| `publishableKey` | `string` | Yes | Publishable API key |
|
|
83
|
+
| `userId` | `string` | Yes | Your user's unique identifier |
|
|
84
|
+
| `userEmail` | `string` | No | User email for account matching |
|
|
85
|
+
| `userToken` | `string` | No | Signed JWT for identity verification |
|
|
86
|
+
| `trialUrl` | `string` | No | URL shown when user not found |
|
|
87
|
+
| `origin` | `string` | No | Override app origin (dev only) |
|
|
88
|
+
| `onReady` | `function` | No | Called when session is ready |
|
|
89
|
+
| `onError` | `function` | No | Called on init error `({ code, message })` |
|
|
59
90
|
|
|
60
|
-
### `SeamlessRoleplay.open(
|
|
91
|
+
### `SeamlessRoleplay.open(data)`
|
|
92
|
+
|
|
93
|
+
Opens the roleplay in a modal dialog overlay.
|
|
61
94
|
|
|
62
95
|
| Field | Type | Required | Description |
|
|
63
96
|
|---|---|---|---|
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
97
|
+
| `name` | `string` | Yes | Full name of the contact |
|
|
98
|
+
| `domain` | `string` | Yes | Company domain (e.g. "stripe.com") |
|
|
99
|
+
| `company` | `string` | Yes | Company name |
|
|
100
|
+
| `title` | `string` | Yes | Job title |
|
|
101
|
+
| `companyDescription` | `string` | No | Brief company description |
|
|
102
|
+
| `liUrl` | `string` | No | LinkedIn profile URL |
|
|
103
|
+
| `onCallStarted` | `function` | No | `({ callId })` |
|
|
104
|
+
| `onCallEnded` | `function` | No | `({ callId, duration? })` |
|
|
105
|
+
| `onClose` | `function` | No | Called when dialog closes |
|
|
106
|
+
| `onError` | `function` | No | `({ code, message })` |
|
|
107
|
+
|
|
108
|
+
### `SeamlessRoleplay.mount(container, data)`
|
|
109
|
+
|
|
110
|
+
Mounts the roleplay into a DOM element (full-page embed). Same `data` options as `open()`.
|
|
69
111
|
|
|
70
112
|
### `SeamlessRoleplay.close()`
|
|
71
113
|
|
|
72
|
-
Closes the
|
|
114
|
+
Closes the active dialog or unmounts.
|
|
73
115
|
|
|
74
116
|
### `SeamlessRoleplay.destroy()`
|
|
75
117
|
|
|
76
|
-
|
|
118
|
+
Tears down everything — clears tokens, timers, and DOM elements.
|
|
119
|
+
|
|
120
|
+
## Auth Flow
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
SDK Backend
|
|
124
|
+
───────────────── ─────────────────
|
|
125
|
+
POST /api/sdk/session
|
|
126
|
+
X-Publishable-Key: pk_live_abc123
|
|
127
|
+
Body: { userId, userEmail? }
|
|
128
|
+
1. Validate key + origin
|
|
129
|
+
2. Find/create user
|
|
130
|
+
3. Mint JWT (1hr TTL)
|
|
131
|
+
←──── { sessionToken, expiresIn }
|
|
132
|
+
OR { error: "USER_NOT_FOUND" }
|
|
133
|
+
|
|
134
|
+
Token stored in memory (not localStorage)
|
|
135
|
+
Auto-refreshes at 80% of TTL
|
|
136
|
+
```
|
|
77
137
|
|
|
78
138
|
## TypeScript
|
|
79
139
|
|
|
80
|
-
Full type declarations are included
|
|
140
|
+
Full type declarations are included:
|
|
81
141
|
|
|
82
142
|
```ts
|
|
83
|
-
import type { SeamlessRoleplaySDK,
|
|
143
|
+
import type { SeamlessRoleplaySDK, SeamlessRoleplayOpenData } from '@rehers/rehers-roleplay-sdk';
|
|
84
144
|
```
|
package/index.d.ts
CHANGED
|
@@ -1,39 +1,90 @@
|
|
|
1
1
|
export interface SeamlessRoleplayInitOptions {
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
/**
|
|
2
|
+
/** Publishable API key (starts with pk_live_ or pk_test_) */
|
|
3
|
+
publishableKey: string;
|
|
4
|
+
/** Your user's unique identifier */
|
|
5
|
+
userId: string;
|
|
6
|
+
/** User email — required for secure account matching */
|
|
7
|
+
userEmail: string;
|
|
8
|
+
/** Optional signed JWT for identity verification */
|
|
9
|
+
userToken?: string;
|
|
10
|
+
/** URL shown when the user is not found (trial mode) */
|
|
11
|
+
trialUrl?: string;
|
|
12
|
+
/** Override the app origin — where the iframe loads from (for dev/testing only) */
|
|
13
|
+
origin?: string;
|
|
14
|
+
/** Called when the SDK session is ready */
|
|
15
|
+
onReady?: () => void;
|
|
16
|
+
/** Called on initialization error */
|
|
17
|
+
onError?: (error: { code: string; message: string }) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SeamlessRoleplayOpenData {
|
|
21
|
+
/** Full name of the contact */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Company domain (e.g. "stripe.com") */
|
|
24
|
+
domain: string;
|
|
25
|
+
/** Company name */
|
|
26
|
+
company: string;
|
|
27
|
+
/** Job title of the contact */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Optional company description */
|
|
30
|
+
companyDescription?: string;
|
|
31
|
+
/** Optional LinkedIn profile URL */
|
|
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
|
+
/** Called when the dialog/mount is closed */
|
|
7
38
|
onClose?: () => void;
|
|
8
|
-
/** Called
|
|
9
|
-
onCallStart?: (data: { callId: string }) => void;
|
|
10
|
-
/** Called when a call ends */
|
|
11
|
-
onCallEnd?: (data: { callId: string; duration?: number }) => void;
|
|
12
|
-
/** Called on error */
|
|
39
|
+
/** Called on error during the session */
|
|
13
40
|
onError?: (data: { code: string; message: string }) => void;
|
|
14
41
|
}
|
|
15
42
|
|
|
16
|
-
export interface
|
|
43
|
+
export interface AddToScenarioContact {
|
|
17
44
|
/** Full name of the contact */
|
|
18
|
-
|
|
19
|
-
/** Company
|
|
20
|
-
|
|
45
|
+
name: string;
|
|
46
|
+
/** Company name */
|
|
47
|
+
company: string;
|
|
21
48
|
/** Job title of the contact */
|
|
22
|
-
|
|
49
|
+
title: string;
|
|
50
|
+
/** Company domain (e.g. "stripe.com") */
|
|
51
|
+
domain: string;
|
|
23
52
|
/** Optional LinkedIn profile URL */
|
|
24
|
-
|
|
25
|
-
/** Optional
|
|
26
|
-
|
|
53
|
+
liUrl?: string;
|
|
54
|
+
/** Optional company description */
|
|
55
|
+
companyDescription?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AddToScenarioCompleteData {
|
|
59
|
+
scenarioId: string;
|
|
60
|
+
scenarioName: string;
|
|
61
|
+
addedCount: number;
|
|
62
|
+
skippedCount: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AddToScenarioOptions {
|
|
66
|
+
/** Array of contacts to add (1–25) */
|
|
67
|
+
contacts: AddToScenarioContact[];
|
|
68
|
+
/** Called when contacts have been successfully added */
|
|
69
|
+
onComplete?: (data: AddToScenarioCompleteData) => void;
|
|
70
|
+
/** Called when the dialog is closed */
|
|
71
|
+
onClose?: () => void;
|
|
72
|
+
/** Called on error */
|
|
73
|
+
onError?: (error: { code: string; message: string }) => void;
|
|
27
74
|
}
|
|
28
75
|
|
|
29
76
|
export interface SeamlessRoleplaySDK {
|
|
30
|
-
/** Initialize the SDK
|
|
77
|
+
/** Initialize the SDK with a publishable key. */
|
|
31
78
|
init(options: SeamlessRoleplayInitOptions): void;
|
|
32
|
-
/** Open the roleplay modal
|
|
33
|
-
open(data:
|
|
34
|
-
/**
|
|
79
|
+
/** Open the roleplay modal for a contact (dialog mode). */
|
|
80
|
+
open(data: SeamlessRoleplayOpenData): void;
|
|
81
|
+
/** Mount the roleplay into a container element (full-page embed). */
|
|
82
|
+
mount(container: HTMLElement, data: SeamlessRoleplayOpenData): void;
|
|
83
|
+
/** Open the add-to-scenario dialog for bulk contact import. */
|
|
84
|
+
addToScenario(options: AddToScenarioOptions): void;
|
|
85
|
+
/** Close the roleplay dialog or unmount. */
|
|
35
86
|
close(): void;
|
|
36
|
-
/**
|
|
87
|
+
/** Destroy the SDK — clears state, timers, and DOM. */
|
|
37
88
|
destroy(): void;
|
|
38
89
|
}
|
|
39
90
|
|
package/package.json
CHANGED
package/roleplay-sdk.js
CHANGED
|
@@ -1,288 +1,717 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SeamlessRoleplay SDK
|
|
3
|
-
*
|
|
2
|
+
* SeamlessRoleplay SDK v2
|
|
3
|
+
*
|
|
4
|
+
* Publishable-key auth model. No build step required.
|
|
4
5
|
*
|
|
5
6
|
* Usage:
|
|
6
|
-
* SeamlessRoleplay.init({
|
|
7
|
-
* SeamlessRoleplay.open({
|
|
8
|
-
* SeamlessRoleplay.close();
|
|
9
|
-
* SeamlessRoleplay.destroy();
|
|
7
|
+
* SeamlessRoleplay.init({ publishableKey: 'pk_live_...', userId: 'user_789' });
|
|
8
|
+
* SeamlessRoleplay.open({ name: '...', domain: '...', company: '...', title: '...' });
|
|
10
9
|
*/
|
|
11
10
|
(function () {
|
|
12
11
|
"use strict";
|
|
13
12
|
|
|
14
|
-
var DEFAULT_APP_ORIGIN = "https://roleplaywithseamless.ai";
|
|
13
|
+
var DEFAULT_APP_ORIGIN = "https://app.roleplaywithseamless.ai";
|
|
14
|
+
var DEFAULT_API_ORIGIN = "https://server.roleplaywithseamless.ai";
|
|
15
|
+
var SESSION_TIMEOUT_MS = 15000;
|
|
16
|
+
var SDK_LOG_PREFIX = "[SeamlessRoleplay]";
|
|
17
|
+
|
|
18
|
+
// ── State ───────────────────────────────────────────────────────────
|
|
19
|
+
var publishableKey = null;
|
|
20
|
+
var userId = null;
|
|
21
|
+
var userEmail = null;
|
|
22
|
+
var userToken = null;
|
|
23
|
+
var trialUrl = null;
|
|
24
|
+
var appOrigin = null;
|
|
25
|
+
|
|
26
|
+
var sessionToken = null;
|
|
27
|
+
var sessionExpiresAt = 0; // epoch ms
|
|
28
|
+
var refreshTimer = null;
|
|
29
|
+
var fetchingSession = null; // single-flight Promise
|
|
30
|
+
|
|
31
|
+
var initCallbacks = { onReady: null, onError: null };
|
|
32
|
+
var callbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
|
|
33
|
+
var addToScenarioCallbacks = { onComplete: null, onClose: null, onError: null };
|
|
34
|
+
var addToScenarioPendingContacts = null;
|
|
15
35
|
|
|
16
|
-
var config = null;
|
|
17
36
|
var overlay = null;
|
|
18
37
|
var iframe = null;
|
|
38
|
+
var mountContainer = null;
|
|
19
39
|
var pendingContactData = null;
|
|
20
|
-
var
|
|
21
|
-
var
|
|
40
|
+
var mode = null; // "dialog" | "mount" | "add-to-scenario"
|
|
41
|
+
var listener = null;
|
|
42
|
+
var initCalled = false;
|
|
43
|
+
var closeTeardownTimer = null;
|
|
44
|
+
|
|
45
|
+
// ── Safe logging ──────────────────────────────────────────────────
|
|
22
46
|
|
|
23
|
-
function
|
|
24
|
-
|
|
47
|
+
function logError(method, err) {
|
|
48
|
+
try {
|
|
49
|
+
if (typeof console !== "undefined" && console.error) {
|
|
50
|
+
console.error(SDK_LOG_PREFIX, method + ":", err && err.message ? err.message : err);
|
|
51
|
+
}
|
|
52
|
+
} catch (_) {
|
|
53
|
+
// Never throw from the logger itself
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function getOrigin() {
|
|
60
|
+
return appOrigin || DEFAULT_APP_ORIGIN;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getApiOrigin() {
|
|
64
|
+
return DEFAULT_API_ORIGIN;
|
|
25
65
|
}
|
|
26
66
|
|
|
27
67
|
function sendToIframe(msg) {
|
|
28
|
-
|
|
29
|
-
iframe.contentWindow
|
|
68
|
+
try {
|
|
69
|
+
if (iframe && iframe.contentWindow) {
|
|
70
|
+
iframe.contentWindow.postMessage(msg, getOrigin());
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
logError("sendToIframe", e);
|
|
30
74
|
}
|
|
31
75
|
}
|
|
32
76
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
contactName: pendingContactData.contactName,
|
|
60
|
-
contactCompany: pendingContactData.contactCompany,
|
|
61
|
-
contactTitle: pendingContactData.contactTitle,
|
|
62
|
-
linkedinUrl: pendingContactData.linkedinUrl || undefined,
|
|
63
|
-
scenarioId: pendingContactData.scenarioId || undefined,
|
|
64
|
-
});
|
|
65
|
-
pendingContactData = null;
|
|
77
|
+
// ── Session management ──────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function fetchSession() {
|
|
80
|
+
if (fetchingSession) return fetchingSession;
|
|
81
|
+
|
|
82
|
+
fetchingSession = new Promise(function (resolve, reject) {
|
|
83
|
+
var url = getApiOrigin() + "/api/sdk/session";
|
|
84
|
+
var body = { userId: userId };
|
|
85
|
+
if (userEmail) body.userEmail = userEmail;
|
|
86
|
+
if (userToken) body.userToken = userToken;
|
|
87
|
+
|
|
88
|
+
var xhr = new XMLHttpRequest();
|
|
89
|
+
xhr.open("POST", url, true);
|
|
90
|
+
xhr.setRequestHeader("Content-Type", "application/json");
|
|
91
|
+
xhr.setRequestHeader("X-Publishable-Key", publishableKey);
|
|
92
|
+
xhr.withCredentials = false;
|
|
93
|
+
xhr.timeout = SESSION_TIMEOUT_MS;
|
|
94
|
+
|
|
95
|
+
xhr.onload = function () {
|
|
96
|
+
fetchingSession = null;
|
|
97
|
+
var data;
|
|
98
|
+
try {
|
|
99
|
+
data = JSON.parse(xhr.responseText);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
reject({ code: "PARSE_ERROR", message: "Invalid response from session endpoint" });
|
|
102
|
+
return;
|
|
66
103
|
}
|
|
67
|
-
break;
|
|
68
104
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
if (xhr.status === 200 && data.sessionToken) {
|
|
106
|
+
sessionToken = data.sessionToken;
|
|
107
|
+
var ttl = (data.expiresIn || 3600) * 1000;
|
|
108
|
+
sessionExpiresAt = Date.now() + ttl;
|
|
109
|
+
scheduleRefresh(ttl);
|
|
110
|
+
resolve({ sessionToken: sessionToken });
|
|
111
|
+
return;
|
|
72
112
|
}
|
|
73
|
-
break;
|
|
74
113
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
114
|
+
if (data.error === "USER_NOT_FOUND") {
|
|
115
|
+
// Trial mode — not a fatal error
|
|
116
|
+
sessionToken = null;
|
|
117
|
+
resolve({ trialMode: true });
|
|
118
|
+
return;
|
|
81
119
|
}
|
|
82
|
-
break;
|
|
83
120
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
121
|
+
reject({
|
|
122
|
+
code: data.error || "SESSION_ERROR",
|
|
123
|
+
message: data.message || "Failed to create session (HTTP " + xhr.status + ")",
|
|
124
|
+
});
|
|
125
|
+
};
|
|
87
126
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
127
|
+
xhr.onerror = function () {
|
|
128
|
+
fetchingSession = null;
|
|
129
|
+
reject({ code: "NETWORK_ERROR", message: "Network error contacting session endpoint" });
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
xhr.ontimeout = function () {
|
|
133
|
+
fetchingSession = null;
|
|
134
|
+
reject({ code: "TIMEOUT", message: "Session request timed out after " + SESSION_TIMEOUT_MS + "ms" });
|
|
135
|
+
};
|
|
95
136
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
s.left = "0";
|
|
103
|
-
s.width = "100%";
|
|
104
|
-
s.height = "100%";
|
|
105
|
-
s.zIndex = "2147483647";
|
|
106
|
-
s.display = "flex";
|
|
107
|
-
s.alignItems = "center";
|
|
108
|
-
s.justifyContent = "center";
|
|
109
|
-
s.background = "rgba(0, 0, 0, 0.6)";
|
|
110
|
-
s.backdropFilter = "blur(4px)";
|
|
111
|
-
|
|
112
|
-
// Container
|
|
113
|
-
var container = document.createElement("div");
|
|
114
|
-
var cs = container.style;
|
|
115
|
-
cs.position = "relative";
|
|
116
|
-
cs.width = "90vw";
|
|
117
|
-
cs.maxWidth = "1100px";
|
|
118
|
-
cs.height = "85vh";
|
|
119
|
-
cs.maxHeight = "800px";
|
|
120
|
-
cs.borderRadius = "16px";
|
|
121
|
-
cs.overflow = "hidden";
|
|
122
|
-
cs.background = "#fff";
|
|
123
|
-
cs.boxShadow = "0 25px 60px rgba(0,0,0,0.3)";
|
|
124
|
-
|
|
125
|
-
// Close button
|
|
126
|
-
var closeBtn = document.createElement("button");
|
|
127
|
-
closeBtn.type = "button";
|
|
128
|
-
closeBtn.innerHTML = "×";
|
|
129
|
-
var cbs = closeBtn.style;
|
|
130
|
-
cbs.position = "absolute";
|
|
131
|
-
cbs.top = "12px";
|
|
132
|
-
cbs.right = "12px";
|
|
133
|
-
cbs.zIndex = "10";
|
|
134
|
-
cbs.width = "32px";
|
|
135
|
-
cbs.height = "32px";
|
|
136
|
-
cbs.border = "none";
|
|
137
|
-
cbs.borderRadius = "50%";
|
|
138
|
-
cbs.background = "rgba(0,0,0,0.08)";
|
|
139
|
-
cbs.color = "#333";
|
|
140
|
-
cbs.fontSize = "20px";
|
|
141
|
-
cbs.cursor = "pointer";
|
|
142
|
-
cbs.display = "flex";
|
|
143
|
-
cbs.alignItems = "center";
|
|
144
|
-
cbs.justifyContent = "center";
|
|
145
|
-
cbs.lineHeight = "1";
|
|
146
|
-
closeBtn.addEventListener("click", function () {
|
|
147
|
-
SeamlessRoleplay.close();
|
|
137
|
+
xhr.onabort = function () {
|
|
138
|
+
fetchingSession = null;
|
|
139
|
+
reject({ code: "ABORTED", message: "Session request was aborted" });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
xhr.send(JSON.stringify(body));
|
|
148
143
|
});
|
|
149
144
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
ifs.display = "block";
|
|
160
|
-
|
|
161
|
-
container.appendChild(closeBtn);
|
|
162
|
-
container.appendChild(iframeEl);
|
|
163
|
-
el.appendChild(container);
|
|
164
|
-
|
|
165
|
-
// Close on backdrop click
|
|
166
|
-
el.addEventListener("click", function (e) {
|
|
167
|
-
if (e.target === el) {
|
|
168
|
-
SeamlessRoleplay.close();
|
|
169
|
-
}
|
|
145
|
+
return fetchingSession;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getSessionToken() {
|
|
149
|
+
if (sessionToken && Date.now() < sessionExpiresAt - 30000) {
|
|
150
|
+
return Promise.resolve(sessionToken);
|
|
151
|
+
}
|
|
152
|
+
return fetchSession().then(function (result) {
|
|
153
|
+
return result.sessionToken || null;
|
|
170
154
|
});
|
|
155
|
+
}
|
|
171
156
|
|
|
172
|
-
|
|
157
|
+
function scheduleRefresh(ttlMs) {
|
|
158
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
159
|
+
var delay = Math.max(ttlMs * 0.8, 5000);
|
|
160
|
+
refreshTimer = setTimeout(function () {
|
|
161
|
+
fetchSession().catch(function () {
|
|
162
|
+
// Silent — next open() will retry
|
|
163
|
+
});
|
|
164
|
+
}, delay);
|
|
173
165
|
}
|
|
174
166
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
167
|
+
// ── Teardown ────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function teardown() {
|
|
170
|
+
try {
|
|
171
|
+
// Cancel any pending close→teardown timer so it can't destroy a new dialog
|
|
172
|
+
if (closeTeardownTimer) {
|
|
173
|
+
clearTimeout(closeTeardownTimer);
|
|
174
|
+
closeTeardownTimer = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (mode === "dialog" || mode === "add-to-scenario") {
|
|
178
|
+
if (overlay && overlay.parentNode) {
|
|
179
|
+
overlay.parentNode.removeChild(overlay);
|
|
180
|
+
}
|
|
181
|
+
overlay = null;
|
|
182
|
+
} else if (mode === "mount") {
|
|
183
|
+
if (iframe && iframe.parentNode) {
|
|
184
|
+
iframe.parentNode.removeChild(iframe);
|
|
185
|
+
}
|
|
186
|
+
mountContainer = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (listener) {
|
|
190
|
+
window.removeEventListener("message", listener);
|
|
191
|
+
listener = null;
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
logError("teardown", e);
|
|
178
195
|
}
|
|
179
|
-
|
|
196
|
+
|
|
197
|
+
// Always reset state even if DOM cleanup failed
|
|
180
198
|
iframe = null;
|
|
181
|
-
iframeAuthenticated = false;
|
|
182
199
|
pendingContactData = null;
|
|
183
|
-
|
|
184
|
-
|
|
200
|
+
addToScenarioPendingContacts = null;
|
|
201
|
+
mode = null;
|
|
202
|
+
callbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
|
|
203
|
+
addToScenarioCallbacks = { onComplete: null, onClose: null, onError: null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Message handler ─────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function dispatchInitToIframe(token) {
|
|
209
|
+
if (addToScenarioPendingContacts) {
|
|
210
|
+
sendToIframe({
|
|
211
|
+
type: "seamless-add-to-scenario-init",
|
|
212
|
+
sessionToken: token,
|
|
213
|
+
publishableKey: publishableKey,
|
|
214
|
+
userId: userId,
|
|
215
|
+
contacts: addToScenarioPendingContacts,
|
|
216
|
+
});
|
|
217
|
+
} else if (token && pendingContactData) {
|
|
218
|
+
sendToIframe({
|
|
219
|
+
type: "seamless-session-init",
|
|
220
|
+
sessionToken: token,
|
|
221
|
+
contact: {
|
|
222
|
+
name: pendingContactData.name,
|
|
223
|
+
domain: pendingContactData.domain,
|
|
224
|
+
company: pendingContactData.company,
|
|
225
|
+
title: pendingContactData.title,
|
|
226
|
+
companyDescription: pendingContactData.companyDescription || undefined,
|
|
227
|
+
liUrl: pendingContactData.liUrl || undefined,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
} else if (trialUrl) {
|
|
231
|
+
sendToIframe({
|
|
232
|
+
type: "seamless-session-init",
|
|
233
|
+
trialUrl: trialUrl,
|
|
234
|
+
contact: null,
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
sendToIframe({
|
|
238
|
+
type: "seamless-session-init",
|
|
239
|
+
trialUrl: null,
|
|
240
|
+
contact: null,
|
|
241
|
+
});
|
|
185
242
|
}
|
|
186
243
|
}
|
|
187
244
|
|
|
245
|
+
function handleMessage(event) {
|
|
246
|
+
try {
|
|
247
|
+
if (event.origin !== getOrigin()) return;
|
|
248
|
+
|
|
249
|
+
var data = event.data;
|
|
250
|
+
if (!data || typeof data.type !== "string") return;
|
|
251
|
+
|
|
252
|
+
switch (data.type) {
|
|
253
|
+
case "ROLEPLAY_READY":
|
|
254
|
+
// Ensure we have a fresh token before sending init to iframe
|
|
255
|
+
getSessionToken()
|
|
256
|
+
.then(function (token) {
|
|
257
|
+
dispatchInitToIframe(token);
|
|
258
|
+
})
|
|
259
|
+
.catch(function () {
|
|
260
|
+
// Token refresh failed — send whatever we have (iframe will handle auth errors)
|
|
261
|
+
dispatchInitToIframe(sessionToken);
|
|
262
|
+
});
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case "ROLEPLAY_CALL_STARTED":
|
|
266
|
+
if (callbacks.onCallStarted) {
|
|
267
|
+
callbacks.onCallStarted({ callId: data.callId });
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case "ROLEPLAY_CALL_ENDED":
|
|
272
|
+
if (callbacks.onCallEnded) {
|
|
273
|
+
callbacks.onCallEnded({ callId: data.callId, duration: data.duration });
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case "ROLEPLAY_ERROR":
|
|
278
|
+
if (callbacks.onError) {
|
|
279
|
+
callbacks.onError({ code: data.code, message: data.message });
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
|
|
283
|
+
case "ROLEPLAY_CLOSED":
|
|
284
|
+
var onClose = callbacks.onClose;
|
|
285
|
+
teardown();
|
|
286
|
+
if (onClose) onClose();
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case "ADD_TO_SCENARIO_COMPLETE":
|
|
290
|
+
var atsOnComplete = addToScenarioCallbacks.onComplete;
|
|
291
|
+
teardown();
|
|
292
|
+
if (atsOnComplete) {
|
|
293
|
+
atsOnComplete({
|
|
294
|
+
scenarioId: data.scenarioId,
|
|
295
|
+
scenarioName: data.scenarioName,
|
|
296
|
+
addedCount: data.addedCount,
|
|
297
|
+
skippedCount: data.skippedCount,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case "ADD_TO_SCENARIO_ERROR":
|
|
303
|
+
if (addToScenarioCallbacks.onError) {
|
|
304
|
+
addToScenarioCallbacks.onError({ code: data.code, message: data.message });
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
|
|
308
|
+
case "ADD_TO_SCENARIO_CLOSED":
|
|
309
|
+
var atsOnClose = addToScenarioCallbacks.onClose;
|
|
310
|
+
teardown();
|
|
311
|
+
if (atsOnClose) atsOnClose();
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
logError("handleMessage", e);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Close ───────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
function close() {
|
|
322
|
+
try {
|
|
323
|
+
sendToIframe({ type: "roleplay-close" });
|
|
324
|
+
if (closeTeardownTimer) clearTimeout(closeTeardownTimer);
|
|
325
|
+
closeTeardownTimer = setTimeout(teardown, 300);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
logError("close", e);
|
|
328
|
+
teardown();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Create iframe ───────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
function createIframe() {
|
|
335
|
+
var iframeEl = document.createElement("iframe");
|
|
336
|
+
iframeEl.src = getOrigin() + "/embed/roleplay-call";
|
|
337
|
+
iframeEl.allow = "camera; microphone; display-capture; autoplay";
|
|
338
|
+
iframeEl.style.width = "100%";
|
|
339
|
+
iframeEl.style.height = "100%";
|
|
340
|
+
iframeEl.style.border = "none";
|
|
341
|
+
iframeEl.style.display = "block";
|
|
342
|
+
return iframeEl;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── SDK API ─────────────────────────────────────────────────────────
|
|
346
|
+
|
|
188
347
|
var SeamlessRoleplay = {
|
|
189
348
|
/**
|
|
190
|
-
* Initialize the SDK
|
|
191
|
-
* @param {Object} opts
|
|
192
|
-
* @param {string} opts.loginPass - Seamless login pass for auth
|
|
193
|
-
* @param {string} [opts.appOrigin] - Override app origin (default: production)
|
|
194
|
-
* @param {Function} [opts.onClose] - Called when modal closes
|
|
195
|
-
* @param {Function} [opts.onCallStart] - Called when call begins
|
|
196
|
-
* @param {Function} [opts.onCallEnd] - Called when call ends ({ callId, duration })
|
|
197
|
-
* @param {Function} [opts.onError] - Called on error ({ code, message })
|
|
349
|
+
* Initialize the SDK with a publishable key.
|
|
198
350
|
*/
|
|
199
351
|
init: function (opts) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
352
|
+
try {
|
|
353
|
+
if (!opts || !opts.publishableKey || !opts.userId || !opts.userEmail) {
|
|
354
|
+
logError("init", "requires { publishableKey, userId, userEmail }");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Reset prior state if init() is called again
|
|
359
|
+
if (refreshTimer) {
|
|
360
|
+
clearTimeout(refreshTimer);
|
|
361
|
+
refreshTimer = null;
|
|
362
|
+
}
|
|
363
|
+
fetchingSession = null;
|
|
364
|
+
sessionToken = null;
|
|
365
|
+
sessionExpiresAt = 0;
|
|
366
|
+
|
|
367
|
+
publishableKey = opts.publishableKey;
|
|
368
|
+
userId = opts.userId;
|
|
369
|
+
userEmail = opts.userEmail || null;
|
|
370
|
+
userToken = opts.userToken || null;
|
|
371
|
+
trialUrl = opts.trialUrl || null;
|
|
372
|
+
appOrigin = opts.origin || null;
|
|
373
|
+
initCallbacks.onReady = opts.onReady || null;
|
|
374
|
+
initCallbacks.onError = opts.onError || null;
|
|
375
|
+
initCalled = true;
|
|
213
376
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
377
|
+
// Fetch session immediately
|
|
378
|
+
fetchSession()
|
|
379
|
+
.then(function (result) {
|
|
380
|
+
if (result.trialMode && !trialUrl) {
|
|
381
|
+
// User not found and no trial URL configured — still call onReady
|
|
382
|
+
// The open() will show an error state in the iframe
|
|
383
|
+
}
|
|
384
|
+
if (initCallbacks.onReady) initCallbacks.onReady();
|
|
385
|
+
})
|
|
386
|
+
.catch(function (err) {
|
|
387
|
+
if (initCallbacks.onError) {
|
|
388
|
+
initCallbacks.onError({ code: err.code || "INIT_ERROR", message: err.message || "Initialization failed" });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
} catch (e) {
|
|
392
|
+
logError("init", e);
|
|
393
|
+
if (opts && typeof opts.onError === "function") {
|
|
394
|
+
try { opts.onError({ code: "SDK_ERROR", message: e.message || "Unexpected error during init" }); } catch (_) {}
|
|
395
|
+
}
|
|
217
396
|
}
|
|
218
|
-
globalListener = handleMessage;
|
|
219
|
-
window.addEventListener("message", globalListener);
|
|
220
397
|
},
|
|
221
398
|
|
|
222
399
|
/**
|
|
223
|
-
* Open the roleplay modal
|
|
224
|
-
* @param {Object} data
|
|
225
|
-
* @param {string} data.contactName
|
|
226
|
-
* @param {string} data.contactCompany
|
|
227
|
-
* @param {string} data.contactTitle
|
|
228
|
-
* @param {string} [data.linkedinUrl]
|
|
229
|
-
* @param {string} [data.scenarioId]
|
|
400
|
+
* Open the roleplay modal for a contact (dialog mode).
|
|
230
401
|
*/
|
|
231
402
|
open: function (data) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
403
|
+
try {
|
|
404
|
+
if (!initCalled) {
|
|
405
|
+
logError("open", "init() must be called first");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (!data || !data.name || !data.domain || !data.company || !data.title) {
|
|
409
|
+
logError("open", "requires { name, domain, company, title }");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// If already open, tear down first (also cancels any pending close timer)
|
|
414
|
+
if (overlay || (iframe && mode)) teardown();
|
|
415
|
+
|
|
416
|
+
pendingContactData = data;
|
|
417
|
+
callbacks.onCallStarted = data.onCallStarted || null;
|
|
418
|
+
callbacks.onCallEnded = data.onCallEnded || null;
|
|
419
|
+
callbacks.onClose = data.onClose || null;
|
|
420
|
+
callbacks.onError = data.onError || null;
|
|
421
|
+
mode = "dialog";
|
|
422
|
+
|
|
423
|
+
// Listen for messages
|
|
424
|
+
listener = handleMessage;
|
|
425
|
+
window.addEventListener("message", listener);
|
|
426
|
+
|
|
427
|
+
// Build overlay
|
|
428
|
+
var el = document.createElement("div");
|
|
429
|
+
el.id = "seamless-roleplay-overlay";
|
|
430
|
+
var s = el.style;
|
|
431
|
+
s.position = "fixed";
|
|
432
|
+
s.top = "0";
|
|
433
|
+
s.left = "0";
|
|
434
|
+
s.width = "100%";
|
|
435
|
+
s.height = "100%";
|
|
436
|
+
s.zIndex = "2147483647";
|
|
437
|
+
s.display = "flex";
|
|
438
|
+
s.alignItems = "center";
|
|
439
|
+
s.justifyContent = "center";
|
|
440
|
+
s.background = "rgba(0, 0, 0, 0.6)";
|
|
441
|
+
s.backdropFilter = "blur(4px)";
|
|
442
|
+
|
|
443
|
+
var container = document.createElement("div");
|
|
444
|
+
var cs = container.style;
|
|
445
|
+
cs.position = "relative";
|
|
446
|
+
cs.width = "90vw";
|
|
447
|
+
cs.maxWidth = "1100px";
|
|
448
|
+
cs.height = "85vh";
|
|
449
|
+
cs.maxHeight = "800px";
|
|
450
|
+
cs.borderRadius = "24px";
|
|
451
|
+
cs.overflow = "hidden";
|
|
452
|
+
cs.background = "#fff";
|
|
453
|
+
cs.boxShadow = "0 25px 60px rgba(0,0,0,0.3)";
|
|
454
|
+
|
|
455
|
+
var closeBtn = document.createElement("button");
|
|
456
|
+
closeBtn.type = "button";
|
|
457
|
+
closeBtn.innerHTML = "×";
|
|
458
|
+
var cbs = closeBtn.style;
|
|
459
|
+
cbs.position = "absolute";
|
|
460
|
+
cbs.top = "12px";
|
|
461
|
+
cbs.right = "12px";
|
|
462
|
+
cbs.zIndex = "10";
|
|
463
|
+
cbs.width = "32px";
|
|
464
|
+
cbs.height = "32px";
|
|
465
|
+
cbs.border = "none";
|
|
466
|
+
cbs.borderRadius = "50%";
|
|
467
|
+
cbs.background = "rgba(0,0,0,0.08)";
|
|
468
|
+
cbs.color = "#333";
|
|
469
|
+
cbs.fontSize = "20px";
|
|
470
|
+
cbs.cursor = "pointer";
|
|
471
|
+
cbs.display = "flex";
|
|
472
|
+
cbs.alignItems = "center";
|
|
473
|
+
cbs.justifyContent = "center";
|
|
474
|
+
cbs.lineHeight = "1";
|
|
475
|
+
closeBtn.addEventListener("click", close);
|
|
476
|
+
|
|
477
|
+
var iframeEl = createIframe();
|
|
478
|
+
|
|
479
|
+
container.appendChild(closeBtn);
|
|
480
|
+
container.appendChild(iframeEl);
|
|
481
|
+
el.appendChild(container);
|
|
482
|
+
|
|
483
|
+
el.addEventListener("click", function (e) {
|
|
484
|
+
if (e.target === el) close();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
overlay = el;
|
|
488
|
+
iframe = iframeEl;
|
|
489
|
+
document.body.appendChild(overlay);
|
|
490
|
+
} catch (e) {
|
|
491
|
+
logError("open", e);
|
|
492
|
+
teardown();
|
|
493
|
+
if (data && typeof data.onError === "function") {
|
|
494
|
+
try { data.onError({ code: "SDK_ERROR", message: e.message || "Unexpected error during open" }); } catch (_) {}
|
|
495
|
+
}
|
|
239
496
|
}
|
|
497
|
+
},
|
|
240
498
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Mount the roleplay into a container element (full-page embed).
|
|
501
|
+
*/
|
|
502
|
+
mount: function (container, data) {
|
|
503
|
+
try {
|
|
504
|
+
if (!initCalled) {
|
|
505
|
+
logError("mount", "init() must be called first");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (!container || !container.appendChild) {
|
|
509
|
+
logError("mount", "requires a DOM element as first argument");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (!data || !data.name || !data.domain || !data.company || !data.title) {
|
|
513
|
+
logError("mount", "requires { name, domain, company, title }");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// If already open, tear down first (also cancels any pending close timer)
|
|
518
|
+
if (overlay || (iframe && mode)) teardown();
|
|
519
|
+
|
|
520
|
+
pendingContactData = data;
|
|
521
|
+
callbacks.onCallStarted = data.onCallStarted || null;
|
|
522
|
+
callbacks.onCallEnded = data.onCallEnded || null;
|
|
523
|
+
callbacks.onClose = data.onClose || null;
|
|
524
|
+
callbacks.onError = data.onError || null;
|
|
525
|
+
mode = "mount";
|
|
526
|
+
mountContainer = container;
|
|
527
|
+
|
|
528
|
+
// Listen for messages
|
|
529
|
+
listener = handleMessage;
|
|
530
|
+
window.addEventListener("message", listener);
|
|
531
|
+
|
|
532
|
+
var iframeEl = createIframe();
|
|
533
|
+
iframe = iframeEl;
|
|
534
|
+
container.appendChild(iframeEl);
|
|
535
|
+
} catch (e) {
|
|
536
|
+
logError("mount", e);
|
|
537
|
+
teardown();
|
|
538
|
+
if (data && typeof data.onError === "function") {
|
|
539
|
+
try { data.onError({ code: "SDK_ERROR", message: e.message || "Unexpected error during mount" }); } catch (_) {}
|
|
540
|
+
}
|
|
244
541
|
}
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Open the add-to-scenario dialog for bulk contact import.
|
|
546
|
+
*/
|
|
547
|
+
addToScenario: function (opts) {
|
|
548
|
+
try {
|
|
549
|
+
if (!initCalled) {
|
|
550
|
+
logError("addToScenario", "init() must be called first");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (!opts || !opts.contacts || !Array.isArray(opts.contacts)) {
|
|
554
|
+
logError("addToScenario", "requires { contacts: [...] }");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (opts.contacts.length === 0) {
|
|
558
|
+
logError("addToScenario", "requires at least 1 contact");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (opts.contacts.length > 25) {
|
|
562
|
+
logError("addToScenario", "supports up to 25 contacts");
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Validate required fields on each contact
|
|
567
|
+
var requiredFields = ["name", "company", "title", "domain"];
|
|
568
|
+
for (var i = 0; i < opts.contacts.length; i++) {
|
|
569
|
+
for (var j = 0; j < requiredFields.length; j++) {
|
|
570
|
+
if (!opts.contacts[i][requiredFields[j]]) {
|
|
571
|
+
logError("addToScenario", "contact at index " + i + " is missing required field: " + requiredFields[j]);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Tear down any existing dialog (also cancels any pending close timer)
|
|
578
|
+
if (overlay || (iframe && mode)) teardown();
|
|
579
|
+
|
|
580
|
+
addToScenarioPendingContacts = opts.contacts;
|
|
581
|
+
addToScenarioCallbacks.onComplete = opts.onComplete || null;
|
|
582
|
+
addToScenarioCallbacks.onClose = opts.onClose || null;
|
|
583
|
+
addToScenarioCallbacks.onError = opts.onError || null;
|
|
584
|
+
mode = "add-to-scenario";
|
|
585
|
+
|
|
586
|
+
// Listen for messages
|
|
587
|
+
listener = handleMessage;
|
|
588
|
+
window.addEventListener("message", listener);
|
|
589
|
+
|
|
590
|
+
// Build overlay — wide, compact dialog
|
|
591
|
+
var el = document.createElement("div");
|
|
592
|
+
el.id = "seamless-roleplay-overlay";
|
|
593
|
+
var s = el.style;
|
|
594
|
+
s.position = "fixed";
|
|
595
|
+
s.top = "0";
|
|
596
|
+
s.left = "0";
|
|
597
|
+
s.width = "100%";
|
|
598
|
+
s.height = "100%";
|
|
599
|
+
s.zIndex = "2147483647";
|
|
600
|
+
s.display = "flex";
|
|
601
|
+
s.alignItems = "center";
|
|
602
|
+
s.justifyContent = "center";
|
|
603
|
+
s.background = "rgba(0, 0, 0, 0.5)";
|
|
604
|
+
s.backdropFilter = "blur(4px)";
|
|
605
|
+
|
|
606
|
+
var container = document.createElement("div");
|
|
607
|
+
var cs = container.style;
|
|
608
|
+
cs.position = "relative";
|
|
609
|
+
cs.width = "520px";
|
|
610
|
+
cs.maxWidth = "95vw";
|
|
611
|
+
cs.height = "320px";
|
|
612
|
+
cs.maxHeight = "80vh";
|
|
613
|
+
cs.borderRadius = "24px";
|
|
614
|
+
cs.overflow = "hidden";
|
|
615
|
+
cs.background = "#fff";
|
|
616
|
+
cs.boxShadow = "0 25px 60px rgba(0,0,0,0.3)";
|
|
617
|
+
|
|
618
|
+
var iframeEl = document.createElement("iframe");
|
|
619
|
+
iframeEl.src = getOrigin() + "/embed/add-to-scenario";
|
|
620
|
+
iframeEl.style.width = "100%";
|
|
621
|
+
iframeEl.style.height = "100%";
|
|
622
|
+
iframeEl.style.border = "none";
|
|
623
|
+
iframeEl.style.display = "block";
|
|
624
|
+
iframeEl.style.position = "absolute";
|
|
625
|
+
iframeEl.style.top = "0";
|
|
626
|
+
iframeEl.style.left = "0";
|
|
627
|
+
iframeEl.style.zIndex = "1";
|
|
628
|
+
|
|
629
|
+
var closeBtn = document.createElement("button");
|
|
630
|
+
closeBtn.type = "button";
|
|
631
|
+
closeBtn.innerHTML = "×";
|
|
632
|
+
var cbs = closeBtn.style;
|
|
633
|
+
cbs.position = "absolute";
|
|
634
|
+
cbs.top = "10px";
|
|
635
|
+
cbs.right = "10px";
|
|
636
|
+
cbs.zIndex = "100";
|
|
637
|
+
cbs.width = "26px";
|
|
638
|
+
cbs.height = "26px";
|
|
639
|
+
cbs.border = "none";
|
|
640
|
+
cbs.borderRadius = "50%";
|
|
641
|
+
cbs.background = "rgba(0,0,0,0.08)";
|
|
642
|
+
cbs.color = "#333";
|
|
643
|
+
cbs.fontSize = "16px";
|
|
644
|
+
cbs.cursor = "pointer";
|
|
645
|
+
cbs.display = "flex";
|
|
646
|
+
cbs.alignItems = "center";
|
|
647
|
+
cbs.justifyContent = "center";
|
|
648
|
+
cbs.lineHeight = "1";
|
|
649
|
+
cbs.transition = "background 0.15s";
|
|
650
|
+
closeBtn.addEventListener("mouseenter", function () { cbs.background = "rgba(0,0,0,0.15)"; });
|
|
651
|
+
closeBtn.addEventListener("mouseleave", function () { cbs.background = "rgba(0,0,0,0.08)"; });
|
|
652
|
+
closeBtn.addEventListener("click", close);
|
|
245
653
|
|
|
246
|
-
|
|
247
|
-
|
|
654
|
+
container.appendChild(iframeEl);
|
|
655
|
+
container.appendChild(closeBtn);
|
|
656
|
+
el.appendChild(container);
|
|
248
657
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
658
|
+
el.addEventListener("click", function (e) {
|
|
659
|
+
if (e.target === el) close();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
overlay = el;
|
|
663
|
+
iframe = iframeEl;
|
|
664
|
+
document.body.appendChild(overlay);
|
|
665
|
+
} catch (e) {
|
|
666
|
+
logError("addToScenario", e);
|
|
667
|
+
teardown();
|
|
668
|
+
if (opts && typeof opts.onError === "function") {
|
|
669
|
+
try { opts.onError({ code: "SDK_ERROR", message: e.message || "Unexpected error during addToScenario" }); } catch (_) {}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
253
672
|
},
|
|
254
673
|
|
|
255
674
|
/**
|
|
256
|
-
* Close the
|
|
675
|
+
* Close the roleplay.
|
|
257
676
|
*/
|
|
258
677
|
close: function () {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
678
|
+
try {
|
|
679
|
+
close();
|
|
680
|
+
} catch (e) {
|
|
681
|
+
logError("close", e);
|
|
682
|
+
teardown();
|
|
683
|
+
}
|
|
263
684
|
},
|
|
264
685
|
|
|
265
686
|
/**
|
|
266
|
-
*
|
|
687
|
+
* Destroy the SDK — clears state, timers, and DOM.
|
|
267
688
|
*/
|
|
268
689
|
destroy: function () {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
690
|
+
try {
|
|
691
|
+
if (refreshTimer) {
|
|
692
|
+
clearTimeout(refreshTimer);
|
|
693
|
+
refreshTimer = null;
|
|
694
|
+
}
|
|
695
|
+
teardown();
|
|
696
|
+
publishableKey = null;
|
|
697
|
+
userId = null;
|
|
698
|
+
userEmail = null;
|
|
699
|
+
userToken = null;
|
|
700
|
+
trialUrl = null;
|
|
701
|
+
sessionToken = null;
|
|
702
|
+
sessionExpiresAt = 0;
|
|
703
|
+
fetchingSession = null;
|
|
704
|
+
initCallbacks = { onReady: null, onError: null };
|
|
705
|
+
initCalled = false;
|
|
706
|
+
} catch (e) {
|
|
707
|
+
logError("destroy", e);
|
|
275
708
|
}
|
|
276
|
-
config = null;
|
|
277
709
|
},
|
|
278
710
|
};
|
|
279
711
|
|
|
280
|
-
// Expose globally
|
|
281
712
|
if (typeof window !== "undefined") {
|
|
282
713
|
window.SeamlessRoleplay = SeamlessRoleplay;
|
|
283
714
|
}
|
|
284
|
-
|
|
285
|
-
// Support CommonJS / ESM
|
|
286
715
|
if (typeof module !== "undefined" && module.exports) {
|
|
287
716
|
module.exports = SeamlessRoleplay;
|
|
288
717
|
}
|