agora-appbuilder-core 4.1.0-beta-3 → 4.1.0-beta-5

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 (24) hide show
  1. package/package.json +1 -1
  2. package/template/customization-api/customize.tsx +11 -0
  3. package/template/src/ai-agent/assets/join-call.png +0 -0
  4. package/template/src/ai-agent/assets/leave-call.png +0 -0
  5. package/template/src/ai-agent/components/AgentControls/AgentConnectionWrapper.tsx +367 -0
  6. package/template/src/ai-agent/components/AgentControls/AgentContext.tsx +7 -0
  7. package/template/src/ai-agent/components/AgentControls/index.tsx +44 -258
  8. package/template/src/ai-agent/components/Bottombar.tsx +3 -5
  9. package/template/src/ai-agent/components/CustomChatPanel.tsx +1 -115
  10. package/template/src/ai-agent/components/CustomCreate.tsx +1 -1
  11. package/template/src/ai-agent/components/CustomSettingsPanel.tsx +43 -5
  12. package/template/src/ai-agent/components/SelectAiAgent.tsx +15 -12
  13. package/template/src/ai-agent/components/UserPrompt.tsx +75 -0
  14. package/template/src/ai-agent/components/agent-chat-panel/agent-chat-ui.tsx +13 -13
  15. package/template/src/ai-agent/components/mobile/Bottombar.tsx +55 -39
  16. package/template/src/ai-agent/components/mobile/Topbar.tsx +1 -2
  17. package/template/src/ai-agent/components/utils.ts +17 -0
  18. package/template/src/ai-agent/index.tsx +18 -12
  19. package/template/src/auth/AuthProvider.tsx +19 -0
  20. package/template/src/components/room-info/useRoomInfo.tsx +16 -4
  21. package/template/src/utils/useJoinRoom.ts +36 -8
  22. package/template/src/ai-agent/components/AgentControls/LeaveCall.png +0 -0
  23. package/template/src/ai-agent/components/AgentControls/Vector.svg +0 -3
  24. package/template/src/ai-agent/components/icons.tsx +0 -227
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agora-appbuilder-core",
3
- "version": "4.1.0-beta-3",
3
+ "version": "4.1.0-beta-5",
4
4
  "description": "React Native template for RTE app builder",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -177,6 +177,17 @@ const mergeCustomization = (
177
177
  );
178
178
  }
179
179
 
180
+ if (externalConfig?.components?.videoCall?.wrapper) {
181
+ const AiAgentVideoCallWrapper = aiAgentConfig.components.videoCall.wrapper;
182
+ const ExternalVideoCallWrapper =
183
+ externalConfig.components.videoCall.wrapper;
184
+ mergedData.components.videoCall.wrapper = props => (
185
+ <AiAgentVideoCallWrapper>
186
+ <ExternalVideoCallWrapper>{props.children}</ExternalVideoCallWrapper>
187
+ </AiAgentVideoCallWrapper>
188
+ );
189
+ }
190
+
180
191
  //override the i18n
