@vanira/sdk 0.0.42 → 0.0.43

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.
Files changed (44) hide show
  1. package/dist/VaniraAI-D2KUxJJO.js +682 -0
  2. package/dist/VaniraAI-EFYOmJ2E.cjs +1 -0
  3. package/dist/__tests__/adapters.test.d.ts +14 -0
  4. package/dist/adapters/PeerConnectionAdapter.d.ts +90 -0
  5. package/dist/adapters/browser/BrowserAudioAdapter.d.ts +9 -0
  6. package/dist/adapters/browser/BrowserDataChannelAdapter.d.ts +16 -0
  7. package/dist/adapters/browser/BrowserMediaAdapter.d.ts +10 -0
  8. package/dist/adapters/browser/BrowserPeerAdapter.d.ts +9 -0
  9. package/dist/adapters/browser/index.d.ts +4 -0
  10. package/dist/adapters/interfaces.d.ts +70 -0
  11. package/dist/adapters/react-native/RNAudioAdapter.d.ts +9 -0
  12. package/dist/adapters/react-native/RNDataChannelAdapter.d.ts +16 -0
  13. package/dist/adapters/react-native/RNMediaAdapter.d.ts +17 -0
  14. package/dist/adapters/react-native/RNPeerAdapter.d.ts +19 -0
  15. package/dist/adapters/react-native/index.d.ts +4 -0
  16. package/dist/browser.d.ts +1 -0
  17. package/dist/core/VaniraAI.d.ts +2 -0
  18. package/dist/index.d.ts +21 -3
  19. package/dist/platforms/browser.cjs +8 -0
  20. package/dist/platforms/browser.d.ts +28 -0
  21. package/dist/platforms/browser.js +479 -0
  22. package/dist/platforms/react-native.cjs +1 -0
  23. package/dist/platforms/react-native.d.ts +46 -0
  24. package/dist/platforms/react-native.js +116 -0
  25. package/dist/react/PresetRenderer.d.ts +1 -2
  26. package/dist/react/index.d.ts +0 -4
  27. package/dist/react-native.d.ts +1 -0
  28. package/dist/runtime/browserRuntime.d.ts +17 -0
  29. package/dist/runtime/reactNativeRuntime.d.ts +18 -0
  30. package/dist/runtime/types.d.ts +96 -0
  31. package/dist/types.d.ts +36 -4
  32. package/dist/ui/presets/WidgetPresetRenderer.d.ts +2 -0
  33. package/dist/vanira-sdk.es.js +1788 -231
  34. package/dist/vanira-sdk.js +36 -36
  35. package/dist/vanira-sdk.js.map +1 -1
  36. package/dist/vanira-sdk.umd.js +1538 -0
  37. package/package.json +18 -3
  38. package/dist/react/presets/CalendarPreset.d.ts +0 -4
  39. package/dist/react/presets/CameraPreset.d.ts +0 -16
  40. package/dist/react/presets/FormPreset.d.ts +0 -4
  41. package/dist/react/presets/NavigatePreset.d.ts +0 -16
  42. package/dist/react/presets/UploadPreset.d.ts +0 -17
  43. package/dist/react/registry.d.ts +0 -3
  44. package/dist/react/types.d.ts +0 -21
