@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,312 @@
1
+ import React, {useEffect, useRef, useState} from 'react';
2
+ import {Pressable, StyleSheet, Text, View} from 'react-native';
3
+ import {RTCView, mediaDevices, type MediaStream} from 'react-native-webrtc';
4
+ import {captureRef} from 'react-native-view-shot';
5
+ import type {PresetClient} from '../types';
6
+ import {captureAndSendStreamingFrame} from '../streaming/mediaFrameUpload';
7
+ import {
8
+ getLiveScreenSession,
9
+ parseLiveScreenArgs,
10
+ stopLiveScreen,
11
+ subscribeLiveScreen,
12
+ type LiveScreenSession,
13
+ } from './liveScreenSession';
14
+
15
+ type Props = {
16
+ client: PresetClient | null;
17
+ };
18
+
19
+ /**
20
+ * Screen share host via react-native-webrtc getDisplayMedia.
21
+ * Frame capture uses view-shot on the RTCView preview (best-effort on mobile).
22
+ */
23
+ export function LiveScreenCaptureHost({client}: Props) {
24
+ const [session, setSession] = useState<LiveScreenSession | null>(
25
+ getLiveScreenSession(),
26
+ );
27
+ const [stream, setStream] = useState<MediaStream | null>(null);
28
+ const [streamError, setStreamError] = useState<string | null>(null);
29
+ const previewRef = useRef<View>(null);
30
+ const frameSeqRef = useRef(0);
31
+ const captureTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
32
+ const autoStopTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
33
+ const runningSessionIdRef = useRef<string | null>(null);
34
+ const uploadingRef = useRef(false);
35
+ const startedSessionRef = useRef<string | null>(null);
36
+
37
+ useEffect(
38
+ () =>
39
+ subscribeLiveScreen(() => {
40
+ setSession(getLiveScreenSession());
41
+ }),
42
+ [],
43
+ );
44
+
45
+ useEffect(() => {
46
+ if (!client) {
47
+ stopLiveScreen(null);
48
+ }
49
+ }, [client]);
50
+
51
+ useEffect(() => {
52
+ if (!session || !client) {
53
+ if (stream) {
54
+ stream.getTracks().forEach(track => track.stop());
55
+ setStream(null);
56
+ }
57
+ runningSessionIdRef.current = null;
58
+ startedSessionRef.current = null;
59
+ if (captureTimerRef.current) {
60
+ clearInterval(captureTimerRef.current);
61
+ captureTimerRef.current = null;
62
+ }
63
+ if (autoStopTimerRef.current) {
64
+ clearTimeout(autoStopTimerRef.current);
65
+ autoStopTimerRef.current = null;
66
+ }
67
+ return;
68
+ }
69
+
70
+ if (runningSessionIdRef.current === session.sessionId) {
71
+ return;
72
+ }
73
+ runningSessionIdRef.current = session.sessionId;
74
+ setStreamError(null);
75
+
76
+ const parsed = parseLiveScreenArgs(session.args);
77
+ let activeStream: MediaStream | null = null;
78
+ let cancelled = false;
79
+
80
+ (async () => {
81
+ try {
82
+ activeStream = await mediaDevices.getDisplayMedia();
83
+ if (cancelled || runningSessionIdRef.current !== session.sessionId) {
84
+ activeStream.getTracks().forEach(track => track.stop());
85
+ return;
86
+ }
87
+
88
+ setStream(activeStream);
89
+ const track = activeStream.getVideoTracks()[0];
90
+ if (track) {
91
+ track.onended = () => {
92
+ console.log('[LiveScreen] User stopped sharing via system UI');
93
+ stopLiveScreen(client);
94
+ };
95
+ }
96
+
97
+ if (startedSessionRef.current !== session.sessionId) {
98
+ startedSessionRef.current = session.sessionId;
99
+ client.sendEvent?.('client_live_screen_started', {
100
+ data: {
101
+ session_id: session.sessionId,
102
+ scope: parsed.scope,
103
+ ...(parsed.scope === 'timed' && parsed.durationSec > 0
104
+ ? {duration_sec: parsed.durationSec}
105
+ : {}),
106
+ display_surface: 'mobile',
107
+ reason: parsed.reason,
108
+ target_fps: parsed.targetFps,
109
+ },
110
+ });
111
+ console.log(
112
+ `[LiveScreen] Session started: ${session.sessionId} (${parsed.targetFps} fps)`,
113
+ );
114
+ }
115
+
116
+ const intervalMs = Math.round(1000 / parsed.targetFps);
117
+ captureTimerRef.current = setInterval(async () => {
118
+ if (
119
+ uploadingRef.current ||
120
+ !previewRef.current ||
121
+ runningSessionIdRef.current !== session.sessionId
122
+ ) {
123
+ return;
124
+ }
125
+ uploadingRef.current = true;
126
+ try {
127
+ const uri = await captureRef(previewRef, {
128
+ format: 'jpg',
129
+ quality: 0.75,
130
+ result: 'tmpfile',
131
+ });
132
+ if (!uri) {
133
+ return;
134
+ }
135
+ frameSeqRef.current = await captureAndSendStreamingFrame(
136
+ client,
137
+ session.sessionId,
138
+ uri,
139
+ parsed.reason,
140
+ frameSeqRef.current,
141
+ () => runningSessionIdRef.current === session.sessionId,
142
+ {source: 'screen'},
143
+ );
144
+ } catch (err) {
145
+ console.warn('[LiveScreen] Frame capture failed:', err);
146
+ } finally {
147
+ uploadingRef.current = false;
148
+ }
149
+ }, intervalMs);
150
+
151
+ if (parsed.scope === 'timed' && parsed.durationSec > 0) {
152
+ autoStopTimerRef.current = setTimeout(() => {
153
+ console.log(
154
+ `[LiveScreen] Timed session expired (${parsed.durationSec}s)`,
155
+ );
156
+ stopLiveScreen(client);
157
+ }, parsed.durationSec * 1000);
158
+ }
159
+ } catch (err: unknown) {
160
+ const message =
161
+ err instanceof Error ? err.message : 'Screen share permission denied';
162
+ console.warn('[LiveScreen] getDisplayMedia failed:', message);
163
+ setStreamError(message);
164
+ client.sendEvent?.('client_live_screen_denied', {
165
+ data: {session_id: session.sessionId, reason: message},
166
+ });
167
+ stopLiveScreen(client);
168
+ }
169
+ })();
170
+
171
+ return () => {
172
+ cancelled = true;
173
+ if (captureTimerRef.current) {
174
+ clearInterval(captureTimerRef.current);
175
+ captureTimerRef.current = null;
176
+ }
177
+ if (autoStopTimerRef.current) {
178
+ clearTimeout(autoStopTimerRef.current);
179
+ autoStopTimerRef.current = null;
180
+ }
181
+ if (activeStream) {
182
+ activeStream.getTracks().forEach(track => track.stop());
183
+ }
184
+ };
185
+ }, [session, client]);
186
+
187
+ if (!session) {
188
+ return null;
189
+ }
190
+
191
+ const parsed = parseLiveScreenArgs(session.args);
192
+ const showPreview = parsed.showPreview;
193
+
194
+ if (streamError) {
195
+ return null;
196
+ }
197
+
198
+ if (!showPreview && stream) {
199
+ return (
200
+ <View
201
+ ref={previewRef}
202
+ collapsable={false}
203
+ style={styles.offscreen}
204
+ pointerEvents="none">
205
+ <RTCView
206
+ streamURL={stream.toURL()}
207
+ style={styles.offscreenRtc}
208
+ objectFit="contain"
209
+ />
210
+ </View>
211
+ );
212
+ }
213
+
214
+ if (!showPreview) {
215
+ return null;
216
+ }
217
+
218
+ return (
219
+ <View style={styles.pipContainer} pointerEvents="box-none">
220
+ <View ref={previewRef} collapsable={false} style={styles.pipFrame}>
221
+ {stream ? (
222
+ <RTCView
223
+ streamURL={stream.toURL()}
224
+ style={styles.rtc}
225
+ objectFit="contain"
226
+ />
227
+ ) : (
228
+ <View style={styles.loading}>
229
+ <Text style={styles.loadingText}>Starting screen share…</Text>
230
+ </View>
231
+ )}
232
+ <View style={styles.badge}>
233
+ <View style={styles.dot} />
234
+ <Text style={styles.badgeText}>SCREEN</Text>
235
+ </View>
236
+ <Pressable
237
+ style={styles.stopBtn}
238
+ onPress={() => stopLiveScreen(client)}
239
+ hitSlop={8}>
240
+ <Text style={styles.stopText}>✕</Text>
241
+ </Pressable>
242
+ </View>
243
+ </View>
244
+ );
245
+ }
246
+
247
+ const styles = StyleSheet.create({
248
+ pipContainer: {
249
+ position: 'absolute',
250
+ bottom: 24,
251
+ right: 16,
252
+ zIndex: 999998,
253
+ elevation: 999998,
254
+ },
255
+ pipFrame: {
256
+ width: 220,
257
+ height: 124,
258
+ borderRadius: 16,
259
+ overflow: 'hidden',
260
+ backgroundColor: '#0f172a',
261
+ borderWidth: 1,
262
+ borderColor: 'rgba(96,165,250,0.25)',
263
+ },
264
+ rtc: {flex: 1, backgroundColor: '#020617'},
265
+ offscreen: {
266
+ position: 'absolute',
267
+ width: 320,
268
+ height: 180,
269
+ opacity: 0.01,
270
+ left: -400,
271
+ top: 0,
272
+ },
273
+ offscreenRtc: {flex: 1},
274
+ loading: {flex: 1, alignItems: 'center', justifyContent: 'center'},
275
+ loadingText: {color: '#94a3b8', fontSize: 11},
276
+ badge: {
277
+ position: 'absolute',
278
+ top: 10,
279
+ left: 10,
280
+ flexDirection: 'row',
281
+ alignItems: 'center',
282
+ gap: 6,
283
+ paddingHorizontal: 8,
284
+ paddingVertical: 3,
285
+ borderRadius: 6,
286
+ backgroundColor: 'rgba(15,23,42,0.8)',
287
+ },
288
+ dot: {
289
+ width: 6,
290
+ height: 6,
291
+ borderRadius: 3,
292
+ backgroundColor: '#3b82f6',
293
+ },
294
+ badgeText: {
295
+ color: '#f8fafc',
296
+ fontSize: 10,
297
+ fontWeight: '700',
298
+ letterSpacing: 0.5,
299
+ },
300
+ stopBtn: {
301
+ position: 'absolute',
302
+ top: 10,
303
+ right: 10,
304
+ width: 22,
305
+ height: 22,
306
+ borderRadius: 11,
307
+ backgroundColor: 'rgba(15,23,42,0.8)',
308
+ alignItems: 'center',
309
+ justifyContent: 'center',
310
+ },
311
+ stopText: {color: '#fff', fontSize: 10, fontWeight: '700'},
312
+ });
@@ -0,0 +1,25 @@
1
+ import {useEffect} from 'react';
2
+ import type {PresetClient, PresetContext} from '../types';
3
+ import {beginLiveScreen} from './liveScreenSession';
4
+
5
+ type Props = {
6
+ client: PresetClient | null;
7
+ ctx: PresetContext;
8
+ onReleaseToolCall?: () => void;
9
+ };
10
+
11
+ /** Headless open handler — screen share runs in LiveScreenPipOverlay until close/disconnect. */
12
+ export function LiveScreenHandler({client, ctx, onReleaseToolCall}: Props) {
13
+ useEffect(() => {
14
+ if (!client) {
15
+ return;
16
+ }
17
+
18
+ const merged = {...ctx.args, ...ctx.clientFields};
19
+ beginLiveScreen(client, merged);
20
+ onReleaseToolCall?.();
21
+ // eslint-disable-next-line react-hooks/exhaustive-deps
22
+ }, [client, ctx.toolCallId]);
23
+
24
+ return null;
25
+ }
@@ -0,0 +1,6 @@
1
+ import type {PresetClient} from '../types';
2
+ import {LiveScreenCaptureHost} from './LiveScreenCaptureHost';
3
+
4
+ export function LiveScreenPipOverlay({client}: {client: PresetClient | null}) {
5
+ return <LiveScreenCaptureHost client={client} />;
6
+ }
@@ -0,0 +1,73 @@
1
+ import type {PresetClient} from '../types';
2
+
3
+ export type LiveScreenSession = {
4
+ sessionId: string;
5
+ client: PresetClient;
6
+ args: Record<string, unknown>;
7
+ };
8
+
9
+ let activeSession: LiveScreenSession | null = null;
10
+ const listeners = new Set<() => void>();
11
+
12
+ function notify(): void {
13
+ listeners.forEach(listener => listener());
14
+ }
15
+
16
+ export function subscribeLiveScreen(listener: () => void): () => void {
17
+ listeners.add(listener);
18
+ return () => listeners.delete(listener);
19
+ }
20
+
21
+ export function getLiveScreenSession(): LiveScreenSession | null {
22
+ return activeSession;
23
+ }
24
+
25
+ export function beginLiveScreen(
26
+ client: PresetClient,
27
+ args: Record<string, unknown>,
28
+ ): string {
29
+ stopLiveScreen(client);
30
+
31
+ const sessionId = `ls_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
32
+ activeSession = {sessionId, client, args};
33
+ notify();
34
+ return sessionId;
35
+ }
36
+
37
+ export function stopLiveScreen(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_screen_stopped', {
49
+ data: {session_id: sessionId},
50
+ });
51
+ }
52
+ console.log(`[LiveScreen] Session stopped: ${sessionId}`);
53
+ }
54
+
55
+ export function parseLiveScreenArgs(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 ?? 1280);
60
+ const reason = String(args.reason ?? 'screen_capture');
61
+ const durationSec = Number(args.duration_sec ?? 0);
62
+ const showPreview =
63
+ args.show_preview !== false && args.show_preview !== 'false';
64
+
65
+ return {
66
+ scope,
67
+ targetFps,
68
+ maxWidth,
69
+ reason,
70
+ durationSec,
71
+ showPreview,
72
+ };
73
+ }
@@ -0,0 +1,29 @@
1
+ import {useEffect} from 'react';
2
+ import type {PresetClient, PresetContext} from '../types';
3
+ import {stopLiveVision} from './liveVisionSession';
4
+
5
+ type Props = {
6
+ client: PresetClient | null;
7
+ ctx: PresetContext;
8
+ onComplete: (payload: Record<string, unknown>) => void;
9
+ onDismiss?: () => void;
10
+ };
11
+
12
+ export function CloseLiveVisionHandler({
13
+ client,
14
+ ctx,
15
+ onComplete,
16
+ onDismiss,
17
+ }: Props) {
18
+ useEffect(() => {
19
+ stopLiveVision(client);
20
+ onComplete({
21
+ status: 'success',
22
+ preset_id: ctx.presetId,
23
+ message: 'Live camera stopped',
24
+ });
25
+ onDismiss?.();
26
+ }, [client, ctx.presetId, onComplete, onDismiss]);
27
+
28
+ return null;
29
+ }