@vanira/sdk 0.0.41 → 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.
- package/dist/VaniraAI-D2KUxJJO.js +682 -0
- package/dist/VaniraAI-EFYOmJ2E.cjs +1 -0
- package/dist/__tests__/adapters.test.d.ts +14 -0
- package/dist/adapters/PeerConnectionAdapter.d.ts +90 -0
- package/dist/adapters/browser/BrowserAudioAdapter.d.ts +9 -0
- package/dist/adapters/browser/BrowserDataChannelAdapter.d.ts +16 -0
- package/dist/adapters/browser/BrowserMediaAdapter.d.ts +10 -0
- package/dist/adapters/browser/BrowserPeerAdapter.d.ts +9 -0
- package/dist/adapters/browser/index.d.ts +4 -0
- package/dist/adapters/interfaces.d.ts +70 -0
- package/dist/adapters/react-native/RNAudioAdapter.d.ts +9 -0
- package/dist/adapters/react-native/RNDataChannelAdapter.d.ts +16 -0
- package/dist/adapters/react-native/RNMediaAdapter.d.ts +17 -0
- package/dist/adapters/react-native/RNPeerAdapter.d.ts +19 -0
- package/dist/adapters/react-native/index.d.ts +4 -0
- package/dist/browser.d.ts +1 -0
- package/dist/core/VaniraAI.d.ts +2 -0
- package/dist/index.d.ts +21 -3
- package/dist/platforms/browser.cjs +8 -0
- package/dist/platforms/browser.d.ts +28 -0
- package/dist/platforms/browser.js +479 -0
- package/dist/platforms/react-native.cjs +1 -0
- package/dist/platforms/react-native.d.ts +46 -0
- package/dist/platforms/react-native.js +116 -0
- package/dist/react/PresetRenderer.d.ts +1 -2
- package/dist/react/index.d.ts +0 -4
- package/dist/react-native.d.ts +1 -0
- package/dist/runtime/browserRuntime.d.ts +17 -0
- package/dist/runtime/reactNativeRuntime.d.ts +18 -0
- package/dist/runtime/types.d.ts +96 -0
- package/dist/types.d.ts +36 -4
- package/dist/ui/presets/WidgetPresetRenderer.d.ts +2 -0
- package/dist/vanira-sdk.es.js +1788 -231
- package/dist/vanira-sdk.js +36 -36
- package/dist/vanira-sdk.js.map +1 -1
- package/dist/vanira-sdk.umd.js +1538 -0
- package/package.json +18 -3
- package/dist/react/presets/CalendarPreset.d.ts +0 -4
- package/dist/react/presets/CameraPreset.d.ts +0 -16
- package/dist/react/presets/FormPreset.d.ts +0 -4
- package/dist/react/presets/NavigatePreset.d.ts +0 -16
- package/dist/react/presets/UploadPreset.d.ts +0 -17
- package/dist/react/registry.d.ts +0 -3
- 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
|
+
};
|