@facepe/widget-mobile-app 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -253,6 +253,7 @@ const styles = StyleSheet.create({
253
253
  | `objectFit` | `'cover' \| 'contain'` | `'cover'` | Video aspect scaling mode. |
254
254
  | `mirror` | `boolean` | `false` | Mirror video orientation. |
255
255
  | `autoStart` | `boolean` | `true` | Automatically connect and start stream on mount. |
256
+ | `posterUrl` | `string \| null` | avatar's portrait | Picture shown until the video starts. Defaults to the avatar's portrait from FacePe; pass `null` for none. |
256
257
  | `enableMic` | `boolean` | `true` | Automatically enable microphone on connect. |
257
258
  | `onTranscript` | `function` | `undefined` | Fires when speech transcription arrives. |
258
259
  | `onSpeakStart` | `function` | `undefined` | Fires when avatar starts speaking. |
@@ -289,5 +290,56 @@ await avatarRef.current?.interrupt();
289
290
 
290
291
  ---
291
292
 
293
+ ## 🗂️ Conversation data (`onConversationEnd`)
294
+
295
+ When a conversation ends, the SDK hands your app the **whole conversation** in
296
+ one record. It stays in your app — the SDK sends it nowhere; you decide whether
297
+ to show it, save it on your server, or ignore it.
298
+
299
+ ```tsx
300
+ <FacePeMobileAvatar
301
+ onConversationEnd={(conversation) => {
302
+ console.log(conversation.durationSec, conversation.transcript.length);
303
+ // e.g. save it on your own server:
304
+ // fetch('https://your-server.example.com/conversations', { method: 'POST', body: JSON.stringify(conversation) });
305
+ }}
306
+ />
307
+ ```
308
+
309
+ It is called once per conversation: after `stop()`, when the stream drops, or
310
+ when the component unmounts.
311
+
312
+ ```json
313
+ {
314
+ "conversationId": "cm_usage_123",
315
+ "avatar": { "id": "cm_avatar_456", "name": "Dharun" },
316
+ "language": "en",
317
+ "platform": "android",
318
+ "sdkVersion": "1.3.0",
319
+ "startedAt": "2026-10-12T11:40:02.000Z",
320
+ "endedAt": "2026-10-12T11:43:17.000Z",
321
+ "durationSec": 195,
322
+ "endReason": "ended",
323
+ "transcript": [
324
+ { "role": "assistant", "text": "Welcome! How can I help you today?", "at": "2026-10-12T11:40:05.000Z" },
325
+ { "role": "user", "text": "Two cheeseburgers please.", "at": "2026-10-12T11:40:11.000Z" }
326
+ ]
327
+ }
328
+ ```
329
+
330
+ | Field | Meaning |
331
+ |---|---|
332
+ | `conversationId` | The conversation's FacePe id (its usage record) |
333
+ | `avatar` | `{ id, name }` of the avatar |
334
+ | `language` | The language requested for the conversation |
335
+ | `platform` | `web`, `android` or `ios` |
336
+ | `sdkVersion` | The SDK version that recorded it |
337
+ | `startedAt`, `endedAt` | ISO 8601 times |
338
+ | `durationSec` | Length in seconds |
339
+ | `endReason` | `ended` (the user or your code ended it), `disconnected` (the stream dropped), `unmounted` (the component went away) |
340
+ | `transcript` | Every line in order: `{ role: "user" \| "assistant", text, at }` |
341
+
342
+ ---
343
+
292
344
  ## 📄 License
293
345
  MIT © FacePe AI
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState, useImperativeHandle, forwardRef, useCallback, } from 'react';
3
- import { View, StyleSheet, ActivityIndicator, Text, TouchableOpacity } from 'react-native';
3
+ import { View, StyleSheet, ActivityIndicator, Text, TouchableOpacity, Platform, Image } from 'react-native';
4
+ import { SDK_VERSION } from './version';
4
5
  import { useFacePeMobileContext } from './FacePeMobileProvider';
5
6
  // Resolve LiveKit dynamically at runtime so bare/Expo projects can run smoothly.
6
7
  // @livekit/react-native brings the native parts (registerGlobals, AudioSession,
@@ -57,8 +58,36 @@ function ensureLiveKitGlobals(LiveKitRN) {
57
58
  console.warn('[FacePeMobileAvatar] registerGlobals warning:', err);
58
59
  }
59
60
  }
