@aihumanity/voice-sdk 0.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,781 @@
1
+ import { UltravoxSession, UltravoxSessionStatus } from 'ultravox-client';
2
+
3
+ // src/VoiceCall.ts
4
+
5
+ // src/client.ts
6
+ async function fetchJoinUrl(opts) {
7
+ if (opts.fetchJoinUrl) {
8
+ return opts.fetchJoinUrl();
9
+ }
10
+ if (!opts.apiUrl) {
11
+ throw new Error("[aihumanity/voice-sdk] apiUrl is required when fetchJoinUrl is not provided.");
12
+ }
13
+ const baseUrl = opts.apiUrl.replace(/\/+$/, "");
14
+ const body = {
15
+ username: opts.username,
16
+ agentName: opts.agentName,
17
+ ...opts.extraJoinUrlBody ?? {}
18
+ };
19
+ if (opts.dataConnection) {
20
+ body.dataConnection = {
21
+ websocketUrl: opts.dataConnection.websocketUrl,
22
+ audioConfig: {
23
+ sampleRate: opts.dataConnection.audioConfig?.sampleRate ?? 16e3,
24
+ channelMode: opts.dataConnection.audioConfig?.channelMode ?? "CHANNEL_MODE_SEPARATED"
25
+ },
26
+ // Spread user-supplied flags first, then apply defaults for the two
27
+ // most common flags so they're always present if not explicitly set.
28
+ dataMessages: {
29
+ ...opts.dataConnection.dataMessages ?? {},
30
+ userStartedSpeaking: opts.dataConnection.dataMessages?.userStartedSpeaking ?? true,
31
+ userStoppedSpeaking: opts.dataConnection.dataMessages?.userStoppedSpeaking ?? true
32
+ }
33
+ };
34
+ }
35
+ if (opts.publicKey) {
36
+ const url2 = baseUrl + (opts.joinUrlPath ?? "/v1/voice/joinurl");
37
+ const res2 = await fetch(url2, {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ "X-Public-Key": opts.publicKey
42
+ },
43
+ body: JSON.stringify(body)
44
+ });
45
+ return parseJoinUrlResponse(res2);
46
+ }
47
+ const token = typeof opts.authToken === "function" ? await opts.authToken() : opts.authToken;
48
+ if (!token) {
49
+ throw new Error(
50
+ "[aihumanity/voice-sdk] Provide publicKey (for browser-direct) or authToken (for server-side)."
51
+ );
52
+ }
53
+ const path = opts.joinUrlPath ?? "/ultravox/secure/joinurl";
54
+ const url = baseUrl + path;
55
+ const res = await fetch(url, {
56
+ method: "POST",
57
+ headers: {
58
+ "Content-Type": "application/json",
59
+ Authorization: `Bearer ${token}`
60
+ },
61
+ body: JSON.stringify(body)
62
+ });
63
+ return parseJoinUrlResponse(res);
64
+ }
65
+ async function parseJoinUrlResponse(res) {
66
+ const text = await res.text();
67
+ if (!res.ok) {
68
+ throw new Error(
69
+ `[aihumanity/voice-sdk] joinUrl request failed (${res.status}): ${text}`
70
+ );
71
+ }
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(text);
75
+ } catch {
76
+ throw new Error(
77
+ `[aihumanity/voice-sdk] joinUrl response was not valid JSON: ${text}`
78
+ );
79
+ }
80
+ if (!parsed.joinUrl) {
81
+ throw new Error(
82
+ "[aihumanity/voice-sdk] joinUrl response did not include `joinUrl`."
83
+ );
84
+ }
85
+ return parsed;
86
+ }
87
+
88
+ // src/emitter.ts
89
+ var TypedEmitter = class {
90
+ constructor() {
91
+ this.listeners = /* @__PURE__ */ new Map();
92
+ }
93
+ on(event, listener) {
94
+ let set = this.listeners.get(event);
95
+ if (!set) {
96
+ set = /* @__PURE__ */ new Set();
97
+ this.listeners.set(event, set);
98
+ }
99
+ set.add(listener);
100
+ return () => this.off(event, listener);
101
+ }
102
+ off(event, listener) {
103
+ this.listeners.get(event)?.delete(listener);
104
+ }
105
+ once(event, listener) {
106
+ const off = this.on(event, ((payload) => {
107
+ off();
108
+ listener(payload);
109
+ }));
110
+ return off;
111
+ }
112
+ emit(event, payload) {
113
+ const set = this.listeners.get(event);
114
+ if (!set) return;
115
+ for (const fn of Array.from(set)) {
116
+ try {
117
+ fn(payload);
118
+ } catch (err) {
119
+ console.error("[aihumanity/voice-sdk] listener error:", err);
120
+ }
121
+ }
122
+ }
123
+ removeAllListeners() {
124
+ this.listeners.clear();
125
+ }
126
+ };
127
+
128
+ // src/types.ts
129
+ var CallStatus = /* @__PURE__ */ ((CallStatus2) => {
130
+ CallStatus2["IDLE"] = "idle";
131
+ CallStatus2["CONNECTING"] = "connecting";
132
+ CallStatus2["CONNECTED"] = "connected";
133
+ CallStatus2["LISTENING"] = "listening";
134
+ CallStatus2["THINKING"] = "thinking";
135
+ CallStatus2["SPEAKING"] = "speaking";
136
+ CallStatus2["DISCONNECTING"] = "disconnecting";
137
+ CallStatus2["DISCONNECTED"] = "disconnected";
138
+ return CallStatus2;
139
+ })(CallStatus || {});
140
+
141
+ // src/VoiceCall.ts
142
+ var DEFAULT_EMOTION_PATTERN = /\[EMOTION_CONTEXT\][^:]*:\s*(\w+)/i;
143
+ var CONTACT_SAVED_PATTERN = /I['']ve (noted|saved|got|recorded) your (contact|info|details|number|email)/i;
144
+ var LIVE_STATUSES = /* @__PURE__ */ new Set([
145
+ "connected" /* CONNECTED */,
146
+ "listening" /* LISTENING */,
147
+ "thinking" /* THINKING */,
148
+ "speaking" /* SPEAKING */
149
+ ]);
150
+ var VoiceCall = class {
151
+ constructor(opts) {
152
+ this.emitter = new TypedEmitter();
153
+ this.session = null;
154
+ this._status = "idle" /* IDLE */;
155
+ this._callId = null;
156
+ this._sessionToken = null;
157
+ this._transcripts = [];
158
+ this._lastEmotion = null;
159
+ this._contactSaved = false;
160
+ this._starting = false;
161
+ this._emotionMeta = null;
162
+ this._pollTimer = null;
163
+ this.opts = opts;
164
+ }
165
+ // ── Public read-only state ────────────────────────────────────────────────
166
+ get status() {
167
+ return this._status;
168
+ }
169
+ get callId() {
170
+ return this._callId;
171
+ }
172
+ get sessionToken() {
173
+ return this._sessionToken;
174
+ }
175
+ get transcripts() {
176
+ return this._transcripts.slice();
177
+ }
178
+ get lastEmotion() {
179
+ return this._lastEmotion;
180
+ }
181
+ get contactSaved() {
182
+ return this._contactSaved;
183
+ }
184
+ get isMicMuted() {
185
+ return this.session?.isMicMuted ?? false;
186
+ }
187
+ get isSpeakerMuted() {
188
+ return this.session?.isSpeakerMuted ?? false;
189
+ }
190
+ /** Server-reported wiring info from the join-url response, if any. */
191
+ get emotionMeta() {
192
+ return this._emotionMeta;
193
+ }
194
+ /** Underlying ultravox-client session. Use sparingly — for power users. */
195
+ get rawSession() {
196
+ return this.session;
197
+ }
198
+ // ── Event API ─────────────────────────────────────────────────────────────
199
+ on(event, listener) {
200
+ return this.emitter.on(event, listener);
201
+ }
202
+ off(event, listener) {
203
+ this.emitter.off(event, listener);
204
+ }
205
+ once(event, listener) {
206
+ return this.emitter.once(event, listener);
207
+ }
208
+ // ── Control ───────────────────────────────────────────────────────────────
209
+ /**
210
+ * Fetches a joinUrl from the backend, opens an Ultravox session, and starts
211
+ * the call. Resolves once `joinCall` has been kicked off (the call goes
212
+ * "live" asynchronously via status events).
213
+ */
214
+ async start() {
215
+ if (this._starting) return;
216
+ if (this._status !== "idle" /* IDLE */ && this._status !== "disconnected" /* DISCONNECTED */) {
217
+ return;
218
+ }
219
+ this._starting = true;
220
+ this.resetMutableState();
221
+ this.setStatus("connecting" /* CONNECTING */);
222
+ try {
223
+ const payload = await fetchJoinUrl(this.opts);
224
+ this._callId = payload.callId ?? null;
225
+ this._sessionToken = payload.sessionToken ?? null;
226
+ this._emotionMeta = payload.emotion ?? null;
227
+ this.surfaceEmotionWarnings(payload);
228
+ const session = new UltravoxSession({
229
+ audioContext: this.opts.audioContext,
230
+ additionalMessages: this.opts.additionalMessages
231
+ });
232
+ this.session = session;
233
+ this.attachSessionListeners(session);
234
+ session.joinCall(payload.joinUrl);
235
+ } catch (err) {
236
+ const e = err instanceof Error ? err : new Error(String(err));
237
+ this.emitter.emit("error", e);
238
+ this.setStatus("idle" /* IDLE */);
239
+ } finally {
240
+ this._starting = false;
241
+ }
242
+ }
243
+ /** Hangs up. Resolves when ultravox-client confirms disconnection. */
244
+ async end() {
245
+ if (!this.session) return;
246
+ this.setStatus("disconnecting" /* DISCONNECTING */);
247
+ try {
248
+ await this.session.leaveCall();
249
+ } catch (err) {
250
+ this.emitter.emit(
251
+ "error",
252
+ err instanceof Error ? err : new Error(String(err))
253
+ );
254
+ }
255
+ }
256
+ muteMic() {
257
+ this.session?.muteMic();
258
+ this.emitter.emit("mic_muted", true);
259
+ }
260
+ unmuteMic() {
261
+ this.session?.unmuteMic();
262
+ this.emitter.emit("mic_muted", false);
263
+ }
264
+ toggleMicMute() {
265
+ if (!this.session) return false;
266
+ this.session.toggleMicMute();
267
+ const muted = this.session.isMicMuted;
268
+ this.emitter.emit("mic_muted", muted);
269
+ return muted;
270
+ }
271
+ muteSpeaker() {
272
+ this.session?.muteSpeaker();
273
+ this.emitter.emit("speaker_muted", true);
274
+ }
275
+ unmuteSpeaker() {
276
+ this.session?.unmuteSpeaker();
277
+ this.emitter.emit("speaker_muted", false);
278
+ }
279
+ toggleSpeakerMute() {
280
+ if (!this.session) return false;
281
+ this.session.toggleSpeakerMute();
282
+ const muted = this.session.isSpeakerMuted;
283
+ this.emitter.emit("speaker_muted", muted);
284
+ return muted;
285
+ }
286
+ /** Sends a text message into the call (no spoken audio from the user). */
287
+ sendText(text, deferResponse = false) {
288
+ this.session?.sendText(text, deferResponse);
289
+ }
290
+ /** Sends an arbitrary data message over Ultravox's data channel. */
291
+ sendData(obj) {
292
+ this.session?.sendData(obj);
293
+ }
294
+ /** Removes all listeners and aborts any active session. */
295
+ dispose() {
296
+ this.stopEmotionPolling();
297
+ void this.session?.leaveCall().catch(() => {
298
+ });
299
+ this.session = null;
300
+ this.emitter.removeAllListeners();
301
+ this._status = "idle" /* IDLE */;
302
+ }
303
+ // ── Internals ─────────────────────────────────────────────────────────────
304
+ resetMutableState() {
305
+ this._callId = null;
306
+ this._sessionToken = null;
307
+ this._transcripts = [];
308
+ this._lastEmotion = null;
309
+ this._contactSaved = false;
310
+ this._emotionMeta = null;
311
+ this.stopEmotionPolling();
312
+ }
313
+ attachSessionListeners(session) {
314
+ session.addEventListener("status", () => this.handleStatusChange(session));
315
+ session.addEventListener("transcripts", () => this.handleTranscripts(session));
316
+ session.addEventListener(
317
+ "experimental_message",
318
+ (evt) => this.handleDataMessage(evt)
319
+ );
320
+ }
321
+ handleStatusChange(session) {
322
+ const raw = session.status;
323
+ this.emitter.emit("raw_status", String(raw));
324
+ const prevStatus = this._status;
325
+ let next;
326
+ switch (raw) {
327
+ case UltravoxSessionStatus.CONNECTING:
328
+ next = "connecting" /* CONNECTING */;
329
+ break;
330
+ case UltravoxSessionStatus.IDLE:
331
+ next = "connected" /* CONNECTED */;
332
+ break;
333
+ case UltravoxSessionStatus.LISTENING:
334
+ next = "listening" /* LISTENING */;
335
+ break;
336
+ case UltravoxSessionStatus.THINKING:
337
+ next = "thinking" /* THINKING */;
338
+ break;
339
+ case UltravoxSessionStatus.SPEAKING:
340
+ next = "speaking" /* SPEAKING */;
341
+ break;
342
+ case UltravoxSessionStatus.DISCONNECTING:
343
+ next = "disconnecting" /* DISCONNECTING */;
344
+ break;
345
+ case UltravoxSessionStatus.DISCONNECTED:
346
+ next = "disconnected" /* DISCONNECTED */;
347
+ break;
348
+ default:
349
+ next = this._status;
350
+ }
351
+ this.setStatus(next);
352
+ const wasLive = LIVE_STATUSES.has(prevStatus);
353
+ const nowLive = LIVE_STATUSES.has(next);
354
+ if (!wasLive && nowLive && this._callId && this.opts.pollEmotion) {
355
+ this.startEmotionPolling();
356
+ }
357
+ if (next === "disconnected" /* DISCONNECTED */) {
358
+ this.stopEmotionPolling();
359
+ this.session = null;
360
+ this.emitter.emit("ended", void 0);
361
+ this.setStatus("idle" /* IDLE */);
362
+ }
363
+ }
364
+ handleTranscripts(session) {
365
+ const raw = session.transcripts ?? [];
366
+ const mapped = raw.map(toPublicTranscript);
367
+ const previous = this._transcripts;
368
+ this._transcripts = mapped;
369
+ for (let i = 0; i < mapped.length; i++) {
370
+ const cur = mapped[i];
371
+ if (!cur) continue;
372
+ const prev = previous[i];
373
+ if (!prev || prev.text !== cur.text || prev.isFinal !== cur.isFinal) {
374
+ this.emitter.emit("transcript", cur);
375
+ }
376
+ }
377
+ this.emitter.emit("transcripts", mapped);
378
+ if (!this._contactSaved) {
379
+ const saved = mapped.some(
380
+ (t) => t.speaker === "agent" /* AGENT */ && t.text && CONTACT_SAVED_PATTERN.test(t.text)
381
+ );
382
+ if (saved) {
383
+ this._contactSaved = true;
384
+ this.emitter.emit("contact_saved", void 0);
385
+ }
386
+ }
387
+ }
388
+ handleDataMessage(evt) {
389
+ const anyEvt = evt;
390
+ const raw = anyEvt.message ?? anyEvt.data ?? evt;
391
+ this.emitter.emit("data_message", raw);
392
+ const text = typeof raw === "string" ? raw : safeStringify(raw);
393
+ const pattern = this.opts.emotionPattern ?? DEFAULT_EMOTION_PATTERN;
394
+ const match = text.match(pattern);
395
+ if (match && match[1]) {
396
+ const label = match[1].toLowerCase();
397
+ this._lastEmotion = label;
398
+ this.emitter.emit("emotion", { label, raw });
399
+ }
400
+ }
401
+ startEmotionPolling() {
402
+ this.stopEmotionPolling();
403
+ if (!this.opts.pollEmotion || !this._callId) return;
404
+ const callId = this._callId;
405
+ const sessionToken = this._sessionToken ?? void 0;
406
+ const interval = this.opts.emotionPollIntervalMs ?? 15e3;
407
+ const poll = async () => {
408
+ try {
409
+ const label = await this.opts.pollEmotion(callId, sessionToken);
410
+ if (label) {
411
+ const normalized = label.toLowerCase();
412
+ this._lastEmotion = normalized;
413
+ this.emitter.emit("emotion", { label: normalized, raw: label });
414
+ }
415
+ } catch {
416
+ }
417
+ };
418
+ poll();
419
+ this._pollTimer = setInterval(poll, interval);
420
+ }
421
+ stopEmotionPolling() {
422
+ if (this._pollTimer !== null) {
423
+ clearInterval(this._pollTimer);
424
+ this._pollTimer = null;
425
+ }
426
+ }
427
+ setStatus(next) {
428
+ if (next === this._status) return;
429
+ this._status = next;
430
+ this.emitter.emit("status", next);
431
+ }
432
+ surfaceEmotionWarnings(payload) {
433
+ const e = payload.emotion;
434
+ if (!e) return;
435
+ if (e.dataConnectionEnabled === false) {
436
+ this.emitter.emit(
437
+ "warning",
438
+ "dataConnection not enabled \u2014 check emotion WebSocket URL."
439
+ );
440
+ }
441
+ if (e.audioEnabled === false) {
442
+ this.emitter.emit(
443
+ "warning",
444
+ "audio not enabled \u2014 audioConfig missing on backend request."
445
+ );
446
+ }
447
+ if (e.emotionBridgeConfigured === false) {
448
+ this.emitter.emit(
449
+ "warning",
450
+ "emotionBridge not configured on the backend."
451
+ );
452
+ }
453
+ if (e.autoSendEnabled === false) {
454
+ this.emitter.emit(
455
+ "warning",
456
+ "auto-send not enabled \u2014 emotion will not feed back into the agent."
457
+ );
458
+ }
459
+ }
460
+ };
461
+ function toPublicTranscript(t) {
462
+ return {
463
+ text: t.text,
464
+ isFinal: t.isFinal,
465
+ speaker: t.speaker,
466
+ medium: t.medium,
467
+ ordinal: t.ordinal
468
+ };
469
+ }
470
+ function safeStringify(v) {
471
+ try {
472
+ return JSON.stringify(v);
473
+ } catch {
474
+ return String(v);
475
+ }
476
+ }
477
+
478
+ // src/widget.ts
479
+ var EMOTION_META = {
480
+ happy: { color: "#4ade80", bg: "rgba(74,222,128,0.10)", border: "rgba(74,222,128,0.28)", label: "Happy" },
481
+ excited: { color: "#facc15", bg: "rgba(250,204,21,0.10)", border: "rgba(250,204,21,0.28)", label: "Excited" },
482
+ joy: { color: "#4ade80", bg: "rgba(74,222,128,0.10)", border: "rgba(74,222,128,0.28)", label: "Joyful" },
483
+ confident: { color: "#6ddcff", bg: "rgba(109,220,255,0.10)", border: "rgba(109,220,255,0.28)", label: "Confident" },
484
+ neutral: { color: "#a0a0b8", bg: "rgba(160,160,184,0.08)", border: "rgba(160,160,184,0.22)", label: "Neutral" },
485
+ calm: { color: "#93c5fd", bg: "rgba(147,197,253,0.08)", border: "rgba(147,197,253,0.22)", label: "Calm" },
486
+ curious: { color: "#c4b5fd", bg: "rgba(196,181,253,0.08)", border: "rgba(196,181,253,0.22)", label: "Curious" },
487
+ sad: { color: "#818cf8", bg: "rgba(129,140,248,0.10)", border: "rgba(129,140,248,0.28)", label: "Sad" },
488
+ anxious: { color: "#fb923c", bg: "rgba(251,146,60,0.10)", border: "rgba(251,146,60,0.28)", label: "Anxious" },
489
+ stressed: { color: "#fb923c", bg: "rgba(251,146,60,0.10)", border: "rgba(251,146,60,0.28)", label: "Stressed" },
490
+ angry: { color: "#f87171", bg: "rgba(248,113,113,0.10)", border: "rgba(248,113,113,0.28)", label: "Angry" },
491
+ fear: { color: "#f87171", bg: "rgba(248,113,113,0.10)", border: "rgba(248,113,113,0.28)", label: "Fearful" },
492
+ disgust: { color: "#fb923c", bg: "rgba(251,146,60,0.10)", border: "rgba(251,146,60,0.28)", label: "Disgust" }
493
+ };
494
+ var STYLE = `
495
+ :host { all: initial; }
496
+ * { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
497
+
498
+ @keyframes spin { to { transform: rotate(360deg); } }
499
+ @keyframes pulse { 0%,100%{box-shadow:0 0 0 6px rgba(255,77,77,0.25),0 4px 20px rgba(192,57,43,0.5);} 50%{box-shadow:0 0 0 10px rgba(255,77,77,0.1),0 4px 24px rgba(192,57,43,0.6);} }
500
+ @keyframes wave { from{transform:scaleY(1);} to{transform:scaleY(2.2);} }
501
+ @keyframes pop { 0%{transform:scale(0.85);opacity:0;} 60%{transform:scale(1.06);} 100%{transform:scale(1);opacity:1;} }
502
+ @keyframes dot { 0%,100%{opacity:1;} 50%{opacity:.3;} }
503
+
504
+ .fab {
505
+ position: fixed; bottom: 28px; right: 28px; z-index: 2147483600;
506
+ width: 60px; height: 60px; border-radius: 50%; border: none; cursor: pointer;
507
+ display: flex; align-items: center; justify-content: center;
508
+ background: linear-gradient(135deg, #6ddcff, #7e60f8);
509
+ box-shadow: 0 4px 20px rgba(126,96,248,0.4);
510
+ transition: background .3s, box-shadow .3s;
511
+ }
512
+ .fab.live {
513
+ background: linear-gradient(135deg, #ff4d4d, #c0392b);
514
+ animation: pulse 2s ease-in-out infinite;
515
+ }
516
+ .fab svg { stroke: #fff; }
517
+ .spinner { width: 20px; height: 20px; border: 2px solid #2a2a3a; border-top: 2px solid #6ddcff; border-radius: 50%; animation: spin .8s linear infinite; }
518
+
519
+ .tooltip {
520
+ position: fixed; bottom: 36px; right: 98px; z-index: 2147483599;
521
+ background: #0d0d14; border: 1px solid #2a2a3a; border-radius: 10px;
522
+ padding: 6px 12px; font-size: 0.8rem; color: #e0e0e0; white-space: nowrap;
523
+ pointer-events: none;
524
+ }
525
+
526
+ .panel {
527
+ position: fixed; bottom: 100px; right: 28px; z-index: 2147483600;
528
+ width: 300px; background: #0d0d14; border: 1px solid #1e1e2e;
529
+ border-radius: 18px; box-shadow: 0 8px 40px rgba(0,0,0,0.6); overflow: hidden;
530
+ color: #fff;
531
+ }
532
+ .panel header {
533
+ background: linear-gradient(135deg, rgba(109,220,255,0.08), rgba(126,96,248,0.12));
534
+ border-bottom: 1px solid #1e1e2e; padding: 16px 20px;
535
+ display: flex; align-items: center; gap: 12px;
536
+ }
537
+ .avatar {
538
+ width: 44px; height: 44px; border-radius: 50%;
539
+ background: linear-gradient(135deg, #6ddcff, #7e60f8);
540
+ display: flex; align-items: center; justify-content: center;
541
+ font-weight: 700; color: #fff; font-size: 1rem; flex-shrink: 0;
542
+ transition: box-shadow .3s;
543
+ }
544
+ .avatar.live { box-shadow: 0 0 0 3px rgba(109,220,255,0.3); }
545
+ .title { font-weight: 700; font-size: 0.95rem; }
546
+ .subtitle { font-size: 0.72rem; color: #7e60f8; letter-spacing: 0.08em; margin-top: 1px; }
547
+ .close {
548
+ margin-left: auto; background: none; border: none; cursor: pointer;
549
+ color: #555; padding: 4px; line-height: 1;
550
+ }
551
+
552
+ .body { padding: 20px; text-align: center; }
553
+ .intro { font-size: 0.85rem; color: #a0a0b8; line-height: 1.6; margin: 0 0 20px; }
554
+ .error { font-size: 0.78rem; color: #ff6b6b; margin-bottom: 12px; }
555
+
556
+ .start-btn {
557
+ width: 100%; padding: 12px; border-radius: 12px; border: none; cursor: pointer;
558
+ background: linear-gradient(135deg, #6ddcff, #7e60f8);
559
+ color: #fff; font-weight: 700; font-size: 0.9rem;
560
+ display: flex; align-items: center; justify-content: center; gap: 8px;
561
+ }
562
+ .start-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
563
+
564
+ .busy { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 12px 0; }
565
+ .busy p { font-size: 0.85rem; color: #a0a0b8; margin: 0; }
566
+
567
+ .live-status { font-size: 0.82rem; color: #6ddcff; margin: 0 0 4px; letter-spacing: 0.04em; }
568
+
569
+ .waveform { display: flex; align-items: center; gap: 3px; height: 24px; justify-content: center; margin-bottom: 12px; }
570
+ .waveform > div {
571
+ width: 3px; border-radius: 2px;
572
+ background: linear-gradient(180deg, #6ddcff, #7e60f8);
573
+ height: 4px;
574
+ }
575
+ .waveform.active > div { animation: wave .6s ease-in-out infinite alternate; }
576
+ .waveform.active > div:nth-child(2) { animation-duration: .7s; }
577
+ .waveform.active > div:nth-child(3) { animation-duration: .8s; }
578
+ .waveform.active > div:nth-child(4) { animation-duration: .9s; }
579
+ .waveform.active > div:nth-child(5) { animation-duration: 1s; }
580
+
581
+ .badge {
582
+ display: inline-flex; align-items: center; gap: 6px;
583
+ background: rgba(109,220,255,0.08); border: 1px solid rgba(109,220,255,0.2);
584
+ border-radius: 20px; padding: 4px 12px; margin-bottom: 14px;
585
+ font-size: 0.72rem; color: #6ddcff;
586
+ }
587
+
588
+ .emotion {
589
+ display: inline-flex; align-items: center; gap: 6px;
590
+ border-radius: 20px; padding: 5px 12px; margin-bottom: 10px;
591
+ font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em;
592
+ animation: pop .35s cubic-bezier(0.34,1.56,0.64,1) both;
593
+ }
594
+ .emotion .dot { width: 6px; height: 6px; border-radius: 50%; animation: dot 1.6s ease-in-out infinite; flex-shrink: 0; }
595
+
596
+ .actions { display: flex; gap: 10px; margin-top: 12px; }
597
+ .actions button {
598
+ flex: 1; padding: 10px; border-radius: 10px; border: 1px solid #2a2a3a;
599
+ background: transparent; color: #a0a0b8; cursor: pointer;
600
+ font-size: 0.82rem; display: flex; align-items: center; justify-content: center; gap: 6px;
601
+ }
602
+ .actions .end { background: #3d1a1a; color: #ff6b6b; border: none; }
603
+ .actions .mute.muted { background: #2a2a3a; color: #6ddcff; }
604
+
605
+ footer { border-top: 1px solid #1a1a28; padding: 10px 20px; font-size: 0.7rem; color: #444; text-align: center; }
606
+
607
+ .hidden { display: none !important; }
608
+ `;
609
+ var MIC_SVG = `<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg>`;
610
+ var HANGUP_SVG = `<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.42 19.42 0 0 1 4.43 9.58 19.79 19.79 0 0 1 1.36.94a2 2 0 0 1 2-.18l3 .01a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11z"/><line x1="23" y1="1" x2="1" y2="23"/></svg>`;
611
+ var CLOSE_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
612
+ var CHECK_SVG = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>`;
613
+ var MUTE_ON_SVG = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="1" y1="1" x2="23" y2="23"/><path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/><path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"/><line x1="12" y1="19" x2="12" y2="22"/></svg>`;
614
+ var MUTE_OFF_SVG = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="19" x2="12" y2="22"/></svg>`;
615
+ function mountFloatingWidget(options) {
616
+ const persona = {
617
+ name: options.persona?.name ?? "the founder",
618
+ title: options.persona?.title ?? "AI voice assistant",
619
+ initials: options.persona?.initials ?? (options.persona?.name ?? "AI").split(/\s+/).map((s) => s[0]?.toUpperCase() ?? "").slice(0, 2).join(""),
620
+ intro: options.persona?.intro ?? `Have a real-time voice conversation with ${options.persona?.name ?? "the agent"}.`,
621
+ footer: options.persona?.footer ?? "Powered by AIHumanity \xB7 Ultravox voice AI"
622
+ };
623
+ const container = options.container ?? document.body;
624
+ const host = document.createElement("div");
625
+ host.style.position = "fixed";
626
+ host.style.zIndex = "2147483600";
627
+ container.appendChild(host);
628
+ const root = host.attachShadow({ mode: "open" });
629
+ const styleEl = document.createElement("style");
630
+ styleEl.textContent = STYLE;
631
+ root.appendChild(styleEl);
632
+ const fab = document.createElement("button");
633
+ fab.className = "fab";
634
+ fab.title = `Talk to ${persona.name}`;
635
+ fab.innerHTML = MIC_SVG;
636
+ root.appendChild(fab);
637
+ const tooltip = document.createElement("div");
638
+ tooltip.className = "tooltip";
639
+ tooltip.textContent = `Talk to ${persona.name}`;
640
+ if (options.showTooltip === false) tooltip.classList.add("hidden");
641
+ root.appendChild(tooltip);
642
+ const panel = document.createElement("div");
643
+ panel.className = "panel hidden";
644
+ panel.innerHTML = `
645
+ <header>
646
+ <div class="avatar">${escapeHtml(persona.initials)}</div>
647
+ <div>
648
+ <div class="title">${escapeHtml(persona.name)}</div>
649
+ <div class="subtitle">${escapeHtml(persona.title)}</div>
650
+ </div>
651
+ <button class="close" aria-label="Close">${CLOSE_SVG}</button>
652
+ </header>
653
+ <div class="body">
654
+ <div class="state-idle">
655
+ <p class="intro">${escapeHtml(persona.intro)}</p>
656
+ <p class="error hidden"></p>
657
+ <button class="start-btn">${MIC_SVG}<span>Start Conversation</span></button>
658
+ </div>
659
+ <div class="state-busy busy hidden"><div class="spinner"></div><p class="busy-label">Connecting\u2026</p></div>
660
+ <div class="state-live hidden">
661
+ <div class="waveform active">
662
+ <div></div><div></div><div></div><div></div><div></div>
663
+ </div>
664
+ <p class="live-status">Live \xB7 Speaking with ${escapeHtml(persona.name)}</p>
665
+ <div class="badge contact-saved hidden">${CHECK_SVG}<span>Contact info saved</span></div>
666
+ <div class="emotion hidden"></div>
667
+ <div class="actions">
668
+ <button class="mute">${MUTE_OFF_SVG}<span>Mute</span></button>
669
+ <button class="end">${HANGUP_SVG}<span>End Call</span></button>
670
+ </div>
671
+ </div>
672
+ </div>
673
+ <footer>${escapeHtml(persona.footer)}</footer>
674
+ `;
675
+ root.appendChild(panel);
676
+ const $ = (sel) => panel.querySelector(sel);
677
+ const closeBtn = $(".close");
678
+ const idleEl = $(".state-idle");
679
+ const busyEl = $(".state-busy");
680
+ const liveEl = $(".state-live");
681
+ const errorEl = $(".error");
682
+ const startBtn = $(".start-btn");
683
+ const busyLabel = $(".busy-label");
684
+ const muteBtn = $(".mute");
685
+ const endBtn = $(".end");
686
+ const savedBadge = $(".contact-saved");
687
+ const emotionEl = $(".emotion");
688
+ const avatarEl = $(".avatar");
689
+ const footerEl = panel.querySelector("footer");
690
+ const call = new VoiceCall(options);
691
+ let panelOpen = false;
692
+ let isLive = false;
693
+ function setPanelOpen(open) {
694
+ panelOpen = open;
695
+ panel.classList.toggle("hidden", !open);
696
+ if (open) tooltip.classList.add("hidden");
697
+ else if (options.showTooltip !== false && !isLive) tooltip.classList.remove("hidden");
698
+ }
699
+ function render() {
700
+ const s = call.status;
701
+ const busy = s === "connecting" /* CONNECTING */ || s === "disconnecting" /* DISCONNECTING */;
702
+ const live = s === "connected" /* CONNECTED */ || s === "listening" /* LISTENING */ || s === "thinking" /* THINKING */ || s === "speaking" /* SPEAKING */;
703
+ isLive = live;
704
+ fab.classList.toggle("live", live);
705
+ fab.title = live ? "End call" : `Talk to ${persona.name}`;
706
+ fab.innerHTML = busy ? `<div class="spinner"></div>` : live ? HANGUP_SVG : MIC_SVG;
707
+ avatarEl.classList.toggle("live", live);
708
+ idleEl.classList.toggle("hidden", busy || live);
709
+ busyEl.classList.toggle("hidden", !busy);
710
+ liveEl.classList.toggle("hidden", !live);
711
+ footerEl.classList.toggle("hidden", live || busy);
712
+ busyLabel.textContent = s === "connecting" /* CONNECTING */ ? `Connecting to ${persona.name}\u2026` : "Ending call\u2026";
713
+ }
714
+ fab.addEventListener("click", () => {
715
+ if (isLive || call.status === "connecting" /* CONNECTING */) {
716
+ void call.end();
717
+ } else {
718
+ setPanelOpen(!panelOpen);
719
+ }
720
+ });
721
+ closeBtn.addEventListener("click", () => setPanelOpen(false));
722
+ startBtn.addEventListener("click", () => {
723
+ setPanelOpen(true);
724
+ errorEl.classList.add("hidden");
725
+ void call.start();
726
+ });
727
+ endBtn.addEventListener("click", () => void call.end());
728
+ muteBtn.addEventListener("click", () => call.toggleMicMute());
729
+ call.on("status", () => render());
730
+ call.on("error", (err) => {
731
+ errorEl.textContent = err.message;
732
+ errorEl.classList.remove("hidden");
733
+ });
734
+ call.on("contact_saved", () => savedBadge.classList.remove("hidden"));
735
+ call.on("emotion", ({ label }) => {
736
+ const meta = EMOTION_META[label] ?? {
737
+ color: "#a0a0b8",
738
+ bg: "rgba(160,160,184,0.08)",
739
+ border: "rgba(160,160,184,0.22)",
740
+ label: label.charAt(0).toUpperCase() + label.slice(1)
741
+ };
742
+ emotionEl.classList.remove("hidden");
743
+ emotionEl.style.background = meta.bg;
744
+ emotionEl.style.border = `1px solid ${meta.border}`;
745
+ emotionEl.style.color = meta.color;
746
+ emotionEl.innerHTML = `<span class="dot" style="background:${meta.color}"></span>${escapeHtml(meta.label)}`;
747
+ });
748
+ call.on("mic_muted", (m) => {
749
+ muteBtn.classList.toggle("muted", m);
750
+ muteBtn.innerHTML = `${m ? MUTE_ON_SVG : MUTE_OFF_SVG}<span>${m ? "Unmute" : "Mute"}</span>`;
751
+ });
752
+ call.on("ended", () => {
753
+ savedBadge.classList.add("hidden");
754
+ emotionEl.classList.add("hidden");
755
+ muteBtn.classList.remove("muted");
756
+ muteBtn.innerHTML = `${MUTE_OFF_SVG}<span>Mute</span>`;
757
+ });
758
+ render();
759
+ return {
760
+ open() {
761
+ setPanelOpen(true);
762
+ void call.start();
763
+ },
764
+ close() {
765
+ void call.end();
766
+ setPanelOpen(false);
767
+ },
768
+ destroy() {
769
+ call.dispose();
770
+ host.remove();
771
+ },
772
+ call
773
+ };
774
+ }
775
+ function escapeHtml(s) {
776
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
777
+ }
778
+
779
+ export { CallStatus, VoiceCall, mountFloatingWidget };
780
+ //# sourceMappingURL=widget.js.map
781
+ //# sourceMappingURL=widget.js.map