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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agora-appbuilder-core",
3
- "version": "4.1.0-beta-4",
3
+ "version": "4.1.0-beta-6",
4
4
  "description": "React Native template for RTE app builder",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -1,4 +1,10 @@
1
- import {UidType, useContent, useRoomInfo, Toast} from 'customization-api';
1
+ import {
2
+ UidType,
3
+ useContent,
4
+ useRoomInfo,
5
+ Toast,
6
+ useRtc,
7
+ } from 'customization-api';
2
8
  import React, {createContext, useContext, useEffect} from 'react';
3
9
  import {AgentContext} from './AgentContext';
4
10
  import {AgentState} from './const';
@@ -25,12 +31,125 @@ export const AgentConnectionProvider: React.FC<{children: React.ReactNode}> = ({
25
31
  agentId,
26
32
  setAgentUID,
27
33
  prompt,
34
+ isSubscribedForStreams,
35
+ setIsSubscribedForStreams,
36
+ addChatItem,
28
37
  } = useContext(AgentContext);
29
38
  const {
30
39
  data: {channel: channel_name, uid: localUid, agents},
31
40
  } = useRoomInfo();
32
41
  const {store} = useContext(StorageContext);
33
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
+
34
153
  useEffect(() => {
35
154
  console.log('debugging users agent contrl', {users});
36
155
  // welcome agent
@@ -87,7 +87,7 @@ export const AgentControl: React.FC = () => {
87
87
  lineHeight: 18,
88
88
  fontWeight: '600',
89
89
  ...fontcolorStyle,
90
- }}>{`${AI_AGENT_STATE[agentConnectionState]}`}</Text>
90
+ }}>{` ${AI_AGENT_STATE[agentConnectionState]}`}</Text>
91
91
  ) : (
92
92
  <></>
93
93
  )}
@@ -52,8 +52,7 @@ export const CustomSettingButton = () => {
52
52
  }
53
53
  return (
54
54
  <ToolbarItem>
55
- {' '}
56
- <IconButton {...iconButtonProps} />{' '}
55
+ <IconButton {...iconButtonProps} />
57
56
  </ToolbarItem>
58
57
  );
59
58
  };
@@ -1,122 +1,8 @@
1
1
  import {StyleSheet, View} from 'react-native';
2
- import React, {useContext} from 'react';
3
- import {useRtc} from 'customization-api';
4
- import {AgentContext} from './AgentControls/AgentContext';
2
+ import React from 'react';
5
3
  import ChatScreen from './agent-chat-panel/agent-chat-ui';
6
4
 
