@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,350 @@
1
+ import React, {useEffect, useMemo, useRef} from 'react';
2
+ import {BookingCalendarModal} from './BookingCalendarModal';
3
+ import {CameraModal} from './CameraModal';
4
+ import {ClipRegionModal} from './clipRegion/ClipRegionModal';
5
+ import {DynamicFormModal} from './DynamicFormModal';
6
+ import {HostElementPresetHandler} from './host/HostElementPresetHandler';
7
+ import {CloseLiveScreenHandler} from './liveScreen/CloseLiveScreenHandler';
8
+ import {LiveScreenHandler} from './liveScreen/LiveScreenHandler';
9
+ import {CloseLiveVisionHandler} from './liveVision/CloseLiveVisionHandler';
10
+ import {LiveVisionHandler} from './liveVision/LiveVisionHandler';
11
+ import {NavigateHandler} from './NavigateHandler';
12
+ import {parseToolCall} from './parseToolCall';
13
+ import {cancelPreset, completePreset} from './presetCompletion';
14
+ import {
15
+ isBoardQueuedPreset,
16
+ isFireAndForgetPreset,
17
+ isNonInteractivePreset,
18
+ VaniraPresetId,
19
+ } from './registry';
20
+ import {UploadModal} from './UploadModal';
21
+ import type {NativePresetRendererProps, PresetContext, PresetId} from './types';
22
+ import {PRESET_RENDERER_KNOWN_PRESETS as KNOWN} from './types';
23
+ import {
24
+ enqueueBoardCommand,
25
+ getBoardTerminalResult,
26
+ resetBoardSession,
27
+ } from './chalkboard/boardQueue';
28
+
29
+ const HOST_ELEMENT_PRESETS = new Set<string>([
30
+ VaniraPresetId.HighlightElement,
31
+ VaniraPresetId.ClickElement,
32
+ VaniraPresetId.SelectOption,
33
+ VaniraPresetId.SetDate,
34
+ ]);
35
+
36
+ const MODAL_PRESETS = new Set<string>([
37
+ VaniraPresetId.Form,
38
+ VaniraPresetId.Calendar,
39
+ VaniraPresetId.Camera,
40
+ VaniraPresetId.Upload,
41
+ VaniraPresetId.ClipRegion,
42
+ ]);
43
+
44
+ /**
45
+ * RN equivalent of PresetRenderer.tsx + WidgetPresetRenderer.handle() routing.
46
+ */
47
+ export function NativePresetRenderer({
48
+ client,
49
+ toolCall,
50
+ onDismiss,
51
+ onReleaseToolCall,
52
+ }: NativePresetRendererProps) {
53
+ const popupShownRef = useRef<string | null>(null);
54
+ const settledBoardResultsRef = useRef<Set<string>>(new Set());
55
+ const lastHandledBoardToolCallIdRef = useRef<string | null>(null);
56
+ const parsed = useMemo(() => parseToolCall(toolCall), [toolCall]);
57
+
58
+ useEffect(() => {
59
+ if (!toolCall) {
60
+ popupShownRef.current = null;
61
+ settledBoardResultsRef.current.clear();
62
+ lastHandledBoardToolCallIdRef.current = null;
63
+ resetBoardSession();
64
+ }
65
+ }, [toolCall]);
66
+
67
+ useEffect(() => {
68
+ if (!parsed?.presetId || !client) {
69
+ return;
70
+ }
71
+
72
+ const presetId = parsed.presetId;
73
+ const isInteractivePopup =
74
+ (KNOWN as readonly string[]).includes(presetId) &&
75
+ MODAL_PRESETS.has(presetId);
76
+
77
+ if (!isInteractivePopup || popupShownRef.current === parsed.toolCallId) {
78
+ return;
79
+ }
80
+
81
+ popupShownRef.current = parsed.toolCallId;
82
+ try {
83
+ client.sendToolResult(parsed.toolCallId, {
84
+ status: 'popup_shown',
85
+ message: `The UI preset ${presetId} is now visible. I am waiting for the user to complete it. STAY SILENT.`,
86
+ });
87
+ client.sendContextUpdate?.({
88
+ ui_state: 'waiting_for_preset_data',
89
+ tool_status: 'active_waiting',
90
+ visible_preset: presetId,
91
+ });
92
+ console.log('[NativePresetRenderer] popup_shown ack sent for', presetId);
93
+ } catch (err) {
94
+ console.warn('[NativePresetRenderer] popup_shown ack failed:', err);
95
+ }
96
+ }, [parsed, client]);
97
+
98
+ useEffect(() => {
99
+ if (!parsed?.presetId || !client || !isBoardQueuedPreset(parsed.presetId)) {
100
+ return;
101
+ }
102
+
103
+ const toolCallId = parsed.toolCallId;
104
+ if (toolCallId && lastHandledBoardToolCallIdRef.current === toolCallId) {
105
+ return;
106
+ }
107
+ if (toolCallId) {
108
+ lastHandledBoardToolCallIdRef.current = toolCallId;
109
+ }
110
+
111
+ const presetId = parsed.presetId;
112
+ const mergedClientFields = {...parsed.arguments, ...parsed.clientFields};
113
+ const ctx: PresetContext = {
114
+ toolCallId: parsed.toolCallId,
115
+ presetId,
116
+ clientFields: mergedClientFields,
117
+ args: parsed.arguments,
118
+ toolCall: parsed.raw,
119
+ };
120
+
121
+ console.log('[NativePresetRenderer] enqueue board preset:', presetId, {
122
+ toolCallId: parsed.toolCallId,
123
+ });
124
+
125
+ enqueueBoardCommand(
126
+ {
127
+ ctx,
128
+ presetId,
129
+ onComplete: result => {
130
+ if (!client || !toolCallId) {
131
+ return;
132
+ }
133
+ if (result?.status === 'cancelled') {
134
+ return;
135
+ }
136
+ if (settledBoardResultsRef.current.has(toolCallId)) {
137
+ return;
138
+ }
139
+ settledBoardResultsRef.current.add(toolCallId);
140
+ client.sendToolResult(toolCallId, result);
141
+ },
142
+ onCancel: (reason, meta) => {
143
+ if (!client || !toolCallId) {
144
+ return;
145
+ }
146
+ const terminalStatus = meta?.status ?? 'cancelled';
147
+ if (settledBoardResultsRef.current.has(toolCallId)) {
148
+ return;
149
+ }
150
+ settledBoardResultsRef.current.add(toolCallId);
151
+ client.sendToolResult(
152
+ toolCallId,
153
+ getBoardTerminalResult(terminalStatus, reason, presetId),
154
+ );
155
+ },
156
+ },
157
+ client,
158
+ );
159
+ }, [parsed, client]);
160
+
161
+ useEffect(() => {
162
+ if (!parsed?.presetId || isBoardQueuedPreset(parsed.presetId)) {
163
+ return;
164
+ }
165
+ resetBoardSession();
166
+ }, [parsed?.toolCallId, parsed?.presetId]);
167
+
168
+ if (!parsed?.presetId) {
169
+ if (toolCall) {
170
+ console.warn(
171
+ '[NativePresetRenderer] toolCall present but no presetId — UI skipped',
172
+ toolCall,
173
+ );
174
+ }
175
+ return null;
176
+ }
177
+
178
+ const presetId = parsed.presetId as PresetId;
179
+
180
+ if (isBoardQueuedPreset(presetId)) {
181
+ return null;
182
+ }
183
+
184
+ const mergedClientFields = {...parsed.arguments, ...parsed.clientFields};
185
+ const ctx: PresetContext = {
186
+ toolCallId: parsed.toolCallId,
187
+ presetId,
188
+ clientFields: mergedClientFields,
189
+ args: parsed.arguments,
190
+ toolCall: parsed.raw,
191
+ };
192
+
193
+ console.log('[NativePresetRenderer] routing preset:', presetId, {
194
+ toolCallId: parsed.toolCallId,
195
+ name: parsed.name,
196
+ });
197
+
198
+ const handleCancel = (reason?: string) => {
199
+ cancelPreset(client, presetId, parsed.toolCallId, reason);
200
+ onDismiss?.();
201
+ };
202
+
203
+ const handleComplete = (payload: Record<string, unknown>) => {
204
+ completePreset(
205
+ client,
206
+ presetId,
207
+ parsed.toolCallId,
208
+ mergedClientFields,
209
+ payload,
210
+ parsed.name,
211
+ );
212
+ if (!isFireAndForgetPreset(presetId)) {
213
+ onDismiss?.();
214
+ }
215
+ };
216
+
217
+ if (HOST_ELEMENT_PRESETS.has(presetId)) {
218
+ return (
219
+ <HostElementPresetHandler
220
+ ctx={ctx}
221
+ onComplete={handleComplete}
222
+ onCancel={handleCancel}
223
+ onDismiss={() => onDismiss?.()}
224
+ />
225
+ );
226
+ }
227
+
228
+ if (presetId === VaniraPresetId.Navigate) {
229
+ return (
230
+ <NavigateHandler
231
+ ctx={ctx}
232
+ onComplete={handleComplete}
233
+ onCancel={handleCancel}
234
+ onDismiss={() => onDismiss?.()}
235
+ />
236
+ );
237
+ }
238
+
239
+ if (presetId === VaniraPresetId.LiveVision) {
240
+ return (
241
+ <LiveVisionHandler
242
+ client={client}
243
+ ctx={ctx}
244
+ onReleaseToolCall={() => onReleaseToolCall?.()}
245
+ />
246
+ );
247
+ }
248
+
249
+ if (presetId === VaniraPresetId.CloseLiveCamera) {
250
+ return (
251
+ <CloseLiveVisionHandler
252
+ client={client}
253
+ ctx={ctx}
254
+ onComplete={handleComplete}
255
+ onDismiss={() => onDismiss?.()}
256
+ />
257
+ );
258
+ }
259
+
260
+ if (presetId === VaniraPresetId.LiveScreen) {
261
+ return (
262
+ <LiveScreenHandler
263
+ client={client}
264
+ ctx={ctx}
265
+ onReleaseToolCall={() => onReleaseToolCall?.()}
266
+ />
267
+ );
268
+ }
269
+
270
+ if (presetId === VaniraPresetId.CloseLiveScreen) {
271
+ return (
272
+ <CloseLiveScreenHandler
273
+ client={client}
274
+ ctx={ctx}
275
+ onComplete={handleComplete}
276
+ onDismiss={() => onDismiss?.()}
277
+ />
278
+ );
279
+ }
280
+
281
+ if (isNonInteractivePreset(presetId) && !MODAL_PRESETS.has(presetId)) {
282
+ return null;
283
+ }
284
+
285
+ let modal: React.ReactNode = null;
286
+ switch (presetId) {
287
+ case VaniraPresetId.Calendar:
288
+ modal = (
289
+ <BookingCalendarModal
290
+ visible
291
+ ctx={ctx}
292
+ onClose={() => handleCancel('User closed the preset')}
293
+ onCancel={handleCancel}
294
+ onComplete={handleComplete}
295
+ />
296
+ );
297
+ break;
298
+ case VaniraPresetId.Form:
299
+ modal = (
300
+ <DynamicFormModal
301
+ visible
302
+ ctx={ctx}
303
+ onClose={() => handleCancel('User closed')}
304
+ onCancel={handleCancel}
305
+ onComplete={handleComplete}
306
+ />
307
+ );
308
+ break;
309
+ case VaniraPresetId.Camera:
310
+ modal = (
311
+ <CameraModal
312
+ visible
313
+ ctx={ctx}
314
+ client={client}
315
+ onClose={() => handleCancel('User closed')}
316
+ onCancel={handleCancel}
317
+ onComplete={handleComplete}
318
+ />
319
+ );
320
+ break;
321
+ case VaniraPresetId.Upload:
322
+ modal = (
323
+ <UploadModal
324
+ visible
325
+ ctx={ctx}
326
+ client={client}
327
+ onClose={() => handleCancel('User closed')}
328
+ onCancel={handleCancel}
329
+ onComplete={handleComplete}
330
+ />
331
+ );
332
+ break;
333
+ case VaniraPresetId.ClipRegion:
334
+ modal = (
335
+ <ClipRegionModal
336
+ visible
337
+ ctx={ctx}
338
+ client={client}
339
+ onClose={() => onDismiss?.()}
340
+ onCancel={handleCancel}
341
+ onComplete={handleComplete}
342
+ />
343
+ );
344
+ break;
345
+ default:
346
+ return null;
347
+ }
348
+
349
+ return modal;
350
+ }
@@ -0,0 +1,75 @@
1
+ import React, {useEffect, useRef} from 'react';
2
+ import {navigateToTarget} from './navigation/navigationBridge';
3
+ import type {PresetContext} from './types';
4
+
5
+ type Props = {
6
+ ctx: PresetContext;
7
+ onComplete: (payload: Record<string, unknown>) => void;
8
+ onCancel: (reason?: string) => void;
9
+ onDismiss: () => void;
10
+ };
11
+
12
+ function resolveTargetUrl(ctx: PresetContext): string {
13
+ const merged = {...ctx.args, ...ctx.clientFields};
14
+ return String(
15
+ merged.target_url ?? ctx.clientFields.target_url ?? ctx.args.target_url ?? '/',
16
+ );
17
+ }
18
+
19
+ /**
20
+ * vanira_navigate — web parity:
21
+ * external URLs open in browser; internal routes need registerInternalRouteHandler().
22
+ */
23
+ export function NavigateHandler({ctx, onComplete, onCancel, onDismiss}: Props) {
24
+ const ranRef = useRef(false);
25
+
26
+ useEffect(() => {
27
+ if (ranRef.current) {
28
+ return;
29
+ }
30
+ ranRef.current = true;
31
+
32
+ const targetUrl = resolveTargetUrl(ctx);
33
+
34
+ (async () => {
35
+ try {
36
+ const event = await navigateToTarget(targetUrl);
37
+
38
+ if (event.external) {
39
+ if (!event.opened) {
40
+ onCancel(`Could not open URL: ${targetUrl}`);
41
+ return;
42
+ }
43
+ onComplete({
44
+ navigated_to: targetUrl,
45
+ status: 'success',
46
+ navigation_type: 'external',
47
+ });
48
+ return;
49
+ }
50
+
51
+ if (!event.handled) {
52
+ console.warn(
53
+ '[NavigateHandler] No internal route handler — register registerInternalRouteHandler() in your app',
54
+ );
55
+ onCancel('No internal navigation handler registered');
56
+ return;
57
+ }
58
+
59
+ onComplete({
60
+ navigated_to: targetUrl,
61
+ path: targetUrl,
62
+ status: 'success',
63
+ navigation_type: 'internal',
64
+ });
65
+ } catch (err: unknown) {
66
+ const message = err instanceof Error ? err.message : 'Navigation failed';
67
+ onCancel(message);
68
+ } finally {
69
+ onDismiss();
70
+ }
71
+ })();
72
+ }, [ctx, onCancel, onComplete, onDismiss]);
73
+
74
+ return null;
75
+ }
@@ -0,0 +1,155 @@
1
+ import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
2
+ import {Modal, StyleSheet, View} from 'react-native';
3
+ import type {VaniraAI} from '../core/VaniraAI';
4
+ import {registerClipRegionCaptureTarget} from './clipRegion/clipRegionBridge';
5
+ import {extractPresetId} from './presetEventHelpers';
6
+ import {ChalkboardOverlay} from './chalkboard/ChalkboardOverlay';
7
+ import {
8
+ abortBoardTools,
9
+ dismissChalkboardUser,
10
+ isBoardQueuedPreset,
11
+ resetBoardSession,
12
+ } from './chalkboard/boardQueue';
13
+ import {subscribeBoardAbort} from './chalkboard/boardAbort';
14
+ import {isNonInteractivePreset, VaniraPresetId} from './registry';
15
+ import {LiveScreenPipOverlay} from './liveScreen/LiveScreenPipOverlay';
16
+ import {stopLiveScreen} from './liveScreen/liveScreenSession';
17
+ import {LiveVisionPipOverlay} from './liveVision/LiveVisionPipOverlay';
18
+ import {stopLiveVision} from './liveVision/liveVisionSession';
19
+ import {NativePresetRenderer} from './NativePresetRenderer';
20
+ import {toPresetClient} from './presetClientAdapter';
21
+
22
+ type PresetHostContextValue = {
23
+ aiClient: VaniraAI | null;
24
+ activeToolCall: unknown | null;
25
+ setAiClient: (client: VaniraAI | null) => void;
26
+ setActiveToolCall: (toolCall: unknown | null) => void;
27
+ clearPreset: () => void;
28
+ };
29
+
30
+ const PresetHostContext = createContext<PresetHostContextValue | null>(null);
31
+
32
+ export function usePresetHost(): PresetHostContextValue {
33
+ const ctx = useContext(PresetHostContext);
34
+ if (!ctx) {
35
+ throw new Error('usePresetHost must be used within PresetHostProvider');
36
+ }
37
+ return ctx;
38
+ }
39
+
40
+ type Props = {children: React.ReactNode};
41
+
42
+ /**
43
+ * Renders NativePresetRenderer at the app root so overlays sit above all screens.
44
+ */
45
+ export function PresetHostProvider({children}: Props) {
46
+ const [aiClient, setAiClient] = useState<VaniraAI | null>(null);
47
+ const [activeToolCall, setActiveToolCall] = useState<unknown | null>(null);
48
+ const captureRootRef = useRef<View>(null);
49
+
50
+ const syncClipCaptureTarget = useCallback(() => {
51
+ registerClipRegionCaptureTarget(captureRootRef.current);
52
+ }, []);
53
+
54
+ const releaseToolCall = useCallback(() => {
55
+ setActiveToolCall(null);
56
+ }, []);
57
+
58
+ const clearPreset = useCallback(() => {
59
+ resetBoardSession();
60
+ const client = toPresetClient(aiClient);
61
+ stopLiveVision(client);
62
+ stopLiveScreen(client);
63
+ setActiveToolCall(null);
64
+ }, [aiClient]);
65
+
66
+ useEffect(() => {
67
+ syncClipCaptureTarget();
68
+ }, [syncClipCaptureTarget]);
69
+
70
+ useEffect(() => {
71
+ return subscribeBoardAbort(detail => {
72
+ abortBoardTools(
73
+ detail.reason || 'Server interrupted',
74
+ detail.toolCallId,
75
+ 'interrupted',
76
+ );
77
+ });
78
+ }, []);
79
+
80
+ const value = useMemo(
81
+ () => ({
82
+ aiClient,
83
+ activeToolCall,
84
+ setAiClient,
85
+ setActiveToolCall,
86
+ clearPreset,
87
+ }),
88
+ [aiClient, activeToolCall, clearPreset],
89
+ );
90
+
91
+ const presetId = activeToolCall ? extractPresetId(activeToolCall) : undefined;
92
+ const boardPreset = presetId ? isBoardQueuedPreset(presetId) : false;
93
+ const headlessPreset =
94
+ (presetId && isNonInteractivePreset(presetId)) ||
95
+ boardPreset ||
96
+ presetId === VaniraPresetId.Camera;
97
+ const presetVisible = !!activeToolCall && !!presetId;
98
+ const modalVisible = presetVisible && !headlessPreset;
99
+
100
+ const renderer = (
101
+ <NativePresetRenderer
102
+ client={toPresetClient(aiClient)}
103
+ toolCall={activeToolCall}
104
+ onDismiss={clearPreset}
105
+ onReleaseToolCall={releaseToolCall}
106
+ />
107
+ );
108
+
109
+ return (
110
+ <PresetHostContext.Provider value={value}>
111
+ <View style={styles.host}>
112
+ <View
113
+ ref={captureRootRef}
114
+ collapsable={false}
115
+ style={styles.root}
116
+ onLayout={syncClipCaptureTarget}>
117
+ {children}
118
+ </View>
119
+ {presetVisible && !modalVisible ? (
120
+ <View
121
+ style={styles.headlessHost}
122
+ pointerEvents={boardPreset ? 'none' : 'box-none'}>
123
+ {renderer}
124
+ </View>
125
+ ) : null}
126
+ <Modal
127
+ visible={modalVisible}
128
+ transparent
129
+ animationType="fade"
130
+ statusBarTranslucent
131
+ presentationStyle="overFullScreen"
132
+ onRequestClose={clearPreset}>
133
+ {modalVisible ? renderer : null}
134
+ </Modal>
135
+ <ChalkboardOverlay onUserDismiss={() => dismissChalkboardUser()} />
136
+ <LiveVisionPipOverlay client={toPresetClient(aiClient)} />
137
+ <LiveScreenPipOverlay client={toPresetClient(aiClient)} />
138
+ </View>
139
+ </PresetHostContext.Provider>
140
+ );
141
+ }
142
+
143
+ const styles = StyleSheet.create({
144
+ host: {
145
+ flex: 1,
146
+ },
147
+ root: {
148
+ flex: 1,
149
+ },
150
+ headlessHost: {
151
+ ...StyleSheet.absoluteFillObject,
152
+ zIndex: 999999,
153
+ elevation: 999999,
154
+ },
155
+ });
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import {Pressable, StyleSheet, Text, View} from 'react-native';
3
+
4
+ type Props = {
5
+ visible: boolean;
6
+ title: string;
7
+ subtitle?: string;
8
+ onClose: () => void;
9
+ children?: React.ReactNode;
10
+ /** When true, modal provides its own Cancel/Confirm (calendar, form). */
11
+ hideDefaultClose?: boolean;
12
+ };
13
+
14
+ /**
15
+ * Preset card content — parent PresetHost wraps this in a root RN Modal.
16
+ */
17
+ export function PresetShellModal({
18
+ visible,
19
+ title,
20
+ subtitle,
21
+ onClose,
22
+ children,
23
+ hideDefaultClose = false,
24
+ }: Props) {
25
+ if (!visible) {
26
+ return null;
27
+ }
28
+
29
+ return (
30
+ <View style={styles.backdrop}>
31
+ <View style={styles.card}>
32
+ <View style={styles.headerRow}>
33
+ <View style={styles.headerText}>
34
+ <Text style={styles.title}>{title}</Text>
35
+ {subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
36
+ </View>
37
+ <Pressable style={styles.closeX} onPress={onClose} hitSlop={8}>
38
+ <Text style={styles.closeXText}>✕</Text>
39
+ </Pressable>
40
+ </View>
41
+ {children ?? (
42
+ <Text style={styles.placeholder}>
43
+ Native preset UI skeleton — logic pending parity with web
44
+ WidgetPresetRenderer.
45
+ </Text>
46
+ )}
47
+ {!hideDefaultClose ? (
48
+ <Pressable style={styles.closeBtn} onPress={onClose}>
49
+ <Text style={styles.closeBtnText}>Close</Text>
50
+ </Pressable>
51
+ ) : null}
52
+ </View>
53
+ </View>
54
+ );
55
+ }
56
+
57
+ const styles = StyleSheet.create({
58
+ backdrop: {
59
+ flex: 1,
60
+ backgroundColor: 'rgba(0,0,0,0.75)',
61
+ justifyContent: 'center',
62
+ padding: 20,
63
+ },
64
+ card: {
65
+ backgroundColor: '#fff',
66
+ borderRadius: 16,
67
+ padding: 20,
68
+ gap: 12,
69
+ maxHeight: '90%',
70
+ },
71
+ headerRow: {
72
+ flexDirection: 'row',
73
+ alignItems: 'flex-start',
74
+ gap: 8,
75
+ },
76
+ headerText: {flex: 1, gap: 4},
77
+ closeX: {
78
+ width: 28,
79
+ height: 28,
80
+ borderRadius: 14,
81
+ backgroundColor: '#f3f4f6',
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ },
85
+ closeXText: {fontSize: 14, color: '#6b7280', fontWeight: '600'},
86
+ title: {fontSize: 18, fontWeight: '700', color: '#111'},
87
+ subtitle: {fontSize: 13, color: '#6b7280', lineHeight: 18},
88
+ placeholder: {fontSize: 13, color: '#374151', lineHeight: 20},
89
+ closeBtn: {
90
+ marginTop: 8,
91
+ backgroundColor: '#111',
92
+ borderRadius: 10,
93
+ paddingVertical: 12,
94
+ alignItems: 'center',
95
+ },
96
+ closeBtnText: {color: '#fff', fontWeight: '600'},
97
+ });