@vanira/sdk 0.0.82

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.
@@ -0,0 +1,1540 @@
1
+ var ne = Object.defineProperty;
2
+ var ae = (s, e, t) => e in s ? ne(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t;
3
+ var l = (s, e, t) => ae(s, typeof e != "symbol" ? e + "" : e, t);
4
+ class ie {
5
+ constructor(e) {
6
+ l(this, "el");
7
+ this.el = new Audio(), this.el.srcObject = e, this.el.play().catch(
8
+ (t) => {
9
+ }
10
+ );
11
+ }
12
+ pause() {
13
+ }
14
+ cleanup() {
15
+ this.el.pause(), this.el.srcObject = null;
16
+ }
17
+ onEnded(e) {
18
+ this.el.onended = e;
19
+ }
20
+ }
21
+ class se {
22
+ createRemotePlayer(e) {
23
+ return new ie(e);
24
+ }
25
+ }
26
+ class re {
27
+ async getUserAudio(e) {
28
+ return navigator.mediaDevices.getUserMedia({ audio: e });
29
+ }
30
+ stopStream(e) {
31
+ e.getTracks().forEach((t) => t.stop());
32
+ }
33
+ }
34
+ class oe {
35
+ create(e) {
36
+ return new RTCPeerConnection({
37
+ iceServers: e.iceServers,
38
+ iceTransportPolicy: e.iceTransportPolicy ?? "all"
39
+ });
40
+ }
41
+ }
42
+ const ce = /* @__PURE__ */ new Set([
43
+ "client_tool_call",
44
+ "clearAudio",
45
+ "client_tool_cancel"
46
+ ]);
47
+ function X(s) {
48
+ if (!s || typeof s != "object") return !1;
49
+ const e = s.event;
50
+ return typeof e == "string" && ce.has(e);
51
+ }
52
+ class le {
53
+ bind(e, t) {
54
+ return e.onopen = () => t.onOpen(), e.onerror = (n) => t.onError(n), e.onmessage = (n) => {
55
+ if (typeof n.data == "string") {
56
+ t.onMessage({ text: n.data });
57
+ return;
58
+ }
59
+ if (n.data instanceof ArrayBuffer) {
60
+ try {
61
+ const a = new TextDecoder().decode(n.data), i = JSON.parse(a);
62
+ X(i) && t.onMessage({ text: a });
63
+ } catch {
64
+ }
65
+ return;
66
+ }
67
+ n.data instanceof Blob && n.data.text().then((a) => {
68
+ t.onMessage({ text: a });
69
+ }).catch((a) => {
70
+ });
71
+ }, {
72
+ send(n) {
73
+ e.readyState === "open" && e.send(n);
74
+ },
75
+ isOpen() {
76
+ return e.readyState === "open";
77
+ },
78
+ close() {
79
+ e.close();
80
+ }
81
+ };
82
+ }
83
+ }
84
+ const V = "https://api.vanira.io", p = {
85
+ Form: "vanira_form",
86
+ Calendar: "vanira_calendar",
87
+ Navigate: "vanira_navigate",
88
+ Upload: "vanira_upload",
89
+ Camera: "vanira_camera",
90
+ HighlightElement: "vanira_highlight_element",
91
+ ClickElement: "vanira_click_element",
92
+ SelectOption: "vanira_select_option",
93
+ SetDate: "vanira_set_date",
94
+ TypeText: "vanira_type_text",
95
+ EraseText: "vanira_erase_text",
96
+ Draw: "vanira_draw",
97
+ EraseDraw: "vanira_erase_draw",
98
+ LiveVision: "vanira_live_vision",
99
+ CloseLiveCamera: "vanira_close_live_camera",
100
+ LiveScreen: "vanira_live_screen",
101
+ CloseLiveScreen: "vanira_close_live_screen",
102
+ ClipRegion: "vanira_clip_region",
103
+ TabScreenshot: "vanira_tab_screenshot",
104
+ ScreenInk: "vanira_screen_ink",
105
+ ScreenCursor: "vanira_screen_cursor",
106
+ ScreenClick: "vanira_screen_click",
107
+ ClearScreenOverlay: "vanira_clear_screen_overlay",
108
+ PageScan: "vanira_page_scan",
109
+ PointAt: "vanira_point_at",
110
+ OutlineTargets: "vanira_outline_targets",
111
+ ClearGuide: "vanira_clear_guide"
112
+ }, de = Object.values(p), he = new Set(de);
113
+ function J(s) {
114
+ return !!s && he.has(s);
115
+ }
116
+ const ue = /* @__PURE__ */ new Set([
117
+ p.ScreenInk,
118
+ p.ScreenCursor,
119
+ p.ScreenClick,
120
+ p.ClearScreenOverlay,
121
+ p.PointAt,
122
+ p.OutlineTargets,
123
+ p.ClearGuide
124
+ ]);
125
+ [
126
+ ...ue,
127
+ p.PageScan,
128
+ p.TabScreenshot
129
+ ];
130
+ function Z(s) {
131
+ if (typeof s == "string")
132
+ try {
133
+ return JSON.parse(s);
134
+ } catch {
135
+ return {};
136
+ }
137
+ return s && typeof s == "object" ? s : {};
138
+ }
139
+ const W = {
140
+ type_on_whiteboard: p.TypeText,
141
+ write_on_whiteboard: p.TypeText,
142
+ write_on_white_board: p.TypeText,
143
+ dom_typing_simulation: p.TypeText,
144
+ erase_text: p.EraseText,
145
+ dom_erase: p.EraseText,
146
+ draw_on_board: p.Draw,
147
+ erase_drawing_board: p.EraseDraw,
148
+ open_live_camera: p.LiveVision,
149
+ live_vision: p.LiveVision,
150
+ close_live_camera: p.CloseLiveCamera,
151
+ stop_live_camera: p.CloseLiveCamera,
152
+ open_live_screen: p.LiveScreen,
153
+ close_live_screen: p.CloseLiveScreen,
154
+ screen_ink: p.ScreenInk,
155
+ screen_cursor: p.ScreenCursor,
156
+ screen_click: p.ScreenClick,
157
+ clear_screen_overlay: p.ClearScreenOverlay,
158
+ tab_screenshot: p.TabScreenshot,
159
+ capture_tab: p.TabScreenshot,
160
+ capture_tab_screenshot: p.TabScreenshot,
161
+ screenshot_tab: p.TabScreenshot,
162
+ page_scan: p.PageScan,
163
+ scan_page: p.PageScan,
164
+ point_at: p.PointAt,
165
+ outline_targets: p.OutlineTargets,
166
+ outline_elements: p.OutlineTargets,
167
+ clear_guide: p.ClearGuide
168
+ };
169
+ function G(s, e) {
170
+ const t = s.text_to_type ?? (e == null ? void 0 : e.text_to_type) ?? s.text ?? (e == null ? void 0 : e.text) ?? s.value ?? (e == null ? void 0 : e.value);
171
+ return typeof t == "string" && t.trim().length > 0;
172
+ }
173
+ function pe(s, e) {
174
+ const t = s.fields ?? (e == null ? void 0 : e.fields);
175
+ return Array.isArray(t) ? t.length > 0 : typeof t == "string" ? t.split(",").map((n) => n.trim()).filter(Boolean).length > 0 : !1;
176
+ }
177
+ function _e(s, e) {
178
+ const t = s == null ? void 0 : s.preset_id;
179
+ if (typeof t == "string" && J(t))
180
+ return t;
181
+ const a = Z(e).preset_id;
182
+ if (typeof a == "string" && J(a))
183
+ return a;
184
+ }
185
+ function fe(s, e, t) {
186
+ const n = Z(e);
187
+ let a = _e(s, n);
188
+ if (a === p.Form && G(n, s) && !pe(n, s))
189
+ return p.TypeText;
190
+ if (a) return a;
191
+ const i = String(t ?? "").trim().toLowerCase();
192
+ if (i && W[i])
193
+ return W[i];
194
+ if (G(n, s))
195
+ return p.TypeText;
196
+ }
197
+ const U = "vanira_latest_call_id", B = "vanira_prospect_id";
198
+ function x(s) {
199
+ return `vanira_latest_call_id_${s}`;
200
+ }
201
+ function M(s) {
202
+ return `vanira_prospect_id_${s}`;
203
+ }
204
+ function Q(s) {
205
+ return `vanira_call_meta_${s}`;
206
+ }
207
+ function C(s, e) {
208
+ if (!s) return null;
209
+ try {
210
+ return s.getItem(e);
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+ function z(s) {
216
+ if (typeof window > "u" || !s) return null;
217
+ const e = [], t = C(window.localStorage, Q(s));
218
+ if (t)
219
+ try {
220
+ const c = JSON.parse(t);
221
+ c != null && c.callId && e.push({
222
+ callId: c.callId,
223
+ prospectId: c.prospectId,
224
+ updatedAt: c.updatedAt || 0
225
+ });
226
+ } catch {
227
+ }
228
+ const n = (c, r, h) => {
229
+ c && e.push({
230
+ callId: c,
231
+ prospectId: r || void 0,
232
+ updatedAt: h
233
+ });
234
+ }, a = C(window.localStorage, M(s)), i = C(window.sessionStorage, M(s)), o = C(window.localStorage, B);
235
+ return n(
236
+ C(window.localStorage, x(s)),
237
+ a || o,
238
+ 2
239
+ ), n(
240
+ C(window.sessionStorage, x(s)),
241
+ i || o,
242
+ 1
243
+ ), n(C(window.localStorage, U), o, 0), n(C(window.sessionStorage, U), o, 0), e.length ? (e.sort((c, r) => r.updatedAt - c.updatedAt), e[0]) : null;
244
+ }
245
+ function me(s, e, t) {
246
+ var a, i, o, c, r, h, u, d, _;
247
+ if (typeof window > "u" || !s || !e) return;
248
+ const n = {
249
+ callId: e,
250
+ prospectId: t,
251
+ updatedAt: Date.now()
252
+ };
253
+ try {
254
+ (a = window.localStorage) == null || a.setItem(Q(s), JSON.stringify(n)), (i = window.localStorage) == null || i.setItem(x(s), e), (o = window.sessionStorage) == null || o.setItem(x(s), e), t && ((c = window.localStorage) == null || c.setItem(M(s), t), (r = window.sessionStorage) == null || r.setItem(M(s), t)), (h = window.localStorage) == null || h.setItem(U, e), (u = window.sessionStorage) == null || u.setItem(U, e), t && ((d = window.localStorage) == null || d.setItem(B, t), (_ = window.sessionStorage) == null || _.setItem(B, t));
255
+ } catch {
256
+ }
257
+ }
258
+ const $ = 16 * 1024, we = 256 * 1024;
259
+ function D(s, e) {
260
+ return (s == null ? void 0 : s.readyState) === "open" && (e == null ? void 0 : e.readyState) === "open";
261
+ }
262
+ function Se(s, e) {
263
+ return {
264
+ control: (s == null ? void 0 : s.readyState) ?? "missing",
265
+ mediaBytes: (e == null ? void 0 : e.readyState) ?? "missing"
266
+ };
267
+ }
268
+ async function ye(s, e, t = 8e3) {
269
+ if (D(s, e))
270
+ return !0;
271
+ const n = Date.now() + t;
272
+ for (; Date.now() < n; ) {
273
+ if (D(s, e))
274
+ return !0;
275
+ await new Promise((a) => setTimeout(a, 50));
276
+ }
277
+ return D(s, e);
278
+ }
279
+ async function Y(s) {
280
+ for (; s.bufferedAmount > we; )
281
+ await new Promise((e) => setTimeout(e, 10));
282
+ }
283
+ async function ve(s) {
284
+ const {
285
+ bytes: e,
286
+ filename: t,
287
+ contentType: n,
288
+ reason: a,
289
+ message: i,
290
+ callId: o,
291
+ mediaBytesChannel: c,
292
+ sendControlEvent: r,
293
+ uploadId: h = crypto.randomUUID(),
294
+ uploadKind: u = "media"
295
+ } = s, d = e.byteLength, _ = d === 0 ? 0 : Math.ceil(d / $);
296
+ r("client_media_upload_start", {
297
+ data: {
298
+ upload_id: h,
299
+ call_id: o,
300
+ reason: a,
301
+ message: i,
302
+ filename: t,
303
+ content_type: n,
304
+ size: d,
305
+ chunk_count: _,
306
+ upload_kind: u
307
+ }
308
+ });
309
+ const f = new Uint8Array(e);
310
+ for (let m = 0; m < d; m += $) {
311
+ await Y(c);
312
+ const S = Math.min(m + $, d);
313
+ c.send(f.subarray(m, S));
314
+ }
315
+ return await Y(c), r("client_media_upload_complete", {
316
+ data: {
317
+ upload_id: h,
318
+ call_id: o,
319
+ reason: a,
320
+ message: i,
321
+ chunk_count: _,
322
+ upload_kind: u
323
+ }
324
+ }), { media_id: h, url: "", content_type: n };
325
+ }
326
+ async function ge(s) {
327
+ return typeof s.arrayBuffer == "function" ? s.arrayBuffer() : new Response(s).arrayBuffer();
328
+ }
329
+ function Ce(s) {
330
+ return null;
331
+ }
332
+ function O() {
333
+ return typeof window > "u" ? "/" : `${window.location.pathname}${window.location.search}${window.location.hash}` || "/";
334
+ }
335
+ class Ie {
336
+ constructor(e) {
337
+ l(this, "active", !1);
338
+ l(this, "initialSent", !1);
339
+ l(this, "lastRoute", "");
340
+ l(this, "sendContext");
341
+ l(this, "isReady");
342
+ l(this, "cleanupFns", []);
343
+ l(this, "initialRetryTimer", null);
344
+ l(this, "routePollTimer", null);
345
+ l(this, "originalPushState", null);
346
+ l(this, "originalReplaceState", null);
347
+ this.sendContext = e.sendContext, this.isReady = e.isReady ?? (() => !0);
348
+ }
349
+ start() {
350
+ typeof window > "u" || this.active || (this.active = !0, this.initialSent = !1, this.lastRoute = O(), this.attachListeners(), this.scheduleInitialContextSend());
351
+ }
352
+ stop() {
353
+ if (!(!this.active && this.cleanupFns.length === 0)) {
354
+ this.active = !1, this.initialSent = !1, this.initialRetryTimer !== null && (clearTimeout(this.initialRetryTimer), this.initialRetryTimer = null), this.routePollTimer !== null && (clearInterval(this.routePollTimer), this.routePollTimer = null);
355
+ for (const e of this.cleanupFns)
356
+ try {
357
+ e();
358
+ } catch {
359
+ }
360
+ this.cleanupFns.length = 0, this.originalPushState && (history.pushState = this.originalPushState, this.originalPushState = null), this.originalReplaceState && (history.replaceState = this.originalReplaceState, this.originalReplaceState = null);
361
+ }
362
+ }
363
+ /** Call when the WebRTC data channel opens (initial context may have been deferred). */
364
+ onChannelReady() {
365
+ this.scheduleInitialContextSend();
366
+ }
367
+ scheduleInitialContextSend(e = 0) {
368
+ if (!(!this.active || this.initialSent)) {
369
+ if (this.isReady()) {
370
+ this.sendRouteContext("call_started"), this.initialSent = !0;
371
+ return;
372
+ }
373
+ e >= 30 || (this.initialRetryTimer = window.setTimeout(
374
+ () => this.scheduleInitialContextSend(e + 1),
375
+ 100
376
+ ));
377
+ }
378
+ }
379
+ attachListeners() {
380
+ const e = (i, o = 0) => {
381
+ const c = () => {
382
+ const r = O();
383
+ r !== this.lastRoute && this.handleRouteChange(r, i);
384
+ };
385
+ o > 0 ? window.setTimeout(c, o) : queueMicrotask(c);
386
+ }, t = () => e("popstate"), n = () => e("hashchange"), a = (i) => {
387
+ const o = i.detail;
388
+ if ((o == null ? void 0 : o.initiated_by) === "agent") {
389
+ e("vanira_navigate_agent", 120);
390
+ return;
391
+ }
392
+ e("vanira_navigate", 120);
393
+ };
394
+ window.addEventListener("popstate", t), window.addEventListener("hashchange", n), window.addEventListener("vanira:navigate", a), this.cleanupFns.push(
395
+ () => window.removeEventListener("popstate", t),
396
+ () => window.removeEventListener("hashchange", n),
397
+ () => window.removeEventListener("vanira:navigate", a)
398
+ ), this.originalPushState = history.pushState.bind(history), this.originalReplaceState = history.replaceState.bind(history), history.pushState = (...i) => {
399
+ this.originalPushState(...i), e("pushState");
400
+ }, history.replaceState = (...i) => {
401
+ this.originalReplaceState(...i), e("replaceState");
402
+ }, this.routePollTimer = window.setInterval(() => {
403
+ if (!this.active) return;
404
+ const i = O();
405
+ i !== this.lastRoute && this.handleRouteChange(i, "location_poll");
406
+ }, 1e3);
407
+ }
408
+ handleRouteChange(e, t) {
409
+ if (!this.active) return;
410
+ const n = this.lastRoute;
411
+ if (n === e) return;
412
+ if (this.lastRoute = e, !this.initialSent) {
413
+ this.scheduleInitialContextSend();
414
+ return;
415
+ }
416
+ !this.isReady() || Ce() || this.sendRouteContext("route_changed", n, t, "user");
417
+ }
418
+ sendRouteContext(e, t, n, a = "user") {
419
+ const i = O();
420
+ this.lastRoute = i;
421
+ const o = {
422
+ ui_state: "page_navigation",
423
+ navigation_led_by: a,
424
+ current_route: i,
425
+ current_page: i,
426
+ user_action: e === "call_started" ? "call_started_on_page" : "user_navigated"
427
+ };
428
+ e === "call_started" ? o.message = `User started the call while viewing ${i}.` : (o.previous_route = t ?? "", o.message = `User navigated from ${t || "(unknown)"} to ${i}.`, n && (o.navigation_source = n));
429
+ try {
430
+ this.sendContext(o);
431
+ } catch {
432
+ }
433
+ }
434
+ }
435
+ const y = class y {
436
+ constructor(e) {
437
+ l(this, "serverUrl");
438
+ l(this, "agentId");
439
+ l(this, "callId");
440
+ l(this, "prospectId");
441
+ l(this, "apiKey");
442
+ l(this, "backendUrl");
443
+ l(this, "token");
444
+ l(this, "onConnected");
445
+ l(this, "onDisconnected");
446
+ l(this, "onError");
447
+ l(this, "onTranscription");
448
+ l(this, "onLocalStream");
449
+ // @ts-ignore
450
+ l(this, "onRemoteTrack");
451
+ l(this, "onClientToolCall");
452
+ l(this, "onSessionStarted");
453
+ l(this, "sessionStartedEmitted", !1);
454
+ l(this, "pc", null);
455
+ l(this, "dataChannel", null);
456
+ l(this, "mediaBytesChannel", null);
457
+ l(this, "audioElement", null);
458
+ l(this, "localStream", null);
459
+ l(this, "micMuted", !1);
460
+ l(this, "connected", !1);
461
+ l(this, "icePollInterval", null);
462
+ l(this, "appliedRemoteCandidates", /* @__PURE__ */ new Set());
463
+ l(this, "iceServers");
464
+ l(this, "callSessionLoaded", !1);
465
+ l(this, "connectGeneration", 0);
466
+ l(this, "runtime");
467
+ l(this, "sessionBehavior");
468
+ l(this, "routeTracker", null);
469
+ l(this, "_status", "idle");
470
+ l(this, "listeners", /* @__PURE__ */ new Map());
471
+ /** Live vision config — auto-starts camera on connect when scope is full_call. */
472
+ l(this, "liveVision");
473
+ l(this, "liveVisionAutoStarted", !1);
474
+ if (!e.agentId) throw new Error("agentId is required");
475
+ const t = !!(e.apiKey || e.token), n = !!(e.serverUrl && e.callId), a = y.normalizeIceServers(
476
+ e.iceServers
477
+ );
478
+ if (!t && !n)
479
+ throw new Error(
480
+ "apiKey or token is required — or pass serverUrl and callId from a prior POST /calls/create"
481
+ );
482
+ this.agentId = e.agentId, this.sessionBehavior = e.sessionBehavior, this.serverUrl = (e.serverUrl || "").replace(/\/$/, ""), this.apiKey = e.apiKey, this.token = e.token, this.backendUrl = (e.backendUrl || V).replace(/\/$/, ""), a.length && (this.iceServers = a), n && (this.callSessionLoaded = !0);
483
+ const i = typeof window < "u";
484
+ this.prospectId = e.prospectId;
485
+ const o = e.sessionBehavior === "continue";
486
+ if (o && i && !e.callId) {
487
+ const r = z(this.agentId);
488
+ r && (e.callId || (this.callId = r.callId), !this.prospectId && r.prospectId && (this.prospectId = r.prospectId));
489
+ }
490
+ if (!this.prospectId && i)
491
+ try {
492
+ const r = z(this.agentId);
493
+ this.prospectId = (r == null ? void 0 : r.prospectId) || window.sessionStorage && window.sessionStorage.getItem("vanira_prospect_id") || window.localStorage && window.localStorage.getItem("vanira_prospect_id") || void 0;
494
+ } catch {
495
+ }
496
+ e.callId ? this.callId = e.callId : o || (this.callId = void 0);
497
+ const c = e.onSessionStarted;
498
+ this.onSessionStarted = (r) => {
499
+ c == null || c(r), this.emit("session_started", r);
500
+ }, this.liveVision = e.liveVision, this.onConnected = () => {
501
+ var r;
502
+ this._status = "connected", (r = e.onConnected) == null || r.call(e), this.emit("connected"), this.maybeAutoStartLiveCamera();
503
+ }, this.onDisconnected = () => {
504
+ var r;
505
+ this._status = "disconnected", (r = e.onDisconnected) == null || r.call(e), this.emit("disconnected");
506
+ }, this.onError = (r) => {
507
+ var u;
508
+ this._status = "error";
509
+ const h = typeof r == "string" ? r : (r == null ? void 0 : r.message) || "Connection error";
510
+ (u = e.onError) == null || u.call(e, r), this.emit("error", h);
511
+ }, this.onTranscription = (r, h) => {
512
+ var u;
513
+ (u = e.onTranscription) == null || u.call(e, r, h), this.emit("transcription", { text: r, isFinal: h });
514
+ }, this.onLocalStream = e.onLocalStream || (() => {
515
+ }), this.onRemoteTrack = (r, h) => {
516
+ var u;
517
+ (u = e.onRemoteTrack) == null || u.call(e, r, h), this.emit("track", { track: r, stream: h });
518
+ }, this.onClientToolCall = (r) => {
519
+ var d;
520
+ (d = e.onClientToolCall) == null || d.call(e, r);
521
+ const h = (r == null ? void 0 : r.data) || r, u = {
522
+ name: String(h.name || h.tool_name || ""),
523
+ arguments: h.arguments || h.args || {},
524
+ tool_call_id: String(h.tool_call_id || h.call_id || ""),
525
+ execution_mode: h.execution_mode === "blocking" ? "blocking" : "fire_and_forget",
526
+ client_fields: h.client_fields || {}
527
+ };
528
+ u.name && u.tool_call_id && this.emit("tool_call", u);
529
+ }, this.runtime = e.runtime, typeof window < "u" && e.trackRouteContext !== !1 && (this.routeTracker = new Ie({
530
+ sendContext: (r) => this.sendContextUpdate(r),
531
+ isReady: () => {
532
+ var r;
533
+ return ((r = this.dataChannel) == null ? void 0 : r.readyState) === "open";
534
+ }
535
+ }));
536
+ }
537
+ /**
538
+ * Create a call via POST /calls/create, then connect over WebRTC.
539
+ * Alias for connect() when using apiKey.
540
+ */
541
+ async createCall() {
542
+ return this.connect();
543
+ }
544
+ /**
545
+ * Connect to the agent (Talk to Agent — two phases).
546
+ *
547
+ * Phase 1: POST /calls/create → call_id, worker_url, ice_servers (fires onSessionStarted).
548
+ * Phase 2: getUserMedia → WebRTC offer to worker_url → ICE trickle → live audio (fires onConnected).
549
+ *
550
+ * Must be called from a user gesture (button click) for microphone access.
551
+ * onSessionStarted means provisioned only — wait for onConnected before the call is live.
552
+ */
553
+ async connect() {
554
+ var t;
555
+ this._status = "connecting";
556
+ const e = ++this.connectGeneration;
557
+ if (await this.loadCallSession(), !this.isConnectCancelled(e)) {
558
+ if (!this.serverUrl)
559
+ throw new Error("[VaniraClient] worker_url missing from /calls/create");
560
+ if (!this.callId)
561
+ throw new Error("[VaniraClient] call_id missing — must come from POST /calls/create");
562
+ if (this.onSessionStarted && this.prospectId && this.callId && !this.sessionStartedEmitted)
563
+ try {
564
+ this.onSessionStarted({ prospectId: this.prospectId, callId: this.callId, serverUrl: this.serverUrl }), this.sessionStartedEmitted = !0;
565
+ } catch {
566
+ }
567
+ try {
568
+ this.appliedRemoteCandidates.clear(), this.stopRemoteIcePolling();
569
+ const n = this.getIceServers();
570
+ this.pc = new RTCPeerConnection({
571
+ iceServers: n,
572
+ iceTransportPolicy: "all"
573
+ });
574
+ let a;
575
+ try {
576
+ a = await navigator.mediaDevices.getUserMedia({
577
+ audio: {
578
+ echoCancellation: !0,
579
+ noiseSuppression: !0,
580
+ autoGainControl: !0,
581
+ sampleRate: { ideal: 16e3 },
582
+ channelCount: 1
583
+ }
584
+ });
585
+ } catch (d) {
586
+ if (d.name === "NotAllowedError" || d.name === "PermissionDeniedError" || (t = d.message) != null && t.includes("Permission denied")) {
587
+ const _ = navigator.userAgent, f = /iPad|iPhone|iPod/.test(_) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
588
+ let m = "Microphone access denied. Please allow microphone access in your browser settings.";
589
+ f && (_.includes("CriOS") ? m = "Microphone access blocked. Please enable it in iOS Settings > Chrome > Microphone, then reload the page." : _.includes("FxiOS") ? m = "Microphone access blocked. Please enable it in iOS Settings > Firefox > Microphone, then reload the page." : m = "Microphone access blocked. Please enable it in iOS Settings > Safari > Microphone (or tap 'aA' > Website Settings > Allow Microphone).");
590
+ const S = new Error(m);
591
+ throw S.name = d.name, S;
592
+ }
593
+ throw d;
594
+ }
595
+ if (this.isConnectCancelled(e)) {
596
+ a.getTracks().forEach((d) => d.stop());
597
+ return;
598
+ }
599
+ if (this.localStream = a, !this.pc || this.isConnectCancelled(e)) {
600
+ a.getTracks().forEach((d) => d.stop());
601
+ return;
602
+ }
603
+ if (this.onLocalStream(a), a.getTracks().forEach((d) => {
604
+ var _;
605
+ (_ = this.pc) == null || _.addTrack(d, a);
606
+ }), !this.pc)
607
+ throw new Error("RTCPeerConnection was closed unexpectedly");
608
+ this.dataChannel = this.pc.createDataChannel("control"), this.dataChannel.onopen = () => {
609
+ var d;
610
+ (d = this.routeTracker) == null || d.onChannelReady();
611
+ }, this.dataChannel.onmessage = (d) => {
612
+ if (typeof d.data == "string")
613
+ try {
614
+ this.handleControlEvent(JSON.parse(d.data));
615
+ } catch {
616
+ }
617
+ else if (d.data instanceof ArrayBuffer)
618
+ try {
619
+ const _ = new TextDecoder().decode(d.data);
620
+ try {
621
+ const f = JSON.parse(_);
622
+ X(f) && this.handleControlEvent(f);
623
+ } catch {
624
+ }
625
+ } catch {
626
+ }
627
+ else d.data instanceof Blob && d.data.text().then((_) => {
628
+ try {
629
+ this.handleControlEvent(JSON.parse(_));
630
+ } catch {
631
+ }
632
+ });
633
+ }, this.dataChannel.onerror = (d) => {
634
+ }, this.mediaBytesChannel = this.pc.createDataChannel("media-bytes", { ordered: !0 }), this.mediaBytesChannel.binaryType = "arraybuffer", this.mediaBytesChannel.onopen = () => {
635
+ }, this.mediaBytesChannel.onerror = (d) => {
636
+ }, this.pc.ontrack = (d) => {
637
+ const _ = d.track, f = d.streams[0];
638
+ _.kind === "audio" ? (this.audioElement = new Audio(), this.audioElement.srcObject = f, this.audioElement.play().catch((m) => {
639
+ }), this.audioElement.onended = () => {
640
+ this.sendEvent("playedStream"), typeof window < "u" && window.dispatchEvent(new CustomEvent("vanira:agent-playback-ended"));
641
+ }, this.onRemoteTrack(_, f)) : _.kind === "video" && this.onRemoteTrack(_, f);
642
+ };
643
+ const i = () => {
644
+ var f, m, S, T, E, R, k, I, v, g, P;
645
+ const d = ((f = this.pc) == null ? void 0 : f.connectionState) === "connected" || ((m = this.pc) == null ? void 0 : m.iceConnectionState) === "connected" || ((S = this.pc) == null ? void 0 : S.iceConnectionState) === "completed", _ = ((T = this.pc) == null ? void 0 : T.connectionState) === "failed" || ((E = this.pc) == null ? void 0 : E.iceConnectionState) === "failed" || ((R = this.pc) == null ? void 0 : R.connectionState) === "closed" || ((k = this.pc) == null ? void 0 : k.iceConnectionState) === "closed" || ((I = this.pc) == null ? void 0 : I.connectionState) === "disconnected" || ((v = this.pc) == null ? void 0 : v.iceConnectionState) === "disconnected";
646
+ ((g = this.pc) == null ? void 0 : g.connectionState) === "connected" && this.stopRemoteIcePolling(), d && !this.connected ? (this.connected = !0, (P = this.routeTracker) == null || P.start(), this.onConnected()) : _ && this.connected && (this.connected = !1, this.liveVisionAutoStarted = !1, this.onDisconnected());
647
+ };
648
+ this.pc.onconnectionstatechange = i, this.pc.oniceconnectionstatechange = i;
649
+ const o = this.getIceTrickleUrl();
650
+ this.pc.onicecandidate = (d) => {
651
+ d.candidate && fetch(o, {
652
+ method: "POST",
653
+ headers: { "Content-Type": "application/json" },
654
+ body: JSON.stringify({ candidate: d.candidate.toJSON() })
655
+ }).catch((_) => {
656
+ });
657
+ };
658
+ const c = await this.pc.createOffer();
659
+ if (this.isConnectCancelled(e) || (await this.pc.setLocalDescription(c), this.isConnectCancelled(e))) return;
660
+ const r = this.serverUrl.includes("?") ? this.serverUrl : `${this.serverUrl}/webrtc?agent=${this.agentId}_${this.callId}`, h = await fetch(r, {
661
+ method: "POST",
662
+ headers: { "Content-Type": "application/json" },
663
+ body: JSON.stringify({
664
+ offer: this.pc.localDescription,
665
+ agentId: this.agentId,
666
+ callId: this.callId
667
+ })
668
+ });
669
+ if (this.isConnectCancelled(e)) return;
670
+ if (!h.ok) {
671
+ const d = await h.json();
672
+ throw new Error(d.error || `HTTP ${h.status}`);
673
+ }
674
+ const { answer: u } = await h.json();
675
+ if (this.isConnectCancelled(e) || !this.pc || (await this.pc.setRemoteDescription(u), this.isConnectCancelled(e))) return;
676
+ this.startRemoteIcePolling(o);
677
+ } catch (n) {
678
+ if (this.isConnectCancelled(e)) return;
679
+ throw this.disconnect(), this.onError(n.message || n), n;
680
+ }
681
+ }
682
+ }
683
+ createCallCacheKey(e, t) {
684
+ const n = this.apiKey ? `key:${this.apiKey.slice(0, 16)}` : `token:${(this.token || "").slice(-16)}`;
685
+ return [
686
+ this.backendUrl,
687
+ this.agentId,
688
+ e,
689
+ t || "",
690
+ this.prospectId || "",
691
+ n
692
+ ].join("|");
693
+ }
694
+ async postCreateCall(e, t, n) {
695
+ var c, r, h;
696
+ const a = await fetch(`${this.backendUrl}/calls/create`, {
697
+ method: "POST",
698
+ headers: e,
699
+ body: JSON.stringify({
700
+ agent_id: this.agentId,
701
+ type: ((c = this.runtime) == null ? void 0 : c.callType) || "web",
702
+ mode: t,
703
+ ...(r = this.runtime) != null && r.runtimeName ? { runtime: this.runtime.runtimeName } : {},
704
+ prospect_id: this.prospectId,
705
+ ...n ? { call_id: n } : {}
706
+ })
707
+ });
708
+ if (!a.ok) {
709
+ const u = await a.json().catch(() => ({}));
710
+ throw new Error(`[VaniraClient] createCall failed (${a.status}): ${((h = u == null ? void 0 : u.detail) == null ? void 0 : h.message) || (u == null ? void 0 : u.message) || a.statusText}`);
711
+ }
712
+ const i = await a.json();
713
+ if (!i.call_id || !i.worker_url)
714
+ throw new Error("[VaniraClient] /calls/create response missing call_id or worker_url");
715
+ const o = y.normalizeIceServers(i.ice_servers);
716
+ if (!o.length)
717
+ throw new Error("[VaniraClient] /calls/create response missing ice_servers");
718
+ return {
719
+ callId: i.call_id,
720
+ prospectId: i.prospect_id || this.prospectId,
721
+ serverUrl: i.worker_url.replace(/\/$/, ""),
722
+ iceServers: o
723
+ };
724
+ }
725
+ applyCreateCallSession(e) {
726
+ if (this.callId = e.callId, this.prospectId = e.prospectId || this.prospectId, this.serverUrl = e.serverUrl, this.iceServers = e.iceServers, this.callSessionLoaded = !0, me(this.agentId, this.callId || "", this.prospectId), this.onSessionStarted && this.prospectId && this.callId && !this.sessionStartedEmitted)
727
+ try {
728
+ this.onSessionStarted({ prospectId: this.prospectId, callId: this.callId, serverUrl: this.serverUrl }), this.sessionStartedEmitted = !0;
729
+ } catch {
730
+ }
731
+ }
732
+ async loadCallSession() {
733
+ if (this.callSessionLoaded) return;
734
+ if (this.serverUrl && this.callId) {
735
+ this.callSessionLoaded = !0;
736
+ return;
737
+ }
738
+ if (!this.apiKey && !this.token)
739
+ throw new Error("[VaniraClient] apiKey or token is required to create a call session");
740
+ const e = { "Content-Type": "application/json" };
741
+ this.apiKey ? e["X-API-Key"] = this.apiKey : this.token && (e.Authorization = this.token.startsWith("Bearer ") ? this.token : `Bearer ${this.token}`);
742
+ const t = this.sessionBehavior === "continue" ? this.callId : void 0, n = this.sessionBehavior === "continue" ? "continue" : "new", a = this.createCallCacheKey(n, t);
743
+ let i = y.inflightCreates.get(a);
744
+ i || (i = this.postCreateCall(e, n, t), y.inflightCreates.set(a, i), i.finally(() => {
745
+ y.inflightCreates.get(a) === i && y.inflightCreates.delete(a);
746
+ }));
747
+ const o = await i;
748
+ this.applyCreateCallSession(o);
749
+ }
750
+ getIceServers() {
751
+ var e;
752
+ if ((e = this.iceServers) != null && e.length) return this.iceServers;
753
+ throw new Error("[VaniraClient] ice_servers missing — pass iceServers from POST /calls/create");
754
+ }
755
+ static normalizeIceServers(e) {
756
+ if (!Array.isArray(e)) return [];
757
+ const t = [];
758
+ for (const n of e) {
759
+ const a = n == null ? void 0 : n.urls, i = Array.isArray(a) ? a.filter(Boolean) : typeof a == "string" && a.trim() ? [a.trim()] : [];
760
+ if (!i.length) continue;
761
+ const o = { urls: i.length === 1 ? i[0] : i }, c = n.username, r = n.credential;
762
+ c && (o.username = c), r && (o.credential = r), t.push(o);
763
+ }
764
+ return t;
765
+ }
766
+ getWorkerOrigin() {
767
+ var e;
768
+ if (!((e = this.serverUrl) != null && e.trim()))
769
+ return "";
770
+ try {
771
+ return new URL(this.serverUrl).origin;
772
+ } catch {
773
+ return this.serverUrl.replace(/\/webrtc.*$/, "").replace(/\/$/, "");
774
+ }
775
+ }
776
+ /**
777
+ * ICE trickle URL derived from worker_url — same host, query params (agent_id, call_id, token), path /webrtc/ice.
778
+ */
779
+ getIceTrickleUrl() {
780
+ try {
781
+ const e = new URL(this.serverUrl);
782
+ return e.pathname.endsWith("/webrtc") ? e.pathname = `${e.pathname}/ice` : e.pathname = "/webrtc/ice", e.toString();
783
+ } catch {
784
+ return `${this.getWorkerOrigin()}/webrtc/ice?agent_id=${this.agentId}&call_id=${this.callId}`;
785
+ }
786
+ }
787
+ startRemoteIcePolling(e) {
788
+ this.stopRemoteIcePolling(), this.icePollInterval = setInterval(async () => {
789
+ if (!this.pc) {
790
+ this.stopRemoteIcePolling();
791
+ return;
792
+ }
793
+ if (this.pc.connectionState === "connected") {
794
+ this.stopRemoteIcePolling();
795
+ return;
796
+ }
797
+ try {
798
+ const t = await fetch(e);
799
+ if (!t.ok) return;
800
+ const { candidates: n } = await t.json();
801
+ for (const a of n || []) {
802
+ const i = JSON.stringify(a);
803
+ if (!this.appliedRemoteCandidates.has(i)) {
804
+ this.appliedRemoteCandidates.add(i);
805
+ try {
806
+ await this.pc.addIceCandidate(a);
807
+ } catch {
808
+ }
809
+ }
810
+ }
811
+ } catch {
812
+ }
813
+ }, 150);
814
+ }
815
+ stopRemoteIcePolling() {
816
+ this.icePollInterval !== null && (clearInterval(this.icePollInterval), this.icePollInterval = null);
817
+ }
818
+ /**
819
+ * Send control event via DataChannel
820
+ */
821
+ sendEvent(e, t = {}) {
822
+ var n;
823
+ if (((n = this.dataChannel) == null ? void 0 : n.readyState) === "open") {
824
+ const a = { event: e, ...t };
825
+ this.dataChannel.send(JSON.stringify(a));
826
+ }
827
+ }
828
+ /** Wait for control DataChannel, then send (live vision lifecycle events). */
829
+ async sendEventWhenReady(e, t = {}, n = 1e4) {
830
+ var i;
831
+ const a = Date.now() + n;
832
+ for (; Date.now() < a; ) {
833
+ if (((i = this.dataChannel) == null ? void 0 : i.readyState) === "open")
834
+ return this.sendEvent(e, t), !0;
835
+ await new Promise((o) => setTimeout(o, 50));
836
+ }
837
+ return !1;
838
+ }
839
+ /** Open live camera session (sends client_live_camera_started via preset handler). */
840
+ startLiveCamera(e = {}) {
841
+ const t = { ...this.liveVision, ...e };
842
+ this.emit("tool_call", this.buildOpenLiveCameraToolCall(t));
843
+ }
844
+ buildOpenLiveCameraToolCall(e = {}) {
845
+ const t = e.scope ?? "until_call_end";
846
+ return {
847
+ name: "open_live_camera",
848
+ tool_call_id: `lv_${crypto.randomUUID()}`,
849
+ execution_mode: "fire_and_forget",
850
+ client_fields: { preset_id: p.LiveVision },
851
+ arguments: {
852
+ mode: "live",
853
+ scope: t,
854
+ target_fps: e.target_fps ?? 1,
855
+ max_fps: e.max_fps ?? 3,
856
+ max_width: e.max_width ?? 640,
857
+ reason: e.reason ?? "camera_capture",
858
+ facing_mode: e.facing_mode ?? "environment",
859
+ show_preview: e.show_preview ?? !0,
860
+ ...t === "timed" && e.duration_sec ? { duration_sec: e.duration_sec } : {}
861
+ }
862
+ };
863
+ }
864
+ maybeAutoStartLiveCamera() {
865
+ const e = this.liveVision;
866
+ !(e != null && e.enabled) || e.scope !== "full_call" || this.liveVisionAutoStarted || (this.liveVisionAutoStarted = !0, this.emit("tool_call", this.buildOpenLiveCameraToolCall(e)));
867
+ }
868
+ /**
869
+ * Handle control events from server
870
+ */
871
+ handleControlEvent(e) {
872
+ var t;
873
+ switch (e.event) {
874
+ case "clearAudio":
875
+ break;
876
+ case "transcription":
877
+ this.onTranscription(e.text, e.isFinal);
878
+ break;
879
+ case "mark":
880
+ break;
881
+ case "client_tool_cancel": {
882
+ const a = (e.data || e).tool_call_id || e.tool_call_id;
883
+ this.dispatchBoardAbort("Tool cancelled by server", a);
884
+ break;
885
+ }
886
+ case "client_tool_call": {
887
+ const n = e.tool_call || e.data || e, a = (n == null ? void 0 : n.tool_call_id) || (n == null ? void 0 : n.call_id) || e.tool_call_id || "";
888
+ a && ((t = this.dataChannel) == null ? void 0 : t.readyState) === "open" && this.dataChannel.send(JSON.stringify({
889
+ event: "client_tool_ack",
890
+ data: { tool_call_id: a }
891
+ }));
892
+ const i = (n == null ? void 0 : n.arguments) || (n == null ? void 0 : n.args) || {}, o = (n == null ? void 0 : n.client_fields) || {}, c = (n == null ? void 0 : n.name) || (n == null ? void 0 : n.tool_name) || "", r = fe(o, i, c);
893
+ if (c === "end_call") {
894
+ a && this.sendToolResult(a, {
895
+ status: "client_ack_end_call",
896
+ message: "Ending call."
897
+ }), window.setTimeout(() => this.disconnect(), 120);
898
+ break;
899
+ }
900
+ if (r) {
901
+ window.dispatchEvent(new CustomEvent("vanira:preset", {
902
+ detail: { toolCall: n, client: this }
903
+ })), this.onClientToolCall(n);
904
+ break;
905
+ }
906
+ this.onClientToolCall(n);
907
+ break;
908
+ }
909
+ }
910
+ }
911
+ /**
912
+ * Disconnect and cleanup
913
+ */
914
+ disconnect() {
915
+ var e;
916
+ this.connectGeneration++, this.stopRemoteIcePolling(), this.appliedRemoteCandidates.clear(), this.audioElement && (this.audioElement.pause(), this.audioElement.srcObject = null), this.dataChannel && (this.dataChannel.close(), this.dataChannel = null), this.mediaBytesChannel && (this.mediaBytesChannel.close(), this.mediaBytesChannel = null), this.pc && (this.pc.getSenders().forEach((t) => {
917
+ t.track && t.track.stop();
918
+ }), this.pc.close(), this.pc = null), this.localStream && (this.localStream.getTracks().forEach((t) => {
919
+ try {
920
+ t.stop();
921
+ } catch {
922
+ }
923
+ }), this.localStream = null), this.connected = !1, (e = this.routeTracker) == null || e.stop(), this.callSessionLoaded = !1, this.sessionStartedEmitted = !1, this.serverUrl = "", this.callId = void 0, this.micMuted = !1, this.iceServers = void 0, this.onDisconnected();
924
+ }
925
+ /**
926
+ * Mute or unmute the local microphone without tearing down the WebRTC session.
927
+ * Uses track.enabled (not pause) so audio resumes correctly when unmuted.
928
+ */
929
+ setMicrophoneMuted(e) {
930
+ var t, n;
931
+ this.micMuted = e, (t = this.localStream) == null || t.getAudioTracks().forEach((a) => {
932
+ a.enabled = !e;
933
+ }), (n = this.pc) == null || n.getSenders().forEach((a) => {
934
+ var i;
935
+ ((i = a.track) == null ? void 0 : i.kind) === "audio" && (a.track.enabled = !e);
936
+ });
937
+ }
938
+ isMicrophoneMuted() {
939
+ return this.micMuted;
940
+ }
941
+ toggleMicrophoneMuted() {
942
+ return this.setMicrophoneMuted(!this.micMuted), this.micMuted;
943
+ }
944
+ isConnectCancelled(e) {
945
+ return e !== this.connectGeneration;
946
+ }
947
+ /** Notify WidgetPresetRenderer to cancel in-flight board animations */
948
+ dispatchBoardAbort(e, t) {
949
+ typeof window > "u" || window.dispatchEvent(new CustomEvent("vanira:board_abort", {
950
+ detail: { reason: e, toolCallId: t }
951
+ }));
952
+ }
953
+ /**
954
+ * Send client-side tool execution result back to the AI server.
955
+ * Use this to unblock a 'blocking' execution mode tool.
956
+ *
957
+ * @param toolCallId - `tool_call_id` from the `client_tool_call` event
958
+ * @param result - Payload returned to the agent
959
+ */
960
+ sendToolResult(e, t) {
961
+ this.callId, this.sendEvent("client_tool_result", {
962
+ call_id: this.callId,
963
+ data: { tool_call_id: e, result: t },
964
+ result: t
965
+ });
966
+ }
967
+ /**
968
+ * Send an acknowledgement that the client received a tool call and rendered it.
969
+ * This MUST be sent immediately upon receiving a 'client_tool_call' event.
970
+ * The SDK now sends this automatically, but this method is exposed for manual use.
971
+ * If the ack is not received within 2.3s, the AI will ask "Did something pop up?"
972
+ * Once acked, the server waits up to 10s for the client_tool_result.
973
+ */
974
+ sendToolAck(e) {
975
+ var t;
976
+ ((t = this.dataChannel) == null ? void 0 : t.readyState) === "open" && this.dataChannel.send(JSON.stringify({
977
+ event: "client_tool_ack",
978
+ data: { tool_call_id: e }
979
+ }));
980
+ }
981
+ /**
982
+ * Silently update the AI's context with what the user is currently viewing.
983
+ * Does NOT interrupt the AI's current speech.
984
+ */
985
+ sendContextUpdate(e) {
986
+ this.sendEvent("client_context_update", { data: { context: e } });
987
+ }
988
+ /**
989
+ * Trigger a client-side action — forces the AI to react to a UI event.
990
+ */
991
+ sendActionTrigger(e, t = {}) {
992
+ this.sendEvent("client_action_trigger", {
993
+ data: {
994
+ action_name: e,
995
+ data: t
996
+ }
997
+ });
998
+ }
999
+ /**
1000
+ * Trigger a client-side interrupt — cuts AI audio and forces it to react to a UI event.
1001
+ */
1002
+ triggerActionInterrupt() {
1003
+ this.sendEvent("action_interrupt");
1004
+ }
1005
+ /**
1006
+ * Upload file to media server without notifying the AI (no client_media_update).
1007
+ * Use for blocking presets — result goes in client_tool_result only.
1008
+ */
1009
+ async uploadMediaForToolResult(e, t = "general") {
1010
+ return this._postMediaUpload(e, t);
1011
+ }
1012
+ /**
1013
+ * Uploads a file (photo, document, screenshot) to the media server
1014
+ * and notifies the AI agent via the WebRTC control DataChannel.
1015
+ *
1016
+ * @param file The file to upload (image, PDF, document)
1017
+ * @param reason Routing key for backend processing (default: 'general')
1018
+ * @param message Optional user text message about the file
1019
+ */
1020
+ async uploadMedia(e, t = "general", n = "") {
1021
+ return await ye(this.dataChannel, this.mediaBytesChannel) ? this._uploadMediaViaDataChannel(e, t, n) : (Se(this.dataChannel, this.mediaBytesChannel), this._uploadMediaViaHTTP(e, t, n));
1022
+ }
1023
+ /**
1024
+ * Upload a live vision / screen frame — HTTP only for now, never sends client_media_update.
1025
+ * Caller sends client_media_frame after this returns.
1026
+ *
1027
+ * Frames require a real media_url (server rejects empty). WebRTC upload returns url="" until
1028
+ * the server adds client_media_upload_ready ack — use HTTP for frame JPEGs until then.
1029
+ */
1030
+ async uploadMediaFrame(e, t = "camera_capture", n) {
1031
+ const a = n && !(e instanceof File) ? new File([e], n, { type: e.type || "image/jpeg" }) : e, { media_id: i, url: o, contentType: c } = await this._postMediaUpload(a, t, !0);
1032
+ if (!i || !o)
1033
+ throw new Error("Frame upload failed: server response missing media_id or url");
1034
+ return { media_id: i, url: o, content_type: c };
1035
+ }
1036
+ /** WebRTC media-bytes path (control start/complete + binary chunks). */
1037
+ async _uploadBytesViaDataChannel(e, t, n, a, i) {
1038
+ if (!this.callId || !this.mediaBytesChannel)
1039
+ throw new Error("Upload failed: call not ready for WebRTC media upload.");
1040
+ const o = i || (e instanceof File ? e.name : a === "frame" ? "frame.jpg" : "upload.bin"), c = e.type || (a === "frame" ? "image/jpeg" : "application/octet-stream"), r = await ge(e);
1041
+ return ve({
1042
+ bytes: r,
1043
+ filename: o,
1044
+ contentType: c,
1045
+ reason: t,
1046
+ message: n,
1047
+ callId: this.callId,
1048
+ mediaBytesChannel: this.mediaBytesChannel,
1049
+ sendControlEvent: (h, u) => this.sendEvent(h, u),
1050
+ uploadKind: a
1051
+ });
1052
+ }
1053
+ async _uploadMediaViaDataChannel(e, t, n) {
1054
+ const a = await this._uploadBytesViaDataChannel(e, t, n, "media");
1055
+ return { media_id: a.media_id, url: a.url };
1056
+ }
1057
+ /** HTTP fallback — original uploadMedia behavior (unchanged). */
1058
+ async _uploadMediaViaHTTP(e, t = "general", n = "") {
1059
+ const { media_id: a, url: i, contentType: o } = await this._postMediaUpload(e, t, !0);
1060
+ if (this.dataChannel && this.dataChannel.readyState === "open") {
1061
+ const c = {
1062
+ event: "client_media_update",
1063
+ data: {
1064
+ media_id: a,
1065
+ media_url: i,
1066
+ content_type: o,
1067
+ reason: t,
1068
+ message: n
1069
+ }
1070
+ };
1071
+ this.dataChannel.send(JSON.stringify(c));
1072
+ }
1073
+ return { media_id: a, url: i };
1074
+ }
1075
+ async _postMediaUpload(e, t, n = !1) {
1076
+ if (!this.serverUrl)
1077
+ throw new Error("Upload failed: serverUrl is not set. Connect the VaniraClient first.");
1078
+ if (!this.callId)
1079
+ throw new Error("Upload failed: callId is missing.");
1080
+ const a = this.getWorkerOrigin();
1081
+ if (!a)
1082
+ throw new Error(
1083
+ "Upload failed: worker_url is not set. Connect the call before uploading media."
1084
+ );
1085
+ const i = `${a}/media/upload`, o = new FormData();
1086
+ o.append("file", e), o.append("call_id", this.callId), o.append("reason", t);
1087
+ const c = await fetch(i, {
1088
+ method: "POST",
1089
+ body: o
1090
+ });
1091
+ if (!c.ok) {
1092
+ let _ = `HTTP ${c.status}`;
1093
+ try {
1094
+ _ = (await c.json()).error || _;
1095
+ } catch {
1096
+ }
1097
+ throw new Error(`Upload failed: ${_}`);
1098
+ }
1099
+ const r = await c.json(), h = r.media_id, u = r.url, d = r.content_type || e.type;
1100
+ if (!h || !u)
1101
+ throw new Error("Upload failed: server response missing media_id or url");
1102
+ return n ? { media_id: h, url: u, contentType: d } : { media_id: h, url: u, contentType: d };
1103
+ }
1104
+ // ─── Event emitter (replaces retired VaniraAI) ───────────────────────────
1105
+ get status() {
1106
+ return this._status;
1107
+ }
1108
+ get isConnected() {
1109
+ return this._status === "connected";
1110
+ }
1111
+ on(e, t) {
1112
+ return this.listeners.has(e) || this.listeners.set(e, /* @__PURE__ */ new Set()), this.listeners.get(e).add(t), this;
1113
+ }
1114
+ off(e, t) {
1115
+ var n;
1116
+ return (n = this.listeners.get(e)) == null || n.delete(t), this;
1117
+ }
1118
+ emit(e, t) {
1119
+ var n;
1120
+ (n = this.listeners.get(e)) == null || n.forEach((a) => a(t));
1121
+ }
1122
+ /** Alias for connect() — VaniraAI compatibility */
1123
+ async start() {
1124
+ return await this.connect(), {
1125
+ callId: this.callId || "",
1126
+ prospectId: this.prospectId || "",
1127
+ serverUrl: this.serverUrl || ""
1128
+ };
1129
+ }
1130
+ /** Alias for disconnect() — VaniraAI compatibility */
1131
+ stop() {
1132
+ this.disconnect(), this._status = "disconnected";
1133
+ }
1134
+ sendToolError(e, t) {
1135
+ this.sendToolResult(e, { status: "error", error: t });
1136
+ }
1137
+ /** @deprecated Use sendContextUpdate */
1138
+ updateContext(e) {
1139
+ this.sendContextUpdate(e);
1140
+ }
1141
+ /** @deprecated Use triggerActionInterrupt + sendActionTrigger */
1142
+ triggerInterrupt(e, t = {}) {
1143
+ this.triggerActionInterrupt(), this.sendActionTrigger(e, t);
1144
+ }
1145
+ interruptAudioOnly() {
1146
+ this.triggerActionInterrupt();
1147
+ }
1148
+ };
1149
+ /** Dedupes concurrent POST /calls/create (e.g. React StrictMode double-mount). */
1150
+ l(y, "inflightCreates", /* @__PURE__ */ new Map());
1151
+ let N = y;
1152
+ var F;
1153
+ const be = {
1154
+ supportsAudioEndedEvent: !0,
1155
+ supportsScreenShare: typeof navigator < "u" && typeof ((F = navigator.mediaDevices) == null ? void 0 : F.getDisplayMedia) == "function",
1156
+ supportsDom: typeof document < "u",
1157
+ supportsHtmlAudio: typeof Audio < "u",
1158
+ supportsCustomElements: typeof customElements < "u",
1159
+ supportsLocalStorage: typeof localStorage < "u",
1160
+ supportsBroadcastChannel: typeof BroadcastChannel < "u"
1161
+ }, Te = {
1162
+ name: "browser",
1163
+ callType: "web",
1164
+ runtimeName: "browser",
1165
+ callIdPrefix: "web_",
1166
+ capabilities: be,
1167
+ audio: new se(),
1168
+ media: new re(),
1169
+ peer: new oe(),
1170
+ dataChannel: new le()
1171
+ };
1172
+ function Ee(s) {
1173
+ return new N({
1174
+ ...s,
1175
+ runtime: Te
1176
+ });
1177
+ }
1178
+ const Re = Ee, b = class b {
1179
+ constructor(e) {
1180
+ l(this, "tabId");
1181
+ l(this, "sessionKey");
1182
+ l(this, "channelName");
1183
+ l(this, "channel", null);
1184
+ l(this, "heartbeatTimer", null);
1185
+ l(this, "listeners", /* @__PURE__ */ new Map());
1186
+ const t = e.replace(/[^a-z0-9_-]/gi, "_");
1187
+ this.sessionKey = `vaniraai_session_${t}`, this.channelName = `vaniraai_channel_${t}`, this.tabId = this._getOrCreateTabId(), this._setupChannel();
1188
+ }
1189
+ // ─── Public API ──────────────────────────────────────────────────────────
1190
+ /** Returns this tab's unique ID (stable across same-tab page refreshes). */
1191
+ getTabId() {
1192
+ return this.tabId;
1193
+ }
1194
+ /**
1195
+ * Try to claim the session for this tab.
1196
+ * Returns true → session claimed (or restored after stale prior tab).
1197
+ * Returns false → another live tab already owns the session.
1198
+ */
1199
+ claimSession() {
1200
+ const e = this._loadSession();
1201
+ if (e) {
1202
+ const a = Date.now() - e.lastActive, i = e.tabId === this.tabId, o = a > b.HEARTBEAT_TIMEOUT_MS;
1203
+ if (i)
1204
+ return this._startHeartbeat(), this._emit("session_restored"), !0;
1205
+ if (!o)
1206
+ return this._emit("tab_conflict"), !1;
1207
+ }
1208
+ const t = e ? { ...e, tabId: this.tabId, lastActive: Date.now() } : { tabId: this.tabId, prospectId: "", chatId: null, conversationId: null, messages: [], lastActive: Date.now() };
1209
+ this._saveSession(t), this._startHeartbeat(), this._broadcast({ type: "took_over", tabId: this.tabId });
1210
+ const n = !!(e != null && e.prospectId);
1211
+ return this._emit(n ? "session_restored" : "session_claimed"), !0;
1212
+ }
1213
+ /**
1214
+ * Force-claim the session even if another tab owns it.
1215
+ * Call this when the user explicitly clicks "Use here" in the conflict UI.
1216
+ */
1217
+ forceClaimSession() {
1218
+ const e = this._loadSession(), t = e ? { ...e, tabId: this.tabId, lastActive: Date.now() } : { tabId: this.tabId, prospectId: "", chatId: null, conversationId: null, messages: [], lastActive: Date.now() };
1219
+ this._saveSession(t), this._startHeartbeat(), this._broadcast({ type: "took_over", tabId: this.tabId }), this._emit("session_claimed");
1220
+ }
1221
+ /** Save / update the prospect and chat IDs for this session. */
1222
+ saveIds(e, t, n) {
1223
+ const a = this._loadSession() ?? this._blankSession();
1224
+ this._saveSession({ ...a, prospectId: e, chatId: t, conversationId: n ?? a.conversationId ?? null, tabId: this.tabId, lastActive: Date.now() });
1225
+ }
1226
+ /** Append a message to the persisted history. */
1227
+ pushMessage(e, t) {
1228
+ const n = this._loadSession() ?? this._blankSession();
1229
+ n.messages.push({ role: e, content: t, timestamp: Date.now() }), n.messages.length > 100 && (n.messages = n.messages.slice(-100)), this._saveSession({ ...n, tabId: this.tabId, lastActive: Date.now() });
1230
+ }
1231
+ /** Update the content of the last assistant message (streaming). */
1232
+ updateLastAssistantMessage(e) {
1233
+ const t = this._loadSession() ?? this._blankSession(), n = t.messages;
1234
+ let a = !1;
1235
+ for (let i = n.length - 1; i >= 0; i--)
1236
+ if (n[i].role === "assistant" && n[i].content === "") {
1237
+ n[i].content = e, a = !0;
1238
+ break;
1239
+ }
1240
+ if (!a) {
1241
+ for (let i = n.length - 1; i >= 0; i--)
1242
+ if (n[i].role === "assistant") {
1243
+ n[i].content = e, a = !0;
1244
+ break;
1245
+ }
1246
+ }
1247
+ a || n.push({ role: "assistant", content: e, timestamp: Date.now() }), this._saveSession({ ...t, tabId: this.tabId, lastActive: Date.now() });
1248
+ }
1249
+ /** Returns the currently persisted session data (null if none). */
1250
+ getSession() {
1251
+ return this._loadSession();
1252
+ }
1253
+ /** Clears ALL session data (prospect_id, chat_id, messages). */
1254
+ clearSession() {
1255
+ localStorage.removeItem(this.sessionKey), this._broadcast({ type: "session_cleared", tabId: this.tabId }), this._emit("session_cleared");
1256
+ }
1257
+ /** Clears chat-specific session data (chatId, conversationId, messages) but keeps the prospectId. */
1258
+ clearChatKeepProspect() {
1259
+ const e = this._loadSession();
1260
+ e && this._saveSession({
1261
+ ...e,
1262
+ chatId: null,
1263
+ conversationId: null,
1264
+ messages: [],
1265
+ lastActive: Date.now()
1266
+ });
1267
+ }
1268
+ /** Stop the heartbeat and release resources. Call on widget destroy. */
1269
+ destroy() {
1270
+ this._stopHeartbeat(), this.channel && (this.channel.close(), this.channel = null);
1271
+ }
1272
+ // ─── Event Emitter ───────────────────────────────────────────────────────
1273
+ on(e, t) {
1274
+ return this.listeners.has(e) || this.listeners.set(e, /* @__PURE__ */ new Set()), this.listeners.get(e).add(t), this;
1275
+ }
1276
+ off(e, t) {
1277
+ var n;
1278
+ return (n = this.listeners.get(e)) == null || n.delete(t), this;
1279
+ }
1280
+ // ─── Private ─────────────────────────────────────────────────────────────
1281
+ _getOrCreateTabId() {
1282
+ let e = sessionStorage.getItem("vaniraai_tab_id");
1283
+ return e || (e = `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, sessionStorage.setItem("vaniraai_tab_id", e)), e;
1284
+ }
1285
+ _setupChannel() {
1286
+ typeof BroadcastChannel > "u" || (this.channel = new BroadcastChannel(this.channelName), this.channel.onmessage = (e) => {
1287
+ const t = e.data;
1288
+ t.tabId !== this.tabId && (t.type === "took_over" && (this._stopHeartbeat(), this._emit("tab_took_over")), t.type, t.type === "session_cleared" && this._emit("session_cleared"));
1289
+ });
1290
+ }
1291
+ _broadcast(e) {
1292
+ var t;
1293
+ (t = this.channel) == null || t.postMessage(e);
1294
+ }
1295
+ _startHeartbeat() {
1296
+ this._stopHeartbeat(), this.heartbeatTimer = setInterval(() => {
1297
+ const e = this._loadSession();
1298
+ e && e.tabId === this.tabId && (this._saveSession({ ...e, lastActive: Date.now() }), this._broadcast({ type: "heartbeat", tabId: this.tabId }));
1299
+ }, b.HEARTBEAT_INTERVAL_MS);
1300
+ }
1301
+ _stopHeartbeat() {
1302
+ this.heartbeatTimer !== null && (clearInterval(this.heartbeatTimer), this.heartbeatTimer = null);
1303
+ }
1304
+ _loadSession() {
1305
+ try {
1306
+ const e = localStorage.getItem(this.sessionKey);
1307
+ return e ? JSON.parse(e) : null;
1308
+ } catch {
1309
+ return null;
1310
+ }
1311
+ }
1312
+ _saveSession(e) {
1313
+ try {
1314
+ localStorage.setItem(this.sessionKey, JSON.stringify(e));
1315
+ } catch {
1316
+ }
1317
+ }
1318
+ _blankSession() {
1319
+ return {
1320
+ tabId: this.tabId,
1321
+ prospectId: "",
1322
+ chatId: null,
1323
+ conversationId: null,
1324
+ messages: [],
1325
+ lastActive: Date.now()
1326
+ };
1327
+ }
1328
+ _emit(e) {
1329
+ var t;
1330
+ (t = this.listeners.get(e)) == null || t.forEach((n) => n());
1331
+ }
1332
+ };
1333
+ /** How often (ms) the active tab broadcasts its heartbeat */
1334
+ l(b, "HEARTBEAT_INTERVAL_MS", 5e3), /** How old (ms) a heartbeat must be before we consider the tab dead */
1335
+ l(b, "HEARTBEAT_TIMEOUT_MS", 15e3);
1336
+ let q = b;
1337
+ class Pe {
1338
+ static async fetchWidgetConfig(e, t) {
1339
+ var a;
1340
+ const n = { "Content-Type": "application/json" };
1341
+ t && (n["X-API-Key"] = t);
1342
+ try {
1343
+ const i = await fetch(`${V}/assistant/widget/${e}/config`, {
1344
+ method: "GET",
1345
+ headers: n
1346
+ });
1347
+ if (!i.ok)
1348
+ throw new Error(`Widget config fetch failed: HTTP ${i.status}`);
1349
+ const o = await i.json();
1350
+ return (a = o.agent) != null && a.client && (o.client = o.agent.client), o;
1351
+ } catch (i) {
1352
+ throw i;
1353
+ }
1354
+ }
1355
+ }
1356
+ let A = "https://inboxapi.vanira.io";
1357
+ const ke = "https://coredb.travelr.club/v1/graphql";
1358
+ class Oe {
1359
+ static setChatUrl(e) {
1360
+ A = e;
1361
+ }
1362
+ static async createChatProspect(e) {
1363
+ var n, a;
1364
+ const t = `
1365
+ mutation CreateChatProspect($prospectGroupId: uuid!, $name: String!) {
1366
+ insert_prospects_one(object: {prospect_group_id: $prospectGroupId, name: $name, source: WEBSITE_WIDGET}) {
1367
+ id
1368
+ }
1369
+ }
1370
+ `;
1371
+ try {
1372
+ const o = await (await fetch(ke, {
1373
+ method: "POST",
1374
+ headers: { "Content-Type": "application/json" },
1375
+ body: JSON.stringify({
1376
+ query: t,
1377
+ variables: {
1378
+ prospectGroupId: e,
1379
+ name: `Widget Guest ${(/* @__PURE__ */ new Date()).toLocaleString()}`
1380
+ }
1381
+ })
1382
+ })).json();
1383
+ return o.errors ? `anon_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` : ((a = (n = o.data) == null ? void 0 : n.insert_prospects_one) == null ? void 0 : a.id) || `anon_${Date.now()}`;
1384
+ } catch {
1385
+ return `anon_${Date.now()}`;
1386
+ }
1387
+ }
1388
+ // Calls the new Go bridge /widget/chat endpoint with an empty message to initialize the session and get the Welcome config + inbox_id
1389
+ static async fetchWelcomeMessage(e, t, n, a) {
1390
+ try {
1391
+ const i = { "Content-Type": "application/json" }, o = await fetch(`${A}/widget/chat`, {
1392
+ method: "POST",
1393
+ headers: i,
1394
+ body: JSON.stringify({
1395
+ agent_id: e,
1396
+ ...n ? { widget_id: n } : {},
1397
+ message: "",
1398
+ prospect_id: t,
1399
+ stream: !1
1400
+ })
1401
+ });
1402
+ if (!o.ok) throw new Error("Failed to fetch welcome message");
1403
+ const c = await o.json();
1404
+ let r = c.response || "Hey! how can I help you ?", h;
1405
+ const u = c.chat_id || c.inbox_id, d = c.conversation_id || null;
1406
+ return c.widget && (h = c.widget), { role: "assistant", content: r, widget: h, chatId: u, conversationId: d };
1407
+ } catch {
1408
+ return { role: "assistant", content: "Hey! how can I help you ?" };
1409
+ }
1410
+ }
1411
+ static async sendChatMessage(e, t, n, a, i, o, c, r, h) {
1412
+ var u, d, _, f;
1413
+ try {
1414
+ const m = { "Content-Type": "application/json" }, S = {
1415
+ agent_id: e,
1416
+ message: n,
1417
+ prospect_id: t,
1418
+ stream: !0
1419
+ };
1420
+ a && (S.inbox_id = a), r && (S.widget_id = r);
1421
+ const T = await fetch(`${A}/widget/chat`, {
1422
+ method: "POST",
1423
+ headers: m,
1424
+ body: JSON.stringify(S)
1425
+ });
1426
+ if (!T.ok) throw new Error("Chat request failed");
1427
+ const E = (u = T.body) == null ? void 0 : u.getReader(), R = new TextDecoder();
1428
+ if (!E) throw new Error("No reader");
1429
+ let k = "", I = "", v = null, g = null;
1430
+ for (; ; ) {
1431
+ const { done: P, value: ee } = await E.read();
1432
+ if (P) break;
1433
+ I += R.decode(ee, { stream: !0 });
1434
+ const j = I.split(`
1435
+ `);
1436
+ I = j.pop() || "";
1437
+ for (const te of j) {
1438
+ const L = te.trim();
1439
+ if (L && L.startsWith("data: ")) {
1440
+ const H = L.slice(6);
1441
+ if (H === "[DONE]") {
1442
+ c(v, g);
1443
+ return;
1444
+ }
1445
+ try {
1446
+ const w = JSON.parse(H);
1447
+ if (w.type === "metadata") {
1448
+ (w.chat_id || w.inbox_id) && (v = w.chat_id || w.inbox_id), w.conversation_id && (g = w.conversation_id);
1449
+ continue;
1450
+ }
1451
+ const K = (f = (_ = (d = w.choices) == null ? void 0 : d[0]) == null ? void 0 : _.delta) == null ? void 0 : f.content;
1452
+ K && (k += K, i(k)), w.widget && o(w.widget), w.chat_id && !v && (v = w.chat_id), w.conversation_id && !g && (g = w.conversation_id);
1453
+ } catch {
1454
+ }
1455
+ }
1456
+ }
1457
+ }
1458
+ c(v, g);
1459
+ } catch {
1460
+ i("Sorry, I encountered an error. Please try again."), c(null, null);
1461
+ }
1462
+ }
1463
+ static listenForAdminReplies(e, t, n) {
1464
+ const a = `${A}/inbox/stream?inbox_id=${e}&sender=${encodeURIComponent(t)}`;
1465
+ let i = null, o = 0, c = !1;
1466
+ const r = () => {
1467
+ c || (i = new EventSource(a), i.onopen = () => {
1468
+ o = 0;
1469
+ }, i.onmessage = (h) => {
1470
+ try {
1471
+ const u = JSON.parse(h.data), d = u.direction === "outgoing", _ = !!u.content, f = u.source !== "ai";
1472
+ d && _ && f && n(u.content);
1473
+ } catch {
1474
+ }
1475
+ }, i.onerror = (h) => {
1476
+ if (i && (i.close(), i = null), c) return;
1477
+ const u = Math.min(2e3 * Math.pow(2, o), 3e4);
1478
+ o++, setTimeout(r, u);
1479
+ });
1480
+ };
1481
+ return r(), {
1482
+ close: () => {
1483
+ c = !0, i && (i.close(), i = null);
1484
+ }
1485
+ };
1486
+ }
1487
+ static async createCall(e, t, n, a, i) {
1488
+ try {
1489
+ const o = { "Content-Type": "application/json" };
1490
+ i && (o["X-API-Key"] = i);
1491
+ const c = await fetch(`${V}/calls/create`, {
1492
+ method: "POST",
1493
+ headers: o,
1494
+ body: JSON.stringify({
1495
+ agent_id: e,
1496
+ prospect_id: n || void 0,
1497
+ type: "web"
1498
+ })
1499
+ }), r = await c.json();
1500
+ if (!c.ok)
1501
+ throw new Error(`[VaniraAI] Call creation failed HTTP ${c.status}`);
1502
+ if (!r.worker_url)
1503
+ throw new Error("[VaniraAI] Worker URL missing from response. Call cannot proceed.");
1504
+ return {
1505
+ callId: r.call_id || r.id || `web_${Date.now()}`,
1506
+ workerUrl: r.worker_url
1507
+ // Pass the full authenticated URL
1508
+ };
1509
+ } catch (o) {
1510
+ throw o;
1511
+ }
1512
+ }
1513
+ static async resolveConversation(e) {
1514
+ try {
1515
+ if (!(await fetch(`${A}/inbox/conversations/resolve`, {
1516
+ method: "POST",
1517
+ headers: { "Content-Type": "application/json" },
1518
+ body: JSON.stringify({ conversation_id: e })
1519
+ })).ok) throw new Error("Failed to resolve conversation");
1520
+ } catch (t) {
1521
+ throw t;
1522
+ }
1523
+ }
1524
+ }
1525
+ export {
1526
+ se as BrowserAudioAdapter,
1527
+ le as BrowserDataChannelAdapter,
1528
+ re as BrowserMediaAdapter,
1529
+ oe as BrowserPeerAdapter,
1530
+ Oe as ChatService,
1531
+ Pe as ConfigService,
1532
+ q as SessionManager,
1533
+ N as VaniraAI,
1534
+ N as VaniraClient,
1535
+ N as WebRTCClient,
1536
+ be as browserCapabilities,
1537
+ Te as browserRuntime,
1538
+ Re as createBrowserAI,
1539
+ Ee as createBrowserClient
1540
+ };