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

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 (75) hide show
  1. package/package.json +2 -2
  2. package/template/agora-rn-uikit/README.md +1 -40
  3. package/template/agora-rn-uikit/src/Contexts/PropsContext.tsx +1 -0
  4. package/template/agora-rn-uikit/src/Contexts/RtcContext.tsx +1 -0
  5. package/template/agora-rn-uikit/src/Reducer/Spotlight.ts +11 -0
  6. package/template/agora-rn-uikit/src/Reducer/index.ts +1 -0
  7. package/template/agora-rn-uikit/src/RtcConfigure.tsx +7 -0
  8. package/template/bridge/rtc/webNg/RtcEngine.ts +4 -1
  9. package/template/customization-api/app-state.ts +11 -7
  10. package/template/customization-api/{customize.ts → customize.tsx} +116 -11
  11. package/template/customization-api/sub-components.ts +4 -0
  12. package/template/customization-api/temp.ts +2 -0
  13. package/template/customization-api/typeDefinition.ts +2 -1
  14. package/template/customization-api/utils.ts +6 -1
  15. package/template/defaultConfig.js +4 -2
  16. package/template/global.d.ts +2 -0
  17. package/template/src/AppRoutes.tsx +15 -5
  18. package/template/src/ai-agent/components/AgentControls/AgentContext.tsx +163 -0
  19. package/template/src/ai-agent/components/AgentControls/LeaveCall.png +0 -0
  20. package/template/src/ai-agent/components/AgentControls/Vector.svg +3 -0
  21. package/template/src/ai-agent/components/AgentControls/const.ts +58 -0
  22. package/template/src/ai-agent/components/AgentControls/index.tsx +293 -0
  23. package/template/src/ai-agent/components/AudioVisualizer.tsx +91 -0
  24. package/template/src/ai-agent/components/Bottombar.tsx +91 -0
  25. package/template/src/ai-agent/components/CustomCreate.tsx +279 -0
  26. package/template/src/ai-agent/components/CustomCreateNative.tsx +265 -0
  27. package/template/src/ai-agent/components/CustomSidePanel.tsx +135 -0
  28. package/template/src/ai-agent/components/FallbackLogo.tsx +80 -0
  29. package/template/src/ai-agent/components/LocalAudioWave.tsx +171 -0
  30. package/template/src/ai-agent/components/agent-chat-panel/agent-chat-ui.tsx +82 -0
  31. package/template/src/ai-agent/components/icons.tsx +227 -0
  32. package/template/src/ai-agent/components/mobile/Bottombar.tsx +47 -0
  33. package/template/src/ai-agent/components/mobile/MobileLayoutComponent.tsx +106 -0
  34. package/template/src/ai-agent/components/mobile/Topbar.tsx +62 -0
  35. package/template/src/ai-agent/components/react-audio-visualize/LiveAudioVisualizer/LiveAudioVisualizer.tsx +173 -0
  36. package/template/src/ai-agent/components/react-audio-visualize/LiveAudioVisualizer/index.ts +1 -0
  37. package/template/src/ai-agent/components/react-audio-visualize/LiveAudioVisualizer/utils.ts +102 -0
  38. package/template/src/ai-agent/components/react-audio-visualize/index.ts +1 -0
  39. package/template/src/ai-agent/components/utils.ts +15 -0
  40. package/template/src/ai-agent/index.tsx +301 -0
  41. package/template/src/ai-agent/routes/CustomLoginRoute.tsx +25 -0
  42. package/template/src/ai-agent/routes/CustomValidateRoute.tsx +25 -0
  43. package/template/src/ai-agent/utils.ts +78 -0
  44. package/template/src/assets/font-styles.css +4 -0
  45. package/template/src/assets/fonts/icomoon.ttf +0 -0
  46. package/template/src/assets/selection.json +1 -1
  47. package/template/src/atoms/CustomIcon.tsx +1 -0
  48. package/template/src/atoms/ImageIcon.tsx +3 -0
  49. package/template/src/atoms/ToolbarItem.tsx +0 -2
  50. package/template/src/components/ChatContext.ts +7 -0
  51. package/template/src/components/Controls.tsx +6 -1
  52. package/template/src/components/ErrorBoundary.tsx +37 -0
  53. package/template/src/components/ErrorBoundaryFallback.tsx +44 -0
  54. package/template/src/components/RTMConfigure.tsx +25 -20
  55. package/template/src/components/participants/Participant.tsx +4 -0
  56. package/template/src/components/participants/UserActionMenuOptions.tsx +34 -1
  57. package/template/src/components/precall/PermissionHelper.tsx +11 -8
  58. package/template/src/language/default-labels/videoCallScreenLabels.ts +8 -0
  59. package/template/src/logger/AppBuilderLogger.tsx +4 -1
  60. package/template/src/pages/Create.tsx +11 -12
  61. package/template/src/pages/VideoCall.tsx +1 -0
  62. package/template/src/pages/video-call/ActionSheet.tsx +33 -29
  63. package/template/src/pages/video-call/SidePanelHeader.tsx +8 -3
  64. package/template/src/pages/video-call/SpotlightHighlighter.tsx +91 -0
  65. package/template/src/pages/video-call/VideoCallMobileView.tsx +17 -6
  66. package/template/src/pages/video-call/VideoCallScreen.tsx +0 -1
  67. package/template/src/pages/video-call/VideoRenderer.tsx +32 -4
  68. package/template/src/rtm-events/constants.ts +2 -0
  69. package/template/src/subComponents/ChatBubble.tsx +34 -15
  70. package/template/src/subComponents/FallbackLogo.tsx +3 -1
  71. package/template/src/subComponents/LocalAudioMute.tsx +20 -2
  72. package/template/src/utils/index.tsx +3 -4
  73. package/template/src/utils/useJoinRoom.ts +14 -0
  74. package/template/src/utils/useSpotlight.ts +31 -0
  75. package/template/tsconfig.json +23 -18
