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.
- package/package.json +1 -1
- package/template/customization-api/customize.tsx +11 -0
- package/template/src/ai-agent/assets/join-call.png +0 -0
- package/template/src/ai-agent/assets/leave-call.png +0 -0
- package/template/src/ai-agent/components/AgentControls/AgentConnectionWrapper.tsx +367 -0
- package/template/src/ai-agent/components/AgentControls/AgentContext.tsx +7 -0
- package/template/src/ai-agent/components/AgentControls/index.tsx +44 -258
- package/template/src/ai-agent/components/Bottombar.tsx +3 -5
- package/template/src/ai-agent/components/CustomChatPanel.tsx +1 -115
- package/template/src/ai-agent/components/CustomCreate.tsx +1 -1
- package/template/src/ai-agent/components/CustomSettingsPanel.tsx +43 -5
- package/template/src/ai-agent/components/SelectAiAgent.tsx +15 -12
- package/template/src/ai-agent/components/UserPrompt.tsx +75 -0
- package/template/src/ai-agent/components/agent-chat-panel/agent-chat-ui.tsx +13 -13
- package/template/src/ai-agent/components/mobile/Bottombar.tsx +55 -39
- package/template/src/ai-agent/components/mobile/Topbar.tsx +1 -2
- package/template/src/ai-agent/components/utils.ts +17 -0
- package/template/src/ai-agent/index.tsx +18 -12
- package/template/src/auth/AuthProvider.tsx +19 -0
- package/template/src/components/room-info/useRoomInfo.tsx +16 -4
- package/template/src/utils/useJoinRoom.ts +36 -8
- package/template/src/ai-agent/components/AgentControls/LeaveCall.png +0 -0
- package/template/src/ai-agent/components/AgentControls/Vector.svg +0 -3
- package/template/src/ai-agent/components/icons.tsx +0 -227
package/package.json
CHANGED
|
@@ -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;
|
|
Binary file
|
|
Binary file
|
|
@@ -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 (
|