@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.
Files changed (148) hide show
  1. package/README.md +239 -0
  2. package/package.json +53 -0
  3. package/src/__tests__/WebRTCClient.integration.test.ts +396 -0
  4. package/src/__tests__/adapters.test.ts +475 -0
  5. package/src/__tests__/httpResponse.test.ts +25 -0
  6. package/src/__tests__/mocks/react-native-incall-manager.ts +8 -0
  7. package/src/__tests__/mocks/react-native-permissions.ts +15 -0
  8. package/src/__tests__/mocks/react-native-webrtc.ts +6 -0
  9. package/src/__tests__/mocks/react-native.ts +28 -0
  10. package/src/__tests__/preset.test.ts +239 -0
  11. package/src/__tests__/resolveRuntimeConfig.test.ts +90 -0
  12. package/src/__tests__/storage.test.ts +211 -0
  13. package/src/__tests__/webrtcSignaling.test.ts +42 -0
  14. package/src/adapters/PeerConnectionAdapter.ts +101 -0
  15. package/src/adapters/browser/BrowserAudioAdapter.ts +43 -0
  16. package/src/adapters/browser/BrowserDataChannelAdapter.ts +69 -0
  17. package/src/adapters/browser/BrowserMediaAdapter.ts +15 -0
  18. package/src/adapters/browser/BrowserPeerAdapter.ts +14 -0
  19. package/src/adapters/browser/index.ts +4 -0
  20. package/src/adapters/interfaces.ts +84 -0
  21. package/src/adapters/react-native/RNAudioAdapter.ts +42 -0
  22. package/src/adapters/react-native/RNDataChannelAdapter.ts +79 -0
  23. package/src/adapters/react-native/RNMediaAdapter.ts +46 -0
  24. package/src/adapters/react-native/RNPeerAdapter.ts +28 -0
  25. package/src/adapters/react-native/callAudioRouting.ts +115 -0
  26. package/src/adapters/react-native/decodeUtf8.ts +72 -0
  27. package/src/adapters/react-native/index.ts +4 -0
  28. package/src/adapters/react-native/rnUploadFile.ts +76 -0
  29. package/src/adapters/storage/BrowserDualStorageAdapter.ts +71 -0
  30. package/src/adapters/storage/MemoryStorageAdapter.ts +50 -0
  31. package/src/adapters/storage/StorageAdapter.ts +21 -0
  32. package/src/adapters/storage/createSyncStorageAdapter.ts +40 -0
  33. package/src/adapters/storage/index.ts +7 -0
  34. package/src/api/services/ChatService.ts +304 -0
  35. package/src/api/services/ConfigService.ts +33 -0
  36. package/src/assets/icons.js +35 -0
  37. package/src/cdn.ts +68 -0
  38. package/src/core/CallSessionStore.ts +137 -0
  39. package/src/core/DraggableController.ts +83 -0
  40. package/src/core/SessionManager.ts +322 -0
  41. package/src/core/VaniraAI.ts +464 -0
  42. package/src/core/WebRTCClient.ts +1012 -0
  43. package/src/core/httpResponse.ts +22 -0
  44. package/src/core/iceServers.ts +18 -0
  45. package/src/core/toolCallNormalize.ts +80 -0
  46. package/src/core/voice-client.js +236 -0
  47. package/src/core/webrtcSignaling.ts +72 -0
  48. package/src/index.js +34 -0
  49. package/src/index.ts +6 -0
  50. package/src/platforms/browser.ts +67 -0
  51. package/src/platforms/react-native.ts +105 -0
  52. package/src/presets/BookingCalendarModal.tsx +457 -0
  53. package/src/presets/CameraModal.tsx +576 -0
  54. package/src/presets/DynamicFormModal.tsx +378 -0
  55. package/src/presets/NativePresetRenderer.tsx +350 -0
  56. package/src/presets/NavigateHandler.tsx +75 -0
  57. package/src/presets/PresetHost.tsx +155 -0
  58. package/src/presets/PresetShellModal.tsx +97 -0
  59. package/src/presets/UploadModal.tsx +321 -0
  60. package/src/presets/calendar/calendarUtils.ts +386 -0
  61. package/src/presets/call/CallSpeakerToggle.tsx +59 -0
  62. package/src/presets/call/callAudioRouting.ts +2 -0
  63. package/src/presets/call/useCallSpeaker.ts +31 -0
  64. package/src/presets/camera/cameraPermissions.ts +18 -0
  65. package/src/presets/camera/cameraStream.ts +19 -0
  66. package/src/presets/camera/cameraUtils.ts +21 -0
  67. package/src/presets/camera/useLivenessFlow.ts +95 -0
  68. package/src/presets/chalkboard/ChalkboardOverlay.tsx +156 -0
  69. package/src/presets/chalkboard/EraseTextHandler.tsx +95 -0
  70. package/src/presets/chalkboard/TypeTextHandler.tsx +107 -0
  71. package/src/presets/chalkboard/boardAbort.ts +36 -0
  72. package/src/presets/chalkboard/boardQueue.ts +620 -0
  73. package/src/presets/chalkboard/chalkboardSession.ts +75 -0
  74. package/src/presets/chalkboard/drawUtils.ts +123 -0
  75. package/src/presets/chalkboard/textUtils.ts +109 -0
  76. package/src/presets/clipRegion/ClipRegionModal.tsx +261 -0
  77. package/src/presets/clipRegion/clipRegionBridge.ts +19 -0
  78. package/src/presets/form/formValidation.ts +104 -0
  79. package/src/presets/form/parseFormFields.ts +171 -0
  80. package/src/presets/host/HostElementPresetHandler.tsx +155 -0
  81. package/src/presets/host/hostPresetBridge.ts +71 -0
  82. package/src/presets/index.ts +63 -0
  83. package/src/presets/liveScreen/CloseLiveScreenHandler.tsx +36 -0
  84. package/src/presets/liveScreen/LiveScreenCaptureHost.tsx +312 -0
  85. package/src/presets/liveScreen/LiveScreenHandler.tsx +25 -0
  86. package/src/presets/liveScreen/LiveScreenPipOverlay.tsx +6 -0
  87. package/src/presets/liveScreen/liveScreenSession.ts +73 -0
  88. package/src/presets/liveVision/CloseLiveVisionHandler.tsx +29 -0
  89. package/src/presets/liveVision/LiveVisionCameraHost.tsx +317 -0
  90. package/src/presets/liveVision/LiveVisionHandler.tsx +26 -0
  91. package/src/presets/liveVision/LiveVisionPipOverlay.tsx +7 -0
  92. package/src/presets/liveVision/liveVisionFrameLoop.ts +38 -0
  93. package/src/presets/liveVision/liveVisionSession.ts +75 -0
  94. package/src/presets/liveVision/liveVisionUpload.ts +62 -0
  95. package/src/presets/navigation/internalRouteRegistry.ts +25 -0
  96. package/src/presets/navigation/navigationBridge.ts +76 -0
  97. package/src/presets/navigation/navigationTypes.ts +12 -0
  98. package/src/presets/parseToolCall.ts +60 -0
  99. package/src/presets/presetClientAdapter.ts +29 -0
  100. package/src/presets/presetCompletion.ts +91 -0
  101. package/src/presets/presetEventHelpers.ts +45 -0
  102. package/src/presets/registry.ts +128 -0
  103. package/src/presets/streaming/mediaFrameUpload.ts +93 -0
  104. package/src/presets/types.ts +74 -0
  105. package/src/presets/upload/pickUploadFile.ts +256 -0
  106. package/src/presets/upload/uploadFormats.ts +163 -0
  107. package/src/presets/upload/uploadUtils.ts +68 -0
  108. package/src/react/PresetRenderer.tsx +144 -0
  109. package/src/react/index.ts +1 -0
  110. package/src/runtime/browserRuntime.ts +54 -0
  111. package/src/runtime/platform.ts +17 -0
  112. package/src/runtime/reactNativeRuntime.ts +68 -0
  113. package/src/runtime/resolveRuntimeConfig.ts +75 -0
  114. package/src/runtime/runtimeBundles.ts +74 -0
  115. package/src/runtime/types.ts +135 -0
  116. package/src/types/react-native-incall-manager.d.ts +17 -0
  117. package/src/types/react-native-webrtc.d.ts +47 -0
  118. package/src/types.ts +133 -0
  119. package/src/ui/VaniraWidget.ts +87 -0
  120. package/src/ui/abstraction/AbstractWidgetProvider.ts +18 -0
  121. package/src/ui/abstraction/interfaces.ts +12 -0
  122. package/src/ui/adapters/VaniraChatAdapter.ts +42 -0
  123. package/src/ui/components/AvatarView.ts +81 -0
  124. package/src/ui/components/ChatWindow.ts +263 -0
  125. package/src/ui/components/FloatingButton.ts +163 -0
  126. package/src/ui/components/FloatingWelcomeChips.ts +137 -0
  127. package/src/ui/components/Panel.ts +120 -0
  128. package/src/ui/components/VoiceOrb.ts +79 -0
  129. package/src/ui/components/VoiceOverlay.ts +497 -0
  130. package/src/ui/components/index.ts +7 -0
  131. package/src/ui/factory/WidgetFactory.ts +16 -0
  132. package/src/ui/icons_data.ts +2 -0
  133. package/src/ui/presets/WidgetPresetRenderer.ts +1802 -0
  134. package/src/ui/presets/types.ts +16 -0
  135. package/src/ui/providers/VaniraInternalProvider.ts +1066 -0
  136. package/src/ui/styles/index.ts +323 -0
  137. package/src/ui/styles/keyframes.ts +76 -0
  138. package/src/ui/styles/theme.ts +57 -0
  139. package/src/ui/styles/widget.css.ts +838 -0
  140. package/src/ui/utils.ts +37 -0
  141. package/src/ui/views/AbstractChatView.ts +93 -0
  142. package/src/ui/views/AbstractVoiceView.ts +57 -0
  143. package/src/ui/views/AvatarOnlyView.ts +78 -0
  144. package/src/ui/views/ChatAvatarView.ts +66 -0
  145. package/src/ui/views/ChatOnlyView.ts +28 -0
  146. package/src/ui/views/ChatVoiceView.ts +15 -0
  147. package/src/ui/views/VoiceOnlyView.ts +25 -0
  148. package/src/ui/views/index.ts +5 -0