7
5
  const CustomSidePanel = () => {
8
- const {RtcEngineUnsafe} = useRtc();
9
- const {isSubscribedForStreams, setIsSubscribedForStreams, addChatItem} =
10
- useContext(AgentContext);
11
-
12
- const messageCache = {};
13
- const TIMEOUT_MS = 5000; // Timeout for incomplete messages
14
-
15
- React.useEffect(() => {
16
- if (!isSubscribedForStreams) {
17
- RtcEngineUnsafe.addListener(
18
- 'onStreamMessage',
19
- handleStreamMessageCallback,
20
- );
21
- setIsSubscribedForStreams(true);
22
- }
23
- }, []);
24
-
25
- const handleStreamMessageCallback = (...args) => {
26
- console.log('rec', args);
27
- parseData(args[1]);
28
- };
29
-
30
- const parseData = data => {
31
- let decoder = new TextDecoder('utf-8');
32
- let decodedMessage = decoder.decode(data);
33
- console.log('[test] textstream raw data', decodedMessage);
34
- handleChunk(decodedMessage);
35
- };
36
- // Function to process received chunk via event emitter
37
- const handleChunk = (formattedChunk: string) => {
38
- try {
39
- // Split the chunk by the delimiter "|"
40
- const [message_id, partIndexStr, totalPartsStr, content] =
41
- formattedChunk.split('|');
42
-
43
- const part_index = parseInt(partIndexStr, 10);
44
- const total_parts =
45
- totalPartsStr === '???' ? -1 : parseInt(totalPartsStr, 10); // -1 means total parts unknown
46
-
47
- // Ensure total_parts is known before processing further
48
- if (total_parts === -1) {
49
- console.warn(
50
- `Total parts for message ${message_id} unknown, waiting for further parts.`,
51
- );
52
- return;
53
- }
54
-
55
- const chunkData = {
56
- message_id,
57
- part_index,
58
- total_parts,
59
- content,
60
- };
61
-
62
- // Check if we already have an entry for this message
63
- if (!messageCache[message_id]) {
64
- messageCache[message_id] = [];
65
- // Set a timeout to discard incomplete messages
66
- setTimeout(() => {
67
- if (messageCache[message_id]?.length !== total_parts) {
68
- console.warn(`Incomplete message with ID ${message_id} discarded`);
69
- delete messageCache[message_id]; // Discard incomplete message
70
- }
71
- }, TIMEOUT_MS);
72
- }
73
-
74
- // Cache this chunk by message_id
75
- messageCache[message_id].push(chunkData);
76
-
77
- // If all parts are received, reconstruct the message
78
- if (messageCache[message_id].length === total_parts) {
79
- const completeMessage = reconstructMessage(messageCache[message_id]);
80
- const data = atob(completeMessage);
81
- const {stream_id, is_final, text, text_ts} = JSON.parse(data);
82
- /** Data type of above object
83
- * stream_id: number
84
- * is_final: boolean
85
- * text: string
86
- * text_ts: number
87
- */
88
- const textItem = {
89
- id: message_id,
90
- uid: stream_id,
91
- time: text_ts,
92
- dataType: 'transcribe',
93
- text: text,
94
- isFinal: is_final,
95
- isSelf: stream_id === 0 ? false : true,
96
- };
97
-
98
- if (text.trim().length > 0) {
99
- //this.emit("textChanged", textItem);
100
- console.warn('emit textChanged: ', textItem);
101
- addChatItem(textItem);
102
- }
103
-
104
- // Clean up the cache
105
- delete messageCache[message_id];
106
- }
107
- } catch (error) {
108
- console.error('Error processing chunk:', error);
109
- }
110
- };
111
-
112
- const reconstructMessage = chunks => {
113
- // Sort chunks by their part index
114
- chunks.sort((a, b) => a.part_index - b.part_index);
115
-
116
- // Concatenate all chunks to form the full message
117
- return chunks.map(chunk => chunk.content).join('');
118
- };
119
-
120
6
  return (
121
7
  <View style={styles.container}>
122
8
  <ChatScreen />
@@ -56,7 +56,7 @@ const CustomCreate = () => {
56
56
  };
57
57
  });
58
58
  // set default meeting name
59
- onChangeRoomTitle(generateChannelId);
59
+ onChangeRoomTitle('Conversational AI');
60
60
  }, []);
61
61
 