@@ -0,0 +1,301 @@
1
+ import React, {useEffect, useState, useContext} from 'react';
2
+ import {ILocalAudioTrack, IRemoteAudioTrack} from 'agora-rtc-sdk-ng';
3
+ import {View, TouchableOpacity, Text} from 'react-native';
4
+ import {
5
+ MaxVideoView,
6
+ useContent,
7
+ useLocalUid,
8
+ type LayoutComponent,
9
+ useRtc,
10
+ useLocalAudio,
11
+ ToolbarPreset,
12
+ useEndCall,
13
+ useSidePanel,
14
+ ToolbarItem,
15
+ IconButton,
16
+ CustomizationApiInterface,
17
+ } from 'customization-api';
18
+ import {isMobileUA} from '../utils/common';
19
+ import AudioVisualizer, {DisconnectedView} from './components/AudioVisualizer';
20
+ import Bottombar from './components/Bottombar';
21
+ import CustomCreate from './components/CustomCreate';
22
+ import CustomCreateNative from './components/CustomCreateNative';
23
+ import {AudioVisualizerEffect} from './components/LocalAudioWave';
24
+ import MobileTopBar from './components/mobile/Topbar';
25
+ import MobileLayoutComponent from './components/mobile/MobileLayoutComponent';
26
+ import MobileBottombar from './components/mobile/Bottombar';
27
+ import {AgentProvider} from './components/AgentControls/AgentContext';
28
+ import {AgentContext} from './components/AgentControls/AgentContext';
29
+ import {AgentState} from './components/AgentControls/const';
30
+ import {
31
+ AGENT_PROXY_URL,
32
+ AGORA_SSO_LOGOUT_PATH,
33
+ AGORA_SSO_BASE,
34
+ } from './components/AgentControls/const';
35
+ import CustomSidePanel from './components/CustomSidePanel';
36
+ import CustomLoginRoute from './routes/CustomLoginRoute';
37
+ import CustomValidateRoute from './routes/CustomValidateRoute';
38
+
39
+ const Topbar = () => {
40
+ const {sidePanel, setSidePanel} = useSidePanel();
41
+
42
+ React.useEffect(() => {
43
+ setSidePanel('agent-transcript-panel');
44
+ }, []);
45
+
46
+ return (
47
+ <ToolbarPreset
48
+ align="top"
49
+ items={{
50
+ 'meeting-title': {hide: true},
51
+ 'participant-count': {hide: true},
52
+ 'recording-status': {hide: true},
53
+ chat: {hide: true},
54
+ participant: {hide: true},
55
+ settings: {hide: false},
56
+ agentTanscript: {
57
+ align: 'end',
58
+ component: () => {
59
+ const {agentAuthToken, setAgentAuthToken} =
60
+ useContext(AgentContext);
61
+ const isOpen = sidePanel === 'agent-transcript-panel';
62
+
63
+ const handlePress = () => {
64
+ setSidePanel(isOpen ? null : 'agent-transcript-panel');
65
+ };
66
+ return (
67
+ <ToolbarItem>
68
+ <IconButton
69
+ iconProps={{
70
+ name: 'chat-nav',
71
+ iconSize: 24,
72
+ tintColor: 'white',
73
+ iconBackgroundColor: isOpen
74
+ ? $config.PRIMARY_ACTION_BRAND_COLOR
75
+ : $config.ICON_BG_COLOR,
76
+ }}
77
+ btnTextProps={{
78
+ textColor: 'white',
79
+ }}
80
+ onPress={handlePress}
81
+ />
82
+ </ToolbarItem>
83
+ );
84
+ },
85
+ },
86
+ Logout: {
87
+ align: 'end',
88
+ component: () => {
89
+ const {agentAuthToken, setAgentAuthToken} =
90
+ useContext(AgentContext);
91
+ const endcall = useEndCall();
92
+
93
+ const ssoLogout = async () => {
94
+ const logoutUrl = `${AGENT_PROXY_URL}/logout`;
95
+
96
+ const response = await fetch(logoutUrl, {
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ Authorization: `Bearer ${agentAuthToken}`,
100
+ },
101
+ });
102
+
103
+ await endcall();
104
+ setAgentAuthToken(null);
105
+
106
+ const data = await response.json();
107
+
108
+ console.log({logoutdata: data});
109
+
110
+ return data;
111
+ };
112
+ const logout = async () => {
113
+ try {
114
+ // await ssoLogout()
115
+ const originURL = window.location.origin + '/create';
116
+ const frontend_redirect_creds = {
117
+ token: agentAuthToken,
118
+ frontend_redirect: originURL,
119
+ };
120
+ const REDIRECT_URL = `${AGENT_PROXY_URL}/logout?state=${JSON.stringify(
121
+ frontend_redirect_creds,
122
+ )}`;
123
+ const ssoUrl = `${AGORA_SSO_BASE}/${AGORA_SSO_LOGOUT_PATH}?redirect_uri=${REDIRECT_URL}`;
124
+ // console.log({REDIRECT_URL})
125
+ window.open(`${ssoUrl}`, '_self');
126
+ } catch (error) {
127
+ console.log({logoutFailed: error});
128
+ }
129
+ };
130
+
131
+ return null;
132
+ return (
133
+ <TouchableOpacity
134
+ style={{
135
+ display: 'flex',
136
+ height: 35,
137
+ padding: 20,
138
+ justifyContent: 'space-between',
139
+ alignItems: 'center',
140
+ gap: 8,
141
+ borderRadius: 4,
142
+ borderWidth: 1,
143
+ borderColor: '#00C2FF',
144
+ flexDirection: 'row',
145
+ }}
146
+ onPress={logout}>
147
+ <Text style={{color: '#FFF'}}>Logout</Text>
148
+ </TouchableOpacity>
149
+ );
150
+ },
151
+ },
152
+ }}
153
+ />
154
+ );
155
+ };
156
+
157
+ const DesktopLayoutComponent: LayoutComponent = () => {
158
+ const localUid = useLocalUid();
159
+ const {defaultContent, activeUids} = useContent();
160
+ const {RtcEngineUnsafe} = useRtc();
161
+ const [localTracks, setLocalTrack] = useState<ILocalAudioTrack | null>(null);
162
+ const [remoteTrack, setRemoteTrack] = useState<IRemoteAudioTrack | null>(
163
+ null,
164
+ );
165
+ const [mediaStreamTrack, setMediaStreamTrack] =
166
+ React.useState<MediaStreamTrack>();
167
+
168
+ const {agentConnectionState, setAgentConnectionState, agentUID} =
169
+ useContext(AgentContext);
170
+
171
+ const {getLocalAudioStream, getRemoteAudioStream} = useLocalAudio();
172
+
173
+ const connected = activeUids.includes(agentUID);
174
+ console.log({activeUids}, 'active uids');
175
+
176
+ // this state occurs when agent_stop is successful, but
177
+ // user is still not disconnected from the RTC channel - state-of-wait
178
+ const isAwaitingLeave = agentConnectionState === AgentState.AWAITING_LEAVE;
179
+ console.log(
180
+ {isAwaitingLeave},
181
+ {connected},
182
+ 'what is going on',
183
+ agentConnectionState,
184
+ );
185
+ useEffect(() => {
186
+ if (getLocalAudioStream()) {
187
+ setLocalTrack(getLocalAudioStream());
188
+ }
189
+ }, [RtcEngineUnsafe]);
190
+
191
+ useEffect(() => {
192
+ if (getRemoteAudioStream(agentUID)) {
193
+ setRemoteTrack(getRemoteAudioStream(agentUID));
194
+ }
195
+ }, [activeUids]);
196
+
197
+ return (
198
+ <View
199
+ style={{
200
+ flex: 1,
201
+ display: 'flex',
202
+ flexDirection: 'column',
203
+ borderRadius: 10,
204
+ }}>
205
+ <MaxVideoView
206
+ user={{
207
+ ...defaultContent[agentUID],
208
+ name: 'OpenAI',
209
+ video: false,
210
+ }}
211
+ CustomChild={() =>
212
+ // show agent voice waves, when agent is connected to the channel, but also not on a state-of-wait,
213
+ connected && !isAwaitingLeave ? (
214
+ <AudioVisualizer audioTrack={remoteTrack} />
215
+ ) : (
216
+ <DisconnectedView isConnected={connected} />
217
+ )
218
+ }
219
+ hideMenuOptions={true}
220
+ />
221
+ <View
222
+ style={{
223
+ position: 'absolute',
224
+ bottom: 10,
225
+ right: 10,
226
+ display: 'flex',
227
+ flexDirection: 'row',
228
+ height: 200,
229
+ width: 300,
230
+ }}>
231
+ <MaxVideoView user={defaultContent[localUid]} hideMenuOptions={true} />
232
+ <View
233
+ style={{
234
+ position: 'absolute',
235
+ bottom: 16,
236
+ right: 10,
237
+ }}>
238
+ {localTracks && (
239
+ <AudioVisualizerEffect
240
+ type="user"
241
+ barWidth={3}
242
+ minBarHeight={2}
243
+ maxBarHeight={25}
244
+ audioTrack={localTracks}
245
+ borderRadius={2}
246
+ gap={4}
247
+ />
248
+ )}
249
+ </View>
250
+ </View>
251
+ </View>
252
+ );
253
+ };
254
+
255
+ export const AI_AGENT_CUSTOMIZATION: CustomizationApiInterface = {
256
+ components: {
257
+ appRoot: AgentProvider,
258
+ create: isMobileUA() ? CustomCreateNative : CustomCreate,
259
+ //preferenceWrapper: AgentProvider,
260
+ videoCall: {
261
+ customLayout() {
262
+ return [
263
+ {
264
+ name: 'Ai-Agent',
265
+ label: 'Ai-Agent',
266
+ icon: '🤖',
267
+ component: isMobileUA()
268
+ ? MobileLayoutComponent
269
+ : DesktopLayoutComponent,
270
+ },
271
+ ];
272
+ },
273
+ customSidePanel: () => {
274
+ return [
275
+ {
276
+ name: 'agent-transcript-panel',
277
+ component: CustomSidePanel,
278
+ title: 'Agent Transcript',
279
+ onClose: () => {},
280
+ },
281
+ ];
282
+ },
283
+ topToolBar: isMobileUA() ? MobileTopBar : Topbar,
284
+ bottomToolBar: isMobileUA() ? MobileBottombar : Bottombar,
285
+ },
286
+ },
287
+ customRoutes: [
288
+ {
289
+ component: CustomLoginRoute,
290
+ exact: true,
291
+ path: '/login',
292
+ isTopLevelRoute: true,
293
+ },
294
+ {
295
+ component: CustomValidateRoute,
296
+ exact: true,
297
+ path: '/validate',
298
+ isTopLevelRoute: true,
299
+ },
300
+ ],
301
+ };
@@ -0,0 +1,25 @@
1
+ import React, {useEffect} from 'react';
2
+ import {Loading} from 'customization-api';
3
+ import {
4
+ AGENT_PROXY_URL,
5
+ AGORA_SSO_BASE,
6
+ AGORA_SSO_LOGIN_PATH,
7
+ AGORA_SSO_CLIENT_ID,
8
+ } from '../components/AgentControls/const';
9
+
10
+ export default function CustomLoginRoute() {
11
+ const handleSSOLogin = () => {
12
+ const REDIRECT_URL = `${AGENT_PROXY_URL}/login`;
13
+ const originURL = window.location.origin + '/validate';
14
+
15
+ const ssoUrl = `${AGORA_SSO_BASE}/${AGORA_SSO_LOGIN_PATH}?scope=basic_info&response_type=code&state=url=${originURL}&redirect_uri=${REDIRECT_URL}&client_id=${AGORA_SSO_CLIENT_ID}`;
16
+ console.log('sso', ssoUrl);
17
+ window.location.href = ssoUrl;
18
+ };
19
+
20
+ useEffect(() => {
21
+ handleSSOLogin();
22
+ }, []);
23
+
24
+ return <Loading text="Redirecting to agora SSO..." />;
25
+ }
@@ -0,0 +1,25 @@
1
+ import React, {useContext, useEffect} from 'react';
2
+ import {useHistory, Loading} from 'customization-api';
3
+ import {AgentContext} from '../components/AgentControls/AgentContext';
4
+
5
+ const CustomValidateRoute = () => {
6
+ const history = useHistory();
7
+ const {setAgentAuthToken} = useContext(AgentContext);
8
+
9
+ useEffect(() => {
10
+ const queryParams = new URLSearchParams(window.location.search);
11
+ queryParams.forEach((value, key) => {
12
+ console.log(`${key}: ${value}`);
13
+ });
14
+ // check for success login from url param and redirect to create using custom param
15
+
16
+ if (queryParams.has('token')) {
17
+ // Redirect to create using custom param only if token exists
18
+ setAgentAuthToken(queryParams.get('token') || '');
19
+ history.push('/create?auth=success');
20
+ }
21
+ }, []);
22
+ return <Loading text="" />;
23
+ };
24
+
25
+ export default CustomValidateRoute;
@@ -0,0 +1,78 @@
1
+ import {IMicrophoneAudioTrack} from 'agora-rtc-sdk-ng';
2
+ import {useState, useEffect} from 'react';
3
+
4
+ export const useMultibandTrackVolume = (
5
+ track?: IMicrophoneAudioTrack | MediaStreamTrack,
6
+ bands: number = 5,
7
+ loPass: number = 100,
8
+ hiPass: number = 600,
9
+ ) => {
10
+ const [frequencyBands, setFrequencyBands] = useState<Float32Array[]>([]);
11
+
12
+ useEffect(() => {
13
+ if (!track) {
14
+ return setFrequencyBands(new Array(bands).fill(new Float32Array(0)));
15
+ }
16
+
17
+ const ctx = new AudioContext();
18
+ let finTrack =
19
+ track instanceof MediaStreamTrack ? track : track.getMediaStreamTrack();
20
+ const mediaStream = new MediaStream([finTrack]);
21
+ const source = ctx.createMediaStreamSource(mediaStream);
22
+ const analyser = ctx.createAnalyser();
23
+ analyser.fftSize = 2048;
24
+
25
+ source.connect(analyser);
26
+
27
+ const bufferLength = analyser.frequencyBinCount;
28
+ const dataArray = new Float32Array(bufferLength);
29
+
30
+ const updateVolume = () => {
31
+ analyser.getFloatFrequencyData(dataArray);
32
+ let frequencies: Float32Array = new Float32Array(dataArray.length);
33
+ for (let i = 0; i < dataArray.length; i++) {
34
+ frequencies[i] = dataArray[i];
35
+ }
36
+ frequencies = frequencies.slice(loPass, hiPass);
37
+
38
+ const normalizedFrequencies = normalizeFrequencies(frequencies);
39
+ const chunkSize = Math.ceil(normalizedFrequencies.length / bands);
40
+ const chunks: Float32Array[] = [];
41
+ for (let i = 0; i < bands; i++) {
42
+ chunks.push(
43
+ normalizedFrequencies.slice(i * chunkSize, (i + 1) * chunkSize),
44
+ );
45
+ }
46
+
47
+ setFrequencyBands(chunks);
48
+ };
49
+
50
+ const interval = setInterval(updateVolume, 10);
51
+
52
+ return () => {
53
+ source.disconnect();
54
+ clearInterval(interval);
55
+ };
56
+ }, [track, loPass, hiPass, bands]);
57
+
58
+ return frequencyBands;
59
+ };
60
+
61
+ export const normalizeFrequencies = (frequencies: Float32Array) => {
62
+ const normalizeDb = (value: number) => {
63
+ const minDb = -100;
64
+ const maxDb = -10;
65
+ let db = 1 - (Math.max(minDb, Math.min(maxDb, value)) * -1) / 100;
66
+ db = Math.sqrt(db);
67
+
68
+ return db;
69
+ };
70
+
71
+ // Normalize all frequency values
72
+ return frequencies.map(value => {
73
+ if (value === -Infinity) {
74
+ return 0;
75
+ }
76
+ return normalizeDb(value);
77
+ });
78
+ };
@@ -24,6 +24,10 @@
24
24
  -moz-osx-font-smoothing: grayscale;
25
25
  }
26
26
 
27
+ .icon-spotlight:before {
28
+ content: '\e9a3';
29
+ color: #fff;
30
+ }
27
31
  .icon-clipboard_outlined:before {
28
32
  content: '\e9a2';
29
33
  }