@facepe/widget-mobile-app 1.3.1 → 1.4.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
@@ -338,6 +338,25 @@ when the component unmounts.
338
338
  | `durationSec` | Length in seconds |
339
339
  | `endReason` | `ended` (the user or your code ended it), `disconnected` (the stream dropped), `unmounted` (the component went away) |
340
340
  | `transcript` | Every line in order: `{ role: "user" \| "assistant", text, at }` |
341
+ | `form` | For a form avatar: `{ title, complete, fields: [{ label, question, required, value }] }`; `null` otherwise |
342
+
343
+ For an avatar that **fills a form**, the record also carries the form as the
344
+ conversation filled it: each question with the customer's answer (`value` is
345
+ `null` for a question that was not answered). FacePe reads the answers from the
346
+ conversation when it ends, so the record arrives a few seconds after the call.
347
+ For other avatars `form` is `null`.
348
+
349
+ ```json
350
+ "form": {
351
+ "title": "Customer Intake Form",
352
+ "complete": true,
353
+ "fields": [
354
+ { "label": "Name", "question": "Could you please tell me your full name?", "required": true, "value": "Tony" },
355
+ { "label": "Phone", "question": "What is the best phone number to reach you?", "required": true, "value": "9659390715" },
356
+ { "label": "Email", "question": "May I have your email address?", "required": false, "value": "tony@gmail.com" }
357
+ ]
358
+ }
359
+ ```
341
360
 
342
361
  ---
343
362
 
@@ -58,6 +58,14 @@ function ensureLiveKitGlobals(LiveKitRN) {
58
58
  console.warn('[FacePeMobileAvatar] registerGlobals warning:', err);
59
59
  }
60
60
  }
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
+ });
61
69
  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) => {
62
70
  const { token, loading: tokenLoading, error: tokenError, apiBaseUrl, refreshToken } = useFacePeMobileContext();
63
71
  // The avatar's portrait, shown until its video arrives. Looked up from the
@@ -109,7 +117,8 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
109
117
  const onTranscriptRef = useRef(onTranscript);
110
118
  onTranscriptRef.current = onTranscript;
111
119
  // A transcript line: recorded for the conversation record, and passed on.
112
- const emitLine = (role, text, id) => {
120
+ const emitLine = (role, said, id) => {
121
+ const text = joinNumberParts(said);
113
122
  const conv = conversationRef.current;
114
123
  if (conv && text) {
115
124
  const same = id ? conv.lines.find((l) => l.id === id) : undefined;
@@ -120,30 +129,51 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
120
129
  }
121
130
  onTranscriptRef.current?.({ payload: { role, text, isFinal: true } });
122
131
  };
123
- // Hands the whole conversation to the app, once.
132
+ // Hands the whole conversation to the app, once. For a form avatar it first
133
+ // asks FacePe for the form as the conversation filled it (a few seconds).
124
134
  const finishConversation = (endReason) => {
125
135
  const conv = conversationRef.current;
126
136
  conversationRef.current = null;
127
137
  if (!conv)
128
138
  return;
129
139
  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
- }
140
+ const transcript = conv.lines.map(({ role, text, at }) => ({ role, text, at }));
141
+ void (async () => {
142
+ let form = null;
143
+ const token = tokenRef.current;
144
+ if (conv.conversationId && transcript.length > 0 && token) {
145
+ try {
146
+ const res = await fetch(`${apiBaseUrl.replace(/\/+$/, '')}/avatar-sessions/${encodeURIComponent(conv.conversationId)}/form`, {
147
+ method: 'POST',
148
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
149
+ body: JSON.stringify({ transcript: transcript.map(({ role, text }) => ({ role, text })) }),
150
+ });
151
+ if (res.ok)
152
+ form = (await res.json())?.data?.form ?? null;
153
+ }
154
+ catch {
155
+ /* offline: the record goes without the form */
156
+ }
157
+ }
158
+ try {
159
+ onConversationEndRef.current?.({
160
+ conversationId: conv.conversationId,
161
+ avatar: conv.avatar,
162
+ language: conv.language,
163
+ platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'web',
164
+ sdkVersion: SDK_VERSION,
165
+ startedAt: conv.startedAt.toISOString(),
166
+ endedAt: endedAt.toISOString(),
167
+ durationSec: Math.max(0, Math.round((endedAt.getTime() - conv.startedAt.getTime()) / 1000)),
168
+ endReason,
169
+ transcript,
170
+ form,
171
+ });
172
+ }
173
+ catch (err) {
174
+ console.warn('[FacePeMobileAvatar] onConversationEnd error:', err);
175
+ }
176
+ })();
147
177
  };
148
178
  const mountedRef = useRef(true);
149
179
  const isConnectingRef = useRef(false);
package/dist/types.d.ts CHANGED
@@ -22,6 +22,20 @@ 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
+ }
25
39
  export interface FacePeConversation {
26
40
  /** The conversation's id (its FacePe usage record), or null if none was opened. */
27
41
  conversationId: string | null;
@@ -41,6 +55,12 @@ export interface FacePeConversation {
41
55
  endReason: 'ended' | 'disconnected' | 'unmounted';
42
56
  /** Every line, in order (a line that grew while being spoken appears once, complete). */
43
57
  transcript: FacePeConversationLine[];
58
+ /**
59
+ * For an avatar that fills a form: its questions with the customer's answers,
60
+ * read from the conversation. Null for other avatars, or when the answers
61
+ * could not be read.
62
+ */
63
+ form: FacePeFilledForm | null;
44
64
  }
45
65
  export interface OrderItem {
46
66
  name: 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.4.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.4.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.4.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",