62
62
  const createRoomAndNavigateToShare = async (
@@ -1,22 +1,55 @@
1
- import React, {useContext} from 'react';
2
- import {View, TextInput, StyleSheet, Text, Platform} from 'react-native';
1
+ import React, {SetStateAction, useContext, useEffect, useState} from 'react';
2
+ import {
3
+ View,
4
+ TextInput,
5
+ StyleSheet,
6
+ Text,
7
+ Platform,
8
+ TouchableOpacity,
9
+ ViewStyle,
10
+ ModalProps,
11
+ TouchableWithoutFeedback,
12
+ Modal,
13
+ } from 'react-native';
3
14
  import ThemeConfig from '../../theme';
4
15
  import {AgentContext} from './AgentControls/AgentContext';
16
+ import {
17
+ IconButton,
18
+ PrimaryButton,
19
+ Spacer,
20
+ TertiaryButton,
21
+ useRoomInfo,
22
+ } from 'customization-api';
23
+ import {useIsDesktop, isMobileUA} from '../../utils/common';
24
+ import hexadecimalTransparency from '../../utils/hexadecimalTransparency';
5
25
 
6
26
  const UserPrompt = () => {
7
- const {prompt, setPrompt, agentConnectionState} = useContext(AgentContext);
27
+ const isDesktop = useIsDesktop()('popup');
28
+ const [isModalOpen, setModalOpen] = useState(false);
29
+ const {prompt, setPrompt, agentConnectionState, agentId} =
30
+ useContext(AgentContext);
31
+ const {
32
+ data: {agents},
33
+ } = useRoomInfo();
34
+
35
+ const [localPrompt, setLocalPrompt] = useState('');
36
+
37
+ useEffect(() => {
38
+ setLocalPrompt(prompt);
39
+ }, [prompt]);
40
+
41
+ useEffect(() => {
42
+ if (agentId) {
43
+ setPrompt(agents?.find(a => a?.id === agentId)?.config?.llm?.prompt);
44
+ }
45
+ }, [agentId, agents, setPrompt]);
8
46
  return (
9
47
  <>
10
48
  <Text style={styles.label}>Prompt</Text>
11
49
  <View style={styles.container}>
12
50
  <TextInput
13
- aria-disabled={
14
- agentConnectionState === 'AGENT_CONNECTED' ? true : false
15
- }
16
- style={[
17
- styles.input,
18
- agentConnectionState === 'AGENT_CONNECTED' ? {opacity: 0.4} : {},
19
- ]}
51
+ aria-disabled={true}
52
+ style={[styles.input, {opacity: 0.4}]}
20
53
  value={prompt}
21
54
  onChangeText={setPrompt}
22
55
  placeholder="Customize Prompt"
@@ -24,11 +57,127 @@ const UserPrompt = () => {
24
57
  multiline={true}
25
58
  />
26
59
  </View>
60
+ <TouchableOpacity
61
+ style={[
62
+ styles.promptBtnContainer,
63
+ agentConnectionState === 'AGENT_CONNECTED'
64
+ ? styles.promptBtnContainerDisabled
65
+ : {},
66
+ ]}
67
+ onPress={() => setModalOpen(true)}>
68
+ <Text style={styles.promptBtnText}>Customize Prompt</Text>
69
+ </TouchableOpacity>
70
+ <PromptModal
71
+ modalVisible={isModalOpen}
72
+ setModalVisible={setModalOpen}
73
+ showCloseIcon={true}
74
+ title={'Customize Prompt'}
75
+ cancelable={false}
76
+ contentContainerStyle={modalStyles.mContainer}>
77
+ <View style={modalStyles.mbody}>
78
+ <View style={[styles.container, {flex: 3}]}>
79
+ <TextInput
80
+ style={[styles.input]}
81
+ value={localPrompt}
82
+ onChangeText={setLocalPrompt}
83
+ placeholder="Customize Prompt"
84
+ numberOfLines={45}
85
+ multiline={true}
86
+ />
87
+ </View>
88
+ <View style={styles.infoTextContainer}>
89
+ <Text style={styles.infoText}>
90
+ Fine-tune your AI agent’s behavior by editing its prompt.
91
+ </Text>
92
+ </View>
93
+ <View
94
+ style={[
95
+ isDesktop ? styles.btnContainer : styles.btnContainerMobile,
96
+ isMobileUA() ? {flex: 1} : {flex: 0.5},
97
+ ]}>
98
+ <View style={isDesktop && {flex: 1}}>
99
+ <TertiaryButton
100
+ containerStyle={{
101
+ width: '100%',
102
+ height: 48,
103
+ paddingVertical: 12,
104
+ paddingHorizontal: 12,
105
+ borderRadius: ThemeConfig.BorderRadius.medium,
106
+ }}
107
+ textStyle={styles.btnText}
108
+ text={'CANCEL'}
109
+ onPress={() => {
110
+ setLocalPrompt(prompt);
111
+ setModalOpen(false);
112
+ }}
113
+ />
114
+ </View>
115
+ <Spacer
116
+ size={isDesktop ? 40 : 20}
117
+ horizontal={isDesktop ? true : false}
118
+ />
119
+ <View style={isDesktop && {flex: 1}}>
120
+ <PrimaryButton
121
+ containerStyle={{
122
+ minWidth: 'auto',
123
+ width: '100%',
124
+ borderRadius: ThemeConfig.BorderRadius.medium,
125
+ height: 48,
126
+ backgroundColor: $config.PRIMARY_ACTION_BRAND_COLOR,
127
+ paddingVertical: 12,
128
+ paddingHorizontal: 12,
129
+ }}
130
+ textStyle={styles.btnText}
131
+ text={'UPDATE'}
132
+ onPress={() => {
133
+ setPrompt(localPrompt);
134
+ setModalOpen(false);
135
+ }}
136
+ />
137
+ </View>
138
+ </View>
139
+ </View>
140
+ </PromptModal>
27
141
  </>
28
142
  );
29
143
  };
30
144
 
31
145
  const styles = StyleSheet.create({
146
+ infoTextContainer: {
147
+ padding: 8,
148
+ },
149
+ infoText: {
150
+ color: $config.FONT_COLOR,
151
+ fontFamily: ThemeConfig.FontFamily.sansPro,
152
+ fontSize: 12,
153
+ lineHeight: 12,
154
+ },
155
+ promptBtnContainer: {
156
+ padding: 8,
157
+ },
158
+ promptBtnContainerDisabled: {
159
+ opacity: 0.4,
160
+ },
161
+ btnContainer: {
162
+ flex: 1,
163
+ flexDirection: 'row',
164
+ justifyContent: 'center',
165
+ alignItems: 'center',
166
+ },
167
+ btnContainerMobile: {
168
+ flexDirection: 'column-reverse',
169
+ },
170
+ promptBtnText: {
171
+ color: $config.PRIMARY_ACTION_BRAND_COLOR,
172
+ fontFamily: ThemeConfig.FontFamily.sansPro,
173
+ fontSize: 14,
174
+ lineHeight: 14,
175
+ },
176
+ btnText: {
177
+ fontWeight: '600',
178
+ fontSize: 16,
179
+ lineHeight: 24,
180
+ },
32
181
  container: {
33
182
  flex: 1,
34
183
  justifyContent: 'center',
@@ -61,4 +210,158 @@ const styles = StyleSheet.create({
61
210
  },
62
211
  });
63
212
 
213
+ interface PromptModalProps extends ModalProps {
214
+ title?: string;
215
+ subtitle?: string;
216
+ modalVisible: boolean;
217
+ setModalVisible: React.Dispatch<SetStateAction<boolean>>;
218
+ showCloseIcon?: boolean;
219
+ children: React.ReactNode;
220
+ contentContainerStyle?: ViewStyle;
221
+ containerStyle?: ViewStyle;
222
+ cancelable?: boolean;
223
+ }
224
+ const PromptModal = (props: PromptModalProps) => {
225
+ const {
226
+ title,
227
+ modalVisible,
228
+ setModalVisible,
229
+ children,
230
+ cancelable = false,
231
+ } = props;
232
+
233
+ const isDesktop = useIsDesktop()('popup');
234
+
235
+ return (
236
+ <Modal
237
+ animationType="none"
238
+ transparent={true}
239
+ visible={modalVisible}
240
+ onRequestClose={() => {
241
+ setModalVisible(false);
242
+ }}>
243
+ <View
244
+ style={[modalStyles.centeredView, isDesktop && {alignItems: 'center'}]}>
245
+ <TouchableWithoutFeedback
246
+ onPress={() => {
247
+ cancelable && setModalVisible(false);
248
+ }}>
249
+ <View style={modalStyles.backDrop} />
250
+ </TouchableWithoutFeedback>
251
+ <View style={[modalStyles.modalView, props?.contentContainerStyle]}>
252
+ <View style={modalStyles.header}>
253
+ <Text style={modalStyles.title}>{title}</Text>
254
+ <View>
255
+ <IconButton
256
+ hoverEffect={true}
257
+ hoverEffectStyle={{
258
+ backgroundColor: $config.ICON_BG_COLOR,
259
+ borderRadius: 20,
260
+ }}
261
+ iconProps={{
262
+ iconType: 'plain',
263
+ iconContainerStyle: {
264
+ padding: isMobileUA() ? 0 : 5,
265
+ },
266
+ name: 'close',
267
+ tintColor: $config.SECONDARY_ACTION_COLOR,
268
+ }}
269
+ onPress={() => {
270
+ setModalVisible(false);
271
+ }}
272
+ />
273
+ </View>
274
+ </View>
275
+ {children}
276
+ </View>
277
+ </View>
278
+ </Modal>
279
+ );
280
+ };
281
+
282
+ const modalStyles = StyleSheet.create({
283
+ mContainer: {
284
+ display: 'flex',
285
+ flexDirection: 'column',
286
+ alignItems: 'flex-start',
287
+ flexShrink: 0,
288
+ // width: 620,
289
+ width: '100%',
290
+ maxWidth: 680,
291
+ minWidth: 340,
292
+ height: 620,
293
+ maxHeight: 620,
294
+ borderRadius: 4,
295
+ zIndex: 2,
296
+ },
297
+ mHeader: {
298
+ display: 'flex',
299
+ flexDirection: 'row',
300
+ width: '100%',
301
+ height: 60,
302
+ paddingHorizontal: 20,
303
+ paddingVertical: 12,
304
+ alignItems: 'center',
305
+ gap: 20,
306
+ flexShrink: 0,
307
+ borderWidth: 1,
308
+ borderColor: $config.CARD_LAYER_3_COLOR,
309
+ backgroundColor: $config.CARD_LAYER_1_COLOR,
310
+ },
311
+ mbody: {
312
+ width: '100%',
313
+ flex: 1,
314
+ padding: 10,
315
+ justifyContent: 'space-around',
316
+ },
317
+ centeredView: {
318
+ flex: 1,
319
+ position: 'relative',
320
+ justifyContent: 'center',
321
+ alignItems: 'center',
322
+ paddingHorizontal: 20,
323
+ },
324
+ modalView: {
325
+ backgroundColor: $config.CARD_LAYER_1_COLOR,
326
+ borderWidth: 1,
327
+ borderColor: $config.CARD_LAYER_3_COLOR,
328
+ borderRadius: ThemeConfig.BorderRadius.large,
329
+ shadowColor: $config.HARD_CODED_BLACK_COLOR,
330
+ shadowOffset: {
331
+ width: 0,
332
+ height: 2,
333
+ },
334
+ shadowOpacity: 0.1,
335
+ shadowRadius: 4,
336
+ elevation: 5,
337
+ maxWidth: 650,
338
+ },
339
+ backDrop: {
340
+ zIndex: 1,
341
+ position: 'absolute',
342
+ top: 0,
343
+ bottom: 0,
344
+ left: 0,
345
+ right: 0,
346
+ backgroundColor:
347
+ $config.HARD_CODED_BLACK_COLOR + hexadecimalTransparency['60%'],
348
+ },
349
+ header: {
350
+ flexDirection: 'row',
351
+ justifyContent: 'space-between',
352
+ alignItems: 'flex-start',
353
+ paddingVertical: 12,
354
+ paddingHorizontal: 20,
355
+ width: '100%',
356
+ },
357
+ title: {
358
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
359
+ fontFamily: ThemeConfig.FontFamily.sansPro,
360
+ fontSize: ThemeConfig.FontSize.xLarge,
361
+ lineHeight: 32,
362
+ fontWeight: '500',
363
+ alignSelf: 'center',
364
+ },
365
+ });
366
+
64
367
  export default UserPrompt;
@@ -25,9 +25,7 @@ const ChatItemBubble = ({item}: {item: ChatItem}) => {
25
25
  paddingTop: 2,
26
26
  marginTop: 0,
27
27
  },
28
- bubbleStyleLayer2: {
29
- padding: 0,
30
- },
28
+ bubbleStyleLayer2: {},
31
29
  }}
32
30
  />
33
31
  );
@@ -74,6 +74,7 @@ export interface AIAgentInterface {
74
74
  llm: {
75
75
  agent_name: string;
76
76
  model: string;
77
+ prompt: string;
77
78
  };
78
79
  tts: {
79
80
  vendor: string;
@@ -52,6 +52,7 @@ const JOIN_CHANNEL_PHRASE_AND_GET_USER = gql`
52
52
  llm {
53
53
  agent_name
54
54
  model
55
+ prompt
55
56
  }
56
57
  tts {
57
58
  vendor
@@ -115,6 +116,7 @@ const JOIN_CHANNEL_PHRASE = gql`
115
116
  llm {
116
117
  agent_name
117
118
  model
119
+ prompt
118
120
  }
119
121
  tts {
120
122
  vendor