60
- export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', style, videoStyle, objectFit = 'cover', mirror = false, autoStart = true, enableMic = true, onTranscript, onSpeakStart, onSpeakEnd, onUserSpeakStart, onUserSpeakEnd, onOrderUpdate, onCheckout, onStatusChange, onError, }, ref) => {
61
+ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', style, videoStyle, objectFit = 'cover', mirror = false, autoStart = true, enableMic = true, onTranscript, onSpeakStart, onSpeakEnd, onUserSpeakStart, onUserSpeakEnd, onOrderUpdate, onCheckout, onStatusChange, onError, onConversationEnd, posterUrl, }, ref) => {
61
62
  const { token, loading: tokenLoading, error: tokenError, apiBaseUrl, refreshToken } = useFacePeMobileContext();
63
+ // The avatar's portrait, shown until its video arrives. Looked up from the
64
+ // avatars this token may use, unless the app gave one (or null for none).
65
+ const [autoPoster, setAutoPoster] = useState(null);
66
+ useEffect(() => {
67
+ if (posterUrl !== undefined || !token)
68
+ return;
69
+ let cancelled = false;
70
+ void (async () => {
71
+ try {
72
+ const res = await fetch(`${apiBaseUrl.replace(/\/+$/, '')}/avatars`, {
73
+ headers: { Authorization: `Bearer ${token}` },
74
+ });
75
+ if (!res.ok)
76
+ return;
77
+ const avatars = (await res.json())?.data?.avatars ?? [];
78
+ const avatar = avatarKey ? avatars.find((a) => a.id === avatarKey) : avatars[0];
79
+ if (!cancelled)
80
+ setAutoPoster(avatar?.previewUrl ?? null);
81
+ }
82
+ catch {
83
+ /* no picture: the plain background stays */
84
+ }
85
+ })();
86
+ return () => {
87
+ cancelled = true;
88
+ };
89
+ }, [token, apiBaseUrl, avatarKey, posterUrl]);
90
+ const poster = posterUrl !== undefined ? posterUrl : autoPoster;
62
91
  const [status, setStatus] = useState('idle');
63
92
  const [videoTrack, setVideoTrack] = useState(null);
64
93
  const [isMicEnabled, setIsMicEnabled] = useState(enableMic);
@@ -72,6 +101,50 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
72
101
  const streamClientRef = useRef(null);
73
102
  const [streamURL, setStreamURL] = useState(null);
74
103
  const usageIdRef = useRef(null);
104
+ // The conversation as it happens, for the onConversationEnd record. Lines
105
+ // that carry an id (a line that grows while spoken) replace their earlier version.
106
+ const conversationRef = useRef(null);
107
+ const onConversationEndRef = useRef(onConversationEnd);
108
+ onConversationEndRef.current = onConversationEnd;
109
+ const onTranscriptRef = useRef(onTranscript);
110
+ onTranscriptRef.current = onTranscript;
111
+ // A transcript line: recorded for the conversation record, and passed on.
112
+ const emitLine = (role, text, id) => {
113
+ const conv = conversationRef.current;
114
+ if (conv && text) {
115
+ const same = id ? conv.lines.find((l) => l.id === id) : undefined;
116
+ if (same)
117
+ same.text = text;
118
+ else
119
+ conv.lines.push({ id, role, text, at: new Date().toISOString() });
120
+ }
121
+ onTranscriptRef.current?.({ payload: { role, text, isFinal: true } });
122
+ };
123
+ // Hands the whole conversation to the app, once.
124
+ const finishConversation = (endReason) => {
125
+ const conv = conversationRef.current;
126
+ conversationRef.current = null;
127
+ if (!conv)
128
+ return;
129
+ const endedAt = new Date();
130
+ try {
131
+ onConversationEndRef.current?.({
132
+ conversationId: conv.conversationId,
133
+ avatar: conv.avatar,
134
+ language: conv.language,
135
+ platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'web',
136
+ sdkVersion: SDK_VERSION,
137
+ startedAt: conv.startedAt.toISOString(),
138
+ endedAt: endedAt.toISOString(),
139
+ durationSec: Math.max(0, Math.round((endedAt.getTime() - conv.startedAt.getTime()) / 1000)),
140
+ endReason,
141
+ transcript: conv.lines.map(({ role, text, at }) => ({ role, text, at })),
142
+ });
143
+ }
144
+ catch (err) {
145
+ console.warn('[FacePeMobileAvatar] onConversationEnd error:', err);
146
+ }
147
+ };
75
148
  const mountedRef = useRef(true);
