@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 +25 -3
- package/dist/FacePeMobileAvatar.js +87 -40
- package/dist/types.d.ts +36 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -246,7 +246,8 @@ const styles = StyleSheet.create({
|
|
|
246
246
|
|
|
247
247
|
| Prop | Type | Default | Description |
|
|
248
248
|
| :--- | :--- | :--- | :--- |
|
|
249
|
-
| `
|
|
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
|
-
"
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
|
|
78
|
-
const
|
|
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,
|
|
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,
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
-
//
|
|
230
|
-
let
|
|
231
|
-
if (!
|
|
232
|
-
// No
|
|
233
|
-
const listRes = await
|
|
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
|
|
242
|
-
if (
|
|
243
|
-
|
|
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 (!
|
|
248
|
-
throw new Error('No
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
2
|
+
export const SDK_VERSION = '1.5.0';
|
package/package.json
CHANGED