@hivegpt/hiveai-angular 0.0.576 → 0.0.578
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/bundles/hivegpt-hiveai-angular.umd.js +214 -356
- package/bundles/hivegpt-hiveai-angular.umd.js.map +1 -1
- package/bundles/hivegpt-hiveai-angular.umd.min.js +1 -1
- package/bundles/hivegpt-hiveai-angular.umd.min.js.map +1 -1
- package/esm2015/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.js +54 -85
- package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +14 -22
- package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +60 -90
- package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +34 -81
- package/fesm2015/hivegpt-hiveai-angular.js +157 -272
- package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
- package/hivegpt-hiveai-angular.metadata.json +1 -1
- package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts +1 -7
- package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts.map +1 -1
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts +3 -5
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts.map +1 -1
- package/lib/components/voice-agent/services/voice-agent.service.d.ts +1 -3
- package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts +12 -5
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -813,7 +813,6 @@ AudioAnalyzerService.decorators = [
|
|
|
813
813
|
* - Emit roomCreated$, userTranscript$, botTranscript$
|
|
814
814
|
* - NO audio logic, NO mic logic. Audio is handled by Daily.js (WebRTC).
|
|
815
815
|
*/
|
|
816
|
-
const WS_CONNECT_TIMEOUT_MS = 10000;
|
|
817
816
|
class WebSocketVoiceClientService {
|
|
818
817
|
constructor() {
|
|
819
818
|
this.ws = null;
|
|
@@ -827,100 +826,54 @@ class WebSocketVoiceClientService {
|
|
|
827
826
|
/** Emits bot transcript updates. */
|
|
828
827
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
829
828
|
}
|
|
830
|
-
/**
|
|
831
|
-
* Connect to signaling WebSocket. No audio over this connection.
|
|
832
|
-
* Resolves when the socket is open; rejects if the connection fails.
|
|
833
|
-
*/
|
|
829
|
+
/** Connect to signaling WebSocket. No audio over this connection. */
|
|
834
830
|
connect(wsUrl) {
|
|
835
831
|
var _a;
|
|
836
832
|
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
837
|
-
return
|
|
833
|
+
return;
|
|
838
834
|
}
|
|
839
835
|
if (this.ws) {
|
|
840
836
|
this.ws.close();
|
|
841
837
|
this.ws = null;
|
|
842
838
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
839
|
+
try {
|
|
840
|
+
this.ws = new WebSocket(wsUrl);
|
|
841
|
+
this.ws.onmessage = (event) => {
|
|
846
842
|
var _a;
|
|
847
|
-
if (settled)
|
|
848
|
-
return;
|
|
849
|
-
settled = true;
|
|
850
843
|
try {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
this.ws = null;
|
|
857
|
-
reject(new Error('WebSocket connection timed out'));
|
|
858
|
-
}, WS_CONNECT_TIMEOUT_MS);
|
|
859
|
-
const clear = () => {
|
|
860
|
-
clearTimeout(timeout);
|
|
861
|
-
};
|
|
862
|
-
try {
|
|
863
|
-
const ws = new WebSocket(wsUrl);
|
|
864
|
-
this.ws = ws;
|
|
865
|
-
ws.onopen = () => {
|
|
866
|
-
if (settled)
|
|
867
|
-
return;
|
|
868
|
-
settled = true;
|
|
869
|
-
clear();
|
|
870
|
-
resolve();
|
|
871
|
-
};
|
|
872
|
-
ws.onmessage = (event) => {
|
|
873
|
-
var _a;
|
|
874
|
-
try {
|
|
875
|
-
const msg = JSON.parse(event.data);
|
|
876
|
-
if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'room_created') {
|
|
877
|
-
const roomUrl = ((_a = msg.room_url) !== null && _a !== void 0 ? _a : msg.roomUrl);
|
|
878
|
-
if (typeof roomUrl === 'string') {
|
|
879
|
-
this.roomCreatedSubject.next(roomUrl);
|
|
880
|
-
}
|
|
844
|
+
const msg = JSON.parse(event.data);
|
|
845
|
+
if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'room_created') {
|
|
846
|
+
const roomUrl = ((_a = msg.room_url) !== null && _a !== void 0 ? _a : msg.roomUrl);
|
|
847
|
+
if (typeof roomUrl === 'string') {
|
|
848
|
+
this.roomCreatedSubject.next(roomUrl);
|
|
881
849
|
}
|
|
882
|
-
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'user_transcript' && typeof msg.text === 'string') {
|
|
883
|
-
this.userTranscriptSubject.next({
|
|
884
|
-
text: msg.text,
|
|
885
|
-
final: msg.final === true,
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'bot_transcript' && typeof msg.text === 'string') {
|
|
889
|
-
this.botTranscriptSubject.next(msg.text);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
catch (_b) {
|
|
893
|
-
// Ignore non-JSON or unknown messages
|
|
894
850
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
this.disconnect();
|
|
901
|
-
reject(new Error('WebSocket connection failed'));
|
|
902
|
-
return;
|
|
851
|
+
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'user_transcript' && typeof msg.text === 'string') {
|
|
852
|
+
this.userTranscriptSubject.next({
|
|
853
|
+
text: msg.text,
|
|
854
|
+
final: msg.final === true,
|
|
855
|
+
});
|
|
903
856
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
console.warn('WebSocketVoiceClient: onerror after open (not forcing disconnect)');
|
|
907
|
-
};
|
|
908
|
-
ws.onclose = () => {
|
|
909
|
-
this.ws = null;
|
|
910
|
-
if (!settled) {
|
|
911
|
-
settled = true;
|
|
912
|
-
clear();
|
|
913
|
-
reject(new Error('WebSocket connection failed'));
|
|
857
|
+
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'bot_transcript' && typeof msg.text === 'string') {
|
|
858
|
+
this.botTranscriptSubject.next(msg.text);
|
|
914
859
|
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
860
|
+
}
|
|
861
|
+
catch (_b) {
|
|
862
|
+
// Ignore non-JSON or unknown messages
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
this.ws.onerror = () => {
|
|
866
|
+
this.disconnect();
|
|
867
|
+
};
|
|
868
|
+
this.ws.onclose = () => {
|
|
920
869
|
this.ws = null;
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
console.error('WebSocketVoiceClient: connect failed', err);
|
|
874
|
+
this.ws = null;
|
|
875
|
+
throw err;
|
|
876
|
+
}
|
|
924
877
|
}
|
|
925
878
|
/** Disconnect and cleanup. */
|
|
926
879
|
disconnect() {
|
|
@@ -963,8 +916,7 @@ class DailyVoiceClientService {
|
|
|
963
916
|
this.remoteAudioElement = null;
|
|
964
917
|
/** AnalyserNode-based remote audio monitor for instant bot speaking detection. */
|
|
965
918
|
this.remoteAudioContext = null;
|
|
966
|
-
|
|
967
|
-
this.remoteSpeakingPollId = null;
|
|
919
|
+
this.remoteSpeakingRAF = null;
|
|
968
920
|
this.speakingSubject = new BehaviorSubject(false);
|
|
969
921
|
this.userSpeakingSubject = new BehaviorSubject(false);
|
|
970
922
|
this.micMutedSubject = new BehaviorSubject(false);
|
|
@@ -982,18 +934,15 @@ class DailyVoiceClientService {
|
|
|
982
934
|
* Connect to Daily room. Acquires mic first for waveform, then joins with audio.
|
|
983
935
|
* @param roomUrl Daily room URL (from room_created)
|
|
984
936
|
* @param token Optional meeting token
|
|
985
|
-
* @param existingStream Optional pre-acquired mic (avoids a second getUserMedia / extra prompts on some browsers)
|
|
986
937
|
*/
|
|
987
|
-
connect(roomUrl, token
|
|
938
|
+
connect(roomUrl, token) {
|
|
988
939
|
return __awaiter(this, void 0, void 0, function* () {
|
|
989
940
|
if (this.callObject) {
|
|
990
941
|
yield this.disconnect();
|
|
991
942
|
}
|
|
992
943
|
try {
|
|
993
|
-
|
|
994
|
-
const stream =
|
|
995
|
-
? existingStream
|
|
996
|
-
: yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
944
|
+
// Get mic stream for both Daily and waveform (single capture)
|
|
945
|
+
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
997
946
|
const audioTrack = stream.getAudioTracks()[0];
|
|
998
947
|
if (!audioTrack) {
|
|
999
948
|
stream.getTracks().forEach((t) => t.stop());
|
|
@@ -1125,7 +1074,7 @@ class DailyVoiceClientService {
|
|
|
1125
1074
|
}
|
|
1126
1075
|
/**
|
|
1127
1076
|
* Monitor remote audio track energy via AnalyserNode.
|
|
1128
|
-
* Polls at ~
|
|
1077
|
+
* Polls at ~60fps and flips speakingSubject based on actual audio energy.
|
|
1129
1078
|
*/
|
|
1130
1079
|
monitorRemoteAudio(track) {
|
|
1131
1080
|
this.stopRemoteAudioMonitor();
|
|
@@ -1139,17 +1088,11 @@ class DailyVoiceClientService {
|
|
|
1139
1088
|
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1140
1089
|
const THRESHOLD = 5;
|
|
1141
1090
|
const SILENCE_MS = 1500;
|
|
1142
|
-
const POLL_MS = 100;
|
|
1143
1091
|
let lastSoundTime = 0;
|
|
1144
1092
|
let isSpeaking = false;
|
|
1145
|
-
|
|
1146
|
-
if (!this.remoteAudioContext)
|
|
1147
|
-
if (this.remoteSpeakingPollId) {
|
|
1148
|
-
clearInterval(this.remoteSpeakingPollId);
|
|
1149
|
-
this.remoteSpeakingPollId = null;
|
|
1150
|
-
}
|
|
1093
|
+
const poll = () => {
|
|
1094
|
+
if (!this.remoteAudioContext)
|
|
1151
1095
|
return;
|
|
1152
|
-
}
|
|
1153
1096
|
analyser.getByteFrequencyData(dataArray);
|
|
1154
1097
|
let sum = 0;
|
|
1155
1098
|
for (let i = 0; i < dataArray.length; i++) {
|
|
@@ -1173,16 +1116,18 @@ class DailyVoiceClientService {
|
|
|
1173
1116
|
console.log(`[VoiceDebug] Bot audio silence detected (speaking=false) — ${new Date().toISOString()}`);
|
|
1174
1117
|
this.ngZone.run(() => this.speakingSubject.next(false));
|
|
1175
1118
|
}
|
|
1176
|
-
|
|
1119
|
+
this.remoteSpeakingRAF = requestAnimationFrame(poll);
|
|
1120
|
+
};
|
|
1121
|
+
this.remoteSpeakingRAF = requestAnimationFrame(poll);
|
|
1177
1122
|
}
|
|
1178
1123
|
catch (err) {
|
|
1179
1124
|
console.warn('DailyVoiceClient: failed to create remote audio monitor', err);
|
|
1180
1125
|
}
|
|
1181
1126
|
}
|
|
1182
1127
|
stopRemoteAudioMonitor() {
|
|
1183
|
-
if (this.
|
|
1184
|
-
|
|
1185
|
-
this.
|
|
1128
|
+
if (this.remoteSpeakingRAF) {
|
|
1129
|
+
cancelAnimationFrame(this.remoteSpeakingRAF);
|
|
1130
|
+
this.remoteSpeakingRAF = null;
|
|
1186
1131
|
}
|
|
1187
1132
|
if (this.remoteAudioContext) {
|
|
1188
1133
|
this.remoteAudioContext.close().catch(() => { });
|
|
@@ -1281,8 +1226,6 @@ class VoiceAgentService {
|
|
|
1281
1226
|
this.callStartTime = 0;
|
|
1282
1227
|
this.durationInterval = null;
|
|
1283
1228
|
this.subscriptions = new Subscription();
|
|
1284
|
-
/** Per-call only; cleared on disconnect / reset / new room so handlers do not stack. */
|
|
1285
|
-
this.callSubscriptions = new Subscription();
|
|
1286
1229
|
this.destroy$ = new Subject();
|
|
1287
1230
|
this.callState$ = this.callStateSubject.asObservable();
|
|
1288
1231
|
this.statusText$ = this.statusTextSubject.asObservable();
|
|
@@ -1294,15 +1237,6 @@ class VoiceAgentService {
|
|
|
1294
1237
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
1295
1238
|
// Waveform visualization only - do NOT use for speaking state
|
|
1296
1239
|
this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe((levels) => this.audioLevelsSubject.next(levels)));
|
|
1297
|
-
// Transcripts: single subscription for service lifetime (avoid stacking on each connect()).
|
|
1298
|
-
// WebSocket is disconnected between calls; no replay — new subscribers (setupVoiceTranscripts)
|
|
1299
|
-
// only receive messages from the new WS after connect.
|
|
1300
|
-
this.subscriptions.add(this.wsClient.userTranscript$
|
|
1301
|
-
.pipe(takeUntil(this.destroy$))
|
|
1302
|
-
.subscribe((t) => this.userTranscriptSubject.next(t)));
|
|
1303
|
-
this.subscriptions.add(this.wsClient.botTranscript$
|
|
1304
|
-
.pipe(takeUntil(this.destroy$))
|
|
1305
|
-
.subscribe((t) => this.botTranscriptSubject.next(t)));
|
|
1306
1240
|
}
|
|
1307
1241
|
ngOnDestroy() {
|
|
1308
1242
|
this.destroy$.next();
|
|
@@ -1313,8 +1247,6 @@ class VoiceAgentService {
|
|
|
1313
1247
|
resetToIdle() {
|
|
1314
1248
|
if (this.callStateSubject.value === 'idle')
|
|
1315
1249
|
return;
|
|
1316
|
-
this.callSubscriptions.unsubscribe();
|
|
1317
|
-
this.callSubscriptions = new Subscription();
|
|
1318
1250
|
this.stopDurationTimer();
|
|
1319
1251
|
this.audioAnalyzer.stop();
|
|
1320
1252
|
this.wsClient.disconnect();
|
|
@@ -1324,8 +1256,7 @@ class VoiceAgentService {
|
|
|
1324
1256
|
this.statusTextSubject.next('');
|
|
1325
1257
|
this.durationSubject.next('0:00');
|
|
1326
1258
|
}
|
|
1327
|
-
connect(apiUrl, token, botId, conversationId, apiKey, eventToken, eventId, eventUrl, domainAuthority, usersApiUrl
|
|
1328
|
-
var _a;
|
|
1259
|
+
connect(apiUrl, token, botId, conversationId, apiKey, eventToken, eventId, eventUrl, domainAuthority, usersApiUrl) {
|
|
1329
1260
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1330
1261
|
if (this.callStateSubject.value !== 'idle') {
|
|
1331
1262
|
console.warn('Call already in progress');
|
|
@@ -1334,41 +1265,26 @@ class VoiceAgentService {
|
|
|
1334
1265
|
try {
|
|
1335
1266
|
this.callStateSubject.next('connecting');
|
|
1336
1267
|
this.statusTextSubject.next('Connecting...');
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1268
|
+
let accessToken = token;
|
|
1269
|
+
// Align with chat drawer token handling: always delegate to
|
|
1270
|
+
// PlatformTokenRefreshService when we have a usersApiUrl, so it can
|
|
1271
|
+
// fall back to stored tokens even if the caller passed an empty token.
|
|
1272
|
+
if (usersApiUrl && isPlatformBrowser(this.platformId)) {
|
|
1273
|
+
try {
|
|
1274
|
+
const ensured = yield this.platformTokenRefresh
|
|
1275
|
+
.ensureValidAccessToken(token, usersApiUrl)
|
|
1276
|
+
.pipe(take(1))
|
|
1277
|
+
.toPromise();
|
|
1278
|
+
if (ensured === null || ensured === void 0 ? void 0 : ensured.accessToken) {
|
|
1279
|
+
accessToken = ensured.accessToken;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
catch (e) {
|
|
1344
1283
|
console.warn('[HiveGpt Voice] Token refresh before connect failed', e);
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
const
|
|
1349
|
-
const baseUrl = apiUrl.replace(/\/$/, '');
|
|
1350
|
-
return {
|
|
1351
|
-
postUrl: `${baseUrl}/ai/ask-voice`,
|
|
1352
|
-
body: JSON.stringify({
|
|
1353
|
-
bot_id: botId,
|
|
1354
|
-
conversation_id: conversationId,
|
|
1355
|
-
voice: 'alloy',
|
|
1356
|
-
}),
|
|
1357
|
-
};
|
|
1358
|
-
});
|
|
1359
|
-
const micPromise = (existingMicStream === null || existingMicStream === void 0 ? void 0 : existingMicStream.getAudioTracks().some((t) => t.readyState === 'live'))
|
|
1360
|
-
? Promise.resolve(existingMicStream)
|
|
1361
|
-
: isPlatformBrowser(this.platformId) &&
|
|
1362
|
-
((_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia)
|
|
1363
|
-
? navigator.mediaDevices
|
|
1364
|
-
.getUserMedia({ audio: true })
|
|
1365
|
-
.catch(() => undefined)
|
|
1366
|
-
: Promise.resolve(undefined);
|
|
1367
|
-
const [accessToken, { postUrl, body }, micStream] = yield Promise.all([
|
|
1368
|
-
tokenPromise,
|
|
1369
|
-
prepPromise,
|
|
1370
|
-
micPromise,
|
|
1371
|
-
]);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const baseUrl = apiUrl.replace(/\/$/, '');
|
|
1287
|
+
const postUrl = `${baseUrl}/ai/ask-voice`;
|
|
1372
1288
|
const headers = {
|
|
1373
1289
|
'Content-Type': 'application/json',
|
|
1374
1290
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -1384,7 +1300,11 @@ class VoiceAgentService {
|
|
|
1384
1300
|
const res = yield fetch(postUrl, {
|
|
1385
1301
|
method: 'POST',
|
|
1386
1302
|
headers,
|
|
1387
|
-
body
|
|
1303
|
+
body: JSON.stringify({
|
|
1304
|
+
bot_id: botId,
|
|
1305
|
+
conversation_id: conversationId,
|
|
1306
|
+
voice: 'alloy',
|
|
1307
|
+
}),
|
|
1388
1308
|
});
|
|
1389
1309
|
if (!res.ok) {
|
|
1390
1310
|
throw new Error(`HTTP ${res.status}`);
|
|
@@ -1394,31 +1314,30 @@ class VoiceAgentService {
|
|
|
1394
1314
|
if (!wsUrl || typeof wsUrl !== 'string') {
|
|
1395
1315
|
throw new Error('No ws_url in response');
|
|
1396
1316
|
}
|
|
1397
|
-
// Subscribe
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1317
|
+
// Subscribe to room_created BEFORE connecting to avoid race
|
|
1318
|
+
this.wsClient.roomCreated$
|
|
1319
|
+
.pipe(take(1), takeUntil(this.destroy$))
|
|
1320
|
+
.subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
|
|
1321
|
+
try {
|
|
1322
|
+
yield this.onRoomCreated(roomUrl);
|
|
1323
|
+
}
|
|
1324
|
+
catch (err) {
|
|
1325
|
+
console.error('Daily join failed:', err);
|
|
1326
|
+
this.callStateSubject.next('ended');
|
|
1327
|
+
this.statusTextSubject.next('Connection failed');
|
|
1328
|
+
yield this.disconnect();
|
|
1329
|
+
throw err;
|
|
1330
|
+
}
|
|
1331
|
+
}));
|
|
1332
|
+
// Forward transcripts from WebSocket
|
|
1333
|
+
this.subscriptions.add(this.wsClient.userTranscript$
|
|
1334
|
+
.pipe(takeUntil(this.destroy$))
|
|
1335
|
+
.subscribe((t) => this.userTranscriptSubject.next(t)));
|
|
1336
|
+
this.subscriptions.add(this.wsClient.botTranscript$
|
|
1337
|
+
.pipe(takeUntil(this.destroy$))
|
|
1338
|
+
.subscribe((t) => this.botTranscriptSubject.next(t)));
|
|
1339
|
+
// Connect signaling WebSocket (no audio over WS)
|
|
1340
|
+
this.wsClient.connect(wsUrl);
|
|
1422
1341
|
}
|
|
1423
1342
|
catch (error) {
|
|
1424
1343
|
console.error('Error connecting voice agent:', error);
|
|
@@ -1429,19 +1348,18 @@ class VoiceAgentService {
|
|
|
1429
1348
|
}
|
|
1430
1349
|
});
|
|
1431
1350
|
}
|
|
1432
|
-
onRoomCreated(roomUrl
|
|
1351
|
+
onRoomCreated(roomUrl) {
|
|
1433
1352
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1434
|
-
|
|
1435
|
-
this.
|
|
1436
|
-
this.callSubscriptions = new Subscription();
|
|
1353
|
+
// Connect Daily.js for WebRTC audio
|
|
1354
|
+
yield this.dailyClient.connect(roomUrl);
|
|
1437
1355
|
// Waveform: use local mic stream from Daily client
|
|
1438
|
-
this.
|
|
1356
|
+
this.dailyClient.localStream$
|
|
1439
1357
|
.pipe(filter((s) => s != null), take(1))
|
|
1440
1358
|
.subscribe((stream) => {
|
|
1441
1359
|
this.audioAnalyzer.start(stream);
|
|
1442
|
-
})
|
|
1443
|
-
this.
|
|
1444
|
-
this.
|
|
1360
|
+
});
|
|
1361
|
+
this.subscriptions.add(this.dailyClient.userSpeaking$.subscribe((s) => this.isUserSpeakingSubject.next(s)));
|
|
1362
|
+
this.subscriptions.add(combineLatest([
|
|
1445
1363
|
this.dailyClient.speaking$,
|
|
1446
1364
|
this.dailyClient.userSpeaking$,
|
|
1447
1365
|
]).subscribe(([bot, user]) => {
|
|
@@ -1461,19 +1379,16 @@ class VoiceAgentService {
|
|
|
1461
1379
|
else if (bot) {
|
|
1462
1380
|
this.callStateSubject.next('talking');
|
|
1463
1381
|
}
|
|
1464
|
-
else {
|
|
1465
|
-
|
|
1466
|
-
this.callStateSubject.next('listening');
|
|
1382
|
+
else if (current === 'talking' || current === 'listening') {
|
|
1383
|
+
this.callStateSubject.next('connected');
|
|
1467
1384
|
}
|
|
1468
1385
|
}));
|
|
1469
|
-
this.
|
|
1386
|
+
this.subscriptions.add(this.dailyClient.micMuted$.subscribe((muted) => this.isMicMutedSubject.next(muted)));
|
|
1470
1387
|
this.statusTextSubject.next('Connecting...');
|
|
1471
1388
|
});
|
|
1472
1389
|
}
|
|
1473
1390
|
disconnect() {
|
|
1474
1391
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1475
|
-
this.callSubscriptions.unsubscribe();
|
|
1476
|
-
this.callSubscriptions = new Subscription();
|
|
1477
1392
|
this.stopDurationTimer();
|
|
1478
1393
|
this.audioAnalyzer.stop();
|
|
1479
1394
|
// Daily first, then WebSocket
|
|
@@ -1524,11 +1439,10 @@ const VOICE_MODAL_CONFIG = new InjectionToken('VOICE_MODAL_CONFIG');
|
|
|
1524
1439
|
const VOICE_MODAL_CLOSE_CALLBACK = new InjectionToken('VOICE_MODAL_CLOSE_CALLBACK');
|
|
1525
1440
|
|
|
1526
1441
|
class VoiceAgentModalComponent {
|
|
1527
|
-
constructor(voiceAgentService, audioAnalyzer, injector
|
|
1442
|
+
constructor(voiceAgentService, audioAnalyzer, injector) {
|
|
1528
1443
|
this.voiceAgentService = voiceAgentService;
|
|
1529
1444
|
this.audioAnalyzer = audioAnalyzer;
|
|
1530
1445
|
this.injector = injector;
|
|
1531
|
-
this.platformId = platformId;
|
|
1532
1446
|
this.close = new EventEmitter();
|
|
1533
1447
|
this.apiKey = '';
|
|
1534
1448
|
this.eventToken = '';
|
|
@@ -1540,8 +1454,6 @@ class VoiceAgentModalComponent {
|
|
|
1540
1454
|
this.usersApiUrl = '';
|
|
1541
1455
|
this.injectedConfig = null;
|
|
1542
1456
|
this.onCloseCallback = null;
|
|
1543
|
-
/** Held until destroy; passed to Daily so we do not stop/re-acquire (avoids extra prompts on some browsers). */
|
|
1544
|
-
this.warmMicStream = null;
|
|
1545
1457
|
/** Hardcoded voice agent avatar (Nia). */
|
|
1546
1458
|
this.displayAvatarUrl = 'https://www.jotform.com/uploads/mehmetkarakasli/form_files/1564593667676a8e85f23758.86945537_icon.png';
|
|
1547
1459
|
this.callState = 'idle';
|
|
@@ -1557,83 +1469,58 @@ class VoiceAgentModalComponent {
|
|
|
1557
1469
|
this.isConnecting = false;
|
|
1558
1470
|
}
|
|
1559
1471
|
ngOnInit() {
|
|
1560
|
-
void this.bootstrap();
|
|
1561
|
-
}
|
|
1562
|
-
bootstrap() {
|
|
1563
1472
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1473
|
+
// When opened via Overlay, config is provided by injection
|
|
1474
|
+
this.injectedConfig = this.injector.get(VOICE_MODAL_CONFIG, null);
|
|
1475
|
+
this.onCloseCallback = this.injector.get(VOICE_MODAL_CLOSE_CALLBACK, null);
|
|
1476
|
+
if (this.injectedConfig) {
|
|
1477
|
+
this.apiUrl = this.injectedConfig.apiUrl;
|
|
1478
|
+
this.token = this.injectedConfig.token;
|
|
1479
|
+
this.botId = this.injectedConfig.botId;
|
|
1480
|
+
this.conversationId = this.injectedConfig.conversationId;
|
|
1481
|
+
this.apiKey = (_a = this.injectedConfig.apiKey) !== null && _a !== void 0 ? _a : '';
|
|
1482
|
+
this.eventToken = (_b = this.injectedConfig.eventToken) !== null && _b !== void 0 ? _b : '';
|
|
1483
|
+
this.eventId = (_c = this.injectedConfig.eventId) !== null && _c !== void 0 ? _c : '';
|
|
1484
|
+
this.eventUrl = (_d = this.injectedConfig.eventUrl) !== null && _d !== void 0 ? _d : '';
|
|
1485
|
+
this.domainAuthority = (_e = this.injectedConfig.domainAuthority) !== null && _e !== void 0 ? _e : 'prod-lite';
|
|
1486
|
+
this.agentName = (_f = this.injectedConfig.agentName) !== null && _f !== void 0 ? _f : this.agentName;
|
|
1487
|
+
this.agentRole = (_g = this.injectedConfig.agentRole) !== null && _g !== void 0 ? _g : this.agentRole;
|
|
1488
|
+
this.agentAvatar = this.injectedConfig.agentAvatar;
|
|
1489
|
+
this.usersApiUrl = (_h = this.injectedConfig.usersApiUrl) !== null && _h !== void 0 ? _h : this.usersApiUrl;
|
|
1490
|
+
}
|
|
1491
|
+
// Subscribe to observables
|
|
1492
|
+
this.subscriptions.push(this.voiceAgentService.callState$.subscribe(state => {
|
|
1493
|
+
this.callState = state;
|
|
1494
|
+
this.isSpeaking = state === 'talking';
|
|
1495
|
+
if (state === 'listening' || state === 'talking') {
|
|
1496
|
+
this.hasLeftConnectedOnce = true;
|
|
1581
1497
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
this.
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
this.
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
this.
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
}));
|
|
1605
|
-
this.subscriptions.push(this.voiceAgentService.audioLevels$.subscribe(levels => {
|
|
1606
|
-
this.audioLevels = levels;
|
|
1607
|
-
}));
|
|
1608
|
-
this.voiceAgentService.resetToIdle();
|
|
1609
|
-
yield this.startCall();
|
|
1610
|
-
});
|
|
1498
|
+
if (state === 'idle' || state === 'ended') {
|
|
1499
|
+
this.hasLeftConnectedOnce = false;
|
|
1500
|
+
}
|
|
1501
|
+
}));
|
|
1502
|
+
this.subscriptions.push(this.voiceAgentService.statusText$.subscribe(text => {
|
|
1503
|
+
this.statusText = text;
|
|
1504
|
+
}));
|
|
1505
|
+
this.subscriptions.push(this.voiceAgentService.duration$.subscribe(duration => {
|
|
1506
|
+
this.duration = duration;
|
|
1507
|
+
}));
|
|
1508
|
+
this.subscriptions.push(this.voiceAgentService.isMicMuted$.subscribe(muted => {
|
|
1509
|
+
this.isMicMuted = muted;
|
|
1510
|
+
}));
|
|
1511
|
+
this.subscriptions.push(this.voiceAgentService.isUserSpeaking$.subscribe(speaking => {
|
|
1512
|
+
this.isUserSpeaking = speaking;
|
|
1513
|
+
}));
|
|
1514
|
+
this.subscriptions.push(this.voiceAgentService.audioLevels$.subscribe(levels => {
|
|
1515
|
+
this.audioLevels = levels;
|
|
1516
|
+
}));
|
|
1517
|
+
// Modal opens in idle state, then immediately starts connecting.
|
|
1518
|
+
this.voiceAgentService.resetToIdle();
|
|
1519
|
+
void this.startCall();
|
|
1611
1520
|
}
|
|
1612
1521
|
ngOnDestroy() {
|
|
1613
|
-
this.subscriptions.forEach(
|
|
1614
|
-
|
|
1615
|
-
var _a;
|
|
1616
|
-
(_a = this.warmMicStream) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((t) => t.stop());
|
|
1617
|
-
this.warmMicStream = null;
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
/** Ensures a live mic stream for this call (re-acquire after Daily stops tracks on hang-up). */
|
|
1621
|
-
ensureMicForCall() {
|
|
1622
|
-
var _a;
|
|
1623
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
1624
|
-
if (!isPlatformBrowser(this.platformId))
|
|
1625
|
-
return undefined;
|
|
1626
|
-
if ((_a = this.warmMicStream) === null || _a === void 0 ? void 0 : _a.getAudioTracks().some((t) => t.readyState === 'live')) {
|
|
1627
|
-
return this.warmMicStream;
|
|
1628
|
-
}
|
|
1629
|
-
try {
|
|
1630
|
-
this.warmMicStream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1631
|
-
return this.warmMicStream;
|
|
1632
|
-
}
|
|
1633
|
-
catch (_b) {
|
|
1634
|
-
return undefined;
|
|
1635
|
-
}
|
|
1636
|
-
});
|
|
1522
|
+
this.subscriptions.forEach(sub => sub.unsubscribe());
|
|
1523
|
+
this.disconnect();
|
|
1637
1524
|
}
|
|
1638
1525
|
startCall() {
|
|
1639
1526
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -1641,8 +1528,7 @@ class VoiceAgentModalComponent {
|
|
|
1641
1528
|
return;
|
|
1642
1529
|
this.isConnecting = true;
|
|
1643
1530
|
try {
|
|
1644
|
-
|
|
1645
|
-
yield this.voiceAgentService.connect(this.apiUrl, this.token, this.botId, this.conversationId, this.apiKey, this.eventToken, this.eventId, this.eventUrl, this.domainAuthority, this.usersApiUrl || undefined, mic);
|
|
1531
|
+
yield this.voiceAgentService.connect(this.apiUrl, this.token, this.botId, this.conversationId, this.apiKey, this.eventToken, this.eventId, this.eventUrl, this.domainAuthority, this.usersApiUrl || undefined);
|
|
1646
1532
|
}
|
|
1647
1533
|
catch (error) {
|
|
1648
1534
|
console.error('Failed to connect voice agent:', error);
|
|
@@ -1681,7 +1567,7 @@ class VoiceAgentModalComponent {
|
|
|
1681
1567
|
/** Call Again: reset to idle then start a new call. */
|
|
1682
1568
|
callAgain() {
|
|
1683
1569
|
this.voiceAgentService.resetToIdle();
|
|
1684
|
-
|
|
1570
|
+
this.startCall();
|
|
1685
1571
|
}
|
|
1686
1572
|
/** Back to Chat: close modal and disconnect. */
|
|
1687
1573
|
backToChat() {
|
|
@@ -1708,8 +1594,7 @@ VoiceAgentModalComponent.decorators = [
|
|
|
1708
1594
|
VoiceAgentModalComponent.ctorParameters = () => [
|
|
1709
1595
|
{ type: VoiceAgentService },
|
|
1710
1596
|
{ type: AudioAnalyzerService },
|
|
1711
|
-
{ type: Injector }
|
|
1712
|
-
{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }
|
|
1597
|
+
{ type: Injector }
|
|
1713
1598
|
];
|
|
1714
1599
|
VoiceAgentModalComponent.propDecorators = {
|
|
1715
1600
|
close: [{ type: Output }],
|