76
149
  const isConnectingRef = useRef(false);
77
150
  const tokenRef = useRef(token);
@@ -191,6 +264,13 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
191
264
  const sessionPayload = await sessionResp.json();
192
265
  const sessionInfo = sessionPayload?.data || sessionPayload;
193
266
  usageIdRef.current = sessionInfo?.usageId || null;
267
+ conversationRef.current = {
268
+ conversationId: usageIdRef.current,
269
+ avatar: { id: String(sessionInfo?.avatar?.id ?? targetAvatarId), name: String(sessionInfo?.avatar?.name ?? '') },
270
+ language: language ?? null,
271
+ startedAt: new Date(),
272
+ lines: [],
273
+ };
194
274
  const connection = sessionInfo?.connection;
195
275
  if (connection?.type === 'stream') {
196
276
  // Hosted avatar: the stream SDK gives plain MediaStreams (video +
@@ -212,7 +292,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
212
292
  if (!text || seen.has(key))
213
293
  continue;
214
294
  seen.add(key);
215
- onTranscript?.({ payload: { role: String(m.role).includes('user') ? 'user' : 'assistant', text, isFinal: true } });
295
+ emitLine(String(m.role).includes('user') ? 'user' : 'assistant', text, m?.id ? String(m.id) : undefined);
216
296
  }
217
297
  });
218
298
  client.addListener(ev.USER_SPEECH_STARTED, () => { setIsUserSpeaking(true); onUserSpeakStart?.(); });
@@ -224,6 +304,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
224
304
  setStreamURL(null);
225
305
  setIsUserSpeaking(false);
226
306
  updateStatus('disconnected');
307
+ finishConversation('disconnected');
227
308
  void endUsage();
228
309
  });
229
310
  const streams = await client.stream();
@@ -335,13 +416,13 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
335
416
  if (type === 'avatar.transcription' || type === 'response.audio_transcript.done') {
336
417
  const str = msg.text || msg.transcript || msg.payload?.text;
337
418
  if (str)
338
- onTranscript?.({ payload: { role: 'assistant', text: String(str).trim() } });
419
+ emitLine('assistant', String(str).trim());
339
420
  }
340
421
  else if (type === 'user.transcription' ||
341
422
  type === 'conversation.item.input_audio_transcription.completed') {
342
423
  const str = msg.text || msg.transcript || msg.payload?.text;
343
424
  if (str)
344
- onTranscript?.({ payload: { role: 'user', text: String(str).trim() } });
425
+ emitLine('user', String(str).trim());
345
426
  }
346
427
  // Order & checkout intents
347
428
  if (type === 'order.update') {
@@ -358,12 +439,17 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
358
439
  });
359
440
  // 4. Connection state transitions
