@vanira/sdk-react-native 0.0.2
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/README.md +239 -0
- package/package.json +53 -0
- package/src/__tests__/WebRTCClient.integration.test.ts +396 -0
- package/src/__tests__/adapters.test.ts +475 -0
- package/src/__tests__/httpResponse.test.ts +25 -0
- package/src/__tests__/mocks/react-native-incall-manager.ts +8 -0
- package/src/__tests__/mocks/react-native-permissions.ts +15 -0
- package/src/__tests__/mocks/react-native-webrtc.ts +6 -0
- package/src/__tests__/mocks/react-native.ts +28 -0
- package/src/__tests__/preset.test.ts +239 -0
- package/src/__tests__/resolveRuntimeConfig.test.ts +90 -0
- package/src/__tests__/storage.test.ts +211 -0
- package/src/__tests__/webrtcSignaling.test.ts +42 -0
- package/src/adapters/PeerConnectionAdapter.ts +101 -0
- package/src/adapters/browser/BrowserAudioAdapter.ts +43 -0
- package/src/adapters/browser/BrowserDataChannelAdapter.ts +69 -0
- package/src/adapters/browser/BrowserMediaAdapter.ts +15 -0
- package/src/adapters/browser/BrowserPeerAdapter.ts +14 -0
- package/src/adapters/browser/index.ts +4 -0
- package/src/adapters/interfaces.ts +84 -0
- package/src/adapters/react-native/RNAudioAdapter.ts +42 -0
- package/src/adapters/react-native/RNDataChannelAdapter.ts +79 -0
- package/src/adapters/react-native/RNMediaAdapter.ts +46 -0
- package/src/adapters/react-native/RNPeerAdapter.ts +28 -0
- package/src/adapters/react-native/callAudioRouting.ts +115 -0
- package/src/adapters/react-native/decodeUtf8.ts +72 -0
- package/src/adapters/react-native/index.ts +4 -0
- package/src/adapters/react-native/rnUploadFile.ts +76 -0
- package/src/adapters/storage/BrowserDualStorageAdapter.ts +71 -0
- package/src/adapters/storage/MemoryStorageAdapter.ts +50 -0
- package/src/adapters/storage/StorageAdapter.ts +21 -0
- package/src/adapters/storage/createSyncStorageAdapter.ts +40 -0
- package/src/adapters/storage/index.ts +7 -0
- package/src/api/services/ChatService.ts +304 -0
- package/src/api/services/ConfigService.ts +33 -0
- package/src/assets/icons.js +35 -0
- package/src/cdn.ts +68 -0
- package/src/core/CallSessionStore.ts +137 -0
- package/src/core/DraggableController.ts +83 -0
- package/src/core/SessionManager.ts +322 -0
- package/src/core/VaniraAI.ts +464 -0
- package/src/core/WebRTCClient.ts +1012 -0
- package/src/core/httpResponse.ts +22 -0
- package/src/core/iceServers.ts +18 -0
- package/src/core/toolCallNormalize.ts +80 -0
- package/src/core/voice-client.js +236 -0
- package/src/core/webrtcSignaling.ts +72 -0
- package/src/index.js +34 -0
- package/src/index.ts +6 -0
- package/src/platforms/browser.ts +67 -0
- package/src/platforms/react-native.ts +105 -0
- package/src/presets/BookingCalendarModal.tsx +457 -0
- package/src/presets/CameraModal.tsx +576 -0
- package/src/presets/DynamicFormModal.tsx +378 -0
- package/src/presets/NativePresetRenderer.tsx +350 -0
- package/src/presets/NavigateHandler.tsx +75 -0
- package/src/presets/PresetHost.tsx +155 -0
- package/src/presets/PresetShellModal.tsx +97 -0
- package/src/presets/UploadModal.tsx +321 -0
- package/src/presets/calendar/calendarUtils.ts +386 -0
- package/src/presets/call/CallSpeakerToggle.tsx +59 -0
- package/src/presets/call/callAudioRouting.ts +2 -0
- package/src/presets/call/useCallSpeaker.ts +31 -0
- package/src/presets/camera/cameraPermissions.ts +18 -0
- package/src/presets/camera/cameraStream.ts +19 -0
- package/src/presets/camera/cameraUtils.ts +21 -0
- package/src/presets/camera/useLivenessFlow.ts +95 -0
- package/src/presets/chalkboard/ChalkboardOverlay.tsx +156 -0
- package/src/presets/chalkboard/EraseTextHandler.tsx +95 -0
- package/src/presets/chalkboard/TypeTextHandler.tsx +107 -0
- package/src/presets/chalkboard/boardAbort.ts +36 -0
- package/src/presets/chalkboard/boardQueue.ts +620 -0
- package/src/presets/chalkboard/chalkboardSession.ts +75 -0
- package/src/presets/chalkboard/drawUtils.ts +123 -0
- package/src/presets/chalkboard/textUtils.ts +109 -0
- package/src/presets/clipRegion/ClipRegionModal.tsx +261 -0
- package/src/presets/clipRegion/clipRegionBridge.ts +19 -0
- package/src/presets/form/formValidation.ts +104 -0
- package/src/presets/form/parseFormFields.ts +171 -0
- package/src/presets/host/HostElementPresetHandler.tsx +155 -0
- package/src/presets/host/hostPresetBridge.ts +71 -0
- package/src/presets/index.ts +63 -0
- package/src/presets/liveScreen/CloseLiveScreenHandler.tsx +36 -0
- package/src/presets/liveScreen/LiveScreenCaptureHost.tsx +312 -0
- package/src/presets/liveScreen/LiveScreenHandler.tsx +25 -0
- package/src/presets/liveScreen/LiveScreenPipOverlay.tsx +6 -0
- package/src/presets/liveScreen/liveScreenSession.ts +73 -0
- package/src/presets/liveVision/CloseLiveVisionHandler.tsx +29 -0
- package/src/presets/liveVision/LiveVisionCameraHost.tsx +317 -0
- package/src/presets/liveVision/LiveVisionHandler.tsx +26 -0
- package/src/presets/liveVision/LiveVisionPipOverlay.tsx +7 -0
- package/src/presets/liveVision/liveVisionFrameLoop.ts +38 -0
- package/src/presets/liveVision/liveVisionSession.ts +75 -0
- package/src/presets/liveVision/liveVisionUpload.ts +62 -0
- package/src/presets/navigation/internalRouteRegistry.ts +25 -0
- package/src/presets/navigation/navigationBridge.ts +76 -0
- package/src/presets/navigation/navigationTypes.ts +12 -0
- package/src/presets/parseToolCall.ts +60 -0
- package/src/presets/presetClientAdapter.ts +29 -0
- package/src/presets/presetCompletion.ts +91 -0
- package/src/presets/presetEventHelpers.ts +45 -0
- package/src/presets/registry.ts +128 -0
- package/src/presets/streaming/mediaFrameUpload.ts +93 -0
- package/src/presets/types.ts +74 -0
- package/src/presets/upload/pickUploadFile.ts +256 -0
- package/src/presets/upload/uploadFormats.ts +163 -0
- package/src/presets/upload/uploadUtils.ts +68 -0
- package/src/react/PresetRenderer.tsx +144 -0
- package/src/react/index.ts +1 -0
- package/src/runtime/browserRuntime.ts +54 -0
- package/src/runtime/platform.ts +17 -0
- package/src/runtime/reactNativeRuntime.ts +68 -0
- package/src/runtime/resolveRuntimeConfig.ts +75 -0
- package/src/runtime/runtimeBundles.ts +74 -0
- package/src/runtime/types.ts +135 -0
- package/src/types/react-native-incall-manager.d.ts +17 -0
- package/src/types/react-native-webrtc.d.ts +47 -0
- package/src/types.ts +133 -0
- package/src/ui/VaniraWidget.ts +87 -0
- package/src/ui/abstraction/AbstractWidgetProvider.ts +18 -0
- package/src/ui/abstraction/interfaces.ts +12 -0
- package/src/ui/adapters/VaniraChatAdapter.ts +42 -0
- package/src/ui/components/AvatarView.ts +81 -0
- package/src/ui/components/ChatWindow.ts +263 -0
- package/src/ui/components/FloatingButton.ts +163 -0
- package/src/ui/components/FloatingWelcomeChips.ts +137 -0
- package/src/ui/components/Panel.ts +120 -0
- package/src/ui/components/VoiceOrb.ts +79 -0
- package/src/ui/components/VoiceOverlay.ts +497 -0
- package/src/ui/components/index.ts +7 -0
- package/src/ui/factory/WidgetFactory.ts +16 -0
- package/src/ui/icons_data.ts +2 -0
- package/src/ui/presets/WidgetPresetRenderer.ts +1802 -0
- package/src/ui/presets/types.ts +16 -0
- package/src/ui/providers/VaniraInternalProvider.ts +1066 -0
- package/src/ui/styles/index.ts +323 -0
- package/src/ui/styles/keyframes.ts +76 -0
- package/src/ui/styles/theme.ts +57 -0
- package/src/ui/styles/widget.css.ts +838 -0
- package/src/ui/utils.ts +37 -0
- package/src/ui/views/AbstractChatView.ts +93 -0
- package/src/ui/views/AbstractVoiceView.ts +57 -0
- package/src/ui/views/AvatarOnlyView.ts +78 -0
- package/src/ui/views/ChatAvatarView.ts +66 -0
- package/src/ui/views/ChatOnlyView.ts +28 -0
- package/src/ui/views/ChatVoiceView.ts +15 -0
- package/src/ui/views/VoiceOnlyView.ts +25 -0
- package/src/ui/views/index.ts +5 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VaniraAI SDK
|
|
3
|
+
* ============
|
|
4
|
+
* A simple, developer-friendly Voice AI client.
|
|
5
|
+
* Hides all WebRTC complexity behind a clean event-emitter API.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const client = new VaniraAI({ agentId: 'your-agent-id', token: 'Bearer your-token' });
|
|
10
|
+
* client.on('tool_call', ({ name, arguments: args, tool_call_id, execution_mode }) => {
|
|
11
|
+
* // render your UI component here
|
|
12
|
+
* if (execution_mode === 'blocking') {
|
|
13
|
+
* client.sendToolResult(tool_call_id, { success: true });
|
|
14
|
+
* }
|
|
15
|
+
* });
|
|
16
|
+
* await client.start();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { WebRTCClient } from './WebRTCClient';
|
|
21
|
+
import { normalizeToolCallFields } from './toolCallNormalize';
|
|
22
|
+
import {
|
|
23
|
+
uploadFileByteSize,
|
|
24
|
+
normalizeUploadMimeForUpload,
|
|
25
|
+
type UploadMediaInput,
|
|
26
|
+
} from '../adapters/react-native/rnUploadFile';
|
|
27
|
+
|
|
28
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export type VaniraAIStatus = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
31
|
+
|
|
32
|
+
export interface VaniraAIConfig {
|
|
33
|
+
/** Your Vanira agent ID from the dashboard */
|
|
34
|
+
agentId: string;
|
|
35
|
+
/** Optional prospect ID. If supplied, the SDK resumes the prospect context. */
|
|
36
|
+
prospectId?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Determines the lifecycle behavior when starting a connection:
|
|
39
|
+
* - 'continue': Reconnects/resumes the call using the provided `callId` or the stored sessionStorage call session.
|
|
40
|
+
* - 'new' (default): Ignores any previous call state and provisions a brand-new call.
|
|
41
|
+
*/
|
|
42
|
+
sessionBehavior?: 'continue' | 'new';
|
|
43
|
+
/** Bearer token for authentication. Required for ICE server fetching. */
|
|
44
|
+
token?: string;
|
|
45
|
+
/** Override the WebRTC server URL. Auto-detected if omitted. */
|
|
46
|
+
serverUrl?: string;
|
|
47
|
+
/** Override the GraphQL endpoint for config fetching. */
|
|
48
|
+
graphqlEndpoint?: string;
|
|
49
|
+
/** Optional: pre-supply your own ICE servers (skips auto-fetch). */
|
|
50
|
+
iceServers?: RTCIceServer[];
|
|
51
|
+
/** Optional call ID override. Auto-generated if omitted. */
|
|
52
|
+
callId?: string;
|
|
53
|
+
/** Your sk_live_* or pk_live_* API key. Required for client-side call creation. */
|
|
54
|
+
apiKey?: string;
|
|
55
|
+
/** Override the default backend URL (e.g. http://localhost:8000). */
|
|
56
|
+
backendUrl?: string;
|
|
57
|
+
/** Platform runtime configuration */
|
|
58
|
+
runtime?: any;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ClientToolCall {
|
|
62
|
+
/** The ref_code of the triggered action */
|
|
63
|
+
name: string;
|
|
64
|
+
/** The mapped payload from client_action_fields */
|
|
65
|
+
arguments: Record<string, any>;
|
|
66
|
+
/** Unique ID for this execution. Required when sending a result back for blocking tools. */
|
|
67
|
+
tool_call_id: string;
|
|
68
|
+
/** Whether the AI is paused waiting for result ('blocking') or keeps talking ('fire_and_forget') */
|
|
69
|
+
execution_mode: 'blocking' | 'fire_and_forget';
|
|
70
|
+
/** Dynamic UI configuration parameters for zero-code presets */
|
|
71
|
+
client_fields?: Record<string, any>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TranscriptionEvent {
|
|
75
|
+
text: string;
|
|
76
|
+
isFinal: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type EventMap = {
|
|
80
|
+
/** Fired when WebRTC connection is fully established */
|
|
81
|
+
connected: void;
|
|
82
|
+
/** Fired when the connection is lost or explicitly disconnected */
|
|
83
|
+
disconnected: void;
|
|
84
|
+
/** Fired on connection or protocol error. Payload is an error message string. */
|
|
85
|
+
error: string;
|
|
86
|
+
/** Fired whenever the AI transcribes speech (both interim and final) */
|
|
87
|
+
transcription: TranscriptionEvent;
|
|
88
|
+
/**
|
|
89
|
+
* Fired when a preset tool call is received (before tool_call).
|
|
90
|
+
* Raw toolCall shape matches WebRTCClient onPreset callback.
|
|
91
|
+
*/
|
|
92
|
+
preset: { toolCall: unknown; client: WebRTCClient };
|
|
93
|
+
/** Fired when the AI triggers a client-side tool (action) */
|
|
94
|
+
tool_call: ClientToolCall;
|
|
95
|
+
/** Fired when a remote media track (like video) is received */
|
|
96
|
+
track: { track: MediaStreamTrack; stream: MediaStream };
|
|
97
|
+
/** Fired when the prospect and call session are successfully generated/retrieved */
|
|
98
|
+
session_started: { prospectId: string; callId: string; serverUrl: string };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type EventCallback<T> = T extends void ? () => void : (payload: T) => void;
|
|
102
|
+
|
|
103
|
+
// ─── VaniraAI Class ────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export class VaniraAI {
|
|
106
|
+
private config: VaniraAIConfig;
|
|
107
|
+
private _status: VaniraAIStatus = 'idle';
|
|
108
|
+
private client: WebRTCClient | null = null;
|
|
109
|
+
private listeners: Map<string, Set<Function>> = new Map();
|
|
110
|
+
|
|
111
|
+
constructor(config: VaniraAIConfig) {
|
|
112
|
+
if (!config.agentId) throw new Error('[VaniraAI] agentId is required');
|
|
113
|
+
this.config = config;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Getters ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/** Returns the active WebRTC worker/server URL */
|
|
119
|
+
get serverUrl(): string | undefined {
|
|
120
|
+
return this.client?.serverUrl;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Returns the active call ID */
|
|
124
|
+
get callId(): string | undefined {
|
|
125
|
+
return this.client?.callId;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Returns the active prospect ID associated with this session */
|
|
129
|
+
get prospectId(): string | undefined {
|
|
130
|
+
return this.client?.prospectId;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Status ──────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/** Current connection status */
|
|
136
|
+
get status(): VaniraAIStatus {
|
|
137
|
+
return this._status;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Shorthand: true when status is 'connected' */
|
|
141
|
+
get isConnected(): boolean {
|
|
142
|
+
return this._status === 'connected';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Event Emitter ───────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Register an event listener.
|
|
149
|
+
* @param event - Event name
|
|
150
|
+
* @param callback - Handler function
|
|
151
|
+
* @returns `this` for chaining
|
|
152
|
+
*/
|
|
153
|
+
on<K extends keyof EventMap>(event: K, callback: EventCallback<EventMap[K]>): this {
|
|
154
|
+
if (!this.listeners.has(event)) {
|
|
155
|
+
this.listeners.set(event, new Set());
|
|
156
|
+
}
|
|
157
|
+
this.listeners.get(event)!.add(callback);
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Remove a previously registered event listener.
|
|
163
|
+
* @param event - Event name
|
|
164
|
+
* @param callback - The exact same function reference that was registered
|
|
165
|
+
*/
|
|
166
|
+
off<K extends keyof EventMap>(event: K, callback: EventCallback<EventMap[K]>): this {
|
|
167
|
+
this.listeners.get(event)?.delete(callback);
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private emit<K extends keyof EventMap>(event: K, payload?: EventMap[K]): void {
|
|
172
|
+
this.listeners.get(event)?.forEach(cb => cb(payload));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Lifecycle ───────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Connect to the Voice AI agent and start the call.
|
|
179
|
+
* Requests microphone permission, establishes WebRTC, and opens the data channel.
|
|
180
|
+
*/
|
|
181
|
+
async start(): Promise<{ callId: string; prospectId: string; serverUrl: string }> {
|
|
182
|
+
if (this._status === 'connecting' || this._status === 'connected') {
|
|
183
|
+
console.warn('[VaniraAI] Already connecting or connected. Call stop() first.');
|
|
184
|
+
return {
|
|
185
|
+
callId: this.callId || '',
|
|
186
|
+
prospectId: this.prospectId || '',
|
|
187
|
+
serverUrl: this.serverUrl || '',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this._setStatus('connecting');
|
|
192
|
+
|
|
193
|
+
const serverUrl = this.config.serverUrl || (this.config.apiKey ? '' : this._inferServerUrl());
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
this.client = new WebRTCClient({
|
|
197
|
+
serverUrl,
|
|
198
|
+
agentId: this.config.agentId,
|
|
199
|
+
callId: this.config.callId,
|
|
200
|
+
prospectId: this.config.prospectId,
|
|
201
|
+
sessionBehavior: this.config.sessionBehavior,
|
|
202
|
+
token: this.config.token,
|
|
203
|
+
apiKey: this.config.apiKey,
|
|
204
|
+
backendUrl: this.config.backendUrl,
|
|
205
|
+
iceServers: this.config.iceServers,
|
|
206
|
+
runtime: this.config.runtime,
|
|
207
|
+
onSessionStarted: (payload) => {
|
|
208
|
+
this.emit('session_started', payload);
|
|
209
|
+
},
|
|
210
|
+
onConnected: () => {
|
|
211
|
+
this._setStatus('connected');
|
|
212
|
+
this.emit('connected');
|
|
213
|
+
},
|
|
214
|
+
onDisconnected: () => {
|
|
215
|
+
this._setStatus('disconnected');
|
|
216
|
+
this.emit('disconnected');
|
|
217
|
+
},
|
|
218
|
+
onError: (err: any) => {
|
|
219
|
+
this._setStatus('error');
|
|
220
|
+
this.emit('error', typeof err === 'string' ? err : (err?.message || 'Connection failed'));
|
|
221
|
+
},
|
|
222
|
+
onTranscription: (text: string, isFinal: boolean) => {
|
|
223
|
+
this.emit('transcription', { text, isFinal });
|
|
224
|
+
},
|
|
225
|
+
onPreset: (payload) => {
|
|
226
|
+
this.emit('preset', payload);
|
|
227
|
+
},
|
|
228
|
+
// @ts-ignore - onClientToolCall is on the dashboard's WebRTCClient; proxy it here
|
|
229
|
+
onClientToolCall: (rawCall: any) => {
|
|
230
|
+
const normalized = normalizeToolCallFields(rawCall);
|
|
231
|
+
const toolCall: ClientToolCall = {
|
|
232
|
+
name: normalized.name,
|
|
233
|
+
arguments: normalized.arguments,
|
|
234
|
+
tool_call_id: normalized.tool_call_id,
|
|
235
|
+
execution_mode: normalized.execution_mode as ClientToolCall['execution_mode'],
|
|
236
|
+
client_fields: normalized.client_fields,
|
|
237
|
+
};
|
|
238
|
+
this.emit('tool_call', toolCall);
|
|
239
|
+
},
|
|
240
|
+
// @ts-ignore - onRemoteTrack is on the dashboard's WebRTCClient; proxy it here
|
|
241
|
+
onRemoteTrack: (track: MediaStreamTrack, stream: MediaStream) => {
|
|
242
|
+
this.emit('track', { track, stream });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await this.client.connect();
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
callId: this.callId || '',
|
|
250
|
+
prospectId: this.prospectId || '',
|
|
251
|
+
serverUrl: this.serverUrl || '',
|
|
252
|
+
};
|
|
253
|
+
} catch (err: any) {
|
|
254
|
+
this._setStatus('error');
|
|
255
|
+
this.emit('error', err?.message || 'Failed to start call');
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Disconnect the call and release all resources (microphone, WebRTC connection).
|
|
262
|
+
*/
|
|
263
|
+
stop(): void {
|
|
264
|
+
if (this.client) {
|
|
265
|
+
this.client.disconnect();
|
|
266
|
+
this.client = null;
|
|
267
|
+
}
|
|
268
|
+
this._setStatus('disconnected');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Mute or unmute your microphone while the call stays connected. */
|
|
272
|
+
setMicrophoneMuted(muted: boolean): void {
|
|
273
|
+
this.client?.setMicrophoneMuted(muted);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
isMicrophoneMuted(): boolean {
|
|
277
|
+
return this.client?.isMicrophoneMuted() ?? false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Client → Server Actions ─────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Send the result of a **blocking** client-side tool back to the AI.
|
|
284
|
+
* The AI is paused and will resume once it receives this.
|
|
285
|
+
*
|
|
286
|
+
* @param toolCallId - The `tool_call_id` from the `tool_call` event
|
|
287
|
+
* @param result - Arbitrary JSON payload the AI needs to continue
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* client.sendToolResult(tool_call_id, { selected_store: 'Store #5' });
|
|
291
|
+
*/
|
|
292
|
+
sendToolResult(toolCallId: string, result: Record<string, any>): void {
|
|
293
|
+
this._assertConnected('sendToolResult');
|
|
294
|
+
this.client!.sendToolResult(toolCallId, result);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Send an error result for a **blocking** client-side tool.
|
|
299
|
+
* Use when the user cancelled or the UI crashed.
|
|
300
|
+
*
|
|
301
|
+
* @param toolCallId - The `tool_call_id` from the `tool_call` event
|
|
302
|
+
* @param errorMessage - Human-readable error description
|
|
303
|
+
*/
|
|
304
|
+
sendToolError(toolCallId: string, errorMessage: string): void {
|
|
305
|
+
this._assertConnected('sendToolError');
|
|
306
|
+
// Fallback: use the generic sendEvent until SDK's WebRTCClient is fully updated
|
|
307
|
+
this.client!.sendEvent('client_tool_result', {
|
|
308
|
+
call_id: toolCallId,
|
|
309
|
+
result: { status: 'error', error: errorMessage }
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Silently update the AI's context with what the user is currently doing on screen.
|
|
315
|
+
* Does NOT interrupt the AI's current speech.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* client.updateContext({ active_page: 'Checkout', item: 'Nike Shoes', price: 149.99 });
|
|
319
|
+
*/
|
|
320
|
+
updateContext(context: Record<string, any>): void {
|
|
321
|
+
this._assertConnected('updateContext');
|
|
322
|
+
this.client!.sendContextUpdate(context);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Force the AI to stop speaking and immediately react to a UI event.
|
|
327
|
+
* Use when the user performs a significant action (e.g., clicks a button, opens a page).
|
|
328
|
+
*
|
|
329
|
+
* Do NOT call this after `uploadMedia()` — `uploadMedia()` already sends
|
|
330
|
+
* `client_media_update` which is the only notification the backend needs.
|
|
331
|
+
* Calling `triggerInterrupt` after an upload causes double-TTS.
|
|
332
|
+
*
|
|
333
|
+
* @param actionName - A descriptive name for the action (e.g., 'user_clicked_cart')
|
|
334
|
+
* @param data - Optional context about the action
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* client.triggerInterrupt('user_opened_checkout', { cart_value: 299 });
|
|
338
|
+
*/
|
|
339
|
+
triggerInterrupt(actionName: string, data: Record<string, any> = {}): void {
|
|
340
|
+
this._assertConnected('triggerInterrupt');
|
|
341
|
+
this.client!.triggerActionInterrupt();
|
|
342
|
+
this.client!.sendActionTrigger(actionName, data);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Cut the AI's current audio mid-sentence WITHOUT sending a client_action_trigger.
|
|
347
|
+
* Use this when you want to interrupt the agent but the next notification will be
|
|
348
|
+
* sent by the SDK itself (e.g. right before `uploadMedia()`).
|
|
349
|
+
*/
|
|
350
|
+
interruptAudioOnly(): void {
|
|
351
|
+
this._assertConnected('interruptAudioOnly');
|
|
352
|
+
this.client!.triggerActionInterrupt();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Send a raw DataChannel event to the server.
|
|
357
|
+
* Used for protocol-level signals that don't have a higher-level API method
|
|
358
|
+
* (e.g. `client_live_camera_started`, `client_media_frame`, `client_live_camera_stopped`).
|
|
359
|
+
*
|
|
360
|
+
* @param event - Event name string
|
|
361
|
+
* @param data - Optional payload merged into the DataChannel message
|
|
362
|
+
*/
|
|
363
|
+
sendEvent(event: string, data: Record<string, any> = {}): void {
|
|
364
|
+
this._assertConnected('sendEvent');
|
|
365
|
+
this.client!.sendEvent(event, data);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Observe inbound DataChannel control events (preset harness / debugging).
|
|
370
|
+
* Install after `start()` resolves. Returns false if the WebRTC client is missing.
|
|
371
|
+
*/
|
|
372
|
+
tapControlEvents(handler: (event: string, msg: Record<string, unknown>) => void): boolean {
|
|
373
|
+
if (!this.client) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
return this.client.tapControlEvents((msg) => {
|
|
377
|
+
handler(String(msg.event ?? 'unknown'), msg as Record<string, unknown>);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Harness-only: inject a synthetic inbound DataChannel event (e.g. client_tool_call). */
|
|
382
|
+
injectControlEvent(msg: Record<string, unknown>): void {
|
|
383
|
+
this._assertConnected('injectControlEvent');
|
|
384
|
+
this.client!.injectControlEvent(msg as import('../types').ControlEvent);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Upload a file (photo, document, screenshot) to the AI agent during a live call.
|
|
389
|
+
* Handles the HTTP upload and DataChannel notification automatically.
|
|
390
|
+
*
|
|
391
|
+
* ⚠️ This method is SELF-CONTAINED:
|
|
392
|
+
* 1. POSTs the file to /media/upload
|
|
393
|
+
* 2. Sends `client_media_update` via the DataChannel
|
|
394
|
+
*
|
|
395
|
+
* The backend LLM receives `client_media_update` and calls `process_media` — NO
|
|
396
|
+
* additional `triggerInterrupt()` or `sendActionTrigger()` is needed.
|
|
397
|
+
* Calling those after `uploadMedia()` will cause a SECOND LLM response (double TTS).
|
|
398
|
+
*
|
|
399
|
+
* @param file - File or Blob to upload (JPEG, PNG, GIF, WebP, PDF — max 50 MB)
|
|
400
|
+
* @param reason - Routing key: 'general' | 'kyc_photo' | 'damage_photo' | 'selfie' | 'document' | 'screenshot'
|
|
401
|
+
* @param message - Optional text the user wants to say about the file
|
|
402
|
+
* @param interruptFirst - Pass `true` to cut the AI's current audio before uploading
|
|
403
|
+
* (sends `action_interrupt` only — NOT `client_action_trigger`)
|
|
404
|
+
* @returns `{ media_id, url }` on success
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* // Correct — one event, full context, LLM gets everything:
|
|
408
|
+
* const result = await client.uploadMedia(file, 'person_verification', 'Here is my ID', true);
|
|
409
|
+
*
|
|
410
|
+
* // ❌ Wrong — causes double TTS:
|
|
411
|
+
* const result = await client.uploadMedia(file, 'person_verification');
|
|
412
|
+
* client.triggerInterrupt('media_uploaded', { url: result.url }); // don't do this
|
|
413
|
+
*/
|
|
414
|
+
async uploadMedia(
|
|
415
|
+
file: UploadMediaInput,
|
|
416
|
+
reason: string = 'general',
|
|
417
|
+
message: string = '',
|
|
418
|
+
interruptFirst: boolean = false
|
|
419
|
+
): Promise<{ media_id: string; url: string; content_type?: string }> {
|
|
420
|
+
this._assertConnected('uploadMedia');
|
|
421
|
+
|
|
422
|
+
// Optionally cut the AI's current audio — audio-only, no action_trigger
|
|
423
|
+
if (interruptFirst) {
|
|
424
|
+
this.client!.triggerActionInterrupt();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Client-side validation — 50 MB limit
|
|
428
|
+
const MAX_SIZE = 50 * 1024 * 1024;
|
|
429
|
+
const byteSize = uploadFileByteSize(file);
|
|
430
|
+
if (byteSize !== undefined && byteSize > MAX_SIZE) {
|
|
431
|
+
throw new Error(`Upload failed: file is too large (${(byteSize / 1024 / 1024).toFixed(1)} MB). Maximum is 50 MB.`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Supported MIME types — parity with web upload preset accept string
|
|
435
|
+
const SUPPORTED_TYPES = [
|
|
436
|
+
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp',
|
|
437
|
+
'application/pdf', 'text/plain', 'text/csv',
|
|
438
|
+
];
|
|
439
|
+
const mimeType = normalizeUploadMimeForUpload(file);
|
|
440
|
+
if (mimeType && !SUPPORTED_TYPES.includes(mimeType)) {
|
|
441
|
+
throw new Error(`Upload failed: unsupported file type '${mimeType}'. Supported: JPEG, JPG, PNG, GIF, WebP, BMP, PDF, TXT, CSV.`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// uploadMedia() in WebRTCClient uploads the file and sends client_media_update
|
|
445
|
+
// via the DataChannel. That single event is everything the backend needs.
|
|
446
|
+
return this.client!.uploadMedia(file, reason, message);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ─── Private helpers ─────────────────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
private _setStatus(status: VaniraAIStatus): void {
|
|
452
|
+
this._status = status;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private _inferServerUrl(): string {
|
|
456
|
+
return 'https://in-godspeed.travelr.club';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private _assertConnected(method: string): void {
|
|
460
|
+
if (!this.client || !this.isConnected) {
|
|
461
|
+
throw new Error(`[VaniraAI] Cannot call ${method}() when not connected. Call start() first.`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|