@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,256 @@
1
+ import type {Asset} from 'react-native-image-picker';
2
+ import {launchImageLibrary} from 'react-native-image-picker';
3
+ import {
4
+ acceptRequiresDocumentPicker,
5
+ mimeMatchesAccept,
6
+ normalizeUploadMime,
7
+ WEB_DEFAULT_UPLOAD_ACCEPT,
8
+ } from './uploadFormats';
9
+ import {ensureGalleryPermission, type RNUploadFile} from './uploadUtils';
10
+
11
+ type DocumentPickerModule = {
12
+ pick: (opts: {
13
+ type: unknown[];
14
+ allowMultiSelection?: boolean;
15
+ }) => Promise<
16
+ Array<{
17
+ uri: string;
18
+ name?: string | null;
19
+ type?: string | null;
20
+ size?: number | null;
21
+ }>
22
+ >;
23
+ types: {
24
+ images: string;
25
+ pdf: string;
26
+ plainText: string;
27
+ csv: string | string[];
28
+ };
29
+ isErrorWithCode?: (err: unknown, code: string) => boolean;
30
+ errorCodes?: {OPERATION_CANCELED?: string};
31
+ };
32
+
33
+ function loadDocumentPicker(): DocumentPickerModule | null {
34
+ try {
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const mod = require('@react-native-documents/picker');
37
+ const pick = mod?.pick ?? mod?.default?.pick;
38
+ const types = mod?.types ?? mod?.default?.types;
39
+ if (typeof pick === 'function' && types) {
40
+ return {pick, types, isErrorWithCode: mod.isErrorWithCode, errorCodes: mod.errorCodes};
41
+ }
42
+ } catch {
43
+ /* host app may not link document picker */
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function acceptToDocumentPickerTypes(
49
+ accept: string,
50
+ docTypes: DocumentPickerModule['types'],
51
+ ): unknown[] {
52
+ const lower = accept.toLowerCase();
53
+ const result: unknown[] = [];
54
+
55
+ const add = (value: unknown) => {
56
+ if (Array.isArray(value)) {
57
+ value.forEach(v => {
58
+ if (!result.includes(v)) {
59
+ result.push(v);
60
+ }
61
+ });
62
+ return;
63
+ }
64
+ if (!result.includes(value)) {
65
+ result.push(value);
66
+ }
67
+ };
68
+
69
+ if (
70
+ lower.includes('image/*') ||
71
+ lower.includes('image/jpeg') ||
72
+ lower.includes('image/jpg') ||
73
+ lower.includes('image/png') ||
74
+ lower.includes('image/gif') ||
75
+ lower.includes('image/webp') ||
76
+ lower.includes('image/bmp')
77
+ ) {
78
+ add(docTypes.images);
79
+ }
80
+ if (lower.includes('pdf') || lower.includes('application/pdf')) {
81
+ add(docTypes.pdf);
82
+ }
83
+ if (lower.includes('text/plain')) {
84
+ add(docTypes.plainText);
85
+ }
86
+ if (lower.includes('text/csv') || lower.includes('csv')) {
87
+ add(docTypes.csv);
88
+ }
89
+
90
+ return result.length ? result : [docTypes.images, docTypes.pdf, docTypes.plainText, docTypes.csv];
91
+ }
92
+
93
+ function assetToUploadFile(
94
+ asset: Asset,
95
+ accept: string,
96
+ ): RNUploadFile | null {
97
+ if (!asset.uri) {
98
+ return null;
99
+ }
100
+
101
+ const name =
102
+ asset.fileName ||
103
+ (asset.uri.split('/').pop()?.split('?')[0] ?? `upload_${Date.now()}.jpg`);
104
+ const type = normalizeUploadMime(name, asset.type);
105
+
106
+ if (!mimeMatchesAccept(type, accept)) {
107
+ return null;
108
+ }
109
+
110
+ return {
111
+ uri: asset.uri,
112
+ type,
113
+ name,
114
+ size: asset.fileSize,
115
+ };
116
+ }
117
+
118
+ function uriToUploadFile(
119
+ uri: string,
120
+ name: string,
121
+ rawType: string | null | undefined,
122
+ size: number | null | undefined,
123
+ accept: string,
124
+ ): RNUploadFile | null {
125
+ const type = normalizeUploadMime(name, rawType);
126
+ if (!mimeMatchesAccept(type, accept)) {
127
+ return null;
128
+ }
129
+ return {uri, type, name, size: size ?? undefined};
130
+ }
131
+
132
+ async function pickFromDocumentLibrary(
133
+ accept: string,
134
+ ): Promise<RNUploadFile | null> {
135
+ const docPicker = loadDocumentPicker();
136
+ if (!docPicker) {
137
+ return null;
138
+ }
139
+
140
+ try {
141
+ const results = await docPicker.pick({
142
+ type: acceptToDocumentPickerTypes(accept, docPicker.types),
143
+ allowMultiSelection: false,
144
+ });
145
+ const file = results[0];
146
+ if (!file?.uri) {
147
+ return null;
148
+ }
149
+ const name = file.name || `upload_${Date.now()}`;
150
+ return uriToUploadFile(file.uri, name, file.type, file.size, accept);
151
+ } catch (err: unknown) {
152
+ const canceled =
153
+ docPicker.isErrorWithCode?.(
154
+ err,
155
+ docPicker.errorCodes?.OPERATION_CANCELED ?? 'OPERATION_CANCELED',
156
+ ) ?? false;
157
+ if (canceled) {
158
+ return null;
159
+ }
160
+ throw err;
161
+ }
162
+ }
163
+
164
+ async function pickFromGallery(accept: string): Promise<RNUploadFile | null> {
165
+ const galleryOk = await ensureGalleryPermission();
166
+ if (!galleryOk) {
167
+ throw new Error('GALLERY_PERMISSION_DENIED');
168
+ }
169
+
170
+ const pickerResult = await launchImageLibrary({
171
+ mediaType: pickerMediaTypeFromAcceptLocal(accept),
172
+ selectionLimit: 1,
173
+ includeBase64: false,
174
+ });
175
+
176
+ if (pickerResult.didCancel) {
177
+ return null;
178
+ }
179
+ if (pickerResult.errorCode) {
180
+ throw new Error(pickerResult.errorMessage ?? pickerResult.errorCode);
181
+ }
182
+
183
+ const asset = pickerResult.assets?.[0];
184
+ if (!asset) {
185
+ return null;
186
+ }
187
+
188
+ const file = assetToUploadFile(asset, accept);
189
+ if (!file) {
190
+ throw new Error('UNSUPPORTED_FILE_TYPE');
191
+ }
192
+ return file;
193
+ }
194
+
195
+ function pickerMediaTypeFromAcceptLocal(
196
+ accept: string,
197
+ ): 'photo' | 'mixed' {
198
+ const lower = accept.toLowerCase();
199
+ return lower.includes('video/') ? 'mixed' : 'photo';
200
+ }
201
+
202
+ export type PickUploadFileResult =
203
+ | {status: 'ok'; file: RNUploadFile}
204
+ | {status: 'cancelled'}
205
+ | {status: 'permission_denied'}
206
+ | {status: 'unsupported_type'}
207
+ | {status: 'error'; message: string};
208
+
209
+ /**
210
+ * Pick a file for vanira_upload — mirrors web `<input accept="...">`.
211
+ * Uses document picker when PDF/text are allowed; gallery for image-only accept.
212
+ */
213
+ export async function pickUploadFile(
214
+ accept: string = WEB_DEFAULT_UPLOAD_ACCEPT,
215
+ ): Promise<PickUploadFileResult> {
216
+ try {
217
+ const useDocumentPicker = acceptRequiresDocumentPicker(accept);
218
+
219
+ if (useDocumentPicker) {
220
+ const docFile = await pickFromDocumentLibrary(accept);
221
+ if (docFile) {
222
+ if (!mimeMatchesAccept(docFile.type, accept)) {
223
+ return {status: 'unsupported_type'};
224
+ }
225
+ return {status: 'ok', file: docFile};
226
+ }
227
+
228
+ // Document picker unavailable — fall back to gallery for images-only subset
229
+ if (!loadDocumentPicker()) {
230
+ const galleryFile = await pickFromGallery(accept);
231
+ if (galleryFile) {
232
+ return {status: 'ok', file: galleryFile};
233
+ }
234
+ return {status: 'cancelled'};
235
+ }
236
+
237
+ return {status: 'cancelled'};
238
+ }
239
+
240
+ const galleryFile = await pickFromGallery(accept);
241
+ if (!galleryFile) {
242
+ return {status: 'cancelled'};
243
+ }
244
+ return {status: 'ok', file: galleryFile};
245
+ } catch (err: unknown) {
246
+ if (err instanceof Error && err.message === 'GALLERY_PERMISSION_DENIED') {
247
+ return {status: 'permission_denied'};
248
+ }
249
+ if (err instanceof Error && err.message === 'UNSUPPORTED_FILE_TYPE') {
250
+ return {status: 'unsupported_type'};
251
+ }
252
+ const message =
253
+ err instanceof Error ? err.message : 'Could not open file picker.';
254
+ return {status: 'error', message};
255
+ }
256
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Upload accept + MIME parity with web WidgetPresetRenderer.renderUpload().
3
+ * Default accept: image/*, application/pdf, text/plain, text/csv
4
+ */
5
+
6
+ /** Same default as web vanira_upload preset. */
7
+ export const WEB_DEFAULT_UPLOAD_ACCEPT =
8
+ 'image/*,application/pdf,text/plain,text/csv';
9
+
10
+ /** MIME types accepted by VaniraAI.uploadMedia (web + RN). */
11
+ export const SUPPORTED_UPLOAD_MIME_TYPES = [
12
+ 'image/jpeg',
13
+ 'image/png',
14
+ 'image/gif',
15
+ 'image/webp',
16
+ 'image/bmp',
17
+ 'application/pdf',
18
+ 'text/plain',
19
+ 'text/csv',
20
+ ] as const;
21
+
22
+ export type SupportedUploadMime = (typeof SUPPORTED_UPLOAD_MIME_TYPES)[number];
23
+
24
+ const EXT_TO_MIME: Record<string, SupportedUploadMime | string> = {
25
+ jpg: 'image/jpeg',
26
+ jpeg: 'image/jpeg',
27
+ png: 'image/png',
28
+ gif: 'image/gif',
29
+ webp: 'image/webp',
30
+ bmp: 'image/bmp',
31
+ heic: 'image/jpeg',
32
+ heif: 'image/jpeg',
33
+ pdf: 'application/pdf',
34
+ txt: 'text/plain',
35
+ csv: 'text/csv',
36
+ };
37
+
38
+ const MIME_ALIASES: Record<string, SupportedUploadMime> = {
39
+ 'image/jpg': 'image/jpeg',
40
+ 'image/pjpeg': 'image/jpeg',
41
+ 'text/comma-separated-values': 'text/csv',
42
+ };
43
+
44
+ /** Normalize picker / filesystem MIME (jpg → jpeg, extension fallback). */
45
+ export function normalizeUploadMime(
46
+ fileName: string,
47
+ rawType?: string | null,
48
+ ): string {
49
+ const trimmed = (rawType ?? '').trim().toLowerCase();
50
+ if (trimmed && trimmed in MIME_ALIASES) {
51
+ return MIME_ALIASES[trimmed];
52
+ }
53
+ if (
54
+ trimmed &&
55
+ trimmed !== 'application/octet-stream' &&
56
+ !trimmed.endsWith('*')
57
+ ) {
58
+ return trimmed;
59
+ }
60
+
61
+ const ext = fileName.split('.').pop()?.toLowerCase();
62
+ if (ext && ext in EXT_TO_MIME) {
63
+ return EXT_TO_MIME[ext];
64
+ }
65
+
66
+ return trimmed || 'application/octet-stream';
67
+ }
68
+
69
+ export function isSupportedUploadMime(mime: string): boolean {
70
+ const normalized = normalizeUploadMime('', mime);
71
+ return (SUPPORTED_UPLOAD_MIME_TYPES as readonly string[]).includes(
72
+ normalized,
73
+ );
74
+ }
75
+
76
+ export function mimeMatchesAccept(mime: string, accept: string): boolean {
77
+ const normalized = normalizeUploadMime('', mime);
78
+ const tokens = accept
79
+ .split(',')
80
+ .map(t => t.trim().toLowerCase())
81
+ .filter(Boolean);
82
+
83
+ if (!tokens.length) {
84
+ return normalized.startsWith('image/');
85
+ }
86
+
87
+ return tokens.some(token => {
88
+ if (token === 'image/*') {
89
+ return normalized.startsWith('image/');
90
+ }
91
+ if (token.endsWith('/*')) {
92
+ const prefix = token.slice(0, -1);
93
+ return normalized.startsWith(prefix);
94
+ }
95
+ const canonical = normalizeUploadMime('', token);
96
+ return normalized === canonical || normalized === token;
97
+ });
98
+ }
99
+
100
+ /** True when accept includes PDF or text types (needs document picker on RN). */
101
+ export function acceptRequiresDocumentPicker(accept: string): boolean {
102
+ const lower = accept.toLowerCase();
103
+ return (
104
+ lower.includes('pdf') ||
105
+ lower.includes('text/plain') ||
106
+ lower.includes('text/csv') ||
107
+ lower.includes('application/') ||
108
+ /\.(pdf|txt|csv)\b/.test(lower)
109
+ );
110
+ }
111
+
112
+ export function formatAcceptHint(accept?: string): string {
113
+ const value = accept?.trim() || WEB_DEFAULT_UPLOAD_ACCEPT;
114
+ const lower = value.toLowerCase();
115
+
116
+ if (value === WEB_DEFAULT_UPLOAD_ACCEPT || lower.includes('text/csv')) {
117
+ return 'JPEG, JPG, PNG, GIF, WebP, BMP, PDF, TXT, CSV · Max 50 MB';
118
+ }
119
+
120
+ const parts: string[] = [];
121
+ if (lower.includes('image')) {
122
+ parts.push('JPEG, JPG, PNG, GIF, WebP, BMP');
123
+ }
124
+ if (lower.includes('pdf')) {
125
+ parts.push('PDF');
126
+ }
127
+ if (lower.includes('text/plain') || lower.includes('.txt')) {
128
+ parts.push('TXT');
129
+ }
130
+ if (lower.includes('csv')) {
131
+ parts.push('CSV');
132
+ }
133
+
134
+ return parts.length
135
+ ? `${parts.join(', ')} · Max 50 MB`
136
+ : 'JPEG, JPG, PNG · Max 50 MB';
137
+ }
138
+
139
+ export function pickerMediaTypeFromAccept(
140
+ accept?: string,
141
+ ): 'photo' | 'mixed' {
142
+ const lower = (accept ?? WEB_DEFAULT_UPLOAD_ACCEPT).toLowerCase();
143
+ if (
144
+ lower.includes('video/') ||
145
+ (lower.includes('image/*') && !acceptRequiresDocumentPicker(accept ?? ''))
146
+ ) {
147
+ return lower.includes('video/') ? 'mixed' : 'photo';
148
+ }
149
+ return 'photo';
150
+ }
151
+
152
+ export function filePreviewIcon(mime: string): string {
153
+ if (mime.startsWith('image/')) {
154
+ return '🖼️';
155
+ }
156
+ if (mime.includes('pdf')) {
157
+ return '📄';
158
+ }
159
+ if (mime.startsWith('text/')) {
160
+ return '📝';
161
+ }
162
+ return '📁';
163
+ }
@@ -0,0 +1,68 @@
1
+ import {Platform} from 'react-native';
2
+ import type {Asset} from 'react-native-image-picker';
3
+ import {PERMISSIONS, RESULTS, request} from 'react-native-permissions';
4
+ import {
5
+ formatAcceptHint,
6
+ mimeMatchesAccept,
7
+ normalizeUploadMime,
8
+ pickerMediaTypeFromAccept,
9
+ WEB_DEFAULT_UPLOAD_ACCEPT,
10
+ } from './uploadFormats';
11
+
12
+ export type RNUploadFile = {
13
+ uri: string;
14
+ type: string;
15
+ name: string;
16
+ size?: number;
17
+ };
18
+
19
+ export {
20
+ formatAcceptHint,
21
+ mimeMatchesAccept,
22
+ normalizeUploadMime,
23
+ pickerMediaTypeFromAccept,
24
+ WEB_DEFAULT_UPLOAD_ACCEPT,
25
+ };
26
+
27
+ export async function ensureGalleryPermission(): Promise<boolean> {
28
+ const permission =
29
+ Platform.OS === 'ios'
30
+ ? PERMISSIONS.IOS.PHOTO_LIBRARY
31
+ : Platform.OS === 'android'
32
+ ? Number(Platform.Version) >= 33
33
+ ? PERMISSIONS.ANDROID.READ_MEDIA_IMAGES
34
+ : PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE
35
+ : null;
36
+
37
+ if (!permission) {
38
+ return true;
39
+ }
40
+
41
+ const result = await request(permission);
42
+ return result === RESULTS.GRANTED || result === RESULTS.LIMITED;
43
+ }
44
+
45
+ export function assetToUploadFile(
46
+ asset: Asset,
47
+ accept: string = WEB_DEFAULT_UPLOAD_ACCEPT,
48
+ ): RNUploadFile | null {
49
+ if (!asset.uri) {
50
+ return null;
51
+ }
52
+
53
+ const name =
54
+ asset.fileName ||
55
+ (asset.uri.split('/').pop()?.split('?')[0] ?? `upload_${Date.now()}.jpg`);
56
+ const type = normalizeUploadMime(name, asset.type);
57
+
58
+ if (!mimeMatchesAccept(type, accept)) {
59
+ return null;
60
+ }
61
+
62
+ return {
63
+ uri: asset.uri,
64
+ type,
65
+ name,
66
+ size: asset.fileSize,
67
+ };
68
+ }
@@ -0,0 +1,144 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { WidgetPresetRenderer } from '../ui/presets/WidgetPresetRenderer';
3
+
4
+ export interface BaseClient {
5
+ sendToolResult: (toolCallId: string, result: any) => void;
6
+ sendToolError: (toolCallId: string, error: string) => void;
7
+ sendContextUpdate?: (data: any) => void;
8
+ triggerActionInterrupt?: () => void;
9
+ sendActionTrigger?: (actionName: string, data: Record<string, any>) => void;
10
+ }
11
+
12
+ export interface PresetRendererProps {
13
+ client: BaseClient | null;
14
+ toolCall: any | null;
15
+ registry?: Record<string, any>;
16
+ /** Fired immediately if the toolCall is NOT a preset */
17
+ onCustomTool?: (toolCall: any) => void;
18
+ }
19
+
20
+ export const PresetRenderer: React.FC<PresetRendererProps> = ({
21
+ client,
22
+ toolCall,
23
+ onCustomTool
24
+ }) => {
25
+ const rendererRef = useRef<WidgetPresetRenderer | null>(null);
26
+
27
+ useEffect(() => {
28
+ rendererRef.current = new WidgetPresetRenderer(null as any);
29
+ return () => {
30
+ rendererRef.current?.dismiss();
31
+ };
32
+ }, []);
33
+
34
+ useEffect(() => {
35
+ if (!toolCall || !rendererRef.current) return;
36
+
37
+ const data = toolCall.data || toolCall;
38
+
39
+ let args: Record<string, any> = {};
40
+ const rawArgs = data.arguments || data.args;
41
+ if (typeof rawArgs === 'string') {
42
+ try { args = JSON.parse(rawArgs); } catch (_) { args = {}; }
43
+ } else if (rawArgs && typeof rawArgs === 'object') {
44
+ args = rawArgs;
45
+ }
46
+
47
+ const clientFields = data.client_fields || {};
48
+ const presetId = clientFields?.preset_id || args?.preset_id;
49
+ const toolCallId = data.tool_call_id || data.call_id || '';
50
+
51
+ // If it's a known preset supported by vanilla WidgetPresetRenderer
52
+ const knownPresets = [
53
+ 'vanira_form',
54
+ 'vanira_calendar',
55
+ 'vanira_navigate',
56
+ 'vanira_upload',
57
+ 'vanira_camera',
58
+ 'vanira_highlight_element',
59
+ 'vanira_type_text',
60
+ 'vanira_erase_text'
61
+ ];
62
+
63
+ if (presetId && knownPresets.includes(presetId)) {
64
+ // Auto-acknowledge so the AI stays silent while user fills the form (skip for highlight, type_text, and erase_text)
65
+ const isNonInteractive = presetId === 'vanira_highlight_element' || presetId === 'vanira_type_text' || presetId === 'vanira_erase_text';
66
+ if (!isNonInteractive && client && client.sendContextUpdate && toolCallId) {
67
+ client.sendToolResult(toolCallId, {
68
+ status: "popup_shown",
69
+ message: `The UI preset ${presetId} is now visible. I am waiting for the user to complete it. STAY SILENT.`
70
+ });
71
+ client.sendContextUpdate({
72
+ ui_state: "waiting_for_preset_data",
73
+ tool_status: "active_waiting",
74
+ visible_preset: presetId
75
+ });
76
+ }
77
+
78
+ // Handle with vanilla renderer
79
+ rendererRef.current.handle(
80
+ toolCall,
81
+ (result) => {
82
+ console.log(`🎯 [PresetRenderer Wrapper] Preset ${presetId} completed! Result:`, result);
83
+ if (client) {
84
+ const payload = typeof result === 'string' ? { response: result } : result;
85
+
86
+ // Skip client action trigger if client_media_update already happened (payload has media_id)
87
+ if (payload && (payload.media_id || payload.url)) {
88
+ console.log('[PresetRenderer Wrapper] Skipping sendActionTrigger because media was already updated.');
89
+ return;
90
+ }
91
+
92
+ // If it's a typing or erase preset, don't send back anything to the AI
93
+ if (presetId === 'vanira_type_text' || presetId === 'vanira_erase_text') {
94
+ console.log(`[PresetRenderer Wrapper] Skipping sendActionTrigger/sendToolResult for preset: ${presetId}`);
95
+ return;
96
+ }
97
+
98
+ // 1. Wake up the AI FIRST to clear its buffers
99
+ if (client.triggerActionInterrupt) {
100
+ console.log(`⚡ [PresetRenderer Wrapper] Triggering action interrupt to wake AI...`);
101
+ client.triggerActionInterrupt();
102
+ }
103
+
104
+ // 2. Send the action trigger with dynamic naming based on the tool
105
+ if (client.sendActionTrigger) {
106
+ const actionName = clientFields.action_name || (presetId === 'vanira_calendar' ? 'calendar_slot_selected' : `user_submitted_${data.name}`);
107
+ console.log(`📤 [PresetRenderer Wrapper] Submitting preset data via sendActionTrigger (${actionName})...`);
108
+ client.sendActionTrigger(actionName, payload);
109
+ } else {
110
+ // Fallback for older clients
111
+ console.log(`📤 [PresetRenderer Wrapper] Submitting tool result for ID: ${toolCallId}`);
112
+ client.sendToolResult(toolCallId, payload);
113
+ }
114
+ }
115
+ },
116
+ (reason) => {
117
+ console.log(`❌ [PresetRenderer Wrapper] Preset ${presetId} cancelled:`, reason);
118
+ if (client) {
119
+ // If it's a typing or erase preset, don't send back anything to the AI
120
+ if (presetId === 'vanira_type_text' || presetId === 'vanira_erase_text') {
121
+ console.log(`[PresetRenderer Wrapper] Skipping sendToolError/sendToolResult onCancel for preset: ${presetId}`);
122
+ return;
123
+ }
124
+
125
+ if (typeof client.sendToolError === 'function') {
126
+ client.sendToolError(toolCallId, reason || 'User cancelled the action');
127
+ } else {
128
+ // Fallback: send a tool result with cancelled status so the AI isn't left hanging
129
+ try {
130
+ client.sendToolResult(toolCallId, { status: 'cancelled', reason: reason || 'User dismissed' });
131
+ } catch (_) { /* silent */ }
132
+ }
133
+ }
134
+ },
135
+ client
136
+ );
137
+ } else if (onCustomTool) {
138
+ // Not a preset. Hand execution back to the developer!
139
+ onCustomTool(toolCall);
140
+ }
141
+ }, [toolCall, client, onCustomTool]);
142
+
143
+ return null;
144
+ };
@@ -0,0 +1 @@
1
+ export * from './PresetRenderer';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Browser Runtime
3
+ *
4
+ * Pre-wires all browser adapters into a VaniraRuntime.
5
+ * Import this module only in browser/web environments — it references
6
+ * browser globals (HTMLAudioElement, navigator.mediaDevices, RTCPeerConnection).
7
+ *
8
+ * Usage:
9
+ * import { browserRuntime, createBrowserClient } from 'vanira-sdk/runtime/browser';
10
+ *
11
+ * const client = createBrowserClient({ agentId: '...', apiKey: '...' });
12
+ */
13
+
14
+ import type { WebRTCClientConfig } from '../types';
15
+
16
+ import { WebRTCClient } from '../core/WebRTCClient';
17
+ import { VaniraAI } from '../core/VaniraAI';
18
+
19
+ import {
20
+ browserDefaultBundle,
21
+ browserCapabilities,
22
+ } from './runtimeBundles';
23
+
24
+ // Re-export bundle + capabilities (single source of truth in runtimeBundles.ts)
25
+ export { browserCapabilities };
26
+ export const browserRuntime = browserDefaultBundle;
27
+
28
+ // ─── Factory functions ────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Create a WebRTCClient pre-wired for the browser runtime.
32
+ * Adapter fields (audioAdapter, mediaAdapter, peerAdapter, dataChannelAdapter)
33
+ * are set automatically — do not set them manually.
34
+ */
35
+ export function createBrowserClient(
36
+ config: Omit<WebRTCClientConfig, 'audioAdapter' | 'mediaAdapter' | 'peerAdapter' | 'dataChannelAdapter'>
37
+ ): WebRTCClient {
38
+ return new WebRTCClient({
39
+ ...config,
40
+ runtime: browserRuntime,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Create a VaniraAI instance pre-wired for the browser runtime.
46
+ */
47
+ export function createBrowserAI(
48
+ config: Omit<WebRTCClientConfig, 'audioAdapter' | 'mediaAdapter' | 'peerAdapter' | 'dataChannelAdapter'>
49
+ ): VaniraAI {
50
+ return new VaniraAI({
51
+ ...config,
52
+ runtime: browserRuntime,
53
+ });
54
+ }