@@ -0,0 +1,682 @@
1
+ var v = Object.defineProperty;
2
+ var T = (g, e, t) => e in g ? v(g, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : g[e] = t;
3
+ var a = (g, e, t) => T(g, typeof e != "symbol" ? e + "" : e, t);
4
+ class b {
5
+ // private iceServers?: RTCIceServer[];
6
+ // private token?: string;
7
+ constructor(e) {
8
+ a(this, "serverUrl");
9
+ a(this, "agentId");
10
+ a(this, "callId");
11
+ a(this, "prospectId");
12
+ a(this, "apiKey");
13
+ a(this, "backendUrl");
14
+ a(this, "token");
15
+ a(this, "onConnected");
16
+ a(this, "onDisconnected");
17
+ a(this, "onError");
18
+ a(this, "onTranscription");
19
+ a(this, "onLocalStream");
20
+ // @ts-ignore
21
+ a(this, "onRemoteTrack");
22
+ a(this, "onClientToolCall");
23
+ a(this, "onSessionStarted");
24
+ a(this, "sessionStartedEmitted", !1);
25
+ a(this, "pc", null);
26
+ a(this, "dataChannel", null);
27
+ a(this, "audioElement", null);
28
+ a(this, "localStream", null);
29
+ a(this, "connected", !1);
30
+ if (!e.agentId) throw new Error("agentId is required");
31
+ if (!e.serverUrl && !e.apiKey) throw new Error("Provide either serverUrl or apiKey (to use createCall())");
32
+ this.agentId = e.agentId, this.serverUrl = e.serverUrl ? e.serverUrl.replace(/\/$/, "") : "", this.apiKey = e.apiKey, this.backendUrl = (e.backendUrl || "https://api.vanira.io").replace(/\/$/, "");
33
+ const t = typeof window < "u";
34
+ if (this.prospectId = e.prospectId, !this.prospectId && t)
35
+ try {
36
+ this.prospectId = window.sessionStorage && window.sessionStorage.getItem("vanira_prospect_id") || window.localStorage && window.localStorage.getItem("vanira_prospect_id") || void 0;
37
+ } catch {
38
+ }
39
+ const n = e.sessionBehavior === "continue";
40
+ if (e.callId)
41
+ this.callId = e.callId;
42
+ else if (n && t)
43
+ try {
44
+ this.callId = window.sessionStorage && window.sessionStorage.getItem("vanira_latest_call_id") || window.localStorage && window.localStorage.getItem("vanira_latest_call_id") || void 0;
45
+ } catch {
46
+ }
47
+ else
48
+ this.callId = void 0;
49
+ this.onConnected = e.onConnected || (() => {
50
+ }), this.onDisconnected = e.onDisconnected || (() => {
51
+ }), this.onError = e.onError || ((i) => console.error("[WebRTC]", i)), this.onTranscription = e.onTranscription || (() => {
52
+ }), this.onLocalStream = e.onLocalStream || (() => {
53
+ }), this.onRemoteTrack = e.onRemoteTrack || (() => {
54
+ }), this.onClientToolCall = e.onClientToolCall || (() => {
55
+ }), this.onSessionStarted = e.onSessionStarted;
56
+ }
57
+ /**
58
+ * Create a call session via the Vanira API, then connect.
59
+ * Requires apiKey in the constructor config.
60
+ * Equivalent to manually calling POST /calls/create + client.connect().
61
+ */
62
+ async createCall() {
63
+ var i;
64
+ if (!this.apiKey) throw new Error("[VaniraAI] apiKey is required to use createCall()");
65
+ const e = {
66
+ "Content-Type": "application/json",
67
+ "X-API-Key": this.apiKey
68
+ }, t = await fetch(`${this.backendUrl}/calls/create`, {
69
+ method: "POST",
70
+ headers: e,
71
+ body: JSON.stringify({
72
+ agent_id: this.agentId,
73
+ type: "web",
74
+ prospect_id: this.prospectId,
75
+ call_id: this.callId
76
+ })
77
+ });
78
+ if (!t.ok) {
79
+ const r = await t.json().catch(() => ({}));
80
+ throw new Error(`[VaniraAI] createCall failed (${t.status}): ${((i = r == null ? void 0 : r.detail) == null ? void 0 : i.message) || (r == null ? void 0 : r.message) || t.statusText}`);
81
+ }
82
+ const n = await t.json();
83
+ if (!n.call_id || !n.worker_url)
84
+ throw new Error("[VaniraAI] createCall response missing call_id or worker_url");
85
+ if (this.callId = n.call_id, this.prospectId = n.prospect_id || this.prospectId, this.serverUrl = n.worker_url, typeof window < "u")
86
+ try {
87
+ window.sessionStorage && (window.sessionStorage.setItem("vanira_prospect_id", this.prospectId || ""), window.sessionStorage.setItem("vanira_latest_call_id", this.callId || "")), window.localStorage && (window.localStorage.setItem("vanira_prospect_id", this.prospectId || ""), window.localStorage.setItem("vanira_latest_call_id", this.callId || ""));
88
+ } catch (r) {
89
+ console.warn("[WebRTC] Failed to save session variables to storage:", r);
90
+ }
91
+ if (this.onSessionStarted && this.prospectId && this.callId)
92
+ try {
93
+ this.onSessionStarted({ prospectId: this.prospectId, callId: this.callId, serverUrl: this.serverUrl }), this.sessionStartedEmitted = !0;
94
+ } catch (r) {
95
+ console.error("[WebRTC] Error in onSessionStarted callback:", r);
96
+ }
97
+ await this.connect();
98
+ }
99
+ /**
100
+ * Connect to the agent. If apiKey is set and serverUrl is not yet resolved,
101
+ * automatically calls the pre-flight API to get call_id + worker_url first.
102
+ */
103
+ async connect() {
104
+ var e;
105
+ if (this.apiKey && !this.serverUrl) {
106
+ await this.createCall();
107
+ return;
108
+ }
109
+ if (!this.serverUrl) throw new Error("[VaniraAI] serverUrl is missing. Provide apiKey or serverUrl.");
110
+ if (this.callId || (this.callId = this.generateCallId()), this.onSessionStarted && this.prospectId && this.callId && !this.sessionStartedEmitted)
111
+ try {
112
+ this.onSessionStarted({ prospectId: this.prospectId, callId: this.callId, serverUrl: this.serverUrl }), this.sessionStartedEmitted = !0;
113
+ } catch (t) {
114
+ console.error("[WebRTC] Error in onSessionStarted callback:", t);
115
+ }
116
+ console.log("🔵 [WebRTC] Starting connection...");
117
+ try {
118
+ this.pc = new RTCPeerConnection({
119
+ iceServers: [
120
+ { urls: "stun:stun.l.google.com:19302" },
121
+ { urls: "stun:stun1.l.google.com:19302" },
122
+ { urls: "stun:global.relay.metered.ca:80" },
123
+ // works on Jio (port 80)
124
+ {
125
+ urls: [
126
+ "turns:global.relay.metered.ca:443?transport=tcp",
127
+ "turn:global.relay.metered.ca:443?transport=tcp",
128
+ "turn:global.relay.metered.ca:80?transport=tcp"
129
+ ],
130
+ username: "fa97658be3343d21da3b65e6",
131
+ credential: "HXHDoqeHbvZrmCuf"
132
+ }
133
+ ],
134
+ iceTransportPolicy: "all"
135
+ // P2P if possible, TURN only as fallback
136
+ }), console.log("using ice servers:", [
137
+ { urls: "stun:stun.l.google.com:19302" },
138
+ { urls: "stun:stun1.l.google.com:19302" }
139
+ ]);
140
+ let t;
141
+ try {
142
+ t = await navigator.mediaDevices.getUserMedia({
143
+ audio: {
144
+ echoCancellation: !0,
145
+ noiseSuppression: !0,
146
+ autoGainControl: !0,
147
+ sampleRate: { ideal: 16e3 },
148
+ channelCount: 1
149
+ }
150
+ });
151
+ } catch (o) {
152
+ if (console.error("🎤 [WebRTC] Microphone access failed:", o), o.name === "NotAllowedError" || o.name === "PermissionDeniedError" || (e = o.message) != null && e.includes("Permission denied")) {
153
+ const s = navigator.userAgent, c = /iPad|iPhone|iPod/.test(s) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
154
+ let p = "Microphone access denied. Please allow microphone access in your browser settings.";
155
+ c && (s.includes("CriOS") ? p = "Microphone access blocked. Please enable it in iOS Settings > Chrome > Microphone, then reload the page." : s.includes("FxiOS") ? p = "Microphone access blocked. Please enable it in iOS Settings > Firefox > Microphone, then reload the page." : p = "Microphone access blocked. Please enable it in iOS Settings > Safari > Microphone (or tap 'aA' > Website Settings > Allow Microphone).");
156
+ const h = new Error(p);
157
+ throw h.name = o.name, h;
158
+ }
159
+ throw o;
160
+ }
161
+ if (console.log("🎤 [WebRTC] Microphone access granted"), this.localStream = t, !this.pc) {
162
+ console.log("[WebRTC] Connection aborted: peer connection closed during setup"), t.getTracks().forEach((o) => o.stop());
163
+ return;
164
+ }
165
+ if (this.onLocalStream(t), t.getTracks().forEach((o) => {
166
+ var s;
167
+ (s = this.pc) == null || s.addTrack(o, t);
168
+ }), !this.pc)
169
+ throw new Error("RTCPeerConnection was closed unexpectedly");
170
+ this.dataChannel = this.pc.createDataChannel("control"), this.dataChannel.onopen = () => console.log("📡 [WebRTC] DataChannel opened"), this.dataChannel.onmessage = (o) => {
171
+ if (typeof o.data == "string")
172
+ try {
173
+ this.handleControlEvent(JSON.parse(o.data));
174
+ } catch {
175
+ console.warn("[WebRTC] Failed to parse message:", o.data);
176
+ }
177
+ else if (o.data instanceof ArrayBuffer)
178
+ try {
179
+ const s = new TextDecoder().decode(o.data);
180
+ try {
181
+ const c = JSON.parse(s);
182
+ c && typeof c == "object" && c.event === "client_tool_call" ? (console.log("[VaniraAI] Safely decoding binary tool_call to JSON:", c), this.handleControlEvent(c)) : console.log("[VaniraAI] Decoded JSON from binary (inspect only):", c);
183
+ } catch {
184
+ console.log("[VaniraAI] Decoded String from binary:", s);
185
+ }
186
+ } catch {
187
+ console.log("[VaniraAI] Received binary data:", o.data.byteLength, "bytes (not decodable)");
188
+ }
189
+ else o.data instanceof Blob && o.data.text().then((s) => {
190
+ try {
191
+ this.handleControlEvent(JSON.parse(s));
192
+ } catch {
193
+ console.warn("[WebRTC] Failed to parse blob data:", s);
194
+ }
195
+ });
196
+ }, this.dataChannel.onerror = (o) => console.error("❌ [WebRTC] DataChannel error:", o), this.pc.ontrack = (o) => {
197
+ const s = o.track, c = o.streams[0];
198
+ console.log(`📥 [WebRTC] Received ${s.kind} track`), s.kind === "audio" ? (console.log("🔊 [WebRTC] Received audio track from server"), this.audioElement = new Audio(), this.audioElement.srcObject = c, this.audioElement.play().catch((p) => console.warn("Audio autoplay blocked:", p)), this.audioElement.onended = () => {
199
+ this.sendEvent("playedStream"), console.log("✅ [WebRTC] TTS playback complete");
200
+ }, this.onRemoteTrack(s, c)) : s.kind === "video" && (console.log("📹 [WebRTC] Video track received"), this.onRemoteTrack(s, c));
201
+ };
202
+ const n = () => {
203
+ var c, p, h, u, f, _, C, m, w, I, S;
204
+ console.log("🔄 [WebRTC] State:", (c = this.pc) == null ? void 0 : c.connectionState, "| ICE:", (p = this.pc) == null ? void 0 : p.iceConnectionState);
205
+ const o = ((h = this.pc) == null ? void 0 : h.connectionState) === "connected" || ((u = this.pc) == null ? void 0 : u.iceConnectionState) === "connected" || ((f = this.pc) == null ? void 0 : f.iceConnectionState) === "completed", s = ((_ = this.pc) == null ? void 0 : _.connectionState) === "failed" || ((C = this.pc) == null ? void 0 : C.iceConnectionState) === "failed" || ((m = this.pc) == null ? void 0 : m.connectionState) === "closed" || ((w = this.pc) == null ? void 0 : w.iceConnectionState) === "closed" || ((I = this.pc) == null ? void 0 : I.connectionState) === "disconnected" || ((S = this.pc) == null ? void 0 : S.iceConnectionState) === "disconnected";
206
+ o && !this.connected ? (this.connected = !0, this.onConnected()) : s && this.connected && (this.connected = !1, this.onDisconnected());
207
+ };
208
+ this.pc.onconnectionstatechange = n, this.pc.oniceconnectionstatechange = n;
209
+ const i = await this.pc.createOffer();
210
+ await this.pc.setLocalDescription(i), console.log("📝 [WebRTC] Created offer, waiting for ICE gathering..."), await this.waitForIceGathering(), console.log("🧊 [WebRTC] ICE gathering complete"), console.log("📤 [WebRTC] Sending offer via HTTP...");
211
+ const r = this.serverUrl.includes("?") ? this.serverUrl : `${this.serverUrl}/webrtc?agent=${this.agentId}_${this.callId}`, l = await fetch(r, {
212
+ method: "POST",
213
+ headers: { "Content-Type": "application/json" },
214
+ body: JSON.stringify({
215
+ offer: this.pc.localDescription,
216
+ agentId: this.agentId,
217
+ callId: this.callId
218
+ })
219
+ });
220
+ if (!l.ok) {
221
+ const o = await l.json();
222
+ throw new Error(o.error || `HTTP ${l.status}`);
223
+ }
224
+ const { answer: d } = await l.json();
225
+ console.log("📥 [WebRTC] Received answer from server"), await this.pc.setRemoteDescription(d), console.log("✅ [WebRTC] Connection established!");
226
+ } catch (t) {
227
+ throw console.error("❌ [WebRTC] Connection failed:", t), this.disconnect(), this.onError(t.message || t), t;
228
+ }
229
+ }
230
+ /**
231
+ * Wait for ICE gathering to complete
232
+ */
233
+ waitForIceGathering() {
234
+ return new Promise((e) => {
235
+ if (!this.pc) return e();
236
+ if (this.pc.iceGatheringState === "complete")
237
+ e();
238
+ else {
239
+ const t = () => {
240
+ var n, i;
241
+ ((n = this.pc) == null ? void 0 : n.iceGatheringState) === "complete" && ((i = this.pc) == null || i.removeEventListener("icegatheringstatechange", t), e());
242
+ };
243
+ this.pc.addEventListener("icegatheringstatechange", t), setTimeout(() => {
244
+ var n;
245
+ (n = this.pc) == null || n.removeEventListener("icegatheringstatechange", t), console.warn("⚠️ [WebRTC] ICE gathering timeout, proceeding anyway"), e();
246
+ }, 5e3);
247
+ }
248
+ });
249
+ }
250
+ /**
251
+ * Send control event via DataChannel
252
+ */
253
+ sendEvent(e, t = {}) {
254
+ var n;
255
+ if (((n = this.dataChannel) == null ? void 0 : n.readyState) === "open") {
256
+ const i = { event: e, ...t };
257
+ console.log(`📤 [WebRTC] Sending event: ${e}`, i), this.dataChannel.send(JSON.stringify(i));
258
+ } else
259
+ console.warn(`⚠️ [WebRTC] Cannot send event ${e} - DataChannel not open`);
260
+ }
261
+ /**
262
+ * Handle control events from server
263
+ */
264
+ handleControlEvent(e) {
265
+ var t;
266
+ switch (e.event) {
267
+ case "clearAudio":
268
+ console.log("🛑 [WebRTC] Interrupt: clearAudio received (leaving stream unpaused)");
269
+ break;
270
+ case "transcription":
271
+ console.log("📝 [WebRTC] Transcription:", e.text), this.onTranscription(e.text, e.isFinal);
272
+ break;
273
+ case "mark":
274
+ console.log("🏷️ [WebRTC] Mark:", e.name);
275
+ break;
276
+ case "client_tool_call": {
277
+ const n = e.tool_call || e.data || e, i = (n == null ? void 0 : n.tool_call_id) || (n == null ? void 0 : n.call_id) || e.tool_call_id || "";
278
+ i && ((t = this.dataChannel) == null ? void 0 : t.readyState) === "open" ? (this.dataChannel.send(JSON.stringify({
279
+ event: "client_tool_ack",
280
+ data: { tool_call_id: i }
281
+ })), console.log("✅ [VaniraAI] Sent client_tool_ack for:", i)) : i || console.warn("⚠️ [VaniraAI] client_tool_call received without tool_call_id — ack skipped");
282
+ const r = (n == null ? void 0 : n.arguments) || (n == null ? void 0 : n.args) || {}, l = (n == null ? void 0 : n.client_fields) || {}, d = (l == null ? void 0 : l.preset_id) || (r == null ? void 0 : r.preset_id);
283
+ if (d) {
284
+ console.log(`🎨 [VaniraAI] Preset detected: ${d}`), console.log("🎨 [VaniraAI] Tool args:", r), console.log("🎨 [VaniraAI] Client fields:", l), console.log(`🎨 [VaniraAI] Preset "${d}" — dispatching vanira:preset event`), window.dispatchEvent(new CustomEvent("vanira:preset", {
285
+ detail: { toolCall: n, client: this }
286
+ })), this.onClientToolCall(n);
287
+ break;
288
+ }
289
+ console.log("🛠️ [VaniraAI] Client Tool Call:", e), this.onClientToolCall(n);
290
+ break;
291
+ }
292
+ default:
293
+ console.log("ℹ️ [WebRTC] Unknown event:", e.event);
294
+ }
295
+ }
296
+ /**
297
+ * Disconnect and cleanup
298
+ */
299
+ disconnect() {
300
+ console.log("🔴 [WebRTC] Disconnecting..."), this.audioElement && (this.audioElement.pause(), this.audioElement.srcObject = null), this.dataChannel && (this.dataChannel.close(), this.dataChannel = null), this.pc && (this.pc.getSenders().forEach((e) => {
301
+ e.track && e.track.stop();
302
+ }), this.pc.close(), this.pc = null), this.localStream && (this.localStream.getTracks().forEach((e) => {
303
+ try {
304
+ e.stop();
305
+ } catch {
306
+ }
307
+ }), this.localStream = null), this.connected = !1, this.onDisconnected();
308
+ }
309
+ /**
310
+ * Generate unique call ID
311
+ */
312
+ generateCallId() {
313
+ return "web_" + Date.now() + "_" + Math.random().toString(36).substr(2, 8);
314
+ }
315
+ /**
316
+ * Send client-side tool execution result back to the AI server.
317
+ * Use this to unblock a 'blocking' execution mode tool.
318
+ */
319
+ sendToolResult(e, t) {
320
+ this.sendEvent("client_tool_result", {
321
+ call_id: e,
322
+ result: t
323
+ });
324
+ }
325
+ /**
326
+ * Send an acknowledgement that the client received a tool call and rendered it.
327
+ * This MUST be sent immediately upon receiving a 'client_tool_call' event.
328
+ * The SDK now sends this automatically, but this method is exposed for manual use.
329
+ * If the ack is not received within 2.3s, the AI will ask "Did something pop up?"
330
+ * Once acked, the server waits up to 10s for the client_tool_result.
331
+ */
332
+ sendToolAck(e) {
333
+ var t;
334
+ ((t = this.dataChannel) == null ? void 0 : t.readyState) === "open" ? (this.dataChannel.send(JSON.stringify({
335
+ event: "client_tool_ack",
336
+ data: { tool_call_id: e }
337
+ })), console.log("✅ [VaniraAI] Sent client_tool_ack for:", e)) : console.warn("⚠️ [VaniraAI] Cannot send tool_ack: DataChannel not open");
338
+ }
339
+ /**
340
+ * Silently update the AI's context with what the user is currently viewing.
341
+ * Does NOT interrupt the AI's current speech.
342
+ */
343
+ sendContextUpdate(e) {
344
+ this.sendEvent("client_context_update", { data: { context: e } });
345
+ }
346
+ /**
347
+ * Trigger a client-side action — forces the AI to react to a UI event.
348
+ */
349
+ sendActionTrigger(e, t = {}) {
350
+ this.sendEvent("client_action_trigger", {
351
+ data: {
352
+ action_name: e,
353
+ data: t
354
+ }
355
+ });
356
+ }
357
+ /**
358
+ * Trigger a client-side interrupt — cuts AI audio and forces it to react to a UI event.
359
+ */
360
+ triggerActionInterrupt() {
361
+ this.sendEvent("action_interrupt"), console.log("🛑 [VaniraAI] Triggered client-side action interrupt");
362
+ }
363
+ /**
364
+ * Uploads a file (photo, document, screenshot) to the media server
365
+ * and notifies the AI agent via the WebRTC control DataChannel.
366
+ *
367
+ * @param file The file to upload (image, PDF, document)
368
+ * @param reason Routing key for backend processing (default: 'general')
369
+ * @param message Optional user text message about the file
370
+ */
371
+ async uploadMedia(e, t = "general", n = "") {
372
+ if (!this.serverUrl)
373
+ throw new Error("Upload failed: serverUrl is not set. Connect the WebRTCClient first.");
374
+ if (!this.callId)
375
+ throw new Error("Upload failed: callId is missing.");
376
+ let i = "";
377
+ try {
378
+ i = new URL(this.serverUrl).origin;
379
+ } catch {
380
+ i = this.serverUrl.replace(/\/webrtc.*$/, "").replace(/\/$/, "");
381
+ }
382
+ const r = `${i}/media/upload`, l = new FormData();
383
+ l.append("file", e), l.append("call_id", this.callId), l.append("reason", t), console.log(`[VaniraAI] Uploading media to ${r} (Call: ${this.callId}, Reason: ${t})...`);
384
+ const d = await fetch(r, {
385
+ method: "POST",
386
+ body: l
387
+ });
388
+ if (!d.ok) {
389
+ let h = `HTTP ${d.status}`;
390
+ try {
391
+ h = (await d.json()).error || h;
392
+ } catch {
393
+ }
394
+ throw new Error(`Upload failed: ${h}`);
395
+ }
396
+ const o = await d.json(), s = o.media_id, c = o.url, p = o.content_type || e.type;
397
+ if (!s || !c)
398
+ throw new Error("Upload failed: server response missing media_id or url");
399
+ if (console.log(`[VaniraAI] Media uploaded successfully. ID: ${s}, URL: ${c}`), this.dataChannel && this.dataChannel.readyState === "open") {
400
+ const h = {
401
+ event: "client_media_update",
402
+ data: {
403
+ media_id: s,
404
+ media_url: c,
405
+ content_type: p,
406
+ reason: t,
407
+ message: n
408
+ }
409
+ };
410
+ this.dataChannel.send(JSON.stringify(h)), console.log("[VaniraAI] Sent client_media_update notification via DataChannel:", h);
411
+ } else
412
+ console.warn("[VaniraAI] WebRTC DataChannel is not open. Media uploaded but AI notification skipped.");
413
+ return { media_id: s, url: c };
414
+ }
415
+ /**
416
+ * Fetch active STUN/TURN ICE servers from the Vanira API.
417
+ * @param apiKey Your sk_live_* or pk_live_* key
418
+ * @param backendUrl Override the default backend URL (default: https://api.vanira.io)
419
+ */
420
+ static async fetchIceServers(e, t = "https://api.vanira.io") {
421
+ try {
422
+ const n = await fetch(`${t}/discovery/ice-servers`, {
423
+ headers: { "X-API-Key": e }
424
+ });
425
+ if (!n.ok) throw new Error("Failed to fetch ICE servers");
426
+ return (await n.json()).ice_servers || [];
427
+ } catch (n) {
428
+ throw console.error("[WebRTC] Failed to fetch ICE servers:", n), n;
429
+ }
430
+ }
431
+ }
432
+ const y = {};
433
+ class k {
434
+ constructor(e) {
435
+ a(this, "config");
436
+ a(this, "_status", "idle");
437
+ a(this, "client", null);
438
+ a(this, "listeners", /* @__PURE__ */ new Map());
439
+ if (!e.agentId) throw new Error("[VaniraAI] agentId is required");
440
+ this.config = e;
441
+ }
442
+ // ─── Getters ─────────────────────────────────────────────────────────────
443
+ /** Returns the active WebRTC worker/server URL */
444
+ get serverUrl() {
445
+ var e;
446
+ return (e = this.client) == null ? void 0 : e.serverUrl;
447
+ }
448
+ /** Returns the active call ID */
449
+ get callId() {
450
+ var e;
451
+ return (e = this.client) == null ? void 0 : e.callId;
452
+ }
453
+ /** Returns the active prospect ID associated with this session */
454
+ get prospectId() {
455
+ var e;
456
+ return (e = this.client) == null ? void 0 : e.prospectId;
457
+ }
458
+ // ─── Status ──────────────────────────────────────────────────────────────
459
+ /** Current connection status */
460
+ get status() {
461
+ return this._status;
462
+ }
463
+ /** Shorthand: true when status is 'connected' */
464
+ get isConnected() {
465
+ return this._status === "connected";
466
+ }
467
+ // ─── Event Emitter ───────────────────────────────────────────────────────
468
+ /**
469
+ * Register an event listener.
470
+ * @param event - Event name
471
+ * @param callback - Handler function
472
+ * @returns `this` for chaining
473
+ */
474
+ on(e, t) {
475
+ return this.listeners.has(e) || this.listeners.set(e, /* @__PURE__ */ new Set()), this.listeners.get(e).add(t), this;
476
+ }
477
+ /**
478
+ * Remove a previously registered event listener.
479
+ * @param event - Event name
480
+ * @param callback - The exact same function reference that was registered
481
+ */
482
+ off(e, t) {
483
+ var n;
484
+ return (n = this.listeners.get(e)) == null || n.delete(t), this;
485
+ }
486
+ emit(e, t) {
487
+ var n;
488
+ (n = this.listeners.get(e)) == null || n.forEach((i) => i(t));
489
+ }
490
+ // ─── Lifecycle ───────────────────────────────────────────────────────────
491
+ /**
492
+ * Connect to the Voice AI agent and start the call.
493
+ * Requests microphone permission, establishes WebRTC, and opens the data channel.
494
+ */
495
+ async start() {
496
+ if (this._status === "connecting" || this._status === "connected")
497
+ return console.warn("[VaniraAI] Already connecting or connected. Call stop() first."), {
498
+ callId: this.callId || "",
499
+ prospectId: this.prospectId || "",
500
+ serverUrl: this.serverUrl || ""
501
+ };
502
+ this._setStatus("connecting");
503
+ const e = this.config.serverUrl || (this.config.apiKey ? "" : this._inferServerUrl());
504
+ try {
505
+ return this.client = new b({
506
+ serverUrl: e,
507
+ agentId: this.config.agentId,
508
+ callId: this.config.callId,
509
+ prospectId: this.config.prospectId,
510
+ sessionBehavior: this.config.sessionBehavior,
511
+ token: this.config.token,
512
+ apiKey: this.config.apiKey,
513
+ backendUrl: this.config.backendUrl,
514
+ iceServers: this.config.iceServers,
515
+ runtime: this.config.runtime,
516
+ onSessionStarted: (t) => {
517
+ this.emit("session_started", t);
518
+ },
519
+ onConnected: () => {
520
+ this._setStatus("connected"), this.emit("connected");
521
+ },
522
+ onDisconnected: () => {
523
+ this._setStatus("disconnected"), this.emit("disconnected");
524
+ },
525
+ onError: (t) => {
526
+ this._setStatus("error"), this.emit("error", typeof t == "string" ? t : (t == null ? void 0 : t.message) || "Connection failed");
527
+ },
528
+ onTranscription: (t, n) => {
529
+ this.emit("transcription", { text: t, isFinal: n });
530
+ },
531
+ // @ts-ignore - onClientToolCall is on the dashboard's WebRTCClient; proxy it here
532
+ onClientToolCall: (t) => {
533
+ const n = (t == null ? void 0 : t.data) || t, i = {
534
+ name: n.name || n.tool_name || "",
535
+ arguments: n.arguments || n.args || {},
536
+ tool_call_id: n.tool_call_id || n.call_id || "",
537
+ execution_mode: n.execution_mode || "fire_and_forget",
538
+ client_fields: n.client_fields || {}
539
+ };
540
+ this.emit("tool_call", i);
541
+ },
542
+ // @ts-ignore - onRemoteTrack is on the dashboard's WebRTCClient; proxy it here
543
+ onRemoteTrack: (t, n) => {
544
+ this.emit("track", { track: t, stream: n });
545
+ }
546
+ }), await this.client.connect(), {
547
+ callId: this.callId || "",
548
+ prospectId: this.prospectId || "",
549
+ serverUrl: this.serverUrl || ""
550
+ };
551
+ } catch (t) {
552
+ throw this._setStatus("error"), this.emit("error", (t == null ? void 0 : t.message) || "Failed to start call"), t;
553
+ }
554
+ }
555
+ /**
556
+ * Disconnect the call and release all resources (microphone, WebRTC connection).
557
+ */
558
+ stop() {
559
+ this.client && (this.client.disconnect(), this.client = null), this._setStatus("disconnected");
560
+ }
561
+ // ─── Client → Server Actions ─────────────────────────────────────────────
562
+ /**
563
+ * Send the result of a **blocking** client-side tool back to the AI.
564
+ * The AI is paused and will resume once it receives this.
565
+ *
566
+ * @param toolCallId - The `tool_call_id` from the `tool_call` event
567
+ * @param result - Arbitrary JSON payload the AI needs to continue
568
+ *
569
+ * @example
570
+ * client.sendToolResult(tool_call_id, { selected_store: 'Store #5' });
571
+ */
572
+ sendToolResult(e, t) {
573
+ this._assertConnected("sendToolResult"), this.client.sendToolResult(e, t);
574
+ }
575
+ /**
576
+ * Send an error result for a **blocking** client-side tool.
577
+ * Use when the user cancelled or the UI crashed.
578
+ *
579
+ * @param toolCallId - The `tool_call_id` from the `tool_call` event
580
+ * @param errorMessage - Human-readable error description
581
+ */
582
+ sendToolError(e, t) {
583
+ this._assertConnected("sendToolError"), this.client.sendEvent("client_tool_result", {
584
+ call_id: e,
585
+ result: { status: "error", error: t }
586
+ });
587
+ }
588
+ /**
589
+ * Silently update the AI's context with what the user is currently doing on screen.
590
+ * Does NOT interrupt the AI's current speech.
591
+ *
592
+ * @example
593
+ * client.updateContext({ active_page: 'Checkout', item: 'Nike Shoes', price: 149.99 });
594
+ */
595
+ updateContext(e) {
596
+ this._assertConnected("updateContext"), this.client.sendContextUpdate(e);
597
+ }
598
+ /**
599
+ * Force the AI to stop speaking and immediately react to a UI event.
600
+ * Use when the user performs a significant action (e.g., clicks a button, opens a page).
601
+ *
602
+ * Do NOT call this after `uploadMedia()` — `uploadMedia()` already sends
603
+ * `client_media_update` which is the only notification the backend needs.
604
+ * Calling `triggerInterrupt` after an upload causes double-TTS.
605
+ *
606
+ * @param actionName - A descriptive name for the action (e.g., 'user_clicked_cart')
607
+ * @param data - Optional context about the action
608
+ *
609
+ * @example
610
+ * client.triggerInterrupt('user_opened_checkout', { cart_value: 299 });
611
+ */
612
+ triggerInterrupt(e, t = {}) {
613
+ this._assertConnected("triggerInterrupt"), this.client.triggerActionInterrupt(), this.client.sendActionTrigger(e, t);
614
+ }
615
+ /**
616
+ * Cut the AI's current audio mid-sentence WITHOUT sending a client_action_trigger.
617
+ * Use this when you want to interrupt the agent but the next notification will be
618
+ * sent by the SDK itself (e.g. right before `uploadMedia()`).
619
+ */
620
+ interruptAudioOnly() {
621
+ this._assertConnected("interruptAudioOnly"), this.client.triggerActionInterrupt();
622
+ }
623
+ /**
624
+ * Upload a file (photo, document, screenshot) to the AI agent during a live call.
625
+ * Handles the HTTP upload and DataChannel notification automatically.
626
+ *
627
+ * ⚠️ This method is SELF-CONTAINED:
628
+ * 1. POSTs the file to /media/upload
629
+ * 2. Sends `client_media_update` via the DataChannel
630
+ *
631
+ * The backend LLM receives `client_media_update` and calls `process_media` — NO
632
+ * additional `triggerInterrupt()` or `sendActionTrigger()` is needed.
633
+ * Calling those after `uploadMedia()` will cause a SECOND LLM response (double TTS).
634
+ *
635
+ * @param file - File or Blob to upload (JPEG, PNG, GIF, WebP, PDF — max 50 MB)
636
+ * @param reason - Routing key: 'general' | 'kyc_photo' | 'damage_photo' | 'selfie' | 'document' | 'screenshot'
637
+ * @param message - Optional text the user wants to say about the file
638
+ * @param interruptFirst - Pass `true` to cut the AI's current audio before uploading
639
+ * (sends `action_interrupt` only — NOT `client_action_trigger`)
640
+ * @returns `{ media_id, url }` on success
641
+ *
642
+ * @example
643
+ * // Correct — one event, full context, LLM gets everything:
644
+ * const result = await client.uploadMedia(file, 'person_verification', 'Here is my ID', true);
645
+ *
646
+ * // ❌ Wrong — causes double TTS:
647
+ * const result = await client.uploadMedia(file, 'person_verification');
648
+ * client.triggerInterrupt('media_uploaded', { url: result.url }); // don't do this
649
+ */
650
+ async uploadMedia(e, t = "general", n = "", i = !1) {
651
+ this._assertConnected("uploadMedia"), i && this.client.triggerActionInterrupt();
652
+ const r = 50 * 1024 * 1024;
653
+ if (e.size > r)
654
+ throw new Error(`Upload failed: file is too large (${(e.size / 1024 / 1024).toFixed(1)} MB). Maximum is 50 MB.`);
655
+ const l = [
656
+ "image/jpeg",
657
+ "image/png",
658
+ "image/gif",
659
+ "image/webp",
660
+ "image/bmp",
661
+ "application/pdf"
662
+ ], d = e.type;
663
+ if (d && !l.includes(d))
664
+ throw new Error(`Upload failed: unsupported file type '${d}'. Supported: JPEG, PNG, GIF, WebP, BMP, PDF.`);
665
+ return this.client.uploadMedia(e, t, n);
666
+ }
667
+ // ─── Private helpers ─────────────────────────────────────────────────────
668
+ _setStatus(e) {
669
+ this._status = e;
670
+ }
671
+ _inferServerUrl() {
672
+ return (typeof import.meta < "u" && y || {}).VITE_WEBRTC_SERVER_URL || "https://in-godspeed.travelr.club";
673
+ }
674
+ _assertConnected(e) {
675
+ if (!this.client || !this.isConnected)
676
+ throw new Error(`[VaniraAI] Cannot call ${e}() when not connected. Call start() first.`);
677
+ }
678
+ }
679
+ export {
680
+ k as V,
681
+ b as W
682
+ };