agora-appbuilder-core 4.1.0-beta-2 → 4.1.0-beta-4

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 (37) hide show
  1. package/package.json +1 -1
  2. package/template/customization-api/atoms.ts +1 -0
  3. package/template/customization-api/customize.tsx +11 -0
  4. package/template/customization-api/sub-components.ts +2 -0
  5. package/template/src/ai-agent/assets/join-call.png +0 -0
  6. package/template/src/ai-agent/assets/leave-call.png +0 -0
  7. package/template/src/ai-agent/components/AgentControls/AgentConnectionWrapper.tsx +248 -0
  8. package/template/src/ai-agent/components/AgentControls/AgentContext.tsx +23 -1
  9. package/template/src/ai-agent/components/AgentControls/const.ts +38 -5
  10. package/template/src/ai-agent/components/AgentControls/index.tsx +60 -257
  11. package/template/src/ai-agent/components/AudioVisualizer.tsx +32 -20
  12. package/template/src/ai-agent/components/Bottombar.tsx +114 -51
  13. package/template/src/ai-agent/components/{CustomSidePanel.tsx → CustomChatPanel.tsx} +3 -3
  14. package/template/src/ai-agent/components/CustomCreate.tsx +7 -185
  15. package/template/src/ai-agent/components/CustomSettingsPanel.tsx +193 -0
  16. package/template/src/ai-agent/components/SelectAiAgent.tsx +74 -0
  17. package/template/src/ai-agent/components/SelectAiAgentVoice.tsx +68 -0
  18. package/template/src/ai-agent/components/UserPrompt.tsx +64 -0
  19. package/template/src/ai-agent/components/agent-chat-panel/agent-chat-ui.tsx +13 -11
  20. package/template/src/ai-agent/components/mobile/Bottombar.tsx +55 -39
  21. package/template/src/ai-agent/components/mobile/MobileLayoutComponent.tsx +1 -1
  22. package/template/src/ai-agent/components/mobile/Topbar.tsx +10 -27
  23. package/template/src/ai-agent/components/utils.ts +17 -0
  24. package/template/src/ai-agent/index.tsx +61 -206
  25. package/template/src/atoms/Dropdown.tsx +1 -1
  26. package/template/src/auth/AuthProvider.tsx +19 -0
  27. package/template/src/components/SettingsView.tsx +7 -3
  28. package/template/src/components/room-info/useRoomInfo.tsx +21 -0
  29. package/template/src/pages/video-call/VideoCallScreen.tsx +18 -3
  30. package/template/src/subComponents/FallbackLogo.tsx +1 -3
  31. package/template/src/subComponents/SelectDevice.tsx +7 -7
  32. package/template/src/subComponents/SidePanelHeader.tsx +2 -2
  33. package/template/src/utils/useJoinRoom.ts +40 -8
  34. package/template/src/ai-agent/components/AgentControls/LeaveCall.png +0 -0
  35. package/template/src/ai-agent/components/AgentControls/Vector.svg +0 -3
  36. package/template/src/ai-agent/components/CustomCreateNative.tsx +0 -265
  37. 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-2",
3
+ "version": "4.1.0-beta-4",
4
4
  "description": "React Native template for RTE app builder",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -7,3 +7,4 @@ export {default as PrimaryButton} from '../src/atoms/PrimaryButton';
7
7
  export {default as TertiaryButton} from '../src/atoms/TertiaryButton';
8
8
  export {default as ActionMenu} from '../src/atoms/ActionMenu';
9
9
  export {default as IconButton} from '../src/atoms/IconButton';
10
+ export {default as Dropdown} from '../src/atoms/Dropdown';
@@ -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;
@@ -91,3 +91,5 @@ export {default as Loading} from '../src/subComponents/Loading';
91
91
  export {default as UserAvatar} from '../src/atoms/UserAvatar';
92
92
  export {default as Card} from '../src/atoms/Card';
93
93
  export {default as ThemeConfig} from '../src/theme';
