@hivegpt/hiveai-angular 0.0.569 → 0.0.571
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 +178 -295
- 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/chat-drawer/chat-drawer.component.js +6 -5
- package/esm2015/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.js +11 -8
- package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +55 -137
- package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +63 -104
- package/esm2015/lib/components/voice-agent/voice-modal-tokens.js +1 -1
- package/fesm2015/hivegpt-hiveai-angular.js +131 -250
- package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
- package/hivegpt-hiveai-angular.metadata.json +1 -1
- package/lib/components/chat-drawer/chat-drawer.component.d.ts.map +1 -1
- package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts +1 -0
- 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 +1 -33
- 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 +6 -17
- package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
- package/lib/components/voice-agent/voice-modal-tokens.d.ts +2 -0
- package/lib/components/voice-agent/voice-modal-tokens.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -895,83 +895,59 @@ WebSocketVoiceClientService.decorators = [
|
|
|
895
895
|
},] }
|
|
896
896
|
];
|
|
897
897
|
|
|
898
|
-
/**
|
|
899
|
-
* Daily.js WebRTC client for voice agent audio.
|
|
900
|
-
* Responsibilities:
|
|
901
|
-
* - Create and manage Daily CallObject
|
|
902
|
-
* - Join Daily room using room_url
|
|
903
|
-
* - Handle mic capture + speaker playback
|
|
904
|
-
* - Bot speaking detection via AnalyserNode on remote track (instant)
|
|
905
|
-
* - User speaking detection via active-speaker-change
|
|
906
|
-
* - Expose speaking$ (bot speaking), userSpeaking$ (user speaking), micMuted$
|
|
907
|
-
* - Expose localStream$ for waveform visualization (AudioAnalyzerService)
|
|
908
|
-
*/
|
|
909
898
|
class DailyVoiceClientService {
|
|
910
899
|
constructor(ngZone) {
|
|
911
900
|
this.ngZone = ngZone;
|
|
912
901
|
this.callObject = null;
|
|
913
902
|
this.localStream = null;
|
|
914
903
|
this.localSessionId = null;
|
|
915
|
-
/** Explicit playback of remote (bot) audio; required in some browsers. */
|
|
916
904
|
this.remoteAudioElement = null;
|
|
917
|
-
/** AnalyserNode-based remote audio monitor for instant bot speaking detection. */
|
|
918
905
|
this.remoteAudioContext = null;
|
|
919
906
|
this.remoteSpeakingRAF = null;
|
|
920
907
|
this.speakingSubject = new BehaviorSubject(false);
|
|
921
908
|
this.userSpeakingSubject = new BehaviorSubject(false);
|
|
922
|
-
this.micMutedSubject = new BehaviorSubject(
|
|
909
|
+
this.micMutedSubject = new BehaviorSubject(true); // 🔴 default muted
|
|
923
910
|
this.localStreamSubject = new BehaviorSubject(null);
|
|
924
|
-
/** True when bot (remote participant) is the active speaker. */
|
|
925
911
|
this.speaking$ = this.speakingSubject.asObservable();
|
|
926
|
-
/** True when user (local participant) is the active speaker. */
|
|
927
912
|
this.userSpeaking$ = this.userSpeakingSubject.asObservable();
|
|
928
|
-
/** True when mic is muted. */
|
|
929
913
|
this.micMuted$ = this.micMutedSubject.asObservable();
|
|
930
|
-
/** Emits local mic stream for waveform visualization. */
|
|
931
914
|
this.localStream$ = this.localStreamSubject.asObservable();
|
|
932
915
|
}
|
|
933
|
-
/**
|
|
934
|
-
* Connect to Daily room. Acquires mic first for waveform, then joins with audio.
|
|
935
|
-
* @param roomUrl Daily room URL (from room_created)
|
|
936
|
-
* @param token Optional meeting token
|
|
937
|
-
*/
|
|
938
916
|
connect(roomUrl, token) {
|
|
939
917
|
return __awaiter(this, void 0, void 0, function* () {
|
|
940
918
|
if (this.callObject) {
|
|
941
919
|
yield this.disconnect();
|
|
942
920
|
}
|
|
943
921
|
try {
|
|
944
|
-
// Get mic
|
|
922
|
+
// 🎤 Get mic (kept for waveform)
|
|
945
923
|
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
946
924
|
const audioTrack = stream.getAudioTracks()[0];
|
|
947
|
-
if (!audioTrack)
|
|
948
|
-
stream.getTracks().forEach((t) => t.stop());
|
|
925
|
+
if (!audioTrack)
|
|
949
926
|
throw new Error('No audio track');
|
|
950
|
-
}
|
|
951
927
|
this.localStream = stream;
|
|
952
928
|
this.localStreamSubject.next(stream);
|
|
953
|
-
// Create audio-only call object
|
|
954
|
-
// videoSource: false = no camera, audioSource = our mic track
|
|
955
929
|
const callObject = Daily.createCallObject({
|
|
956
930
|
videoSource: false,
|
|
957
931
|
audioSource: audioTrack,
|
|
958
932
|
});
|
|
959
933
|
this.callObject = callObject;
|
|
960
934
|
this.setupEventHandlers(callObject);
|
|
961
|
-
//
|
|
962
|
-
|
|
935
|
+
// 🔴 Ensure mic is OFF before join
|
|
936
|
+
callObject.setLocalAudio(false);
|
|
937
|
+
this.micMutedSubject.next(true);
|
|
963
938
|
const joinOptions = { url: roomUrl };
|
|
964
|
-
if (typeof token === 'string' && token.trim()
|
|
939
|
+
if (typeof token === 'string' && token.trim()) {
|
|
965
940
|
joinOptions.token = token;
|
|
966
941
|
}
|
|
967
942
|
yield callObject.join(joinOptions);
|
|
968
|
-
console.log(`[VoiceDebug]
|
|
943
|
+
console.log(`[VoiceDebug] Joined room — ${new Date().toISOString()}`);
|
|
969
944
|
const participants = callObject.participants();
|
|
970
945
|
if (participants === null || participants === void 0 ? void 0 : participants.local) {
|
|
971
946
|
this.localSessionId = participants.local.session_id;
|
|
972
947
|
}
|
|
973
|
-
//
|
|
974
|
-
|
|
948
|
+
// 🔴 Force sync again (Daily sometimes overrides)
|
|
949
|
+
callObject.setLocalAudio(false);
|
|
950
|
+
this.micMutedSubject.next(true);
|
|
975
951
|
}
|
|
976
952
|
catch (err) {
|
|
977
953
|
this.cleanup();
|
|
@@ -980,8 +956,6 @@ class DailyVoiceClientService {
|
|
|
980
956
|
});
|
|
981
957
|
}
|
|
982
958
|
setupEventHandlers(call) {
|
|
983
|
-
// active-speaker-change: used ONLY for user speaking detection.
|
|
984
|
-
// Bot speaking is detected by our own AnalyserNode (instant, no debounce).
|
|
985
959
|
call.on('active-speaker-change', (event) => {
|
|
986
960
|
this.ngZone.run(() => {
|
|
987
961
|
var _a;
|
|
@@ -990,23 +964,20 @@ class DailyVoiceClientService {
|
|
|
990
964
|
this.userSpeakingSubject.next(false);
|
|
991
965
|
return;
|
|
992
966
|
}
|
|
993
|
-
|
|
994
|
-
this.userSpeakingSubject.next(isLocal);
|
|
967
|
+
this.userSpeakingSubject.next(peerId === this.localSessionId);
|
|
995
968
|
});
|
|
996
969
|
});
|
|
997
|
-
// track-started / track-stopped: set up remote audio playback + AnalyserNode monitor.
|
|
998
970
|
call.on('track-started', (event) => {
|
|
999
971
|
this.ngZone.run(() => {
|
|
1000
|
-
var _a, _b, _c, _d;
|
|
972
|
+
var _a, _b, _c, _d, _e;
|
|
1001
973
|
const p = event === null || event === void 0 ? void 0 : event.participant;
|
|
1002
974
|
const type = (_a = event === null || event === void 0 ? void 0 : event.type) !== null && _a !== void 0 ? _a : (_b = event === null || event === void 0 ? void 0 : event.track) === null || _b === void 0 ? void 0 : _b.kind;
|
|
1003
|
-
const track = event === null || event === void 0 ? void 0 : event.track;
|
|
1004
975
|
if (p && !p.local && type === 'audio') {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
this.playRemoteTrack(
|
|
1009
|
-
this.monitorRemoteAudio(
|
|
976
|
+
const track = (_c = event.track) !== null && _c !== void 0 ? _c : (_e = (_d = p === null || p === void 0 ? void 0 : p.tracks) === null || _d === void 0 ? void 0 : _d.audio) === null || _e === void 0 ? void 0 : _e.track;
|
|
977
|
+
if (track) {
|
|
978
|
+
console.log('[VoiceDebug] Remote audio track received');
|
|
979
|
+
this.playRemoteTrack(track);
|
|
980
|
+
this.monitorRemoteAudio(track);
|
|
1010
981
|
}
|
|
1011
982
|
}
|
|
1012
983
|
});
|
|
@@ -1025,57 +996,27 @@ class DailyVoiceClientService {
|
|
|
1025
996
|
call.on('left-meeting', () => {
|
|
1026
997
|
this.ngZone.run(() => this.cleanup());
|
|
1027
998
|
});
|
|
1028
|
-
call.on('error', (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
console.error('DailyVoiceClient: Daily error', (_a = event === null || event === void 0 ? void 0 : event.errorMsg) !== null && _a !== void 0 ? _a : event);
|
|
1032
|
-
this.cleanup();
|
|
1033
|
-
});
|
|
999
|
+
call.on('error', (e) => {
|
|
1000
|
+
console.error('Daily error:', e);
|
|
1001
|
+
this.cleanup();
|
|
1034
1002
|
});
|
|
1035
1003
|
}
|
|
1036
|
-
/**
|
|
1037
|
-
* Play remote (bot) audio track via a dedicated audio element.
|
|
1038
|
-
* Required in many browsers where Daily's internal playback does not output to speakers.
|
|
1039
|
-
*/
|
|
1040
1004
|
playRemoteTrack(track) {
|
|
1041
1005
|
this.stopRemoteAudio();
|
|
1042
1006
|
try {
|
|
1043
|
-
console.log(`[VoiceDebug] playRemoteTrack called — track.readyState=${track.readyState}, track.muted=${track.muted} — ${new Date().toISOString()}`);
|
|
1044
|
-
track.onunmute = () => {
|
|
1045
|
-
console.log(`[VoiceDebug] Remote audio track UNMUTED (audio data arriving) — ${new Date().toISOString()}`);
|
|
1046
|
-
};
|
|
1047
1007
|
const stream = new MediaStream([track]);
|
|
1048
1008
|
const audio = new Audio();
|
|
1049
1009
|
audio.autoplay = true;
|
|
1050
1010
|
audio.srcObject = stream;
|
|
1051
1011
|
this.remoteAudioElement = audio;
|
|
1052
|
-
audio.
|
|
1053
|
-
console.
|
|
1054
|
-
};
|
|
1055
|
-
let firstTimeUpdate = true;
|
|
1056
|
-
audio.ontimeupdate = () => {
|
|
1057
|
-
if (firstTimeUpdate) {
|
|
1058
|
-
firstTimeUpdate = false;
|
|
1059
|
-
console.log(`[VoiceDebug] Audio element first TIMEUPDATE (actual audio output) — ${new Date().toISOString()}`);
|
|
1060
|
-
}
|
|
1061
|
-
};
|
|
1062
|
-
const p = audio.play();
|
|
1063
|
-
if (p && typeof p.then === 'function') {
|
|
1064
|
-
p.then(() => {
|
|
1065
|
-
console.log(`[VoiceDebug] audio.play() resolved — ${new Date().toISOString()}`);
|
|
1066
|
-
}).catch((err) => {
|
|
1067
|
-
console.warn('DailyVoiceClient: remote audio play failed (may need user gesture)', err);
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1012
|
+
audio.play().catch(() => {
|
|
1013
|
+
console.warn('Autoplay blocked');
|
|
1014
|
+
});
|
|
1070
1015
|
}
|
|
1071
1016
|
catch (err) {
|
|
1072
|
-
console.warn('
|
|
1017
|
+
console.warn('Audio playback error', err);
|
|
1073
1018
|
}
|
|
1074
1019
|
}
|
|
1075
|
-
/**
|
|
1076
|
-
* Monitor remote audio track energy via AnalyserNode.
|
|
1077
|
-
* Polls at ~60fps and flips speakingSubject based on actual audio energy.
|
|
1078
|
-
*/
|
|
1079
1020
|
monitorRemoteAudio(track) {
|
|
1080
1021
|
this.stopRemoteAudioMonitor();
|
|
1081
1022
|
try {
|
|
@@ -1085,104 +1026,81 @@ class DailyVoiceClientService {
|
|
|
1085
1026
|
analyser.fftSize = 256;
|
|
1086
1027
|
source.connect(analyser);
|
|
1087
1028
|
this.remoteAudioContext = ctx;
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
let isSpeaking = false;
|
|
1093
|
-
const poll = () => {
|
|
1029
|
+
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
1030
|
+
let speaking = false;
|
|
1031
|
+
let lastSound = 0;
|
|
1032
|
+
const loop = () => {
|
|
1094
1033
|
if (!this.remoteAudioContext)
|
|
1095
1034
|
return;
|
|
1096
|
-
analyser.getByteFrequencyData(
|
|
1097
|
-
|
|
1098
|
-
for (let i = 0; i < dataArray.length; i++) {
|
|
1099
|
-
sum += dataArray[i];
|
|
1100
|
-
}
|
|
1101
|
-
const avg = sum / dataArray.length;
|
|
1035
|
+
analyser.getByteFrequencyData(data);
|
|
1036
|
+
const avg = data.reduce((a, b) => a + b, 0) / data.length;
|
|
1102
1037
|
const now = Date.now();
|
|
1103
|
-
if (avg >
|
|
1104
|
-
|
|
1105
|
-
if (!
|
|
1106
|
-
|
|
1107
|
-
console.log(`[VoiceDebug] Bot audio energy detected (speaking=true) — avg=${avg.toFixed(1)} — ${new Date().toISOString()}`);
|
|
1038
|
+
if (avg > 5) {
|
|
1039
|
+
lastSound = now;
|
|
1040
|
+
if (!speaking) {
|
|
1041
|
+
speaking = true;
|
|
1108
1042
|
this.ngZone.run(() => {
|
|
1109
1043
|
this.userSpeakingSubject.next(false);
|
|
1110
1044
|
this.speakingSubject.next(true);
|
|
1111
1045
|
});
|
|
1112
1046
|
}
|
|
1113
1047
|
}
|
|
1114
|
-
else if (
|
|
1115
|
-
|
|
1116
|
-
console.log(`[VoiceDebug] Bot audio silence detected (speaking=false) — ${new Date().toISOString()}`);
|
|
1048
|
+
else if (speaking && now - lastSound > 1500) {
|
|
1049
|
+
speaking = false;
|
|
1117
1050
|
this.ngZone.run(() => this.speakingSubject.next(false));
|
|
1118
1051
|
}
|
|
1119
|
-
this.remoteSpeakingRAF = requestAnimationFrame(
|
|
1052
|
+
this.remoteSpeakingRAF = requestAnimationFrame(loop);
|
|
1120
1053
|
};
|
|
1121
|
-
this.remoteSpeakingRAF = requestAnimationFrame(
|
|
1122
|
-
}
|
|
1123
|
-
catch (err) {
|
|
1124
|
-
console.warn('DailyVoiceClient: failed to create remote audio monitor', err);
|
|
1054
|
+
this.remoteSpeakingRAF = requestAnimationFrame(loop);
|
|
1125
1055
|
}
|
|
1056
|
+
catch (_a) { }
|
|
1126
1057
|
}
|
|
1127
1058
|
stopRemoteAudioMonitor() {
|
|
1059
|
+
var _a;
|
|
1128
1060
|
if (this.remoteSpeakingRAF) {
|
|
1129
1061
|
cancelAnimationFrame(this.remoteSpeakingRAF);
|
|
1130
1062
|
this.remoteSpeakingRAF = null;
|
|
1131
1063
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
this.remoteAudioContext = null;
|
|
1135
|
-
}
|
|
1064
|
+
(_a = this.remoteAudioContext) === null || _a === void 0 ? void 0 : _a.close().catch(() => { });
|
|
1065
|
+
this.remoteAudioContext = null;
|
|
1136
1066
|
}
|
|
1137
1067
|
stopRemoteAudio() {
|
|
1138
1068
|
if (this.remoteAudioElement) {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
this.remoteAudioElement.srcObject = null;
|
|
1142
|
-
}
|
|
1143
|
-
catch (_) { }
|
|
1069
|
+
this.remoteAudioElement.pause();
|
|
1070
|
+
this.remoteAudioElement.srcObject = null;
|
|
1144
1071
|
this.remoteAudioElement = null;
|
|
1145
1072
|
}
|
|
1146
1073
|
}
|
|
1147
|
-
/** Set mic muted state. */
|
|
1148
1074
|
setMuted(muted) {
|
|
1149
1075
|
if (!this.callObject)
|
|
1150
1076
|
return;
|
|
1151
1077
|
this.callObject.setLocalAudio(!muted);
|
|
1152
1078
|
this.micMutedSubject.next(muted);
|
|
1079
|
+
console.log(`[VoiceDebug] Mic ${muted ? 'MUTED' : 'UNMUTED'}`);
|
|
1153
1080
|
}
|
|
1154
|
-
/** Disconnect and cleanup. */
|
|
1155
1081
|
disconnect() {
|
|
1156
1082
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1157
|
-
if (!this.callObject)
|
|
1158
|
-
this.cleanup();
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1083
|
+
if (!this.callObject)
|
|
1084
|
+
return this.cleanup();
|
|
1161
1085
|
try {
|
|
1162
1086
|
yield this.callObject.leave();
|
|
1163
1087
|
}
|
|
1164
|
-
catch (
|
|
1165
|
-
// ignore
|
|
1166
|
-
}
|
|
1088
|
+
catch (_a) { }
|
|
1167
1089
|
this.cleanup();
|
|
1168
1090
|
});
|
|
1169
1091
|
}
|
|
1170
1092
|
cleanup() {
|
|
1093
|
+
var _a, _b;
|
|
1171
1094
|
this.stopRemoteAudioMonitor();
|
|
1172
1095
|
this.stopRemoteAudio();
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (this.localStream) {
|
|
1178
|
-
this.localStream.getTracks().forEach((t) => t.stop());
|
|
1179
|
-
this.localStream = null;
|
|
1180
|
-
}
|
|
1096
|
+
(_a = this.callObject) === null || _a === void 0 ? void 0 : _a.destroy().catch(() => { });
|
|
1097
|
+
this.callObject = null;
|
|
1098
|
+
(_b = this.localStream) === null || _b === void 0 ? void 0 : _b.getTracks().forEach((t) => t.stop());
|
|
1099
|
+
this.localStream = null;
|
|
1181
1100
|
this.localSessionId = null;
|
|
1182
1101
|
this.speakingSubject.next(false);
|
|
1183
1102
|
this.userSpeakingSubject.next(false);
|
|
1184
1103
|
this.localStreamSubject.next(null);
|
|
1185
|
-
// Keep last micMuted state; will reset on next connect
|
|
1186
1104
|
}
|
|
1187
1105
|
}
|
|
1188
1106
|
DailyVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function DailyVoiceClientService_Factory() { return new DailyVoiceClientService(i0.ɵɵinject(i0.NgZone)); }, token: DailyVoiceClientService, providedIn: "root" });
|
|
@@ -1195,21 +1113,8 @@ DailyVoiceClientService.ctorParameters = () => [
|
|
|
1195
1113
|
{ type: NgZone }
|
|
1196
1114
|
];
|
|
1197
1115
|
|
|
1198
|
-
/**
|
|
1199
|
-
* Voice agent orchestrator. Coordinates WebSocket (signaling) and Daily.js (WebRTC audio).
|
|
1200
|
-
*
|
|
1201
|
-
* CRITICAL: This service must NEVER use Socket.IO or ngx-socket-io. Voice flow uses only:
|
|
1202
|
-
* - Native WebSocket (WebSocketVoiceClientService) for signaling (room_created, transcripts)
|
|
1203
|
-
* - Daily.js (DailyVoiceClientService) for WebRTC audio. Audio does NOT flow over WebSocket.
|
|
1204
|
-
*
|
|
1205
|
-
* - Maintains callState, statusText, duration, isMicMuted, isUserSpeaking, audioLevels
|
|
1206
|
-
* - Uses WebSocket for room_created and transcripts only (no audio)
|
|
1207
|
-
* - Uses Daily.js for all audio, mic, and real-time speaking detection
|
|
1208
|
-
*/
|
|
1209
1116
|
class VoiceAgentService {
|
|
1210
|
-
constructor(audioAnalyzer, wsClient, dailyClient, platformTokenRefresh,
|
|
1211
|
-
/** `Object` not `object` — ngc metadata collection rejects the `object` type in DI params. */
|
|
1212
|
-
platformId) {
|
|
1117
|
+
constructor(audioAnalyzer, wsClient, dailyClient, platformTokenRefresh, platformId) {
|
|
1213
1118
|
this.audioAnalyzer = audioAnalyzer;
|
|
1214
1119
|
this.wsClient = wsClient;
|
|
1215
1120
|
this.dailyClient = dailyClient;
|
|
@@ -1218,7 +1123,7 @@ class VoiceAgentService {
|
|
|
1218
1123
|
this.callStateSubject = new BehaviorSubject('idle');
|
|
1219
1124
|
this.statusTextSubject = new BehaviorSubject('');
|
|
1220
1125
|
this.durationSubject = new BehaviorSubject('00:00');
|
|
1221
|
-
this.isMicMutedSubject = new BehaviorSubject(
|
|
1126
|
+
this.isMicMutedSubject = new BehaviorSubject(true);
|
|
1222
1127
|
this.isUserSpeakingSubject = new BehaviorSubject(false);
|
|
1223
1128
|
this.audioLevelsSubject = new BehaviorSubject([]);
|
|
1224
1129
|
this.userTranscriptSubject = new Subject();
|
|
@@ -1235,7 +1140,6 @@ class VoiceAgentService {
|
|
|
1235
1140
|
this.audioLevels$ = this.audioLevelsSubject.asObservable();
|
|
1236
1141
|
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
1237
1142
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
1238
|
-
// Waveform visualization only - do NOT use for speaking state
|
|
1239
1143
|
this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe((levels) => this.audioLevelsSubject.next(levels)));
|
|
1240
1144
|
}
|
|
1241
1145
|
ngOnDestroy() {
|
|
@@ -1243,32 +1147,30 @@ class VoiceAgentService {
|
|
|
1243
1147
|
this.subscriptions.unsubscribe();
|
|
1244
1148
|
this.disconnect();
|
|
1245
1149
|
}
|
|
1246
|
-
/**
|
|
1150
|
+
/**
|
|
1151
|
+
* Tear down transports and reset UI state so a new `connect()` can run.
|
|
1152
|
+
* `connect()` only proceeds from `idle`; use this after `ended` or when reopening the modal.
|
|
1153
|
+
*/
|
|
1247
1154
|
resetToIdle() {
|
|
1248
|
-
if (this.callStateSubject.value === 'idle')
|
|
1249
|
-
return;
|
|
1250
1155
|
this.stopDurationTimer();
|
|
1251
1156
|
this.audioAnalyzer.stop();
|
|
1252
1157
|
this.wsClient.disconnect();
|
|
1253
|
-
// Fire-and-forget: Daily disconnect is async; connect() will await if needed
|
|
1254
1158
|
void this.dailyClient.disconnect();
|
|
1255
1159
|
this.callStateSubject.next('idle');
|
|
1256
1160
|
this.statusTextSubject.next('');
|
|
1257
|
-
this.durationSubject.next('
|
|
1161
|
+
this.durationSubject.next('00:00');
|
|
1162
|
+
this.isMicMutedSubject.next(true);
|
|
1163
|
+
this.isUserSpeakingSubject.next(false);
|
|
1164
|
+
this.audioLevelsSubject.next([]);
|
|
1258
1165
|
}
|
|
1259
|
-
connect(apiUrl, token, botId, conversationId, apiKey, eventToken, eventUrl, domainAuthority, usersApiUrl) {
|
|
1166
|
+
connect(apiUrl, token, botId, conversationId, apiKey, eventToken, eventId, eventUrl, domainAuthority, usersApiUrl) {
|
|
1260
1167
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1261
|
-
if (this.callStateSubject.value !== 'idle')
|
|
1262
|
-
console.warn('Call already in progress');
|
|
1168
|
+
if (this.callStateSubject.value !== 'idle')
|
|
1263
1169
|
return;
|
|
1264
|
-
}
|
|
1265
1170
|
try {
|
|
1266
1171
|
this.callStateSubject.next('connecting');
|
|
1267
|
-
this.statusTextSubject.next('Connecting...');
|
|
1172
|
+
this.statusTextSubject.next('Connecting to agent...');
|
|
1268
1173
|
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
1174
|
if (usersApiUrl && isPlatformBrowser(this.platformId)) {
|
|
1273
1175
|
try {
|
|
1274
1176
|
const ensured = yield this.platformTokenRefresh
|
|
@@ -1279,93 +1181,77 @@ class VoiceAgentService {
|
|
|
1279
1181
|
accessToken = ensured.accessToken;
|
|
1280
1182
|
}
|
|
1281
1183
|
}
|
|
1282
|
-
catch (
|
|
1283
|
-
console.warn('[HiveGpt Voice] Token refresh before connect failed', e);
|
|
1284
|
-
}
|
|
1184
|
+
catch (_a) { }
|
|
1285
1185
|
}
|
|
1286
|
-
const
|
|
1287
|
-
const postUrl =
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
Authorization: `Bearer ${accessToken}`,
|
|
1291
|
-
'domain-authority': domainAuthority,
|
|
1292
|
-
'eventtoken': eventToken,
|
|
1293
|
-
'eventurl': eventUrl,
|
|
1294
|
-
'hive-bot-id': botId,
|
|
1295
|
-
'x-api-key': apiKey,
|
|
1296
|
-
'ngrok-skip-browser-warning': 'true',
|
|
1297
|
-
};
|
|
1298
|
-
// POST to get ws_url for signaling
|
|
1186
|
+
const base = (apiUrl || '').replace(/\/+$/, '');
|
|
1187
|
+
const postUrl = `${base}/ai/ask-voice`;
|
|
1188
|
+
// Same as chat-drawer `/ai/ask` headers: use `eventId` (camelCase), value from host `this.eventId`.
|
|
1189
|
+
const eventIdHeader = (eventId && String(eventId).trim()) || '';
|
|
1299
1190
|
const res = yield fetch(postUrl, {
|
|
1300
1191
|
method: 'POST',
|
|
1301
|
-
headers
|
|
1192
|
+
headers: {
|
|
1193
|
+
'Content-Type': 'application/json',
|
|
1194
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1195
|
+
'domain-authority': domainAuthority,
|
|
1196
|
+
eventtoken: eventToken,
|
|
1197
|
+
eventurl: eventUrl,
|
|
1198
|
+
'hive-bot-id': botId,
|
|
1199
|
+
'x-api-key': apiKey,
|
|
1200
|
+
eventId: eventIdHeader,
|
|
1201
|
+
},
|
|
1302
1202
|
body: JSON.stringify({
|
|
1303
1203
|
bot_id: botId,
|
|
1304
1204
|
conversation_id: conversationId,
|
|
1305
1205
|
voice: 'alloy',
|
|
1306
1206
|
}),
|
|
1307
1207
|
});
|
|
1308
|
-
if (!res.ok) {
|
|
1309
|
-
throw new Error(`HTTP ${res.status}`);
|
|
1310
|
-
}
|
|
1311
1208
|
const json = yield res.json();
|
|
1312
1209
|
const wsUrl = json === null || json === void 0 ? void 0 : json.rn_ws_url;
|
|
1313
|
-
if (!wsUrl || typeof wsUrl !== 'string') {
|
|
1314
|
-
throw new Error('No ws_url in response');
|
|
1315
|
-
}
|
|
1316
|
-
// Subscribe to room_created BEFORE connecting to avoid race
|
|
1317
1210
|
this.wsClient.roomCreated$
|
|
1318
1211
|
.pipe(take(1), takeUntil(this.destroy$))
|
|
1319
1212
|
.subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
|
|
1320
|
-
|
|
1321
|
-
yield this.onRoomCreated(roomUrl);
|
|
1322
|
-
}
|
|
1323
|
-
catch (err) {
|
|
1324
|
-
console.error('Daily join failed:', err);
|
|
1325
|
-
this.callStateSubject.next('ended');
|
|
1326
|
-
this.statusTextSubject.next('Connection failed');
|
|
1327
|
-
yield this.disconnect();
|
|
1328
|
-
throw err;
|
|
1329
|
-
}
|
|
1213
|
+
yield this.onRoomCreated(roomUrl);
|
|
1330
1214
|
}));
|
|
1331
|
-
|
|
1332
|
-
this.subscriptions.add(this.wsClient.
|
|
1333
|
-
.pipe(takeUntil(this.destroy$))
|
|
1334
|
-
.subscribe((t) => this.userTranscriptSubject.next(t)));
|
|
1335
|
-
this.subscriptions.add(this.wsClient.botTranscript$
|
|
1336
|
-
.pipe(takeUntil(this.destroy$))
|
|
1337
|
-
.subscribe((t) => this.botTranscriptSubject.next(t)));
|
|
1338
|
-
// Connect signaling WebSocket (no audio over WS)
|
|
1215
|
+
this.subscriptions.add(this.wsClient.userTranscript$.subscribe((t) => this.userTranscriptSubject.next(t)));
|
|
1216
|
+
this.subscriptions.add(this.wsClient.botTranscript$.subscribe((t) => this.botTranscriptSubject.next(t)));
|
|
1339
1217
|
this.wsClient.connect(wsUrl);
|
|
1340
1218
|
}
|
|
1341
|
-
catch (
|
|
1342
|
-
console.error('Error connecting voice agent:', error);
|
|
1219
|
+
catch (e) {
|
|
1343
1220
|
this.callStateSubject.next('ended');
|
|
1344
|
-
yield this.disconnect();
|
|
1345
|
-
this.statusTextSubject.next('Connection failed');
|
|
1346
|
-
throw error;
|
|
1347
1221
|
}
|
|
1348
1222
|
});
|
|
1349
1223
|
}
|
|
1350
1224
|
onRoomCreated(roomUrl) {
|
|
1351
1225
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1352
|
-
// Connect Daily.js for WebRTC audio
|
|
1353
1226
|
yield this.dailyClient.connect(roomUrl);
|
|
1354
|
-
//
|
|
1355
|
-
this.dailyClient.
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1227
|
+
// 🔴 Start MUTED
|
|
1228
|
+
this.dailyClient.setMuted(true);
|
|
1229
|
+
this.isMicMutedSubject.next(true);
|
|
1230
|
+
this.statusTextSubject.next('Listening to agent...');
|
|
1231
|
+
// ✅ Enable mic on FIRST bot speech
|
|
1232
|
+
let handled = false;
|
|
1233
|
+
this.dailyClient.speaking$.pipe(filter(Boolean), take(1)).subscribe(() => {
|
|
1234
|
+
if (handled)
|
|
1235
|
+
return;
|
|
1236
|
+
handled = true;
|
|
1237
|
+
console.log('[VoiceFlow] First bot response → enabling mic');
|
|
1238
|
+
this.dailyClient.setMuted(false);
|
|
1239
|
+
this.statusTextSubject.next('You can speak now');
|
|
1359
1240
|
});
|
|
1360
|
-
|
|
1241
|
+
// ⛑️ Fallback (if bot fails)
|
|
1242
|
+
setTimeout(() => {
|
|
1243
|
+
if (!handled) {
|
|
1244
|
+
console.warn('[VoiceFlow] Fallback → enabling mic');
|
|
1245
|
+
this.dailyClient.setMuted(false);
|
|
1246
|
+
this.statusTextSubject.next('You can speak now');
|
|
1247
|
+
}
|
|
1248
|
+
}, 8000);
|
|
1249
|
+
// rest same
|
|
1361
1250
|
this.subscriptions.add(combineLatest([
|
|
1362
1251
|
this.dailyClient.speaking$,
|
|
1363
1252
|
this.dailyClient.userSpeaking$,
|
|
1364
1253
|
]).subscribe(([bot, user]) => {
|
|
1365
1254
|
const current = this.callStateSubject.value;
|
|
1366
|
-
if (current === 'connecting' && !bot) {
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
1255
|
if (current === 'connecting' && bot) {
|
|
1370
1256
|
this.callStartTime = Date.now();
|
|
1371
1257
|
this.startDurationTimer();
|
|
@@ -1378,19 +1264,16 @@ class VoiceAgentService {
|
|
|
1378
1264
|
else if (bot) {
|
|
1379
1265
|
this.callStateSubject.next('talking');
|
|
1380
1266
|
}
|
|
1381
|
-
else
|
|
1267
|
+
else {
|
|
1382
1268
|
this.callStateSubject.next('connected');
|
|
1383
1269
|
}
|
|
1384
1270
|
}));
|
|
1385
|
-
this.subscriptions.add(this.dailyClient.micMuted$.subscribe((muted) => this.isMicMutedSubject.next(muted)));
|
|
1386
|
-
this.statusTextSubject.next('Connecting...');
|
|
1387
1271
|
});
|
|
1388
1272
|
}
|
|
1389
1273
|
disconnect() {
|
|
1390
1274
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1391
1275
|
this.stopDurationTimer();
|
|
1392
1276
|
this.audioAnalyzer.stop();
|
|
1393
|
-
// Daily first, then WebSocket
|
|
1394
1277
|
yield this.dailyClient.disconnect();
|
|
1395
1278
|
this.wsClient.disconnect();
|
|
1396
1279
|
this.callStateSubject.next('ended');
|
|
@@ -1402,22 +1285,16 @@ class VoiceAgentService {
|
|
|
1402
1285
|
this.dailyClient.setMuted(!current);
|
|
1403
1286
|
}
|
|
1404
1287
|
startDurationTimer() {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
}
|
|
1412
|
-
};
|
|
1413
|
-
updateDuration();
|
|
1414
|
-
this.durationInterval = setInterval(updateDuration, 1000);
|
|
1288
|
+
this.durationInterval = setInterval(() => {
|
|
1289
|
+
const elapsed = Math.floor((Date.now() - this.callStartTime) / 1000);
|
|
1290
|
+
const m = Math.floor(elapsed / 60);
|
|
1291
|
+
const s = elapsed % 60;
|
|
1292
|
+
this.durationSubject.next(`${m}:${String(s).padStart(2, '0')}`);
|
|
1293
|
+
}, 1000);
|
|
1415
1294
|
}
|
|
1416
1295
|
stopDurationTimer() {
|
|
1417
|
-
if (this.durationInterval)
|
|
1296
|
+
if (this.durationInterval)
|
|
1418
1297
|
clearInterval(this.durationInterval);
|
|
1419
|
-
this.durationInterval = null;
|
|
1420
|
-
}
|
|
1421
1298
|
}
|
|
1422
1299
|
}
|
|
1423
1300
|
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(WebSocketVoiceClientService), i0.ɵɵinject(DailyVoiceClientService), i0.ɵɵinject(PlatformTokenRefreshService), i0.ɵɵinject(i0.PLATFORM_ID)); }, token: VoiceAgentService, providedIn: "root" });
|
|
@@ -1445,6 +1322,7 @@ class VoiceAgentModalComponent {
|
|
|
1445
1322
|
this.close = new EventEmitter();
|
|
1446
1323
|
this.apiKey = '';
|
|
1447
1324
|
this.eventToken = '';
|
|
1325
|
+
this.eventId = '';
|
|
1448
1326
|
this.eventUrl = '';
|
|
1449
1327
|
this.domainAuthority = 'prod-lite';
|
|
1450
1328
|
this.agentName = 'AI Assistant';
|
|
@@ -1467,7 +1345,7 @@ class VoiceAgentModalComponent {
|
|
|
1467
1345
|
this.isConnecting = false;
|
|
1468
1346
|
}
|
|
1469
1347
|
ngOnInit() {
|
|
1470
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
1348
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1471
1349
|
// When opened via Overlay, config is provided by injection
|
|
1472
1350
|
this.injectedConfig = this.injector.get(VOICE_MODAL_CONFIG, null);
|
|
1473
1351
|
this.onCloseCallback = this.injector.get(VOICE_MODAL_CLOSE_CALLBACK, null);
|
|
@@ -1478,12 +1356,13 @@ class VoiceAgentModalComponent {
|
|
|
1478
1356
|
this.conversationId = this.injectedConfig.conversationId;
|
|
1479
1357
|
this.apiKey = (_a = this.injectedConfig.apiKey) !== null && _a !== void 0 ? _a : '';
|
|
1480
1358
|
this.eventToken = (_b = this.injectedConfig.eventToken) !== null && _b !== void 0 ? _b : '';
|
|
1481
|
-
this.
|
|
1482
|
-
this.
|
|
1483
|
-
this.
|
|
1484
|
-
this.
|
|
1359
|
+
this.eventId = (_c = this.injectedConfig.eventId) !== null && _c !== void 0 ? _c : '';
|
|
1360
|
+
this.eventUrl = (_d = this.injectedConfig.eventUrl) !== null && _d !== void 0 ? _d : '';
|
|
1361
|
+
this.domainAuthority = (_e = this.injectedConfig.domainAuthority) !== null && _e !== void 0 ? _e : 'prod-lite';
|
|
1362
|
+
this.agentName = (_f = this.injectedConfig.agentName) !== null && _f !== void 0 ? _f : this.agentName;
|
|
1363
|
+
this.agentRole = (_g = this.injectedConfig.agentRole) !== null && _g !== void 0 ? _g : this.agentRole;
|
|
1485
1364
|
this.agentAvatar = this.injectedConfig.agentAvatar;
|
|
1486
|
-
this.usersApiUrl = (
|
|
1365
|
+
this.usersApiUrl = (_h = this.injectedConfig.usersApiUrl) !== null && _h !== void 0 ? _h : this.usersApiUrl;
|
|
1487
1366
|
}
|
|
1488
1367
|
// Subscribe to observables
|
|
1489
1368
|
this.subscriptions.push(this.voiceAgentService.callState$.subscribe(state => {
|
|
@@ -1525,7 +1404,7 @@ class VoiceAgentModalComponent {
|
|
|
1525
1404
|
return;
|
|
1526
1405
|
this.isConnecting = true;
|
|
1527
1406
|
try {
|
|
1528
|
-
yield this.voiceAgentService.connect(this.apiUrl, this.token, this.botId, this.conversationId, this.apiKey, this.eventToken, this.eventUrl, this.domainAuthority, this.usersApiUrl || undefined);
|
|
1407
|
+
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);
|
|
1529
1408
|
}
|
|
1530
1409
|
catch (error) {
|
|
1531
1410
|
console.error('Failed to connect voice agent:', error);
|
|
@@ -1601,6 +1480,7 @@ VoiceAgentModalComponent.propDecorators = {
|
|
|
1601
1480
|
conversationId: [{ type: Input }],
|
|
1602
1481
|
apiKey: [{ type: Input }],
|
|
1603
1482
|
eventToken: [{ type: Input }],
|
|
1483
|
+
eventId: [{ type: Input }],
|
|
1604
1484
|
eventUrl: [{ type: Input }],
|
|
1605
1485
|
domainAuthority: [{ type: Input }],
|
|
1606
1486
|
agentName: [{ type: Input }],
|
|
@@ -4963,7 +4843,7 @@ class ChatDrawerComponent {
|
|
|
4963
4843
|
}); // returns current time in 'shortTime' format
|
|
4964
4844
|
}
|
|
4965
4845
|
openVoiceModal() {
|
|
4966
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
4846
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
4967
4847
|
const conversationId = (_b = (_a = this.conversationKey) !== null && _a !== void 0 ? _a : this.conversationService.getKey(this.botId, false, this.eventId)) !== null && _b !== void 0 ? _b : '';
|
|
4968
4848
|
this.voiceModalConversationId = conversationId;
|
|
4969
4849
|
this.setupVoiceTranscripts();
|
|
@@ -4980,12 +4860,13 @@ class ChatDrawerComponent {
|
|
|
4980
4860
|
conversationId,
|
|
4981
4861
|
apiKey: (_g = this.apiKey) !== null && _g !== void 0 ? _g : '',
|
|
4982
4862
|
eventToken: (_h = this.eventToken) !== null && _h !== void 0 ? _h : '',
|
|
4983
|
-
|
|
4984
|
-
|
|
4863
|
+
eventId: (_j = this.eventId) !== null && _j !== void 0 ? _j : '',
|
|
4864
|
+
eventUrl: (_k = this.eventUrl) !== null && _k !== void 0 ? _k : '',
|
|
4865
|
+
domainAuthority: (_l = this.domainAuthorityValue) !== null && _l !== void 0 ? _l : 'prod-lite',
|
|
4985
4866
|
agentName: this.botName || 'AI Assistant',
|
|
4986
4867
|
agentRole: this.botSkills || 'AI Agent Specialist',
|
|
4987
4868
|
agentAvatar: this.botIcon,
|
|
4988
|
-
usersApiUrl: (
|
|
4869
|
+
usersApiUrl: (_o = (_m = this.environment) === null || _m === void 0 ? void 0 : _m.USERS_API) !== null && _o !== void 0 ? _o : '',
|
|
4989
4870
|
};
|
|
4990
4871
|
const closeCallback = () => {
|
|
4991
4872
|
if (this.voiceModalOverlayRef) {
|