@@ -0,0 +1,317 @@
1
+ import React, {useEffect, useRef, useState} from 'react';
2
+ import {Platform, Pressable, StyleSheet, Text, View} from 'react-native';
3
+ import {
4
+ Camera,
5
+ useCameraDevice,
6
+ useCameraPermission,
7
+ type Camera as CameraRef,
8
+ } from 'react-native-vision-camera';
9
+ import type {PresetClient} from '../types';
10
+ import {captureAndSendLiveVisionFrame} from './liveVisionFrameLoop';
11
+ import {
12
+ getLiveVisionSession,
13
+ parseLiveVisionArgs,
14
+ stopLiveVision,
15
+ subscribeLiveVision,
16
+ type LiveVisionSession,
17
+ } from './liveVisionSession';
18
+
19
+ type Props = {
20
+ client: PresetClient | null;
21
+ };
22
+
23
+ function toFileUri(path: string): string {
24
+ if (path.startsWith('file://')) {
25
+ return path;
26
+ }
27
+ return Platform.OS === 'android' ? `file://${path}` : path;
28
+ }
29
+
30
+ async function captureCameraFrame(
31
+ camera: CameraRef,
32
+ ): Promise<string | null> {
33
+ if (Platform.OS === 'android' && typeof camera.takeSnapshot === 'function') {
34
+ const snapshot = await camera.takeSnapshot({quality: 75});
35
+ return snapshot?.path ? toFileUri(snapshot.path) : null;
36
+ }
37
+
38
+ const photo = await camera.takePhoto({
39
+ flash: 'off',
40
+ enableShutterSound: false,
41
+ });
42
+ return photo?.path ? toFileUri(photo.path) : null;
43
+ }
44
+
45
+ /**
46
+ * Live vision camera host — web parity via a dedicated camera pipeline
47
+ * (hidden video + canvas on web; Vision Camera takePhoto/snapshot on RN).
48
+ *
49
+ * Does NOT use RTCView + ViewShot (black frames on GL surfaces).
50
+ */
51
+ export function LiveVisionCameraHost({client}: Props) {
52
+ const [session, setSession] = useState<LiveVisionSession | null>(
53
+ getLiveVisionSession(),
54
+ );
55
+ const [cameraReady, setCameraReady] = useState(false);
56
+ const cameraRef = useRef<CameraRef>(null);
57
+ const frameSeqRef = useRef(0);
58
+ const captureTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
59
+ const autoStopTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
60
+ const runningSessionIdRef = useRef<string | null>(null);
61
+ const uploadingRef = useRef(false);
62
+ const startedSessionRef = useRef<string | null>(null);
63
+
64
+ const {hasPermission, requestPermission} = useCameraPermission();
65
+
66
+ useEffect(
67
+ () =>
68
+ subscribeLiveVision(() => {
69
+ setSession(getLiveVisionSession());
70
+ }),
71
+ [],
72
+ );
73
+
74
+ useEffect(() => {
75
+ if (!client) {
76
+ stopLiveVision(null);
77
+ }
78
+ }, [client]);
79
+
80
+ useEffect(() => {
81
+ if (!session) {
82
+ startedSessionRef.current = null;
83
+ runningSessionIdRef.current = null;
84
+ frameSeqRef.current = 0;
85
+ setCameraReady(false);
86
+ return;
87
+ }
88
+
89
+ if (!hasPermission) {
90
+ requestPermission().then(granted => {
91
+ if (!granted) {
92
+ console.warn('[LiveVision] Camera permission denied');
93
+ stopLiveVision(session.client);
94
+ }
95
+ });
96
+ }
97
+ }, [session, hasPermission, requestPermission]);
98
+
99
+ const parsed = session ? parseLiveVisionArgs(session.args) : null;
100
+ const device = useCameraDevice(
101
+ parsed?.facingMode === 'user' ? 'front' : 'back',
102
+ );
103
+
104
+ useEffect(() => {
105
+ if (!session || !client || !device || !hasPermission || !cameraReady) {
106
+ return;
107
+ }
108
+
109
+ const activeSessionId = session.sessionId;
110
+ if (startedSessionRef.current === activeSessionId) {
111
+ return;
112
+ }
113
+
114
+ const {
115
+ scope,
116
+ targetFps,
117
+ reason,
118
+ durationSec,
119
+ showPreview,
120
+ } = parseLiveVisionArgs(session.args);
121
+ const activeClient = session.client;
122
+
123
+ startedSessionRef.current = activeSessionId;
124
+ runningSessionIdRef.current = activeSessionId;
125
+ frameSeqRef.current = 0;
126
+
127
+ activeClient.sendEvent?.('client_live_camera_started', {
128
+ data: {
129
+ session_id: activeSessionId,
130
+ scope,
131
+ ...(scope === 'timed' && durationSec > 0
132
+ ? {duration_sec: durationSec}
133
+ : {}),
134
+ reason,
135
+ target_fps: targetFps,
136
+ },
137
+ });
138
+ console.log(
139
+ `[LiveVision] Session started: ${activeSessionId} (${scope}, ${targetFps} fps, preview=${showPreview})`,
140
+ );
141
+
142
+ const intervalMs = Math.max(250, Math.round(1000 / targetFps));
143
+ const warmup = setTimeout(() => {
144
+ captureTimerRef.current = setInterval(async () => {
145
+ const camera = cameraRef.current;
146
+ if (
147
+ uploadingRef.current ||
148
+ !camera ||
149
+ runningSessionIdRef.current !== activeSessionId
150
+ ) {
151
+ return;
152
+ }
153
+
154
+ uploadingRef.current = true;
155
+ try {
156
+ const frameUri = await captureCameraFrame(camera);
157
+ if (!frameUri || runningSessionIdRef.current !== activeSessionId) {
158
+ return;
159
+ }
160
+
161
+ frameSeqRef.current = await captureAndSendLiveVisionFrame(
162
+ activeClient,
163
+ activeSessionId,
164
+ frameUri,
165
+ reason,
166
+ frameSeqRef.current,
167
+ () => getLiveVisionSession()?.sessionId === activeSessionId,
168
+ );
169
+ } catch (err) {
170
+ console.warn('[LiveVision] Frame capture failed:', err);
171
+ } finally {
172
+ uploadingRef.current = false;
173
+ }
174
+ }, intervalMs);
175
+ }, 800);
176
+
177
+ if (scope === 'timed' && durationSec > 0) {
178
+ autoStopTimerRef.current = setTimeout(() => {
179
+ console.log(`[LiveVision] Timed session expired (${durationSec}s)`);
180
+ stopLiveVision(activeClient);
181
+ }, durationSec * 1000);
182
+ }
183
+
184
+ return () => {
185
+ clearTimeout(warmup);
186
+ if (captureTimerRef.current) {
187
+ clearInterval(captureTimerRef.current);
188
+ captureTimerRef.current = null;
189
+ }
190
+ if (autoStopTimerRef.current) {
191
+ clearTimeout(autoStopTimerRef.current);
192
+ autoStopTimerRef.current = null;
193
+ }
194
+ if (runningSessionIdRef.current === activeSessionId) {
195
+ runningSessionIdRef.current = null;
196
+ }
197
+ if (startedSessionRef.current === activeSessionId) {
198
+ startedSessionRef.current = null;
199
+ }
200
+ };
201
+ }, [session, client, device, hasPermission, cameraReady]);
202
+
203
+ if (!session || !device || !hasPermission) {
204
+ return null;
205
+ }
206
+
207
+ const showPreview = parsed?.showPreview ?? true;
208
+ const mirror = parsed?.facingMode === 'user';
209
+
210
+ if (!showPreview) {
211
+ return (
212
+ <View style={styles.hiddenHost}>
213
+ <Camera
214
+ ref={cameraRef}
215
+ style={styles.offscreenCamera}
216
+ device={device}
217
+ isActive={true}
218
+ photo={true}
219
+ video={Platform.OS === 'android'}
220
+ isMirrored={mirror}
221
+ onInitialized={() => setCameraReady(true)}
222
+ />
223
+ </View>
224
+ );
225
+ }
226
+
227
+ return (
228
+ <View style={styles.pipHost} pointerEvents="box-none">
229
+ <View style={styles.pipCard}>
230
+ <Camera
231
+ ref={cameraRef}
232
+ style={styles.pipCamera}
233
+ device={device}
234
+ isActive={true}
235
+ photo={true}
236
+ video={Platform.OS === 'android'}
237
+ isMirrored={mirror}
238
+ onInitialized={() => setCameraReady(true)}
239
+ />
240
+ <View style={styles.pipBadge}>
241
+ <Text style={styles.pipBadgeText}>LIVE</Text>
242
+ </View>
243
+ <Pressable
244
+ style={styles.pipStop}
245
+ onPress={() => stopLiveVision(client)}
246
+ hitSlop={8}>
247
+ <Text style={styles.pipStopText}>Stop</Text>
248
+ </Pressable>
249
+ </View>
250
+ </View>
251
+ );
252
+ }
253
+
254
+ const styles = StyleSheet.create({
255
+ hiddenHost: {
256
+ position: 'absolute',
257
+ width: 640,
258
+ height: 480,
259
+ left: -2000,
260
+ top: 0,
261
+ opacity: 0.01,
262
+ overflow: 'hidden',
263
+ },
264
+ offscreenCamera: {
265
+ width: 640,
266
+ height: 480,
267
+ },
268
+ pipHost: {
269
+ ...StyleSheet.absoluteFillObject,
270
+ zIndex: 1000001,
271
+ elevation: 1000001,
272
+ justifyContent: 'flex-end',
273
+ alignItems: 'flex-end',
274
+ padding: 20,
275
+ },
276
+ pipCard: {
277
+ width: 180,
278
+ height: 135,
279
+ borderRadius: 16,
280
+ overflow: 'hidden',
281
+ borderWidth: 2,
282
+ borderColor: '#f87171',
283
+ backgroundColor: '#000',
284
+ },
285
+ pipCamera: {
286
+ flex: 1,
287
+ },
288
+ pipBadge: {
289
+ position: 'absolute',
290
+ top: 8,
291
+ left: 8,
292
+ backgroundColor: '#dc2626',
293
+ borderRadius: 6,
294
+ paddingHorizontal: 6,
295
+ paddingVertical: 2,
296
+ },
297
+ pipBadgeText: {
298
+ color: '#fff',
299
+ fontSize: 10,
300
+ fontWeight: '800',
301
+ letterSpacing: 0.8,
302
+ },
303
+ pipStop: {
304
+ position: 'absolute',
305
+ bottom: 8,
306
+ right: 8,
307
+ backgroundColor: 'rgba(15, 23, 42, 0.85)',
308
+ borderRadius: 8,
309
+ paddingHorizontal: 10,
310
+ paddingVertical: 4,
311
+ },
312
+ pipStopText: {
313
+ color: '#f8fafc',
314
+ fontSize: 11,
315
+ fontWeight: '700',
316
+ },
317
+ });
@@ -0,0 +1,26 @@
1
+ import {useEffect} from 'react';
2
+ import type {PresetClient, PresetContext} from '../types';
3
+ import {beginLiveVision} from './liveVisionSession';
4
+
5
+ type Props = {
6
+ client: PresetClient | null;
7
+ ctx: PresetContext;
8
+ /** Must NOT stop the live-vision session — only clears active tool_call UI. */
9
+ onReleaseToolCall?: () => void;
10
+ };
11
+
12
+ /** Headless open handler — camera runs in LiveVisionPipOverlay until close/disconnect. */
13
+ export function LiveVisionHandler({client, ctx, onReleaseToolCall}: Props) {
14
+ useEffect(() => {
15
+ if (!client) {
16
+ return;
17
+ }
18
+
19
+ const merged = {...ctx.args, ...ctx.clientFields};
20
+ beginLiveVision(client, merged);
21
+ onReleaseToolCall?.();
22
+ // eslint-disable-next-line react-hooks/exhaustive-deps
23
+ }, [client, ctx.toolCallId]);
24
+
25
+ return null;
26
+ }
@@ -0,0 +1,7 @@
1
+ import type {PresetClient} from '../types';
2
+ import {LiveVisionCameraHost} from './LiveVisionCameraHost';
3
+
4
+ /** Root overlay for active live-vision sessions (PiP or offscreen capture). */
5
+ export function LiveVisionPipOverlay({client}: {client: PresetClient | null}) {
6
+ return <LiveVisionCameraHost client={client} />;
7
+ }
@@ -0,0 +1,38 @@
1
+ import type {PresetClient} from '../types';
2
+ import {
3
+ sendLiveVisionFrameEvent,
4
+ uploadLiveVisionFrame,
5
+ } from './liveVisionUpload';
6
+
7
+ /**
8
+ * Mirrors web WidgetPresetRenderer.captureAndSendFrame:
9
+ * raw /media/upload then client_media_frame only (no client_media_update).
10
+ */
11
+ export async function captureAndSendLiveVisionFrame(
12
+ client: PresetClient,
13
+ sessionId: string,
14
+ frameUri: string,
15
+ reason: string,
16
+ frameSeq: number,
17
+ isSessionActive: () => boolean,
18
+ ): Promise<number> {
19
+ if (!isSessionActive()) {
20
+ return frameSeq;
21
+ }
22
+
23
+ const {media_id, url} = await uploadLiveVisionFrame(
24
+ client,
25
+ sessionId,
26
+ frameUri,
27
+ reason,
28
+ );
29
+
30
+ if (!isSessionActive()) {
31
+ return frameSeq;
32
+ }
33
+
34
+ const nextSeq = frameSeq + 1;
35
+ sendLiveVisionFrameEvent(client, sessionId, nextSeq, media_id, url, reason);
36
+ console.log(`[LiveVision] Frame #${nextSeq} sent (media_id: ${media_id})`);
37
+ return nextSeq;
38
+ }
@@ -0,0 +1,75 @@
1
+ import type {PresetClient} from '../types';
2
+
3
+ export type LiveVisionSession = {
4
+ sessionId: string;
5
+ client: PresetClient;
6
+ args: Record<string, unknown>;
7
+ };
8
+
9
+ let activeSession: LiveVisionSession | null = null;
10
+ const listeners = new Set<() => void>();
11
+
12
+ function notify(): void {
13
+ listeners.forEach(listener => listener());
14
+ }
15
+
16
+ export function subscribeLiveVision(listener: () => void): () => void {
17
+ listeners.add(listener);
18
+ return () => listeners.delete(listener);
19
+ }
20
+
21
+ export function getLiveVisionSession(): LiveVisionSession | null {
22
+ return activeSession;
23
+ }
24
+
25
+ export function beginLiveVision(
26
+ client: PresetClient,
27
+ args: Record<string, unknown>,
28
+ ): string {
29
+ stopLiveVision(client);
30
+
31
+ const sessionId = `lv_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
32
+ activeSession = {sessionId, client, args};
33
+ notify();
34
+ return sessionId;
35
+ }
36
+
37
+ export function stopLiveVision(client: PresetClient | null): void {
38
+ if (!activeSession) {
39
+ return;
40
+ }
41
+
42
+ const sessionId = activeSession.sessionId;
43
+ const notifyClient = client ?? activeSession.client;
44
+ activeSession = null;
45
+ notify();
46
+
47
+ if (notifyClient?.sendEvent) {
48
+ notifyClient.sendEvent('client_live_camera_stopped', {
49
+ data: {session_id: sessionId},
50
+ });
51
+ }
52
+ console.log(`[LiveVision] Session stopped: ${sessionId}`);
53
+ }
54
+
55
+ export function parseLiveVisionArgs(args: Record<string, unknown>) {
56
+ const scope = String(args.scope ?? 'until_call_end');
57
+ const maxFps = Number(args.max_fps ?? 3);
58
+ const targetFps = Math.min(Number(args.target_fps ?? 1), maxFps);
59
+ const maxWidth = Number(args.max_width ?? 640);
60
+ const reason = String(args.reason ?? 'camera_capture');
61
+ const durationSec = Number(args.duration_sec ?? 0);
62
+ const facingMode = String(args.facing_mode ?? 'environment');
63
+ const showPreview =
64
+ args.show_preview !== false && args.show_preview !== 'false';
65
+
66
+ return {
67
+ scope,
68
+ targetFps,
69
+ maxWidth,
70
+ reason,
71
+ durationSec,
72
+ facingMode,
73
+ showPreview,
74
+ };
75
+ }
@@ -0,0 +1,62 @@
1
+ import type {PresetClient} from '../types';
2
+
3
+ export function resolveUploadBase(serverUrl: string): string {
4
+ if (!serverUrl) {
5
+ return '';
6
+ }
7
+ try {
8
+ return new URL(serverUrl).origin;
9
+ } catch {
10
+ return serverUrl.replace(/\/webrtc.*$/, '').replace(/\/$/, '');
11
+ }
12
+ }
13
+
14
+ export async function uploadLiveVisionFrame(
15
+ client: PresetClient,
16
+ sessionId: string,
17
+ frameUri: string,
18
+ reason: string,
19
+ ): Promise<{media_id: string; url: string}> {
20
+ const uploadBase = resolveUploadBase(client.serverUrl ?? '');
21
+ const callId = client.callId ?? '';
22
+ const uploadUrl = `${uploadBase}/media/upload`;
23
+
24
+ const form = new FormData();
25
+ form.append('file', {
26
+ uri: frameUri,
27
+ type: 'image/jpeg',
28
+ name: `frame_${sessionId}.jpg`,
29
+ } as unknown as Blob);
30
+ form.append('call_id', callId);
31
+ form.append('reason', reason);
32
+
33
+ const res = await fetch(uploadUrl, {method: 'POST', body: form});
34
+ if (!res.ok) {
35
+ throw new Error(`Upload HTTP ${res.status}`);
36
+ }
37
+ const data = (await res.json()) as {media_id?: string; url?: string};
38
+ if (!data.media_id || !data.url) {
39
+ throw new Error('Upload response missing media_id/url');
40
+ }
41
+ return {media_id: data.media_id, url: data.url};
42
+ }
43
+
44
+ export function sendLiveVisionFrameEvent(
45
+ client: PresetClient,
46
+ sessionId: string,
47
+ frameSeq: number,
48
+ media_id: string,
49
+ media_url: string,
50
+ reason: string,
51
+ ): void {
52
+ client.sendEvent?.('client_media_frame', {
53
+ data: {
54
+ session_id: sessionId,
55
+ frame_seq: frameSeq,
56
+ media_id,
57
+ media_url,
58
+ content_type: 'image/jpeg',
59
+ reason,
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,25 @@
1
+ import type {InternalRouteHandler} from './navigationTypes';
2
+
3
+ let internalRouteHandler: InternalRouteHandler | null = null;
4
+
5
+ /**
6
+ * Register SPA / in-app router handler — mirrors web `vanira:navigate` listener
7
+ * that sets `detail.handled = true`.
8
+ *
9
+ * Kept free of react-native imports so consumer apps can import this symbol
10
+ * without pulling RN types through the SDK package graph.
11
+ */
12
+ export function registerInternalRouteHandler(
13
+ handler: InternalRouteHandler | null,
14
+ ): () => void {
15
+ internalRouteHandler = handler;
16
+ return () => {
17
+ if (internalRouteHandler === handler) {
18
+ internalRouteHandler = null;
19
+ }
20
+ };
21
+ }
22
+
23
+ export function getInternalRouteHandler(): InternalRouteHandler | null {
24
+ return internalRouteHandler;
25
+ }
@@ -0,0 +1,76 @@
1
+ import {Linking} from 'react-native';
2
+ import {getInternalRouteHandler} from './internalRouteRegistry';
3
+ import type {NavigationEvent} from './navigationTypes';
4
+
5
+ export type {NavigationEvent, InternalRouteHandler} from './navigationTypes';
6
+ export {registerInternalRouteHandler} from './internalRouteRegistry';
7
+
8
+ let lastNavigation: NavigationEvent | null = null;
9
+ const listeners = new Set<(event: NavigationEvent) => void>();
10
+
11
+ function notify(event: NavigationEvent): void {
12
+ lastNavigation = event;
13
+ listeners.forEach(listener => listener(event));
14
+ }
15
+
16
+ export function subscribeNavigation(
17
+ listener: (event: NavigationEvent) => void,
18
+ ): () => void {
19
+ listeners.add(listener);
20
+ if (lastNavigation) {
21
+ listener(lastNavigation);
22
+ }
23
+ return () => listeners.delete(listener);
24
+ }
25
+
26
+ export function getLastNavigation(): NavigationEvent | null {
27
+ return lastNavigation;
28
+ }
29
+
30
+ function isExternalUrl(url: string): boolean {
31
+ return /^https?:\/\//i.test(url);
32
+ }
33
+
34
+ /**
35
+ * RN equivalent of web WidgetPresetRenderer.renderNavigate:
36
+ * - external → Linking.openURL
37
+ * - internal → internalRouteHandler must return true or navigation fails
38
+ */
39
+ export async function navigateToTarget(
40
+ targetUrl: string,
41
+ ): Promise<NavigationEvent> {
42
+ const url = String(targetUrl || '/').trim() || '/';
43
+ const external = isExternalUrl(url);
44
+
45
+ if (external) {
46
+ const canOpen = await Linking.canOpenURL(url);
47
+ if (canOpen) {
48
+ await Linking.openURL(url);
49
+ }
50
+ const event: NavigationEvent = {
51
+ url,
52
+ external: true,
53
+ opened: canOpen,
54
+ handled: canOpen,
55
+ at: Date.now(),
56
+ };
57
+ notify(event);
58
+ return event;
59
+ }
60
+
61
+ const internalRouteHandler = getInternalRouteHandler();
62
+ let handled = false;
63
+ if (internalRouteHandler) {
64
+ handled = Boolean(await Promise.resolve(internalRouteHandler(url)));
65
+ }
66
+
67
+ const event: NavigationEvent = {
68
+ url,
69
+ external: false,
70
+ opened: handled,
71
+ handled,
72
+ at: Date.now(),
73
+ };
74
+ notify(event);
75
+ return event;
76
+ }
@@ -0,0 +1,12 @@
1
+ export type NavigationEvent = {
2
+ url: string;
3
+ external: boolean;
4
+ opened: boolean;
5
+ /** Internal routes require an app handler (web: vanira:navigate detail.handled). */
6
+ handled: boolean;
7
+ at: number;
8
+ };
9
+
10
+ export type InternalRouteHandler = (
11
+ path: string,
12
+ ) => boolean | Promise<boolean>;
@@ -0,0 +1,60 @@
1
+ import type {ParsedToolCall, PresetId} from './types';
2
+ import {TOOL_NAME_TO_PRESET_ID, isVaniraPresetId} from './registry';
3
+
4
+ /** Same normalization as PresetRenderer.tsx + WidgetPresetRenderer.handle(). */
5
+ export function parseToolCall(toolCall: unknown): ParsedToolCall | null {
6
+ if (!toolCall || typeof toolCall !== 'object') {
7
+ return null;
8
+ }
9
+ let data =
10
+ (toolCall as {data?: Record<string, unknown>}).data ??
11
+ (toolCall as Record<string, unknown>);
12
+
13
+ // Unwrap nested tool_call envelopes from some server payloads.
14
+ const nested = data.tool_call;
15
+ if (nested && typeof nested === 'object') {
16
+ data = nested as Record<string, unknown>;
17
+ }
18
+
19
+ let args: Record<string, unknown> = {};
20
+ const rawArgs = data.arguments ?? data.args;
21
+ if (typeof rawArgs === 'string') {
22
+ try {
23
+ args = JSON.parse(rawArgs) as Record<string, unknown>;
24
+ } catch {
25
+ args = {};
26
+ }
27
+ } else if (rawArgs && typeof rawArgs === 'object') {
28
+ args = rawArgs as Record<string, unknown>;
29
+ }
30
+
31
+ const clientFields = (data.client_fields ??
32
+ data.clientFields ??
33
+ {}) as Record<string, unknown>;
34
+ const fn = data.function;
35
+ const functionName =
36
+ fn && typeof fn === 'object'
37
+ ? String((fn as Record<string, unknown>).name ?? '')
38
+ : '';
39
+ const name = String(
40
+ data.name ?? data.tool_name ?? data.ref_code ?? functionName ?? '',
41
+ );
42
+ const presetIdRaw =
43
+ clientFields.preset_id ?? args.preset_id ?? TOOL_NAME_TO_PRESET_ID[name];
44
+ const presetId =
45
+ typeof presetIdRaw === 'string' && isVaniraPresetId(presetIdRaw)
46
+ ? (presetIdRaw as PresetId)
47
+ : undefined;
48
+ const toolCallId = String(
49
+ data.tool_call_id ?? data.call_id ?? '',
50
+ );
51
+
52
+ return {
53
+ raw: toolCall,
54
+ name,
55
+ toolCallId,
56
+ arguments: args,
57
+ clientFields,
58
+ presetId,
59
+ };
60
+ }