94
+ export {default as SelectDevice} from '../src/subComponents/SelectDevice';
95
+ export {EditName} from '../src/components/SettingsView';
@@ -0,0 +1,248 @@
1
+ import {UidType, useContent, useRoomInfo, Toast} from 'customization-api';
2
+ import React, {createContext, useContext, useEffect} from 'react';
3
+ import {AgentContext} from './AgentContext';
4
+ import {AgentState} from './const';
5
+ import StorageContext from '../../../components/StorageContext';
6
+
7
+ export interface AgentContextInterface {
8
+ toggleAgentConnection: (forceStop?: boolean) => Promise<boolean>;
9
+ }
10
+
11
+ export const AgentConnectionContext = createContext<AgentContextInterface>({
12
+ toggleAgentConnection: () => {
13
+ return Promise.resolve(false);
14
+ },
15
+ });
16
+
17
+ export const AgentConnectionProvider: React.FC<{children: React.ReactNode}> = ({
18
+ children,
19
+ }) => {
20
+ const {activeUids: users} = useContent();
21
+ const {
22
+ agentUID,
23
+ agentConnectionState,
24
+ setAgentConnectionState,
25
+ agentId,
26
+ setAgentUID,
27
+ prompt,
28
+ } = useContext(AgentContext);
29
+ const {
30
+ data: {channel: channel_name, uid: localUid, agents},
31
+ } = useRoomInfo();
32
+ const {store} = useContext(StorageContext);
33
+
34
+ useEffect(() => {
35
+ console.log('debugging users agent contrl', {users});
36
+ // welcome agent
37
+ const aiAgentUID = users.filter(item => item === agentUID);
38
+
39
+ if (
40
+ aiAgentUID.length &&
41
+ agentConnectionState === AgentState.AWAITING_JOIN
42
+ ) {
43
+ setAgentConnectionState(AgentState.AGENT_CONNECTED);
44
+
45
+ Toast.show({
46
+ leadingIconName: 'tick-fill',
47
+ type: 'success',
48
+ text1: 'Say Hi!!',
49
+ text2: null,
50
+ visibilityTime: 3000,
51
+ primaryBtn: null,
52
+ secondaryBtn: null,
53
+ leadingIcon: null,
54
+ });
55
+ }
56
+ // when agent leaves, show left toast, and set agent to not connected state
57
+ if (
58
+ !aiAgentUID.length &&
59
+ agentConnectionState === AgentState.AWAITING_LEAVE
60
+ ) {
61
+ setAgentConnectionState(AgentState.NOT_CONNECTED);
62
+ }
63
+ }, [users, agentUID]);
64
+
65
+ const handleConnectionToggle = async (forceStop: boolean = false) => {
66
+ try {
67
+ // connect to agent when agent is in not connected state or when earlier connect failed
68
+ if (
69
+ agentConnectionState === AgentState.NOT_CONNECTED ||
70
+ agentConnectionState === AgentState.AGENT_REQUEST_FAILED ||
71
+ agentConnectionState === AgentState.AWAITING_LEAVE
72
+ ) {
73
+ try {
74
+ setAgentConnectionState(AgentState.REQUEST_SENT);
75
+ const data = await connectToAIAgent(
76
+ 'start',
77
+ channel_name,
78
+ localUid,
79
+ store.token,
80
+ {
81
+ agent_id: agentId,
82
+ prompt: prompt,
83
+ voice: agents.find(a => a.id === agentId)?.config?.tts?.params
84
+ ?.voice_name,
85
+ },
86
+ );
87
+ // console.log("response X-Client-ID", newClientId, typeof newClientId)
88
+ // @ts-ignore
89
+ const {agent_uid = null} = data;
90
+
91
+ //setClientId(agent_id);
92
+ setAgentUID(agent_uid);
93
+
94
+ setAgentConnectionState(AgentState.AWAITING_JOIN);
95
+
96
+ Toast.show({
97
+ leadingIconName: 'tick-fill',
98
+ type: 'success',
99
+ text1: 'Agent requested to join',
100
+ text2: null,
101
+ visibilityTime: 3000,
102
+ primaryBtn: null,
103
+ secondaryBtn: null,
104
+ leadingIcon: null,
105
+ });
106
+ return Promise.resolve(true);
107
+ } catch (agentConnectError) {
108
+ setAgentConnectionState(AgentState.AGENT_REQUEST_FAILED);
109
+
110
+ if (agentConnectError.toString().indexOf('401') !== -1) {
111
+ Toast.show({
112
+ leadingIconName: 'alert',
113
+ type: 'error',
114
+ text1: 'Your session is expired. Please sign in to join call.',
115
+ text2: null,
116
+ visibilityTime: 5000,
117
+ primaryBtn: null,
118
+ secondaryBtn: null,
119
+ leadingIcon: null,
120
+ });
121
+ } else {
122
+ Toast.show({
123
+ leadingIconName: 'alert',
124
+ type: 'error',
125
+ text1: 'Uh oh! Agent failed to connect',
126
+ text2: null,
127
+ visibilityTime: 5000,
128
+ primaryBtn: null,
129
+ secondaryBtn: null,
130
+ leadingIcon: null,
131
+ });
132
+ }
133
+
134
+ throw agentConnectError;
135
+ }
136
+ }
137
+ // disconnect agent with agent is already connected or when earlier disconnect failed
138
+ if (
139
+ forceStop === true ||
140
+ agentConnectionState === AgentState.AGENT_CONNECTED ||
141
+ agentConnectionState === AgentState.AGENT_DISCONNECT_FAILED
142
+ ) {
143
+ try {
144
+ setAgentConnectionState(AgentState.AGENT_DISCONNECT_REQUEST);
145
+ await connectToAIAgent('stop', channel_name, localUid, store.token, {
146
+ agent_id: agentId,
147
+ });
148
+ setAgentConnectionState(AgentState.AWAITING_LEAVE);
149
+ if (!forceStop) {
150
+ Toast.show({
151
+ leadingIconName: 'tick-fill',
152
+ type: 'success',
153
+ text1: 'Agent disconnected',
154
+ text2: null,
155
+ visibilityTime: 3000,
156
+ primaryBtn: null,
157
+ secondaryBtn: null,
158
+ leadingIcon: null,
159
+ });
160
+ }
161
+ return Promise.resolve(true);
162
+ } catch (agentDisconnectError) {
163
+ setAgentConnectionState(AgentState.AGENT_DISCONNECT_FAILED);
164
+
165
+ Toast.show({
166
+ leadingIconName: 'alert',
167
+ type: 'error',
168
+ text1: 'Uh oh! Agent failed to disconnect',
169
+ text2: null,
170
+ visibilityTime: 5000,
171
+ primaryBtn: null,
172
+ secondaryBtn: null,
173
+ leadingIcon: null,
174
+ });
175
+
176
+ throw agentDisconnectError;
177
+ }
178
+ }
179
+ } catch (error) {
180
+ console.log(`Agent failed to connect/disconnect - ${error}`);
181
+ }
182
+ };
183
+
184
+ const value = {
185
+ toggleAgentConnection: handleConnectionToggle,
186
+ };
187
+
188
+ return (
189
+ <AgentConnectionContext.Provider value={value}>
190
+ {children}
191
+ </AgentConnectionContext.Provider>
192
+ );
193
+ };
194
+
195
+ export const connectToAIAgent = async (
196
+ agentAction: 'start' | 'stop',
197
+ channel_name: string,
198
+ localUid: UidType,
199
+ agentAuthToken: string,
200
+ data?: {agent_id: string; prompt?: string; voice?: string},
201
+ ): Promise<{}> => {
202
+ // const apiUrl = '/api/proxy';
203
+ const apiUrl = $config.BACKEND_ENDPOINT + '/v1/convoai';
204
+ const requestBody = {
205
+ channel_name: channel_name,
206
+ uid: localUid, // user uid // localUid or 0
207
+ };
208
+
209
+ if (data && data?.agent_id) {
210
+ requestBody['ai_agent_id'] = data.agent_id;
211
+ }
212
+ if (data && data?.voice) {
213
+ requestBody['voice'] = data.voice;
214
+ }
215
+ if (data && data?.prompt) {
216
+ requestBody['prompt'] = data.prompt;
217
+ }
218
+
219
+ const headers: HeadersInit = {
220
+ 'Content-Type': 'application/json',
221
+ Authorization: `Bearer ${agentAuthToken}`,
222
+ };
223
+
224
+ try {
225
+ const response = await fetch(`${apiUrl}/${agentAction}`, {
226
+ method: 'POST',
227
+ headers: headers,
228
+ body: JSON.stringify(requestBody),
229
+ });
230
+
231
+ if (!response.ok) {
232
+ throw new Error(`HTTP error! status: ${response.status}`);
233
+ }
234
+
235
+ const data = await response.json();
236
+
237
+ console.log(
238
+ `AI agent ${agentAction === 'start' ? 'connected' : 'disconnected'}`,
239
+ data,
240
+ );
241
+ if (agentAction === 'start') {
242
+ return data;
243
+ }
244
+ } catch (error) {
245
+ console.error(`Failed to ${agentAction} AI agent connection:`, error);
246
+ throw error;
247
+ }
248
+ };
@@ -1,7 +1,7 @@
1
1
  import React, {createContext, useState} from 'react';