360
441
  room.on(LiveKitRN.RoomEvent.Disconnected, () => {
361
- if (!mountedRef.current)
362
- return;
442
+ if (!mountedRef.current || roomRef.current !== room)
443
+ return; // stop() handles its own end
444
+ roomRef.current = null;
363
445
  setVideoTrack(null);
364
446
  setIsAvatarSpeaking(false);
365
447
  setIsUserSpeaking(false);
366
448
  updateStatus('disconnected');
449
+ // The stream dropped: close the conversation now, so it is not
450
+ // left open (and billed) until the server's 30-minute limit.
451
+ finishConversation('disconnected');
452
+ void endUsage();
367
453
  });
368
454
  room.on(LiveKitRN.RoomEvent.Reconnecting, () => {
369
455
  updateStatus('reconnecting');
@@ -423,6 +509,8 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
423
509
  setStreamURL(null);
424
510
  if (failedClient)
425
511
  await failedClient.stopStreaming().catch(() => undefined);
512
+ // Opened on the server but never got going: still hand over the record.
513
+ finishConversation('disconnected');
426
514
  if (!mountedRef.current)
427
515
  return;
428
516
  const msg = err?.message || 'Connection failed';
@@ -452,7 +540,8 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
452
540
  onError,
453
541
  ]);
454
542
  // Stop streaming session
455
- const stopStream = useCallback(async () => {
543
+ const stopStream = useCallback(async (reason = 'ended') => {
544
+ finishConversation(reason);
456
545
  const room = roomRef.current;
457
546
  roomRef.current = null;
458
547
  const streamClient = streamClientRef.current;
@@ -561,7 +650,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
561
650
  }
562
651
  return () => {
563
652
  mountedRef.current = false;
564
- void stopStream();
653
+ void stopStream('unmounted');
565
654
  };
566
655
  }, [autoStart, token, tokenLoading, tokenError]);
567
656
  // Handle token loading
@@ -584,9 +673,9 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
584
673
  // A plain object, not an array: VideoView spreads `style` into its
585
674
  // container ({ ...style }), and spreading an array loses every
586
675
  // size — the video got no height and stayed invisible.
587
- style: StyleSheet.flatten([styles.video, videoStyle]), objectFit: objectFit, mirror: mirror })) : (_jsx(View, { style: [styles.container, styles.center], children: status === 'connecting' || status === 'reconnecting' ? (_jsxs(_Fragment, { children: [_jsx(ActivityIndicator, { size: "large", color: "#8B5CF6" }), _jsx(Text, { style: styles.loadingText, children: status === 'connecting'
588
- ? 'Connecting to FacePe AI Avatar…'
589
- : 'Reconnecting stream…' })] })) : status === 'error' ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.errorTitle, children: "Connection Failed" }), _jsx(Text, { style: styles.errorText, children: errorMessage || 'Unable to connect to stream' }), _jsx(TouchableOpacity, { style: styles.retryButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.retryText, children: "Reconnect" }) })] })) : (_jsx(TouchableOpacity, { style: styles.startButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.startText, children: "Start Avatar Session" }) })) })), status === 'connected' && (isAvatarSpeaking || isUserSpeaking) && (_jsxs(View, { style: styles.speakerPill, children: [_jsx(View, { style: [
676
+ style: StyleSheet.flatten([styles.video, videoStyle]), objectFit: objectFit, mirror: mirror })) : (_jsxs(View, { style: [styles.container, styles.center, !!poster && (status === 'idle' || status === 'disconnected') && styles.overPoster], children: [poster && _jsx(Image, { source: { uri: poster }, style: StyleSheet.absoluteFill, resizeMode: "cover" }), poster && status !== 'idle' && status !== 'disconnected' && _jsx(View, { style: [StyleSheet.absoluteFill, styles.dim] }), status === 'connecting' || status === 'reconnecting' || status === 'connected' ? (
677
+ // connected: the stream is up and the first video frame is on its way.
678
+ _jsxs(_Fragment, { children: [_jsx(ActivityIndicator, { size: "large", color: "#8B5CF6" }), _jsx(Text, { style: styles.loadingText, children: status === 'reconnecting' ? 'Reconnecting stream…' : 'Connecting to FacePe AI Avatar…' })] })) : status === 'error' ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.errorTitle, children: "Connection Failed" }), _jsx(Text, { style: styles.errorText, children: errorMessage || 'Unable to connect to stream' }), _jsx(TouchableOpacity, { style: styles.retryButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.retryText, children: "Reconnect" }) })] })) : (_jsx(TouchableOpacity, { style: styles.startButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.startText, children: "Start Avatar Session" }) }))] })), status === 'connected' && (isAvatarSpeaking || isUserSpeaking) && (_jsxs(View, { style: styles.speakerPill, children: [_jsx(View, { style: [
590
679
  styles.speakerDot,
591
680
  isAvatarSpeaking ? styles.dotAvatar : styles.dotUser,
592
681
  ] }), _jsx(Text, { style: styles.speakerText, children: isAvatarSpeaking ? 'Avatar speaking' : 'Listening to you…' })] }))] }));