181
192
  if (externalConfig?.i18n && externalConfig?.i18n?.length) {
182
193
  mergedData.i18n = externalConfig.i18n;
@@ -0,0 +1,367 @@
1
+ import {
2
+ UidType,
3
+ useContent,
4
+ useRoomInfo,
5
+ Toast,
6
+ useRtc,
7
+ } from 'customization-api';
8
+ import React, {createContext, useContext, useEffect} from 'react';
9
+ import {AgentContext} from './AgentContext';
10
+ import {AgentState} from './const';
11
+ import StorageContext from '../../../components/StorageContext';
12
+
13
+ export interface AgentContextInterface {
14
+ toggleAgentConnection: (forceStop?: boolean) => Promise<boolean>;
15
+ }
16
+
17
+ export const AgentConnectionContext = createContext<AgentContextInterface>({
18
+ toggleAgentConnection: () => {
19
+ return Promise.resolve(false);
20
+ },
21
+ });
22
+
23
+ export const AgentConnectionProvider: React.FC<{children: React.ReactNode}> = ({
24
+ children,
25
+ }) => {
26
+ const {activeUids: users} = useContent();
27
+ const {
28
+ agentUID,
29
+ agentConnectionState,
30
+ setAgentConnectionState,
31
+ agentId,
32
+ setAgentUID,
33
+ prompt,
34
+ isSubscribedForStreams,
35
+ setIsSubscribedForStreams,
36
+ addChatItem,
37
+ } = useContext(AgentContext);
38
+ const {
39
+ data: {channel: channel_name, uid: localUid, agents},
40
+ } = useRoomInfo();
41
+ const {store} = useContext(StorageContext);
42
+
43
+ const {RtcEngineUnsafe} = useRtc();
44
+
45
+ const messageCache = {};
46
+ const TIMEOUT_MS = 5000; // Timeout for incomplete messages
47
+
48
+ React.useEffect(() => {
49
+ if (!isSubscribedForStreams) {
50
+ RtcEngineUnsafe.addListener(
51
+ 'onStreamMessage',
52
+ handleStreamMessageCallback,
53
+ );
54
+ setIsSubscribedForStreams(true);
55
+ }
56
+ }, []);
57
+
58
+ const handleStreamMessageCallback = (...args) => {
59
+ console.log('rec', args);
60
+ parseData(args[1]);
61
+ };
62
+
63
+ const parseData = data => {
64
+ let decoder = new TextDecoder('utf-8');
65
+ let decodedMessage = decoder.decode(data);
66
+ console.log('[test] textstream raw data', decodedMessage);
67
+ handleChunk(decodedMessage);
68
+ };
69
+ // Function to process received chunk via event emitter
70
+ const handleChunk = (formattedChunk: string) => {
71
+ try {
72
+ // Split the chunk by the delimiter "|"
73
+ const [message_id, partIndexStr, totalPartsStr, content] =
74
+ formattedChunk.split('|');
75
+
76
+ const part_index = parseInt(partIndexStr, 10);
77
+ const total_parts =
78
+ totalPartsStr === '???' ? -1 : parseInt(totalPartsStr, 10); // -1 means total parts unknown
79
+
80
+ // Ensure total_parts is known before processing further
81
+ if (total_parts === -1) {
82
+ console.warn(
83
+ `Total parts for message ${message_id} unknown, waiting for further parts.`,
84
+ );
85
+ return;
86
+ }
87
+
88
+ const chunkData = {
89
+ message_id,
90
+ part_index,
91
+ total_parts,
92
+ content,
93
+ };
94
+
95
+ // Check if we already have an entry for this message
96
+ if (!messageCache[message_id]) {
97
+ messageCache[message_id] = [];
98
+ // Set a timeout to discard incomplete messages
99
+ setTimeout(() => {
100
+ if (messageCache[message_id]?.length !== total_parts) {
101
+ console.warn(`Incomplete message with ID ${message_id} discarded`);
102
+ delete messageCache[message_id]; // Discard incomplete message
103
+ }
104
+ }, TIMEOUT_MS);
105
+ }
106
+
107
+ // Cache this chunk by message_id
108
+ messageCache[message_id].push(chunkData);
109
+
110
+ // If all parts are received, reconstruct the message
111
+ if (messageCache[message_id].length === total_parts) {
112
+ const completeMessage = reconstructMessage(messageCache[message_id]);
113
+ const data = atob(completeMessage);
114
+ const {stream_id, is_final, text, text_ts} = JSON.parse(data);
115
+ /** Data type of above object
116
+ * stream_id: number
117
+ * is_final: boolean
118
+ * text: string
119
+ * text_ts: number
120
+ */
121
+ const textItem = {
122
+ id: message_id,
123
+ uid: stream_id,
124
+ time: text_ts,
125
+ dataType: 'transcribe',
126
+ text: text,
127
+ isFinal: is_final,
128
+ isSelf: stream_id === 0 ? false : true,
129
+ };
130
+
131
+ if (text.trim().length > 0) {
132
+ //this.emit("textChanged", textItem);
133
+ console.warn('emit textChanged: ', textItem);
134
+ addChatItem(textItem);
135
+ }
136
+
137
+ // Clean up the cache
138
+ delete messageCache[message_id];
139
+ }
140
+ } catch (error) {
141
+ console.error('Error processing chunk:', error);
142
+ }
143
+ };
144
+
145
+ const reconstructMessage = chunks => {
146
+ // Sort chunks by their part index
147
+ chunks.sort((a, b) => a.part_index - b.part_index);
148
+
149
+ // Concatenate all chunks to form the full message
150
+ return chunks.map(chunk => chunk.content).join('');
151
+ };
152
+
153
+ useEffect(() => {
154
+ console.log('debugging users agent contrl', {users});
155
+ // welcome agent
156
+ const aiAgentUID = users.filter(item => item === agentUID);
157
+
158
+ if (
159
+ aiAgentUID.length &&
160
+ agentConnectionState === AgentState.AWAITING_JOIN
161
+ ) {
162
+ setAgentConnectionState(AgentState.AGENT_CONNECTED);
163
+
164
+ Toast.show({
165
+ leadingIconName: 'tick-fill',
166
+ type: 'success',
167
+ text1: 'Say Hi!!',
168
+ text2: null,
169
+ visibilityTime: 3000,
170
+ primaryBtn: null,
171
+ secondaryBtn: null,
172
+ leadingIcon: null,
173
+ });
174
+ }
175
+ // when agent leaves, show left toast, and set agent to not connected state
176
+ if (
177
+ !aiAgentUID.length &&
178
+ agentConnectionState === AgentState.AWAITING_LEAVE
179
+ ) {
180
+ setAgentConnectionState(AgentState.NOT_CONNECTED);
181
+ }
182
+ }, [users, agentUID]);
183
+
184
+ const handleConnectionToggle = async (forceStop: boolean = false) => {
185
+ try {
186
+ // connect to agent when agent is in not connected state or when earlier connect failed
187
+ if (
188
+ agentConnectionState === AgentState.NOT_CONNECTED ||
189
+ agentConnectionState === AgentState.AGENT_REQUEST_FAILED ||
190
+ agentConnectionState === AgentState.AWAITING_LEAVE
191
+ ) {
192
+ try {
193
+ setAgentConnectionState(AgentState.REQUEST_SENT);
194
+ const data = await connectToAIAgent(
195
+ 'start',
196
+ channel_name,
197
+ localUid,
198
+ store.token,
199
+ {
200
+ agent_id: agentId,
201
+ prompt: prompt,
202
+ voice: agents.find(a => a.id === agentId)?.config?.tts?.params
203
+ ?.voice_name,
204
+ },
205
+ );
206
+ // console.log("response X-Client-ID", newClientId, typeof newClientId)
207
+ // @ts-ignore
208
+ const {agent_uid = null} = data;
209
+
210
+ //setClientId(agent_id);
211
+ setAgentUID(agent_uid);
212
+
213
+ setAgentConnectionState(AgentState.AWAITING_JOIN);
214
+
215
+ Toast.show({
216
+ leadingIconName: 'tick-fill',
217
+ type: 'success',
218
+ text1: 'Agent requested to join',
219
+ text2: null,
220
+ visibilityTime: 3000,
221
+ primaryBtn: null,
222
+ secondaryBtn: null,
223
+ leadingIcon: null,
224
+ });
225
+ return Promise.resolve(true);
226
+ } catch (agentConnectError) {
227
+ setAgentConnectionState(AgentState.AGENT_REQUEST_FAILED);
228
+
229
+ if (agentConnectError.toString().indexOf('401') !== -1) {
230
+ Toast.show({
231
+ leadingIconName: 'alert',
232
+ type: 'error',
233
+ text1: 'Your session is expired. Please sign in to join call.',
234
+ text2: null,
235
+ visibilityTime: 5000,
236
+ primaryBtn: null,
237
+ secondaryBtn: null,
238
+ leadingIcon: null,
239
+ });
240
+ } else {
241
+ Toast.show({
242
+ leadingIconName: 'alert',
243
+ type: 'error',
244
+ text1: 'Uh oh! Agent failed to connect',
245
+ text2: null,
246
+ visibilityTime: 5000,
247
+ primaryBtn: null,
248
+ secondaryBtn: null,
249
+ leadingIcon: null,
250
+ });
251
+ }
252
+
253
+ throw agentConnectError;
254
+ }
255
+ }
256
+ // disconnect agent with agent is already connected or when earlier disconnect failed
257
+ if (
258
+ forceStop === true ||
259
+ agentConnectionState === AgentState.AGENT_CONNECTED ||
260
+ agentConnectionState === AgentState.AGENT_DISCONNECT_FAILED
261
+ ) {
262
+ try {
263
+ setAgentConnectionState(AgentState.AGENT_DISCONNECT_REQUEST);
264
+ await connectToAIAgent('stop', channel_name, localUid, store.token, {
265
+ agent_id: agentId,
266
+ });
267
+ setAgentConnectionState(AgentState.AWAITING_LEAVE);
268
+ if (!forceStop) {
269
+ Toast.show({
270
+ leadingIconName: 'tick-fill',
271
+ type: 'success',
272
+ text1: 'Agent disconnected',
273
+ text2: null,
274
+ visibilityTime: 3000,
275
+ primaryBtn: null,
276
+ secondaryBtn: null,
277
+ leadingIcon: null,
278
+ });
279
+ }
280
+ return Promise.resolve(true);
281
+ } catch (agentDisconnectError) {
282
+ setAgentConnectionState(AgentState.AGENT_DISCONNECT_FAILED);
283
+
284
+ Toast.show({
285
+ leadingIconName: 'alert',
286
+ type: 'error',
287
+ text1: 'Uh oh! Agent failed to disconnect',
288
+ text2: null,
289
+ visibilityTime: 5000,
290
+ primaryBtn: null,
291
+ secondaryBtn: null,
292
+ leadingIcon: null,
293
+ });
294
+
295
+ throw agentDisconnectError;
296
+ }
297
+ }
298
+ } catch (error) {
299
+ console.log(`Agent failed to connect/disconnect - ${error}`);
300
+ }
301
+ };
302
+
303
+ const value = {
304
+ toggleAgentConnection: handleConnectionToggle,
305
+ };
306
+
307
+ return (
308
+ <AgentConnectionContext.Provider value={value}>
309
+ {children}
310
+ </AgentConnectionContext.Provider>
311
+ );
312
+ };
313
+
314
+ export const connectToAIAgent = async (
315
+ agentAction: 'start' | 'stop',
316
+ channel_name: string,
317
+ localUid: UidType,
318
+ agentAuthToken: string,
319
+ data?: {agent_id: string; prompt?: string; voice?: string},
320
+ ): Promise<{}> => {
321
+ // const apiUrl = '/api/proxy';
322
+ const apiUrl = $config.BACKEND_ENDPOINT + '/v1/convoai';
323
+ const requestBody = {
324
+ channel_name: channel_name,
325
+ uid: localUid, // user uid // localUid or 0
326
+ };
327
+
328
+ if (data && data?.agent_id) {
329
+ requestBody['ai_agent_id'] = data.agent_id;
330
+ }
331
+ if (data && data?.voice) {
332
+ requestBody['voice'] = data.voice;
333
+ }
334
+ if (data && data?.prompt) {
335
+ requestBody['prompt'] = data.prompt;
336
+ }
337
+
338
+ const headers: HeadersInit = {
339
+ 'Content-Type': 'application/json',
340
+ Authorization: `Bearer ${agentAuthToken}`,
341
+ };
342
+
343
+ try {
344
+ const response = await fetch(`${apiUrl}/${agentAction}`, {
345
+ method: 'POST',
346
+ headers: headers,
347
+ body: JSON.stringify(requestBody),
348
+ });
349
+
350
+ if (!response.ok) {
351
+ throw new Error(`HTTP error! status: ${response.status}`);
352
+ }
353
+
354
+ const data = await response.json();
355
+
356
+ console.log(
357
+ `AI agent ${agentAction === 'start' ? 'connected' : 'disconnected'}`,
358
+ data,
359
+ );
360
+ if (agentAction === 'start') {
361
+ return data;
362
+ }
363
+ } catch (error) {
364
+ console.error(`Failed to ${agentAction} AI agent connection:`, error);
365
+ throw error;
366
+ }
367
+ };
@@ -26,6 +26,8 @@ export interface AgentContextInterface {
26
26
  setAgentId: (id: string) => void;
27
27
  agentVoice?: keyof typeof AI_AGENT_VOICE | '';
28
28
  setAgentVoice: (voice: keyof typeof AI_AGENT_VOICE) => void;
29
+ prompt?: string;
30
+ setPrompt: (prompt: string) => void;
29
31
  }
30
32
 
31
33
  export const AgentContext = createContext<AgentContextInterface>({
@@ -43,6 +45,8 @@ export const AgentContext = createContext<AgentContextInterface>({
43
45
  setAgentVoice: () => {},
44
46
  agentId: '',
45
47
  setAgentId: () => {},
48
+ prompt: '',
49
+ setPrompt: () => {},
46
50
  });
47
51
 
48
52
  /**
@@ -85,6 +89,7 @@ export const AgentProvider: React.FC<{children: React.ReactNode}> = ({
85
89
  const [agentId, setAgentId] = useState('');
86
90
  const [agentVoice, setAgentVoice] =
87
91
  useState<AgentContextInterface['agentVoice']>('');
92
+ const [prompt, setPrompt] = useState('');
88
93
 
89
94
  /**
90
95
  * Adds a new chat item to the chat state while ensuring:
@@ -170,6 +175,8 @@ export const AgentProvider: React.FC<{children: React.ReactNode}> = ({
170
175
  setAgentId,
171
176
  agentVoice,
172
177
  setAgentVoice,
178
+ prompt,
179
+ setPrompt,
173
180
  };
174
181
 
175
182
  return (