2
2
  import {AIAgentState, AgentState} from './const';
3
3
  import {UidType} from 'customization-api';
4
-
4
+ import {AI_AGENT_VOICE} from './const';
5
5
  export interface ChatItem {
6
6
  id: string;
7
7
  uid: UidType;
@@ -22,6 +22,12 @@ export interface AgentContextInterface {
22
22
  setAgentUID: (uid: UidType | null) => void;
23
23
  chatItems: ChatItem[];
24
24
  addChatItem: (newItem: ChatItem) => void;
25
+ agentId: string;
26
+ setAgentId: (id: string) => void;
27
+ agentVoice?: keyof typeof AI_AGENT_VOICE | '';
28
+ setAgentVoice: (voice: keyof typeof AI_AGENT_VOICE) => void;
29
+ prompt?: string;
30
+ setPrompt: (prompt: string) => void;
25
31
  }
26
32
 
27
33
  export const AgentContext = createContext<AgentContextInterface>({
@@ -35,6 +41,12 @@ export const AgentContext = createContext<AgentContextInterface>({
35
41
  setAgentUID: () => {},
36
42
  chatItems: [],
37
43
  addChatItem: () => {}, // Default no-op
44
+ agentVoice: '',
45
+ setAgentVoice: () => {},
46
+ agentId: '',
47
+ setAgentId: () => {},
48
+ prompt: '',
49
+ setPrompt: () => {},
38
50
  });
39
51
 
40
52
  /**
@@ -74,6 +86,10 @@ export const AgentProvider: React.FC<{children: React.ReactNode}> = ({
74
86
  const [agentUID, setAgentUID] = useState<UidType | null>(null);
75
87
  const [isSubscribedForStreams, setIsSubscribedForStreams] = useState(false);
76
88
  const [chatItems, setChatItems] = useState<ChatItem[]>([]);
89
+ const [agentId, setAgentId] = useState('');
90
+ const [agentVoice, setAgentVoice] =
91
+ useState<AgentContextInterface['agentVoice']>('');
92
+ const [prompt, setPrompt] = useState('');
77
93
 
78
94
  /**
79
95
  * Adds a new chat item to the chat state while ensuring:
@@ -155,6 +171,12 @@ export const AgentProvider: React.FC<{children: React.ReactNode}> = ({
155
171
  setAgentUID,
156
172
  chatItems,
157
173
  addChatItem, // Expose the function in the context
174
+ agentId,
175
+ setAgentId,
176
+ agentVoice,
177
+ setAgentVoice,
178
+ prompt,
179
+ setPrompt,
158
180
  };
159
181
 
160
182
  return (
@@ -1,16 +1,16 @@
1
1
  import {isMobileUA} from '../../../utils/common';
2
2
 
3
3
  export const AI_AGENT_STATE = {
4
- NOT_CONNECTED: 'Start Call',
5
- REQUEST_SENT: isMobileUA() ? 'Start Call' : 'Requesting agent join..', // loading - reg
6
- AWAITING_JOIN: isMobileUA() ? 'Start Call' : 'Agent will join shortly..', // loading
4
+ NOT_CONNECTED: 'Join Call',
5
+ REQUEST_SENT: isMobileUA() ? 'Join Call' : 'Requesting agent join..', // loading - reg
6
+ AWAITING_JOIN: isMobileUA() ? 'Join Call' : 'Agent will join shortly..', // loading
7
7
  AGENT_CONNECTED: 'End Call',
8
- AGENT_REQUEST_FAILED: 'Start Call',
8
+ AGENT_REQUEST_FAILED: 'Join Call',
9
9
  AGENT_DISCONNECT_REQUEST: isMobileUA()
10
10
  ? 'End Call'
11
11
  : 'Disconnecting agent...', // loading - req
12
12
  AGENT_DISCONNECT_FAILED: 'End Call',
13
- AWAITING_LEAVE: 'Start Call', // loading
13
+ AWAITING_LEAVE: 'Join Call', // loading
14
14
  } as const;
15
15
 
16
16
  export type AIAgentState = keyof typeof AI_AGENT_STATE;
@@ -56,3 +56,36 @@ export const AGORA_SSO_LOGIN_PATH = '/api/v0/oauth/authorize';
56
56
  export const AGORA_SSO_LOGOUT_PATH = '/api/v0/logout';
57
57
 
58
58
  export const AGORA_SSO_CLIENT_ID = 'openai_agora';
59
+
60
+ export const AI_AGENT_VOICE = {
61
+ 'en-US-AvaMultilingualNeural': 'en-US-AvaMultilingualNeural',
62
+ 'en-US-AndrewMultilingualNeural': 'en-US-AndrewMultilingualNeural',
63
+ 'en-US-EmmaMultilingualNeural': 'en-US-EmmaMultilingualNeural',
64
+ 'en-US-BrianMultilingualNeural': 'en-US-BrianMultilingualNeural',
65
+ 'en-US-AvaNeural': 'en-US-AvaNeural',
66
+ 'en-US-AndrewNeural': 'en-US-AndrewNeural',
67
+ 'en-US-EmmaNeural': 'en-US-EmmaNeural',
68
+ 'en-US-BrianNeural': 'en-US-BrianNeural',
69
+ 'en-US-JennyNeural': 'en-US-JennyNeural',
70
+ 'en-US-GuyNeural': 'en-US-GuyNeural',
71
+ 'en-US-AriaNeural': 'en-US-AriaNeural',
72
+ 'en-US-DavisNeural': 'en-US-DavisNeural',
73
+ 'en-US-JaneNeural': 'en-US-JaneNeural',
74
+ 'en-US-JasonNeural': 'en-US-JasonNeural',
75
+ 'en-US-SaraNeural': 'en-US-SaraNeural',
76
+ 'en-US-TonyNeural': 'en-US-TonyNeural',
77
+ 'en-US-NancyNeural': 'en-US-NancyNeural',
78
+ 'en-US-AmberNeural': 'en-US-AmberNeural',
79
+ 'en-US-AnaNeural': 'en-US-AnaNeural',
80
+ 'en-US-AshleyNeural': 'en-US-AshleyNeural',
81
+ 'en-US-BrandonNeural': 'en-US-BrandonNeural',
82
+ 'en-US-ChristopherNeural': 'en-US-ChristopherNeural',
83
+ 'en-US-CoraNeural': 'en-US-CoraNeural',
84
+ 'en-US-ElizabethNeural': 'en-US-ElizabethNeural',
85
+ 'en-US-EricNeural': 'en-US-EricNeural',
86
+ 'en-US-JacobNeural': 'en-US-JacobNeural',
87
+ 'en-US-JennyMultilingualNeural4': 'en-US-JennyMultilingualNeural4',
88
+ 'en-US-MichelleNeural': 'en-US-MichelleNeural',
89
+ 'en-US-MonicaNeural': 'en-US-MonicaNeural',
90
+ 'en-US-RogerNeural': 'en-US-RogerNeural',
91
+ };