@@ -606,6 +695,14 @@ const styles = StyleSheet.create({
606
695
  width: '100%',
607
696
  height: '100%',
608
697
  },
698
+ // Over a portrait, keep the face clear: the button sits at the bottom.
699
+ overPoster: {
700
+ justifyContent: 'flex-end',
701
+ paddingBottom: 28,
702
+ },
703
+ dim: {
704
+ backgroundColor: 'rgba(15, 23, 42, 0.45)',
705
+ },
609
706
  center: {
610
707
  justifyContent: 'center',
611
708
  alignItems: 'center',
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './types';
2
2
  export * from './FacePeMobileProvider';
3
3
  export * from './FacePeMobileAvatar';
4
4
  export * from './useFacePeAvatar';
5
+ export { SDK_VERSION } from './version';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from './types';
2
2
  export * from './FacePeMobileProvider';
3
3
  export * from './FacePeMobileAvatar';
4
4
  export * from './useFacePeAvatar';
5
+ export { SDK_VERSION } from './version';
package/dist/types.d.ts CHANGED
@@ -11,6 +11,37 @@ export interface TranscriptPayload {
11
11
  text: string;
12
12
  isFinal?: boolean;
13
13
  }
14
+ /** One line of a finished conversation, with the time it was said (ISO 8601). */
15
+ export interface FacePeConversationLine {
16
+ role: 'user' | 'assistant';
17
+ text: string;
18
+ at: string;
19
+ }
20
+ /**
21
+ * Everything about one conversation, given once when it ends
22
+ * (`onConversationEnd`). It stays in your app: the SDK sends it nowhere — you
23
+ * decide whether to show, keep or send it.
24
+ */
25
+ export interface FacePeConversation {
26
+ /** The conversation's id (its FacePe usage record), or null if none was opened. */
27
+ conversationId: string | null;
28
+ avatar: {
29
+ id: string;
30
+ name: string;
31
+ };
32
+ /** The language requested for the conversation (ISO 639-1). */
33
+ language: string | null;
34
+ platform: 'android' | 'ios' | 'web';
35
+ sdkVersion: string;
36
+ /** When it started and ended (ISO 8601). */
37
+ startedAt: string;
38
+ endedAt: string;
39
+ durationSec: number;
40
+ /** `ended`: by `stop()`; `disconnected`: the stream dropped; `unmounted`: the component went away. */
41
+ endReason: 'ended' | 'disconnected' | 'unmounted';
42
+ /** Every line, in order (a line that grew while being spoken appears once, complete). */
43
+ transcript: FacePeConversationLine[];
44
+ }
14
45
  export interface OrderItem {
15
46
  name: string;
16
47
  qty: number;
@@ -69,6 +100,13 @@ export interface FacePeMobileAvatarProps {
69
100
  onCheckout?: () => void;
70
101
  /** Called when stream connection status changes */
71
102
  onStatusChange?: (status: FacePeConnectionStatus) => void;
103
+ /**
104
+ * Picture shown until the video starts (idle, connecting, errors). By default
105
+ * the avatar's own portrait from FacePe; pass your own URL, or null for none.
106
+ */
107
+ posterUrl?: string | null;
108
+ /** Called once when a conversation ends, with the whole conversation (stays in your app) */
109
+ onConversationEnd?: (conversation: FacePeConversation) => void;
72
110
  /** Called when an error occurs */
73
111
  onError?: (event: {
74
112
  code: string;
@@ -0,0 +1,2 @@
1
+ /** This package's version, reported in conversation records. Keep in step with package.json. */
2
+ export declare const SDK_VERSION = "1.3.1";
@@ -0,0 +1,2 @@
1
+ /** This package's version, reported in conversation records. Keep in step with package.json. */
2
+ export const SDK_VERSION = '1.3.1';
package/package.json CHANGED
@@ -1,61 +1,61 @@
1
- {
2
- "name": "@facepe/widget-mobile-app",
3
- "version": "1.2.0",
4
- "description": "FacePe AI 3D Video Avatar Native Live Streaming SDK for React Native (iOS & Android)",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "files": [
9
- "dist",
10
- "README.md"
11
- ],
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "keywords": [
16
- "facepe",
17
- "avatar",
18
- "ai-avatar",
19
- "livekit",
20
- "react-native",
21
- "ios",
22
- "android",
23
- "webrtc",
24
- "streaming",
25
- "digital-human"
26
- ],
27
- "author": "FacePe AI",
28
- "license": "MIT",
29
- "scripts": {
30
- "lint": "tsc --noEmit",
31
- "build": "tsc"
32
- },
33
- "peerDependencies": {
34
- "@livekit/react-native": ">=2.0.0",
35
- "@livekit/react-native-webrtc": ">=124.0.0",
36
- "livekit-client": ">=2.0.0",
37
- "react": ">=18.0.0",
38
- "react-native": ">=0.70.0",
39
- "@anam-ai/js-sdk": ">=4.27.0"
40
- },
41
- "peerDependenciesMeta": {
42
- "@livekit/react-native": {
43
- "optional": true
44
- },
45
- "@livekit/react-native-webrtc": {
46
- "optional": true
47
- },
48
- "livekit-client": {
49
- "optional": true
50
- },
51
- "@anam-ai/js-sdk": {
52
- "optional": true
53
- }
54
- },
55
- "devDependencies": {
56
- "@types/react": "^18.3.3",
57
- "react": "18.3.1",
58
- "react-native": "0.74.0",
59
- "typescript": "^5.4.5"
60
- }
61
- }
1
+ {
2
+ "name": "@facepe/widget-mobile-app",
3
+ "version": "1.3.1",
4
+ "description": "FacePe AI 3D Video Avatar Native Live Streaming SDK for React Native (iOS & Android)",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "keywords": [
16
+ "facepe",
17
+ "avatar",
18
+ "ai-avatar",
19
+ "livekit",
20
+ "react-native",
21
+ "ios",
22
+ "android",
23
+ "webrtc",
24
+ "streaming",
25
+ "digital-human"
26
+ ],
27
+ "author": "FacePe AI",
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "lint": "tsc --noEmit",
31
+ "build": "tsc"
32
+ },
33
+ "peerDependencies": {
34
+ "@livekit/react-native": ">=2.0.0",
35
+ "@livekit/react-native-webrtc": ">=124.0.0",
36
+ "livekit-client": ">=2.0.0",
37
+ "react": ">=18.0.0",
38
+ "react-native": ">=0.70.0",
39
+ "@anam-ai/js-sdk": ">=4.27.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@livekit/react-native": {
43
+ "optional": true
44
+ },
45
+ "@livekit/react-native-webrtc": {
46
+ "optional": true
47
+ },
48
+ "livekit-client": {
49
+ "optional": true
50
+ },
51
+ "@anam-ai/js-sdk": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "^18.3.3",
57
+ "react": "18.3.1",
58
+ "react-native": "0.74.0",
59
+ "typescript": "^5.4.5"
60
+ }
61
+ }