@facepe/widget-mobile-app 1.3.1 → 1.5.0

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
@@ -246,7 +246,8 @@ const styles = StyleSheet.create({
246
246
 
247
247
  | Prop | Type | Default | Description |
248
248
  | :--- | :--- | :--- | :--- |
249
- | `avatarKey` | `string` | *(optional)* | Specific avatar key or ID assigned to your company. |
249
+ | `agentId` | `string` | first agent assigned to the user | The agent to talk to — its id from the FacePe dashboard (SDK page) or `GET /agents`. |
250
+ | `avatarKey` | `string` | *(deprecated)* | The same as `agentId`, for apps written before agents. |
250
251
  | `language` | `string` | `'en'` | Spoken language code (`'en'`, `'hi'`, `'es'`, `'ta'`, `'te'`, etc.). |
251
252
  | `style` | `ViewStyle` | `undefined` | Style applied to avatar container. |
252
253
  | `videoStyle` | `ViewStyle` | `undefined` | Style applied to native video view. |
@@ -312,7 +313,7 @@ when the component unmounts.
312
313
  ```json
313
314
  {
314
315
  "conversationId": "cm_usage_123",
315
- "avatar": { "id": "cm_avatar_456", "name": "Dharun" },
316
+ "agent": { "id": "cm_agent_456", "name": "Front Desk" },
316
317
  "language": "en",
317
318
  "platform": "android",
318
319
  "sdkVersion": "1.3.0",
@@ -330,7 +331,7 @@ when the component unmounts.
330
331
  | Field | Meaning |
331
332
  |---|---|
332
333
  | `conversationId` | The conversation's FacePe id (its usage record) |
333
- | `avatar` | `{ id, name }` of the avatar |
334
+ | `agent` | `{ id, name }` of the agent that held the conversation (`avatar` is the same, kept for older apps) |
334
335
  | `language` | The language requested for the conversation |
335
336
  | `platform` | `web`, `android` or `ios` |
336
337
  | `sdkVersion` | The SDK version that recorded it |
@@ -338,6 +339,27 @@ when the component unmounts.
338
339
  | `durationSec` | Length in seconds |
339
340
  | `endReason` | `ended` (the user or your code ended it), `disconnected` (the stream dropped), `unmounted` (the component went away) |
340
341
  | `transcript` | Every line in order: `{ role: "user" \| "assistant", text, at }` |
342
+ | `form` | For a form avatar: `{ title, complete, fields: [{ label, question, required, value }], documents, uploadWhere }` — `documents` are what the avatar asked the customer to upload at the end; `null` for other avatars |
343
+
344
+ For an avatar that **fills a form**, the record also carries the form as the
345
+ conversation filled it: each question with the customer's answer (`value` is
346
+ `null` for a question that was not answered). FacePe reads the answers from the
347
+ conversation when it ends, so the record arrives a few seconds after the call.
348
+ For other avatars `form` is `null`.
349
+
350
+ ```json
351
+ "form": {
352
+ "title": "Customer Intake Form",
353
+ "complete": true,
354
+ "fields": [
355
+ { "label": "Name", "question": "Could you please tell me your full name?", "required": true, "value": "Tony" },
356
+ { "label": "Phone", "question": "What is the best phone number to reach you?", "required": true, "value": "9659390715" },
357
+ { "label": "Email", "question": "May I have your email address?", "required": false, "value": "tony@gmail.com" }
358
+ ],
359
+ "documents": ["ID proof (Aadhaar / PAN)", "Address proof"],
360
+ "uploadWhere": "on our website"
361
+ }
362
+ ```
341
363
 
342
364
  ---
343
365
 
@@ -58,8 +58,26 @@ function ensureLiveKitGlobals(LiveKitRN) {
58
58
  console.warn('[FacePeMobileAvatar] registerGlobals warning:', err);
59
59
  }
60
60
  }
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
+ * Speech recognition writes a spoken phone number in parts ("9659-3907-1526").
63
+ * Long numbers are shown as plain digits; short ones ("2-3") are left as said.
64
+ */
65
+ const joinNumberParts = (text) => text.replace(/\d+(?:[ \t]*[-\u2010\u2011\u2013][ \t]*\d+)+/g, (m) => {
66
+ const digits = m.replace(/\D/g, '');
67
+ return digits.length >= 7 ? digits : m;
68
+ });
69
+ /**
70
+ * Calls the gateway's agent route; a FacePe server from before agents has
71
+ * only the avatar route (404), so that one is tried next.
72
+ */
73
+ async function gatewayFetch(base, agentPath, avatarPath, init) {
74
+ const res = await fetch(`${base}${agentPath}`, init);
75
+ return res.status === 404 ? fetch(`${base}${avatarPath}`, init) : res;
76
+ }
77
+ export const FacePeMobileAvatar = forwardRef(({ agentId, 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) => {
62
78
  const { token, loading: tokenLoading, error: tokenError, apiBaseUrl, refreshToken } = useFacePeMobileContext();
79
+ // The agent to talk to (`avatarKey` is its name from before agents).
80
+ const chosenAgent = agentId ?? avatarKey;
63
81
  // The avatar's portrait, shown until its video arrives. Looked up from the
64
82
  // avatars this token may use, unless the app gave one (or null for none).
65
83
  const [autoPoster, setAutoPoster] = useState(null);
@@ -69,13 +87,14 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
69
87
  let cancelled = false;
70
88
  void (async () => {
71
89
  try {
72
- const res = await fetch(`${apiBaseUrl.replace(/\/+$/, '')}/avatars`, {
90
+ const res = await gatewayFetch(apiBaseUrl.replace(/\/+$/, ''), '/agents', '/avatars', {
73
91
  headers: { Authorization: `Bearer ${token}` },
74
92
  });
75
93
  if (!res.ok)
76
94
  return;
77
- const avatars = (await res.json())?.data?.avatars ?? [];
78
- const avatar = avatarKey ? avatars.find((a) => a.id === avatarKey) : avatars[0];
95
+ const data = (await res.json())?.data;
96
+ const agents = data?.agents ?? data?.avatars ?? [];
97
+ const avatar = chosenAgent ? agents.find((a) => a.id === chosenAgent) : agents[0];
79
98
  if (!cancelled)
80
99
  setAutoPoster(avatar?.previewUrl ?? null);
81
100
  }
@@ -86,7 +105,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
86
105
  return () => {
87
106
  cancelled = true;
88
107
  };
89
- }, [token, apiBaseUrl, avatarKey, posterUrl]);
108
+ }, [token, apiBaseUrl, chosenAgent, posterUrl]);
90
109
  const poster = posterUrl !== undefined ? posterUrl : autoPoster;
91
110
  const [status, setStatus] = useState('idle');
92
111
  const [videoTrack, setVideoTrack] = useState(null);
@@ -109,7 +128,8 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
109
128
  const onTranscriptRef = useRef(onTranscript);
110
129
  onTranscriptRef.current = onTranscript;
111
130
  // A transcript line: recorded for the conversation record, and passed on.
112
- const emitLine = (role, text, id) => {
131
+ const emitLine = (role, said, id) => {
132
+ const text = joinNumberParts(said);
113
133
  const conv = conversationRef.current;
114
134
  if (conv && text) {
115
135
  const same = id ? conv.lines.find((l) => l.id === id) : undefined;
@@ -120,30 +140,53 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
120
140
  }
121
141
  onTranscriptRef.current?.({ payload: { role, text, isFinal: true } });
122
142
  };
123
- // Hands the whole conversation to the app, once.
143
+ // Hands the whole conversation to the app, once. For a form avatar it first
144
+ // asks FacePe for the form as the conversation filled it (a few seconds).
124
145
  const finishConversation = (endReason) => {
125
146
  const conv = conversationRef.current;
126
147
  conversationRef.current = null;
127
148
  if (!conv)
128
149
  return;
129
150
  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
- }
151
+ const transcript = conv.lines.map(({ role, text, at }) => ({ role, text, at }));
152
+ void (async () => {
153
+ let form = null;
154
+ const token = tokenRef.current;
155
+ if (conv.conversationId && transcript.length > 0 && token) {
156
+ try {
157
+ const id = encodeURIComponent(conv.conversationId);
158
+ const res = await gatewayFetch(apiBaseUrl.replace(/\/+$/, ''), `/agent-sessions/${id}/form`, `/avatar-sessions/${id}/form`, {
159
+ method: 'POST',
160
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
161
+ body: JSON.stringify({ transcript: transcript.map(({ role, text }) => ({ role, text })) }),
162
+ });
163
+ if (res.ok)
164
+ form = (await res.json())?.data?.form ?? null;
165
+ }
166
+ catch {
167
+ /* offline: the record goes without the form */
168
+ }
169
+ }
170
+ try {
171
+ onConversationEndRef.current?.({
172
+ conversationId: conv.conversationId,
173
+ agent: conv.agent,
174
+ avatar: conv.agent,
175
+ language: conv.language,
176
+ platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'web',
177
+ sdkVersion: SDK_VERSION,
178
+ startedAt: conv.startedAt.toISOString(),
179
+ endedAt: endedAt.toISOString(),
180
+ durationSec: Math.max(0, Math.round((endedAt.getTime() - conv.startedAt.getTime()) / 1000)),
181
+ endReason,
182
+ transcript,
183
+ form,
184
+ });
185
+ }
186
+ catch (err) {
187
+ console.warn('[FacePeMobileAvatar] onConversationEnd error:', err);
188
+ }
189
+ })();
147
190
  };
148
191
  const mountedRef = useRef(true);
149
192
  const isConnectingRef = useRef(false);
@@ -189,7 +232,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
189
232
  return;
190
233
  try {
191
234
  const cleanBase = apiBaseUrl.replace(/\/+$/, '');
192
- await fetch(`${cleanBase}/avatar-sessions/${uId}/end`, {
235
+ await gatewayFetch(cleanBase, `/agent-sessions/${uId}/end`, `/avatar-sessions/${uId}/end`, {
193
236
  method: 'POST',
194
237
  headers: {
195
238
  'Content-Type': 'application/json',
@@ -226,29 +269,30 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
226
269
  throw new Error('Authentication token not available. Please verify your token provider.');
227
270
  }
228
271
  const cleanBase = apiBaseUrl.replace(/\/+$/, '');
229
- // Resolve target avatar ID
230
- let targetAvatarId = avatarKey;
231
- if (!targetAvatarId) {
232
- // No avatar given: use the first one assigned to this token's user.
233
- const listRes = await fetch(`${cleanBase}/avatars`, {
272
+ // The agent to talk to
273
+ let targetAgentId = chosenAgent;
274
+ if (!targetAgentId) {
275
+ // No agent given: use the first one assigned to this token's user.
276
+ const listRes = await gatewayFetch(cleanBase, '/agents', '/avatars', {
234
277
  method: 'GET',
235
278
  headers: {
236
279
  Authorization: `Bearer ${currentToken}`,
237
280
  },
238
281
  });
239
282
  if (listRes.ok) {
240
- const listData = await listRes.json();
241
- const avatars = listData?.data?.avatars || [];
242
- if (avatars.length > 0) {
243
- targetAvatarId = avatars[0].id;
283
+ const listData = (await listRes.json())?.data;
284
+ const agents = listData?.agents || listData?.avatars || [];
285
+ if (agents.length > 0) {
286
+ targetAgentId = agents[0].id;
244
287
  }
245
288
  }
246
289
  }
247
- if (!targetAvatarId) {
248
- throw new Error('No Avatar configured or assigned to this token.');
290
+ if (!targetAgentId) {
291
+ throw new Error('No agent is assigned to these keys. Create one on the FacePe dashboard (Agents).');
249
292
  }
250
293
  // Request live streaming session from FacePe SDK backend
251
- const sessionResp = await fetch(`${cleanBase}/avatars/${targetAvatarId}/sessions`, {
294
+ const agentPath = encodeURIComponent(targetAgentId);
295
+ const sessionResp = await gatewayFetch(cleanBase, `/agents/${agentPath}/sessions`, `/avatars/${agentPath}/sessions`, {
252
296
  method: 'POST',
253
297
  headers: {
254
298
  'Content-Type': 'application/json',
@@ -258,7 +302,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
258
302
  });
259
303
  if (!sessionResp.ok) {
260
304
  const errBody = await sessionResp.json().catch(() => ({}));
261
- const errMsg = errBody?.error?.message || errBody?.message || 'Failed to initialize avatar session';
305
+ const errMsg = errBody?.error?.message || errBody?.message || 'Failed to start the conversation';
262
306
  throw new Error(errMsg);
263
307
  }
264
308
  const sessionPayload = await sessionResp.json();
@@ -266,7 +310,10 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
266
310
  usageIdRef.current = sessionInfo?.usageId || null;
267
311
  conversationRef.current = {
268
312
  conversationId: usageIdRef.current,
269
- avatar: { id: String(sessionInfo?.avatar?.id ?? targetAvatarId), name: String(sessionInfo?.avatar?.name ?? '') },
313
+ agent: (() => {
314
+ const who = sessionInfo?.agent ?? sessionInfo?.avatar;
315
+ return { id: String(who?.id ?? targetAgentId), name: String(who?.name ?? '') };
316
+ })(),
270
317
  language: language ?? null,
271
318
  startedAt: new Date(),
272
319
  lines: [],
@@ -524,7 +571,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
524
571
  }
525
572
  }, [
526
573
  hasLiveKit,
527
- avatarKey,
574
+ chosenAgent,
528
575
  language,
529
576
  enableMic,
530
577
  apiBaseUrl,
package/dist/types.d.ts CHANGED
@@ -22,9 +22,33 @@ export interface FacePeConversationLine {
22
22
  * (`onConversationEnd`). It stays in your app: the SDK sends it nowhere — you
23
23
  * decide whether to show, keep or send it.
24
24
  */
25
+ /** One question of a form avatar, with the customer's answer (null: not answered). */
26
+ export interface FacePeFormAnswer {
27
+ label: string;
28
+ question: string;
29
+ required: boolean;
30
+ value: string | null;
31
+ }
32
+ /** A form avatar's form, filled from the conversation. */
33
+ export interface FacePeFilledForm {
34
+ title: string;
35
+ /** Every required question has an answer. */
36
+ complete: boolean;
37
+ fields: FacePeFormAnswer[];
38
+ /** Documents the customer was asked to upload once the form was done (may be empty). */
39
+ documents: string[];
40
+ /** Where to upload them, as the company wrote it (empty when not set). */
41
+ uploadWhere: string;
42
+ }
25
43
  export interface FacePeConversation {
26
44
  /** The conversation's id (its FacePe usage record), or null if none was opened. */
27
45
  conversationId: string | null;
46
+ /** The agent that held the conversation. */
47
+ agent: {
48
+ id: string;
49
+ name: string;
50
+ };
51
+ /** @deprecated The same as `agent` (kept for apps from before agents). */
28
52
  avatar: {
29
53
  id: string;
30
54
  name: string;
@@ -41,6 +65,12 @@ export interface FacePeConversation {
41
65
  endReason: 'ended' | 'disconnected' | 'unmounted';
42
66
  /** Every line, in order (a line that grew while being spoken appears once, complete). */
43
67
  transcript: FacePeConversationLine[];
68
+ /**
69
+ * For an avatar that fills a form: its questions with the customer's answers,
70
+ * read from the conversation. Null for other avatars, or when the answers
71
+ * could not be read.
72
+ */
73
+ form: FacePeFilledForm | null;
44
74
  }
45
75
  export interface OrderItem {
46
76
  name: string;
@@ -64,7 +94,12 @@ export interface TokenProviderParams {
64
94
  }
65
95
  export type TokenProvider = (params: TokenProviderParams) => Promise<string>;
66
96
  export interface FacePeMobileAvatarProps {
67
- /** Specific avatar identifier or key override */
97
+ /**
98
+ * The agent to talk to (its id, from the FacePe dashboard's SDK page or
99
+ * GET /agents). Without one, the first agent assigned to the keys' user.
100
+ */
101
+ agentId?: string;
102
+ /** @deprecated Use `agentId` (the same id; kept for apps from before agents). */
68
103
  avatarKey?: string;
69
104
  /** Language to converse in ('en', 'es', 'hi', etc.) */
70
105
  language?: string;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** This package's version, reported in conversation records. Keep in step with package.json. */
2
- export declare const SDK_VERSION = "1.3.1";
2
+ export declare const SDK_VERSION = "1.5.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** This package's version, reported in conversation records. Keep in step with package.json. */
2
- export const SDK_VERSION = '1.3.1';
2
+ export const SDK_VERSION = '1.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@facepe/widget-mobile-app",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "FacePe AI 3D Video Avatar Native Live Streaming SDK for React Native (iOS & Android)",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",