@thisispamela/widget 1.1.0

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/dist/widget.js ADDED
@@ -0,0 +1,1097 @@
1
+ var Pamela = (function (exports) {
2
+ 'use strict';
3
+
4
+ /**
5
+ * Simple event emitter for widget callbacks
6
+ */
7
+ class EventEmitter {
8
+ constructor() {
9
+ this.listeners = new Map();
10
+ }
11
+ on(event, callback) {
12
+ const list = this.listeners.get(event) ?? [];
13
+ list.push(callback);
14
+ this.listeners.set(event, list);
15
+ }
16
+ off(event, callback) {
17
+ const list = this.listeners.get(event);
18
+ if (!list)
19
+ return;
20
+ const idx = list.indexOf(callback);
21
+ if (idx !== -1)
22
+ list.splice(idx, 1);
23
+ if (list.length === 0)
24
+ this.listeners.delete(event);
25
+ }
26
+ emit(event, ...args) {
27
+ const list = this.listeners.get(event);
28
+ if (!list)
29
+ return;
30
+ list.forEach((fn) => {
31
+ try {
32
+ fn(...args);
33
+ }
34
+ catch (_) {
35
+ // Swallow to avoid breaking other listeners
36
+ }
37
+ });
38
+ }
39
+ removeAllListeners(event) {
40
+ if (event) {
41
+ this.listeners.delete(event);
42
+ }
43
+ else {
44
+ this.listeners.clear();
45
+ }
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Lightweight fetch-based API client for the widget.
51
+ * Imports shared types from @thisispamela/sdk for alignment.
52
+ */
53
+ class PamelaError extends Error {
54
+ constructor(message, options) {
55
+ super(message);
56
+ this.name = 'PamelaError';
57
+ this.code = options?.code;
58
+ this.errorCode = options?.errorCode;
59
+ this.statusCode = options?.statusCode;
60
+ this.details = options?.details;
61
+ }
62
+ }
63
+ function mapResponseToCall(raw) {
64
+ return {
65
+ id: raw.id,
66
+ status: raw.status,
67
+ to: '',
68
+ task: '',
69
+ createdAt: raw.created_at,
70
+ };
71
+ }
72
+ function mapStatusToCall(raw) {
73
+ const transcript = raw.transcript?.map((t) => ({
74
+ role: t.role,
75
+ content: t.content,
76
+ timestamp: t.timestamp ?? 0,
77
+ }));
78
+ return {
79
+ id: raw.id,
80
+ status: raw.status,
81
+ to: raw.to,
82
+ task: '',
83
+ duration: raw.duration_seconds,
84
+ transcript,
85
+ summary: raw.summary,
86
+ createdAt: raw.created_at,
87
+ completedAt: raw.completed_at,
88
+ };
89
+ }
90
+ class PamelaApiClient {
91
+ constructor(options) {
92
+ this.authToken = options.authToken;
93
+ this.baseUrl = (options.baseUrl || 'https://api.thisispamela.com').replace(/\/$/, '');
94
+ }
95
+ async request(method, path, body) {
96
+ const url = `${this.baseUrl}${path}`;
97
+ const headers = {
98
+ 'Content-Type': 'application/json',
99
+ Authorization: `Bearer ${this.authToken}`,
100
+ };
101
+ const response = await fetch(url, {
102
+ method,
103
+ headers,
104
+ body: body ? JSON.stringify(body) : undefined,
105
+ });
106
+ const data = await response.json().catch(() => ({}));
107
+ if (!response.ok) {
108
+ const d = data;
109
+ const message = d.detail != null
110
+ ? typeof d.detail === 'string'
111
+ ? d.detail
112
+ : d.detail?.message ?? 'Request failed'
113
+ : d.message ?? 'Request failed';
114
+ const detail = data.detail;
115
+ const code = typeof detail === 'object' ? detail?.error_code : undefined;
116
+ const errCode = data.detail?.error_code
117
+ ?? data.error_code;
118
+ throw new PamelaError(message, {
119
+ code: code != null ? String(code) : undefined,
120
+ errorCode: typeof errCode === 'number' ? errCode : undefined,
121
+ statusCode: response.status,
122
+ });
123
+ }
124
+ return data;
125
+ }
126
+ async createCall(options) {
127
+ const body = {
128
+ to: options.to,
129
+ task: options.task,
130
+ voice: options.voice ?? 'auto',
131
+ agent_name: options.agentName,
132
+ caller_name: options.callerName,
133
+ max_duration_seconds: options.maxDuration,
134
+ metadata: options.metadata,
135
+ webhooks: options.webhooks,
136
+ };
137
+ const raw = await this.request('POST', '/api/b2b/v1/calls', body);
138
+ const call = mapResponseToCall(raw);
139
+ call.to = options.to;
140
+ call.task = options.task;
141
+ return call;
142
+ }
143
+ async getCall(callId) {
144
+ const raw = await this.request('GET', `/api/b2b/v1/calls/${callId}`);
145
+ return mapStatusToCall(raw);
146
+ }
147
+ async cancelCall(callId) {
148
+ await this.request('POST', `/api/b2b/v1/calls/${callId}/cancel`);
149
+ }
150
+ /**
151
+ * Poll call status until terminal state or timeout.
152
+ */
153
+ async pollCallStatus(callId, options = {}) {
154
+ const intervalMs = options.intervalMs ?? 2000;
155
+ const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
156
+ const deadline = Date.now() + timeoutMs;
157
+ const poll = async () => {
158
+ const call = await this.getCall(callId);
159
+ const terminal = ['completed', 'failed', 'cancelled'].includes(call.status);
160
+ if (terminal)
161
+ return call;
162
+ if (Date.now() >= deadline)
163
+ return call;
164
+ await new Promise((r) => setTimeout(r, intervalMs));
165
+ return poll();
166
+ };
167
+ return poll();
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Design tokens aligned with WEBSITE_DESIGN_SCHEMA.md (light + dark).
173
+ * Plan aliases: --pamela-primary, --pamela-background, --pamela-border-radius for host overrides.
174
+ */
175
+ const LIGHT_TOKENS = `
176
+ :root, .pamela-widget-root {
177
+ --pamela-accent: #F27A1A;
178
+ --pamela-accent-2: #F06C4F;
179
+ --pamela-accent-3: #F3A84C;
180
+ --pamela-accent-gradient: linear-gradient(90deg, var(--pamela-accent), var(--pamela-accent-2));
181
+ --pamela-primary: var(--pamela-accent);
182
+ --pamela-bg: #ffffff;
183
+ --pamela-background: var(--pamela-bg);
184
+ --pamela-surface: rgba(255, 255, 255, 0.78);
185
+ --pamela-surface-border: rgba(231, 171, 132, 0.24);
186
+ --pamela-surface-strong: rgba(255, 255, 255, 0.88);
187
+ --pamela-surface-liquid: linear-gradient(135deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.38));
188
+ --pamela-transcript-agent-bg: rgba(255, 255, 255, 0.2);
189
+ --pamela-transcript-recipient-bg: rgba(250, 147, 28, 0.2);
190
+ --pamela-text: #1C1917;
191
+ --pamela-text-secondary: rgba(28, 25, 17, 0.78);
192
+ --pamela-text-muted: rgba(28, 25, 17, 0.55);
193
+ --pamela-shadow: 0 12px 26px rgba(28, 25, 17, 0.1);
194
+ --pamela-shadow-hover: 0 24px 44px rgba(28, 25, 17, 0.18);
195
+ --pamela-glow: 0 0 32px rgba(250, 147, 28, 0.2);
196
+ --pamela-cta-bg: linear-gradient(120deg, rgba(255, 255, 255, 0.7), rgba(250, 147, 28, 0.32));
197
+ --pamela-cta-border: 1px solid rgba(250, 147, 28, 0.4);
198
+ --pamela-blur: 24px;
199
+ --pamela-radius-sm: 0.5rem;
200
+ --pamela-radius-md: 0.75rem;
201
+ --pamela-radius-lg: 1rem;
202
+ --pamela-radius-xl: 1.5rem;
203
+ --pamela-border-radius: var(--pamela-radius-md);
204
+ --pamela-focus-ring: 0 0 0 3px rgba(250, 147, 28, 0.28);
205
+ --pamela-font: 'Poppins', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
206
+ }
207
+ `;
208
+ const DARK_TOKENS = `
209
+ .pamela-dark .pamela-widget-root,
210
+ .pamela-widget-root.pamela-theme-dark,
211
+ [data-pamela-theme="dark"] .pamela-widget-root {
212
+ --pamela-bg: #171513;
213
+ --pamela-background: var(--pamela-bg);
214
+ --pamela-surface: rgba(255, 255, 255, 0.08);
215
+ --pamela-surface-border: rgba(255, 255, 255, 0.18);
216
+ --pamela-surface-strong: rgba(255, 255, 255, 0.22);
217
+ --pamela-surface-liquid: linear-gradient(150deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.05));
218
+ --pamela-transcript-agent-bg: rgba(255, 255, 255, 0.12);
219
+ --pamela-transcript-recipient-bg: rgba(250, 147, 28, 0.18);
220
+ --pamela-text: rgba(255, 255, 255, 0.95);
221
+ --pamela-text-secondary: rgba(255, 255, 255, 0.72);
222
+ --pamela-text-muted: rgba(255, 255, 255, 0.5);
223
+ --pamela-shadow: 0 16px 32px rgba(0, 0, 0, 0.5);
224
+ --pamela-shadow-hover: 0 26px 44px rgba(0, 0, 0, 0.7);
225
+ --pamela-glow: 0 0 30px rgba(250, 147, 28, 0.24);
226
+ --pamela-cta-bg: linear-gradient(120deg, rgba(255, 255, 255, 0.22), rgba(250, 147, 28, 0.34));
227
+ --pamela-cta-border: 1px solid rgba(250, 147, 28, 0.5);
228
+ --pamela-focus-ring: 0 0 0 3px rgba(250, 147, 28, 0.35);
229
+ }
230
+ `;
231
+ const WIDGET_BASE = `
232
+ .pamela-widget-root {
233
+ font-family: var(--pamela-font);
234
+ box-sizing: border-box;
235
+ }
236
+ .pamela-widget-root *,
237
+ .pamela-widget-root *::before,
238
+ .pamela-widget-root *::after {
239
+ box-sizing: border-box;
240
+ }
241
+ @keyframes pamela-pulse {
242
+ 0%, 100% { transform: scale(1); opacity: 0.9; }
243
+ 50% { transform: scale(1.05); opacity: 1; }
244
+ }
245
+ @keyframes pamela-pulse-active {
246
+ 0%, 100% { transform: scale(1); }
247
+ 50% { transform: scale(1.12); }
248
+ }
249
+ `;
250
+ let styleEl = null;
251
+ function injectStyles(theme) {
252
+ if (typeof document === 'undefined')
253
+ return;
254
+ if (styleEl && document.contains(styleEl))
255
+ return;
256
+ styleEl = document.createElement('style');
257
+ styleEl.setAttribute('data-pamela-widget', 'tokens');
258
+ styleEl.textContent = LIGHT_TOKENS + DARK_TOKENS + WIDGET_BASE;
259
+ (document.head || document.documentElement).appendChild(styleEl);
260
+ }
261
+ function getThemeClass(theme) {
262
+ return theme === 'dark' ? 'pamela-theme-dark' : '';
263
+ }
264
+ /**
265
+ * Apply custom CSS variable overrides to widget roots.
266
+ * Call after injectStyles; host can pass options.styles from init.
267
+ */
268
+ function applyStyleOverrides(overrides) {
269
+ if (typeof document === 'undefined' || !overrides || typeof overrides !== 'object')
270
+ return;
271
+ const roots = document.querySelectorAll('.pamela-widget-root');
272
+ roots.forEach((root) => {
273
+ if (!(root instanceof HTMLElement))
274
+ return;
275
+ const r = root;
276
+ if (overrides['--pamela-accent'])
277
+ r.style.setProperty('--pamela-accent', overrides['--pamela-accent']);
278
+ if (overrides['--pamela-accent-2'])
279
+ r.style.setProperty('--pamela-accent-2', overrides['--pamela-accent-2']);
280
+ if (overrides['--pamela-radius']) {
281
+ r.style.setProperty('--pamela-radius-sm', overrides['--pamela-radius']);
282
+ r.style.setProperty('--pamela-radius-md', overrides['--pamela-radius']);
283
+ r.style.setProperty('--pamela-radius-lg', overrides['--pamela-radius']);
284
+ r.style.setProperty('--pamela-border-radius', overrides['--pamela-radius']);
285
+ }
286
+ if (overrides['--pamela-font'])
287
+ r.style.setProperty('--pamela-font', overrides['--pamela-font']);
288
+ if (overrides['--pamela-primary'])
289
+ r.style.setProperty('--pamela-primary', overrides['--pamela-primary']);
290
+ if (overrides['--pamela-background']) {
291
+ r.style.setProperty('--pamela-bg', overrides['--pamela-background']);
292
+ r.style.setProperty('--pamela-background', overrides['--pamela-background']);
293
+ }
294
+ if (overrides['--pamela-text'])
295
+ r.style.setProperty('--pamela-text', overrides['--pamela-text']);
296
+ if (overrides['--pamela-border-radius'])
297
+ r.style.setProperty('--pamela-border-radius', overrides['--pamela-border-radius']);
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Inline SVG icons for the widget (no external assets)
303
+ */
304
+ const ICON_LOGO = '<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="16" cy="16" r="14" fill="url(#pamela-g)" stroke="rgba(255,255,255,0.4)" stroke-width="1.5"/><path d="M12 11v10l8-5-8-5z" fill="white"/><defs><linearGradient id="pamela-g" x1="8" y1="8" x2="24" y2="24" gradientUnits="userSpaceOnUse"><stop stop-color="#F27A1A"/><stop offset="1" stop-color="#F06C4F"/></linearGradient></defs></svg>';
305
+ const ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
306
+ const ICON_CALL = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z"/></svg>';
307
+
308
+ /**
309
+ * Phone helpers for the widget (lightweight, no external deps).
310
+ */
311
+ function normalizePhone(value) {
312
+ const digits = value.replace(/[^\d+]/g, "");
313
+ if (!digits)
314
+ return "";
315
+ if (digits.startsWith("+"))
316
+ return digits;
317
+ if (digits.length === 10)
318
+ return `+1${digits}`;
319
+ if (digits.length === 11 && digits.startsWith("1"))
320
+ return `+${digits}`;
321
+ return `+${digits}`;
322
+ }
323
+ function isValidE164(value) {
324
+ return /^\+[1-9]\d{6,14}$/.test(value);
325
+ }
326
+
327
+ function buildPanelStyles(options = {}) {
328
+ const { width, minWidth, maxWidth, padding = "1rem", borderRadiusVar = "var(--pamela-radius-lg)", gap = "0.75rem", } = options;
329
+ const base = [
330
+ "background:var(--pamela-surface-liquid);border:1px solid var(--pamela-surface-border);",
331
+ `border-radius:${borderRadiusVar};box-shadow:var(--pamela-shadow);padding:${padding};`,
332
+ "backdrop-filter:blur(var(--pamela-blur));-webkit-backdrop-filter:blur(var(--pamela-blur));",
333
+ `display:flex;flex-direction:column;gap:${gap};`,
334
+ ];
335
+ if (width)
336
+ base.unshift(`width:${width};`);
337
+ if (minWidth)
338
+ base.unshift(`min-width:${minWidth};`);
339
+ if (maxWidth)
340
+ base.unshift(`max-width:${maxWidth};`);
341
+ return base.join(" ");
342
+ }
343
+ function buildInputStyles(options = {}) {
344
+ const { padding = "0.5rem 0.75rem", fontSize = "0.875rem" } = options;
345
+ return [
346
+ `width:100%;padding:${padding};border-radius:var(--pamela-radius-md);`,
347
+ "border:1px solid var(--pamela-surface-border);background:var(--pamela-surface-strong);",
348
+ `color:var(--pamela-text);font-family:var(--pamela-font);font-size:${fontSize};`,
349
+ "transition:border-color 0.15s, box-shadow 0.15s;",
350
+ ].join(" ");
351
+ }
352
+ function buildCtaStyles(options = {}) {
353
+ const { padding = "0.6rem 1rem" } = options;
354
+ return [
355
+ `width:100%;padding:${padding};border-radius:var(--pamela-radius-md);border:var(--pamela-cta-border);`,
356
+ "background:var(--pamela-cta-bg);color:var(--pamela-text);font-weight:600;font-family:var(--pamela-font);",
357
+ "cursor:pointer;box-shadow:var(--pamela-shadow);transition:opacity 0.2s, transform 0.15s;",
358
+ ].join(" ");
359
+ }
360
+
361
+ /**
362
+ * Floating call button that expands to a mini form (phone + task + Call Now)
363
+ */
364
+ const ROOT_CLASS$1 = 'pamela-widget-root';
365
+ const WRAPPER_CLASS = 'pamela-float-wrapper';
366
+ const BUTTON_CLASS = 'pamela-float-button';
367
+ const PANEL_CLASS = 'pamela-float-panel';
368
+ const INPUT_CLASS = 'pamela-float-input';
369
+ const CTA_CLASS = 'pamela-float-cta';
370
+ class FloatingButton {
371
+ constructor(options) {
372
+ this.root = null;
373
+ this.wrapper = null;
374
+ this.panel = null;
375
+ this.expanded = false;
376
+ this.currentCall = null;
377
+ this.options = options;
378
+ }
379
+ render(container) {
380
+ injectStyles(this.options.theme);
381
+ const themeClass = getThemeClass(this.options.theme);
382
+ const root = document.createElement('div');
383
+ root.className = `${ROOT_CLASS$1} ${themeClass}`.trim();
384
+ root.setAttribute('data-pamela-float', 'true');
385
+ const positionClass = `pamela-float-${this.options.position.replace('-', '_')}`;
386
+ const wrapper = document.createElement('div');
387
+ wrapper.className = `${WRAPPER_CLASS} ${positionClass}`;
388
+ wrapper.style.cssText = this.getWrapperStyles();
389
+ const button = document.createElement('button');
390
+ button.type = 'button';
391
+ button.className = BUTTON_CLASS;
392
+ button.setAttribute('aria-label', 'Open Pamela call');
393
+ button.innerHTML = ICON_LOGO;
394
+ button.style.cssText = this.getButtonStyles();
395
+ const panel = document.createElement('div');
396
+ panel.className = PANEL_CLASS;
397
+ panel.setAttribute('hidden', 'true');
398
+ panel.style.cssText = buildPanelStyles({
399
+ minWidth: '280px',
400
+ maxWidth: '320px',
401
+ padding: '1rem',
402
+ borderRadiusVar: 'var(--pamela-radius-lg)',
403
+ });
404
+ const phoneInput = document.createElement('input');
405
+ phoneInput.type = 'tel';
406
+ phoneInput.placeholder = 'Phone number';
407
+ phoneInput.className = INPUT_CLASS;
408
+ phoneInput.setAttribute('aria-label', 'Phone number');
409
+ phoneInput.style.cssText = buildInputStyles();
410
+ const taskInput = document.createElement('textarea');
411
+ taskInput.placeholder = 'What should Pamela do?';
412
+ taskInput.rows = 2;
413
+ taskInput.className = INPUT_CLASS;
414
+ taskInput.setAttribute('aria-label', 'Task');
415
+ taskInput.style.cssText = buildInputStyles();
416
+ const statusEl = document.createElement('div');
417
+ statusEl.setAttribute('aria-live', 'polite');
418
+ statusEl.style.cssText =
419
+ 'font-size: 0.75rem; color: var(--pamela-text-muted); margin-top: 0.25rem; min-height: 1rem;';
420
+ const cta = document.createElement('button');
421
+ cta.type = 'button';
422
+ cta.className = CTA_CLASS;
423
+ cta.style.cssText = buildCtaStyles();
424
+ const ctaSpan = document.createElement('span');
425
+ ctaSpan.style.cssText = 'display:inline-flex;align-items:center;gap:0.35rem;';
426
+ ctaSpan.innerHTML = ICON_CALL;
427
+ ctaSpan.appendChild(document.createTextNode(' Call Now'));
428
+ cta.appendChild(ctaSpan);
429
+ panel.appendChild(phoneInput);
430
+ panel.appendChild(taskInput);
431
+ panel.appendChild(statusEl);
432
+ panel.appendChild(cta);
433
+ button.addEventListener('click', () => this.toggle());
434
+ cta.addEventListener('click', () => this.handleSubmit(phoneInput, taskInput, statusEl));
435
+ wrapper.appendChild(button);
436
+ wrapper.appendChild(panel);
437
+ root.appendChild(wrapper);
438
+ container.appendChild(root);
439
+ this.root = root;
440
+ this.wrapper = wrapper;
441
+ this.panel = panel;
442
+ return root;
443
+ }
444
+ getWrapperStyles() {
445
+ const base = 'position:fixed;z-index:999999;display:flex;flex-direction:column;align-items:flex-end;gap:0.5rem;';
446
+ const inset = '24px';
447
+ const pos = this.options.position;
448
+ if (pos === 'bottom-right')
449
+ return `${base} bottom:${inset};right:${inset};`;
450
+ if (pos === 'bottom-left')
451
+ return `${base} bottom:${inset};left:${inset};`;
452
+ if (pos === 'top-right')
453
+ return `${base} top:${inset};right:${inset};`;
454
+ return `${base} top:${inset};left:${inset};`;
455
+ }
456
+ getButtonStyles() {
457
+ return [
458
+ 'width:56px;height:56px;border-radius:50%;border:none;cursor:pointer;',
459
+ 'background:var(--pamela-accent-gradient);box-shadow:var(--pamela-shadow), var(--pamela-glow);',
460
+ 'display:flex;align-items:center;justify-content:center;transition:transform 0.2s, box-shadow 0.2s;',
461
+ ].join(' ');
462
+ }
463
+ toggle() {
464
+ this.expanded = !this.expanded;
465
+ if (this.panel) {
466
+ this.panel.hidden = !this.expanded;
467
+ }
468
+ if (this.expanded)
469
+ this.options.onExpand?.();
470
+ else
471
+ this.options.onCollapse?.();
472
+ }
473
+ handleSubmit(phoneInput, taskInput, statusEl) {
474
+ const to = normalizePhone(phoneInput.value.trim());
475
+ const task = taskInput.value.trim();
476
+ if (!to || !task) {
477
+ statusEl.textContent = 'Please enter phone number and task.';
478
+ return;
479
+ }
480
+ if (!isValidE164(to)) {
481
+ statusEl.textContent = 'Please use a valid phone number (e.g. +1 555 123 4567).';
482
+ return;
483
+ }
484
+ statusEl.textContent = '';
485
+ this.options.onCreateCall(to, task);
486
+ }
487
+ setCallStatus(call) {
488
+ this.currentCall = call;
489
+ if (!this.panel)
490
+ return;
491
+ const statusEl = this.panel.querySelector('[aria-live="polite"]');
492
+ if (!statusEl)
493
+ return;
494
+ if (!call) {
495
+ statusEl.textContent = '';
496
+ return;
497
+ }
498
+ if (call.status === 'completed') {
499
+ statusEl.textContent = 'Call completed.' + (call.summary ? ' ' + call.summary.slice(0, 80) + '…' : '');
500
+ }
501
+ else if (call.status === 'failed' || call.status === 'cancelled') {
502
+ statusEl.textContent = `Call ${call.status}.`;
503
+ }
504
+ else {
505
+ statusEl.textContent = `Call ${call.status}…`;
506
+ }
507
+ }
508
+ destroy() {
509
+ this.root?.remove();
510
+ this.root = null;
511
+ this.wrapper = null;
512
+ this.panel = null;
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Inline button mode: attach call buttons to elements with data-pamela-call
518
+ * Auto-initializes on DOM ready and watches for dynamically added elements.
519
+ */
520
+ const DATA_CALL = 'data-pamela-call';
521
+ const DATA_TO = 'data-to';
522
+ const DATA_TASK = 'data-task';
523
+ const DATA_VOICE = 'data-voice';
524
+ const DATA_LABEL = 'data-label';
525
+ const ATTR_INIT = 'data-pamela-inline-init';
526
+ const DEFAULT_LABEL = 'Call with Pamela';
527
+ function getStyles() {
528
+ return [
529
+ '.pamela-inline-btn{display:inline-flex;align-items:center;gap:0.35rem;padding:0.4rem 0.75rem;',
530
+ 'border-radius:var(--pamela-radius-md, 0.75rem);border:1px solid var(--pamela-surface-border, rgba(231,171,132,0.24));',
531
+ 'background:var(--pamela-cta-bg, linear-gradient(120deg, rgba(255,255,255,0.7), rgba(250,147,28,0.32)));',
532
+ 'color:var(--pamela-text, #1C1917);font-family:var(--pamela-font);font-size:0.875rem;font-weight:600;',
533
+ 'cursor:pointer;box-shadow:0 2px 8px rgba(28,25,17,0.08);transition:opacity 0.2s, transform 0.15s;}',
534
+ '.pamela-inline-btn:hover{opacity:0.95;transform:translateY(-1px);}',
535
+ '.pamela-inline-btn:disabled{opacity:0.6;cursor:not-allowed;transform:none;}',
536
+ ].join('');
537
+ }
538
+ function injectInlineStyles() {
539
+ if (typeof document === 'undefined')
540
+ return;
541
+ const id = 'pamela-inline-styles';
542
+ if (document.getElementById(id))
543
+ return;
544
+ const el = document.createElement('style');
545
+ el.id = id;
546
+ el.setAttribute('data-pamela-widget', 'inline');
547
+ el.textContent = getStyles();
548
+ (document.head || document.documentElement).appendChild(el);
549
+ }
550
+ class InlineButton {
551
+ constructor(options) {
552
+ this.observer = null;
553
+ this.styleInjected = false;
554
+ this.options = options;
555
+ }
556
+ /**
557
+ * Scan the document for [data-pamela-call] and attach buttons.
558
+ */
559
+ attach(root = document) {
560
+ if (!this.styleInjected) {
561
+ injectInlineStyles();
562
+ this.styleInjected = true;
563
+ }
564
+ const scope = root instanceof Document ? root.body : root;
565
+ if (!scope)
566
+ return;
567
+ const elements = scope.querySelectorAll(`[${DATA_CALL}]:not([${ATTR_INIT}])`);
568
+ elements.forEach((el) => this.attachToElement(el));
569
+ }
570
+ attachToElement(el) {
571
+ if (el.getAttribute(ATTR_INIT) === 'true')
572
+ return;
573
+ const to = (el.getAttribute(DATA_TO) ?? '').trim();
574
+ const task = (el.getAttribute(DATA_TASK) ?? '').trim();
575
+ const voice = (el.getAttribute(DATA_VOICE) ?? '').trim() || undefined;
576
+ const label = el.getAttribute(DATA_LABEL) ?? DEFAULT_LABEL;
577
+ if (!to || !task)
578
+ return;
579
+ el.setAttribute(ATTR_INIT, 'true');
580
+ const button = document.createElement('button');
581
+ button.type = 'button';
582
+ button.className = 'pamela-inline-btn';
583
+ button.textContent = label;
584
+ button.setAttribute('aria-label', `Call ${to} with Pamela: ${task}`);
585
+ button.addEventListener('click', (e) => {
586
+ e.preventDefault();
587
+ e.stopPropagation();
588
+ const resolvedTo = normalizePhone(to);
589
+ this.options.onCallClick({ to: resolvedTo, task, voice });
590
+ });
591
+ if (el.tagName === 'BUTTON' || el.tagName === 'A') {
592
+ el.replaceWith(button);
593
+ }
594
+ else {
595
+ el.appendChild(button);
596
+ }
597
+ }
598
+ /**
599
+ * Start watching for dynamically added [data-pamela-call] elements.
600
+ */
601
+ observe(root = document) {
602
+ const scope = root instanceof Document ? root.body : root;
603
+ if (!scope || this.observer)
604
+ return;
605
+ this.observer = new MutationObserver((mutations) => {
606
+ for (const m of mutations) {
607
+ if (m.type !== 'childList')
608
+ continue;
609
+ for (const node of m.addedNodes) {
610
+ if (node instanceof HTMLElement) {
611
+ if (node.hasAttribute?.(DATA_CALL))
612
+ this.attachToElement(node);
613
+ node.querySelectorAll?.(`[${DATA_CALL}]:not([${ATTR_INIT}])`).forEach((el) => this.attachToElement(el));
614
+ }
615
+ }
616
+ }
617
+ });
618
+ this.observer.observe(scope, { childList: true, subtree: true });
619
+ }
620
+ destroy() {
621
+ this.observer?.disconnect();
622
+ this.observer = null;
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Full-featured call modal with status + transcript display.
628
+ */
629
+ const ROOT_CLASS = 'pamela-widget-root';
630
+ class Modal {
631
+ constructor(options) {
632
+ this.root = null;
633
+ this.overlay = null;
634
+ this.panel = null;
635
+ this.orbEl = null;
636
+ this.statusEl = null;
637
+ this.transcriptEl = null;
638
+ this.summaryEl = null;
639
+ this.expanded = false;
640
+ this.boundKeydown = null;
641
+ this.options = options;
642
+ }
643
+ render(container) {
644
+ injectStyles(this.options.theme);
645
+ const themeClass = getThemeClass(this.options.theme);
646
+ const root = document.createElement('div');
647
+ root.className = `${ROOT_CLASS} ${themeClass}`.trim();
648
+ root.setAttribute('data-pamela-modal', 'true');
649
+ const overlay = document.createElement('div');
650
+ overlay.style.cssText = this.getOverlayStyles();
651
+ overlay.setAttribute('hidden', 'true');
652
+ const panel = document.createElement('div');
653
+ panel.style.cssText = buildPanelStyles({
654
+ width: 'min(520px, 95vw)',
655
+ padding: '1.25rem',
656
+ borderRadiusVar: 'var(--pamela-radius-xl)',
657
+ });
658
+ // VoiceOrb for status visualization
659
+ const orbWrapper = document.createElement('div');
660
+ orbWrapper.style.cssText = 'display:flex;justify-content:center;margin-bottom:0.75rem;';
661
+ const orb = document.createElement('div');
662
+ orb.className = 'pamela-modal-orb';
663
+ orb.style.cssText = this.getOrbStyles();
664
+ orb.setAttribute('hidden', 'true');
665
+ orbWrapper.appendChild(orb);
666
+ const header = document.createElement('div');
667
+ header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;';
668
+ const title = document.createElement('div');
669
+ title.textContent = 'Call with Pamela';
670
+ title.style.cssText = 'font-weight:600;color:var(--pamela-text);';
671
+ const closeBtn = document.createElement('button');
672
+ closeBtn.type = 'button';
673
+ closeBtn.setAttribute('aria-label', 'Close');
674
+ closeBtn.style.cssText = this.getCloseStyles();
675
+ closeBtn.innerHTML = ICON_CLOSE;
676
+ closeBtn.addEventListener('click', () => this.close());
677
+ header.appendChild(title);
678
+ header.appendChild(closeBtn);
679
+ const phoneInput = document.createElement('input');
680
+ phoneInput.type = 'tel';
681
+ phoneInput.placeholder = 'Phone number';
682
+ phoneInput.style.cssText = buildInputStyles({
683
+ padding: '0.6rem 0.75rem',
684
+ fontSize: '0.9rem',
685
+ });
686
+ phoneInput.setAttribute('aria-label', 'Phone number');
687
+ const taskInput = document.createElement('textarea');
688
+ taskInput.placeholder = 'What should Pamela do?';
689
+ taskInput.rows = 3;
690
+ taskInput.style.cssText = buildInputStyles({
691
+ padding: '0.6rem 0.75rem',
692
+ fontSize: '0.9rem',
693
+ });
694
+ taskInput.setAttribute('aria-label', 'Task');
695
+ const voiceSelect = document.createElement('select');
696
+ voiceSelect.style.cssText = this.getSelectStyles();
697
+ voiceSelect.setAttribute('aria-label', 'Voice');
698
+ ['auto', 'female', 'male'].forEach((v) => {
699
+ const opt = document.createElement('option');
700
+ opt.value = v;
701
+ opt.textContent = `Voice: ${v}`;
702
+ if (this.options.defaults?.voice && this.options.defaults.voice === v) {
703
+ opt.selected = true;
704
+ }
705
+ voiceSelect.appendChild(opt);
706
+ });
707
+ const status = document.createElement('div');
708
+ status.setAttribute('aria-live', 'polite');
709
+ status.style.cssText = 'font-size:0.8rem;color:var(--pamela-text-muted);min-height:1rem;';
710
+ const cta = document.createElement('button');
711
+ cta.type = 'button';
712
+ cta.style.cssText = buildCtaStyles({ padding: '0.65rem 1rem' });
713
+ const ctaSpan = document.createElement('span');
714
+ ctaSpan.style.cssText = 'display:inline-flex;align-items:center;gap:0.35rem;';
715
+ ctaSpan.innerHTML = ICON_CALL;
716
+ ctaSpan.appendChild(document.createTextNode(' Call Now'));
717
+ cta.appendChild(ctaSpan);
718
+ cta.addEventListener('click', () => this.handleSubmit(phoneInput, taskInput, voiceSelect, status));
719
+ const transcriptWrapper = document.createElement('div');
720
+ transcriptWrapper.style.cssText = this.getTranscriptWrapperStyles();
721
+ const transcriptTitle = document.createElement('div');
722
+ transcriptTitle.textContent = 'Transcript';
723
+ transcriptTitle.style.cssText = 'font-size:0.8rem;color:var(--pamela-text-secondary);margin-bottom:0.25rem;';
724
+ const transcript = document.createElement('div');
725
+ transcript.style.cssText = this.getTranscriptStyles();
726
+ transcriptWrapper.appendChild(transcriptTitle);
727
+ transcriptWrapper.appendChild(transcript);
728
+ const summary = document.createElement('div');
729
+ summary.style.cssText = 'font-size:0.85rem;color:var(--pamela-text);margin-top:0.5rem;';
730
+ panel.appendChild(orbWrapper);
731
+ panel.appendChild(header);
732
+ panel.appendChild(phoneInput);
733
+ panel.appendChild(taskInput);
734
+ panel.appendChild(voiceSelect);
735
+ panel.appendChild(status);
736
+ panel.appendChild(cta);
737
+ panel.appendChild(transcriptWrapper);
738
+ panel.appendChild(summary);
739
+ overlay.appendChild(panel);
740
+ root.appendChild(overlay);
741
+ container.appendChild(root);
742
+ overlay.addEventListener('click', (e) => {
743
+ if (e.target === overlay)
744
+ this.close();
745
+ });
746
+ this.boundKeydown = (e) => {
747
+ if (this.expanded && e.key === 'Escape')
748
+ this.close();
749
+ };
750
+ document.addEventListener('keydown', this.boundKeydown);
751
+ this.root = root;
752
+ this.overlay = overlay;
753
+ this.panel = panel;
754
+ this.orbEl = orb;
755
+ this.statusEl = status;
756
+ this.transcriptEl = transcript;
757
+ this.summaryEl = summary;
758
+ return root;
759
+ }
760
+ open() {
761
+ if (!this.overlay)
762
+ return;
763
+ this.overlay.removeAttribute('hidden');
764
+ this.expanded = true;
765
+ }
766
+ close() {
767
+ if (!this.overlay)
768
+ return;
769
+ this.overlay.setAttribute('hidden', 'true');
770
+ this.expanded = false;
771
+ this.options.onClose?.();
772
+ }
773
+ setCallStatus(call) {
774
+ if (!this.statusEl)
775
+ return;
776
+ if (!call) {
777
+ this.statusEl.textContent = '';
778
+ if (this.orbEl)
779
+ this.orbEl.setAttribute('hidden', 'true');
780
+ if (this.transcriptEl)
781
+ this.transcriptEl.innerHTML = '';
782
+ if (this.summaryEl)
783
+ this.summaryEl.textContent = '';
784
+ return;
785
+ }
786
+ // Show orb during active call states
787
+ const isActive = ['queued', 'ringing', 'in_progress'].includes(call.status);
788
+ if (this.orbEl) {
789
+ if (isActive) {
790
+ this.orbEl.removeAttribute('hidden');
791
+ this.orbEl.style.animation = call.status === 'in_progress' ? 'pamela-pulse-active 1s ease-in-out infinite' : 'pamela-pulse 2s ease-in-out infinite';
792
+ }
793
+ else {
794
+ this.orbEl.setAttribute('hidden', 'true');
795
+ }
796
+ }
797
+ if (call.status === 'completed') {
798
+ this.statusEl.textContent = 'Call completed.';
799
+ }
800
+ else if (call.status === 'failed' || call.status === 'cancelled') {
801
+ this.statusEl.textContent = `Call ${call.status}.`;
802
+ }
803
+ else {
804
+ this.statusEl.textContent = `Call ${call.status}...`;
805
+ }
806
+ if (this.transcriptEl && call.transcript) {
807
+ this.transcriptEl.innerHTML = '';
808
+ call.transcript.forEach((entry) => {
809
+ this.transcriptEl?.appendChild(this.renderTranscriptEntry(entry));
810
+ });
811
+ }
812
+ if (this.summaryEl && call.summary) {
813
+ this.summaryEl.textContent = `Summary: ${call.summary}`;
814
+ }
815
+ }
816
+ destroy() {
817
+ if (this.boundKeydown) {
818
+ document.removeEventListener('keydown', this.boundKeydown);
819
+ this.boundKeydown = null;
820
+ }
821
+ this.root?.remove();
822
+ this.root = null;
823
+ this.overlay = null;
824
+ this.panel = null;
825
+ this.orbEl = null;
826
+ this.statusEl = null;
827
+ this.transcriptEl = null;
828
+ this.summaryEl = null;
829
+ }
830
+ renderTranscriptEntry(entry) {
831
+ const bubble = document.createElement('div');
832
+ bubble.style.cssText = this.getTranscriptBubbleStyles(entry.role);
833
+ bubble.textContent = entry.content;
834
+ return bubble;
835
+ }
836
+ handleSubmit(phoneInput, taskInput, voiceSelect, statusEl) {
837
+ const to = normalizePhone(phoneInput.value.trim());
838
+ const task = taskInput.value.trim();
839
+ const voice = voiceSelect.value || undefined;
840
+ if (!to || !task) {
841
+ statusEl.textContent = 'Please enter phone number and task.';
842
+ return;
843
+ }
844
+ if (!isValidE164(to)) {
845
+ statusEl.textContent = 'Please use a valid phone number (e.g. +1 555 123 4567).';
846
+ return;
847
+ }
848
+ statusEl.textContent = '';
849
+ this.options.onCreateCall(to, task, voice);
850
+ }
851
+ getOverlayStyles() {
852
+ return [
853
+ 'position:fixed;inset:0;z-index:999998;background:rgba(0,0,0,0.4);',
854
+ 'display:flex;align-items:center;justify-content:center;padding:1.5rem;',
855
+ ].join(' ');
856
+ }
857
+ getSelectStyles() {
858
+ return [
859
+ 'width:100%;padding:0.5rem 0.75rem;border-radius:var(--pamela-radius-md);',
860
+ 'border:1px solid var(--pamela-surface-border);background:var(--pamela-surface-strong);',
861
+ 'color:var(--pamela-text);font-family:var(--pamela-font);font-size:0.85rem;',
862
+ ].join(' ');
863
+ }
864
+ getCloseStyles() {
865
+ return [
866
+ 'width:32px;height:32px;border-radius:50%;border:1px solid var(--pamela-surface-border);',
867
+ 'background:var(--pamela-surface-strong);color:var(--pamela-text);cursor:pointer;',
868
+ 'display:flex;align-items:center;justify-content:center;',
869
+ ].join(' ');
870
+ }
871
+ getTranscriptWrapperStyles() {
872
+ return [
873
+ 'margin-top:0.5rem;padding:0.75rem;border-radius:var(--pamela-radius-md);',
874
+ 'border:1px solid var(--pamela-surface-border);background:var(--pamela-surface);',
875
+ ].join(' ');
876
+ }
877
+ getTranscriptStyles() {
878
+ return 'display:flex;flex-direction:column;gap:0.5rem;max-height:180px;overflow:auto;';
879
+ }
880
+ getTranscriptBubbleStyles(role) {
881
+ const base = [
882
+ 'padding:0.5rem 0.75rem;border-radius:var(--pamela-radius-md);font-size:0.85rem;',
883
+ 'color:var(--pamela-text);',
884
+ ].join(' ');
885
+ if (role === 'agent') {
886
+ return base + ' background:var(--pamela-transcript-agent-bg);';
887
+ }
888
+ return base + ' background:var(--pamela-transcript-recipient-bg);';
889
+ }
890
+ getOrbStyles() {
891
+ return [
892
+ 'width:64px;height:64px;border-radius:50%;',
893
+ 'background:var(--pamela-accent-gradient);',
894
+ 'box-shadow:0 0 40px rgba(250,147,28,0.4), 0 0 60px rgba(253,139,116,0.3);',
895
+ 'animation:pamela-pulse 2s ease-in-out infinite;',
896
+ ].join(' ');
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Main Pamela widget class: init, call, events, UI modes
902
+ */
903
+ function detectTheme() {
904
+ if (typeof document === 'undefined')
905
+ return 'light';
906
+ const explicit = document.documentElement.getAttribute('data-pamela-theme');
907
+ if (explicit === 'light' || explicit === 'dark')
908
+ return explicit;
909
+ const hasDark = document.documentElement.classList.contains('dark') ||
910
+ document.body?.classList.contains('dark') ||
911
+ document.body?.classList.contains('dark-mode');
912
+ if (hasDark)
913
+ return 'dark';
914
+ if (window.matchMedia?.('(prefers-color-scheme: dark)').matches)
915
+ return 'dark';
916
+ return 'light';
917
+ }
918
+ class Pamela {
919
+ constructor() {
920
+ this.api = null;
921
+ this.options = null;
922
+ this.events = new EventEmitter();
923
+ this.floatingButton = null;
924
+ this.inlineButton = null;
925
+ this.modal = null;
926
+ this.container = null;
927
+ this.theme = 'light';
928
+ }
929
+ static init(options) {
930
+ if (Pamela.instance) {
931
+ Pamela.instance.destroy();
932
+ }
933
+ const instance = new Pamela();
934
+ instance.init(options);
935
+ Pamela.instance = instance;
936
+ return instance;
937
+ }
938
+ init(options) {
939
+ const authToken = options.accessToken || options.apiKey;
940
+ if (!authToken) {
941
+ throw new PamelaError('apiKey or accessToken is required');
942
+ }
943
+ this.options = options;
944
+ this.theme = options.theme === 'auto' ? detectTheme() : options.theme ?? 'light';
945
+ this.api = new PamelaApiClient({
946
+ authToken,
947
+ baseUrl: options.baseUrl,
948
+ });
949
+ injectStyles(this.theme);
950
+ this.container = document.body;
951
+ if (options.mode === 'floating' || (!options.mode && authToken)) {
952
+ this.floatingButton = new FloatingButton({
953
+ position: options.position ?? 'bottom-right',
954
+ theme: this.theme,
955
+ defaults: options.defaults,
956
+ onCreateCall: (to, task) => this.handleCreateCall(to, task),
957
+ onExpand: () => this.events.emit('modal:open'),
958
+ onCollapse: () => this.events.emit('modal:close'),
959
+ });
960
+ this.floatingButton.render(this.container);
961
+ }
962
+ const wantsModal = options.mode === 'modal' || options.mode === 'inline';
963
+ if (wantsModal) {
964
+ this.modal = new Modal({
965
+ theme: this.theme,
966
+ defaults: options.defaults,
967
+ onCreateCall: (to, task, voice) => this.handleCreateCall(to, task, voice),
968
+ onClose: () => this.events.emit('modal:close'),
969
+ });
970
+ this.modal.render(this.container);
971
+ }
972
+ if (options.styles && Object.keys(options.styles).length > 0) {
973
+ applyStyleOverrides(options.styles);
974
+ }
975
+ if (options.mode === 'inline') {
976
+ this.inlineButton = new InlineButton({
977
+ theme: this.theme,
978
+ onCallClick: (params) => {
979
+ this.show();
980
+ this.handleCreateCall(params.to, params.task, params.voice);
981
+ },
982
+ });
983
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
984
+ document.addEventListener('DOMContentLoaded', () => {
985
+ this.inlineButton?.attach();
986
+ this.inlineButton?.observe();
987
+ });
988
+ }
989
+ else {
990
+ this.inlineButton.attach();
991
+ this.inlineButton.observe();
992
+ }
993
+ }
994
+ }
995
+ async handleCreateCall(to, task, voice) {
996
+ const opts = this.options?.defaults ?? {};
997
+ try {
998
+ const call = await this.call({
999
+ to,
1000
+ task,
1001
+ voice: voice ?? opts.voice,
1002
+ agentName: opts.agentName,
1003
+ callerName: opts.callerName,
1004
+ maxDuration: opts.maxDuration,
1005
+ });
1006
+ this.events.emit('call:start', call);
1007
+ this.floatingButton?.setCallStatus(call);
1008
+ this.modal?.setCallStatus(call);
1009
+ const updated = await this.api.pollCallStatus(call.id);
1010
+ this.events.emit('call:update', updated);
1011
+ this.events.emit('call:complete', updated);
1012
+ this.floatingButton?.setCallStatus(updated);
1013
+ this.modal?.setCallStatus(updated);
1014
+ }
1015
+ catch (err) {
1016
+ this.events.emit('call:error', err);
1017
+ this.floatingButton?.setCallStatus(null);
1018
+ this.modal?.setCallStatus(null);
1019
+ }
1020
+ }
1021
+ call(options) {
1022
+ if (!this.api)
1023
+ throw new PamelaError('Pamela not initialized. Call Pamela.init() first.');
1024
+ return this.api.createCall(options);
1025
+ }
1026
+ getCall(callId) {
1027
+ if (!this.api)
1028
+ throw new PamelaError('Pamela not initialized. Call Pamela.init() first.');
1029
+ return this.api.getCall(callId);
1030
+ }
1031
+ cancel(callId) {
1032
+ if (!this.api)
1033
+ throw new PamelaError('Pamela not initialized. Call Pamela.init() first.');
1034
+ return this.api.cancelCall(callId);
1035
+ }
1036
+ on(event, callback) {
1037
+ this.events.on(event, callback);
1038
+ }
1039
+ off(event, callback) {
1040
+ this.events.off(event, callback);
1041
+ }
1042
+ show() {
1043
+ this.events.emit('modal:open');
1044
+ this.modal?.open();
1045
+ }
1046
+ hide() {
1047
+ this.events.emit('modal:close');
1048
+ this.modal?.close();
1049
+ }
1050
+ destroy() {
1051
+ this.floatingButton?.destroy();
1052
+ this.inlineButton?.destroy();
1053
+ this.modal?.destroy();
1054
+ this.floatingButton = null;
1055
+ this.inlineButton = null;
1056
+ this.modal = null;
1057
+ this.api = null;
1058
+ this.options = null;
1059
+ this.events.removeAllListeners();
1060
+ if (Pamela.instance === this) {
1061
+ Pamela.instance = null;
1062
+ }
1063
+ }
1064
+ static getInstance() {
1065
+ return Pamela.instance;
1066
+ }
1067
+ }
1068
+ Pamela.instance = null;
1069
+
1070
+ /**
1071
+ * @thisispamela/widget - Embeddable Pamela Voice AI widget
1072
+ *
1073
+ * Usage (script tag):
1074
+ * <script src="https://cdn.thisispamela.com/widget.js"></script>
1075
+ * <script>
1076
+ * Pamela.init({ apiKey: 'pk_live_xxx', mode: 'floating' });
1077
+ * </script>
1078
+ *
1079
+ * Usage (ESM):
1080
+ * import Pamela from '@thisispamela/widget';
1081
+ * Pamela.init({ apiKey: 'pk_live_xxx' });
1082
+ */
1083
+ // Expose Pamela on global for script-tag usage (Pamela.init())
1084
+ if (typeof globalThis !== 'undefined') {
1085
+ globalThis.Pamela = Pamela;
1086
+ }
1087
+
1088
+ exports.Pamela = Pamela;
1089
+ exports.PamelaError = PamelaError;
1090
+ exports.default = Pamela;
1091
+
1092
+ Object.defineProperty(exports, '__esModule', { value: true });
1093
+
1094
+ return exports;
1095
+
1096
+ })({});
1097
+ //# sourceMappingURL=widget.js.map